Skip to content

Commit f8fef7a

Browse files
committed
Auto merge of #12245 - epage:script, r=weihanglo
feat: Initial support for single-file packages ### What does this PR try to resolve? This is the first step towards #12207. In particular, this focuses on pulling in the [demo](https://github.com/epage/cargo-script-mvs) roughly as-is to serve as a baseline for further PRs. I have a couple months of runtime (multiple times a week) using the version of the demo included here. ### How should we test and review this PR? Commit-by-commit. Most likely, the last (docs) commit should be done first to provide context for the others. Naming is hard. I came up with these terms just so we can have ways to refer to them. Feedback is welcome. - `-Zscript` for this general feature (not great but didn't want to spend too long coming up with a throwaway name) - "single-file package": Rust code and a cargo manifest in a single file - "embedded manifest": the explicit manifest inside of a single-file package - "manifest command": when we interpret `cargo <name>` as referring to a single-file package, and similar to "built-in commands" and "external commands". Keep in mind that this is a very hacky solution with many deficiencies and is mostly starting as a baseline for implementing and reviewing those improvements, including - Switching from `regex` to `syn` for extracting manifests for greater resilience - Matching `cargo new`s logic when sanitizing the inferred package name - Allowing `cargo <name>` to also be a `Cargo.toml` file (for consistency in where manifests are accepted) - Allowing single-file packages to be used wherever a manifest is accepted To minimize conflict pain, I would ask that we consider what feedback can be handled in a follow up (though still mention it!). It'll be much easier creating multiple, independent PRs once this baseline is merged to address concerns. ### Additional information The only affect for people on stable is that they may get a warning if they have an external subcommand that will be shadowed when this feature is implemented. This will allow us to collect feedback, without blocking people, so we can have an idea of how "safe" our precedence scheme is for interpreting `cargo <name>`. As of right now, aliases with a `.` in them will be ignored (well, technically their suffix will be exposed as an alias). We directly inject the name into a lookup string into the config that uses `.` as the separator, so we drill down until we get to the leaf element. Ideally, we would be generating / parsing the lookup key using TOML key syntax so we can better report that this won't be supported after this change :)
2 parents 18a28a1 + 6b0b5a8 commit f8fef7a

File tree

12 files changed

+1681
-44
lines changed

12 files changed

+1681
-44
lines changed

Cargo.lock

