Repository: axodotdev/cargo-dist
Pull request #426 (2023-09-14 19:34:46)

feat: add a linkage report subcommand
This adds a new linkage checker subcommand, making it easier for us to determine what a packages' binaries have linked against. It can output in both human-readable and JSON forms, making it useful as a CI step and as input to other commands.

The human-readable output looks like this:

```
cargo-dist (aarch64-apple-darwin):

System: /usr/lib/libiconv.2.dylib /usr/lib/libSystem.B.dylib
Homebrew:
Public (unmanaged):
Frameworks: /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation /System/Library/Frameworks/Security.framework/Versions/A/Security
Other:
```

The JSON output looks like this:

```json
[
  {
    "binary": "akextract",
    "target": "aarch64-apple-darwin",
    "system": [
      "/usr/lib/libiconv.2.dylib",
      "/usr/lib/libSystem.B.dylib"
    ],
    "homebrew": [],
    "public_unmanaged": [],
    "other": [],
    "frameworks": []
  },
  {
    "binary": "akmetadata",
    "target": "aarch64-apple-darwin",
    "system": [
      "/usr/lib/libiconv.2.dylib",
      "/usr/lib/libSystem.B.dylib"
    ],
    "homebrew": [],
    "public_unmanaged": [],
    "other": [],
    "frameworks": []
  },
  {
    "binary": "akrepack",
    "target": "aarch64-apple-darwin",
    "system": [
      "/usr/lib/libiconv.2.dylib",
      "/usr/lib/libSystem.B.dylib"
    ],
    "homebrew": [],
    "public_unmanaged": [],
    "other": [],
    "frameworks": []
  }
]
```

The current version only implements macOS, but I'll be adding Linux support soon. Windows support will require more research.

refs #422.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index f7480059..2c38c0e2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -301,14 +301,17 @@ dependencies = [
  "cargo_metadata 0.17.0",
  "clap",
  "clap-cargo",
+ "comfy-table",
  "console",
  "cruet",
  "dialoguer",
  "flate2",
+ "goblin",
  "guppy",
  "include_dir",
  "insta",
  "itertools 0.11.0",
+ "mach_object",
  "miette",
  "minijinja",
  "newline-converter",
@@ -510,6 +513,18 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
 
+[[package]]
+name = "comfy-table"
+version = "7.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ab77dbd8adecaf3f0db40581631b995f312a8a5ae3aa9993188bb8f23d83a5b"
+dependencies = [
+ "crossterm",
+ "strum",
+ "strum_macros",
+ "unicode-width",
+]
+
 [[package]]
 name = "console"
 version = "0.15.7"
@@ -562,6 +577,31 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "crossterm"
+version = "0.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13"
+dependencies = [
+ "bitflags 1.3.2",
+ "crossterm_winapi",
+ "libc",
+ "mio",
+ "parking_lot",
+ "signal-hook",
+ "signal-hook-mio",
+ "winapi",
+]
+
+[[package]]
+name = "crossterm_winapi"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "cruet"
 version = "0.13.3"
@@ -827,6 +867,17 @@ version = "0.28.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
 
+[[package]]
+name = "goblin"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f27c1b4369c2cd341b5de549380158b105a04c331be5db9110eef7b6d2742134"
+dependencies = [
+ "log 0.4.20",
+ "plain",
+ "scroll",
+]
+
 [[package]]
 name = "guppy"
 version = "0.15.2"
@@ -1193,6 +1244,16 @@ version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
 
+[[package]]
+name = "lock_api"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
 [[package]]
 name = "log"
 version = "0.3.9"
@@ -1219,6 +1280,22 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "mach_object"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b6f2d7176b94027af58085a2c9d27c4e416586caba409c314569213901d6068"
+dependencies = [
+ "bitflags 1.3.2",
+ "byteorder",
+ "lazy_static",
+ "libc",
+ "log 0.4.20",
+ "thiserror",
+ "time",
+ "uuid",
+]
+
 [[package]]
 name = "memchr"
 version = "2.6.3"
@@ -1298,6 +1375,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
 dependencies = [
  "libc",
+ "log 0.4.20",
  "wasi",
  "windows-sys 0.48.0",
 ]
@@ -1394,6 +1472,29 @@ version = "3.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
 
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.48.5",
+]
+
 [[package]]
 name = "parse-changelog"
 version = "0.6.2"
@@ -1482,6 +1583,12 @@ version = "0.3.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
 
+[[package]]
+name = "plain"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.66"
@@ -1663,6 +1770,12 @@ dependencies = [
  "untrusted",
 ]
 
+[[package]]
+name = "rustversion"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
+
 [[package]]
 name = "ryu"
 version = "1.0.15"
@@ -1702,6 +1815,32 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "scroll"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da"
+dependencies = [
+ "scroll_derive",
+]
+
+[[package]]
+name = "scroll_derive"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.31",
+]
+
 [[package]]
 name = "sct"
 version = "0.7.0"
@@ -1818,6 +1957,36 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
 
+[[package]]
+name = "signal-hook"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-mio"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
+dependencies = [
+ "libc",
+ "mio",
+ "signal-hook",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "similar"
 version = "2.2.1"
@@ -1883,6 +2052,25 @@ version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 
+[[package]]
+name = "strum"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
+
+[[package]]
+name = "strum_macros"
+version = "0.24.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn 1.0.109",
+]
+
 [[package]]
 name = "subtle"
 version = "2.5.0"
@@ -2068,8 +2256,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48"
 dependencies = [
  "deranged",
+ "itoa",
  "serde",
  "time-core",
+ "time-macros",
 ]
 
 [[package]]
@@ -2078,6 +2268,15 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
 
+[[package]]
+name = "time-macros"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572"
+dependencies = [
+ "time-core",
+]
+
 [[package]]
 name = "tinyvec"
 version = "1.6.0"
diff --git a/book/src/ci/github.md b/book/src/ci/github.md
index ec222d54..feffad98 100644
--- a/book/src/ci/github.md
+++ b/book/src/ci/github.md
@@ -157,6 +157,57 @@ By default cargo-dist breaks build tasks onto more machines than strictly necess
 
 
 
