1use std::{
4 fs::{self, File},
5 io::{Cursor, Read},
6};
7
8use axoasset::SourceFile;
9use axoprocess::Cmd;
10use camino::Utf8PathBuf;
11use cargo_dist_schema::{
12 AssetInfo, BuildEnvironment, DistManifest, GlibcVersion, Library, Linkage, PackageManager,
13 TripleNameRef,
14};
15use comfy_table::{presets::UTF8_FULL, Table};
16use goblin::Object;
17use mach_object::{LoadCommand, OFile};
18use tracing::warn;
19
20use crate::{config::Config, errors::*, gather_work, Artifact, DistGraph};
21
22#[derive(Debug)]
24pub struct LinkageArgs {
25 pub print_output: bool,
27 pub print_json: bool,
29 pub from_json: Option<String>,
31}
32
33pub fn do_linkage(cfg: &Config, args: &LinkageArgs) -> DistResult<()> {
35 let manifest = if let Some(target) = args.from_json.clone() {
36 let file = SourceFile::load_local(target)?;
37 file.deserialize_json()?
38 } else {
39 let (dist, mut manifest) = gather_work(cfg)?;
40 compute_linkage_assuming_local_build(&dist, &mut manifest, cfg)?;
41 manifest
42 };
43
44 if args.print_output {
45 eprintln!("{}", LinkageDisplay(&manifest));
46 }
47 if args.print_json {
48 let string = serde_json::to_string_pretty(&manifest).unwrap();
49 println!("{string}");
50 }
51 Ok(())
52}
53
54fn compute_linkage_assuming_local_build(
57 dist: &DistGraph,
58 manifest: &mut DistManifest,
59 cfg: &Config,
60) -> DistResult<()> {
61 let targets = &cfg.targets;
62 let artifacts = &dist.artifacts;
63 let dist_dir = &dist.dist_dir;
64
65 for target in targets {
66 let artifacts: Vec<Artifact> = artifacts
67 .clone()
68 .into_iter()
69 .filter(|r| r.target_triples.contains(target))
70 .collect();
71
72 if artifacts.is_empty() {
73 eprintln!("No matching artifact for target {target}");
74 continue;
75 }
76
77 for artifact in artifacts {
78 let path = Utf8PathBuf::from(&dist_dir).join(format!("{}-{target}", artifact.id));
79
80 for (bin_idx, binary_relpath) in artifact.required_binaries {
81 let bin = dist.binary(bin_idx);
82 let bin_path = path.join(binary_relpath);
83 if !bin_path.exists() {
84 eprintln!("Binary {bin_path} missing; skipping check");
85 } else {
86 let linkage = determine_linkage(&bin_path, target);
87 manifest.assets.insert(
88 bin.id.clone(),
89 AssetInfo {
90 id: bin.id.clone(),
91 name: bin.name.clone(),
92 system: dist.system_id.clone(),
93 linkage: Some(linkage),
94 target_triples: vec![target.clone()],
95 },
96 );
97 }
98 }
99 }
100 }
101
102 Ok(())
103}
104
105pub struct LinkageDisplay<'a>(pub &'a DistManifest);
107
108impl std::fmt::Display for LinkageDisplay<'_> {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 for asset in self.0.assets.values() {
111 let Some(linkage) = &asset.linkage else {
112 continue;
113 };
114 let name = &asset.name;
115 let targets = asset.target_triples.join(", ");
116 write!(f, "{name}")?;
117 if !targets.is_empty() {
118 write!(f, " ({targets})")?;
119 }
120 writeln!(f, "\n")?;
121 format_linkage_table(f, linkage)?;
122 }
123 Ok(())
124 }
125}
126
127fn format_linkage_table(f: &mut std::fmt::Formatter<'_>, linkage: &Linkage) -> std::fmt::Result {
129 let mut table = Table::new();
130 table
131 .load_preset(UTF8_FULL)
132 .set_header(vec!["Category", "Libraries"])
133 .add_row(vec![
134 "System",
135 linkage
136 .system
137 .clone()
138 .into_iter()
139 .map(|l| l.to_string())
140 .collect::<Vec<String>>()
141 .join("\n")
142 .as_str(),
143 ])
144 .add_row(vec![
145 "Homebrew",
146 linkage
147 .homebrew
148 .clone()
149 .into_iter()
150 .map(|l| l.to_string())
151 .collect::<Vec<String>>()
152 .join("\n")
153 .as_str(),
154 ])
155 .add_row(vec![
156 "Public (unmanaged)",
157 linkage
158 .public_unmanaged
159 .clone()
160 .into_iter()
161 .map(|l| l.path)
162 .collect::<Vec<String>>()
163 .join("\n")
164 .as_str(),
165 ])
166 .add_row(vec![
167 "Frameworks",
168 linkage
169 .frameworks
170 .clone()
171 .into_iter()
172 .map(|l| l.path)
173 .collect::<Vec<String>>()
174 .join("\n")
175 .as_str(),
176 ])
177 .add_row(vec![
178 "Other",
179 linkage
180 .other
181 .clone()
182 .into_iter()
183 .map(|l| l.to_string())
184 .collect::<Vec<String>>()
185 .join("\n")
186 .as_str(),
187 ]);
188 write!(f, "{table}")
189}
190
191pub fn library_from_homebrew(library: String) -> Library {
193 let brew_prefix = if library.starts_with("/opt/homebrew/opt/") {
196 Some("/opt/homebrew/opt/")
197 } else if library.starts_with("/usr/local/opt/") {
198 Some("/usr/local/opt/")
199 } else {
200 None
201 };
202
203 if let Some(prefix) = brew_prefix {
204 let cloned = library.clone();
205 let stripped = cloned.strip_prefix(prefix).unwrap();
206 let mut package = stripped.split('/').next().unwrap().to_owned();
207
208 let receipt = Utf8PathBuf::from(&prefix)
212 .join(&package)
213 .join("INSTALL_RECEIPT.json");
214
215 if receipt.exists() {
219 let _ = SourceFile::load_local(&receipt)
220 .and_then(|file| file.deserialize_json())
221 .map(|parsed: serde_json::Value| {
222 if let Some(tap) = parsed["source"]["tap"].as_str() {
223 if tap != "homebrew/core" {
224 package = format!("{tap}/{package}");
225 }
226 }
227 });
228 }
229
230 Library {
231 path: library,
232 source: Some(package.to_owned()),
233 package_manager: Some(PackageManager::Homebrew),
234 }
235 } else {
236 Library {
237 path: library,
238 source: None,
239 package_manager: None,
240 }
241 }
242}
243
244pub fn library_from_apt(library: String) -> DistResult<Library> {
246 if std::env::consts::OS != "linux" {
248 return Ok(Library {
249 path: library,
250 source: None,
251 package_manager: None,
252 });
253 }
254
255 let process = Cmd::new("dpkg", "get linkage info from dpkg")
256 .arg("--search")
257 .arg(&library)
258 .output();
259 match process {
260 Ok(output) => {
261 let output = String::from_utf8(output.stdout)?;
262
263 let package = output.split(':').next().unwrap();
264 let source = if package.is_empty() {
265 None
266 } else {
267 Some(package.to_owned())
268 };
269 let package_manager = if source.is_some() {
270 Some(PackageManager::Apt)
271 } else {
272 None
273 };
274
275 Ok(Library {
276 path: library,
277 source,
278 package_manager,
279 })
280 }
281 Err(_) => Ok(Library {
283 path: library,
284 source: None,
285 package_manager: None,
286 }),
287 }
288}
289
290fn do_otool(path: &Utf8PathBuf) -> DistResult<Vec<String>> {
291 let mut libraries = vec![];
292
293 let mut f = File::open(path)?;
294 let mut buf = vec![];
295 let size = f.read_to_end(&mut buf).unwrap();
296 let mut cur = Cursor::new(&buf[..size]);
297 if let Ok(OFile::MachFile {
298 header: _,
299 commands,
300 }) = OFile::parse(&mut cur)
301 {
302 let commands = commands
303 .iter()
304 .map(|load| load.command())
305 .cloned()
306 .collect::<Vec<LoadCommand>>();
307
308 for command in commands {
309 match command {
310 LoadCommand::IdDyLib(ref dylib)
311 | LoadCommand::LoadDyLib(ref dylib)
312 | LoadCommand::LoadWeakDyLib(ref dylib)
313 | LoadCommand::ReexportDyLib(ref dylib)
314 | LoadCommand::LoadUpwardDylib(ref dylib)
315 | LoadCommand::LazyLoadDylib(ref dylib) => {
316 libraries.push(dylib.name.to_string());
317 }
318 _ => {}
319 }
320 }
321 }
322
323 Ok(libraries)
324}
325
326fn do_ldd(path: &Utf8PathBuf) -> DistResult<Vec<String>> {
327 let mut libraries = vec![];
328
329 let output = Cmd::new("ldd", "get linkage info from ldd")
334 .arg(path)
335 .check(false)
336 .output()?;
337
338 let result = String::from_utf8_lossy(&output.stdout).to_string();
339 let lines = result.trim_end().split('\n');
340
341 for line in lines {
342 let line = line.trim();
343
344 if line.starts_with("not a dynamic executable") || line.starts_with("statically linked") {
347 break;
348 }
349
350 if line.starts_with("linux-vdso") {
352 continue;
353 }
354
355 if let Some(path) = line.split(" => ").nth(1) {
357 let lib = (path.split(' ').next().unwrap()).to_owned();
361 let realpath = fs::canonicalize(&lib)?;
362 libraries.push(realpath.to_string_lossy().to_string());
363 } else {
364 continue;
365 }
366 }
367
368 Ok(libraries)
369}
370
371fn do_pe(path: &Utf8PathBuf) -> DistResult<Vec<String>> {
372 let buf = std::fs::read(path)?;
373 match Object::parse(&buf)? {
374 Object::PE(pe) => Ok(pe.libraries.into_iter().map(|s| s.to_owned()).collect()),
375 Object::Archive(_) => Ok(vec![]),
377 _ => Err(DistError::LinkageCheckUnsupportedBinary),
378 }
379}
380
381pub fn determine_linkage(path: &Utf8PathBuf, target: &TripleNameRef) -> Linkage {
385 match try_determine_linkage(path, target) {
386 Ok(linkage) => linkage,
387 Err(e) => {
388 warn!("Skipping linkage for {path}:\n{:?}", miette::Report::new(e));
389 Linkage::default()
390 }
391 }
392}
393
394fn try_determine_linkage(path: &Utf8PathBuf, target: &TripleNameRef) -> DistResult<Linkage> {
396 let libraries = if target.is_darwin() {
397 do_otool(path)?
398 } else if target.is_linux() {
399 if std::env::consts::OS != "linux" {
401 return Err(DistError::LinkageCheckInvalidOS {
402 host: std::env::consts::OS.to_owned(),
403 target: target.to_owned(),
404 });
405 }
406 do_ldd(path)?
407 } else if target.is_windows() {
408 do_pe(path)?
409 } else {
410 return Err(DistError::LinkageCheckUnsupportedBinary);
411 };
412
413 let mut linkage = Linkage {
414 system: Default::default(),
415 homebrew: Default::default(),
416 public_unmanaged: Default::default(),
417 frameworks: Default::default(),
418 other: Default::default(),
419 };
420 for library in libraries {
421 if library.starts_with("/opt/homebrew") {
422 linkage
423 .homebrew
424 .insert(library_from_homebrew(library.clone()));
425 } else if library.starts_with("/usr/lib") || library.starts_with("/lib") {
426 linkage.system.insert(library_from_apt(library.clone())?);
427 } else if library.starts_with("/System/Library/Frameworks")
428 || library.starts_with("/Library/Frameworks")
429 {
430 linkage.frameworks.insert(Library::new(library.clone()));
431 } else if library.starts_with("/usr/local") {
432 if std::fs::canonicalize(&library)?.starts_with("/usr/local/Cellar") {
433 linkage
434 .homebrew
435 .insert(library_from_homebrew(library.clone()));
436 } else {
437 linkage
438 .public_unmanaged
439 .insert(Library::new(library.clone()));
440 }
441 } else {
442 linkage.other.insert(library_from_apt(library.clone())?);
443 }
444 }
445
446 Ok(linkage)
447}
448
449pub fn determine_build_environment(target: &TripleNameRef) -> BuildEnvironment {
452 if target.is_darwin() {
453 determine_macos_build_environment().unwrap_or(BuildEnvironment::Indeterminate)
454 } else if target.is_linux() {
455 determine_linux_build_environment().unwrap_or(BuildEnvironment::Indeterminate)
456 } else if target.is_windows() {
457 BuildEnvironment::Windows
458 } else {
459 BuildEnvironment::Indeterminate
460 }
461}
462
463fn determine_linux_build_environment() -> DistResult<BuildEnvironment> {
464 if std::env::consts::OS != "linux" {
467 return Ok(BuildEnvironment::Indeterminate);
468 }
469
470 let mut cmd = Cmd::new("ldd", "determine glibc version");
471 cmd.arg("--version");
472 let output = cmd.output()?;
473 let output_str = String::from_utf8(output.stdout)?;
474 let first_line = output_str.lines().next().unwrap_or(&output_str).trim_end();
475 let glibc_version = if !first_line.contains("GNU libc") && !first_line.contains("GLIBC") {
477 None
478 } else {
479 first_line
484 .split(' ')
485 .next_back()
486 .and_then(|s| s.split_once('.').map(glibc_from_tuple))
487 .transpose()?
488 };
489
490 Ok(BuildEnvironment::Linux { glibc_version })
491}
492
493fn glibc_from_tuple(versions: (&str, &str)) -> Result<GlibcVersion, DistError> {
494 let major = versions.0.parse::<u64>()?;
495 let series = versions.1.parse::<u64>()?;
496
497 Ok(GlibcVersion { major, series })
498}
499
500fn determine_macos_build_environment() -> DistResult<BuildEnvironment> {
501 if std::env::consts::OS != "macos" {
504 return Ok(BuildEnvironment::Indeterminate);
505 }
506
507 let mut cmd = Cmd::new("sw_vers", "determine OS version");
508 cmd.arg("-productVersion");
509 let output = cmd.output()?;
510 let os_version = String::from_utf8(output.stdout)?.trim_end().to_owned();
511
512 Ok(BuildEnvironment::MacOS { os_version })
513}