+58-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ exclude = [
1313
[workspace.dependencies]
1414
anyhow = "1.0.47"
1515
base64 = "0.21.0"
16+
blake3 = "1.3.3"
1617
bytesize = "1.0"
1718
cargo = { path = "" }
1819
cargo-credential = { version = "0.2.0", path = "credential/cargo-credential" }
@@ -66,6 +67,7 @@ pretty_env_logger = "0.4"
6667
proptest = "1.1.0"
6768
pulldown-cmark = { version = "0.9.2", default-features = false }
6869
rand = "0.8.5"
70+
regex = "1.8.3"
6971
rustfix = "0.6.0"
7072
same-file = "1.0.6"
7173
security-framework = "2.0.0"
@@ -79,6 +81,7 @@ sha2 = "0.10.6"
7981
shell-escape = "0.1.4"
8082
snapbox = { version = "0.4.0", features = ["diff", "path"] }
8183
strip-ansi-escapes = "0.1.0"
84+
syn = { version = "2.0.14", features = ["extra-traits", "full"] }
8285
tar = { version = "0.4.38", default-features = false }
8386
tempfile = "3.1.0"
8487
termcolor = "1.1.2"
@@ -112,6 +115,7 @@ path = "src/cargo/lib.rs"
112115
[dependencies]
113116
anyhow.workspace = true
114117
base64.workspace = true
118+
blake3.workspace = true
115119
bytesize.workspace = true
116120
cargo-platform.workspace = true
117121
cargo-util.workspace = true
@@ -147,7 +151,9 @@ os_info.workspace = true
147151
pasetors.workspace = true
148152
pathdiff.workspace = true
149153
pretty_env_logger = { workspace = true, optional = true }
154+
pulldown-cmark.workspace = true
150155
rand.workspace = true
156+
regex.workspace = true
151157
rustfix.workspace = true
152158
semver.workspace = true
153159
serde = { workspace = true, features = ["derive"] }
@@ -157,6 +163,7 @@ serde_json = { workspace = true, features = ["raw_value"] }
157163
sha1.workspace = true
158164
shell-escape.workspace = true
159165
strip-ansi-escapes.workspace = true
166+
syn.workspace = true
160167
tar.workspace = true
161168
tempfile.workspace = true
162169
termcolor.workspace = true

crates/cargo-test-support/src/lib.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@ impl FileBuilder {
110110

111111
fn mk(&mut self) {
112112
if self.executable {
113-
self.path.set_extension(env::consts::EXE_EXTENSION);
113+
let mut path = self.path.clone().into_os_string();
114+
write!(path, "{}", env::consts::EXE_SUFFIX).unwrap();
115+
self.path = path.into();
114116
}
115117

116118
self.dirname().mkdir_p();

deny.toml

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ allow = [
106106
"MIT-0",
107107
"Apache-2.0",
108108
"BSD-3-Clause",
109+
"BSD-2-Clause",
109110
"MPL-2.0",
110111
"Unicode-DFS-2016",
111112
"CC0-1.0",

src/bin/cargo/cli.rs

+77-14
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ fn expand_aliases(
258258
args: ArgMatches,
259259
mut already_expanded: Vec<String>,
260260
) -> Result<(ArgMatches, GlobalArgs), CliError> {
261-
if let Some((cmd, args)) = args.subcommand() {
261+
if let Some((cmd, sub_args)) = args.subcommand() {
262262
let exec = commands::builtin_exec(cmd);
263263
let aliased_cmd = super::aliased_command(config, cmd);
264264

@@ -274,7 +274,7 @@ fn expand_aliases(
274274
// Here we ignore errors from aliasing as we already favor built-in command,
275275
// and alias doesn't involve in this context.
276276

277-
if let Some(values) = args.get_many::<OsString>("") {
277+
if let Some(values) = sub_args.get_many::<OsString>("") {
278278
// Command is built-in and is not conflicting with alias, but contains ignored values.
279279
return Err(anyhow::format_err!(
280280
"\
@@ -305,17 +305,34 @@ For more information, see issue #10049 <https://github.com/rust-lang/cargo/issue
305305
))?;
306306
}
307307
}
308+
if commands::run::is_manifest_command(cmd) {
309+
if config.cli_unstable().script {
310+
return Ok((args, GlobalArgs::default()));
311+
} else {
312+
config.shell().warn(format_args!(
313+
"\
314+
user-defined alias `{cmd}` has the appearance of a manfiest-command
315+
This was previously accepted but will be phased out when `-Zscript` is stabilized.
316+
For more information, see issue #12207 <https://github.com/rust-lang/cargo/issues/12207>."
317+
))?;
318+
}
319+
}
308320

309321
let mut alias = alias
310322
.into_iter()
311323
.map(|s| OsString::from(s))
312324
.collect::<Vec<_>>();
313-
alias.extend(args.get_many::<OsString>("").unwrap_or_default().cloned());
325+
alias.extend(
326+
sub_args
327+
.get_many::<OsString>("")
328+
.unwrap_or_default()
329+
.cloned(),
330+
);
314331
// new_args strips out everything before the subcommand, so
315332
// capture those global options now.
316333
// Note that an alias to an external command will not receive
317334
// these arguments. That may be confusing, but such is life.
318-
let global_args = GlobalArgs::new(args);
335+
let global_args = GlobalArgs::new(sub_args);
319336
let new_args = cli().no_binary_name(true).try_get_matches_from(alias)?;
320337

321338
let new_cmd = new_args.subcommand_name().expect("subcommand is required");
@@ -382,19 +399,54 @@ fn config_configure(
382399
Ok(())
383400
}
384401

402+
/// Precedence isn't the most obvious from this function because
403+
/// - Some is determined by `expand_aliases`
404+
/// - Some is enforced by `avoid_ambiguity_between_builtins_and_manifest_commands`
405+
///
406+
/// In actuality, it is:
407+
/// 1. built-ins xor manifest-command
408+
/// 2. aliases
409+
/// 3. external subcommands
385410
fn execute_subcommand(config: &mut Config, cmd: &str, subcommand_args: &ArgMatches) -> CliResult {
386411
if let Some(exec) = commands::builtin_exec(cmd) {
387412
return exec(config, subcommand_args);
388413
}
389414

390-
let mut ext_args: Vec<&OsStr> = vec![OsStr::new(cmd)];
391-
ext_args.extend(
392-
subcommand_args
393-
.get_many::<OsString>("")
394-
.unwrap_or_default()
395-
.map(OsString::as_os_str),
396-
);
397-
super::execute_external_subcommand(config, cmd, &ext_args)
415+
if commands::run::is_manifest_command(cmd) {
416+
let ext_path = super::find_external_subcommand(config, cmd);
417+
if !config.cli_unstable().script && ext_path.is_some() {
418+
config.shell().warn(format_args!(
419+
"\
420+
external subcommand `{cmd}` has the appearance of a manfiest-command
421+
This was previously accepted but will be phased out when `-Zscript` is stabilized.
422+
For more information, see issue #12207 <https://github.com/rust-lang/cargo/issues/12207>.",
423+
))?;
424+
let mut ext_args = vec![OsStr::new(cmd)];
425+
ext_args.extend(
426+
subcommand_args
427+
.get_many::<OsString>("")
428+
.unwrap_or_default()
429+
.map(OsString::as_os_str),
430+
);
431+
super::execute_external_subcommand(config, cmd, &ext_args)
432+
} else {
433+
let ext_args: Vec<OsString> = subcommand_args
434+
.get_many::<OsString>("")
435+
.unwrap_or_default()
436+
.cloned()
437+
.collect();
438+
commands::run::exec_manifest_command(config, cmd, &ext_args)
439+
}
440+
} else {
441+
let mut ext_args = vec![OsStr::new(cmd)];
442+
ext_args.extend(
443+
subcommand_args
444+
.get_many::<OsString>("")
445+
.unwrap_or_default()
446+
.map(OsString::as_os_str),
447+
);
448+
super::execute_external_subcommand(config, cmd, &ext_args)
449+
}
398450
}
399451

400452
#[derive(Default)]
@@ -438,9 +490,9 @@ pub fn cli() -> Command {
438490
#[allow(clippy::disallowed_methods)]
439491
let is_rustup = std::env::var_os("RUSTUP_HOME").is_some();
440492
let usage = if is_rustup {
441-
"cargo [+toolchain] [OPTIONS] [COMMAND]"
493+
"cargo [+toolchain] [OPTIONS] [COMMAND]\n cargo [+toolchain] [OPTIONS] -Zscript <MANIFEST_RS> [ARGS]..."
442494
} else {
443-
"cargo [OPTIONS] [COMMAND]"
495+
"cargo [OPTIONS] [COMMAND]\n cargo [OPTIONS] -Zscript <MANIFEST_RS> [ARGS]..."
444496
};
445497
Command::new("cargo")
446498
// Subcommands all count their args' display order independently (from 0),
@@ -570,3 +622,14 @@ impl LazyConfig {
570622
fn verify_cli() {
571623
cli().debug_assert();
572624
}
625+
626+
#[test]
627+
fn avoid_ambiguity_between_builtins_and_manifest_commands() {
628+
for cmd in commands::builtin() {
629+
let name = cmd.get_name();
630+
assert!(
631+
!commands::run::is_manifest_command(&name),
632+
"built-in command {name} is ambiguous with manifest-commands"
633+
)
634+
}
635+
}

0 commit comments

Comments
 (0)