+### Checking what your build linked against
+
+> since 0.4.0
+
+Although most Rust builds are statically linked and contain their own Rust dependencies, some crates will end up dynamically linking against system libraries. It's useful to know what your software picked up&mdash;sometimes this will help you catch things you may not have intended, like dynamically linking to OpenSSL, or allow you to check for package manager-provided libraries your users will need to have installed in order to be able to run your software.
+
+cargo-dist provides a linkage report during your CI build in order to allow you to check for this. For macOS and Linux, it's able to categorize the targets it linked against to help you gauge whether or not it's likely to cause problems for your users. To view this, check the detailed view of your CI build and consult the "Build" step from the `upload-local artifacts` jobs.
+
+This feature is defined for advanced users; most users won't need to use it. It's most useful for developers with specialized build setups who want to ensure that their binaries will be safe for all of their users. A few examples of users who may need to use it:
+
+* Users with custom runners with extra packages installed beyond what's included in the operating system;
+* Users who have installed extra packages using cargo-dist's system dependency feature;
+* Users whose cargo buildsystems include extra C dependencies.
+
+The report is divided into categories to help you make sense of where these libraries are from and what it might mean for your users. These categories are:
+
+* System: Libraries that come with your operating system. On Linux, these packages are all provided by the system's package manager, and the linkage report includes information about which package includes each library. Some of these packages will be included in the base OS, and will be safe to rely on, while you'll need to ensure your users have others. If you're using standard base images like GitHub Actions's and haven't installed additional packages using apt, the packages in this list should be preinstalled for your users. On macOS, these packages are shipped with the operating system and not managed by a package manager; you can always rely on these being there within the same version of macOS.
+* Homebrew (macOS only): Libraries that are provided by the Homebrew package manager for macOS. These packages are not installed by default, so your users will need to have them installed in order to be able to use your software.
+* Public (unmanaged): Libraries which are present in public locations, but which are not managed or provided by the system or a package manager. Because these are not standard parts of the operating system, your users will be unlikely to have them.
+* Frameworks (macOS only): Frameworks, a special type of library provided by macOS. Frameworks installed in the `/System` directory come with the operating system and are available to all users.
+* Other: A catch-all category for any libraries which don't fall in the previous categories.
+
+Here's an example of what a linkage report looks like for a Linux binary;
+
+```
+axolotlsay (x86_64-unknown-linux-gnu):
+
+┌────────────────────┬─────────────────────────────────────────────────┐
+│ Category           ┆ Libraries                                       │
+╞════════════════════╪═════════════════════════════════════════════════╡
+│ System             ┆ /lib/x86_64-linux-gnu/libgcc_s.so.1 (libgcc-s1) │
+│                    ┆ /lib/x86_64-linux-gnu/libpthread.so.0 (libc6)   │
+│                    ┆ /lib/x86_64-linux-gnu/libc.so.6 (libc6)         │
+├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ Homebrew           ┆                                                 │
+├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ Public (unmanaged) ┆                                                 │
+├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ Frameworks         ┆                                                 │
+├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+│ Other              ┆                                                 │
+└────────────────────┴─────────────────────────────────────────────────┘
+```
+
+#### Limitations
+
+While the linkage report can be run locally, the report for Linux artifacts can only be run on Linux.
+
+The Windows report is currently unable to provide information about the sources of libraries.
+
+
 [config-fail-fast]: ../reference/config.md#fail-fast
 [config-merge-tasks]: ../reference/config.md#merge-tasks
 [config-allow-dirty]: ../reference/config.md#allow-dirty
diff --git a/cargo-dist-schema/src/lib.rs b/cargo-dist-schema/src/lib.rs
index 89a3c9ec..0a06e301 100644
--- a/cargo-dist-schema/src/lib.rs
+++ b/cargo-dist-schema/src/lib.rs
@@ -72,6 +72,8 @@ pub struct DistManifest {
     #[serde(default)]
     #[serde(skip_serializing_if = "Option::is_none")]
     pub ci: Option<CiInfo>,
+    /// Data about dynamic linkage in the built libraries
+    pub linkage: Vec<Linkage>,
 }
 
 /// CI backend info
@@ -356,6 +358,7 @@ impl DistManifest {
             artifacts,
             publish_prereleases: false,
             ci: None,
+            linkage: vec![],
         }
     }
 
@@ -387,6 +390,35 @@ impl DistManifest {
     }
 }
 
+/// Information about dynamic libraries used by a binary
+#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
+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<Library>,
+    /// Libraries provided by the Homebrew package manager
+    pub homebrew: Vec<Library>,
+    /// Public libraries not provided by the system and not managed by any package manager
+    pub public_unmanaged: Vec<Library>,
+    /// Libraries which don't fall into any other categories
+    pub other: Vec<Library>,
+    /// Frameworks, only used on macOS
+    pub frameworks: Vec<Library>,
+}
+
+/// Represents a dynamic library located somewhere on the system
+#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
+pub struct Library {
+    /// The path to the library; on platforms without that information, it will be a basename instead
+    pub path: String,
+    /// The package from which a library comes, if relevant
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub source: Option<String>,
+}
+
 /// Helper to read the raw version from serialized json
 fn dist_version(input: &str) -> Option<Version> {
     #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
diff --git a/cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap b/cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap
index 2abe3889..39adfc89 100644
--- a/cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap
+++ b/cargo-dist-schema/src/snapshots/cargo_dist_schema__emit.snap
@@ -7,6 +7,9 @@ expression: json_schema
   "title": "DistManifest",
   "description": "A report of the releases and artifacts that cargo-dist generated",
   "type": "object",
+  "required": [
+    "linkage"
+  ],
   "properties": {
     "announcement_changelog": {
       "description": "A changelog for the announcement",
@@ -66,6 +69,13 @@ expression: json_schema
         "null"
       ]
     },
+    "linkage": {
+      "description": "Data about dynamic linkage in the built libraries",
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/Linkage"
+      }
+    },
     "publish_prereleases": {
       "description": "Whether to publish prereleases to package managers",
       "default": false,
@@ -419,6 +429,84 @@ expression: json_schema
         }
       }
     },
