Repository: davidlattimore/wild (rust)

Issue:
`-v` or `--version` should continue linking if inputs are provided
This one was tricky to diagnose.

CMake during initial configuration tries to find out the compiler's ABI, but that fails with wild: `-- Detecting C compiler ABI info - failed`. Turns out it will try to link the test executable like that: `cc -v -Wl,-v CMakeFiles/cmTC_68e1c.dir/CMakeCCompilerABI.c.o -o cmTC_68e1c` and `-Wl,-v` is the problem here as wild will just print the version and exit.

Minimized version with ld.bfd:
```
❯ rm -f /tmp/wild_test_file && ld -lc -o /tmp/wild_test_file -v && test -e /tmp/wild_test_file && echo "exists" || echo "missing"
GNU ld (GNU Binutils) 2.43.1
ld: warning: cannot find entry symbol _start; not setting start address
exists

❯ rm -f /tmp/wild_test_file && ld /usr/lib/libc.a -o /tmp/wild_test_file -v && test -e /tmp/wild_test_file && echo "exists" || echo "missing"
GNU ld (GNU Binutils) 2.43.1
ld: warning: cannot find entry symbol _start; not setting start address
exists

❯ rm -f /tmp/wild_test_file && ld /usr/lib/libc.a -o /tmp/wild_test_file --version && test -e /tmp/wild_test_file && echo "exists" || echo "missing"
GNU ld (GNU Binutils) 2.43.1
Copyright (C) 2024 Free Software Foundation, Inc.
This program is free software; you may redistribute it under the terms of
the GNU General Public License version 3 or (at your option) a later version.
This program has absolutely no warranty.
missing
```
With wild:
```
❯ rm -f /tmp/wild_test_file && ./ld -lc -o /tmp/wild_test_file -v && test -e /tmp/wild_test_file && echo "exists" || echo "missing"
Wild version 0.3.0 (compatible with GNU linkers)
missing

❯ rm -f /tmp/wild_test_file && ./ld /usr/lib/libc.a -o /tmp/wild_test_file -v && test -e /tmp/wild_test_file && echo "exists" || echo "missing"
Wild version 0.3.0 (compatible with GNU linkers)
missing

# This one is correct
❯ rm -f /tmp/wild_test_file && ./ld /usr/lib/libc.a -o /tmp/wild_test_file --version && test -e /tmp/wild_test_file && echo "exists" || echo "missing"
Wild version 0.3.0 (compatible with GNU linkers)
missing

# Bonus, there is one more discrepancy with ld.bfd; without -v
❯ rm -f /tmp/wild_test_file && ./ld /usr/lib/libc.a -o /tmp/wild_test_file && test -e /tmp/wild_test_file && echo "exists" || echo "missing"
Error: Failed to activate <prelude>

Caused by:
    Missing _start symbol
missing 
```

Basically wild should print the version and proceed with linking when `args` field of `Args` structure is not empty and `-v` was passed.

Pull request:
Continue if input files are present when printing version
Fix #360 

Patch:
diff --git a/libwild/src/args.rs b/libwild/src/args.rs
index 9572d26..f07eed4 100644
--- a/libwild/src/args.rs
+++ b/libwild/src/args.rs
@@ -69,6 +69,7 @@ pub(crate) struct Args {
     pub(crate) print_allocations: Option<FileId>,
     pub(crate) execstack: bool,
     pub(crate) verify_allocation_consistency: bool,
+    pub(crate) should_print_version: bool,
 
     output_kind: Option<OutputKind>,
     is_dynamic_executable: bool,
@@ -83,15 +84,6 @@ pub(crate) enum BuildIdOption {
     Uuid,
 }
 
-#[allow(clippy::large_enum_variant)]
-pub(crate) enum Action {
-    /// The default. Link something.
-    Link(Args),
-
-    /// Print the linker version.
-    Version,
-}
-
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub(crate) enum OutputKind {
     StaticExecutable(RelocationModel),
@@ -247,12 +239,13 @@ impl Default for Args {
             build_id: BuildIdOption::None,
             files_per_group: None,
             no_undefined: false,
+            should_print_version: false,
         }
     }
 }
 
 // Parse the supplied input arguments, which should not include the program name.
