Skip to content

Commit

Permalink
feat: add a linkage checker subcommand
Browse files Browse the repository at this point in the history
  • Loading branch information
mistydemeo committed Sep 14, 2023
1 parent 44daca8 commit b4e1fa4
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 2 deletions.
14 changes: 14 additions & 0 deletions cargo-dist/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ pub enum Commands {
/// Generate CI scripts for orchestrating cargo-dist (deprecated in favour of generate)
#[clap(disable_version_flag = true)]
GenerateCi(GenerateCiArgs),
/// Report on the dynamic libraries used by the built artifacts.
#[clap(disable_version_flag = true)]
Linkage(LinkageArgs),
/// Generate the final build manifest without running any builds.
///
/// This command is designed to match the exact behaviour of
Expand Down Expand Up @@ -277,6 +280,17 @@ pub struct GenerateCiArgs {
#[clap(default_value_t = false)]
pub check: bool,
}
#[derive(Args, Clone, Debug)]
pub struct LinkageArgs {
/// Print human-readable output
#[clap(long)]
#[clap(default_value_t = false)]
pub print_output: bool,
/// Print output as JSON
#[clap(long)]
#[clap(default_value_t = false)]
pub print_json: bool,
}

#[derive(Args, Clone, Debug)]
pub struct HelpMarkdownArgs {}
Expand Down
8 changes: 8 additions & 0 deletions cargo-dist/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ pub enum DistError {
/// The missing keys
keys: &'static [&'static str],
},
/// Linkage check can't be run for this combination of OS and target
#[error("unable to run linkage check for {target} on {host}")]
LinkageCheckInvalidOS {
/// The OS the check was run on
host: String,
/// The OS being checked
target: String,
},
}