+    "Library": {
+      "description": "Represents a dynamic library located somewhere on the system",
+      "type": "object",
+      "required": [
+        "path"
+      ],
+      "properties": {
+        "path": {
+          "description": "The path to the library; on platforms without that information, it will be a basename instead",
+          "type": "string"
+        },
+        "source": {
+          "description": "The package from which a library comes, if relevant",
+          "type": [
+            "string",
+            "null"
+          ]
+        }
+      }
+    },
+    "Linkage": {
+      "description": "Information about dynamic libraries used by a binary",
+      "type": "object",
+      "required": [
+        "binary",
+        "frameworks",
+        "homebrew",
+        "other",
+        "public_unmanaged",
+        "system",
+        "target"
+      ],
+      "properties": {
+        "binary": {
+          "description": "The filename of the binary",
+          "type": "string"
+        },
+        "frameworks": {
+          "description": "Frameworks, only used on macOS",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Library"
+          }
+        },
+        "homebrew": {
+          "description": "Libraries provided by the Homebrew package manager",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Library"
+          }
+        },
+        "other": {
+          "description": "Libraries which don't fall into any other categories",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Library"
+          }
+        },
+        "public_unmanaged": {
+          "description": "Public libraries not provided by the system and not managed by any package manager",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Library"
+          }
+        },
+        "system": {
+          "description": "Libraries included with the operating system",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Library"
+          }
+        },
+        "target": {
+          "description": "The target triple for which the binary was built",
+          "type": "string"
+        }
+      }
+    },
     "PrRunMode": {
       "description": "Type of job to run on pull request",
       "oneOf": [
diff --git a/cargo-dist/Cargo.toml b/cargo-dist/Cargo.toml
index de9eb12b..75b0438f 100644
--- a/cargo-dist/Cargo.toml
+++ b/cargo-dist/Cargo.toml
@@ -35,6 +35,7 @@ cargo-dist-schema = { version = "=0.3.1", path = "../cargo-dist-schema" }
 axoasset = { version = "0.5.1", features = ["json-serde", "toml-serde", "toml-edit", "compression"] }
 axoproject = { version = "0.4.7", default-features = false, features = ["cargo-projects"] }
 
+comfy-table = "7.0.1"
 miette = { version = "5.6.0" }
 thiserror = "1.0.35"
 tracing = { version = "0.1.36", features = ["log"] }
@@ -53,6 +54,8 @@ include_dir = "0.7.3"
 itertools = "0.11.0"
 cargo-wix = "0.3.7"
 uuid = { version = "1", features = ["v4"] }
+mach_object = "0.1"
+goblin = "0.7.1"
 
 [dev-dependencies]
 insta = { version = "1.26.0", features = ["filters"] }
diff --git a/cargo-dist/src/cli.rs b/cargo-dist/src/cli.rs
index 17eb5022..65315340 100644
--- a/cargo-dist/src/cli.rs
+++ b/cargo-dist/src/cli.rs
@@ -128,6 +128,9 @@ pub enum Commands {
     #[clap(disable_version_flag = true)]
     #[clap(hide = 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
@@ -192,6 +195,12 @@ pub struct BuildArgs {
     #[clap(long, short, value_enum)]
     #[clap(default_value_t = ArtifactMode::Host)]
     pub artifacts: ArtifactMode,
+
+    /// What extra information to print, if anything. Currently supported:
+    ///
+    /// * linkage: prints information on dynamic libraries used by build artifacts
+    #[clap(long, short)]
+    pub print: Vec<String>,
 }
 
 /// How we should select the artifacts to build
@@ -278,6 +287,24 @@ 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,
+    #[clap(long)]
+    #[clap(hide = true)]
+    #[clap(default_value = "")]
+    pub artifacts: String,
+    /// Read linkage data from JSON rather than parsing from binaries
+    #[clap(long)]
+    pub from_json: Option<String>,
+}
 
 #[derive(Args, Clone, Debug)]
 pub struct HelpMarkdownArgs {}
diff --git a/cargo-dist/src/errors.rs b/cargo-dist/src/errors.rs
index 234d70e1..2a0dee45 100644
--- a/cargo-dist/src/errors.rs
+++ b/cargo-dist/src/errors.rs
@@ -28,6 +28,10 @@ pub enum DistError {
     #[diagnostic(transparent)]
     Asset(#[from] axoasset::AxoassetError),
 
+    /// random string error
+    #[error(transparent)]
+    FromUtf8Error(#[from] std::string::FromUtf8Error),
+
     /// A problem with a jinja template, which is always a cargo-dist bug
     #[error("Failed to render template")]
     #[diagnostic(help("this is a bug in cargo-dist, let us know and we'll fix it: https://github.com/axodotdev/cargo-dist/issues/new"))]
@@ -240,6 +244,21 @@ pub enum DistError {
         /// Name of the msi
         style: String,
     },
+    /// Linkage report can't be run for this combination of OS and target
+    #[error("unable to run linkage report for {target} on {host}")]
+    LinkageCheckInvalidOS {
+        /// The OS the check was run on
+        host: String,
+        /// The OS being checked
+        target: String,
+    },
+    /// Linkage report can't be run for this target
+    #[error("unable to run linkage report for this type of binary")]
+    LinkageCheckUnsupportedBinary {},
+
+    /// random i/o error
+    #[error(transparent)]
+    Goblin(#[from] goblin::error::Error),
 }
 
 impl From<minijinja::Error> for DistError {
diff --git a/cargo-dist/src/lib.rs b/cargo-dist/src/lib.rs
index 963d6c23..d7870009 100644
--- a/cargo-dist/src/lib.rs
+++ b/cargo-dist/src/lib.rs
@@ -12,10 +12,12 @@
 
 use std::{
     collections::{BTreeMap, HashMap},
+    fs::File,
+    io::{Cursor, Read},
     process::Command,
 };
 
-use axoasset::LocalAsset;
+use axoasset::{LocalAsset, SourceFile};
 use backend::{
     ci::CiInfo,
     installer::{self, homebrew::HomebrewInstallerInfo, npm::NpmInstallerInfo, InstallerImpl},
@@ -23,10 +25,14 @@ use backend::{
 };
 use camino::{Utf8Path, Utf8PathBuf};
 use cargo_dist_schema::{Asset, AssetKind, DistManifest, ExecutableAsset};
+use comfy_table::{presets::UTF8_FULL, Table};
 use config::{
     ArtifactMode, ChecksumStyle, CompressionImpl, Config, DirtyMode, GenerateMode, ZipStyle,
 };
+use goblin::Object;
+use mach_object::{LoadCommand, OFile};
 use semver::Version;
+use serde::{Deserialize, Serialize};
 use tracing::{info, warn};
 
 use errors::*;
@@ -71,7 +77,7 @@ pub fn do_build(cfg: &Config) -> Result<DistManifest> {
         run_build_step(&dist, step)?;
     }
 
-    Ok(build_manifest(cfg, &dist))
+    Ok(build_manifest(cfg, &dist)?)
 }
 
 /// Just generate the manifest produced by `cargo dist build` without building
@@ -79,10 +85,10 @@ pub fn do_manifest(cfg: &Config) -> Result<DistManifest> {
     check_integrity(cfg)?;
     let dist = gather_work(cfg)?;
 
-    Ok(build_manifest(cfg, &dist))
+    Ok(build_manifest(cfg, &dist)?)
 }
 
-fn build_manifest(cfg: &Config, dist: &DistGraph) -> DistManifest {
+fn build_manifest(cfg: &Config, dist: &DistGraph) -> DistResult<DistManifest> {
     // Report the releases
     let mut releases = vec![];
     let mut all_artifacts = BTreeMap::<String, cargo_dist_schema::Artifact>::new();
@@ -111,6 +117,13 @@ fn build_manifest(cfg: &Config, dist: &DistGraph) -> DistManifest {
         })
     }
 
+    let linkage = fetch_linkage(
+        cfg.targets.clone(),
+        dist.artifacts.clone(),
+        dist.dist_dir.clone(),
+    )?;
+    let linkage = linkage.iter().map(|l| l.to_schema()).collect();
+
     let mut manifest = DistManifest::new(releases, all_artifacts);
 
     // build metadata
@@ -142,7 +155,9 @@ fn build_manifest(cfg: &Config, dist: &DistGraph) -> DistManifest {
 
     manifest.publish_prereleases = dist.publish_prereleases;
 
-    manifest
+    manifest.linkage = linkage;
+
+    Ok(manifest)
 }
 
 fn manifest_artifact(
@@ -621,6 +636,17 @@ 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,
+    /// Read linkage data from JSON rather than performing a live check
+    pub from_json: Option<String>,
+}
+
 fn do_generate_preflight_checks(dist: &DistGraph) -> Result<()> {
     // Enforce cargo-dist-version, unless...
     //
@@ -713,6 +739,419 @@ pub fn run_generate(dist: &DistGraph, args: &GenerateArgs) -> Result<()> {
     Ok(())
 }
 
+fn fetch_linkage(
+    targets: Vec<String>,
+    artifacts: Vec<Artifact>,
+    dist_dir: Utf8PathBuf,
+) -> DistResult<Vec<Linkage>> {
+    let mut reports = vec![];
+
+    for target in targets {
+        let artifacts: Vec<Artifact> = artifacts
+            .clone()
+            .into_iter()
+            .filter(|r| r.target_triples.contains(&target))
+            .collect();
+
+        if artifacts.is_empty() {
+            eprintln!("No matching artifact for target {target}");
+            continue;
+        }
+
+        for artifact in artifacts {
+            let path = Utf8PathBuf::from(&dist_dir).join(format!("{}-{target}", artifact.id));
+
+            for (_, binary) in artifact.required_binaries {
+                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)?);
+                }
+            }
+        }
+    }
+
+    Ok(reports)
+}
+
+/// 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 reports: Vec<Linkage> = if let Some(target) = args.from_json.clone() {
+        let file = SourceFile::load_local(&target)?;
+        file.deserialize_json()?
+    } else {
+        fetch_linkage(cfg.targets.clone(), dist.artifacts, dist.dist_dir)?
+    };
+
+    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, Deserialize, 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<Library>,
+    /// Libraries provided by the Homebrew package manager
+    pub homebrew: Vec<Library>,
+    /// Public libraries not provided by the system and not managed by any package manager
+    pub public_unmanaged: Vec<Library>,
+    /// Libraries which don't fall into any other categories
+    pub other: Vec<Library>,
+    /// Frameworks, only used on macOS
+    pub frameworks: Vec<Library>,
+}
+
+impl Linkage {
+    /// Formatted human-readable output
+    pub fn report(&self) -> String {
+        let mut table = Table::new();
+        table
+            .load_preset(UTF8_FULL)
+            .set_header(vec!["Category", "Libraries"])
+            .add_row(vec![
+                "System",
+                self.system
+                    .clone()
+                    .into_iter()
+                    .map(|l| l.to_string_pretty())
+                    .collect::<Vec<String>>()
+                    .join("\n")
+                    .as_str(),
+            ])
+            .add_row(vec![
+                "Homebrew",
+                self.homebrew
+                    .clone()
+                    .into_iter()
+                    .map(|l| l.to_string_pretty())
+                    .collect::<Vec<String>>()
+                    .join("\n")
+                    .as_str(),
+            ])
+            .add_row(vec![
+                "Public (unmanaged)",
+                self.public_unmanaged
+                    .clone()
+                    .into_iter()
+                    .map(|l| l.path)
+                    .collect::<Vec<String>>()
+                    .join("\n")
+                    .as_str(),
+            ])
+            .add_row(vec![
+                "Frameworks",
+                self.frameworks
+                    .clone()
+                    .into_iter()
+                    .map(|l| l.path)
+                    .collect::<Vec<String>>()
+                    .join("\n")
+                    .as_str(),
+            ])
+            .add_row(vec![
+                "Other",
+                self.other
+                    .clone()
+                    .into_iter()
+                    .map(|l| l.to_string_pretty())
+                    .collect::<Vec<String>>()
+                    .join("\n")
+                    .as_str(),
+            ]);
+
+        let s = format!(
+            r#"{} ({}):
+
+{table}"#,
+            self.binary, self.target,
+        );
+
+        s.to_owned()
+    }
+
+    fn to_schema(&self) -> cargo_dist_schema::Linkage {
+        cargo_dist_schema::Linkage {
+            binary: self.binary.clone(),
+            target: self.target.clone(),
+            system: self.system.iter().map(|s| s.to_schema()).collect(),
+            homebrew: self.homebrew.iter().map(|s| s.to_schema()).collect(),
+            public_unmanaged: self
+                .public_unmanaged
+                .iter()
+                .map(|s| s.to_schema())
+                .collect(),
+            other: self.other.iter().map(|s| s.to_schema()).collect(),
+            frameworks: self.frameworks.iter().map(|s| s.to_schema()).collect(),
+        }
+    }
+
+    /// Constructs a Linkage from a cargo_dist_schema::Linkage
+    pub fn from_schema(other: &cargo_dist_schema::Linkage) -> Self {
+        Self {
+            binary: other.binary.clone(),
+            target: other.target.clone(),
+            system: other.system.iter().map(Library::from_schema).collect(),
+            homebrew: other.homebrew.iter().map(Library::from_schema).collect(),
+            public_unmanaged: other
+                .public_unmanaged
+                .iter()
+                .map(Library::from_schema)
+                .collect(),
+            other: other.other.iter().map(Library::from_schema).collect(),
+            frameworks: other.frameworks.iter().map(Library::from_schema).collect(),
+        }
+    }
+}
+
+/// Represents a dynamic library located somewhere on the system
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct Library {
+    /// The path to the library; on platforms without that information, it will be a basename instead
+    pub path: String,
+    /// The package from which a library comes, if relevant
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub source: Option<String>,
+}
+
+impl Library {
+    fn new(library: String) -> Self {
+        Self {
+            path: library,
+            source: None,
+        }
+    }
+
+    fn to_schema(&self) -> cargo_dist_schema::Library {
+        cargo_dist_schema::Library {
+            path: self.path.clone(),
+            source: self.source.clone(),
+        }
+    }
+
+    fn from_schema(other: &cargo_dist_schema::Library) -> Self {
+        Self {
+            path: other.path.clone(),
+            source: other.source.clone(),
+        }
+    }
+
+    fn from_homebrew(library: String) -> Self {
+        // Doesn't currently support Homebrew installations in
+        // non-default locations
+        let brew_prefix = if library.starts_with("/opt/homebrew/opt/") {
+            Some("/opt/homebrew/opt/")
+        } else if library.starts_with("/usr/local/opt/") {
+            Some("/usr/local/opt/")
+        } else {
+            None
+        };
+
+        if let Some(prefix) = brew_prefix {
+            let cloned = library.clone();
+            let stripped = cloned.strip_prefix(prefix).unwrap();
+            let package = stripped.split('/').nth(0).unwrap();
+
+            Self {
+                path: library,
+                source: Some(package.to_owned()),
+            }
+        } else {
+            Self {
+                path: library,
+                source: None,
+            }
+        }
+    }
+
+    fn maybe_apt(library: String) -> DistResult<Self> {
+        // We can't get this information on other OSs
+        if std::env::consts::OS != "linux" {
+            return Ok(Self {
+                path: library,
+                source: None,
+            });
+        }
+
+        let process = Command::new("dpkg")
+            .arg("--search")
+            .arg(&library)
+            .output()
+            .into_diagnostic();
+        match process {
+            Ok(output) => {
+                let output = String::from_utf8(output.stdout)?;
+
+                let package = output.split(':').nth(0).unwrap();
+
+                Ok(Self {
+                    path: library,
+                    source: Some(package.to_owned()),
+                })
+            }
+            // Couldn't find a package for this file
+            Err(_) => Ok(Self {
+                path: library,
+                source: None,
+            }),
+        }
+    }
+
+    fn to_string_pretty(&self) -> String {
+        if let Some(package) = &self.source {
+            format!("{} ({package})", self.path).to_owned()
+        } else {
+            self.path.clone()
+        }
+    }
+}
+
+fn do_otool(path: &Utf8PathBuf) -> DistResult<Vec<String>> {
+    let mut libraries = vec![];
+
+    let mut f = File::open(path)?;
+    let mut buf = vec![];
+    let size = f.read_to_end(&mut buf).unwrap();
+    let mut cur = Cursor::new(&buf[..size]);
+    if let OFile::MachFile {
+        header: _,
+        commands,
+    } = OFile::parse(&mut cur).unwrap()
+    {
+        let commands = commands
+            .iter()
+            .map(|load| load.command())
+            .cloned()
+            .collect::<Vec<LoadCommand>>();
+
+        for command in commands {
+            match command {
+                LoadCommand::IdDyLib(ref dylib)
+                | LoadCommand::LoadDyLib(ref dylib)
+                | LoadCommand::LoadWeakDyLib(ref dylib)
+                | LoadCommand::ReexportDyLib(ref dylib)
+                | LoadCommand::LoadUpwardDylib(ref dylib)
+                | LoadCommand::LazyLoadDylib(ref dylib) => {
+                    libraries.push(dylib.name.to_string());
+                }
+                _ => {}
+            }
+        }
+    }
+
+    Ok(libraries)
+}
+
+fn do_ldd(path: &Utf8PathBuf) -> DistResult<Vec<String>> {
+    let mut libraries = vec![];
+
+    let output = Command::new("ldd")
+        .arg(path)
+        .output()
+        .expect("Unable to run ldd");
+
+    let result = String::from_utf8_lossy(&output.stdout).to_string();
+    let lines = result.trim_end().split('\n');
+
+    for line in lines {
+        let line = line.trim();
+        // Not a library that actually concerns us
+        if line.starts_with("linux-vdso") {
+            continue;
+        }
+
+        // Format: libname.so.1 => /path/to/libname.so.1 (address)
+        if let Some(path) = line.split(" => ").nth(1) {
+            libraries.push((path.split(' ').next().unwrap()).to_owned());
+        } else {
+            continue;
+        }
+    }
+
+    Ok(libraries)
+}
+
+fn do_pe(path: &Utf8PathBuf) -> DistResult<Vec<String>> {
+    let buf = std::fs::read(path)?;
+    match Object::parse(&buf)? {
+        Object::PE(pe) => Ok(pe.libraries.into_iter().map(|s| s.to_owned()).collect()),
+        _ => Err(DistError::LinkageCheckUnsupportedBinary {}),
+    }
+}
+
+fn determine_linkage(path: &Utf8PathBuf, target: &str) -> DistResult<Linkage> {
+    let libraries = match target {
+        // Can be run on any OS
+        "i686-apple-darwin" | "x86_64-apple-darwin" | "aarch64-apple-darwin" => do_otool(path)?,
+        "i686-unknown-linux-gnu" | "x86_64-unknown-linux-gnu" | "aarch64-unknown-linux-gnu" => {
+            // Currently can only be run on Linux
+            if std::env::consts::OS != "linux" {
+                return Err(DistError::LinkageCheckInvalidOS {
+                    host: std::env::consts::OS.to_owned(),
+                    target: target.to_owned(),
+                });
+            }
+            do_ldd(path)?
+        }
+        // Can be run on any OS
+        "i686-pc-windows-msvc" | "x86_64-pc-windows-msvc" | "aarch64-pc-windows-msvc" => {
+            do_pe(path)?
+        }
+        _ => return Err(DistError::LinkageCheckUnsupportedBinary {}),
+    };
+
+    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::from_homebrew(library.clone()));
+        } else if library.starts_with("/usr/lib") || library.starts_with("/lib") {
+            linkage.system.push(Library::maybe_apt(library.clone())?);
+        } else if library.starts_with("/System/Library/Frameworks")
+            || library.starts_with("/Library/Frameworks")
+        {
+            linkage.frameworks.push(Library::new(library.clone()));
+        } else if library.starts_with("/usr/local") {
+            if std::fs::canonicalize(&library)?.starts_with("/usr/local/Cellar") {
+                linkage
+                    .homebrew
+                    .push(Library::from_homebrew(library.clone()));
+            } else {
+                linkage.public_unmanaged.push(Library::new(library.clone()));
+            }
+        } else {
+            linkage.other.push(Library::maybe_apt(library.clone())?);
+        }
+    }
+
+    Ok(linkage)
+}
+
 /// Run any necessary integrity checks for "primary" commands like build/plan
 ///
 /// (This is currently equivalent to `cargo dist generate --check`)
