diff --git a/.gitignore b/.gitignore index 7e4973a..d5f9568 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /build/ +/dist/ /**/target/ __pycache__/ *.py[cod] diff --git a/README.md b/README.md index b47154f..2b6f9a5 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,23 @@ bash scripts/build-release.sh Der Befehl führt die Rust-, Python- und Rauchtests aus, baut alle drei Programme im Release-Modus und erzeugt `build/release.json` mit Build-ID und SHA-256-Prüfsummen. +## Eigenständige Anwendung + +Für einen normalen x86-64-Linux-Rechner kann eine einzelne ausführbare Datei gebaut +werden. Sie enthält GUI und Hardware-Scanner und verwendet standardmäßig +`~/.local/share/csr-intake` für Berichte und Einstellungen: + +```bash +bash scripts/build-release.sh +bash scripts/build-standalone.sh +./dist/csr-intake-x86_64.run +``` + +Ein anderer Datenordner kann mit `--data /pfad/zum/ordner` gewählt werden. Das Paket +ist für die Linux-Version des Build-Rechners bestimmt und benötigt Python 3, SQLite, +NetworkManager sowie die jeweiligen Diagnoseprogramme. `--check-bundle` prüft die +eingebetteten Dateien, ohne die Oberfläche zu starten. + ## ISO erstellen Das Zielverzeichnis muss neu und leer sein: diff --git a/gui/src/main.rs b/gui/src/main.rs index 286ab73..23c6a5b 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -121,6 +121,18 @@ fn setup(data: &Path) -> Result<(), String> { fs::remove_file(data.join(".write-test")).map_err(|e| e.to_string())?; workflow::init_index(data) } +fn default_data_dir() -> PathBuf { + std::env::var_os("XDG_DATA_HOME") + .filter(|value| !value.is_empty()) + .map(PathBuf::from) + .or_else(|| { + std::env::var_os("HOME") + .filter(|value| !value.is_empty()) + .map(|home| PathBuf::from(home).join(".local/share")) + }) + .unwrap_or_else(|| PathBuf::from(".")) + .join("csr-intake") +} fn label(key: &str, en: bool) -> String { match key { "display" => tr(en, "Bildschirm", "Display"), @@ -1845,12 +1857,20 @@ fn main() -> eframe::Result<()> { println!("CS-R Intake {} · {BUILD}", env!("CARGO_PKG_VERSION")); return Ok(()); } + if args.iter().any(|a| a == "--help" || a == "-h") { + println!( + "CS-R Intake {}\nUsage: intake-gui [--data DIRECTORY] [--version]\nDefault data directory: {}", + env!("CARGO_PKG_VERSION"), + default_data_dir().display() + ); + return Ok(()); + } let data = args .iter() .position(|a| a == "--data") .and_then(|i| args.get(i + 1)) .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from("/mnt/intake-data")); + .unwrap_or_else(default_data_dir); eframe::run_native( "CS-R Intake", eframe::NativeOptions { diff --git a/scripts/build-standalone.sh b/scripts/build-standalone.sh new file mode 100755 index 0000000..e6e461f --- /dev/null +++ b/scripts/build-standalone.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +OUT=${1:-$ROOT/dist/csr-intake-x86_64.run} +GUI="$ROOT/gui/target/release/intake-gui" +SCANNER="$ROOT/scanner/target/release/pc-intake" +MANIFEST="$ROOT/build/release.json" + +for file in "$GUI" "$SCANNER" "$MANIFEST"; do + [[ -f $file ]] || { + echo 'Release files are missing. Run bash scripts/build-release.sh first.' >&2 + exit 2 + } +done + +BUILD_ID=$(python3 "$ROOT/scripts/release-manifest.py" --source-id) +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 +} +VERSION=$(sed -E 's/^CS-R Intake ([^ ]+).*/\1/' <<< "$GUI_VERSION") + +TMP=$(mktemp -d) +cleanup() { + find "$TMP" -depth -delete 2>/dev/null || true +} +trap cleanup EXIT + +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" +( + cd "$TMP/app" + sha256sum intake-gui pc-intake release.json > SHA256SUMS +) +tar -C "$TMP/app" -czf "$TMP/payload.tar.gz" . + +cat > "$TMP/launcher" </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 +has_data=0 +for argument in "\$@"; do + [ "\$argument" = '--data' ] && has_data=1 +done +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 + +payload_line=$(( $(wc -l < "$TMP/launcher") + 1 )) +sed -i "s/__PAYLOAD_LINE__/$payload_line/" "$TMP/launcher" +cat "$TMP/launcher" "$TMP/payload.tar.gz" > "$OUT" +chmod 755 "$OUT" +"$OUT" --check-bundle +echo "Standalone executable created: $OUT"