From 4e43149228b58dad8dae6df9114eb7557fc5d105 Mon Sep 17 00:00:00 2001 From: Rui Ueyama Date: Sun, 11 Oct 2026 12:29:48 +0900 Subject: [PATCH] Parse the Mach-O linker's options once The Mach-O driver guessed a target, parsed the options for it, and started over with the right target when the guess was wrong, as the ELF driver does. But nothing in the Mach-O option parser depends on the target until it has found the target, from -arch or the input files, so a wrong guess only parsed everything twice and gave the options' warnings twice. parse_args now parses the options once and settles the target, and the driver calls that target's link function with the result, which first resolves what depends on the target (resolve_args). The driver no longer takes an initial target or loops, and a target's link function no longer reports another target or an exit status: a failed link exits in the middle, so it returns nothing. Co-Authored-By: Claude Opus 5.5 --- arch/macho-arm64/lib.rs | 7 ++-- arch/macho-x86_64/lib.rs | 7 ++-- cli/main.rs | 32 ++++++++-------- macho/arch.rs | 4 +- macho/cmdline.rs | 79 ++++++++++++++++++++++------------------ macho/driver.rs | 57 ++++++++--------------------- 6 files changed, 82 insertions(+), 104 deletions(-) diff --git a/arch/macho-arm64/lib.rs b/arch/macho-arm64/lib.rs index f51ca8715..92e9cdfbb 100644 --- a/arch/macho-arm64/lib.rs +++ b/arch/macho-arm64/lib.rs @@ -1,8 +1,7 @@ //! The linker instantiated for arm64. Every target has a crate like this //! one, so that the compiler can build the targets in parallel. -/// Links for this target, or reports the target the inputs are actually -/// for. -pub fn link(cmdline: mold_macho::driver::Cmdline) -> mold_macho::driver::LinkResult { - mold_macho::driver::link::(cmdline) +/// Links for this target. +pub fn link(parsed: mold_macho::driver::ParsedArgs) { + mold_macho::driver::link::(parsed) } diff --git a/arch/macho-x86_64/lib.rs b/arch/macho-x86_64/lib.rs index 5651f750f..e2bc0dcb2 100644 --- a/arch/macho-x86_64/lib.rs +++ b/arch/macho-x86_64/lib.rs @@ -1,8 +1,7 @@ //! The linker instantiated for x86_64. Every target has a crate like this //! one, so that the compiler can build the targets in parallel. -/// Links for this target, or reports the target the inputs are actually -/// for. -pub fn link(cmdline: mold_macho::driver::Cmdline) -> mold_macho::driver::LinkResult { - mold_macho::driver::link::(cmdline) +/// Links for this target. +pub fn link(parsed: mold_macho::driver::ParsedArgs) { + mold_macho::driver::link::(parsed) } diff --git a/cli/main.rs b/cli/main.rs index cb5e40fa1..58b97de2e 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -22,10 +22,12 @@ use std::path::Path; static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; type LinkFn = fn(Cmdline) -> LinkResult; +type MachoLinkFn = fn(mold_macho::driver::ParsedArgs); -// Each target has its own monomorphized link function. Start with the first -// enabled target and switch to the matching function if the inputs are for -// another target. +// Each target has its own monomorphized link function. The ELF linker +// starts with the first enabled target and switches to the matching +// function if the inputs are for another target. The Mach-O linker finds +// the target as it parses the options, and calls its function. const ELF_TARGETS: &[(&str, LinkFn)] = &[ #[cfg(feature = "x86_64")] ("x86_64", mold_elf_x86_64::link), @@ -69,17 +71,17 @@ const ELF_TARGETS: &[(&str, LinkFn)] = &[ ("loongarch32", mold_elf_loongarch32::link), ]; -const MACHO_TARGETS: &[(&str, LinkFn)] = &[ +const MACHO_TARGETS: &[(&str, MachoLinkFn)] = &[ #[cfg(feature = "macho-arm64")] ("arm64", mold_macho_arm64::link), #[cfg(feature = "macho-x86_64")] ("x86_64", mold_macho_x86_64::link), ]; -fn link_for_target(targets: &[(&str, LinkFn)], target: &str, cmdline: Cmdline) -> LinkResult { +fn link_fn(targets: &[(&str, F)], target: &str) -> F { for &(name, link) in targets { if name == target { - return link(cmdline); + return link; } } eprintln!( @@ -97,18 +99,16 @@ fn is_ld64(argv0: &OsStr) -> bool { fn main() { let args: Vec<_> = std::env::args_os().collect(); - let macho = args.first().is_some_and(|argv0| is_ld64(argv0)); - let targets = if macho { MACHO_TARGETS } else { ELF_TARGETS }; + if args.first().is_some_and(|argv0| is_ld64(argv0)) { + let link = |target: &str, parsed| link_fn(MACHO_TARGETS, target)(parsed); + mold_macho::driver::main(args, link); + std::process::exit(0); + } - let Some(&(initial_target, _)) = targets.first() else { + let Some(&(initial_target, _)) = ELF_TARGETS.first() else { eprintln!("mold: no targets enabled; rebuild mold with the appropriate target support"); std::process::exit(1); }; - let link = |target: &str, cmdline| link_for_target(targets, target, cmdline); - let status = if macho { - mold_macho::driver::main(args, initial_target, link) - } else { - mold_elf::driver::main(args, initial_target, link) - }; - std::process::exit(status); + let link = |target: &str, cmdline| link_fn(ELF_TARGETS, target)(cmdline); + std::process::exit(mold_elf::driver::main(args, initial_target, link)); } diff --git a/macho/arch.rs b/macho/arch.rs index 1f3aa709f..76936031b 100644 --- a/macho/arch.rs +++ b/macho/arch.rs @@ -316,8 +316,8 @@ pub trait Target: Copy + Default + Send + Sync + 'static { fn apply_optimization_hints(_ctx: &Context, _buf: &mut [u8]) {} } -/// The canonical name of a target named on the command line, borrowed -/// from static storage so that a restart for that target can carry it. +/// The canonical name of a target named on the command line, if mold +/// links for it. pub fn canonical_name(name: &str) -> Option<&'static str> { [Arm64::NAME, X86_64::NAME].into_iter().find(|&canonical| canonical == name) } diff --git a/macho/cmdline.rs b/macho/cmdline.rs index 24a0e2f8a..f1193ca10 100644 --- a/macho/cmdline.rs +++ b/macho/cmdline.rs @@ -1363,8 +1363,8 @@ fn read_filelist(arg: &OsStr) -> (PathBuf, Vec) { (path.to_path_buf(), files) } -/// What option parsing needs to know about the target: the driver -/// parses once per speculated target, as mold does. +/// What resolving the options needs to know about the target (see +/// resolve_args). pub struct TargetTraits { pub name: &'static str, pub page_size: u64, @@ -1394,16 +1394,16 @@ enum OutputKind { /// the kind of output (None for one not given), the symbol lists, and /// what the checks of the options need. #[derive(Default)] -struct ParseState<'a> { +struct ParseState { kind: OutputKind, /// -target's triple, which overrides -arch and -platform_version. - target_triple: Option<&'a str>, + target_triple: Option, /// The first two platforms the options name that cannot go /// together (see set_platform). incompatible_platforms: Option<(u32, u32)>, arch_variant: bool, /// -no_allow_dylib_sub_type_mismatches's architectures. - dylib_subtype_list: Option<&'a [u8]>, + dylib_subtype_list: Option>, /// Whether -e names the entry point. explicit_entry: bool, pie: Option, @@ -1870,7 +1870,9 @@ fn initial_args() -> Args { } } -/// Parses all options. `cmdline` includes the program name. +/// Parses all options, and settles the target they are for. What depends +/// on the target is left to resolve_args. `cmdline` includes the program +/// name. /// /// Options are matched as bytes and their arguments keep the bytes they /// were given in: paths, install names and rpaths pass through to the @@ -1878,7 +1880,7 @@ fn initial_args() -> Args { /// symbols, segments and sections are bytes, UTF-8 or not, as the /// inputs' are. Arguments that are text by nature (versions, numbers, /// the -undefined treatment) must be UTF-8. -pub fn parse_args(target: &TargetTraits, cmdline: &[Cow<'_, OsStr>]) -> Args { +pub fn parse_args(cmdline: &[Cow<'_, OsStr>]) -> ParsedArgs { let mut args = initial_args(); let mut st = ParseState::default(); @@ -1975,7 +1977,7 @@ pub fn parse_args(target: &TargetTraits, cmdline: &[Cow<'_, OsStr>]) -> Args { .unwrap_or_else(|| fatal!("unknown -arch name: {arch}")), ); } - b"-target" => st.target_triple = Some(cur.next_text(name)), + b"-target" => st.target_triple = Some(cur.next_text(name).to_owned()), b"-platform_version" => read_platform_version(&mut cur, &mut args, &mut st, name), // The minimum versions of macOS and iOS, under their old // names too; ld-prime says the iOS one was renamed. @@ -2002,7 +2004,7 @@ pub fn parse_args(target: &TargetTraits, cmdline: &[Cow<'_, OsStr>]) -> Args { b"-arch_errors_fatal" => args.arch_errors_fatal = true, b"-allow_sub_type_mismatches" => args.allow_sub_type_mismatches = true, b"-no_allow_dylib_sub_type_mismatches" => { - st.dylib_subtype_list = Some(cur.next_arg(name).as_encoded_bytes()); + st.dylib_subtype_list = Some(cur.next_arg(name).as_encoded_bytes().to_vec()); } b"-deployment_target_mismatches" => { args.deployment_target_mismatches = parse_treatment(name, cur.next_arg(name), true); @@ -2629,18 +2631,44 @@ pub fn parse_args(target: &TargetTraits, cmdline: &[Cow<'_, OsStr>]) -> Args { finish_options(&mut args); set_output_kind(&mut args, st.kind); - if let Some(triple) = st.target_triple { + if let Some(triple) = &st.target_triple { apply_target_triple(&mut args, triple); } if args.inputs.is_empty() { exit_without_inputs(&args); } - // A parse for another target than this one is redone by the driver, - // so what depends on the target is left to that parse. - if !resolve_target(target, &mut args) { - return args; + // Without -arch, the first object file names the architecture; + // without -platform_version (or the like), the objects name the + // platform too. + if args.arch.is_none() { + args.arch = Some(crate::reader::detect_machine_type(&args)); } + if args.platform == 0 { + crate::reader::infer_platform(&mut args); + } + check_deployment_target(&args); + ParsedArgs { args, st } +} +/// The command line as parse_args reads it: what the options say, with +/// the target they are for settled, but not yet what depends on the +/// target (see resolve_args). +pub struct ParsedArgs { + args: Args, + st: ParseState, +} + +impl ParsedArgs { + /// The target the link is for. + pub fn arch(&self) -> &'static str { + self.args.arch.unwrap() + } +} + +/// Resolves what depends on the target: the defaults of many options, and +/// the checks of the options against the target and one another. +pub fn resolve_args(target: &TargetTraits, parsed: ParsedArgs) -> Args { + let ParsedArgs { mut args, mut st } = parsed; check_arch_options(target, &mut args, &st); resolve_defaults(target, &mut args, &st); std::mem::take(&mut st.lists).build(&mut args); @@ -2721,27 +2749,6 @@ fn exit_without_inputs(args: &Args) -> ! { fatal!("no object files specified"); } -/// Settles what the image is linked for, and returns whether that is -/// `target`. Without -arch, the first object file names the -/// architecture (see reader::detect_machine_type); without -/// -platform_version (or the like), the parse for the target looks for -/// the platform in the objects too (see reader::infer_platform). mold's -/// driver detects the machine type after the parse, but here the -/// defaults of many options depend on the target and the platform. -fn resolve_target(target: &TargetTraits, args: &mut Args) -> bool { - if args.arch.is_none() { - args.arch = Some(crate::reader::detect_machine_type(args)); - } - if args.arch != Some(target.name) { - return false; - } - if args.platform == 0 { - crate::reader::infer_platform(args); - } - check_deployment_target(args); - true -} - /// Refuses an iOS deployment target older than 4.3, the first iOS to /// slide a main executable, whatever the link's options or objects /// name it by: ld-prime makes every image position-independent, and @@ -2767,7 +2774,7 @@ fn check_arch_options(target: &TargetTraits, args: &mut Args, st: &ParseState) { } let env_subtypes = std::env::var_os("LD_DYLIB_CPU_SUBTYPES_MUST_MATCH"); let env_subtypes = env_subtypes.as_deref().map(OsStr::as_encoded_bytes); - if let Some(list) = st.dylib_subtype_list.or(env_subtypes) { + if let Some(list) = st.dylib_subtype_list.as_deref().or(env_subtypes) { args.dylib_subtypes_must_match = names_cpu_family(list, target.name); } } diff --git a/macho/driver.rs b/macho/driver.rs index 1530bb407..d06dc29df 100644 --- a/macho/driver.rs +++ b/macho/driver.rs @@ -1,9 +1,7 @@ //! The linker driver: runs the passes in order. -use std::borrow::Cow; -use std::ffi::{OsStr, OsString}; +use std::ffi::OsString; use std::ops::Range; -use std::sync::Arc; use rayon::prelude::*; @@ -19,50 +17,26 @@ use crate::passes; use crate::reader; use crate::symbol_moves; -/// The fully expanded command line. -pub type Cmdline = Arc<[Cow<'static, OsStr>]>; +pub use crate::cmdline::ParsedArgs; -/// The exit status of a link, or the name of the target the inputs are -/// actually for. -pub type LinkResult = Result; - -/// Runs the linker with the given command line. Returns the exit status. +/// Runs the linker with the given command line. A link that fails exits +/// in the middle (see mold_common::error::checkpoint). /// -/// `initial_target` is an enabled target used for the initial argument -/// parsing. `link_for_target` links for a named target, or reports the -/// target the inputs are actually for; the executable provides both, as -/// the targets are instantiated in crates of their own. -pub fn main( - argv: Vec, - initial_target: &str, - link_for_target: impl Fn(&str, Cmdline) -> LinkResult, -) -> i32 { - let cmdline: Cmdline = mold_common::response_file::expand_response_files(argv, true).into(); - - // Parse with an enabled target's defaults; if the target turns out to - // be different, start over with the right one. - let mut target = initial_target; - loop { - match link_for_target(target, Arc::clone(&cmdline)) { - Ok(status) => return status, - Err(actual) => target = actual, - } - } +/// `link_for_target` links for a named target; the executable provides +/// it, as the targets are instantiated in crates of their own. +pub fn main(argv: Vec, link_for_target: impl Fn(&str, ParsedArgs)) { + let cmdline = mold_common::response_file::expand_response_files(argv, true); + let parsed = cmdline::parse_args(&cmdline); + link_for_target(parsed.arch(), parsed) } fn target_traits() -> cmdline::TargetTraits { cmdline::TargetTraits { name: E::NAME, page_size: E::PAGE_SIZE } } -/// Links for the target `E`, or reports the target the inputs are -/// actually for. -pub fn link(cmdline: Cmdline) -> LinkResult { - let args = cmdline::parse_args(&target_traits::(), &cmdline); - - // Redo if the target does not match with our speculation. - if args.arch != Some(E::NAME) { - return Err(args.arch.unwrap()); - } +/// Links for the target `E`. +pub fn link(parsed: ParsedArgs) { + let args = cmdline::resolve_args(&target_traits::(), parsed); // Fork so exit latency (unmapping every input) hides behind the // parent's return, as in mold; MOLD_NO_FORK=1 keeps one process @@ -117,7 +91,7 @@ pub fn link(cmdline: Cmdline) -> LinkResult { mold_common::error::checkpoint(); mold_common::subprocess::notify_parent(); drop(t_all); - return Ok(0); + return; } // If there's a bitcode file, do link-time optimization. @@ -181,7 +155,7 @@ pub fn link(cmdline: Cmdline) -> LinkResult { mold_common::subprocess::notify_parent(); drop(t_all); passes::show_stats(&ctx); - return Ok(0); + return; } // Merge the initializers, literals and Objective-C references, and @@ -375,7 +349,6 @@ pub fn link(cmdline: Cmdline) -> LinkResult { mold_common::subprocess::notify_parent(); drop(t_all); passes::show_stats(&ctx); - Ok(0) } /// Copies all chunks to the output buffer and applies relocations, in