diff --git a/cargo-dist/src/main.rs b/cargo-dist/src/main.rs
index 2556d2bf..67cb9381 100644
--- a/cargo-dist/src/main.rs
+++ b/cargo-dist/src/main.rs
@@ -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;
 
@@ -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),
@@ -139,6 +140,14 @@ fn print_json(out: &mut Term, report: &DistManifest) -> Result<(), std::io::Erro
     Ok(())
 }
 
+fn print_human_linkage(out: &mut Term, report: &DistManifest) -> Result<(), std::io::Error> {
+    for linkage in &report.linkage {
+        writeln!(out, "{}", Linkage::from_schema(linkage).report())?;
+    }
+
+    Ok(())
+}
+
 fn cmd_dist(cli: &Cli, args: &BuildArgs) -> Result<(), miette::Report> {
     let config = cargo_dist::config::Config {
         needs_coherent_announcement_tag: true,
@@ -156,6 +165,12 @@ fn cmd_dist(cli: &Cli, args: &BuildArgs) -> Result<(), miette::Report> {
         OutputFormat::Human => print_human(&mut out, &report).into_diagnostic()?,
         OutputFormat::Json => print_json(&mut out, &report).into_diagnostic()?,
     }
+
+    let mut err = Term::stderr();
+    if args.print.contains(&"linkage".to_owned()) {
+        print_human_linkage(&mut err, &report).into_diagnostic()?;
+    }
+
     Ok(())
 }
 
@@ -195,6 +210,7 @@ fn cmd_plan(cli: &Cli, _args: &PlanArgs) -> Result<(), miette::Report> {
     let args = &ManifestArgs {
         build_args: BuildArgs {
             artifacts: cli::ArtifactMode::All,
+            print: vec![],
         },
     };
 
@@ -238,6 +254,28 @@ 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,
+        from_json: args.from_json.clone(),
+    };
+    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,
diff --git a/cargo-dist/src/tasks.rs b/cargo-dist/src/tasks.rs
index 87c510e8..fb5b07e8 100644
--- a/cargo-dist/src/tasks.rs
+++ b/cargo-dist/src/tasks.rs
@@ -385,7 +385,7 @@ impl SymbolKind {
 }
 
 /// A distributable artifact we want to build
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub struct Artifact {
     /// Unique id for the Artifact (its file name)
     ///
@@ -417,7 +417,7 @@ pub struct Artifact {
 
 /// Info about an archive (zip/tarball) that should be made. Currently this is always part
 /// of an Artifact, and the final output will be [`Artifact::file_path`][].
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub struct Archive {
     /// An optional prefix path to nest all the archive contents under
     /// If None then all the archive's contents will be placed in the root
@@ -435,7 +435,7 @@ pub struct Archive {
 }
 
 /// A kind of artifact (more specific fields)
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 #[allow(clippy::large_enum_variant)]
 pub enum ArtifactKind {
     /// An Archive containing binaries (aka ExecutableZip)
@@ -449,20 +449,20 @@ pub enum ArtifactKind {
 }
 
 /// An Archive containing binaries (aka ExecutableZip)
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub struct ExecutableZip {
     // everything important is already part of Artifact
 }
 
 /// A Symbols/Debuginfo Artifact
-#[derive(Debug)]
+#[derive(Clone, Debug)]
 pub struct Symbols {
     /// The kind of symbols this is
     kind: SymbolKind,
 }
 
 /// 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,
diff --git a/cargo-dist/templates/ci/github_ci.yml.j2 b/cargo-dist/templates/ci/github_ci.yml.j2
index 5d02d612..2bbed7e7 100644
--- a/cargo-dist/templates/ci/github_ci.yml.j2
+++ b/cargo-dist/templates/ci/github_ci.yml.j2
@@ -122,13 +122,13 @@ jobs:
         if: ${{ hashFiles('Brewfile') == '' }}
         run: |
           # Actually do builds and make zips and whatnot
-          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
           echo "cargo dist ran successfully"
       - name: Build artifacts (using Brewfile)
         if: ${{ hashFiles('Brewfile') != '' }}
         run: |
           # Actually do builds and make zips and whatnot
-          brew bundle exec -- cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          brew bundle exec -- cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
           echo "cargo dist ran successfully"
       - id: cargo-dist
         name: Post-build


Test patch:
diff --git a/cargo-dist/tests/snapshots/akaikatana_basic.snap b/cargo-dist/tests/snapshots/akaikatana_basic.snap
index 72def183..1cf91b52 100644
--- a/cargo-dist/tests/snapshots/akaikatana_basic.snap
+++ b/cargo-dist/tests/snapshots/akaikatana_basic.snap
@@ -1265,7 +1265,8 @@ Install-Binary "$Args"
       },
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 ================ github-ci.yml ================
@@ -1381,13 +1382,13 @@ jobs:
         if: ${{ hashFiles('Brewfile') == '' }}
         run: |
           # Actually do builds and make zips and whatnot
-          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
           echo "cargo dist ran successfully"
       - name: Build artifacts (using Brewfile)
         if: ${{ hashFiles('Brewfile') != '' }}
         run: |
           # Actually do builds and make zips and whatnot
-          brew bundle exec -- cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          brew bundle exec -- cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
           echo "cargo dist ran successfully"
       - id: cargo-dist
         name: Post-build
diff --git a/cargo-dist/tests/snapshots/akaikatana_repo_with_dot_git.snap b/cargo-dist/tests/snapshots/akaikatana_repo_with_dot_git.snap
index 72def183..1cf91b52 100644
--- a/cargo-dist/tests/snapshots/akaikatana_repo_with_dot_git.snap
+++ b/cargo-dist/tests/snapshots/akaikatana_repo_with_dot_git.snap
@@ -1265,7 +1265,8 @@ Install-Binary "$Args"
       },
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 ================ github-ci.yml ================
@@ -1381,13 +1382,13 @@ jobs:
         if: ${{ hashFiles('Brewfile') == '' }}
         run: |
           # Actually do builds and make zips and whatnot
-          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
           echo "cargo dist ran successfully"
       - name: Build artifacts (using Brewfile)
         if: ${{ hashFiles('Brewfile') != '' }}
         run: |
           # Actually do builds and make zips and whatnot
-          brew bundle exec -- cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          brew bundle exec -- cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
           echo "cargo dist ran successfully"
       - id: cargo-dist
         name: Post-build
diff --git a/cargo-dist/tests/snapshots/axolotlsay_basic.snap b/cargo-dist/tests/snapshots/axolotlsay_basic.snap
index 9672c162..f20b4efb 100644
--- a/cargo-dist/tests/snapshots/axolotlsay_basic.snap
+++ b/cargo-dist/tests/snapshots/axolotlsay_basic.snap
@@ -2164,7 +2164,8 @@ maybeInstall(true).then(run);
       },
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 ================ github-ci.yml ================
@@ -2276,13 +2277,13 @@ jobs:
         if: ${{ hashFiles('Brewfile') == '' }}
         run: |
           # Actually do builds and make zips and whatnot
-          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
           echo "cargo dist ran successfully"
       - name: Build artifacts (using Brewfile)
         if: ${{ hashFiles('Brewfile') != '' }}
         run: |
           # Actually do builds and make zips and whatnot
-          brew bundle exec -- cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          brew bundle exec -- cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
           echo "cargo dist ran successfully"
       - id: cargo-dist
         name: Post-build
diff --git a/cargo-dist/tests/snapshots/axolotlsay_edit_existing.snap b/cargo-dist/tests/snapshots/axolotlsay_edit_existing.snap
index 3329642f..f5946519 100644
--- a/cargo-dist/tests/snapshots/axolotlsay_edit_existing.snap
+++ b/cargo-dist/tests/snapshots/axolotlsay_edit_existing.snap
@@ -2139,7 +2139,8 @@ maybeInstall(true).then(run);
       },
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 ================ github-ci.yml ================
@@ -2251,13 +2252,13 @@ jobs:
         if: ${{ hashFiles('Brewfile') == '' }}
         run: |
           # Actually do builds and make zips and whatnot
-          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
           echo "cargo dist ran successfully"
       - name: Build artifacts (using Brewfile)
         if: ${{ hashFiles('Brewfile') != '' }}
         run: |
           # Actually do builds and make zips and whatnot
-          brew bundle exec -- cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          brew bundle exec -- cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
           echo "cargo dist ran successfully"
       - id: cargo-dist
         name: Post-build
diff --git a/cargo-dist/tests/snapshots/axolotlsay_no_homebrew_publish.snap b/cargo-dist/tests/snapshots/axolotlsay_no_homebrew_publish.snap
index 1c09f94c..e9a292f6 100644
--- a/cargo-dist/tests/snapshots/axolotlsay_no_homebrew_publish.snap
+++ b/cargo-dist/tests/snapshots/axolotlsay_no_homebrew_publish.snap
@@ -2139,7 +2139,8 @@ maybeInstall(true).then(run);
       },
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 ================ github-ci.yml ================
@@ -2251,13 +2252,13 @@ jobs:
         if: ${{ hashFiles('Brewfile') == '' }}
         run: |
           # Actually do builds and make zips and whatnot