impl From<minijinja::Error> for DistError {
Expand Down
169 changes: 169 additions & 0 deletions cargo-dist/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use config::{
ArtifactMode, ChecksumStyle, CompressionImpl, Config, DirtyMode, GenerateMode, ZipStyle,
};
use semver::Version;
use serde::Serialize;
use tracing::{info, warn};

use errors::*;
Expand Down Expand Up @@ -617,6 +618,15 @@ pub struct GenerateArgs {
pub modes: Vec<GenerateMode>,
}

/// Arguments for `cargo dist linkage` ([`do_linkage][])
#[derive(Debug)]
pub struct LinkageArgs {
/// Print human-readable output
pub print_output: bool,
/// Print output as JSON
pub print_json: bool,
}

fn do_generate_preflight_checks(dist: &DistGraph) -> Result<()> {
// Enforce cargo-dist-version, unless...
//
Expand Down Expand Up @@ -709,6 +719,165 @@ pub fn run_generate(dist: &DistGraph, args: &GenerateArgs) -> Result<()> {
Ok(())
}

/// Determinage dynamic linkage of built artifacts (impl of `cargo dist linkage`)
pub fn do_linkage(cfg: &Config, args: &LinkageArgs) -> Result<()> {
let dist = gather_work(cfg)?;

let mut reports = vec![];

for target in cfg.targets.clone() {
let releases: Vec<Release> = dist
.releases
.clone()
.into_iter()
.filter(|r| r.targets.contains(&target))
.collect();

if releases.is_empty() {
eprintln!("No matching release for target {target}");
continue;
}

for release in releases {
let path = Utf8PathBuf::from(&dist.dist_dir).join(format!("{}-{target}", release.id));

for (_, binary) in release.bins {
let bin_path = path.join(binary);
if !bin_path.exists() {
eprintln!("Binary {bin_path} missing; skipping check");
} else {
reports.push(determine_linkage(&bin_path, &target)?);
}
}
}
}

if args.print_output {
for report in &reports {
eprintln!("{}", report.report());
}
}
if args.print_json {
let j = serde_json::to_string(&reports).unwrap();
println!("{}", j);
}

Ok(())
}

/// Information about dynamic libraries used by a binary
#[derive(Debug, Serialize)]
pub struct Linkage {
/// The filename of the binary
pub binary: String,
/// The target triple for which the binary was built
pub target: String,
/// Libraries included with the operating system
pub system: Vec<String>,
/// Libraries provided by the Homebrew package manager
pub homebrew: Vec<String>,
/// Public libraries not provided by the system and not managed by any package manager
pub public_unmanaged: Vec<String>,
/// Libraries which don't fall into any other categories
pub other: Vec<String>,
/// Frameworks, only used on macOS
pub frameworks: Vec<String>,
}

impl Linkage {
/// Formatted human-readable output
pub fn report(&self) -> String {
let s = format!(
r#"{} ({}):
System: {}
Homebrew: {}
Public (unmanaged): {}
Frameworks: {}
Other: {}"#,
self.binary,
self.target,
self.system.join(" "),
self.homebrew.join(" "),
self.public_unmanaged.join(" "),
self.frameworks.join(" "),
self.other.join(" "),
);

s.to_owned()
}
}

fn do_otool(path: &Utf8PathBuf) -> DistResult<Vec<String>> {
let output = Command::new("otool")
.arg("-XL")
.arg(path)
.output()
.expect("otool failed to run");

let mut result = String::from_utf8_lossy(&output.stdout).to_string();
if let Some(stripped) = result.strip_suffix('\n') {
result = stripped.to_owned();
}
let libraries: Vec<String> = result
.split('\n')
// Lines are formatted like:
// "/usr/lib/libiconv.2.dylib (compatibility version 7.0.0, current version 7.0.0)"
.map(|line| {
line.trim_start()
.split('(')
.next()
.unwrap()
.trim_end()
.to_string()
})
.collect();

Ok(libraries)
}

fn determine_linkage(path: &Utf8PathBuf, target: &str) -> DistResult<Linkage> {
// Update this as more OSs are supported
if std::env::consts::OS != "macos" {
return Err(DistError::LinkageCheckInvalidOS {
host: std::env::consts::OS.to_owned(),
target: target.to_owned(),
});
}

let libraries = do_otool(path)?;
let mut linkage = Linkage {
binary: path.file_name().unwrap().to_owned(),
target: target.to_owned(),
system: vec![],
homebrew: vec![],
public_unmanaged: vec![],
frameworks: vec![],
other: vec![],
};
for library in libraries {
if library.starts_with("/opt/homebrew") {
linkage.homebrew.push(library.clone());
} else if library.starts_with("/usr/lib") || library.starts_with("/lib") {
linkage.system.push(library.clone());
} else if library.starts_with("/System/Library/Frameworks")
|| library.starts_with("/Library/Frameworks")
{
linkage.frameworks.push(library.clone());
} else if library.starts_with("/usr/local") {
if std::fs::canonicalize(&library)?.starts_with("/usr/local/Cellar") {
linkage.homebrew.push(library.clone());
} else {
linkage.public_unmanaged.push(library.clone());
}
} else {
linkage.other.push(library.clone());
}
}

Ok(linkage)
}

/// Run any necessary integrity checks for "primary" commands like build/plan
///
/// (This is currently equivalent to `cargo dist generate --check`)
Expand Down
24 changes: 23 additions & 1 deletion cargo-dist/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use cli::{
use console::Term;
use miette::IntoDiagnostic;

use crate::cli::{BuildArgs, GenerateArgs, GenerateCiArgs, InitArgs};
use crate::cli::{BuildArgs, GenerateArgs, GenerateCiArgs, InitArgs, LinkageArgs};

mod cli;

Expand All @@ -33,6 +33,7 @@ fn real_main(cli: &axocli::CliApp<Cli>) -> Result<(), miette::Report> {
Commands::Init(args) => cmd_init(config, args),
Commands::Generate(args) => cmd_generate(config, args),
Commands::GenerateCi(args) => cmd_generate_ci(config, args),
Commands::Linkage(args) => cmd_linkage(config, args),
Commands::Manifest(args) => cmd_manifest(config, args),
Commands::Plan(args) => cmd_plan(config, args),
Commands::HelpMarkdown(args) => cmd_help_md(config, args),
Expand Down Expand Up @@ -244,6 +245,27 @@ fn cmd_generate(cli: &Cli, args: &GenerateArgs) -> Result<(), miette::Report> {
do_generate(&config, &args)
}

fn cmd_linkage(cli: &Cli, args: &LinkageArgs) -> Result<(), miette::Report> {
let config = cargo_dist::config::Config {
needs_coherent_announcement_tag: false,
artifact_mode: cargo_dist::config::ArtifactMode::All,
no_local_paths: cli.no_local_paths,
allow_all_dirty: cli.allow_dirty,
targets: cli.target.clone(),
ci: cli.ci.iter().map(|ci| ci.to_lib()).collect(),
installers: cli.installer.iter().map(|ins| ins.to_lib()).collect(),
announcement_tag: cli.tag.clone(),
};
let mut options = cargo_dist::LinkageArgs {
print_output: args.print_output,
print_json: args.print_json,
};
if !args.print_output && !args.print_json {
options.print_output = true;
}
do_linkage(&config, &options)
}

fn cmd_generate_ci(cli: &Cli, args: &GenerateCiArgs) -> Result<(), miette::Report> {
cmd_generate(
cli,
Expand Down
2 changes: 1 addition & 1 deletion cargo-dist/src/tasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ pub struct Symbols {
}

/// A logical release of an application that artifacts are grouped under
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct Release {
/// The name of the app
pub app_name: String,
Expand Down

0 comments on commit b4e1fa4

Please sign in to comment.