use std::env;
use std::ffi::OsString;
use std::io::Write;
use std::path::Path;
use std::process::{Command, Stdio};
const INSTALLER: &[u8] = include_bytes!("../install-jetson.sh");
#[derive(Debug, PartialEq, Eq)]
enum BootstrapAction {
Help,
Version,
Check,
Install {
launch: bool,
forward: Vec<OsString>,
},
}
pub fn run() -> i32 {
let args = env::args_os().skip(1).collect::<Vec<_>>();
match bootstrap_action(&args) {
BootstrapAction::Help => {
print_help();
0
}
BootstrapAction::Version => {
println!("lios {} (installer)", env!("CARGO_PKG_VERSION"));
0
}
BootstrapAction::Check => run_installer(true, false, &[]),
BootstrapAction::Install { launch, forward } => run_installer(false, launch, &forward),
}
}
fn bootstrap_action(args: &[OsString]) -> BootstrapAction {
match args.first().and_then(|arg| arg.to_str()) {
Some("-h" | "--help") => BootstrapAction::Help,
Some("-V" | "--version") => BootstrapAction::Version,
Some("--check") => BootstrapAction::Check,
Some("install" | "--install" | "---install") => BootstrapAction::Install {
launch: false,
forward: Vec::new(),
},
_ => BootstrapAction::Install {
launch: true,
forward: args.to_vec(),
},
}
}
fn run_installer(check_only: bool, launch: bool, forward: &[OsString]) -> i32 {
if !cfg!(target_os = "linux") {
eprintln!("Lios is supported on Linux.");
return 1;
}
let current_executable = env::current_exe().ok();
let mut installer = Command::new("sh");
installer.args(["-s", "--"]);
if check_only {
installer.arg("--check");
} else if launch {
installer.arg("--launch").args(forward);
}
installer
.env("LIOS_CRATE_VERSION", env!("CARGO_PKG_VERSION"))
.stdin(Stdio::piped());
if let Some(install_root) = current_executable.as_deref().and_then(cargo_install_root) {
installer.env("LIOS_INSTALL_ROOT", install_root);
}
let mut child = match installer.spawn() {
Ok(child) => child,
Err(error) => {
eprintln!("Could not start the Lios installer: {error}");
return 1;
}
};
let write_result = child
.stdin
.take()
.ok_or_else(|| "installer input was unavailable".to_string())
.and_then(|mut stdin| {
stdin
.write_all(INSTALLER)
.map_err(|error| format!("could not stream installer: {error}"))
});
if let Err(message) = write_result {
let _ = child.kill();
let _ = child.wait();
eprintln!("Could not prepare the Lios installer: {message}");
return 1;
}
let status = match child.wait() {
Ok(status) => status,
Err(error) => {
eprintln!("Could not wait for the Lios installer: {error}");
return 1;
}
};
if !status.success() {
return status.code().unwrap_or(1);
}
0
}
fn cargo_install_root(executable: &Path) -> Option<&Path> {
if executable.file_name()? != "lios" {
return None;
}
let bin = executable.parent()?;
if bin.file_name()? != "bin" {
return None;
}
let root = bin.parent()?;
(root.join(".crates2.json").is_file() || root.join(".crates.toml").is_file()).then_some(root)
}
fn print_help() {
println!(
"Lios Terminal installer\n\nUsage:\n lios\n lios install\n lios --check\n\nThe first run installs native GTK/VTE dependencies, replaces this bootstrap with the full Lios terminal, creates the desktop launcher, and starts Lios."
);
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::{self, OpenOptions};
use std::os::unix::fs::OpenOptionsExt;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn no_arguments_installs_then_launches() {
assert_eq!(
bootstrap_action(&[]),
BootstrapAction::Install {
launch: true,
forward: Vec::new()
}
);
}
#[test]
fn install_only_provisions_native_binary() {
assert_eq!(
bootstrap_action(&[OsString::from("install")]),
BootstrapAction::Install {
launch: false,
forward: Vec::new()
}
);
}
#[test]
fn launch_arguments_are_forwarded_after_install() {
let args = vec![OsString::from("--theme"), OsString::from("bronco")];
assert_eq!(
bootstrap_action(&args),
BootstrapAction::Install {
launch: true,
forward: args
}
);
}
#[test]
fn install_root_requires_cargo_metadata_and_bin_layout() {
let root = test_directory("root-detection");
let bin = root.join("bin");
fs::create_dir_all(&bin).unwrap();
let executable = bin.join("lios");
fs::write(&executable, []).unwrap();
assert_eq!(cargo_install_root(&executable), None);
fs::write(root.join(".crates2.json"), "{}").unwrap();
assert_eq!(cargo_install_root(&executable), Some(root.as_path()));
assert_eq!(cargo_install_root(&bin.join("other")), None);
assert_eq!(cargo_install_root(&root.join("lios")), None);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn embedded_installer_pins_version_preserves_root_and_launch_arguments() {
let directory = test_directory("installer-contract");
let fake_bin = directory.join("fake-bin");
let install_root = directory.join("custom root");
let home = directory.join("home");
let cargo_log = directory.join("cargo.log");
let native_log = directory.join("native.log");
let fake_native = directory.join("native-lios");
fs::create_dir_all(&fake_bin).unwrap();
fs::create_dir_all(&home).unwrap();
write_executable(
&fake_bin.join("pkg-config"),
"#!/bin/sh\ncase \"$1\" in\n --atleast-version=*) exit 0 ;;\n --modversion) case \"$2\" in gtk4) printf '4.6.0\\n' ;; *) printf '0.70.6\\n' ;; esac ;;\nesac\n",
);
write_executable(
&fake_bin.join("rustc"),
"#!/bin/sh\nprintf 'rustc 1.85.0 (test)\\n'\n",
);
write_executable(
&fake_bin.join("cargo"),
"#!/bin/sh\nprintf '%s\\n' \"$*\" > \"$LIOS_CARGO_LOG\"\nroot=\nwhile [ \"$#\" -gt 0 ]; do\n if [ \"$1\" = --root ]; then root=$2; shift 2; else shift; fi\ndone\n[ -n \"$root\" ] || exit 91\nmkdir -p \"$root/bin\"\ncp \"$LIOS_FAKE_NATIVE\" \"$root/bin/lios\"\nchmod 755 \"$root/bin/lios\"\n",
);
write_executable(
&fake_native,
"#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$LIOS_NATIVE_LOG\"\nif [ \"${1:-}\" = --version ]; then printf 'lios %s\\n' \"$LIOS_CRATE_VERSION\"; fi\n",
);
let path = format!(
"{}:{}",
fake_bin.display(),
env::var("PATH").unwrap_or_default()
);
let mut command = Command::new("sh");
command
.args(["-s", "--", "--launch", "--theme", "bronco"])
.env("PATH", path)
.env("HOME", &home)
.env("LIOS_INSTALL_ROOT", &install_root)
.env("LIOS_CRATE_VERSION", env!("CARGO_PKG_VERSION"))
.env("LIOS_CARGO_LOG", &cargo_log)
.env("LIOS_NATIVE_LOG", &native_log)
.env("LIOS_FAKE_NATIVE", &fake_native)
.stdin(Stdio::piped())
.stdout(Stdio::null());
let mut child = command.spawn().unwrap();
child.stdin.take().unwrap().write_all(INSTALLER).unwrap();
assert!(child.wait().unwrap().success());
let cargo_args = fs::read_to_string(cargo_log).unwrap();
assert!(cargo_args.contains("--locked --force --version"));
assert!(cargo_args.contains(&format!("={}", env!("CARGO_PKG_VERSION"))));
assert!(cargo_args.contains("--features native"));
assert!(cargo_args.contains(&format!("--root {}", install_root.display())));
assert!(cargo_args.ends_with("lios\n"));
let native_calls = fs::read_to_string(native_log).unwrap();
assert_eq!(
native_calls.lines().collect::<Vec<_>>(),
["--version", "--install", "--theme bronco"]
);
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn embedded_installer_default_matches_the_rust_package_version() {
let installer = std::str::from_utf8(INSTALLER).unwrap();
let expected = format!(
"LIOS_CRATE_VERSION=\"${{LIOS_CRATE_VERSION:-{}}}\"",
env!("CARGO_PKG_VERSION")
);
assert!(
installer.lines().any(|line| line == expected),
"embedded installer must default to the same version as Cargo.toml"
);
}
fn test_directory(label: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
env::temp_dir().join(format!("lios-{label}-{}-{nanos:x}", std::process::id()))
}
fn write_executable(path: &Path, contents: &str) {
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o700)
.open(path)
.unwrap();
file.write_all(contents.as_bytes()).unwrap();
}
}