-          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
           echo "cargo dist ran successfully"
       - name: Build artifacts (using Brewfile)
         if: ${{ hashFiles('Brewfile') != '' }}
         run: |
           # Actually do builds and make zips and whatnot
-          brew bundle exec -- cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          brew bundle exec -- cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
           echo "cargo dist ran successfully"
       - id: cargo-dist
         name: Post-build
diff --git a/cargo-dist/tests/snapshots/axolotlsay_ssldotcom_windows_sign.snap b/cargo-dist/tests/snapshots/axolotlsay_ssldotcom_windows_sign.snap
index 68fcb5c9..cb68bb32 100644
--- a/cargo-dist/tests/snapshots/axolotlsay_ssldotcom_windows_sign.snap
+++ b/cargo-dist/tests/snapshots/axolotlsay_ssldotcom_windows_sign.snap
@@ -1257,7 +1257,8 @@ Install-Binary "$Args"
       },
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 ================ github-ci.yml ================
@@ -1369,13 +1370,13 @@ jobs:
         if: ${{ hashFiles('Brewfile') == '' }}
         run: |
           # Actually do builds and make zips and whatnot
-          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
           echo "cargo dist ran successfully"
       - name: Build artifacts (using Brewfile)
         if: ${{ hashFiles('Brewfile') != '' }}
         run: |
           # Actually do builds and make zips and whatnot
