lios 0.1.70

A gorgeous GTK4/VTE Linux terminal with live themes, glass backgrounds, session prompt profiles, Sixel images, and safe GPU controls.
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();
    }
}