Vollständig portables Linux-ELF bauen
Some checks failed
Tests / test (push) Has been cancelled

This commit is contained in:
2026-10-15 11:23:21 +02:00
parent e799682e10
commit bf34593bbb
10 changed files with 581 additions and 166 deletions

64
launcher/Cargo.lock generated Normal file
View 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
View 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
View 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(&current).map(|m| m.len()).ok();
if replace {
let _ = fs::remove_file(&runtime);
if fs::hard_link(&current, &runtime).is_err() {
fs::copy(&current, &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)
}