-          brew bundle exec -- cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          brew bundle exec -- cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
           echo "cargo dist ran successfully"
       - id: cargo-dist
         name: Post-build
diff --git a/cargo-dist/tests/snapshots/axolotlsay_ssldotcom_windows_sign_prod.snap b/cargo-dist/tests/snapshots/axolotlsay_ssldotcom_windows_sign_prod.snap
index 631c6994..5806dfc1 100644
--- a/cargo-dist/tests/snapshots/axolotlsay_ssldotcom_windows_sign_prod.snap
+++ b/cargo-dist/tests/snapshots/axolotlsay_ssldotcom_windows_sign_prod.snap
@@ -1257,7 +1257,8 @@ Install-Binary "$Args"
       },
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 ================ github-ci.yml ================
@@ -1369,13 +1370,13 @@ jobs:
         if: ${{ hashFiles('Brewfile') == '' }}
         run: |
           # Actually do builds and make zips and whatnot
-          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
           echo "cargo dist ran successfully"
       - name: Build artifacts (using Brewfile)
         if: ${{ hashFiles('Brewfile') != '' }}
         run: |
           # Actually do builds and make zips and whatnot
-          brew bundle exec -- cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          brew bundle exec -- cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
           echo "cargo dist ran successfully"
       - id: cargo-dist
         name: Post-build
