elf_loader 0.17.0

A no_std-friendly ELF loader and runtime linker for Rust.
Documentation
use std::{
    env, fs,
    path::{Path, PathBuf},
    process::Command,
};

enum Compiler {
    Rust,
    C,
}

pub(crate) struct FixtureBuild {
    source_dir: PathBuf,
    output_dir: PathBuf,
    compiler: String,
    rust_target: Option<String>,
}

#[allow(dead_code)]
impl FixtureBuild {
    pub(crate) fn rust(source_dir: &str, output_dir: &str) -> Self {
        Self::new(source_dir, output_dir, Compiler::Rust)
    }

    pub(crate) fn c(source_dir: &str, output_dir: &str) -> Self {
        Self::new(source_dir, output_dir, Compiler::C)
    }

    fn new(source_dir: &str, output_dir: &str, compiler: Compiler) -> Self {
        let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        let target = target();
        let (compiler, rust_target) = match compiler {
            Compiler::Rust => (
                env::var("RUSTC").unwrap_or_else(|_| "rustc".to_owned()),
                target.clone(),
            ),
            Compiler::C => (c_compiler(target.as_deref()), None),
        };
        let output_dir = root
            .join("target/test-fixtures")
            .join(target.unwrap_or_else(|| "native".to_owned()))
            .join(output_dir);
        fs::create_dir_all(&output_dir).expect("failed to create test fixture directory");

        Self {
            source_dir: root.join(source_dir),
            output_dir,
            compiler,
            rust_target,
        }
    }

    fn source(&self, name: impl AsRef<Path>) -> PathBuf {
        self.source_dir.join(name)
    }

    pub(crate) fn output(&self, name: impl AsRef<Path>) -> PathBuf {
        self.output_dir.join(name)
    }

    fn command(&self) -> Command {
        let mut command = Command::new(&self.compiler);
        if let Some(target) = &self.rust_target {
            command.arg("--target").arg(target);
        }
        command
    }

    pub(crate) fn compile(
        &self,
        source: impl AsRef<Path>,
        output: impl AsRef<Path>,
        dependencies: &[&Path],
        configure: impl FnOnce(&mut Command),
    ) -> PathBuf {
        let source = self.source(source);
        let output = self.output(output);
        if self.needs_rebuild(
            &output,
            std::iter::once(source.as_path()).chain(dependencies.iter().copied()),
        ) {
            let mut command = self.command();
            command.arg(&source);
            configure(&mut command);
            command.arg("-o").arg(&output);
            self.run(&mut command, &format!("compile {}", output.display()));
        }
        output
    }

    pub(crate) fn rust_cdylib(
        &self,
        source: impl AsRef<Path>,
        name: &str,
        dependencies: &[&Path],
        configure: impl FnOnce(&mut Command),
    ) -> PathBuf {
        self.compile(source, format!("lib{name}.so"), dependencies, |command| {
            command
                .arg("--crate-type=cdylib")
                .arg("--crate-name")
                .arg(name)
                .arg("-O")
                .arg("-C")
                .arg("panic=abort")
                .arg("-C")
                .arg("linker=rust-lld");
            configure(command);
        })
    }

    pub(crate) fn c_shared(
        &self,
        source: impl AsRef<Path>,
        output: impl AsRef<Path>,
        dependencies: &[&Path],
        configure: impl FnOnce(&mut Command),
    ) -> PathBuf {
        self.compile(source, output, dependencies, |command| {
            command
                .arg("-shared")
                .arg("-fPIC")
                .arg("-fno-stack-protector")
                .arg("-nostdlib");
            configure(command);
        })
    }

    fn run(&self, command: &mut Command, step: &str) {
        let status = command
            .status()
            .unwrap_or_else(|error| panic!("failed to spawn command for {step}: {error}"));
        assert!(status.success(), "command failed while trying to {step}");
    }

    fn needs_rebuild(
        &self,
        output: &Path,
        inputs: impl IntoIterator<Item = impl AsRef<Path>>,
    ) -> bool {
        let Ok(output_time) = output.metadata().and_then(|metadata| metadata.modified()) else {
            return true;
        };
        inputs.into_iter().any(|input| {
            input
                .as_ref()
                .metadata()
                .and_then(|metadata| metadata.modified())
                .map_or(true, |input_time| input_time > output_time)
        })
    }
}

fn c_compiler(target: Option<&str>) -> String {
    if let Ok(compiler) = env::var("CC")
        && !compiler.is_empty()
    {
        return compiler;
    }

    let candidates: &[&str] = match target {
        Some("aarch64-unknown-linux-gnu") => {
            &["aarch64-linux-gnu-gcc", "aarch64-unknown-linux-gnu-gcc"]
        }
        Some("arm-unknown-linux-gnueabihf") => {
            &["arm-unknown-linux-gnueabihf-gcc", "arm-linux-gnueabihf-gcc"]
        }
        Some("i586-unknown-linux-gnu") => &["i686-linux-gnu-gcc", "i586-linux-gnu-gcc"],
        Some("loongarch64-unknown-linux-gnu") => &[
            "loongarch64-unknown-linux-gnu-gcc",
            "loongarch64-linux-gnu-gcc",
        ],
        Some("riscv64gc-unknown-linux-gnu") => {
            &["riscv64-linux-gnu-gcc", "riscv64-unknown-linux-gnu-gcc"]
        }
        _ => &["cc"],
    };
    candidates
        .iter()
        .find(|compiler| command_exists(compiler))
        .unwrap_or_else(|| panic!("no C compiler found for {}", target.unwrap_or("host")))
        .to_string()
}

fn command_exists(command: &str) -> bool {
    env::var_os("PATH").is_some_and(|paths| {
        env::split_paths(&paths).any(|directory| directory.join(command).is_file())
    })
}

fn target() -> Option<String> {
    env::var("TARGET")
        .or_else(|_| env::var("CARGO_BUILD_TARGET"))
        .ok()
        .filter(|target| !target.is_empty())
        .or_else(|| {
            match env::consts::ARCH {
                "x86_64" => Some("x86_64-unknown-linux-gnu"),
                "x86" => Some("i586-unknown-linux-gnu"),
                "aarch64" => Some("aarch64-unknown-linux-gnu"),
                "riscv64" => Some("riscv64gc-unknown-linux-gnu"),
                "loongarch64" => Some("loongarch64-unknown-linux-gnu"),
                "arm" => Some("arm-unknown-linux-gnueabihf"),
                _ => None,
            }
            .map(str::to_owned)
        })
}