axbuild 0.4.14

An OS build lib toolkit used by arceos
Documentation
use super::*;

pub(super) fn apply_case_script_envs(
    command: &mut Command,
    layout: &case_assets::CaseAssetLayout,
    script_envs: &[(String, String)],
) -> anyhow::Result<()> {
    let host_path = std::env::var_os("PATH").unwrap_or_default();
    let mut path_entries = Vec::new();
    path_entries.push(layout.command_wrapper_dir.clone());
    path_entries.extend(std::env::split_paths(&host_path));

    let path = std::env::join_paths(path_entries)
        .map_err(|e| anyhow::anyhow!("failed to build case script PATH: {e}"))?;
    command.env("PATH", path);
    command.env("QEMU_LD_PREFIX", &layout.staging_root);
    command.env("LD_LIBRARY_PATH", guest_library_path(&layout.staging_root));

    for (key, value) in script_envs {
        command.env(key, value);
    }

    Ok(())
}

pub(crate) fn case_script_envs(
    case: &TestQemuCase,
    layout: &case_assets::CaseAssetLayout,
    config: &CaseAssetConfig,
) -> Vec<(String, String)> {
    vec![
        (
            config.script_env.staging_root.clone(),
            layout.staging_root.display().to_string(),
        ),
        (
            config.script_env.case_dir.clone(),
            case.case_dir.display().to_string(),
        ),
        (
            config.script_env.case_c_dir.clone(),
            case_c_source_dir(case).display().to_string(),
        ),
        (
            config.script_env.case_work_dir.clone(),
            layout.work_dir.display().to_string(),
        ),
        (
            config.script_env.case_build_dir.clone(),
            layout.build_dir.display().to_string(),
        ),
        (
            config.script_env.case_overlay_dir.clone(),
            layout.overlay_dir.display().to_string(),
        ),
    ]
}

pub(super) fn write_guest_command_wrappers(
    layout: &case_assets::CaseAssetLayout,
    qemu_runner: &Path,
) -> anyhow::Result<()> {
    let mut guest_commands = BTreeMap::new();
    for relative_dir in ["bin", "sbin", "usr/bin", "usr/sbin"] {
        let dir_path = layout.staging_root.join(relative_dir);
        if !dir_path.is_dir() {
            continue;
        }

        let mut entries = fs::read_dir(&dir_path)
            .with_context(|| format!("failed to read {}", dir_path.display()))?
            .collect::<Result<Vec<_>, _>>()
            .with_context(|| format!("failed to read {}", dir_path.display()))?;
        entries.sort_by_key(|left| left.file_name());

        for entry in entries {
            let name = entry.file_name();
            if guest_commands.contains_key(name.as_os_str()) {
                continue;
            }

            let file_type = entry.file_type().with_context(|| {
                format!("failed to inspect guest command {}", entry.path().display())
            })?;
            if !file_type.is_file() && !file_type.is_symlink() {
                continue;
            }
            guest_commands.insert(
                name,
                format!("{relative_dir}/{}", entry.file_name().to_string_lossy()),
            );
        }
    }

    for (name, relative_guest_path) in guest_commands {
        let wrapper_path = layout.command_wrapper_dir.join(&name);
        if relative_guest_path == "sbin/apk" {
            write_apk_wrapper_script(&wrapper_path, qemu_runner, &layout.staging_root, layout)?;
        } else {
            write_guest_exec_wrapper(
                &wrapper_path,
                qemu_runner,
                &layout.staging_root,
                &relative_guest_path,
                None,
            )?;
        }
    }

    Ok(())
}

pub(super) fn ensure_guest_tool_exists(
    staging_root: &Path,
    relative_path: &str,
) -> anyhow::Result<()> {
    let path = staging_root.join(relative_path);
    ensure!(
        path.is_file(),
        "staging root is missing required guest tool `{}`; install it in prebuild.sh",
        path.display()
    );
    Ok(())
}

