This commit is contained in:
@@ -3,6 +3,7 @@ set -Eeuo pipefail
|
||||
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
export CSR_BUILD_ID
|
||||
CSR_BUILD_ID=$(python3 "$ROOT/scripts/release-manifest.py" --source-id)
|
||||
cargo fmt --check --manifest-path "$ROOT/launcher/Cargo.toml"
|
||||
for component in scanner gui; do
|
||||
cargo fmt --check --manifest-path "$ROOT/$component/Cargo.toml"
|
||||
cargo clippy --locked --manifest-path "$ROOT/$component/Cargo.toml" --all-targets -- -D warnings
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -Eeuo pipefail
|
||||
|
||||
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
OUT=${1:-$ROOT/dist/csr-intake-x86_64.run}
|
||||
OUT=${1:-$ROOT/dist/csr-intake-x86_64}
|
||||
GUI="$ROOT/gui/target/release/intake-gui"
|
||||
SCANNER="$ROOT/scanner/target/release/pc-intake"
|
||||
MANIFEST="$ROOT/build/release.json"
|
||||
@@ -16,7 +16,7 @@ for file in "$GUI" "$SCANNER" "$MANIFEST"; do
|
||||
done
|
||||
|
||||
BUILD_ID=$(python3 "$ROOT/scripts/release-manifest.py" --source-id)
|
||||
GUI_VERSION=$("$GUI" --version)
|
||||
GUI_VERSION=$($GUI --version)
|
||||
grep -qF "$BUILD_ID" <<< "$GUI_VERSION" || {
|
||||
echo 'Release binaries are stale. Run bash scripts/build-release.sh first.' >&2
|
||||
exit 2
|
||||
@@ -24,152 +24,136 @@ grep -qF "$BUILD_ID" <<< "$GUI_VERSION" || {
|
||||
VERSION=$(sed -E 's/^CS-R Intake ([^ ]+).*/\1/' <<< "$GUI_VERSION")
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
cleanup() {
|
||||
find "$TMP" -depth -delete 2>/dev/null || true
|
||||
}
|
||||
cleanup() { find "$TMP" -depth -delete 2>/dev/null || true; }
|
||||
trap cleanup EXIT
|
||||
APP="$TMP/app"
|
||||
mkdir -p "$APP/bin" "$APP/etc" "$APP/lib" "$APP/python/bin" "$APP/python/lib/python/lib-dynload" "$(dirname "$OUT")"
|
||||
install -m755 "$GUI" "$APP/intake-gui"
|
||||
install -m755 "$SCANNER" "$APP/pc-intake"
|
||||
install -m644 "$MANIFEST" "$APP/release.json"
|
||||
python3 "$ROOT/scripts/release-manifest.py" --verify-release "$APP"
|
||||
|
||||
mkdir -p "$TMP/app" "$(dirname "$OUT")"
|
||||
install -m755 "$GUI" "$TMP/app/intake-gui"
|
||||
install -m755 "$SCANNER" "$TMP/app/pc-intake"
|
||||
install -m644 "$MANIFEST" "$TMP/app/release.json"
|
||||
python3 "$ROOT/scripts/release-manifest.py" --verify-release "$TMP/app"
|
||||
PYTHON=$(readlink -f "$(command -v python3)")
|
||||
PYTHON_STDLIB=$(python3 -c 'import sysconfig; print(sysconfig.get_path("stdlib"))')
|
||||
install -m755 "$PYTHON" "$APP/python/bin/python3"
|
||||
cp -a "$PYTHON_STDLIB/lib-dynload/." "$APP/python/lib/python/lib-dynload/"
|
||||
|
||||
for tool in nmcli id timeout curl sh; do
|
||||
source=$(command -v "$tool" 2>/dev/null || true)
|
||||
[[ -n $source ]] || { echo "Required portable runtime tool is missing: $tool" >&2; exit 2; }
|
||||
install -m755 "$(readlink -f "$source")" "$APP/bin/$tool"
|
||||
done
|
||||
for tool in smartctl stress-ng ffmpeg ffplay arecord aplay speaker-test lsusb xdg-open mpv lspci glmark2 memtester; do
|
||||
source=$(command -v "$tool" 2>/dev/null || true)
|
||||
[[ -z $source ]] || install -m755 "$(readlink -f "$source")" "$APP/bin/$tool"
|
||||
done
|
||||
for certificates in /etc/ssl/certs/ca-certificates.crt /etc/ca-certificates/extracted/tls-ca-bundle.pem; do
|
||||
if [[ -f $certificates ]]; then install -m644 "$certificates" "$APP/etc/ca-certificates.crt"; break; fi
|
||||
done
|
||||
|
||||
python3 - "$PYTHON_STDLIB" "$APP/python/lib/python.zip" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys, zipfile
|
||||
source = Path(sys.argv[1])
|
||||
output = Path(sys.argv[2])
|
||||
excluded = {'site-packages', '__pycache__', 'test', 'tests', 'idlelib', 'tkinter', 'turtledemo', 'ensurepip', 'venv'}
|
||||
with zipfile.ZipFile(output, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
|
||||
for path in sorted(source.rglob('*.py')):
|
||||
relative = path.relative_to(source)
|
||||
if any(part in excluded for part in relative.parts):
|
||||
continue
|
||||
archive.write(path, relative)
|
||||
PY
|
||||
|
||||
python3 - "$APP" <<'PY'
|
||||
from pathlib import Path
|
||||
import re, shutil, subprocess, sys
|
||||
app = Path(sys.argv[1])
|
||||
lib = app / 'lib'
|
||||
seeds = [app/'intake-gui', app/'pc-intake', app/'python/bin/python3', *sorted((app/'bin').iterdir()), *sorted((app/'python/lib/python/lib-dynload').glob('*.so'))]
|
||||
optional = ['libX11.so.6','libXcursor.so.1','libXi.so.6','libXrandr.so.2','libxcb.so.1','libXau.so.6','libXdmcp.so.6','libwayland-client.so.0','libxkbcommon.so.0','libEGL.so.1','libGL.so.1','libGLX.so.0','libOpenGL.so.0','libnss_files.so.2','libnss_dns.so.2','libresolv.so.2']
|
||||
cache = subprocess.run(['ldconfig','-p'], text=True, check=True, capture_output=True).stdout
|
||||
for name in optional:
|
||||
match = re.search(r'^\s*'+re.escape(name)+r'.* => (\S+)$', cache, re.M)
|
||||
if match: seeds.append(Path(match.group(1)))
|
||||
queue, seen = list(seeds), set()
|
||||
while queue:
|
||||
binary = queue.pop()
|
||||
try: key = binary.resolve()
|
||||
except OSError: continue
|
||||
if key in seen or not key.is_file(): continue
|
||||
seen.add(key)
|
||||
result = subprocess.run(['ldd', str(key)], text=True, capture_output=True)
|
||||
for line in result.stdout.splitlines():
|
||||
paths = re.findall(r'(/[^\s()]+)', line)
|
||||
for value in paths:
|
||||
dependency = Path(value)
|
||||
if dependency.exists(): queue.append(dependency)
|
||||
for source in sorted(seen):
|
||||
if source.is_relative_to(app.resolve()): continue
|
||||
destination = lib / source.name
|
||||
if not destination.exists(): shutil.copy2(source, destination)
|
||||
loader = Path('/usr/lib/ld-linux-x86-64.so.2')
|
||||
if not loader.exists(): loader = Path('/lib64/ld-linux-x86-64.so.2').resolve()
|
||||
shutil.copy2(loader, lib/'ld-linux-x86-64.so.2')
|
||||
PY
|
||||
|
||||
contains_credentials=0
|
||||
if [[ -f $BUNDLE_CONFIG/inventory-api-key && -f $BUNDLE_CONFIG/inventory-server.crt ]]; then
|
||||
key=$(tr -d '\r\n' < "$BUNDLE_CONFIG/inventory-api-key")
|
||||
[[ -n $key && $key != *[[:space:]]* ]] || {
|
||||
echo 'Bundled inventory API key is empty or malformed.' >&2
|
||||
exit 2
|
||||
}
|
||||
[[ -n $key && $key != *[[:space:]]* ]] || { echo 'Bundled API key is malformed.' >&2; exit 2; }
|
||||
openssl x509 -in "$BUNDLE_CONFIG/inventory-server.crt" -noout -checkend 0 >/dev/null || {
|
||||
echo 'Bundled inventory certificate is invalid or expired.' >&2
|
||||
exit 2
|
||||
echo 'Bundled inventory certificate is invalid or expired.' >&2; exit 2;
|
||||
}
|
||||
mkdir -m700 "$TMP/app/default-config"
|
||||
install -m600 "$BUNDLE_CONFIG/inventory-api-key" "$TMP/app/default-config/inventory-api-key"
|
||||
install -m600 "$BUNDLE_CONFIG/inventory-server.crt" "$TMP/app/default-config/inventory-server.crt"
|
||||
printf '%s\n' '{"url":"https://192.168.178.17"}' > "$TMP/app/default-config/inventory.json"
|
||||
chmod 600 "$TMP/app/default-config/inventory.json"
|
||||
mkdir -m700 "$APP/default-config"
|
||||
install -m600 "$BUNDLE_CONFIG/inventory-api-key" "$APP/default-config/inventory-api-key"
|
||||
install -m600 "$BUNDLE_CONFIG/inventory-server.crt" "$APP/default-config/inventory-server.crt"
|
||||
printf '%s\n' '{"url":"https://192.168.178.17"}' > "$APP/default-config/inventory.json"
|
||||
chmod 600 "$APP/default-config/inventory.json"
|
||||
contains_credentials=1
|
||||
elif [[ -e $BUNDLE_CONFIG/inventory-api-key || -e $BUNDLE_CONFIG/inventory-server.crt ]]; then
|
||||
echo 'Both inventory-api-key and inventory-server.crt are required for bundled configuration.' >&2
|
||||
echo 'Both inventory-api-key and inventory-server.crt are required.' >&2
|
||||
exit 2
|
||||
fi
|
||||
(
|
||||
cd "$TMP/app"
|
||||
find . -type f ! -name SHA256SUMS -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS
|
||||
)
|
||||
tar -C "$TMP/app" -czf "$TMP/payload.tar.gz" .
|
||||
|
||||
cat > "$TMP/launcher" <<EOF
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
BUILD_ID='$BUILD_ID'
|
||||
VERSION='$VERSION'
|
||||
PAYLOAD_LINE=__PAYLOAD_LINE__
|
||||
env -i \
|
||||
PYTHONHOME="$APP/python" \
|
||||
PYTHONPATH="$APP/python/lib/python.zip:$APP/python/lib/python/lib-dynload" \
|
||||
"$APP/lib/ld-linux-x86-64.so.2" --library-path "$APP/lib" "$APP/python/bin/python3" \
|
||||
-c 'import argparse, datetime, hashlib, json, pathlib, sqlite3, ssl, urllib.request, uuid; print("Bundled Python OK")'
|
||||
|
||||
cache_root=\${XDG_CACHE_HOME:-\${HOME:-\$PWD}/.cache}/csr-intake
|
||||
app_dir=\$cache_root/\$BUILD_ID
|
||||
|
||||
extract_bundle() {
|
||||
bundle_valid() {
|
||||
[ -x "\$app_dir/intake-gui" ] && [ -x "\$app_dir/pc-intake" ] &&
|
||||
(cd "\$app_dir" && sha256sum -c SHA256SUMS >/dev/null 2>&1)
|
||||
}
|
||||
if ! bundle_valid; then
|
||||
if [ -e "\$app_dir" ]; then
|
||||
find "\$app_dir" -depth -delete 2>/dev/null || true
|
||||
fi
|
||||
temp_dir=\$cache_root/.extract-\$BUILD_ID-\$\$
|
||||
mkdir -p "\$temp_dir"
|
||||
trap 'find "\$temp_dir" -depth -delete 2>/dev/null || true' EXIT HUP INT TERM
|
||||
tail -n +"\$PAYLOAD_LINE" "\$0" | gzip -dc | tar -xf - -C "\$temp_dir"
|
||||
(cd "\$temp_dir" && sha256sum -c SHA256SUMS >/dev/null)
|
||||
mkdir -p "\$cache_root"
|
||||
if ! mv -T "\$temp_dir" "\$app_dir" 2>/dev/null; then
|
||||
find "\$temp_dir" -depth -delete 2>/dev/null || true
|
||||
fi
|
||||
trap - EXIT HUP INT TERM
|
||||
fi
|
||||
bundle_valid
|
||||
}
|
||||
|
||||
case \${1:-} in
|
||||
--version)
|
||||
printf 'CS-R Intake %s · %s\n' "\$VERSION" "\$BUILD_ID"
|
||||
exit 0
|
||||
;;
|
||||
--check-bundle)
|
||||
extract_bundle
|
||||
printf 'Standalone bundle verified: %s\n' "\$BUILD_ID"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
extract_bundle
|
||||
data_dir=\${XDG_DATA_HOME:-\${HOME:-\$PWD}/.local/share}/csr-intake
|
||||
expect_data=0
|
||||
has_data=0
|
||||
for argument in "\$@"; do
|
||||
if [ "\$expect_data" -eq 1 ]; then
|
||||
data_dir=\$argument
|
||||
expect_data=0
|
||||
elif [ "\$argument" = '--data' ]; then
|
||||
has_data=1
|
||||
expect_data=1
|
||||
fi
|
||||
done
|
||||
[ "\$expect_data" -eq 0 ] || {
|
||||
echo 'Missing directory after --data.' >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
if [ -d "\$app_dir/default-config" ]; then
|
||||
config_dir=\$data_dir/config
|
||||
mkdir -p "\$config_dir"
|
||||
chmod 700 "\$config_dir"
|
||||
for name in inventory.json inventory-api-key inventory-server.crt; do
|
||||
target=\$config_dir/\$name
|
||||
if [ ! -e "\$target" ]; then
|
||||
install -m600 "\$app_dir/default-config/\$name" "\$target"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
workshop_config=\$data_dir/config/workshop.json
|
||||
if [ -f "\$workshop_config" ]; then
|
||||
keyboard_layout=\$(python3 - "\$workshop_config" <<'PY' 2>/dev/null || true
|
||||
import json
|
||||
import sys
|
||||
layout = json.load(open(sys.argv[1], encoding='utf-8')).get('keyboard_layout', '')
|
||||
if layout in ('de', 'us'):
|
||||
print(layout)
|
||||
python3 - "$APP" "$TMP/payload.gz" "$TMP/payload.manifest" <<'PY'
|
||||
from pathlib import Path
|
||||
import gzip, os, struct, sys
|
||||
root, output, manifest = map(Path, sys.argv[1:])
|
||||
files = sorted(path for path in root.rglob('*') if path.is_file())
|
||||
lines=[]
|
||||
with output.open('wb') as raw, gzip.GzipFile(fileobj=raw, mode='wb', compresslevel=9, mtime=0) as archive:
|
||||
archive.write(b'CSRPKG3\0')
|
||||
for path in files:
|
||||
name = path.relative_to(root).as_posix().encode()
|
||||
data = path.read_bytes()
|
||||
mode = path.stat().st_mode & 0o777
|
||||
archive.write(struct.pack('<IIQ', len(name), mode, len(data)))
|
||||
archive.write(name); archive.write(data)
|
||||
digest=0xcbf29ce484222325
|
||||
for byte in data: digest=((digest ^ byte) * 0x100000001b3) & 0xffffffffffffffff
|
||||
lines.append(f'{digest:016x}\t{mode:o}\t{len(data)}\t{name.decode()}')
|
||||
archive.write(struct.pack('<I', 0))
|
||||
manifest.write_text('\n'.join(lines)+'\n')
|
||||
PY
|
||||
)
|
||||
if [ -n "\$keyboard_layout" ]; then
|
||||
export XKB_DEFAULT_LAYOUT=\$keyboard_layout
|
||||
fi
|
||||
fi
|
||||
|
||||
export CSR_SCANNER="\$app_dir/pc-intake"
|
||||
if [ "\$has_data" -eq 1 ]; then
|
||||
exec "\$app_dir/intake-gui" "\$@"
|
||||
fi
|
||||
exec "\$app_dir/intake-gui" "\$@" --data "\$data_dir"
|
||||
EOF
|
||||
export CSR_BUILD_ID="$BUILD_ID" CSR_VERSION="$VERSION"
|
||||
export CSR_PAYLOAD_FILE="$TMP/payload.gz" CSR_PAYLOAD_MANIFEST="$TMP/payload.manifest"
|
||||
export RUSTFLAGS="${RUSTFLAGS:-} -C target-feature=+crt-static"
|
||||
cargo clippy --locked --manifest-path "$ROOT/launcher/Cargo.toml" --all-targets -- -D warnings
|
||||
cargo build --locked --release --manifest-path "$ROOT/launcher/Cargo.toml"
|
||||
install -m$([[ $contains_credentials -eq 1 ]] && echo 700 || echo 755) \
|
||||
"$ROOT/launcher/target/release/csr-intake-launcher" "$OUT"
|
||||
|
||||
payload_line=$(( $(wc -l < "$TMP/launcher") + 1 ))
|
||||
sed -i "s/__PAYLOAD_LINE__/$payload_line/" "$TMP/launcher"
|
||||
cat "$TMP/launcher" "$TMP/payload.tar.gz" > "$OUT"
|
||||
if [[ $contains_credentials -eq 1 ]]; then
|
||||
chmod 700 "$OUT"
|
||||
else
|
||||
chmod 755 "$OUT"
|
||||
file "$OUT" | grep -q 'static-pie linked' || { echo 'Launcher is not statically linked.' >&2; exit 2; }
|
||||
if ldd "$OUT" 2>&1 | grep -qv 'statically linked\|not a dynamic executable'; then
|
||||
echo 'Launcher unexpectedly depends on host libraries.' >&2; exit 2
|
||||
fi
|
||||
"$OUT" --check-bundle
|
||||
if [[ $contains_credentials -eq 1 ]]; then
|
||||
echo "Standalone executable created with inventory configuration: $OUT"
|
||||
else
|
||||
echo "Standalone executable created: $OUT"
|
||||
fi
|
||||
echo "Standalone ELF created: $OUT"
|
||||
|
||||
@@ -19,6 +19,9 @@ def source_id():
|
||||
base = ROOT / component
|
||||
paths.extend((base / 'src').rglob('*.rs'))
|
||||
paths.extend(p for p in [base / 'Cargo.toml', base / 'Cargo.lock', base / 'build.rs'] if p.exists())
|
||||
launcher = ROOT / 'launcher'
|
||||
paths.extend((launcher / 'src').rglob('*.rs'))
|
||||
paths.extend(p for p in [launcher / 'Cargo.toml', launcher / 'Cargo.lock'] if p.exists())
|
||||
paths.extend(p for p in (ROOT / 'scripts').iterdir() if p.is_file())
|
||||
h = hashlib.sha256()
|
||||
for path in sorted(paths):
|
||||
|
||||
Reference in New Issue
Block a user