Skip to main content

cargo_binutils/
lib.rs

1use std::io::{self, BufReader, Write};
2use std::path::Path;
3use std::process::{Command, Stdio};
4use std::{env, str};
5
6use anyhow::{bail, Result};
7use cargo_metadata::camino::Utf8Component;
8use cargo_metadata::{Artifact, CargoOpt, Message, Metadata, MetadataCommand};
9use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand};
10use rustc_cfg::Cfg;
11
12pub use tool::Tool;
13
14mod llvm;
15mod postprocess;
16mod rustc;
17mod tool;
18
19/// Search for `file` in `path` and its parent directories
20fn search<'p>(path: &'p Path, file: &str) -> Option<&'p Path> {
21    path.ancestors().find(|dir| dir.join(file).exists())
22}
23
24fn parse<T>(path: &Path) -> Result<T>
25where
26    T: for<'de> serde::Deserialize<'de>,
27{
28    use std::fs::File;
29    use std::io::Read;
30    use toml::de;
31
32    let mut s = String::new();
33    File::open(path)?.read_to_string(&mut s)?;
34    Ok(de::from_str(&s)?)
35}
36
37/// Execution context
38// TODO this should be some sort of initialize once, read-only singleton
39pub struct Context {
40    cfg: Cfg,
41    /// Final compilation target
42    target: String,
43}
44
45impl Context {
46    /* Constructors */
47    /// Get a context structure from a built artifact.
48    fn from_artifact(metadata: Metadata, artifact: &Artifact) -> Result<Self> {
49        // Currently there is no clean way to get the target triple from cargo so we can only make
50        // an approximation, we do this by extracting the target triple from the artifacts path.
51        // For more info on the path structure see: https://doc.rust-lang.org/cargo/guide/build-cache.html
52
53        // In the future it may be possible to replace this code and use a cargo feature:
54        // See: https://github.com/rust-lang/cargo/issues/5579, https://github.com/rust-lang/cargo/issues/8002
55
56        // Should always succeed.
57        let target_path = artifact.filenames[0].strip_prefix(metadata.target_directory)?;
58        let target_name = if let Some(Utf8Component::Normal(path)) = target_path.components().next()
59        {
60            // TODO: How will custom profiles impact this?
61            if path == "debug" || path == "release" {
62                // Looks like this artifact was built for the host.
63                rustc_version::version_meta()?.host
64            } else {
65                // The artifact
66                path.to_string()
67            }
68        } else {
69            unreachable!();
70        };
71
72        Self::from_target_name(&target_name)
73    }
74
75    /// Get a context structure from a provided target flag, used when cargo
76    /// was not used to build the binary.
77    fn from_flag(metadata: Metadata, target_flag: Option<&str>) -> Result<Self> {
78        let host_target_name = rustc_version::version_meta()?.host;
79
80        // Get the "default" target override in .cargo/config.
81        let mut config_target_name = None;
82        let config: toml::Value;
83
84        if let Some(path) = search(metadata.workspace_root.as_std_path(), ".cargo/config") {
85            config = parse(&path.join(".cargo/config"))?;
86            config_target_name = config
87                .get("build")
88                .and_then(|build| build.get("target"))
89                .and_then(|target| target.as_str());
90        }
91
92        // Find the actual target.
93        let target_name = target_flag
94            .or(config_target_name)
95            .unwrap_or(&host_target_name);
96
97        Self::from_target_name(target_name)
98    }
99
100    fn from_target_name(target_name: &str) -> Result<Self> {
101        let cfg = Cfg::of(target_name)?;
102
103        Ok(Context {
104            cfg,
105            target: target_name.to_string(),
106        })
107    }
108}
109
110enum BuildType<'a> {
111    Any,
112    Bin(&'a str),
113    Example(&'a str),
114    Test(&'a str),
115    Bench(&'a str),
116    Lib,
117}
118
119impl BuildType<'_> {
120    fn matches(&self, artifact: &Artifact) -> bool {
121        match self {
122            BuildType::Bin(target_name)
123            | BuildType::Example(target_name)
124            | BuildType::Test(target_name)
125            | BuildType::Bench(target_name) => {
126                artifact.target.name == *target_name && artifact.executable.is_some()
127            }
128            // For info about 'kind' values see:
129            // https://github.com/rust-lang/cargo/blob/d47a9545db81fe6d7e6c542bc8154f09d0e6c788/src/cargo/core/manifest.rs#L166-L181
130            // The only "Any" artifacts we can support are bins and examples, so let's make sure
131            // no-one slips us a "custom-build" in form of a build.rs in
132            BuildType::Any => artifact
133                .target
134                .kind
135                .iter()
136                .any(|s| s == "bin" || s == "example"),
137            // Since LibKind can be an arbitrary string `LibKind:Other(String)` we filter by what it can't be
138            BuildType::Lib => artifact.target.kind.iter().any(|s| {
139                s != "bin" && s != "example" && s != "test" && s != "custom-build" && s != "bench"
140            }),
141        }
142    }
143}
144
145fn args(tool: Tool, examples: Option<&str>) -> ArgMatches {
146    let name = tool.name();
147    let about = format!("Proxy for the `llvm-{name}` tool shipped with the Rust toolchain.");
148    let after_help = format!(
149        "\
150The arguments specified *after* the `--` will be passed to the proxied tool invocation.
151
152To see all the flags the proxied tool accepts run `cargo-{} -- --help`.{}",
153        name,
154        examples.unwrap_or("")
155    );
156
157    let app = ClapCommand::new(format!("cargo-{name}"))
158        .about(about)
159        .version(env!("CARGO_PKG_VERSION"))
160        // as this is used as a Cargo subcommand the first argument will be the name of the binary
161        // we ignore this argument
162        .args(&[
163            Arg::new("binary-name").hide(true),
164            Arg::new("verbose")
165                .long("verbose")
166                .short('v')
167                .action(ArgAction::Count)
168                .help("Use verbose output (-vv cargo verbose or -vvv for build.rs output)"),
169            Arg::new("args")
170                .last(true)
171                .num_args(1..)
172                .help("The arguments to be proxied to the tool"),
173        ])
174        .after_help(after_help);
175
176    if tool.needs_build() {
177        app.args(&[
178            Arg::new("quiet")
179                .long("quiet")
180                .short('q')
181                .action(ArgAction::SetTrue)
182                .help("Don't print build output from `cargo build`"),
183            Arg::new("package")
184                .long("package")
185                .short('p')
186                .value_name("SPEC")
187                .help("Package to build (see `cargo help pkgid`)"),
188            Arg::new("jobs")
189                .long("jobs")
190                .short('j')
191                .value_name("N")
192                .help("Number of parallel jobs, defaults to # of CPUs"),
193            Arg::new("lib")
194                .long("lib")
195                .action(ArgAction::SetTrue)
196                .conflicts_with_all(["bin", "example", "test", "bench"])
197                .help("Build only this package's library"),
198            Arg::new("bin")
199                .long("bin")
200                .value_name("NAME")
201                .conflicts_with_all(["lib", "example", "test", "bench"])
202                .help("Build only the specified binary"),
203            Arg::new("example")
204                .long("example")
205                .value_name("NAME")
206                .conflicts_with_all(["lib", "bin", "test", "bench"])
207                .help("Build only the specified example"),
208            Arg::new("test")
209                .long("test")
210                .value_name("NAME")
211                .conflicts_with_all(["lib", "bin", "example", "bench"])
212                .help("Build only the specified test target"),
213            Arg::new("bench")
214                .long("bench")
215                .value_name("NAME")
216                .conflicts_with_all(["lib", "bin", "example", "test"])
217                .help("Build only the specified bench target"),
218            Arg::new("release")
219                .long("release")
220                .action(ArgAction::SetTrue)
221                .help("Build artifacts in release mode, with optimizations"),
222            Arg::new("profile")
223                .long("profile")
224                .value_name("PROFILE-NAME")
225                .help("Build artifacts with the specified profile"),
226            Arg::new("manifest-path")
227                .long("manifest-path")
228                .help("Path to Cargo.tom"),
229            Arg::new("features")
230                .long("features")
231                .short('F')
232                .value_name("FEATURES")
233                .help("Space-separated list of features to activate"),
234            Arg::new("all-features")
235                .long("all-features")
236                .action(ArgAction::SetTrue)
237                .help("Activate all available features"),
238            Arg::new("no-default-features")
239                .long("no-default-features")
240                .action(ArgAction::SetTrue)
241                .help("Do not activate the `default` feature"),
242            Arg::new("target")
243                .long("target")
244                .value_name("TRIPLE")
245                .help("Target triple for which the code is compiled"),
246            Arg::new("config")
247                .long("config")
248                .value_name("CONFIG")
249                .help("Override a configuration value"),
250            Arg::new("color")
251                .long("color")
252                .action(ArgAction::Set)
253                .value_parser(clap::builder::PossibleValuesParser::new([
254                    "auto", "always", "never",
255                ]))
256                .help("Coloring: auto, always, never"),
257            Arg::new("frozen")
258                .long("frozen")
259                .action(ArgAction::SetTrue)
260                .help("Require Cargo.lock and cache are up to date"),
261            Arg::new("locked")
262                .long("locked")
263                .action(ArgAction::SetTrue)
264                .help("Require Cargo.lock is up to date"),
265            Arg::new("offline")
266                .long("offline")
267                .action(ArgAction::SetTrue)
268                .help("Run without accessing the network"),
269            Arg::new("unstable-features")
270                .short('Z')
271                .action(ArgAction::Append)
272                .value_name("FLAG")
273                .help("Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details"),
274        ])
275        .get_matches()
276    } else {
277        app.get_matches()
278    }
279}
280
281pub fn run(tool: Tool, matches: ArgMatches) -> Result<i32> {
282    let mut metadata_command = MetadataCommand::new();
283    if let Some(features) = matches.get_many::<String>("features") {
284        metadata_command.features(CargoOpt::SomeFeatures(
285            features.map(|s| s.to_owned()).collect(),
286        ));
287    }
288    if matches.get_flag("no-default-features") {
289        metadata_command.features(CargoOpt::NoDefaultFeatures);
290    }
291    if matches.get_flag("all-features") {
292        metadata_command.features(CargoOpt::AllFeatures);
293    }
294    if let Some(path) = matches.get_one::<String>("manifest-path") {
295        metadata_command.manifest_path(path);
296    }
297    let metadata = metadata_command.exec()?;
298    if metadata.workspace_members.is_empty() {
299        bail!("Unable to find workspace members");
300    }
301
302    let mut tool_args = vec![];
303    if let Some(args) = matches.get_many::<String>("args") {
304        tool_args.extend(args.map(|s| s.as_str()));
305    }
306
307    let tool_help = tool_args.first() == Some(&"--help");
308
309    let target_artifact = if tool.needs_build() && !tool_help {
310        cargo_build(&matches, &metadata)?
311    } else {
312        None
313    };
314
315    let mut lltool = Command::new(format!("rust-{}", tool.name()));
316
317    if tool == Tool::Objdump {
318        let ctxt = if let Some(artifact) = &target_artifact {
319            Context::from_artifact(metadata, artifact)?
320        } else {
321            Context::from_flag(
322                metadata,
323                matches.get_one::<String>("target").map(|s| s.as_str()),
324            )?
325        };
326
327        let arch_name = llvm::arch_name(&ctxt.cfg, &ctxt.target);
328
329        if arch_name == "thumb" {
330            // `-arch-name=thumb` doesn't produce the right output so instead we pass
331            // `-triple=$target`, which contains more information about the target
332            lltool.args(["--triple", &ctxt.target]);
333        } else {
334            lltool.args(&[format!("--arch-name={arch_name}")]);
335        }
336    }
337
338    // Extra flags
339    if let Tool::Readobj = tool {
340        // The default output style of `readobj` is JSON-like, which is not user friendly, so we
341        // change it to the human readable GNU style
342        lltool.arg("--elf-output-style=GNU");
343    }
344
345    if tool.needs_build() {
346        // Artifact
347        if let Some(artifact) = &target_artifact {
348            let file = match &artifact.executable {
349                // Example and bins have an executable
350                Some(val) => val,
351                // Libs have an rlib and an rmeta. We want the rlib, which always
352                // comes first in the filenames array after some quick testing.
353                //
354                // We could instead look for files ending in .rlib, but that would
355                // fail for cdylib and other fancy crate kinds.
356                None => &artifact.filenames[0],
357            };
358
359            match tool {
360                // Tools that don't need a build
361                Tool::Ar | Tool::As | Tool::Cov | Tool::Lld | Tool::Profdata => {}
362                // for some tools we change the CWD (current working directory) and
363                // make the artifact path relative. This makes the path that the
364                // tool will print easier to read. e.g. `libfoo.rlib` instead of
365                // `/home/user/rust/project/target/$T/debug/libfoo.rlib`.
366                Tool::Objdump | Tool::Nm | Tool::Readobj | Tool::Size => {
367                    lltool
368                        .current_dir(file.parent().unwrap())
369                        .arg(file.file_name().unwrap());
370                }
371                Tool::Objcopy | Tool::Strip => {
372                    lltool.arg(file);
373                }
374            }
375        }
376    }
377
378    // User flags
379    lltool.args(&tool_args);
380
381    if matches.get_count("verbose") > 0 {
382        eprintln!("{lltool:?}");
383    }
384
385    let stdout = io::stdout();
386    let mut stdout = stdout.lock();
387
388    let output = lltool.stderr(Stdio::inherit()).output()?;
389
390    // post process output
391    let processed_output = match tool {
392        Tool::Ar
393        | Tool::As
394        | Tool::Cov
395        | Tool::Lld
396        | Tool::Objcopy
397        | Tool::Profdata
398        | Tool::Strip => output.stdout.into(),
399        Tool::Nm | Tool::Objdump | Tool::Readobj => postprocess::demangle(&output.stdout),
400        Tool::Size => postprocess::size(&output.stdout),
401    };
402
403    stdout.write_all(&processed_output)?;
404
405    if output.status.success() {
406        Ok(0)
407    } else {
408        Ok(output.status.code().unwrap_or(1))
409    }
410}
411
412fn cargo_build(matches: &ArgMatches, metadata: &Metadata) -> Result<Option<Artifact>> {
413    let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
414    let mut cargo = Command::new(cargo);
415    cargo.arg("build");
416
417    let (build_type, verbose) = cargo_build_args(matches, &mut cargo);
418    let quiet = matches.get_flag("quiet");
419
420    cargo.arg("--message-format=json-diagnostic-rendered-ansi");
421    cargo.stdout(Stdio::piped());
422
423    if verbose > 0 {
424        eprintln!("{cargo:?}");
425    }
426
427    let mut child = cargo.spawn()?;
428    let stdout = BufReader::new(child.stdout.take().expect("Pipe to cargo process failed"));
429
430    // Note: We call `collect` to ensure we don't block stdout which could prevent the process from exiting
431    let messages = Message::parse_stream(stdout).collect::<Vec<_>>();
432
433    let status = child.wait()?;
434
435    let mut target_artifact: Option<Artifact> = None;
436    for message in messages {
437        match message? {
438            Message::CompilerArtifact(artifact) => {
439                if metadata.workspace_members.contains(&artifact.package_id)
440                    && build_type.matches(&artifact)
441                {
442                    if target_artifact.is_some() {
443                        bail!("Can only have one matching artifact but found several");
444                    }
445
446                    target_artifact = Some(artifact);
447                }
448            }
449            Message::CompilerMessage(msg) => {
450                if !quiet || verbose > 1 {
451                    if let Some(rendered) = msg.message.rendered {
452                        print!("{rendered}");
453                    }
454                }
455            }
456            _ => (),
457        }
458    }
459
460    if !status.success() {
461        bail!("Failed to parse crate metadata");
462    }
463
464    if target_artifact.is_none() {
465        bail!("Could not determine the wanted artifact");
466    }
467
468    Ok(target_artifact)
469}
470
471fn cargo_build_args<'a>(matches: &'a ArgMatches, cargo: &mut Command) -> (BuildType<'a>, u64) {
472    if matches.get_flag("quiet") {
473        cargo.arg("--quiet");
474    }
475
476    if let Some(package) = matches.get_one::<String>("package") {
477        cargo.arg("--package");
478        cargo.arg(package);
479    }
480
481    if let Some(config) = matches.get_many::<String>("config") {
482        for c in config {
483            cargo.args(["--config", c]);
484        }
485    }
486
487    if let Some(jobs) = matches.get_one::<String>("jobs") {
488        cargo.arg("-j");
489        cargo.arg(jobs);
490    }
491
492    let build_type = if matches.get_flag("lib") {
493        cargo.args(["--lib"]);
494        BuildType::Lib
495    } else if let Some(bin_name) = matches.get_one::<String>("bin") {
496        cargo.args(["--bin", bin_name]);
497        BuildType::Bin(bin_name)
498    } else if let Some(example_name) = matches.get_one::<String>("example") {
499        cargo.args(["--example", example_name]);
500        BuildType::Example(example_name)
501    } else if let Some(test_name) = matches.get_one::<String>("test") {
502        cargo.args(["--test", test_name]);
503        BuildType::Test(test_name)
504    } else if let Some(bench_name) = matches.get_one::<String>("bench") {
505        cargo.args(["--bench", bench_name]);
506        BuildType::Bench(bench_name)
507    } else {
508        BuildType::Any
509    };
510
511    if matches.get_flag("release") {
512        cargo.arg("--release");
513    }
514
515    if let Some(profile) = matches.get_one::<String>("profile") {
516        cargo.arg("--profile");
517        cargo.arg(profile);
518    }
519
520    if let Some(manifest_path) = matches.get_one::<String>("manifest-path") {
521        cargo.args(["--manifest-path", manifest_path]);
522    }
523
524    if let Some(features) = matches.get_many::<String>("features") {
525        for feature in features {
526            cargo.args(["--features", feature]);
527        }
528    }
529    if matches.get_flag("no-default-features") {
530        cargo.arg("--no-default-features");
531    }
532    if matches.get_flag("all-features") {
533        cargo.arg("--all-features");
534    }
535
536    // NOTE we do *not* use `project.target()` here because Cargo will figure things out on
537    // its own (i.e. it will search and parse .cargo/config, etc.)
538    if let Some(target) = matches.get_one::<String>("target") {
539        cargo.args(["--target", target]);
540    }
541
542    let verbose = matches.get_count("verbose") as u64;
543    if verbose > 1 {
544        cargo.arg(format!("-{}", "v".repeat((verbose - 1) as usize)));
545    }
546
547    if let Some(color) = matches.get_one::<String>("color") {
548        cargo.arg("--color");
549        cargo.arg(color);
550    }
551
552    if matches.get_flag("frozen") {
553        cargo.arg("--frozen");
554    }
555
556    if matches.get_flag("locked") {
557        cargo.arg("--locked");
558    }
559
560    if matches.get_flag("offline") {
561        cargo.arg("--offline");
562    }
563
564    if let Some(unstable_features) = matches.get_many::<String>("unstable-features") {
565        for unstable_feature in unstable_features {
566            cargo.args(["-Z", unstable_feature]);
567        }
568    }
569
570    (build_type, verbose)
571}