use std::{
collections::{BTreeSet, HashMap},
fs,
path::{Path, PathBuf},
process::{Command, Stdio},
time::UNIX_EPOCH,
};
use anyhow::{Context, bail};
use super::{compile::cc_for_arch, features::has_feature};
use crate::{build::ARCEOS_LINKER_SCRIPT, support::process::ProcessExt};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct LinkScripts {
pub(super) script: PathBuf,
pub(super) search_dirs: Vec<PathBuf>,
pub(super) pie: bool,
}
pub(super) fn find_link_scripts(
target_dir: &Path,
target: &str,
mode: &str,
platform: &str,
features: &[String],
) -> anyhow::Result<LinkScripts> {
if has_feature(features, "plat-dyn") {
let script = find_final_linker_script(target_dir, target, mode)?;
let search_dirs = find_dynamic_linker_search_dirs(target_dir, target, mode)?;
return Ok(LinkScripts {
script,
search_dirs,
pie: true,
});
}
let script = find_final_linker_script(target_dir, target, mode)?;
let search_dirs = find_linker_search_dirs(target_dir, target, mode, platform, features)?;
Ok(LinkScripts {
script,
search_dirs,
pie: false,
})
}
pub(super) fn find_final_linker_script(
target_dir: &Path,
target: &str,
mode: &str,
) -> anyhow::Result<PathBuf> {
let build_dir = target_dir.join(target).join(mode).join("build");
let mut candidates = Vec::new();
if build_dir.is_dir() {
for entry in fs::read_dir(&build_dir)
.with_context(|| format!("failed to read {}", build_dir.display()))?
{
let entry = entry?;
let path = entry.path();
if path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with("ax-runtime-"))
{
let linker_script = path.join("out").join(ARCEOS_LINKER_SCRIPT);
if linker_script.is_file() {
let modified = linker_script
.metadata()
.and_then(|metadata| metadata.modified())
.unwrap_or(UNIX_EPOCH);
candidates.push((modified, linker_script));
}
}
}
}
candidates.sort_by(|(left_time, left_path), (right_time, right_path)| {
right_time
.cmp(left_time)
.then_with(|| left_path.cmp(right_path))
});
candidates
.into_iter()
.map(|(_, path)| path)
.next()
.with_context(|| {
format!(
"expected final linker script under {} after ax-libc cargo build",
build_dir.join("ax-runtime-*/out").display()
)
})
}
pub(super) fn find_linker_search_dirs(
target_dir: &Path,
target: &str,
mode: &str,
platform: &str,
features: &[String],
) -> anyhow::Result<Vec<PathBuf>> {
let build_dir = target_dir.join(target).join(mode).join("build");
let mut dirs = BTreeSet::new();
let runtime_out = latest_out_dir_with_script(&build_dir, "ax-runtime-", ARCEOS_LINKER_SCRIPT)?;
dirs.insert(runtime_out);
let platform_out = latest_out_dir_with_script(
&build_dir,
platform_linker_owner_prefix(platform, features),
"axplat.x",
)?;
dirs.insert(platform_out);
Ok(dirs.into_iter().collect())
}
fn find_dynamic_linker_search_dirs(
target_dir: &Path,
target: &str,
mode: &str,
) -> anyhow::Result<Vec<PathBuf>> {
let build_dir = target_dir.join(target).join(mode).join("build");
let mut dirs = BTreeSet::new();
dirs.insert(latest_out_dir_with_script(
&build_dir,
"ax-runtime-",
ARCEOS_LINKER_SCRIPT,
)?);
dirs.insert(latest_out_dir_with_script(
&build_dir,
"axplat-dyn-",
"axplat.x",
)?);
dirs.insert(latest_out_dir_with_script(
&build_dir, "somehal-", "link.x",
)?);
dirs.insert(latest_out_dir_with_script(
&build_dir,
"someboot-",
"someboot.x",
)?);
Ok(dirs.into_iter().collect())
}
fn latest_out_dir_with_script(
build_dir: &Path,
package_prefix: &str,
script_name: &str,
) -> anyhow::Result<PathBuf> {
let mut candidates = Vec::new();
if build_dir.is_dir() {
for entry in fs::read_dir(build_dir)
.with_context(|| format!("failed to read {}", build_dir.display()))?
{
let entry = entry?;
let path = entry.path();
if !path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with(package_prefix))
{
continue;
}
let out_dir = path.join("out");
let script = out_dir.join(script_name);
if script.is_file() {
let modified = script
.metadata()
.and_then(|metadata| metadata.modified())
.unwrap_or(UNIX_EPOCH);
candidates.push((modified, out_dir));
}
}
}
candidates.sort_by(|(left_time, left_path), (right_time, right_path)| {
right_time
.cmp(left_time)
.then_with(|| left_path.cmp(right_path))
});
candidates
.into_iter()
.map(|(_, path)| path)
.next()
.with_context(|| {
format!(
"expected linker script `{script_name}` under {}/{}*/out after ax-libc cargo build",
build_dir.display(),
package_prefix
)
})
}
fn platform_linker_owner_prefix(platform: &str, features: &[String]) -> &'static str {
if has_feature(features, "plat-dyn") {
return "axplat-dyn-";
}
match platform {
"loongarch64-qemu-virt" => "ax-plat-loongarch64-qemu-virt-",
_ => "ax-hal-",
}
}
pub(super) fn link_c_app(
arch: &str,
link_scripts: &LinkScripts,
elf_path: &Path,
rust_lib: &Path,
libc: &Path,
app_objects: &[PathBuf],
libgcc: Option<PathBuf>,
) -> anyhow::Result<()> {
let mut command = Command::new("rust-lld");
command
.arg("-flavor")
.arg("gnu")
.arg("-m")
.arg(lld_machine(arch)?)
.arg("-nostdlib")
.arg("-static")
.arg("--gc-sections")
.arg("-znostart-stop-gc");
for dir in &link_scripts.search_dirs {
command.arg(format!("-L{}", dir.display()));
}
command.arg(format!("-T{}", link_scripts.script.display()));
if link_scripts.pie {
command.arg("-pie");
} else {
command.arg("-no-pie");
}
if let Some(libgcc) = libgcc {
command.arg(libgcc);
}
command
.args(app_objects)
.arg(libc)
.arg(rust_lib)
.arg("-o")
.arg(elf_path);
command
.exec()
.with_context(|| format!("failed to link {}", elf_path.display()))
}
fn lld_machine(arch: &str) -> anyhow::Result<&'static str> {
match arch {
"aarch64" => Ok("aarch64elf"),
"loongarch64" => Ok("elf64loongarch"),
"riscv64" => Ok("elf64lriscv"),
"x86_64" => Ok("elf_x86_64"),
arch => bail!("unsupported ArceOS C link architecture `{arch}`"),
}
}
pub(super) fn libgcc(arch: &str, features: &[String]) -> anyhow::Result<Option<PathBuf>> {
if !has_feature(features, "fp-simd") || !matches!(arch, "riscv64" | "aarch64") {
return Ok(None);
}
let output = Command::new(cc_for_arch(arch))
.arg("-print-libgcc-file-name")
.stdout(Stdio::piped())
.output()
.context("failed to query libgcc path")?;
if !output.status.success() {
bail!("failed to query libgcc path with status {}", output.status);
}
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok((!path.is_empty()).then(|| PathBuf::from(path)))
}
pub(super) fn platform_name(env: &HashMap<String, String>) -> String {
env.get("AX_PLATFORM")
.cloned()
.unwrap_or_else(|| "qemu".to_string())
}