diff --git a/cargo-dist/tests/snapshots/axolotlsay_user_publish_job.snap b/cargo-dist/tests/snapshots/axolotlsay_user_publish_job.snap
index 0acc4368..6fdb5938 100644
--- a/cargo-dist/tests/snapshots/axolotlsay_user_publish_job.snap
+++ b/cargo-dist/tests/snapshots/axolotlsay_user_publish_job.snap
@@ -2139,7 +2139,8 @@ maybeInstall(true).then(run);
       },
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 ================ github-ci.yml ================
@@ -2251,13 +2252,13 @@ jobs:
         if: ${{ hashFiles('Brewfile') == '' }}
         run: |
           # Actually do builds and make zips and whatnot
-          cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
           echo "cargo dist ran successfully"
       - name: Build artifacts (using Brewfile)
         if: ${{ hashFiles('Brewfile') != '' }}
         run: |
           # Actually do builds and make zips and whatnot
-          brew bundle exec -- cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
+          brew bundle exec -- cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
           echo "cargo dist ran successfully"
       - id: cargo-dist
         name: Post-build
diff --git a/cargo-dist/tests/snapshots/install_path_cargo_home.snap b/cargo-dist/tests/snapshots/install_path_cargo_home.snap
index e1186d93..58c4c1a2 100644
--- a/cargo-dist/tests/snapshots/install_path_cargo_home.snap
+++ b/cargo-dist/tests/snapshots/install_path_cargo_home.snap
@@ -1267,7 +1267,8 @@ Install-Binary "$Args"
       },
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 
diff --git a/cargo-dist/tests/snapshots/install_path_env_no_subdir.snap b/cargo-dist/tests/snapshots/install_path_env_no_subdir.snap
index f1d4fdff..90226fe7 100644
--- a/cargo-dist/tests/snapshots/install_path_env_no_subdir.snap
+++ b/cargo-dist/tests/snapshots/install_path_env_no_subdir.snap
@@ -1247,7 +1247,8 @@ Install-Binary "$Args"
       },
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 
diff --git a/cargo-dist/tests/snapshots/install_path_env_subdir.snap b/cargo-dist/tests/snapshots/install_path_env_subdir.snap
index df09ecfa..a9b8b4c0 100644
--- a/cargo-dist/tests/snapshots/install_path_env_subdir.snap
+++ b/cargo-dist/tests/snapshots/install_path_env_subdir.snap
@@ -1247,7 +1247,8 @@ Install-Binary "$Args"
       },
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 
diff --git a/cargo-dist/tests/snapshots/install_path_env_subdir_space.snap b/cargo-dist/tests/snapshots/install_path_env_subdir_space.snap
index ba98aa3c..abcc9d27 100644
--- a/cargo-dist/tests/snapshots/install_path_env_subdir_space.snap
+++ b/cargo-dist/tests/snapshots/install_path_env_subdir_space.snap
@@ -1247,7 +1247,8 @@ Install-Binary "$Args"
       },
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 
diff --git a/cargo-dist/tests/snapshots/install_path_env_subdir_space_deeper.snap b/cargo-dist/tests/snapshots/install_path_env_subdir_space_deeper.snap
index fb812149..87dee32b 100644
--- a/cargo-dist/tests/snapshots/install_path_env_subdir_space_deeper.snap
+++ b/cargo-dist/tests/snapshots/install_path_env_subdir_space_deeper.snap
@@ -1247,7 +1247,8 @@ Install-Binary "$Args"
       },
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 
diff --git a/cargo-dist/tests/snapshots/install_path_home_subdir_deeper.snap b/cargo-dist/tests/snapshots/install_path_home_subdir_deeper.snap
index c6ceea75..e707cc13 100644
--- a/cargo-dist/tests/snapshots/install_path_home_subdir_deeper.snap
+++ b/cargo-dist/tests/snapshots/install_path_home_subdir_deeper.snap
@@ -1247,7 +1247,8 @@ Install-Binary "$Args"
       },
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 
diff --git a/cargo-dist/tests/snapshots/install_path_home_subdir_min.snap b/cargo-dist/tests/snapshots/install_path_home_subdir_min.snap
index 03b9716d..ebcc2fa1 100644
--- a/cargo-dist/tests/snapshots/install_path_home_subdir_min.snap
+++ b/cargo-dist/tests/snapshots/install_path_home_subdir_min.snap
@@ -1247,7 +1247,8 @@ Install-Binary "$Args"
       },
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 
diff --git a/cargo-dist/tests/snapshots/install_path_home_subdir_space.snap b/cargo-dist/tests/snapshots/install_path_home_subdir_space.snap
index bbcd88e7..e7c619fe 100644
--- a/cargo-dist/tests/snapshots/install_path_home_subdir_space.snap
+++ b/cargo-dist/tests/snapshots/install_path_home_subdir_space.snap
@@ -1247,7 +1247,8 @@ Install-Binary "$Args"
       },
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 
diff --git a/cargo-dist/tests/snapshots/install_path_home_subdir_space_deeper.snap b/cargo-dist/tests/snapshots/install_path_home_subdir_space_deeper.snap
index 075084c1..80ae9850 100644
--- a/cargo-dist/tests/snapshots/install_path_home_subdir_space_deeper.snap
+++ b/cargo-dist/tests/snapshots/install_path_home_subdir_space_deeper.snap
@@ -1247,7 +1247,8 @@ Install-Binary "$Args"
       },
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 
diff --git a/cargo-dist/tests/snapshots/lib_manifest.snap b/cargo-dist/tests/snapshots/lib_manifest.snap
index 573563f1..7c6a654b 100644
--- a/cargo-dist/tests/snapshots/lib_manifest.snap
+++ b/cargo-dist/tests/snapshots/lib_manifest.snap
@@ -19,7 +19,8 @@ stdout:
       "artifacts_matrix": {},
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 stderr:
diff --git a/cargo-dist/tests/snapshots/lib_manifest_slash.snap b/cargo-dist/tests/snapshots/lib_manifest_slash.snap
index 573563f1..7c6a654b 100644
--- a/cargo-dist/tests/snapshots/lib_manifest_slash.snap
+++ b/cargo-dist/tests/snapshots/lib_manifest_slash.snap
@@ -19,7 +19,8 @@ stdout:
       "artifacts_matrix": {},
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 stderr:
diff --git a/cargo-dist/tests/snapshots/long-help.snap b/cargo-dist/tests/snapshots/long-help.snap
index cca421d2..db8efdd7 100644
--- a/cargo-dist/tests/snapshots/long-help.snap
+++ b/cargo-dist/tests/snapshots/long-help.snap
@@ -14,6 +14,7 @@ Commands:
   build     Build artifacts
   init      Setup or update cargo-dist
   generate  Generate one or more pieces of configuration
