diff --git a/crates/cargo-util-schemas/src/lib.rs b/crates/cargo-util-schemas/src/lib.rs index e3a51284f07..a850c894a8b 100644 --- a/crates/cargo-util-schemas/src/lib.rs +++ b/crates/cargo-util-schemas/src/lib.rs @@ -10,6 +10,7 @@ pub mod core; pub mod manifest; +pub mod messages; #[cfg(feature = "unstable-schema")] pub mod schema; diff --git a/crates/cargo-util-schemas/src/messages.rs b/crates/cargo-util-schemas/src/messages.rs new file mode 100644 index 00000000000..45eb0ece4ea --- /dev/null +++ b/crates/cargo-util-schemas/src/messages.rs @@ -0,0 +1,32 @@ +//! Schemas for JSON messages emitted by Cargo. + +use std::collections::BTreeMap; +use std::path::PathBuf; + +/// File information of a package archive generated by `cargo package --list`. +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "snake_case")] +pub struct PackageList { + /// The Package ID Spec of the package. + pub id: crate::core::PackageIdSpec, + /// A map of relative paths in the archive to their detailed file information. + pub files: BTreeMap, +} + +/// Where the file is from. +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "snake_case", tag = "kind")] +pub enum PackageFile { + /// File being copied from another location. + Copy { + /// An absolute path to the actual file content + path: PathBuf, + }, + /// File being generated during packaging + Generate { + /// An absolute path to the original file the generated one is based on. + /// if any. + #[serde(skip_serializing_if = "Option::is_none")] + path: Option, + }, +} diff --git a/src/bin/cargo/commands/package.rs b/src/bin/cargo/commands/package.rs index 58b935d0b9b..dbc61e914a5 100644 --- a/src/bin/cargo/commands/package.rs +++ b/src/bin/cargo/commands/package.rs @@ -1,6 +1,8 @@ use crate::command_prelude::*; -use cargo::ops::{self, PackageOpts}; +use cargo::ops; +use cargo::ops::PackageMessageFormat; +use cargo::ops::PackageOpts; pub fn cli() -> Command { subcommand("package") @@ -30,6 +32,13 @@ pub fn cli() -> Command { "exclude-lockfile", "Don't include the lock file when packaging", )) + .arg( + opt("message-format", "Output representation (unstable)") + .value_name("FMT") + // This currently requires and only works with `--list`. + .requires("list") + .value_parser(PackageMessageFormat::POSSIBLE_VALUES), + ) .arg_silent_suggestion() .arg_package_spec_no_all( "Package(s) to assemble", @@ -75,12 +84,21 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult { } let specs = args.packages_from_flags()?; + let fmt = if let Some(fmt) = args._value_of("message-format") { + gctx.cli_unstable() + .fail_if_stable_opt("--message-format", 11666)?; + fmt.parse()? + } else { + PackageMessageFormat::Human + }; + ops::package( &ws, &PackageOpts { gctx, verify: !args.flag("no-verify"), list: args.flag("list"), + fmt, check_metadata: !args.flag("no-metadata"), allow_dirty: args.flag("allow-dirty"), include_lockfile: !args.flag("exclude-lockfile"), diff --git a/src/cargo/ops/cargo_package/mod.rs b/src/cargo/ops/cargo_package/mod.rs index 973c78679c8..71d43bc0198 100644 --- a/src/cargo/ops/cargo_package/mod.rs +++ b/src/cargo/ops/cargo_package/mod.rs @@ -1,4 +1,6 @@ -use std::collections::{BTreeSet, HashMap}; +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::collections::HashMap; use std::fs::{self, File}; use std::io::prelude::*; use std::io::SeekFrom; @@ -32,6 +34,7 @@ use crate::util::HumanBytes; use crate::{drop_println, ops}; use anyhow::{bail, Context as _}; use cargo_util::paths; +use cargo_util_schemas::messages; use flate2::{Compression, GzBuilder}; use tar::{Builder, EntryType, Header, HeaderMode}; use tracing::debug; @@ -40,10 +43,38 @@ use unicase::Ascii as UncasedAscii; mod vcs; mod verify; +/// Message format for `cargo package`. +/// +/// Currently only affect the output of the `--list` flag. +#[derive(Debug, Clone)] +pub enum PackageMessageFormat { + Human, + Json, +} + +impl PackageMessageFormat { + pub const POSSIBLE_VALUES: [&str; 2] = ["human", "json"]; + + pub const DEFAULT: &str = "human"; +} + +impl std::str::FromStr for PackageMessageFormat { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "human" => Ok(PackageMessageFormat::Human), + "json" => Ok(PackageMessageFormat::Json), + f => bail!("unknown message format `{f}`"), + } + } +} + #[derive(Clone)] pub struct PackageOpts<'gctx> { pub gctx: &'gctx GlobalContext, pub list: bool, + pub fmt: PackageMessageFormat, pub check_metadata: bool, pub allow_dirty: bool, pub include_lockfile: bool, @@ -78,9 +109,13 @@ enum FileContents { enum GeneratedFile { /// Generates `Cargo.toml` by rewriting the original. - Manifest, - /// Generates `Cargo.lock` in some cases (like if there is a binary). - Lockfile, + /// + /// Associated path is the original manifest path. + Manifest(PathBuf), + /// Generates `Cargo.lock`. + /// + /// Associated path is the path to the original lock file, if existing. + Lockfile(Option), /// Adds a `.cargo_vcs_info.json` file if in a git repo. VcsInfo(vcs::VcsInfo), } @@ -236,8 +271,33 @@ fn do_package<'a>( let ar_files = prepare_archive(ws, &pkg, &opts)?; if opts.list { - for ar_file in &ar_files { - drop_println!(ws.gctx(), "{}", ar_file.rel_str); + match opts.fmt { + PackageMessageFormat::Human => { + // While this form is called "human", + // it keeps the old file-per-line format for compatibility. + for ar_file in &ar_files { + drop_println!(ws.gctx(), "{}", ar_file.rel_str); + } + } + PackageMessageFormat::Json => { + let message = messages::PackageList { + id: pkg.package_id().to_spec(), + files: BTreeMap::from_iter(ar_files.into_iter().map(|f| { + let file = match f.contents { + FileContents::OnDisk(path) => messages::PackageFile::Copy { path }, + FileContents::Generated( + GeneratedFile::Manifest(path) + | GeneratedFile::Lockfile(Some(path)), + ) => messages::PackageFile::Generate { path: Some(path) }, + FileContents::Generated( + GeneratedFile::VcsInfo(_) | GeneratedFile::Lockfile(None), + ) => messages::PackageFile::Generate { path: None }, + }; + (f.rel_path, file) + })), + }; + let _ = ws.gctx().shell().print_json(&message); + } } } else { let tarball = create_package(ws, &pkg, ar_files, local_reg.as_ref())?; @@ -444,7 +504,9 @@ fn build_ar_list( .push(ArchiveFile { rel_path: PathBuf::from("Cargo.toml"), rel_str: "Cargo.toml".to_string(), - contents: FileContents::Generated(GeneratedFile::Manifest), + contents: FileContents::Generated(GeneratedFile::Manifest( + pkg.manifest_path().to_owned(), + )), }); } else { ws.gctx().shell().warn(&format!( @@ -454,6 +516,8 @@ fn build_ar_list( } if include_lockfile { + let lockfile_path = ws.lock_root().as_path_unlocked().join(LOCKFILE_NAME); + let lockfile_path = lockfile_path.exists().then_some(lockfile_path); let rel_str = "Cargo.lock"; result .entry(UncasedAscii::new(rel_str)) @@ -461,7 +525,7 @@ fn build_ar_list( .push(ArchiveFile { rel_path: PathBuf::from(rel_str), rel_str: rel_str.to_string(), - contents: FileContents::Generated(GeneratedFile::Lockfile), + contents: FileContents::Generated(GeneratedFile::Lockfile(lockfile_path)), }); } @@ -780,8 +844,10 @@ fn tar( } FileContents::Generated(generated_kind) => { let contents = match generated_kind { - GeneratedFile::Manifest => publish_pkg.manifest().to_normalized_contents()?, - GeneratedFile::Lockfile => build_lock(ws, &publish_pkg, local_reg)?, + GeneratedFile::Manifest(_) => { + publish_pkg.manifest().to_normalized_contents()? + } + GeneratedFile::Lockfile(_) => build_lock(ws, &publish_pkg, local_reg)?, GeneratedFile::VcsInfo(ref s) => serde_json::to_string_pretty(s)?, }; header.set_entry_type(EntryType::file()); diff --git a/src/cargo/ops/mod.rs b/src/cargo/ops/mod.rs index 7d35f952995..9960c12649d 100644 --- a/src/cargo/ops/mod.rs +++ b/src/cargo/ops/mod.rs @@ -10,7 +10,10 @@ pub use self::cargo_fetch::{fetch, FetchOptions}; pub use self::cargo_install::{install, install_list}; pub use self::cargo_new::{init, new, NewOptions, NewProjectKind, VersionControl}; pub use self::cargo_output_metadata::{output_metadata, ExportInfo, OutputMetadataOptions}; -pub use self::cargo_package::{check_yanked, package, PackageOpts}; +pub use self::cargo_package::check_yanked; +pub use self::cargo_package::package; +pub use self::cargo_package::PackageMessageFormat; +pub use self::cargo_package::PackageOpts; pub use self::cargo_pkgid::pkgid; pub use self::cargo_read_manifest::read_package; pub use self::cargo_run::run; diff --git a/src/cargo/ops/registry/publish.rs b/src/cargo/ops/registry/publish.rs index c064c01c2dd..85cb6118023 100644 --- a/src/cargo/ops/registry/publish.rs +++ b/src/cargo/ops/registry/publish.rs @@ -144,6 +144,7 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> { gctx: opts.gctx, verify: opts.verify, list: false, + fmt: ops::PackageMessageFormat::Human, check_metadata: true, allow_dirty: opts.allow_dirty, include_lockfile: true, diff --git a/src/doc/man/cargo-package.md b/src/doc/man/cargo-package.md index e3e12090998..217f41632df 100644 --- a/src/doc/man/cargo-package.md +++ b/src/doc/man/cargo-package.md @@ -116,6 +116,48 @@ lock-files will be generated under the assumption that dependencies will be published to this registry. {{/option}} +{{#option "`--message-format` _fmt_" }} +Specifies the output message format. +Currently, it only works with `--list` and affects the file listing format. +This is unstable and requires `-Zunstable-options`. +Valid output formats: + +- `human` (default): Display in a file-per-line format. +- `json`: Emit machine-readable JSON information about each package. + One package per JSON line (Newline delimited JSON). + ```javascript + { + /* The Package ID Spec of the package. */ + "id": "path+file:///home/foo#0.0.0", + /* Files of this package */ + "files" { + /* Relative path in the archive file. */ + "Cargo.toml.orig": { + /* Where the file is from. + - "generate" for file being generated during packaging + - "copy" for file being copied from another location. + */ + "kind": "copy", + /* For the "copy" kind, + it is an absolute path to the actual file content. + For the "generate" kind, + it is the original file the generated one is based on. + */ + "path": "/home/foo/Cargo.toml" + }, + "Cargo.toml": { + "kind": "generate", + "path": "/home/foo/Cargo.toml" + }, + "src/main.rs": { + "kind": "copy", + "path": "/home/foo/src/main.rs" + } + } + } + ``` +{{/option}} + {{/options}} {{> section-package-selection }} diff --git a/src/doc/man/generated_txt/cargo-package.txt b/src/doc/man/generated_txt/cargo-package.txt index 25b1f6bc289..086b68d050d 100644 --- a/src/doc/man/generated_txt/cargo-package.txt +++ b/src/doc/man/generated_txt/cargo-package.txt @@ -114,6 +114,45 @@ OPTIONS multiple inter-dependent crates, lock-files will be generated under the assumption that dependencies will be published to this registry. + --message-format fmt + Specifies the output message format. Currently, it only works with + --list and affects the file listing format. This is unstable and + requires -Zunstable-options. Valid output formats: + + o human (default): Display in a file-per-line format. + + o json: Emit machine-readable JSON information about each package. + One package per JSON line (Newline delimited JSON). + { + /* The Package ID Spec of the package. */ + "id": "path+file:///home/foo#0.0.0", + /* Files of this package */ + "files" { + /* Relative path in the archive file. */ + "Cargo.toml.orig": { + /* Where the file is from. + - "generate" for file being generated during packaging + - "copy" for file being copied from another location. + */ + "kind": "copy", + /* For the "copy" kind, + it is an absolute path to the actual file content. + For the "generate" kind, + it is the original file the generated one is based on. + */ + "path": "/home/foo/Cargo.toml" + }, + "Cargo.toml": { + "kind": "generate", + "path": "/home/foo/Cargo.toml" + }, + "src/main.rs": { + "kind": "copy", + "path": "/home/foo/src/main.rs" + } + } + } + Package Selection By default, when no package selection options are given, the packages selected depend on the selected manifest file (based on the current diff --git a/src/doc/src/commands/cargo-package.md b/src/doc/src/commands/cargo-package.md index d131bc7f902..ea9ee490bee 100644 --- a/src/doc/src/commands/cargo-package.md +++ b/src/doc/src/commands/cargo-package.md @@ -113,6 +113,49 @@ lock-files will be generated under the assumption that dependencies will be published to this registry. +
--message-format fmt
+
Specifies the output message format. +Currently, it only works with --list and affects the file listing format. +This is unstable and requires -Zunstable-options. +Valid output formats:

