Skip to content

Commit 856f1ba

Browse files
authored
feat(package): add unstable --message-format flag (#15311)
### What does this PR try to resolve? #11666 This adds an unstable `--message-format` flag to `cargo package` to help `--list` output in newline-delimited JSON format. See <https://github.com/weihanglo/cargo/blob/package-list-fmt/src/doc/man/cargo-package.md#package-options> for more on what is provided. Open questions - `--list json` or `--message-format json`, see #15311 (comment) - a single json blob vs N? What is N? See #15311 (comment) - Is the current format `plain` or `human`, see #15311 (comment) - snake_case or kebab-case, see #15311 (comment) ### How should we test and review this PR? * This currently outputs absolute paths. If we don't want to show absolute paths in the JSON output, could switch to relative path to either package root or cwd (I prefer the latter though). * The actual schema, format option names, and field names is open to discuss and change. See also <#12377> for reference.
2 parents 521bd76 + e354769 commit 856f1ba

File tree

14 files changed

+685
-46
lines changed

14 files changed

+685
-46
lines changed

crates/cargo-util-schemas/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
1111
pub mod core;
1212
pub mod manifest;
13+
pub mod messages;
1314
#[cfg(feature = "unstable-schema")]
1415
pub mod schema;
1516

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//! Schemas for JSON messages emitted by Cargo.
2+
3+
use std::collections::BTreeMap;
4+
use std::path::PathBuf;
5+
6+
/// File information of a package archive generated by `cargo package --list`.
7+
#[derive(Debug, serde::Serialize)]
8+
#[serde(rename_all = "snake_case")]
9+
pub struct PackageList {
10+
/// The Package ID Spec of the package.
11+
pub id: crate::core::PackageIdSpec,
12+
/// A map of relative paths in the archive to their detailed file information.
13+
pub files: BTreeMap<PathBuf, PackageFile>,
14+
}
15+
16+
/// Where the file is from.
17+
#[derive(Debug, serde::Serialize)]
18+
#[serde(rename_all = "snake_case", tag = "kind")]
19+
pub enum PackageFile {
20+
/// File being copied from another location.
21+
Copy {
22+
/// An absolute path to the actual file content
23+
path: PathBuf,
24+
},
25+
/// File being generated during packaging
26+
Generate {
27+
/// An absolute path to the original file the generated one is based on.
28+
/// if any.
29+
#[serde(skip_serializing_if = "Option::is_none")]
30+
path: Option<PathBuf>,
31+
},
32+
}

src/bin/cargo/commands/package.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::command_prelude::*;
22

3-
use cargo::ops::{self, PackageOpts};
3+
use cargo::ops;
4+
use cargo::ops::PackageMessageFormat;
5+
use cargo::ops::PackageOpts;
46

57
pub fn cli() -> Command {
68
subcommand("package")
@@ -30,6 +32,13 @@ pub fn cli() -> Command {
3032
"exclude-lockfile",
3133
"Don't include the lock file when packaging",
3234
))
35+
.arg(
36+
opt("message-format", "Output representation (unstable)")
37+
.value_name("FMT")
38+
// This currently requires and only works with `--list`.
39+
.requires("list")
40+
.value_parser(PackageMessageFormat::POSSIBLE_VALUES),
41+
)
3342
.arg_silent_suggestion()
3443
.arg_package_spec_no_all(
3544
"Package(s) to assemble",
@@ -75,12 +84,21 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
7584
}
7685
let specs = args.packages_from_flags()?;
7786

87+
let fmt = if let Some(fmt) = args._value_of("message-format") {
88+
gctx.cli_unstable()
89+
.fail_if_stable_opt("--message-format", 11666)?;
90+
fmt.parse()?
91+
} else {
92+
PackageMessageFormat::Human
93+
};
94+
7895
ops::package(
7996
&ws,
8097
&PackageOpts {
8198
gctx,
8299
verify: !args.flag("no-verify"),
83100
list: args.flag("list"),
101+
fmt,
84102
check_metadata: !args.flag("no-metadata"),
85103
allow_dirty: args.flag("allow-dirty"),
86104
include_lockfile: !args.flag("exclude-lockfile"),

src/cargo/ops/cargo_package/mod.rs

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use std::collections::{BTreeSet, HashMap};
1+
use std::collections::BTreeMap;
2+
use std::collections::BTreeSet;
3+
use std::collections::HashMap;
24
use std::fs::{self, File};
35
use std::io::prelude::*;
46
use std::io::SeekFrom;
@@ -32,6 +34,7 @@ use crate::util::HumanBytes;
3234
use crate::{drop_println, ops};
3335
use anyhow::{bail, Context as _};
3436
use cargo_util::paths;
37+
use cargo_util_schemas::messages;
3538
use flate2::{Compression, GzBuilder};
3639
use tar::{Builder, EntryType, Header, HeaderMode};
3740
use tracing::debug;
@@ -40,10 +43,38 @@ use unicase::Ascii as UncasedAscii;
4043
mod vcs;
4144
mod verify;
4245

46+
/// Message format for `cargo package`.
47+
///
48+
/// Currently only affect the output of the `--list` flag.
49+
#[derive(Debug, Clone)]
50+
pub enum PackageMessageFormat {
51+
Human,
52+
Json,
53+
}
54+
55+
impl PackageMessageFormat {
56+
pub const POSSIBLE_VALUES: [&str; 2] = ["human", "json"];
57+
58+
pub const DEFAULT: &str = "human";
59+
}
60+
61+
impl std::str::FromStr for PackageMessageFormat {
62+
type Err = anyhow::Error;
63+
64+
fn from_str(s: &str) -> Result<PackageMessageFormat, anyhow::Error> {
65+
match s {
66+
"human" => Ok(PackageMessageFormat::Human),
67+
"json" => Ok(PackageMessageFormat::Json),
68+
f => bail!("unknown message format `{f}`"),
69+
}
70+
}
71+
}
72+
4373
#[derive(Clone)]
4474
pub struct PackageOpts<'gctx> {
4575
pub gctx: &'gctx GlobalContext,
4676
pub list: bool,
77+
pub fmt: PackageMessageFormat,
4778
pub check_metadata: bool,
4879
pub allow_dirty: bool,
4980
pub include_lockfile: bool,
@@ -78,9 +109,13 @@ enum FileContents {
78109

79110
enum GeneratedFile {
80111
/// Generates `Cargo.toml` by rewriting the original.
81-
Manifest,
82-
/// Generates `Cargo.lock` in some cases (like if there is a binary).
83-
Lockfile,
112+
///
113+
/// Associated path is the original manifest path.
114+
Manifest(PathBuf),
115+
/// Generates `Cargo.lock`.
116+
///
117+
/// Associated path is the path to the original lock file, if existing.
118+
Lockfile(Option<PathBuf>),
84119
/// Adds a `.cargo_vcs_info.json` file if in a git repo.
85120
VcsInfo(vcs::VcsInfo),
86121
}
@@ -236,8 +271,33 @@ fn do_package<'a>(
236271
let ar_files = prepare_archive(ws, &pkg, &opts)?;
237272

238273
if opts.list {
239-
for ar_file in &ar_files {
240-
drop_println!(ws.gctx(), "{}", ar_file.rel_str);
274+
match opts.fmt {
275+
PackageMessageFormat::Human => {
276+
// While this form is called "human",
277+
// it keeps the old file-per-line format for compatibility.
278+
for ar_file in &ar_files {
279+
drop_println!(ws.gctx(), "{}", ar_file.rel_str);
280+
}
281+
}
282+
PackageMessageFormat::Json => {
283+
let message = messages::PackageList {
284+
id: pkg.package_id().to_spec(),
285+
files: BTreeMap::from_iter(ar_files.into_iter().map(|f| {
286+
let file = match f.contents {
287+
FileContents::OnDisk(path) => messages::PackageFile::Copy { path },
288+
FileContents::Generated(
289+
GeneratedFile::Manifest(path)
290+
| GeneratedFile::Lockfile(Some(path)),
291+
) => messages::PackageFile::Generate { path: Some(path) },
292+
FileContents::Generated(
293+
GeneratedFile::VcsInfo(_) | GeneratedFile::Lockfile(None),
294+
) => messages::PackageFile::Generate { path: None },
295+
};
296+
(f.rel_path, file)
297+
})),
298+
};
299+
let _ = ws.gctx().shell().print_json(&message);
300+
}
241301
}
242302
} else {
243303
let tarball = create_package(ws, &pkg, ar_files, local_reg.as_ref())?;
@@ -444,7 +504,9 @@ fn build_ar_list(
444504
.push(ArchiveFile {
445505
rel_path: PathBuf::from("Cargo.toml"),
446506
rel_str: "Cargo.toml".to_string(),
447-
contents: FileContents::Generated(GeneratedFile::Manifest),
507+
contents: FileContents::Generated(GeneratedFile::Manifest(
508+
pkg.manifest_path().to_owned(),
509+
)),
448510
});
449511
} else {
450512
ws.gctx().shell().warn(&format!(
@@ -454,14 +516,16 @@ fn build_ar_list(
454516
}
455517

456518
if include_lockfile {
519+
let lockfile_path = ws.lock_root().as_path_unlocked().join(LOCKFILE_NAME);
520+
let lockfile_path = lockfile_path.exists().then_some(lockfile_path);
457521
let rel_str = "Cargo.lock";
458522
result
459523
.entry(UncasedAscii::new(rel_str))
460524
.or_insert_with(Vec::new)
461525
.push(ArchiveFile {
462526
rel_path: PathBuf::from(rel_str),
463527
rel_str: rel_str.to_string(),
464-
contents: FileContents::Generated(GeneratedFile::Lockfile),
528+
contents: FileContents::Generated(GeneratedFile::Lockfile(lockfile_path)),
465529
});
466530
}
467531

@@ -780,8 +844,10 @@ fn tar(
780844
}
781845
FileContents::Generated(generated_kind) => {
782846
let contents = match generated_kind {
783-
GeneratedFile::Manifest => publish_pkg.manifest().to_normalized_contents()?,
784-
GeneratedFile::Lockfile => build_lock(ws, &publish_pkg, local_reg)?,
847+
GeneratedFile::Manifest(_) => {
848+
publish_pkg.manifest().to_normalized_contents()?
849+
}
850+
GeneratedFile::Lockfile(_) => build_lock(ws, &publish_pkg, local_reg)?,
785851
GeneratedFile::VcsInfo(ref s) => serde_json::to_string_pretty(s)?,
786852
};
787853
header.set_entry_type(EntryType::file());

src/cargo/ops/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ pub use self::cargo_fetch::{fetch, FetchOptions};
1010
pub use self::cargo_install::{install, install_list};
1111
pub use self::cargo_new::{init, new, NewOptions, NewProjectKind, VersionControl};
1212
pub use self::cargo_output_metadata::{output_metadata, ExportInfo, OutputMetadataOptions};
13-
pub use self::cargo_package::{check_yanked, package, PackageOpts};
13+
pub use self::cargo_package::check_yanked;
14+
pub use self::cargo_package::package;
15+
pub use self::cargo_package::PackageMessageFormat;
16+
pub use self::cargo_package::PackageOpts;
1417
pub use self::cargo_pkgid::pkgid;
1518
pub use self::cargo_read_manifest::read_package;
1619
pub use self::cargo_run::run;

src/cargo/ops/registry/publish.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ pub fn publish(ws: &Workspace<'_>, opts: &PublishOpts<'_>) -> CargoResult<()> {
144144
gctx: opts.gctx,
145145
verify: opts.verify,
146146
list: false,
147+
fmt: ops::PackageMessageFormat::Human,
147148
check_metadata: true,
148149
allow_dirty: opts.allow_dirty,
149150
include_lockfile: true,

src/doc/man/cargo-package.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,48 @@ lock-files will be generated under the assumption that dependencies will be
116116
published to this registry.
117117
{{/option}}
118118

119+
{{#option "`--message-format` _fmt_" }}
120+
Specifies the output message format.
121+
Currently, it only works with `--list` and affects the file listing format.
122+
This is unstable and requires `-Zunstable-options`.
123+
Valid output formats:
124+
125+
- `human` (default): Display in a file-per-line format.
126+
- `json`: Emit machine-readable JSON information about each package.
127+
One package per JSON line (Newline delimited JSON).
128+
```javascript
129+
{
130+
/* The Package ID Spec of the package. */
131+
"id": "path+file:///home/foo#0.0.0",
132+
/* Files of this package */
133+
"files" {
134+
/* Relative path in the archive file. */
135+
"Cargo.toml.orig": {
136+
/* Where the file is from.
137+
- "generate" for file being generated during packaging
138+
- "copy" for file being copied from another location.
139+
*/
140+
"kind": "copy",
141+
/* For the "copy" kind,
142+
it is an absolute path to the actual file content.
143+
For the "generate" kind,
144+
it is the original file the generated one is based on.
145+
*/
146+
"path": "/home/foo/Cargo.toml"
147+
},
148+
"Cargo.toml": {
149+
"kind": "generate",
150+
"path": "/home/foo/Cargo.toml"
151+
},
152+
"src/main.rs": {
153+
"kind": "copy",
154+
"path": "/home/foo/src/main.rs"
155+
}
156+
}
157+
}
158+
```
159+
{{/option}}
160+
119161
{{/options}}
120162

121163
{{> section-package-selection }}

src/doc/man/generated_txt/cargo-package.txt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,45 @@ OPTIONS
114114
multiple inter-dependent crates, lock-files will be generated under
115115
the assumption that dependencies will be published to this registry.
116116

117+
--message-format fmt
118+
Specifies the output message format. Currently, it only works with
119+
--list and affects the file listing format. This is unstable and
120+
requires -Zunstable-options. Valid output formats:
121+
122+
o human (default): Display in a file-per-line format.
123+
124+
o json: Emit machine-readable JSON information about each package.
125+
One package per JSON line (Newline delimited JSON).
126+
{
127+
/* The Package ID Spec of the package. */
128+
"id": "path+file:///home/foo#0.0.0",
129+
/* Files of this package */
130+
"files" {
131+
/* Relative path in the archive file. */
132+
"Cargo.toml.orig": {
133+
/* Where the file is from.
134+
- "generate" for file being generated during packaging
135+
- "copy" for file being copied from another location.
136+
*/
137+
"kind": "copy",
138+
/* For the "copy" kind,
139+
it is an absolute path to the actual file content.
140+
For the "generate" kind,
141+
it is the original file the generated one is based on.
142+
*/
143+
"path": "/home/foo/Cargo.toml"
144+
},
145+
"Cargo.toml": {
146+
"kind": "generate",
147+
"path": "/home/foo/Cargo.toml"
148+
},
149+
"src/main.rs": {
150+
"kind": "copy",
151+
"path": "/home/foo/src/main.rs"
152+
}
153+
}
154+
}
155+
117156
Package Selection
118157
By default, when no package selection options are given, the packages
119158
selected depend on the selected manifest file (based on the current

0 commit comments

Comments
 (0)