+  linkage   Report on the dynamic libraries used by the built artifacts
   manifest  Generate the final build manifest without running any builds
   plan      Get a plan of what to build (and check project status)
   help      Print this message or the help of the given subcommand(s)
diff --git a/cargo-dist/tests/snapshots/manifest.snap b/cargo-dist/tests/snapshots/manifest.snap
index 13f735ca..e4ee11c3 100644
--- a/cargo-dist/tests/snapshots/manifest.snap
+++ b/cargo-dist/tests/snapshots/manifest.snap
@@ -261,7 +261,8 @@ stdout:
       },
       "pr_run_mode": "plan"
     }
-  }
+  },
+  "linkage": []
 }
 
 stderr:
diff --git a/cargo-dist/tests/snapshots/markdown-help.snap b/cargo-dist/tests/snapshots/markdown-help.snap
index f0851b4e..37986feb 100644
--- a/cargo-dist/tests/snapshots/markdown-help.snap
+++ b/cargo-dist/tests/snapshots/markdown-help.snap
@@ -22,6 +22,7 @@ cargo dist <COMMAND>
 * [build](#cargo-dist-build): Build artifacts
 * [init](#cargo-dist-init): Setup or update cargo-dist
 * [generate](#cargo-dist-generate): Generate one or more pieces of configuration
+* [linkage](#cargo-dist-linkage): Report on the dynamic libraries used by the built artifacts
 * [manifest](#cargo-dist-manifest): Generate the final build manifest without running any builds
 * [plan](#cargo-dist-plan): Get a plan of what to build (and check project status)
 * [help](#cargo-dist-help): Print this message or the help of the given subcommand(s)
@@ -122,6 +123,11 @@ Possible values:
 - host:   Fuzzily build "as much as possible" for the host system
 - all:    Build all the artifacts; useful for `cargo dist manifest`
 
+#### `-p, --print <PRINT>`
+What extra information to print, if anything. Currently supported:
+
+* linkage: prints information on dynamic libraries used by build artifacts
+
 #### `-h, --help`
 Print help (see a summary with '-h')
 
@@ -188,6 +194,32 @@ Print help (see a summary with '-h')
 ### GLOBAL OPTIONS
 This subcommand accepts all the [global options](#global-options)
 
+<br><br><br>
+## cargo dist linkage
+Report on the dynamic libraries used by the built artifacts
+
+### Usage
+
+```text
+cargo dist linkage [OPTIONS]
+```
+
+### Options
+#### `--print-output`
+Print human-readable output
+
+#### `--print-json`
+Print output as JSON
+
+#### `--from-json <FROM_JSON>`
+Read linkage data from JSON rather than parsing from binaries
+
+#### `-h, --help`
+Print help (see a summary with '-h')
+
+### GLOBAL OPTIONS
+This subcommand accepts all the [global options](#global-options)
+
 <br><br><br>
 ## cargo dist manifest
 Generate the final build manifest without running any builds.
@@ -224,6 +256,11 @@ Possible values:
 - host:   Fuzzily build "as much as possible" for the host system
 - all:    Build all the artifacts; useful for `cargo dist manifest`
 
+#### `-p, --print <PRINT>`
+What extra information to print, if anything. Currently supported:
+
+* linkage: prints information on dynamic libraries used by build artifacts
+
 #### `-h, --help`
 Print help (see a summary with '-h')
 
@@ -267,6 +304,7 @@ cargo dist help [COMMAND]
 * [build](#cargo-dist-build): Build artifacts
 * [init](#cargo-dist-init): Setup or update cargo-dist
 * [generate](#cargo-dist-generate): Generate one or more pieces of configuration
+* [linkage](#cargo-dist-linkage): Report on the dynamic libraries used by the built artifacts
 * [manifest](#cargo-dist-manifest): Generate the final build manifest without running any builds
 * [plan](#cargo-dist-plan): Get a plan of what to build (and check project status)
 * [help](#cargo-dist-help): Print this message or the help of the given subcommand(s)
diff --git a/cargo-dist/tests/snapshots/short-help.snap b/cargo-dist/tests/snapshots/short-help.snap
index 1707e02f..6e6b540e 100644
--- a/cargo-dist/tests/snapshots/short-help.snap
+++ b/cargo-dist/tests/snapshots/short-help.snap
@@ -12,6 +12,7 @@ Commands:
   build     Build artifacts
   init      Setup or update cargo-dist
   generate  Generate one or more pieces of configuration
+  linkage   Report on the dynamic libraries used by the built artifacts
   manifest  Generate the final build manifest without running any builds
   plan      Get a plan of what to build (and check project status)
   help      Print this message or the help of the given subcommand(s)