+
    +
  • human (default): Display in a file-per-line format.
  • +
  • json: Emit machine-readable JSON information about each package. +One package per JSON line (Newline delimited JSON). +
    {
    +  /* The Package ID Spec of the package. */
    +  "id": "path+file:///home/foo#0.0.0",
    +  /* Files of this package */
    +  "files" {
    +    /* Relative path in the archive file. */
    +    "Cargo.toml.orig": {
    +      /* Where the file is from.
    +         - "generate" for file being generated during packaging
    +         - "copy" for file being copied from another location.
    +      */
    +      "kind": "copy",
    +      /* For the "copy" kind,
    +         it is an absolute path to the actual file content.
    +         For the "generate" kind,
    +         it is the original file the generated one is based on.
    +      */
    +      "path": "/home/foo/Cargo.toml"
    +    },
    +    "Cargo.toml": {
    +      "kind": "generate",
    +      "path": "/home/foo/Cargo.toml"
    +    },
    +    "src/main.rs": {
    +      "kind": "copy",
    +      "path": "/home/foo/src/main.rs"
    +    }
    +  }
    +}
    +
    +
  • +
+ + ### Package Selection diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index d28258ea556..362b92b730b 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -123,6 +123,7 @@ Each new feature described below should explain how to use it. * [package-workspace](#package-workspace) --- Allows for packaging and publishing multiple crates in a workspace. * [native-completions](#native-completions) --- Move cargo shell completions to native completions. * [warnings](#warnings) --- controls warning behavior; options for allowing or denying warnings. + * [Package message format](#package-message-format) --- Message format for `cargo package`. ## allow-features @@ -1889,6 +1890,16 @@ Specify which packages participate in [feature unification](../reference/feature * `package` _(unimplemented)_: Dependency features are considered on a package-by-package basis, preferring duplicate builds of dependencies when different sets of features are activated by the packages. +## Package message format + +* Tracking Issue: [#11666](https://github.com/rust-lang/cargo/issues/11666) + +The `--message-format` flag in `cargo package` controls the output message format. +Currently, it only works with the `--list` flag and affects the file listing format, +Requires `-Zunstable-options`. +See [`cargo package --message-format`](../commands/cargo-package.md#option-cargo-package---message-format) +for more information. + # Stabilized and removed features ## Compile progress diff --git a/src/etc/man/cargo-package.1 b/src/etc/man/cargo-package.1 index f07c4a9a2ca..0dca951f851 100644 --- a/src/etc/man/cargo-package.1 +++ b/src/etc/man/cargo-package.1 @@ -149,6 +149,57 @@ to this registry, but if we are packaging multiple inter\-dependent crates, lock\-files will be generated under the assumption that dependencies will be published to this registry. .RE +.sp +\fB\-\-message\-format\fR \fIfmt\fR +.RS 4 +Specifies the output message format. +Currently, it only works with \fB\-\-list\fR and affects the file listing format. +This is unstable and requires \fB\-Zunstable\-options\fR\&. +Valid output formats: +.sp +.RS 4 +\h'-04'\(bu\h'+03'\fBhuman\fR (default): Display in a file\-per\-line format. +.RE +.sp +.RS 4 +\h'-04'\(bu\h'+03'\fBjson\fR: Emit machine\-readable JSON information about each package. +One package per JSON line (Newline delimited JSON). +.sp +.RS 4 +.nf +{ + /* The Package ID Spec of the package. */ + "id": "path+file:///home/foo#0.0.0", + /* Files of this package */ + "files" { + /* Relative path in the archive file. */ + "Cargo.toml.orig": { + /* Where the file is from. + \- "generate" for file being generated during packaging + \- "copy" for file being copied from another location. + */ + "kind": "copy", + /* For the "copy" kind, + it is an absolute path to the actual file content. + For the "generate" kind, + it is the original file the generated one is based on. + */ + "path": "/home/foo/Cargo.toml" + }, + "Cargo.toml": { + "kind": "generate", + "path": "/home/foo/Cargo.toml" + }, + "src/main.rs": { + "kind": "copy", + "path": "/home/foo/src/main.rs" + } + } +} +.fi +.RE +.RE +.RE .SS "Package Selection" By default, when no package selection options are given, the packages selected depend on the selected manifest file (based on the current working directory if diff --git a/tests/testsuite/cargo_package/help/stdout.term.svg b/tests/testsuite/cargo_package/help/stdout.term.svg index 48804731064..671b716e705 100644 --- a/tests/testsuite/cargo_package/help/stdout.term.svg +++ b/tests/testsuite/cargo_package/help/stdout.term.svg @@ -1,4 +1,4 @@ - +