pub(crate) fn qemu_user_binary_names(arch: &str) -> anyhow::Result<&'static [&'static str]> {
    Ok(cross_compile_spec(arch)?.qemu_user_binaries)
}

pub(super) fn write_guest_exec_wrapper(
    path: &Path,
    qemu_runner: &Path,
    staging_root: &Path,
    guest_relative_path: &str,
    extra_args: Option<String>,
) -> anyhow::Result<()> {
    let guest_path = staging_root.join(guest_relative_path);
    let mut body = format!(
        "export QEMU_LD_PREFIX={root}\nexport LD_LIBRARY_PATH={lib_path}\nexec {qemu} -0 {guest} \
         -L {root} {guest}",
        root = shell_single_quote(staging_root),
        lib_path = shell_single_quote(guest_library_path(staging_root)),
        qemu = shell_single_quote(qemu_runner),
        guest = shell_single_quote(&guest_path),
    );
    if let Some(extra_args) = extra_args {
        body.push(' ');
        body.push_str(&extra_args);
    }
    body.push_str(" \"$@\"\n");

    write_wrapper_script(path, &body)
}

pub(super) fn write_apk_wrapper_script(
    path: &Path,
    qemu_runner: &Path,
    staging_root: &Path,
    layout: &case_assets::CaseAssetLayout,
) -> anyhow::Result<()> {
    let body = format!(
        "export QEMU_LD_PREFIX={root}\nexport LD_LIBRARY_PATH={lib_path}\nexec {qemu} -L {root} \
         {apk} --root {root} --repositories-file {repositories} --keys-dir {keys} --cache-dir \
         {cache} --update-cache --timeout 60 --no-interactive --force-no-chroot --scripts=no \
         \"$@\"\n",
        root = shell_single_quote(staging_root),
        lib_path = shell_single_quote(guest_library_path(staging_root)),
        qemu = shell_single_quote(qemu_runner),
        apk = shell_single_quote(staging_root.join("sbin/apk")),
        repositories = shell_single_quote(staging_root.join("etc/apk/repositories")),
        keys = shell_single_quote(staging_root.join("etc/apk/keys")),
        cache = shell_single_quote(&layout.apk_cache_dir),
    );
    write_wrapper_script(path, &body)
}

pub(super) fn guest_library_path(staging_root: &Path) -> String {
    format!(
        "{}:{}",
        staging_root.join("lib").display(),
        staging_root.join("usr/lib").display()
    )
}

pub(super) fn write_wrapper_script(path: &Path, body: &str) -> anyhow::Result<()> {
    fs::write(path, format!("#!/bin/sh\nset -eu\n{body}"))
        .with_context(|| format!("failed to write {}", path.display()))?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = fs::metadata(path)
            .with_context(|| format!("failed to stat {}", path.display()))?
            .permissions();
        perms.set_mode(0o755);
        fs::set_permissions(path, perms)
            .with_context(|| format!("failed to chmod {}", path.display()))?;
    }
    Ok(())
}

pub(super) fn find_host_binary_candidates(candidates: &[&str]) -> anyhow::Result<PathBuf> {
    candidates
        .iter()
        .find_map(|candidate| find_optional_host_binary(candidate))
        .ok_or_else(|| {
            anyhow::anyhow!(
                "required host binary was not found in PATH; tried: {}",
                candidates.join(", ")
            )
        })
}

pub(super) fn find_optional_host_binary(name: &str) -> Option<PathBuf> {
    std::env::var_os("PATH").and_then(|path_var| {
        std::env::split_paths(&path_var)
            .map(|dir| dir.join(name))
            .find(|candidate| candidate.is_file())
    })
}

pub(super) fn shell_single_quote(path: impl AsRef<Path>) -> String {
    let value = path.as_ref().display().to_string().replace('\'', "'\\''");
    format!("'{value}'")
}