use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LinkPlan {
pub args: Vec<String>,
pub output: PathBuf,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkerOs {
Macos,
Linux,
Other,
}
pub fn build_link_plan(
captured_linker_args: &[String],
new_object: &Path,
output: &Path,
target_os: LinkerOs,
extra_objects: &[std::path::PathBuf],
extra_exports: &[&str],
) -> LinkPlan {
let mut args = filter_captured_linker_args(captured_linker_args);
if !args.iter().any(|a| a == "-shared") {
args.push("-shared".into());
}
match target_os {
LinkerOs::Macos => {
args.push("-Wl,-undefined,dynamic_lookup".into());
for sym in extra_exports {
args.push(format!("-Wl,-exported_symbol,{sym}"));
}
if !args.iter().any(|a| a == "-isysroot") {
if let Some(sdk_kind) = detect_apple_sdk(&args) {
if let Some(sdk_path) = xcrun_sdk_path(sdk_kind) {
args.push("-isysroot".into());
args.push(sdk_path);
}
}
}
}
LinkerOs::Linux => {
args.push("-Wl,--unresolved-symbols=ignore-all".into());
}
LinkerOs::Other => {}
}
for obj in extra_objects {
args.push(obj.to_string_lossy().into());
}
args.push(new_object.to_string_lossy().into());
args.push("-o".into());
args.push(output.to_string_lossy().into());
LinkPlan {
args,
output: output.to_path_buf(),
}
}
fn detect_apple_sdk(args: &[String]) -> Option<&'static str> {
let mut iter = args.iter();
while let Some(a) = iter.next() {
if a != "-target" {
continue;
}
let triple = iter.next()?;
return Some(if triple.contains("-simulator") {
"iphonesimulator"
} else if triple.contains("apple-ios") {
"iphoneos"
} else if triple.contains("apple-darwin") {
"macosx"
} else {
return None;
});
}
None
}
fn xcrun_sdk_path(kind: &str) -> Option<String> {
let out = std::process::Command::new("xcrun")
.args(["--sdk", kind, "--show-sdk-path"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let path = String::from_utf8(out.stdout).ok()?.trim().to_string();
if path.is_empty() {
None
} else {
Some(path)
}
}
pub fn linker_os_for_host() -> LinkerOs {
if cfg!(target_os = "macos") || cfg!(target_os = "ios") {
LinkerOs::Macos
} else if cfg!(target_os = "linux") || cfg!(target_os = "android") {
LinkerOs::Linux
} else {
LinkerOs::Other
}
}
fn filter_captured_linker_args(args: &[String]) -> Vec<String> {
let mut out = Vec::with_capacity(args.len());
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if arg == "-o" && i + 1 < args.len() {
i += 2;
continue;
}
if arg == "-shared" {
i += 1;
continue;
}
if arg == "-undefined" && i + 1 < args.len() {
i += 2;
continue;
}
if is_object_or_archive_input(arg) {
i += 1;
continue;
}
if arg.starts_with("-Wl,-undefined,") {
i += 1;
continue;
}
if arg.starts_with("-Wl,--unresolved-symbols=") {
i += 1;
continue;
}
if arg.starts_with("-Wl,--version-script=") || arg.starts_with("--version-script=") {
i += 1;
continue;
}
if (arg == "-Wl,--version-script" || arg == "--version-script") && i + 1 < args.len() {
i += 2;
continue;
}
if arg == "-Wl,-exported_symbols_list" && i + 1 < args.len() {
i += 2;
continue;
}
if arg.starts_with("-Wl,-exported_symbols_list,") {
i += 1;
continue;
}
if arg == "-Wl,--no-undefined-version" || arg == "--no-undefined-version" {
i += 1;
continue;
}
if arg == "-Wl,-dead_strip" || arg == "-Wl,--gc-sections" {
i += 1;
continue;
}
out.push(arg.clone());
i += 1;
}
out
}
fn is_object_or_archive_input(arg: &str) -> bool {
if arg.starts_with('-') {
return false;
}
let ext = Path::new(arg)
.extension()
.and_then(|e| e.to_str())
.map(str::to_ascii_lowercase);
matches!(
ext.as_deref(),
Some("o" | "rlib" | "a" | "so" | "dylib" | "obj" | "lib"),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn s(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
#[test]
fn filter_drops_object_inputs() {
let kept = filter_captured_linker_args(&s(&[
"-O3",
"/tmp/foo.o",
"/tmp/bar.rlib",
"/tmp/libstd.a",
"-l",
"iconv",
]));
assert_eq!(kept, s(&["-O3", "-l", "iconv"]));
}
#[test]
fn filter_drops_dynamic_libraries_too() {
let kept =
filter_captured_linker_args(&s(&["/tmp/libfoo.so", "/tmp/libbar.dylib", "-shared"]));
assert!(kept.is_empty(), "expected empty, got {kept:?}");
}
#[test]
fn filter_keeps_search_path_and_link_flags() {
let kept = filter_captured_linker_args(&s(&[
"-L",
"/sdk/lib",
"-L/different/dir",
"-lcurl",
"-l",
"z",
"-Wl,-rpath,/some/path",
"-isysroot",
"/Applications/Xcode.app/.../MacOSX.sdk",
"-arch",
"arm64",
"-target",
"arm64-apple-macosx14.0.0",
"-fuse-ld=lld",
"-mmacosx-version-min=11.0",
]));
assert_eq!(
kept,
s(&[
"-L",
"/sdk/lib",
"-L/different/dir",
"-lcurl",
"-l",
"z",
"-Wl,-rpath,/some/path",
"-isysroot",
"/Applications/Xcode.app/.../MacOSX.sdk",
"-arch",
"arm64",
"-target",
"arm64-apple-macosx14.0.0",
"-fuse-ld=lld",
"-mmacosx-version-min=11.0",
]),
);
}
#[test]
fn filter_drops_existing_output_path() {
let kept =
filter_captured_linker_args(&s(&["-shared", "-o", "/old/libfoo.dylib", "/tmp/foo.o"]));
assert!(kept.is_empty(), "got {kept:?}");
}
#[test]
fn filter_drops_fat_build_version_scripts_and_no_undefined_version() {
let kept = filter_captured_linker_args(&s(&[
"-Wl,--version-script=/tmp/rustcXX/list",
"-Wl,--version-script=/ws/target/.whisker/android-jni-exports.ver",
"-Wl,--no-undefined-version",
"-Wl,--as-needed",
"-arch",
"arm64",
]));
assert_eq!(kept, s(&["-Wl,--as-needed", "-arch", "arm64"]));
}
#[test]
fn filter_drops_separated_version_script_form() {
let kept =
filter_captured_linker_args(&s(&["--version-script", "/tmp/rustcXX/list", "-pie"]));
assert_eq!(kept, s(&["-pie"]));
}
#[test]
fn filter_drops_existing_undefined_dynamic_lookup() {
let kept = filter_captured_linker_args(&s(&[
"-undefined",
"dynamic_lookup",
"-Wl,-undefined,dynamic_lookup",
"-Wl,--unresolved-symbols=ignore-all",
"-arch",
"arm64",
]));
assert_eq!(kept, s(&["-arch", "arm64"]));
}
#[test]
fn filter_drops_dead_strip_and_gc_sections() {
let kept = filter_captured_linker_args(&s(&[
"-Wl,-dead_strip",
"-Wl,--gc-sections",
"-arch",
"arm64",
]));
assert_eq!(kept, s(&["-arch", "arm64"]));
}
#[test]
fn filter_keeps_dash_l_with_path_that_starts_with_l() {
let kept = filter_captured_linker_args(&s(&["-llog", "-lstdc++"]));
assert_eq!(kept, s(&["-llog", "-lstdc++"]));
}
#[test]
fn filter_keeps_framework_pairs() {
let kept = filter_captured_linker_args(&s(&[
"-framework",
"Foundation",
"-framework",
"CoreFoundation",
]));
assert_eq!(
kept,
s(&["-framework", "Foundation", "-framework", "CoreFoundation",]),
);
}
#[test]
fn object_detection_covers_common_extensions() {
for path in [
"foo.o",
"foo.rlib",
"foo.a",
"foo.so",
"foo.dylib",
"foo.OBJ",
"foo.LIB", "/abs/path/lib.a",
"rel/dir/foo.o",
] {
assert!(is_object_or_archive_input(path), "{path}");
}
}
#[test]
fn object_detection_rejects_flags_and_non_object_paths() {
for path in [
"-shared",
"-o",
"-Llib",
"-llog",
"/some/source.rs",
"Foundation",
"foo.txt",
"bar",
] {
assert!(!is_object_or_archive_input(path), "{path}");
}
}
#[test]
fn macos_plan_appends_shared_dynamic_lookup_object_and_output() {
let plan = build_link_plan(
&s(&["-isysroot", "/sdk", "-arch", "arm64"]),
Path::new("/o/demo.o"),
Path::new("/o/libdemo.dylib"),
LinkerOs::Macos,
&[],
&[],
);
assert_eq!(
plan.args,
s(&[
"-isysroot",
"/sdk",
"-arch",
"arm64",
"-shared",
"-Wl,-undefined,dynamic_lookup",
"/o/demo.o",
"-o",
"/o/libdemo.dylib",
]),
);
assert_eq!(plan.output, Path::new("/o/libdemo.dylib"));
}
#[test]
fn macos_plan_does_not_double_shared_when_captured_already_had_it() {
let plan = build_link_plan(
&s(&["-shared", "-isysroot", "/sdk"]),
Path::new("/o/demo.o"),
Path::new("/o/libdemo.dylib"),
LinkerOs::Macos,
&[],
&[],
);
let shared_count = plan.args.iter().filter(|a| *a == "-shared").count();
assert_eq!(shared_count, 1, "got args: {:?}", plan.args);
}
#[test]
fn macos_plan_replaces_old_object_inputs_with_just_the_new_one() {
let plan = build_link_plan(
&s(&["/old/a.o", "/old/b.o", "/old/libstd.rlib"]),
Path::new("/new/demo.o"),
Path::new("/new/libdemo.dylib"),
LinkerOs::Macos,
&[],
&[],
);
let mut object_args: Vec<&str> = Vec::new();
let mut i = 0;
while i < plan.args.len() {
if plan.args[i] == "-o" {
i += 2;
continue;
}
if is_object_or_archive_input(&plan.args[i]) {
object_args.push(&plan.args[i]);
}
i += 1;
}
assert_eq!(object_args, vec!["/new/demo.o"]);
}
#[test]
fn macos_plan_replaces_old_output_with_new_one() {
let plan = build_link_plan(
&s(&["-o", "/old/libold.dylib"]),
Path::new("/new/demo.o"),
Path::new("/new/libnew.dylib"),
LinkerOs::Macos,
&[],
&[],
);
let dash_o = plan.args.iter().position(|a| a == "-o").unwrap();
assert_eq!(
plan.args.get(dash_o + 1).map(String::as_str),
Some("/new/libnew.dylib")
);
assert_eq!(plan.args.iter().filter(|a| *a == "-o").count(), 1);
}
#[test]
fn linux_plan_uses_unresolved_symbols_ignore_all_directive() {
let plan = build_link_plan(
&s(&["-pie", "-L", "/ndk/lib"]),
Path::new("/o/demo.o"),
Path::new("/o/libdemo.so"),
LinkerOs::Linux,
&[],
&[],
);
assert_eq!(
plan.args,
s(&[
"-pie",
"-L",
"/ndk/lib",
"-shared",
"-Wl,--unresolved-symbols=ignore-all",
"/o/demo.o",
"-o",
"/o/libdemo.so",
]),
);
}
#[test]
fn linux_plan_drops_pre_existing_unresolved_directive() {
let plan = build_link_plan(
&s(&["-Wl,--unresolved-symbols=ignore-all", "-L", "/ndk/lib"]),
Path::new("/o/demo.o"),
Path::new("/o/libdemo.so"),
LinkerOs::Linux,
&[],
&[],
);
let count = plan
.args
.iter()
.filter(|a| *a == "-Wl,--unresolved-symbols=ignore-all")
.count();
assert_eq!(count, 1, "args: {:?}", plan.args);
}
#[test]
fn linux_plan_appends_extra_objects_before_new_object() {
let stub: std::path::PathBuf = "/o/stub.o".into();
let plan = build_link_plan(
&s(&["-L", "/ndk/lib"]),
Path::new("/o/demo.o"),
Path::new("/o/libdemo.so"),
LinkerOs::Linux,
std::slice::from_ref(&stub),
&[],
);
assert_eq!(
plan.args,
s(&[
"-L",
"/ndk/lib",
"-shared",
"-Wl,--unresolved-symbols=ignore-all",
"/o/stub.o",
"/o/demo.o",
"-o",
"/o/libdemo.so",
]),
);
}
#[test]
fn linux_plan_with_empty_extras_links_only_the_new_object() {
let plan = build_link_plan(
&s(&["-L", "/ndk/lib"]),
Path::new("/o/demo.o"),
Path::new("/o/libdemo.so"),
LinkerOs::Linux,
&[],
&[],
);
assert!(
!plan.args.iter().any(|a| a.contains("--no-as-needed")
|| a.ends_with(".so") && a != "/o/libdemo.so"),
"no host-dylib back-edge expected: {:?}",
plan.args,
);
}
#[test]
fn macos_plan_also_threads_extra_objects() {
let stub: std::path::PathBuf = "/o/stub.o".into();
let plan = build_link_plan(
&s(&["-isysroot", "/sdk"]),
Path::new("/o/demo.o"),
Path::new("/o/libdemo.dylib"),
LinkerOs::Macos,
std::slice::from_ref(&stub),
&[],
);
assert!(
plan.args.iter().any(|a| a == "/o/stub.o"),
"stub object should appear on macOS plan: {:?}",
plan.args,
);
}
#[test]
fn other_os_plan_omits_unresolved_directive() {
let plan = build_link_plan(
&s(&["-machine:x64"]),
Path::new("/o/demo.obj"),
Path::new("/o/demo.dll"),
LinkerOs::Other,
&[],
&[],
);
assert!(
!plan.args.iter().any(|a| a.starts_with("-Wl,")),
"args: {:?}",
plan.args,
);
assert!(plan.args.contains(&"-shared".into()));
assert!(plan.args.iter().any(|a| a.ends_with("demo.obj")));
}
#[test]
fn linker_os_for_host_picks_an_os_consistent_with_cfg() {
let os = linker_os_for_host();
if cfg!(target_os = "macos") || cfg!(target_os = "ios") {
assert_eq!(os, LinkerOs::Macos);
} else if cfg!(target_os = "linux") || cfg!(target_os = "android") {
assert_eq!(os, LinkerOs::Linux);
} else {
assert_eq!(os, LinkerOs::Other);
}
}
}