-pub(crate) fn parse<S: AsRef<str>, I: Iterator<Item = S>>(mut input: I) -> Result<Action> {
+pub(crate) fn parse<S: AsRef<str>, I: Iterator<Item = S>>(mut input: I) -> Result<Args> {
     let mut args = Args {
         files_per_group: std::env::var(FILES_PER_GROUP_ENV)
             .ok()
@@ -261,8 +254,6 @@ pub(crate) fn parse<S: AsRef<str>, I: Iterator<Item = S>>(mut input: I) -> Resul
         ..Default::default()
     };
 
-    let mut action = None;
-
     let mut unrecognised = Vec::new();
 
     let mut save_dir = SaveDir::new()?;
@@ -487,7 +478,7 @@ pub(crate) fn parse<S: AsRef<str>, I: Iterator<Item = S>>(mut input: I) -> Resul
         } else if let Some(rest) = long_arg_split_prefix("gc-stats-ignore=") {
             args.gc_stats_ignore.push(rest.to_owned());
         } else if long_arg_eq("version") || arg == "-v" {
-            action = Some(Action::Version);
+            args.should_print_version = true;
         } else if long_arg_eq("verbose-gc-stats") {
             args.verbose_gc_stats = true;
         } else if let Some(rest) = long_arg_split_prefix("debug-address=") {
@@ -536,11 +527,7 @@ pub(crate) fn parse<S: AsRef<str>, I: Iterator<Item = S>>(mut input: I) -> Resul
 
     save_dir.finish()?;
 
-    if let Some(a) = action {
-        return Ok(a);
-    }
-
-    Ok(Action::Link(args))
+    Ok(args)
 }
 
 const fn default_target_arch() -> Architecture {
@@ -556,7 +543,7 @@ const fn default_target_arch() -> Architecture {
     }
 }
 
-fn parse_from_argument_file(path: &Path) -> Result<Action> {
+fn parse_from_argument_file(path: &Path) -> Result<Args> {
     let contents = std::fs::read_to_string(path)
         .with_context(|| format!("Failed to read arguments from file `{}`", path.display()))?;
     parse(arguments_from_string(&contents)?.into_iter())
@@ -801,7 +788,6 @@ fn warn_unsupported(opt: &str) -> Result {
 #[cfg(test)]
 mod tests {
     use super::SILENTLY_IGNORED_FLAGS;
-    use crate::args::Action;
     use crate::args::InputSpec;
     use itertools::Itertools;
     use std::num::NonZeroUsize;
@@ -902,6 +888,7 @@ mod tests {
         "--discard-locals",
         "-X",
         "-EL",
+        "-v",
     ];
 
     #[track_caller]
@@ -911,9 +898,7 @@ mod tests {
 
     #[test]
     fn test_parse() {
-        let Action::Link(args) = super::parse(INPUT1.iter()).unwrap() else {
-            panic!("Unexpected action");
-        };
+        let args = super::parse(INPUT1.iter()).unwrap();
         assert!(args.is_relocatable());
         assert_eq!(
             args.inputs
@@ -937,6 +922,7 @@ mod tests {
         );
         assert_eq!(args.soname, Some("bar".to_owned()));
         assert_eq!(args.num_threads, NonZeroUsize::new(1).unwrap());
+        assert!(args.should_print_version);
     }
 
     #[test]
diff --git a/libwild/src/lib.rs b/libwild/src/lib.rs
index fa8547f..90b2f4d 100644
--- a/libwild/src/lib.rs
+++ b/libwild/src/lib.rs
@@ -60,14 +60,12 @@ pub(crate) mod x86_64;
 pub use subprocess::run_in_subprocess;
 
 pub struct Linker {
-    action: args::Action,
+    args: Args,
 }
 
 impl Linker {
     pub fn from_args<S: AsRef<str>, I: Iterator<Item = S>>(args: I) -> error::Result<Self> {
-        Ok(Linker {
-            action: parse(args)?,
-        })
+        Ok(Linker { args: parse(args)? })
     }
 
     pub fn run(&self) -> error::Result {
@@ -80,44 +78,40 @@ impl Linker {
         &self,
         done_closure: Option<Box<dyn FnOnce()>>,
     ) -> error::Result {
-        match &self.action {
-            args::Action::Link(args) => {
-                if args.time_phases {
-                    timing::init_tracing();
-                } else if args.write_trace {
-                    output_trace::init(args);
-                } else if args.print_allocations.is_some() {
-                    debug_trace::init();
-                } else {
-                    tracing_subscriber::registry()
-                        .with(fmt::layer())
-                        .with(EnvFilter::from_default_env())
-                        .init();
-                }
-                match args.arch {
-                    arch::Architecture::X86_64 => {
-                        link::<storage::InMemory, x86_64::X86_64>(args, done_closure)
-                    }
-                    arch::Architecture::AArch64 => {
-                        link::<storage::InMemory, aarch64::AArch64>(args, done_closure)
-                    }
-                }
+        let args = &self.args;
+        if args.time_phases {
+            timing::init_tracing();
+        } else if args.write_trace {
+            output_trace::init(args);
+        } else if args.print_allocations.is_some() {
+            debug_trace::init();
+        } else {
+            tracing_subscriber::registry()
+                .with(fmt::layer())
+                .with(EnvFilter::from_default_env())
+                .init();
+        }
+        if args.should_print_version {
+            println!(
+                "Wild version {} (compatible with GNU linkers)",
+                env!("CARGO_PKG_VERSION")
+            );
+            if args.inputs.is_empty() {
+                return Ok(());
+            }
+        }
+        match args.arch {
+            arch::Architecture::X86_64 => {
+                link::<storage::InMemory, x86_64::X86_64>(args, done_closure)
             }
-            args::Action::Version => {
-                println!(
-                    "Wild version {} (compatible with GNU linkers)",
-                    env!("CARGO_PKG_VERSION")
-                );
-                Ok(())
+            arch::Architecture::AArch64 => {
+                link::<storage::InMemory, aarch64::AArch64>(args, done_closure)
             }
         }
     }
 
     pub fn should_fork(&self) -> bool {
-        match &self.action {
-            args::Action::Link(args) => args.should_fork(),
-            args::Action::Version => false,
-        }
+        self.args.should_fork()
     }
 }
 


Test patch:
diff --git a/wild/tests/sources/trivial.c b/wild/tests/sources/trivial.c
index cd90806..7769947 100644
--- a/wild/tests/sources/trivial.c
+++ b/wild/tests/sources/trivial.c
@@ -2,6 +2,7 @@
 //#ExpectSym: _start .text
 //#ExpectSym: exit_syscall .text
 //#EnableLinker:lld
+//#LinkArgs: -v
 
 #include "exit.h"