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 14bf78f
Show file tree
Hide file tree
Showing 5 changed files with 216 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
170 changes: 170 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,166 @@ 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.len() == 0 {

Check failure on line 736 in cargo-dist/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

length comparison to zero

error: length comparison to zero --> cargo-dist/src/lib.rs:736:12 | 736 | if releases.len() == 0 { | ^^^^^^^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `releases.is_empty()` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#len_zero = note: `-D clippy::len-zero` implied by `-D warnings`

Check failure on line 736 in cargo-dist/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

length comparison to zero

error: length comparison to zero --> cargo-dist/src/lib.rs:736:12 | 736 | if releases.len() == 0 { | ^^^^^^^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `releases.is_empty()` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#len_zero = note: `-D clippy::len-zero` implied by `-D warnings`
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.to_string());
}
}
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 to_string(&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()
}

Check failure on line 808 in cargo-dist/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

implementation of inherent method `to_string(&self) -> String` for type `Linkage`

error: implementation of inherent method `to_string(&self) -> String` for type `Linkage` --> cargo-dist/src/lib.rs:789:5 | 789 | / pub fn to_string(&self) -> String { 790 | | let s = format!( 791 | | r#"{} ({}): 792 | | ... | 807 | | s.to_owned() 808 | | } | |_____^ | = help: implement trait `Display` for type `Linkage` instead = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#inherent_to_string = note: `-D clippy::inherent-to-string` implied by `-D warnings`

Check failure on line 808 in cargo-dist/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

implementation of inherent method `to_string(&self) -> String` for type `Linkage`

error: implementation of inherent method `to_string(&self) -> String` for type `Linkage` --> cargo-dist/src/lib.rs:789:5 | 789 | / pub fn to_string(&self) -> String { 790 | | let s = format!( 791 | | r#"{} ({}): 792 | | ... | 807 | | s.to_owned() 808 | | } | |_____^ | = help: implement trait `Display` for type `Linkage` instead = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#inherent_to_string = note: `-D clippy::inherent-to-string` implied by `-D warnings`
}

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')
.into_iter()

Check failure on line 824 in cargo-dist/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

useless conversion to the same type: `std::str::Split<'_, char>`

error: useless conversion to the same type: `std::str::Split<'_, char>` --> cargo-dist/src/lib.rs:822:34 | 822 | let libraries: Vec<String> = result | __________________________________^ 823 | | .split('\n') 824 | | .into_iter() | |____________________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#useless_conversion = note: `-D clippy::useless-conversion` implied by `-D warnings` help: consider removing `.into_iter()` | 822 ~ let libraries: Vec<String> = result 823 + .split('\n') |

Check failure on line 824 in cargo-dist/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

useless conversion to the same type: `std::str::Split<'_, char>`

error: useless conversion to the same type: `std::str::Split<'_, char>` --> cargo-dist/src/lib.rs:822:34 | 822 | let libraries: Vec<String> = result | __________________________________^ 823 | | .split('\n') 824 | | .into_iter() | |____________________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#useless_conversion = note: `-D clippy::useless-conversion` implied by `-D warnings` help: consider removing `.into_iter()` | 822 ~ let libraries: Vec<String> = result 823 + .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('(')
.nth(0)

Check failure on line 830 in cargo-dist/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

called `.nth(0)` on a `std::iter::Iterator`, when `.next()` is equivalent

error: called `.nth(0)` on a `std::iter::Iterator`, when `.next()` is equivalent --> cargo-dist/src/lib.rs:828:13 | 828 | / line.trim_start() 829 | | .split('(') 830 | | .nth(0) | |_______________________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#iter_nth_zero = note: `-D clippy::iter-nth-zero` implied by `-D warnings` help: try calling `.next()` instead of `.nth(0)` | 828 ~ line.trim_start() 829 + .split('(').next() |

Check failure on line 830 in cargo-dist/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

called `.nth(0)` on a `std::iter::Iterator`, when `.next()` is equivalent

error: called `.nth(0)` on a `std::iter::Iterator`, when `.next()` is equivalent --> cargo-dist/src/lib.rs:828:13 | 828 | / line.trim_start() 829 | | .split('(') 830 | | .nth(0) | |_______________________^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#iter_nth_zero = note: `-D clippy::iter-nth-zero` implied by `-D warnings` help: try calling `.next()` instead of `.nth(0)` | 828 ~ line.trim_start() 829 + .split('(').next() |
.unwrap()
.trim_end()
.to_string()
})
.collect();

Ok(libraries)
}

fn determine_linkage(path: &Utf8PathBuf, target: &String) -> DistResult<Linkage> {

Check failure on line 840 in cargo-dist/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

writing `&String` instead of `&str` involves a new object where a slice will do

error: writing `&String` instead of `&str` involves a new object where a slice will do --> cargo-dist/src/lib.rs:840:50 | 840 | fn determine_linkage(path: &Utf8PathBuf, target: &String) -> DistResult<Linkage> { | ^^^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#ptr_arg = note: `-D clippy::ptr-arg` implied by `-D warnings` help: change this to | 840 ~ fn determine_linkage(path: &Utf8PathBuf, target: &str) -> DistResult<Linkage> { 841 | // Update this as more OSs are supported ... 844 | host: std::env::consts::OS.to_owned(), 845 ~ target: target.to_owned(), 846 | }); ... 851 | binary: path.file_name().unwrap().to_owned(), 852 ~ target: target.to_owned(), |

Check failure on line 840 in cargo-dist/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

writing `&String` instead of `&str` involves a new object where a slice will do

error: writing `&String` instead of `&str` involves a new object where a slice will do --> cargo-dist/src/lib.rs:840:50 | 840 | fn determine_linkage(path: &Utf8PathBuf, target: &String) -> DistResult<Linkage> { | ^^^^^^^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#ptr_arg = note: `-D clippy::ptr-arg` implied by `-D warnings` help: change this to | 840 ~ fn determine_linkage(path: &Utf8PathBuf, target: &str) -> DistResult<Linkage> { 841 | // Update this as more OSs are supported ... 844 | host: std::env::consts::OS.to_owned(), 845 ~ target: target.to_owned(), 846 | }); ... 851 | binary: path.file_name().unwrap().to_owned(), 852 ~ target: target.to_owned(), |
// 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.clone(),
});
}

let libraries = do_otool(&path)?;

Check failure on line 849 in cargo-dist/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

this expression creates a reference which is immediately dereferenced by the compiler

error: this expression creates a reference which is immediately dereferenced by the compiler --> cargo-dist/src/lib.rs:849:30 | 849 | let libraries = do_otool(&path)?; | ^^^^^ help: change this to: `path` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow = note: `-D clippy::needless-borrow` implied by `-D warnings`

Check failure on line 849 in cargo-dist/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

this expression creates a reference which is immediately dereferenced by the compiler

error: this expression creates a reference which is immediately dereferenced by the compiler --> cargo-dist/src/lib.rs:849:30 | 849 | let libraries = do_otool(&path)?; | ^^^^^ help: change this to: `path` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_borrow = note: `-D clippy::needless-borrow` implied by `-D warnings`
let mut linkage = Linkage {
binary: path.file_name().unwrap().to_owned(),
target: target.clone(),
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 == false && args.print_json == false {
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 14bf78f

Please sign in to comment.