This commit is contained in:
@@ -12,7 +12,7 @@ jobs:
|
|||||||
- name: Systempakete installieren
|
- name: Systempakete installieren
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y pkg-config libx11-dev libxkbcommon-dev libwayland-dev libgl1-mesa-dev
|
sudo apt-get install -y pkg-config libx11-dev libxkbcommon-dev libwayland-dev libgl1-mesa-dev network-manager curl
|
||||||
- name: Rust installieren
|
- name: Rust installieren
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
@@ -21,10 +21,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
bash -n scripts/*.sh tests/*.sh
|
bash -n scripts/*.sh tests/*.sh
|
||||||
bash scripts/build-release.sh
|
bash scripts/build-release.sh
|
||||||
./dist/csr-intake-x86_64.run --check-bundle
|
./dist/csr-intake-x86_64 --check-bundle
|
||||||
|
file dist/csr-intake-x86_64 | grep -q 'static-pie linked'
|
||||||
|
env -i HOME="$RUNNER_TEMP" XDG_CACHE_HOME="$RUNNER_TEMP/cache" PATH=/nonexistent ./dist/csr-intake-x86_64 --check-bundle
|
||||||
- name: Eigenständige Anwendung bereitstellen
|
- name: Eigenständige Anwendung bereitstellen
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: csr-intake-x86_64
|
name: csr-intake-x86_64
|
||||||
path: dist/csr-intake-x86_64.run
|
path: dist/csr-intake-x86_64
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|||||||
40
README.md
40
README.md
@@ -7,7 +7,7 @@ auch ohne Netzwerk lokal.
|
|||||||
|
|
||||||
## Ablauf in der Werkstatt
|
## Ablauf in der Werkstatt
|
||||||
|
|
||||||
1. `csr-intake-x86_64.run` auf dem zu prüfenden Linux-Rechner starten.
|
1. `csr-intake-x86_64` auf dem zu prüfenden Linux-Rechner starten.
|
||||||
2. Beim ersten Einsatz unter **Einstellungen** das Werkstatt-WLAN verbinden und
|
2. Beim ersten Einsatz unter **Einstellungen** das Werkstatt-WLAN verbinden und
|
||||||
**WLAN speichern** aktiviert lassen.
|
**WLAN speichern** aktiviert lassen.
|
||||||
3. Das Gerät wird über DMI-UUID oder Hersteller und Seriennummer erkannt.
|
3. Das Gerät wird über DMI-UUID oder Hersteller und Seriennummer erkannt.
|
||||||
@@ -39,27 +39,32 @@ Reparaturen, Berichte und die Synchronisationswarteschlange sind über das Menü
|
|||||||
|
|
||||||
## Anwendung starten
|
## Anwendung starten
|
||||||
|
|
||||||
Die Release-Datei enthält Oberfläche und Hardware-Scanner. Sie entpackt geprüfte
|
Die Release-Datei ist ein statisch gelinktes Linux-ELF. Sie enthält Oberfläche,
|
||||||
Programmdateien in den Benutzer-Cache und speichert Einstellungen, Berichte und die
|
Hardware-Scanner, Python-Laufzeit, TLS, SQLite und alle unmittelbar benötigten
|
||||||
Offline-Warteschlange standardmäßig unter `~/.local/share/csr-intake`.
|
Laufzeitbibliotheken. Sie benötigt weder eine installierte Python-Version noch
|
||||||
|
Shell-, Archiv- oder glibc-Werkzeuge. Geprüfte Programmdateien werden in den
|
||||||
|
Benutzer-Cache entpackt. Einstellungen, Berichte und die Offline-Warteschlange
|
||||||
|
liegen standardmäßig unter `~/.local/share/csr-intake`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
chmod +x csr-intake-x86_64.run
|
chmod +x csr-intake-x86_64
|
||||||
./csr-intake-x86_64.run
|
./csr-intake-x86_64
|
||||||
```
|
```
|
||||||
|
|
||||||
Ein anderer Datenordner kann mit `--data /pfad/zum/ordner` gewählt werden:
|
Ein anderer Datenordner kann mit `--data /pfad/zum/ordner` gewählt werden:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./csr-intake-x86_64.run --data "$HOME/csr-intake-daten"
|
./csr-intake-x86_64 --data "$HOME/csr-intake-daten"
|
||||||
./csr-intake-x86_64.run --version
|
./csr-intake-x86_64 --version
|
||||||
./csr-intake-x86_64.run --check-bundle
|
./csr-intake-x86_64 --check-bundle
|
||||||
```
|
```
|
||||||
|
|
||||||
Die Anwendung ist für x86-64 Linux gebaut. Sie benötigt eine grafische Sitzung,
|
Die Anwendung ist für x86-64 Linux ab Kernel 4.4 gebaut. Clientprogramme für WLAN,
|
||||||
Python 3, SQLite und NetworkManager. Einzelne Diagnosen verwenden Systemwerkzeuge
|
HTTP, Audio, Video, SMART und USB werden in den Build übernommen, sofern sie auf dem
|
||||||
wie `smartctl`, `stress-ng`, `ffmpeg`, `arecord`, `lsusb` und `xdg-open`. Fehlt ein
|
Build-Rechner verfügbar sind. Benötigt werden nur eine grafische Linux-Sitzung, der
|
||||||
Werkzeug, wird der Test als **nicht verfügbar** und nicht als Hardwarefehler bewertet.
|
Kernel und die Systemdienste für die tatsächlich verwendete Hardware, insbesondere
|
||||||
|
der NetworkManager-Dienst für WLAN. Ein im Build nicht vorhandenes Diagnosewerkzeug
|
||||||
|
macht nur den betreffenden Test **nicht verfügbar**.
|
||||||
|
|
||||||
## Konfiguration
|
## Konfiguration
|
||||||
|
|
||||||
@@ -85,13 +90,13 @@ aus und erzeugt anschließend genau ein verteilbares Artefakt:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash scripts/build-release.sh
|
bash scripts/build-release.sh
|
||||||
./dist/csr-intake-x86_64.run --check-bundle
|
./dist/csr-intake-x86_64 --check-bundle
|
||||||
```
|
```
|
||||||
|
|
||||||
Das Ergebnis liegt in `dist/csr-intake-x86_64.run`. `build/release.json` enthält
|
Das Ergebnis liegt in `dist/csr-intake-x86_64`. `build/release.json` enthält
|
||||||
Build-ID, Versionen und SHA-256-Prüfsummen der eingebetteten Programme. Die CI stellt
|
Build-ID, Versionen und SHA-256-Prüfsummen der eingebetteten Programme. Die CI stellt
|
||||||
bei jedem erfolgreichen Build eine neutrale `.run`-Datei ohne Zugangsdaten als
|
bei jedem erfolgreichen Build eine neutrale ELF-Datei ohne Zugangsdaten als Artefakt
|
||||||
Artefakt bereit.
|
bereit.
|
||||||
|
|
||||||
Für eine werkstattgebundene Datei können `inventory-api-key` und
|
Für eine werkstattgebundene Datei können `inventory-api-key` und
|
||||||
`inventory-server.crt` vor dem Build in `.bundle-secrets/` abgelegt werden. Der Build
|
`inventory-server.crt` vor dem Build in `.bundle-secrets/` abgelegt werden. Der Build
|
||||||
@@ -106,6 +111,7 @@ einen lesbaren Zugangsschlüssel und darf nur wie ein Passwort weitergegeben wer
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `gui/` | Bedienoberfläche und geführte Tests |
|
| `gui/` | Bedienoberfläche und geführte Tests |
|
||||||
| `scanner/` | Hardwareerkennung und Messwerte |
|
| `scanner/` | Hardwareerkennung und Messwerte |
|
||||||
|
| `launcher/` | Statischer ELF-Starter und eingebettete Laufzeit |
|
||||||
| `scripts/` | Build, Synchronisation und lokale Datenwerkzeuge |
|
| `scripts/` | Build, Synchronisation und lokale Datenwerkzeuge |
|
||||||
| `tests/` | Python- und Rauchtests |
|
| `tests/` | Python- und Rauchtests |
|
||||||
| `config.example/` | Beispiel für die Inventarverbindung |
|
| `config.example/` | Beispiel für die Inventarverbindung |
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{path::Path, process::Command};
|
use std::{path::Path, process::Command};
|
||||||
const BRIDGE: &str = include_str!("../../scripts/inventory-sync.py");
|
const BRIDGE: &str = include_str!("../../scripts/inventory-sync.py");
|
||||||
|
|
||||||
|
fn python() -> Command {
|
||||||
|
if let Some(launcher) = std::env::var_os("CSR_RUNTIME_LAUNCHER") {
|
||||||
|
let mut command = Command::new(launcher);
|
||||||
|
command.arg("--internal-python");
|
||||||
|
command
|
||||||
|
} else {
|
||||||
|
Command::new("python3")
|
||||||
|
}
|
||||||
|
}
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct QueueEntry {
|
pub struct QueueEntry {
|
||||||
pub asset_id: String,
|
pub asset_id: String,
|
||||||
@@ -12,7 +22,7 @@ pub struct QueueEntry {
|
|||||||
pub report_path: String,
|
pub report_path: String,
|
||||||
}
|
}
|
||||||
fn call(data: &Path, args: &[&str]) -> Result<String, String> {
|
fn call(data: &Path, args: &[&str]) -> Result<String, String> {
|
||||||
let out = Command::new("python3")
|
let out = python()
|
||||||
.args(["-c", BRIDGE, "--data"])
|
.args(["-c", BRIDGE, "--data"])
|
||||||
.arg(data)
|
.arg(data)
|
||||||
.args(args)
|
.args(args)
|
||||||
@@ -48,7 +58,7 @@ pub struct HardwareIdentity {
|
|||||||
pub fn identify(identity: &crate::workflow::Identity) -> Result<HardwareIdentity, String> {
|
pub fn identify(identity: &crate::workflow::Identity) -> Result<HardwareIdentity, String> {
|
||||||
let report = serde_json::json!({"system_uuid": identity.uuid, "serial_number": identity.serial,
|
let report = serde_json::json!({"system_uuid": identity.uuid, "serial_number": identity.serial,
|
||||||
"manufacturer": identity.manufacturer});
|
"manufacturer": identity.manufacturer});
|
||||||
let output = Command::new("python3")
|
let output = python()
|
||||||
.args(["-c", BRIDGE, "--identity-json", &report.to_string()])
|
.args(["-c", BRIDGE, "--identity-json", &report.to_string()])
|
||||||
.output()
|
.output()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|||||||
@@ -86,24 +86,29 @@ struct App {
|
|||||||
station_ready: bool,
|
station_ready: bool,
|
||||||
next_station_attempt: Instant,
|
next_station_attempt: Instant,
|
||||||
}
|
}
|
||||||
fn scanner_path() -> PathBuf {
|
fn scanner_command() -> Command {
|
||||||
|
if let Some(launcher) = std::env::var_os("CSR_RUNTIME_LAUNCHER") {
|
||||||
|
let mut command = Command::new(launcher);
|
||||||
|
command.arg("--internal-scanner");
|
||||||
|
return command;
|
||||||
|
}
|
||||||
if let Some(p) = std::env::var_os("CSR_SCANNER") {
|
if let Some(p) = std::env::var_os("CSR_SCANNER") {
|
||||||
return PathBuf::from(p);
|
return Command::new(p);
|
||||||
}
|
}
|
||||||
// Prefer the scanner bundled beside this executable, then a system installation.
|
// Prefer the scanner bundled beside this executable, then a system installation.
|
||||||
if let Ok(exe) = std::env::current_exe() {
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
if let Some(parent) = exe.parent() {
|
if let Some(parent) = exe.parent() {
|
||||||
let sibling = parent.join("pc-intake");
|
let sibling = parent.join("pc-intake");
|
||||||
if sibling.exists() {
|
if sibling.exists() {
|
||||||
return sibling;
|
return Command::new(sibling);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let local = Path::new(env!("CARGO_MANIFEST_DIR")).join("../scanner/target/release/pc-intake");
|
let local = Path::new(env!("CARGO_MANIFEST_DIR")).join("../scanner/target/release/pc-intake");
|
||||||
if local.exists() {
|
if local.exists() {
|
||||||
local
|
Command::new(local)
|
||||||
} else {
|
} else {
|
||||||
PathBuf::from("/usr/local/bin/pc-intake")
|
Command::new("/usr/local/bin/pc-intake")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn setup(data: &Path) -> Result<(), String> {
|
fn setup(data: &Path) -> Result<(), String> {
|
||||||
@@ -337,20 +342,17 @@ impl App {
|
|||||||
}
|
}
|
||||||
let data = self.data.clone();
|
let data = self.data.clone();
|
||||||
let asset = self.inspection.as_ref().unwrap().asset.clone();
|
let asset = self.inspection.as_ref().unwrap().asset.clone();
|
||||||
let scanner = scanner_path();
|
let mut scanner = scanner_command();
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
self.receiver = Some(rx);
|
self.receiver = Some(rx);
|
||||||
self.status = "Hardware scan running / Hardware-Scan läuft …".into();
|
self.status = "Hardware scan running / Hardware-Scan läuft …".into();
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let result = (|| {
|
let result = (|| {
|
||||||
let mut command = Command::new("timeout");
|
scanner
|
||||||
command
|
|
||||||
.args(["--kill-after=5s", "600s"])
|
|
||||||
.arg(scanner)
|
|
||||||
.args(["--asset-id", &asset, "--output"])
|
.args(["--asset-id", &asset, "--output"])
|
||||||
.arg(data.join("reports"));
|
.arg(data.join("reports"));
|
||||||
command.arg(if full { "--compile" } else { "--quick" });
|
scanner.arg(if full { "--compile" } else { "--quick" });
|
||||||
let out = command.output().map_err(|e| format!("Scanner: {e}"))?;
|
let out = scanner.output().map_err(|e| format!("Scanner: {e}"))?;
|
||||||
if !out.status.success() {
|
if !out.status.success() {
|
||||||
return Err(String::from_utf8_lossy(&out.stderr).into_owned());
|
return Err(String::from_utf8_lossy(&out.stderr).into_owned());
|
||||||
}
|
}
|
||||||
|
|||||||
64
launcher/Cargo.lock
generated
Normal file
64
launcher/Cargo.lock
generated
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc32fast"
|
||||||
|
version = "1.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "csr-intake-launcher"
|
||||||
|
version = "2.6.0"
|
||||||
|
dependencies = [
|
||||||
|
"flate2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"miniz_oxide",
|
||||||
|
"zlib-rs",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd-adler32"
|
||||||
|
version = "0.3.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zlib-rs"
|
||||||
|
version = "0.6.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
|
||||||
13
launcher/Cargo.toml
Normal file
13
launcher/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "csr-intake-launcher"
|
||||||
|
version = "2.6.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
flate2 = "1"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
panic = "abort"
|
||||||
330
launcher/src/main.rs
Normal file
330
launcher/src/main.rs
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
use flate2::read::GzDecoder;
|
||||||
|
use std::env;
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::{self, BufReader, Read, Write};
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use std::os::unix::process::CommandExt;
|
||||||
|
use std::path::{Component, Path, PathBuf};
|
||||||
|
use std::process::{self, Command};
|
||||||
|
|
||||||
|
const BUILD_ID: &str = env!("CSR_BUILD_ID");
|
||||||
|
const VERSION: &str = env!("CSR_VERSION");
|
||||||
|
const PAYLOAD: &[u8] = include_bytes!(env!("CSR_PAYLOAD_FILE"));
|
||||||
|
const MANIFEST: &str = include_str!(env!("CSR_PAYLOAD_MANIFEST"));
|
||||||
|
const MAGIC: &[u8; 8] = b"CSRPKG3\0";
|
||||||
|
|
||||||
|
fn cache_dir() -> PathBuf {
|
||||||
|
env::var_os("XDG_CACHE_HOME")
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.or_else(|| env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache")))
|
||||||
|
.unwrap_or_else(|| env::temp_dir().join(format!("csr-intake-{}", process::id())))
|
||||||
|
.join("csr-intake")
|
||||||
|
.join(BUILD_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_data_dir() -> PathBuf {
|
||||||
|
env::var_os("XDG_DATA_HOME")
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.or_else(|| env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/share")))
|
||||||
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
|
.join("csr-intake")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fnv(path: &Path) -> io::Result<u64> {
|
||||||
|
let mut input = BufReader::new(File::open(path)?);
|
||||||
|
let mut hash = 0xcbf29ce484222325u64;
|
||||||
|
let mut buf = [0u8; 65536];
|
||||||
|
loop {
|
||||||
|
let count = input.read(&mut buf)?;
|
||||||
|
if count == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for byte in &buf[..count] {
|
||||||
|
hash = (hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid(root: &Path) -> bool {
|
||||||
|
MANIFEST.lines().all(|line| {
|
||||||
|
let mut fields = line.splitn(4, '\t');
|
||||||
|
let expected_hash = fields.next().and_then(|v| u64::from_str_radix(v, 16).ok());
|
||||||
|
let expected_mode = fields.next().and_then(|v| u32::from_str_radix(v, 8).ok());
|
||||||
|
let expected_size = fields.next().and_then(|v| v.parse::<u64>().ok());
|
||||||
|
let name = fields.next();
|
||||||
|
let (Some(hash), Some(mode), Some(size), Some(name)) =
|
||||||
|
(expected_hash, expected_mode, expected_size, name)
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let path = root.join(name);
|
||||||
|
let Ok(metadata) = fs::metadata(&path) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
metadata.is_file()
|
||||||
|
&& metadata.len() == size
|
||||||
|
&& metadata.permissions().mode() & 0o777 == mode
|
||||||
|
&& fnv(&path).ok() == Some(hash)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u32(reader: &mut impl Read) -> io::Result<u32> {
|
||||||
|
let mut bytes = [0u8; 4];
|
||||||
|
reader.read_exact(&mut bytes)?;
|
||||||
|
Ok(u32::from_le_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u64(reader: &mut impl Read) -> io::Result<u64> {
|
||||||
|
let mut bytes = [0u8; 8];
|
||||||
|
reader.read_exact(&mut bytes)?;
|
||||||
|
Ok(u64::from_le_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn safe_relative(value: &str) -> io::Result<&Path> {
|
||||||
|
let path = Path::new(value);
|
||||||
|
if value.is_empty()
|
||||||
|
|| path
|
||||||
|
.components()
|
||||||
|
.any(|c| !matches!(c, Component::Normal(_)))
|
||||||
|
{
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"unsafe bundle path",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unpack(destination: &Path) -> io::Result<()> {
|
||||||
|
let mut reader = GzDecoder::new(PAYLOAD);
|
||||||
|
let mut magic = [0u8; 8];
|
||||||
|
reader.read_exact(&mut magic)?;
|
||||||
|
if &magic != MAGIC {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"invalid bundle header",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
loop {
|
||||||
|
let name_len = read_u32(&mut reader)?;
|
||||||
|
if name_len == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if name_len > 4096 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"bundle path too long",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mode = read_u32(&mut reader)?;
|
||||||
|
let size = read_u64(&mut reader)?;
|
||||||
|
let mut name = vec![0u8; name_len as usize];
|
||||||
|
reader.read_exact(&mut name)?;
|
||||||
|
let name = std::str::from_utf8(&name)
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "non-UTF-8 bundle path"))?;
|
||||||
|
let path = destination.join(safe_relative(name)?);
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let mut output = File::create(&path)?;
|
||||||
|
io::copy(&mut reader.by_ref().take(size), &mut output)?;
|
||||||
|
output.flush()?;
|
||||||
|
fs::set_permissions(&path, fs::Permissions::from_mode(mode & 0o777))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_bundle() -> io::Result<PathBuf> {
|
||||||
|
let app = cache_dir();
|
||||||
|
if !valid(&app) {
|
||||||
|
if app.exists() {
|
||||||
|
fs::remove_dir_all(&app)?;
|
||||||
|
}
|
||||||
|
let parent = app.parent().unwrap();
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
let temp = parent.join(format!(".extract-{BUILD_ID}-{}", process::id()));
|
||||||
|
if temp.exists() {
|
||||||
|
fs::remove_dir_all(&temp)?;
|
||||||
|
}
|
||||||
|
fs::create_dir(&temp)?;
|
||||||
|
if let Err(error) = unpack(&temp).and_then(|_| {
|
||||||
|
if valid(&temp) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"bundle checksum mismatch",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
let _ = fs::remove_dir_all(&temp);
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
if let Err(error) = fs::rename(&temp, &app) {
|
||||||
|
if !valid(&app) {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
let _ = fs::remove_dir_all(&temp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let runtime = app.join("runtime-launcher");
|
||||||
|
let current = env::current_exe()?;
|
||||||
|
let replace = fs::metadata(&runtime).map(|m| m.len()).ok()
|
||||||
|
!= fs::metadata(¤t).map(|m| m.len()).ok();
|
||||||
|
if replace {
|
||||||
|
let _ = fs::remove_file(&runtime);
|
||||||
|
if fs::hard_link(¤t, &runtime).is_err() {
|
||||||
|
fs::copy(¤t, &runtime)?;
|
||||||
|
}
|
||||||
|
fs::set_permissions(&runtime, fs::Permissions::from_mode(0o700))?;
|
||||||
|
}
|
||||||
|
Ok(app)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn loader_command(app: &Path, program: &Path) -> Command {
|
||||||
|
let libraries = app.join("lib");
|
||||||
|
let mut command = Command::new(app.join("lib/ld-linux-x86-64.so.2"));
|
||||||
|
command.arg("--library-path").arg(&libraries).arg(program);
|
||||||
|
command.env("LD_LIBRARY_PATH", &libraries);
|
||||||
|
command.env(
|
||||||
|
"PATH",
|
||||||
|
format!(
|
||||||
|
"{}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||||
|
app.join("bin").display()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let certificates = app.join("etc/ca-certificates.crt");
|
||||||
|
if certificates.exists() {
|
||||||
|
command.env("SSL_CERT_FILE", &certificates);
|
||||||
|
command.env("CURL_CA_BUNDLE", &certificates);
|
||||||
|
}
|
||||||
|
command
|
||||||
|
}
|
||||||
|
|
||||||
|
fn internal(app: &Path, kind: &str, args: impl Iterator<Item = String>) -> ! {
|
||||||
|
let mut command = if kind == "python" {
|
||||||
|
let mut command = loader_command(app, &app.join("python/bin/python3"));
|
||||||
|
command.env("PYTHONHOME", app.join("python"));
|
||||||
|
command.env(
|
||||||
|
"PYTHONPATH",
|
||||||
|
format!(
|
||||||
|
"{}:{}",
|
||||||
|
app.join("python/lib/python.zip").display(),
|
||||||
|
app.join("python/lib/python/lib-dynload").display()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
command
|
||||||
|
} else {
|
||||||
|
loader_command(app, &app.join("pc-intake"))
|
||||||
|
};
|
||||||
|
command.args(args);
|
||||||
|
let error = command.exec();
|
||||||
|
eprintln!("Could not start bundled {kind}: {error}");
|
||||||
|
process::exit(126)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn seed_config(app: &Path, data: &Path) -> io::Result<()> {
|
||||||
|
let source = app.join("default-config");
|
||||||
|
if !source.is_dir() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let target = data.join("config");
|
||||||
|
fs::create_dir_all(&target)?;
|
||||||
|
fs::set_permissions(&target, fs::Permissions::from_mode(0o700))?;
|
||||||
|
for name in [
|
||||||
|
"inventory.json",
|
||||||
|
"inventory-api-key",
|
||||||
|
"inventory-server.crt",
|
||||||
|
] {
|
||||||
|
let destination = target.join(name);
|
||||||
|
if !destination.exists() {
|
||||||
|
fs::copy(source.join(name), &destination)?;
|
||||||
|
fs::set_permissions(destination, fs::Permissions::from_mode(0o600))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keyboard_layout(data: &Path) -> Option<String> {
|
||||||
|
let text = fs::read_to_string(data.join("config/workshop.json")).ok()?;
|
||||||
|
let rest = text
|
||||||
|
.split_once("\"keyboard_layout\"")?
|
||||||
|
.1
|
||||||
|
.split_once(':')?
|
||||||
|
.1
|
||||||
|
.trim_start();
|
||||||
|
let value = rest.strip_prefix('"')?.split_once('"')?.0;
|
||||||
|
matches!(value, "de" | "us").then(|| value.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fail(context: &str, error: impl std::fmt::Display) -> ! {
|
||||||
|
eprintln!("CS-R Intake: {context}: {error}");
|
||||||
|
process::exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut args = env::args().skip(1);
|
||||||
|
let first = args.next();
|
||||||
|
if first.as_deref() == Some("--version") {
|
||||||
|
println!("CS-R Intake {VERSION} · {BUILD_ID}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if matches!(
|
||||||
|
first.as_deref(),
|
||||||
|
Some("--internal-python" | "--internal-scanner")
|
||||||
|
) {
|
||||||
|
let app = env::current_exe()
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| p.parent().map(Path::to_owned))
|
||||||
|
.unwrap_or_else(|| fail("runtime location", "unavailable"));
|
||||||
|
internal(
|
||||||
|
&app,
|
||||||
|
if first.as_deref() == Some("--internal-python") {
|
||||||
|
"python"
|
||||||
|
} else {
|
||||||
|
"scanner"
|
||||||
|
},
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let app = ensure_bundle().unwrap_or_else(|e| fail("bundle extraction failed", e));
|
||||||
|
if first.as_deref() == Some("--check-bundle") {
|
||||||
|
println!("Standalone ELF verified: {BUILD_ID}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut gui_args = Vec::new();
|
||||||
|
if let Some(value) = first {
|
||||||
|
gui_args.push(value);
|
||||||
|
}
|
||||||
|
gui_args.extend(args);
|
||||||
|
let mut data = default_data_dir();
|
||||||
|
let mut has_data = false;
|
||||||
|
let mut index = 0;
|
||||||
|
while index < gui_args.len() {
|
||||||
|
if gui_args[index] == "--data" {
|
||||||
|
has_data = true;
|
||||||
|
data = gui_args
|
||||||
|
.get(index + 1)
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| fail("argument error", "missing directory after --data"));
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
seed_config(&app, &data).unwrap_or_else(|e| fail("could not install configuration", e));
|
||||||
|
if !has_data {
|
||||||
|
gui_args.extend(["--data".into(), data.to_string_lossy().into_owned()]);
|
||||||
|
}
|
||||||
|
if let Some(layout) = keyboard_layout(&data) {
|
||||||
|
env::set_var("XKB_DEFAULT_LAYOUT", layout);
|
||||||
|
}
|
||||||
|
env::set_var("CSR_RUNTIME_LAUNCHER", app.join("runtime-launcher"));
|
||||||
|
env::set_var("CSR_SCANNER", app.join("pc-intake"));
|
||||||
|
let mut command = loader_command(&app, &app.join("intake-gui"));
|
||||||
|
command.args(gui_args);
|
||||||
|
let error = command.exec();
|
||||||
|
fail("could not start GUI", error)
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ set -Eeuo pipefail
|
|||||||
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||||
export CSR_BUILD_ID
|
export CSR_BUILD_ID
|
||||||
CSR_BUILD_ID=$(python3 "$ROOT/scripts/release-manifest.py" --source-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
|
for component in scanner gui; do
|
||||||
cargo fmt --check --manifest-path "$ROOT/$component/Cargo.toml"
|
cargo fmt --check --manifest-path "$ROOT/$component/Cargo.toml"
|
||||||
cargo clippy --locked --manifest-path "$ROOT/$component/Cargo.toml" --all-targets -- -D warnings
|
cargo clippy --locked --manifest-path "$ROOT/$component/Cargo.toml" --all-targets -- -D warnings
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
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"
|
GUI="$ROOT/gui/target/release/intake-gui"
|
||||||
SCANNER="$ROOT/scanner/target/release/pc-intake"
|
SCANNER="$ROOT/scanner/target/release/pc-intake"
|
||||||
MANIFEST="$ROOT/build/release.json"
|
MANIFEST="$ROOT/build/release.json"
|
||||||
@@ -16,7 +16,7 @@ for file in "$GUI" "$SCANNER" "$MANIFEST"; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
BUILD_ID=$(python3 "$ROOT/scripts/release-manifest.py" --source-id)
|
BUILD_ID=$(python3 "$ROOT/scripts/release-manifest.py" --source-id)
|
||||||
GUI_VERSION=$("$GUI" --version)
|
GUI_VERSION=$($GUI --version)
|
||||||
grep -qF "$BUILD_ID" <<< "$GUI_VERSION" || {
|
grep -qF "$BUILD_ID" <<< "$GUI_VERSION" || {
|
||||||
echo 'Release binaries are stale. Run bash scripts/build-release.sh first.' >&2
|
echo 'Release binaries are stale. Run bash scripts/build-release.sh first.' >&2
|
||||||
exit 2
|
exit 2
|
||||||
@@ -24,152 +24,136 @@ grep -qF "$BUILD_ID" <<< "$GUI_VERSION" || {
|
|||||||
VERSION=$(sed -E 's/^CS-R Intake ([^ ]+).*/\1/' <<< "$GUI_VERSION")
|
VERSION=$(sed -E 's/^CS-R Intake ([^ ]+).*/\1/' <<< "$GUI_VERSION")
|
||||||
|
|
||||||
TMP=$(mktemp -d)
|
TMP=$(mktemp -d)
|
||||||
cleanup() {
|
cleanup() { find "$TMP" -depth -delete 2>/dev/null || true; }
|
||||||
find "$TMP" -depth -delete 2>/dev/null || true
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
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")"
|
PYTHON=$(readlink -f "$(command -v python3)")
|
||||||
install -m755 "$GUI" "$TMP/app/intake-gui"
|
PYTHON_STDLIB=$(python3 -c 'import sysconfig; print(sysconfig.get_path("stdlib"))')
|
||||||
install -m755 "$SCANNER" "$TMP/app/pc-intake"
|
install -m755 "$PYTHON" "$APP/python/bin/python3"
|
||||||
install -m644 "$MANIFEST" "$TMP/app/release.json"
|
cp -a "$PYTHON_STDLIB/lib-dynload/." "$APP/python/lib/python/lib-dynload/"
|
||||||
python3 "$ROOT/scripts/release-manifest.py" --verify-release "$TMP/app"
|
|
||||||
|
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
|
contains_credentials=0
|
||||||
if [[ -f $BUNDLE_CONFIG/inventory-api-key && -f $BUNDLE_CONFIG/inventory-server.crt ]]; then
|
if [[ -f $BUNDLE_CONFIG/inventory-api-key && -f $BUNDLE_CONFIG/inventory-server.crt ]]; then
|
||||||
key=$(tr -d '\r\n' < "$BUNDLE_CONFIG/inventory-api-key")
|
key=$(tr -d '\r\n' < "$BUNDLE_CONFIG/inventory-api-key")
|
||||||
[[ -n $key && $key != *[[:space:]]* ]] || {
|
[[ -n $key && $key != *[[:space:]]* ]] || { echo 'Bundled API key is malformed.' >&2; exit 2; }
|
||||||
echo 'Bundled inventory API key is empty or malformed.' >&2
|
|
||||||
exit 2
|
|
||||||
}
|
|
||||||
openssl x509 -in "$BUNDLE_CONFIG/inventory-server.crt" -noout -checkend 0 >/dev/null || {
|
openssl x509 -in "$BUNDLE_CONFIG/inventory-server.crt" -noout -checkend 0 >/dev/null || {
|
||||||
echo 'Bundled inventory certificate is invalid or expired.' >&2
|
echo 'Bundled inventory certificate is invalid or expired.' >&2; exit 2;
|
||||||
exit 2
|
|
||||||
}
|
}
|
||||||
mkdir -m700 "$TMP/app/default-config"
|
mkdir -m700 "$APP/default-config"
|
||||||
install -m600 "$BUNDLE_CONFIG/inventory-api-key" "$TMP/app/default-config/inventory-api-key"
|
install -m600 "$BUNDLE_CONFIG/inventory-api-key" "$APP/default-config/inventory-api-key"
|
||||||
install -m600 "$BUNDLE_CONFIG/inventory-server.crt" "$TMP/app/default-config/inventory-server.crt"
|
install -m600 "$BUNDLE_CONFIG/inventory-server.crt" "$APP/default-config/inventory-server.crt"
|
||||||
printf '%s\n' '{"url":"https://192.168.178.17"}' > "$TMP/app/default-config/inventory.json"
|
printf '%s\n' '{"url":"https://192.168.178.17"}' > "$APP/default-config/inventory.json"
|
||||||
chmod 600 "$TMP/app/default-config/inventory.json"
|
chmod 600 "$APP/default-config/inventory.json"
|
||||||
contains_credentials=1
|
contains_credentials=1
|
||||||
elif [[ -e $BUNDLE_CONFIG/inventory-api-key || -e $BUNDLE_CONFIG/inventory-server.crt ]]; then
|
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
|
exit 2
|
||||||
fi
|
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
|
env -i \
|
||||||
#!/bin/sh
|
PYTHONHOME="$APP/python" \
|
||||||
set -eu
|
PYTHONPATH="$APP/python/lib/python.zip:$APP/python/lib/python/lib-dynload" \
|
||||||
BUILD_ID='$BUILD_ID'
|
"$APP/lib/ld-linux-x86-64.so.2" --library-path "$APP/lib" "$APP/python/bin/python3" \
|
||||||
VERSION='$VERSION'
|
-c 'import argparse, datetime, hashlib, json, pathlib, sqlite3, ssl, urllib.request, uuid; print("Bundled Python OK")'
|
||||||
PAYLOAD_LINE=__PAYLOAD_LINE__
|
|
||||||
|
|
||||||
cache_root=\${XDG_CACHE_HOME:-\${HOME:-\$PWD}/.cache}/csr-intake
|
python3 - "$APP" "$TMP/payload.gz" "$TMP/payload.manifest" <<'PY'
|
||||||
app_dir=\$cache_root/\$BUILD_ID
|
from pathlib import Path
|
||||||
|
import gzip, os, struct, sys
|
||||||
extract_bundle() {
|
root, output, manifest = map(Path, sys.argv[1:])
|
||||||
bundle_valid() {
|
files = sorted(path for path in root.rglob('*') if path.is_file())
|
||||||
[ -x "\$app_dir/intake-gui" ] && [ -x "\$app_dir/pc-intake" ] &&
|
lines=[]
|
||||||
(cd "\$app_dir" && sha256sum -c SHA256SUMS >/dev/null 2>&1)
|
with output.open('wb') as raw, gzip.GzipFile(fileobj=raw, mode='wb', compresslevel=9, mtime=0) as archive:
|
||||||
}
|
archive.write(b'CSRPKG3\0')
|
||||||
if ! bundle_valid; then
|
for path in files:
|
||||||
if [ -e "\$app_dir" ]; then
|
name = path.relative_to(root).as_posix().encode()
|
||||||
find "\$app_dir" -depth -delete 2>/dev/null || true
|
data = path.read_bytes()
|
||||||
fi
|
mode = path.stat().st_mode & 0o777
|
||||||
temp_dir=\$cache_root/.extract-\$BUILD_ID-\$\$
|
archive.write(struct.pack('<IIQ', len(name), mode, len(data)))
|
||||||
mkdir -p "\$temp_dir"
|
archive.write(name); archive.write(data)
|
||||||
trap 'find "\$temp_dir" -depth -delete 2>/dev/null || true' EXIT HUP INT TERM
|
digest=0xcbf29ce484222325
|
||||||
tail -n +"\$PAYLOAD_LINE" "\$0" | gzip -dc | tar -xf - -C "\$temp_dir"
|
for byte in data: digest=((digest ^ byte) * 0x100000001b3) & 0xffffffffffffffff
|
||||||
(cd "\$temp_dir" && sha256sum -c SHA256SUMS >/dev/null)
|
lines.append(f'{digest:016x}\t{mode:o}\t{len(data)}\t{name.decode()}')
|
||||||
mkdir -p "\$cache_root"
|
archive.write(struct.pack('<I', 0))
|
||||||
if ! mv -T "\$temp_dir" "\$app_dir" 2>/dev/null; then
|
manifest.write_text('\n'.join(lines)+'\n')
|
||||||
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)
|
|
||||||
PY
|
PY
|
||||||
)
|
|
||||||
if [ -n "\$keyboard_layout" ]; then
|
|
||||||
export XKB_DEFAULT_LAYOUT=\$keyboard_layout
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
export CSR_SCANNER="\$app_dir/pc-intake"
|
export CSR_BUILD_ID="$BUILD_ID" CSR_VERSION="$VERSION"
|
||||||
if [ "\$has_data" -eq 1 ]; then
|
export CSR_PAYLOAD_FILE="$TMP/payload.gz" CSR_PAYLOAD_MANIFEST="$TMP/payload.manifest"
|
||||||
exec "\$app_dir/intake-gui" "\$@"
|
export RUSTFLAGS="${RUSTFLAGS:-} -C target-feature=+crt-static"
|
||||||
fi
|
cargo clippy --locked --manifest-path "$ROOT/launcher/Cargo.toml" --all-targets -- -D warnings
|
||||||
exec "\$app_dir/intake-gui" "\$@" --data "\$data_dir"
|
cargo build --locked --release --manifest-path "$ROOT/launcher/Cargo.toml"
|
||||||
EOF
|
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 ))
|
file "$OUT" | grep -q 'static-pie linked' || { echo 'Launcher is not statically linked.' >&2; exit 2; }
|
||||||
sed -i "s/__PAYLOAD_LINE__/$payload_line/" "$TMP/launcher"
|
if ldd "$OUT" 2>&1 | grep -qv 'statically linked\|not a dynamic executable'; then
|
||||||
cat "$TMP/launcher" "$TMP/payload.tar.gz" > "$OUT"
|
echo 'Launcher unexpectedly depends on host libraries.' >&2; exit 2
|
||||||
if [[ $contains_credentials -eq 1 ]]; then
|
|
||||||
chmod 700 "$OUT"
|
|
||||||
else
|
|
||||||
chmod 755 "$OUT"
|
|
||||||
fi
|
fi
|
||||||
"$OUT" --check-bundle
|
"$OUT" --check-bundle
|
||||||
if [[ $contains_credentials -eq 1 ]]; then
|
echo "Standalone ELF created: $OUT"
|
||||||
echo "Standalone executable created with inventory configuration: $OUT"
|
|
||||||
else
|
|
||||||
echo "Standalone executable created: $OUT"
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ def source_id():
|
|||||||
base = ROOT / component
|
base = ROOT / component
|
||||||
paths.extend((base / 'src').rglob('*.rs'))
|
paths.extend((base / 'src').rglob('*.rs'))
|
||||||
paths.extend(p for p in [base / 'Cargo.toml', base / 'Cargo.lock', base / 'build.rs'] if p.exists())
|
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())
|
paths.extend(p for p in (ROOT / 'scripts').iterdir() if p.is_file())
|
||||||
h = hashlib.sha256()
|
h = hashlib.sha256()
|
||||||
for path in sorted(paths):
|
for path in sorted(paths):
|
||||||
|
|||||||
Reference in New Issue
Block a user