From 87e128b3ceb7d35af8a32df0733f0234affcdb05 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 7 Jun 2023 14:53:37 -0500 Subject: [PATCH 01/10] feat: Add -Zscript --- src/cargo/core/features.rs | 2 ++ src/doc/src/reference/unstable.md | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index 7e86a359635..e953a2b598e 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -728,6 +728,7 @@ unstable_cli_options!( registry_auth: bool = ("Authentication for alternative registries, and generate registry authentication tokens using asymmetric cryptography"), rustdoc_map: bool = ("Allow passing external documentation mappings to rustdoc"), rustdoc_scrape_examples: bool = ("Allows Rustdoc to scrape code examples from reverse-dependencies"), + script: bool = ("Enable support for single-file, `.rs` packages"), separate_nightlies: bool = (HIDDEN), skip_rustdoc_fingerprint: bool = (HIDDEN), target_applies_to_host: bool = ("Enable the `target-applies-to-host` key in the .cargo/config.toml file"), @@ -1102,6 +1103,7 @@ impl CliUnstable { "rustdoc-scrape-examples" => self.rustdoc_scrape_examples = parse_empty(k, v)?, "separate-nightlies" => self.separate_nightlies = parse_empty(k, v)?, "skip-rustdoc-fingerprint" => self.skip_rustdoc_fingerprint = parse_empty(k, v)?, + "script" => self.script = parse_empty(k, v)?, "target-applies-to-host" => self.target_applies_to_host = parse_empty(k, v)?, "unstable-options" => self.unstable_options = parse_empty(k, v)?, _ => bail!("unknown `-Z` flag specified: {}", k), diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index 7484db9b582..67c084de26d 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -107,6 +107,7 @@ Each new feature described below should explain how to use it. * [registry-auth](#registry-auth) --- Adds support for authenticated registries, and generate registry authentication tokens using asymmetric cryptography. * Other * [gitoxide](#gitoxide) --- Use `gitoxide` instead of `git2` for a set of operations. + * [script](#script) --- Enable support for single-file `.rs` packages. ### allow-features @@ -1392,6 +1393,10 @@ Valid operations are the following: * When the unstable feature is on, fetching/cloning a git repository is always a shallow fetch. This roughly equals to `git fetch --depth 1` everywhere. * Even with the presence of `Cargo.lock` or specifying a commit `{ rev = "…" }`, gitoxide is still smart enough to shallow fetch without unshallowing the existing repository. +### script + +* Tracking Issue: [#12207](https://github.com/rust-lang/cargo/issues/12207) + ### `[lints]` * Tracking Issue: [#12115](https://github.com/rust-lang/cargo/issues/12115) From c0dd8ae36b85af5a848a933acb3dfe4acdad0e36 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 7 Jun 2023 16:11:50 -0500 Subject: [PATCH 02/10] refactor(cli): Allow use of both args/sub_args --- src/bin/cargo/cli.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/bin/cargo/cli.rs b/src/bin/cargo/cli.rs index f46a499e4a6..117595ef1be 100644 --- a/src/bin/cargo/cli.rs +++ b/src/bin/cargo/cli.rs @@ -258,7 +258,7 @@ fn expand_aliases( args: ArgMatches, mut already_expanded: Vec, ) -> Result<(ArgMatches, GlobalArgs), CliError> { - if let Some((cmd, args)) = args.subcommand() { + if let Some((cmd, sub_args)) = args.subcommand() { let exec = commands::builtin_exec(cmd); let aliased_cmd = super::aliased_command(config, cmd); @@ -274,7 +274,7 @@ fn expand_aliases( // Here we ignore errors from aliasing as we already favor built-in command, // and alias doesn't involve in this context. - if let Some(values) = args.get_many::("") { + if let Some(values) = sub_args.get_many::("") { // Command is built-in and is not conflicting with alias, but contains ignored values. return Err(anyhow::format_err!( "\ @@ -310,12 +310,17 @@ For more information, see issue #10049 >(); - alias.extend(args.get_many::("").unwrap_or_default().cloned()); + alias.extend( + sub_args + .get_many::("") + .unwrap_or_default() + .cloned(), + ); // new_args strips out everything before the subcommand, so // capture those global options now. // Note that an alias to an external command will not receive // these arguments. That may be confusing, but such is life. - let global_args = GlobalArgs::new(args); + let global_args = GlobalArgs::new(sub_args); let new_args = cli().no_binary_name(true).try_get_matches_from(alias)?; let new_cmd = new_args.subcommand_name().expect("subcommand is required"); From 21736eda0c2eab0332e8dd2c18ff237f78763d9d Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 7 Jun 2023 16:11:50 -0500 Subject: [PATCH 03/10] feat(cli): Interpret some subcommands as manifest-commands --- src/bin/cargo/cli.rs | 6 +- src/bin/cargo/commands/run.rs | 19 +++++- tests/testsuite/main.rs | 1 + tests/testsuite/script.rs | 106 ++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 tests/testsuite/script.rs diff --git a/src/bin/cargo/cli.rs b/src/bin/cargo/cli.rs index 117595ef1be..7357dc8e6e5 100644 --- a/src/bin/cargo/cli.rs +++ b/src/bin/cargo/cli.rs @@ -399,7 +399,11 @@ fn execute_subcommand(config: &mut Config, cmd: &str, subcommand_args: &ArgMatch .unwrap_or_default() .map(OsString::as_os_str), ); - super::execute_external_subcommand(config, cmd, &ext_args) + if commands::run::is_manifest_command(cmd) { + commands::run::exec_manifest_command(config, cmd, &ext_args) + } else { + super::execute_external_subcommand(config, cmd, &ext_args) + } } #[derive(Default)] diff --git a/src/bin/cargo/commands/run.rs b/src/bin/cargo/commands/run.rs index cde754c7a03..0414d8e9bb8 100644 --- a/src/bin/cargo/commands/run.rs +++ b/src/bin/cargo/commands/run.rs @@ -1,3 +1,7 @@ +use std::ffi::OsStr; +use std::ffi::OsString; +use std::path::Path; + use crate::command_prelude::*; use crate::util::restricted_names::is_glob_pattern; use cargo::core::Verbosity; @@ -13,7 +17,7 @@ pub fn cli() -> Command { .arg( Arg::new("args") .help("Arguments for the binary or example to run") - .value_parser(value_parser!(std::ffi::OsString)) + .value_parser(value_parser!(OsString)) .num_args(0..) .trailing_var_arg(true), ) @@ -101,3 +105,16 @@ pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { } }) } + +pub fn is_manifest_command(arg: &str) -> bool { + let path = Path::new(arg); + 1 < path.components().count() || path.extension() == Some(OsStr::new("rs")) +} + +pub fn exec_manifest_command(config: &Config, cmd: &str, _args: &[&OsStr]) -> CliResult { + if !config.cli_unstable().script { + return Err(anyhow::anyhow!("running `{cmd}` requires `-Zscript`").into()); + } + + todo!("support for running manifest-commands is not yet implemented") +} diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index 170a226674f..2c282c0a33e 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -123,6 +123,7 @@ mod rustdoc_extern_html; mod rustdocflags; mod rustflags; mod rustup; +mod script; mod search; mod shell_quoting; mod source_replacement; diff --git a/tests/testsuite/script.rs b/tests/testsuite/script.rs new file mode 100644 index 00000000000..c20a9c33c10 --- /dev/null +++ b/tests/testsuite/script.rs @@ -0,0 +1,106 @@ +const ECHO_SCRIPT: &str = r#"#!/usr/bin/env cargo + +fn main() { + let mut args = std::env::args_os(); + let bin = args.next().unwrap().to_str().unwrap().to_owned(); + let args = args.collect::>(); + println!("bin: {bin}"); + println!("args: {args:?}"); +} +"#; + +#[cargo_test] +fn basic_rs() { + let p = cargo_test_support::project() + .file("echo.rs", ECHO_SCRIPT) + .build(); + + p.cargo("-Zscript echo.rs") + .arg("--help") // An arg that, if processed by cargo, will cause problems + .masquerade_as_nightly_cargo(&["script"]) + .with_status(101) + .with_stdout("") + .with_stderr("\ +thread 'main' panicked at 'not yet implemented: support for running manifest-commands is not yet implemented', [..] +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +") + .run(); +} + +#[cargo_test] +fn basic_path() { + let p = cargo_test_support::project() + .file("echo", ECHO_SCRIPT) + .build(); + + p.cargo("-Zscript ./echo") + .arg("--help") // An arg that, if processed by cargo, will cause problems + .masquerade_as_nightly_cargo(&["script"]) + .with_status(101) + .with_stdout("") + .with_stderr("\ +thread 'main' panicked at 'not yet implemented: support for running manifest-commands is not yet implemented', [..] +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +") + .run(); +} + +#[cargo_test] +fn path_required() { + let p = cargo_test_support::project() + .file("echo", ECHO_SCRIPT) + .build(); + + p.cargo("-Zscript echo") + .arg("--help") // An arg that, if processed by cargo, will cause problems + .masquerade_as_nightly_cargo(&["script"]) + .with_status(101) + .with_stdout("") + .with_stderr( + "\ +error: no such command: `echo` + +Did you mean `bench`? + +View all installed commands with `cargo --list` +", + ) + .run(); +} + +#[cargo_test] +fn requires_nightly() { + let p = cargo_test_support::project() + .file("echo.rs", ECHO_SCRIPT) + .build(); + + p.cargo("echo.rs") + .arg("--help") // An arg that, if processed by cargo, will cause problems + .with_status(101) + .with_stdout("") + .with_stderr( + "\ +error: running `echo.rs` requires `-Zscript` +", + ) + .run(); +} + +#[cargo_test] +fn requires_z_flag() { + let p = cargo_test_support::project() + .file("echo.rs", ECHO_SCRIPT) + .build(); + + p.cargo("echo.rs") + .arg("--help") // An arg that, if processed by cargo, will cause problems + .masquerade_as_nightly_cargo(&["script"]) + .with_status(101) + .with_stdout("") + .with_stderr( + "\ +error: running `echo.rs` requires `-Zscript` +", + ) + .run(); +} From b2b4d9771fcc1c4d379aa36094b354799decdede Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 8 Jun 2023 13:27:40 -0500 Subject: [PATCH 04/10] test(cli): Verify precedence over external subcommands --- crates/cargo-test-support/src/lib.rs | 4 +- tests/testsuite/script.rs | 55 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/crates/cargo-test-support/src/lib.rs b/crates/cargo-test-support/src/lib.rs index 0997d7e8023..a2fa54c601d 100644 --- a/crates/cargo-test-support/src/lib.rs +++ b/crates/cargo-test-support/src/lib.rs @@ -110,7 +110,9 @@ impl FileBuilder { fn mk(&mut self) { if self.executable { - self.path.set_extension(env::consts::EXE_EXTENSION); + let mut path = self.path.clone().into_os_string(); + write!(path, "{}", env::consts::EXE_SUFFIX).unwrap(); + self.path = path.into(); } self.dirname().mkdir_p(); diff --git a/tests/testsuite/script.rs b/tests/testsuite/script.rs index c20a9c33c10..b4b1933ef19 100644 --- a/tests/testsuite/script.rs +++ b/tests/testsuite/script.rs @@ -9,6 +9,10 @@ fn main() { } "#; +fn path() -> Vec { + std::env::split_paths(&std::env::var_os("PATH").unwrap_or_default()).collect() +} + #[cargo_test] fn basic_rs() { let p = cargo_test_support::project() @@ -68,6 +72,57 @@ error: no such command: `echo` .run(); } +#[cargo_test] +#[cfg(unix)] +fn manifest_precedence_over_plugins() { + let p = cargo_test_support::project() + .file("echo.rs", ECHO_SCRIPT) + .executable(std::path::Path::new("path-test").join("cargo-echo.rs"), "") + .build(); + + // With path - fmt is there with known description + let mut path = path(); + path.push(p.root().join("path-test")); + let path = std::env::join_paths(path.iter()).unwrap(); + + p.cargo("-Zscript echo.rs") + .arg("--help") // An arg that, if processed by cargo, will cause problems + .env("PATH", &path) + .masquerade_as_nightly_cargo(&["script"]) + .with_status(101) + .with_stdout("") + .with_stderr("\ +thread 'main' panicked at 'not yet implemented: support for running manifest-commands is not yet implemented', [..] +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +") + .run(); +} + +#[cargo_test] +#[cfg(unix)] +fn manifest_precedence_over_plugins_stable() { + let p = cargo_test_support::project() + .file("echo.rs", ECHO_SCRIPT) + .executable(std::path::Path::new("path-test").join("cargo-echo.rs"), "") + .build(); + + let mut path = path(); + path.push(p.root().join("path-test")); + let path = std::env::join_paths(path.iter()).unwrap(); + + p.cargo("echo.rs") + .arg("--help") // An arg that, if processed by cargo, will cause problems + .env("PATH", &path) + .with_status(101) + .with_stdout("") + .with_stderr( + "\ +error: running `echo.rs` requires `-Zscript` +", + ) + .run(); +} + #[cargo_test] fn requires_nightly() { let p = cargo_test_support::project() From 1a30fc83192d1d841ce0d012d30ec40831008399 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 7 Jun 2023 16:11:50 -0500 Subject: [PATCH 05/10] feat(cli): Define precedence among subcommands I decided to start things off fairly simple. It either looks like the user is specifying a manifest or they aren't. This is assuming we only need to handle `cargo foo.rs` and `cargo ./foo.rs` and not `cargo foo`. I think in most shebang cases, path multiple components will present which makes that a dead giveaway and likely to not overlap with aliases and subcommands. For reference, dlang's dub goes a lot further 1. Checks for the subcommand name being `-` 2. Checks if subcommand name ends with `.d` 3. Checks if subcommand name is built-in 4. Checks if a file with the subcommand name exists 5. Checks if a file with the subcommand name + `.d` exists This would allow a `run.d` to override `dub-run` which doesn't seem good. --- src/bin/cargo/cli.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/bin/cargo/cli.rs b/src/bin/cargo/cli.rs index 7357dc8e6e5..62e46eb1ae1 100644 --- a/src/bin/cargo/cli.rs +++ b/src/bin/cargo/cli.rs @@ -305,6 +305,9 @@ For more information, see issue #10049 CliResult { if let Some(exec) = commands::builtin_exec(cmd) { return exec(config, subcommand_args); @@ -579,3 +590,14 @@ impl LazyConfig { fn verify_cli() { cli().debug_assert(); } + +#[test] +fn avoid_ambiguity_between_builtins_and_manifest_commands() { + for cmd in commands::builtin() { + let name = cmd.get_name(); + assert!( + !commands::run::is_manifest_command(&name), + "built-in command {name} is ambiguous with manifest-commands" + ) + } +} From 3c15d24960fe1f6d71c081da142d6e4367cc67da Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 7 Jun 2023 16:50:38 -0500 Subject: [PATCH 06/10] fix(cli): Warn on stable for precedence changes This will give us a window to collect feedback on if this affects anyone. --- src/bin/cargo/cli.rs | 24 ++++++++++++++++++++++-- tests/testsuite/script.rs | 7 ++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/bin/cargo/cli.rs b/src/bin/cargo/cli.rs index 62e46eb1ae1..4d47a8e6b6f 100644 --- a/src/bin/cargo/cli.rs +++ b/src/bin/cargo/cli.rs @@ -306,7 +306,16 @@ For more information, see issue #10049 ." + ))?; + } } let mut alias = alias @@ -411,7 +420,18 @@ fn execute_subcommand(config: &mut Config, cmd: &str, subcommand_args: &ArgMatch .map(OsString::as_os_str), ); if commands::run::is_manifest_command(cmd) { - commands::run::exec_manifest_command(config, cmd, &ext_args) + let ext_path = super::find_external_subcommand(config, cmd); + if !config.cli_unstable().script && ext_path.is_some() { + config.shell().warn(format_args!( + "\ +external subcommand `{cmd}` has the appearance of a manfiest-command +This was previously accepted but will be phased out when `-Zscript` is stabilized. +For more information, see issue #12207 .", + ))?; + super::execute_external_subcommand(config, cmd, &ext_args) + } else { + commands::run::exec_manifest_command(config, cmd, &ext_args) + } } else { super::execute_external_subcommand(config, cmd, &ext_args) } diff --git a/tests/testsuite/script.rs b/tests/testsuite/script.rs index b4b1933ef19..169e08278a1 100644 --- a/tests/testsuite/script.rs +++ b/tests/testsuite/script.rs @@ -100,7 +100,7 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace #[cargo_test] #[cfg(unix)] -fn manifest_precedence_over_plugins_stable() { +fn warn_when_plugin_masks_manifest_on_stable() { let p = cargo_test_support::project() .file("echo.rs", ECHO_SCRIPT) .executable(std::path::Path::new("path-test").join("cargo-echo.rs"), "") @@ -113,11 +113,12 @@ fn manifest_precedence_over_plugins_stable() { p.cargo("echo.rs") .arg("--help") // An arg that, if processed by cargo, will cause problems .env("PATH", &path) - .with_status(101) .with_stdout("") .with_stderr( "\ -error: running `echo.rs` requires `-Zscript` +warning: external subcommand `echo.rs` has the appearance of a manfiest-command +This was previously accepted but will be phased out when `-Zscript` is stabilized. +For more information, see issue #12207 . ", ) .run(); From 2bd9f144f859368ebca725954f234a77032121d6 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 8 Jun 2023 15:33:51 -0500 Subject: [PATCH 07/10] refactor(cli): Pull out run error handling --- src/bin/cargo/commands/run.rs | 48 ++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/bin/cargo/commands/run.rs b/src/bin/cargo/commands/run.rs index 0414d8e9bb8..3b61ff9c3a7 100644 --- a/src/bin/cargo/commands/run.rs +++ b/src/bin/cargo/commands/run.rs @@ -81,29 +81,7 @@ pub fn exec(config: &mut Config, args: &ArgMatches) -> CliResult { } }; - ops::run(&ws, &compile_opts, &values_os(args, "args")).map_err(|err| { - let proc_err = match err.downcast_ref::() { - Some(e) => e, - None => return CliError::new(err, 101), - }; - - // If we never actually spawned the process then that sounds pretty - // bad and we always want to forward that up. - let exit_code = match proc_err.code { - Some(exit) => exit, - None => return CliError::new(err, 101), - }; - - // If `-q` was passed then we suppress extra error information about - // a failed process, we assume the process itself printed out enough - // information about why it failed so we don't do so as well - let is_quiet = config.shell().verbosity() == Verbosity::Quiet; - if is_quiet { - CliError::code(exit_code) - } else { - CliError::new(err, exit_code) - } - }) + ops::run(&ws, &compile_opts, &values_os(args, "args")).map_err(|err| to_run_error(config, err)) } pub fn is_manifest_command(arg: &str) -> bool { @@ -118,3 +96,27 @@ pub fn exec_manifest_command(config: &Config, cmd: &str, _args: &[&OsStr]) -> Cl todo!("support for running manifest-commands is not yet implemented") } + +fn to_run_error(config: &cargo::util::Config, err: anyhow::Error) -> CliError { + let proc_err = match err.downcast_ref::() { + Some(e) => e, + None => return CliError::new(err, 101), + }; + + // If we never actually spawned the process then that sounds pretty + // bad and we always want to forward that up. + let exit_code = match proc_err.code { + Some(exit) => exit, + None => return CliError::new(err, 101), + }; + + // If `-q` was passed then we suppress extra error information about + // a failed process, we assume the process itself printed out enough + // information about why it failed so we don't do so as well + let is_quiet = config.shell().verbosity() == Verbosity::Quiet; + if is_quiet { + CliError::code(exit_code) + } else { + CliError::new(err, exit_code) + } +} From c421e0bf3a08a7bb116e9140c4f3f2d65688f16a Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 8 Jun 2023 15:37:58 -0500 Subject: [PATCH 08/10] refactor(cli): Align the two run's params --- src/bin/cargo/cli.rs | 26 +++++++++++++++++++------- src/bin/cargo/commands/run.rs | 2 +- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/bin/cargo/cli.rs b/src/bin/cargo/cli.rs index 4d47a8e6b6f..cd4f797010f 100644 --- a/src/bin/cargo/cli.rs +++ b/src/bin/cargo/cli.rs @@ -412,13 +412,6 @@ fn execute_subcommand(config: &mut Config, cmd: &str, subcommand_args: &ArgMatch return exec(config, subcommand_args); } - let mut ext_args: Vec<&OsStr> = vec![OsStr::new(cmd)]; - ext_args.extend( - subcommand_args - .get_many::("") - .unwrap_or_default() - .map(OsString::as_os_str), - ); if commands::run::is_manifest_command(cmd) { let ext_path = super::find_external_subcommand(config, cmd); if !config.cli_unstable().script && ext_path.is_some() { @@ -428,11 +421,30 @@ external subcommand `{cmd}` has the appearance of a manfiest-command This was previously accepted but will be phased out when `-Zscript` is stabilized. For more information, see issue #12207 .", ))?; + let mut ext_args = vec![OsStr::new(cmd)]; + ext_args.extend( + subcommand_args + .get_many::("") + .unwrap_or_default() + .map(OsString::as_os_str), + ); super::execute_external_subcommand(config, cmd, &ext_args) } else { + let ext_args: Vec = subcommand_args + .get_many::("") + .unwrap_or_default() + .cloned() + .collect(); commands::run::exec_manifest_command(config, cmd, &ext_args) } } else { + let mut ext_args = vec![OsStr::new(cmd)]; + ext_args.extend( + subcommand_args + .get_many::("") + .unwrap_or_default() + .map(OsString::as_os_str), + ); super::execute_external_subcommand(config, cmd, &ext_args) } } diff --git a/src/bin/cargo/commands/run.rs b/src/bin/cargo/commands/run.rs index 3b61ff9c3a7..117a7c8e67c 100644 --- a/src/bin/cargo/commands/run.rs +++ b/src/bin/cargo/commands/run.rs @@ -89,7 +89,7 @@ pub fn is_manifest_command(arg: &str) -> bool { 1 < path.components().count() || path.extension() == Some(OsStr::new("rs")) } -pub fn exec_manifest_command(config: &Config, cmd: &str, _args: &[&OsStr]) -> CliResult { +pub fn exec_manifest_command(config: &Config, cmd: &str, _args: &[OsString]) -> CliResult { if !config.cli_unstable().script { return Err(anyhow::anyhow!("running `{cmd}` requires `-Zscript`").into()); } From 33c9d8e2fd66566f3691048c7160ee697f8293f0 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 8 Jun 2023 14:47:02 -0500 Subject: [PATCH 09/10] feat(cli): Pull in cargo-script-mvs core logic This is no where near the implementation we want but I think we should develop incrementally on top of what we already have. See https://github.com/epage/cargo-script-mvs/tree/main --- Cargo.lock | 65 ++- Cargo.toml | 7 + deny.toml | 1 + src/bin/cargo/commands/run.rs | 18 +- src/cargo/util/toml/embedded.rs | 860 ++++++++++++++++++++++++++++++++ src/cargo/util/toml/mod.rs | 1 + tests/testsuite/script.rs | 404 ++++++++++++++- 7 files changed, 1322 insertions(+), 34 deletions(-) create mode 100644 src/cargo/util/toml/embedded.rs diff --git a/Cargo.lock b/Cargo.lock index 806123dab49..b8d4d9d3056 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.2.6" @@ -81,12 +90,24 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + [[package]] name = "arrayvec" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + [[package]] name = "atty" version = "0.2.14" @@ -169,6 +190,20 @@ dependencies = [ "typenum", ] +[[package]] +name = "blake3" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "729b71f35bd3fa1a4c86b85d32c8b9069ea7fe14f7a53cfabb65f62d4265b888" +dependencies = [ + "arrayref", + "arrayvec 0.7.2", + "cc", + "cfg-if", + "constant_time_eq", + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -248,6 +283,7 @@ version = "0.73.0" dependencies = [ "anyhow", "base64", + "blake3", "bytesize", "cargo-platform 0.1.3", "cargo-test-macro", @@ -287,7 +323,9 @@ dependencies = [ "pasetors", "pathdiff", "pretty_env_logger", + "pulldown-cmark", "rand", + "regex", "rustfix", "same-file", "semver", @@ -299,6 +337,7 @@ dependencies = [ "shell-escape", "snapbox", "strip-ansi-escapes", + "syn 2.0.14", "tar", "tempfile", "termcolor", @@ -516,6 +555,12 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" +[[package]] +name = "constant_time_eq" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" + [[package]] name = "content_inspector" version = "0.2.4" @@ -1652,7 +1697,7 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" dependencies = [ - "aho-corasick", + "aho-corasick 0.7.20", "bstr", "fnv", "log", @@ -2535,7 +2580,7 @@ dependencies = [ "rand", "rand_chacha", "rand_xorshift", - "regex-syntax", + "regex-syntax 0.6.29", "rusty-fork", "tempfile", "unarray", @@ -2663,13 +2708,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.3" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ - "aho-corasick", + "aho-corasick 1.0.2", "memchr", - "regex-syntax", + "regex-syntax 0.7.2", ] [[package]] @@ -2684,6 +2729,12 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +[[package]] +name = "regex-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" + [[package]] name = "resolver-tests" version = "0.0.0" @@ -3451,7 +3502,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" dependencies = [ - "arrayvec", + "arrayvec 0.5.2", "utf8parse", "vte_generate_state_changes", ] diff --git a/Cargo.toml b/Cargo.toml index 17f5af414b6..d6d7e76d669 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ exclude = [ [workspace.dependencies] anyhow = "1.0.47" base64 = "0.21.0" +blake3 = "1.3.3" bytesize = "1.0" cargo = { path = "" } cargo-credential = { version = "0.2.0", path = "credential/cargo-credential" } @@ -66,6 +67,7 @@ pretty_env_logger = "0.4" proptest = "1.1.0" pulldown-cmark = { version = "0.9.2", default-features = false } rand = "0.8.5" +regex = "1.8.3" rustfix = "0.6.0" same-file = "1.0.6" security-framework = "2.0.0" @@ -79,6 +81,7 @@ sha2 = "0.10.6" shell-escape = "0.1.4" snapbox = { version = "0.4.0", features = ["diff", "path"] } strip-ansi-escapes = "0.1.0" +syn = { version = "2.0.14", features = ["extra-traits", "full"] } tar = { version = "0.4.38", default-features = false } tempfile = "3.1.0" termcolor = "1.1.2" @@ -112,6 +115,7 @@ path = "src/cargo/lib.rs" [dependencies] anyhow.workspace = true base64.workspace = true +blake3.workspace = true bytesize.workspace = true cargo-platform.workspace = true cargo-util.workspace = true @@ -147,7 +151,9 @@ os_info.workspace = true pasetors.workspace = true pathdiff.workspace = true pretty_env_logger = { workspace = true, optional = true } +pulldown-cmark.workspace = true rand.workspace = true +regex.workspace = true rustfix.workspace = true semver.workspace = true serde = { workspace = true, features = ["derive"] } @@ -157,6 +163,7 @@ serde_json = { workspace = true, features = ["raw_value"] } sha1.workspace = true shell-escape.workspace = true strip-ansi-escapes.workspace = true +syn.workspace = true tar.workspace = true tempfile.workspace = true termcolor.workspace = true diff --git a/deny.toml b/deny.toml index 89d08eacc8f..71d74cb5b8a 100644 --- a/deny.toml +++ b/deny.toml @@ -106,6 +106,7 @@ allow = [ "MIT-0", "Apache-2.0", "BSD-3-Clause", + "BSD-2-Clause", "MPL-2.0", "Unicode-DFS-2016", "CC0-1.0", diff --git a/src/bin/cargo/commands/run.rs b/src/bin/cargo/commands/run.rs index 117a7c8e67c..f5d970b94b0 100644 --- a/src/bin/cargo/commands/run.rs +++ b/src/bin/cargo/commands/run.rs @@ -89,12 +89,26 @@ pub fn is_manifest_command(arg: &str) -> bool { 1 < path.components().count() || path.extension() == Some(OsStr::new("rs")) } -pub fn exec_manifest_command(config: &Config, cmd: &str, _args: &[OsString]) -> CliResult { +pub fn exec_manifest_command(config: &Config, cmd: &str, args: &[OsString]) -> CliResult { if !config.cli_unstable().script { return Err(anyhow::anyhow!("running `{cmd}` requires `-Zscript`").into()); } - todo!("support for running manifest-commands is not yet implemented") + let manifest_path = Path::new(cmd); + if !manifest_path.exists() { + return Err( + anyhow::anyhow!("manifest `{}` does not exist", manifest_path.display()).into(), + ); + } + let manifest_path = crate::util::try_canonicalize(manifest_path)?; + let script = cargo::util::toml::embedded::RawScript::parse_from(&manifest_path)?; + let ws = script.to_workspace(config)?; + + let mut compile_opts = + cargo::ops::CompileOptions::new(config, cargo::core::compiler::CompileMode::Build)?; + compile_opts.spec = cargo::ops::Packages::Default; + + cargo::ops::run(&ws, &compile_opts, args).map_err(|err| to_run_error(config, err)) } fn to_run_error(config: &cargo::util::Config, err: anyhow::Error) -> CliError { diff --git a/src/cargo/util/toml/embedded.rs b/src/cargo/util/toml/embedded.rs new file mode 100644 index 00000000000..43c964abc66 --- /dev/null +++ b/src/cargo/util/toml/embedded.rs @@ -0,0 +1,860 @@ +use anyhow::Context as _; + +use crate::core::Workspace; +use crate::CargoResult; +use crate::Config; + +const DEFAULT_EDITION: crate::core::features::Edition = + crate::core::features::Edition::LATEST_STABLE; +const DEFAULT_VERSION: &str = "0.0.0"; +const DEFAULT_PUBLISH: bool = false; + +pub struct RawScript { + manifest: String, + body: String, + path: std::path::PathBuf, +} + +impl RawScript { + pub fn parse_from(path: &std::path::Path) -> CargoResult { + let body = std::fs::read_to_string(path) + .with_context(|| format!("failed to script at {}", path.display()))?; + Self::parse(&body, path) + } + + pub fn parse(body: &str, path: &std::path::Path) -> CargoResult { + let comment = match extract_comment(body) { + Ok(manifest) => Some(manifest), + Err(err) => { + log::trace!("failed to extract doc comment: {err}"); + None + } + } + .unwrap_or_default(); + let manifest = match extract_manifest(&comment)? { + Some(manifest) => Some(manifest), + None => { + log::trace!("failed to extract manifest"); + None + } + } + .unwrap_or_default(); + let body = body.to_owned(); + let path = path.to_owned(); + Ok(Self { + manifest, + body, + path, + }) + } + + pub fn to_workspace<'cfg>(&self, config: &'cfg Config) -> CargoResult> { + let target_dir = config + .target_dir() + .transpose() + .unwrap_or_else(|| default_target_dir().map(crate::util::Filesystem::new))?; + // HACK: without cargo knowing about embedded manifests, the only way to create a + // `Workspace` is either + // - Create a temporary one on disk + // - Create an "ephemeral" workspace **but** compilation re-loads ephemeral workspaces + // from the registry rather than what we already have on memory, causing it to fail + // because the registry doesn't know about embedded manifests. + let manifest_path = self.write(config, target_dir.as_path_unlocked())?; + let workspace = Workspace::new(&manifest_path, config)?; + Ok(workspace) + } + + fn write( + &self, + config: &Config, + target_dir: &std::path::Path, + ) -> CargoResult { + let hash = self.hash().to_string(); + assert_eq!(hash.len(), 64); + let mut workspace_root = target_dir.to_owned(); + workspace_root.push("eval"); + workspace_root.push(&hash[0..2]); + workspace_root.push(&hash[2..4]); + workspace_root.push(&hash[4..]); + workspace_root.push(self.package_name()?); + std::fs::create_dir_all(&workspace_root).with_context(|| { + format!( + "failed to create temporary workspace at {}", + workspace_root.display() + ) + })?; + let manifest_path = workspace_root.join("Cargo.toml"); + let manifest = self.expand_manifest(config)?; + write_if_changed(&manifest_path, &manifest)?; + Ok(manifest_path) + } + + pub fn expand_manifest(&self, config: &Config) -> CargoResult { + let manifest = self + .expand_manifest_(config) + .with_context(|| format!("failed to parse manifest at {}", self.path.display()))?; + let manifest = remap_paths( + manifest, + self.path.parent().ok_or_else(|| { + anyhow::format_err!("no parent directory for {}", self.path.display()) + })?, + )?; + let manifest = toml::to_string_pretty(&manifest)?; + Ok(manifest) + } + + fn expand_manifest_(&self, config: &Config) -> CargoResult { + let mut manifest: toml::Table = toml::from_str(&self.manifest)?; + + for key in ["workspace", "lib", "bin", "example", "test", "bench"] { + if manifest.contains_key(key) { + anyhow::bail!("`{key}` is not allowed in embedded manifests") + } + } + + // Prevent looking for a workspace by `read_manifest_from_str` + manifest.insert("workspace".to_owned(), toml::Table::new().into()); + + let package = manifest + .entry("package".to_owned()) + .or_insert_with(|| toml::Table::new().into()) + .as_table_mut() + .ok_or_else(|| anyhow::format_err!("`package` must be a table"))?; + for key in ["workspace", "build", "links"] { + if package.contains_key(key) { + anyhow::bail!("`package.{key}` is not allowed in embedded manifests") + } + } + let name = self.package_name()?; + let hash = self.hash(); + let bin_name = format!("{name}_{hash}"); + package + .entry("name".to_owned()) + .or_insert(toml::Value::String(name)); + package + .entry("version".to_owned()) + .or_insert_with(|| toml::Value::String(DEFAULT_VERSION.to_owned())); + package.entry("edition".to_owned()).or_insert_with(|| { + let _ = config.shell().warn(format_args!( + "`package.edition` is unspecifiead, defaulting to `{}`", + DEFAULT_EDITION + )); + toml::Value::String(DEFAULT_EDITION.to_string()) + }); + package + .entry("publish".to_owned()) + .or_insert_with(|| toml::Value::Boolean(DEFAULT_PUBLISH)); + + let mut bin = toml::Table::new(); + bin.insert("name".to_owned(), toml::Value::String(bin_name)); + bin.insert( + "path".to_owned(), + toml::Value::String( + self.path + .to_str() + .ok_or_else(|| anyhow::format_err!("path is not valid UTF-8"))? + .into(), + ), + ); + manifest.insert( + "bin".to_owned(), + toml::Value::Array(vec![toml::Value::Table(bin)]), + ); + + let release = manifest + .entry("profile".to_owned()) + .or_insert_with(|| toml::Value::Table(Default::default())) + .as_table_mut() + .ok_or_else(|| anyhow::format_err!("`profile` must be a table"))? + .entry("release".to_owned()) + .or_insert_with(|| toml::Value::Table(Default::default())) + .as_table_mut() + .ok_or_else(|| anyhow::format_err!("`profile.release` must be a table"))?; + release + .entry("strip".to_owned()) + .or_insert_with(|| toml::Value::Boolean(true)); + + Ok(manifest) + } + + fn package_name(&self) -> CargoResult { + let name = self + .path + .file_stem() + .ok_or_else(|| anyhow::format_err!("no file name"))? + .to_string_lossy(); + let mut slug = String::new(); + for (i, c) in name.chars().enumerate() { + match (i, c) { + (0, '0'..='9') => { + slug.push('_'); + slug.push(c); + } + (_, '0'..='9') | (_, 'a'..='z') | (_, '_') | (_, '-') => { + slug.push(c); + } + (_, 'A'..='Z') => { + // Convert uppercase characters to lowercase to avoid `non_snake_case` warnings. + slug.push(c.to_ascii_lowercase()); + } + (_, _) => { + slug.push('_'); + } + } + } + Ok(slug) + } + + fn hash(&self) -> blake3::Hash { + blake3::hash(self.body.as_bytes()) + } +} + +fn default_target_dir() -> CargoResult { + let mut cargo_home = home::cargo_home()?; + cargo_home.push("eval"); + cargo_home.push("target"); + Ok(cargo_home) +} + +fn write_if_changed(path: &std::path::Path, new: &str) -> CargoResult<()> { + let write_needed = match std::fs::read_to_string(path) { + Ok(current) => current != new, + Err(_) => true, + }; + if write_needed { + std::fs::write(path, new).with_context(|| format!("failed to write {}", path.display()))?; + } + Ok(()) +} + +/// Locates a "code block manifest" in Rust source. +fn extract_comment(input: &str) -> CargoResult { + let re_crate_comment = regex::Regex::new( + // We need to find the first `/*!` or `//!` that *isn't* preceded by something that would + // make it apply to anything other than the crate itself. Because we can't do this + // accurately, we'll just require that the doc-comment is the *first* thing in the file + // (after the optional shebang). + r"(?x)(^\s*|^\#![^\[].*?(\r\n|\n))(/\*!|//(!|/))", + ) + .unwrap(); + let re_margin = regex::Regex::new(r"^\s*\*( |$)").unwrap(); + let re_space = regex::Regex::new(r"^(\s+)").unwrap(); + let re_nesting = regex::Regex::new(r"/\*|\*/").unwrap(); + let re_comment = regex::Regex::new(r"^\s*//(!|/)").unwrap(); + + fn n_leading_spaces(s: &str, n: usize) -> anyhow::Result<()> { + if !s.chars().take(n).all(|c| c == ' ') { + anyhow::bail!("leading {n:?} chars aren't all spaces: {s:?}") + } + Ok(()) + } + + /// Returns a slice of the input string with the leading shebang, if there is one, omitted. + fn strip_shebang(s: &str) -> &str { + let re_shebang = regex::Regex::new(r"^#![^\[].*?(\r\n|\n)").unwrap(); + re_shebang.find(s).map(|m| &s[m.end()..]).unwrap_or(s) + } + + // First, we will look for and slice out a contiguous, inner doc-comment which must be *the + // very first thing* in the file. `#[doc(...)]` attributes *are not supported*. Multiple + // single-line comments cannot have any blank lines between them. + let input = strip_shebang(input); // `re_crate_comment` doesn't work with shebangs + let start = re_crate_comment + .captures(input) + .ok_or_else(|| anyhow::format_err!("no doc-comment found"))? + .get(3) + .ok_or_else(|| anyhow::format_err!("no doc-comment found"))? + .start(); + + let input = &input[start..]; + + if let Some(input) = input.strip_prefix("/*!") { + // On every line: + // + // - update nesting level and detect end-of-comment + // - if margin is None: + // - if there appears to be a margin, set margin. + // - strip off margin marker + // - update the leading space counter + // - strip leading space + // - append content + let mut r = String::new(); + + let mut leading_space = None; + let mut margin = None; + let mut depth: u32 = 1; + + for line in input.lines() { + if depth == 0 { + break; + } + + // Update nesting and look for end-of-comment. + let mut end_of_comment = None; + + for (end, marker) in re_nesting.find_iter(line).map(|m| (m.start(), m.as_str())) { + match (marker, depth) { + ("/*", _) => depth += 1, + ("*/", 1) => { + end_of_comment = Some(end); + depth = 0; + break; + } + ("*/", _) => depth -= 1, + _ => panic!("got a comment marker other than /* or */"), + } + } + + let line = end_of_comment.map(|end| &line[..end]).unwrap_or(line); + + // Detect and strip margin. + margin = margin.or_else(|| re_margin.find(line).map(|m| m.as_str())); + + let line = if let Some(margin) = margin { + let end = line + .char_indices() + .take(margin.len()) + .map(|(i, c)| i + c.len_utf8()) + .last() + .unwrap_or(0); + &line[end..] + } else { + line + }; + + // Detect and strip leading indentation. + leading_space = leading_space.or_else(|| re_space.find(line).map(|m| m.end())); + + // Make sure we have only leading spaces. + // + // If we see a tab, fall over. I *would* expand them, but that gets into the question of how *many* spaces to expand them to, and *where* is the tab, because tabs are tab stops and not just N spaces. + n_leading_spaces(line, leading_space.unwrap_or(0))?; + + let strip_len = line.len().min(leading_space.unwrap_or(0)); + let line = &line[strip_len..]; + + // Done. + r.push_str(line); + + // `lines` removes newlines. Ideally, it wouldn't do that, but hopefully this shouldn't cause any *real* problems. + r.push('\n'); + } + + Ok(r) + } else if input.starts_with("//!") || input.starts_with("///") { + let mut r = String::new(); + + let mut leading_space = None; + + for line in input.lines() { + // Strip leading comment marker. + let content = match re_comment.find(line) { + Some(m) => &line[m.end()..], + None => break, + }; + + // Detect and strip leading indentation. + leading_space = leading_space.or_else(|| { + re_space + .captures(content) + .and_then(|c| c.get(1)) + .map(|m| m.end()) + }); + + // Make sure we have only leading spaces. + // + // If we see a tab, fall over. I *would* expand them, but that gets into the question of how *many* spaces to expand them to, and *where* is the tab, because tabs are tab stops and not just N spaces. + n_leading_spaces(content, leading_space.unwrap_or(0))?; + + let strip_len = content.len().min(leading_space.unwrap_or(0)); + let content = &content[strip_len..]; + + // Done. + r.push_str(content); + + // `lines` removes newlines. Ideally, it wouldn't do that, but hopefully this shouldn't cause any *real* problems. + r.push('\n'); + } + + Ok(r) + } else { + Err(anyhow::format_err!("no doc-comment found")) + } +} + +/// Extracts the first `Cargo` fenced code block from a chunk of Markdown. +fn extract_manifest(comment: &str) -> CargoResult> { + use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; + + // To match librustdoc/html/markdown.rs, opts. + let exts = Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES; + + let md = Parser::new_ext(comment, exts); + + let mut inside = false; + let mut output = None; + + for item in md { + match item { + Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(ref info))) + if info.to_lowercase() == "cargo" => + { + if output.is_some() { + anyhow::bail!("multiple `cargo` manifests present") + } else { + output = Some(String::new()); + } + inside = true; + } + Event::Text(ref text) if inside => { + let s = output.get_or_insert(String::new()); + s.push_str(text); + } + Event::End(Tag::CodeBlock(_)) if inside => { + inside = false; + } + _ => (), + } + } + + Ok(output) +} + +#[cfg(test)] +mod test_expand { + use super::*; + + macro_rules! si { + ($i:expr) => { + RawScript::parse($i, std::path::Path::new("/home/me/test.rs")) + .unwrap_or_else(|err| panic!("{}", err)) + .expand_manifest(&Config::default().unwrap()) + .unwrap_or_else(|err| panic!("{}", err)) + }; + } + + #[test] + fn test_default() { + snapbox::assert_eq( + r#"[[bin]] +name = "test_a472c7a31645d310613df407eab80844346938a3b8fe4f392cae059cb181aa85" +path = "/home/me/test.rs" + +[package] +edition = "2021" +name = "test" +publish = false +version = "0.0.0" + +[profile.release] +strip = true + +[workspace] +"#, + si!(r#"fn main() {}"#), + ); + } + + #[test] + fn test_dependencies() { + snapbox::assert_eq( + r#"[[bin]] +name = "test_3a1fa07700654ea2e893f70bb422efa7884eb1021ccacabc5466efe545da8a0b" +path = "/home/me/test.rs" + +[dependencies] +time = "0.1.25" + +[package] +edition = "2021" +name = "test" +publish = false +version = "0.0.0" + +[profile.release] +strip = true + +[workspace] +"#, + si!(r#" +//! ```cargo +//! [dependencies] +//! time="0.1.25" +//! ``` +fn main() {} +"#), + ); + } +} + +#[cfg(test)] +mod test_comment { + use super::*; + + macro_rules! ec { + ($s:expr) => { + extract_comment($s).unwrap_or_else(|err| panic!("{}", err)) + }; + } + + #[test] + fn test_no_comment() { + snapbox::assert_eq( + "no doc-comment found", + extract_comment( + r#" +fn main () { +} +"#, + ) + .unwrap_err() + .to_string(), + ); + } + + #[test] + fn test_no_comment_she_bang() { + snapbox::assert_eq( + "no doc-comment found", + extract_comment( + r#"#!/usr/bin/env cargo-eval + +fn main () { +} +"#, + ) + .unwrap_err() + .to_string(), + ); + } + + #[test] + fn test_comment() { + snapbox::assert_eq( + r#"Here is a manifest: + +```cargo +[dependencies] +time = "*" +``` +"#, + ec!(r#"//! Here is a manifest: +//! +//! ```cargo +//! [dependencies] +//! time = "*" +//! ``` +fn main() {} +"#), + ); + } + + #[test] + fn test_comment_shebang() { + snapbox::assert_eq( + r#"Here is a manifest: + +```cargo +[dependencies] +time = "*" +``` +"#, + ec!(r#"#!/usr/bin/env cargo-eval + +//! Here is a manifest: +//! +//! ```cargo +//! [dependencies] +//! time = "*" +//! ``` +fn main() {} +"#), + ); + } + + #[test] + fn test_multiline_comment() { + snapbox::assert_eq( + r#" +Here is a manifest: + +```cargo +[dependencies] +time = "*" +``` + +"#, + ec!(r#"/*! +Here is a manifest: + +```cargo +[dependencies] +time = "*" +``` +*/ + +fn main() { +} +"#), + ); + } + + #[test] + fn test_multiline_comment_shebang() { + snapbox::assert_eq( + r#" +Here is a manifest: + +```cargo +[dependencies] +time = "*" +``` + +"#, + ec!(r#"#!/usr/bin/env cargo-eval + +/*! +Here is a manifest: + +```cargo +[dependencies] +time = "*" +``` +*/ + +fn main() { +} +"#), + ); + } + + #[test] + fn test_multiline_block_comment() { + snapbox::assert_eq( + r#" +Here is a manifest: + +```cargo +[dependencies] +time = "*" +``` + +"#, + ec!(r#"/*! + * Here is a manifest: + * + * ```cargo + * [dependencies] + * time = "*" + * ``` + */ +fn main() {} +"#), + ); + } + + #[test] + fn test_multiline_block_comment_shebang() { + snapbox::assert_eq( + r#" +Here is a manifest: + +```cargo +[dependencies] +time = "*" +``` + +"#, + ec!(r#"#!/usr/bin/env cargo-eval + +/*! + * Here is a manifest: + * + * ```cargo + * [dependencies] + * time = "*" + * ``` + */ +fn main() {} +"#), + ); + } +} + +/// Given a Cargo manifest, attempts to rewrite relative file paths to absolute ones, allowing the manifest to be relocated. +fn remap_paths( + mani: toml::Table, + package_root: &std::path::Path, +) -> anyhow::Result { + // Values that need to be rewritten: + let paths: &[&[&str]] = &[ + &["build-dependencies", "*", "path"], + &["dependencies", "*", "path"], + &["dev-dependencies", "*", "path"], + &["package", "build"], + &["target", "*", "dependencies", "*", "path"], + ]; + + let mut mani = toml::Value::Table(mani); + + for path in paths { + iterate_toml_mut_path(&mut mani, path, &mut |v| { + if let toml::Value::String(s) = v { + if std::path::Path::new(s).is_relative() { + let p = package_root.join(&*s); + if let Some(p) = p.to_str() { + *s = p.into() + } + } + } + Ok(()) + })? + } + + match mani { + toml::Value::Table(mani) => Ok(mani), + _ => unreachable!(), + } +} + +/// Iterates over the specified TOML values via a path specification. +fn iterate_toml_mut_path( + base: &mut toml::Value, + path: &[&str], + on_each: &mut F, +) -> anyhow::Result<()> +where + F: FnMut(&mut toml::Value) -> anyhow::Result<()>, +{ + if path.is_empty() { + return on_each(base); + } + + let cur = path[0]; + let tail = &path[1..]; + + if cur == "*" { + if let toml::Value::Table(tab) = base { + for (_, v) in tab { + iterate_toml_mut_path(v, tail, on_each)?; + } + } + } else if let toml::Value::Table(tab) = base { + if let Some(v) = tab.get_mut(cur) { + iterate_toml_mut_path(v, tail, on_each)?; + } + } + + Ok(()) +} + +#[cfg(test)] +mod test_manifest { + use super::*; + + macro_rules! smm { + ($c:expr) => { + extract_manifest($c) + }; + } + + #[test] + fn test_no_code_fence() { + assert_eq!( + smm!( + r#"There is no manifest in this comment. +"# + ) + .unwrap(), + None + ); + } + + #[test] + fn test_no_cargo_code_fence() { + assert_eq!( + smm!( + r#"There is no manifest in this comment. + +``` +This is not a manifest. +``` + +```rust +println!("Nor is this."); +``` + + Or this. +"# + ) + .unwrap(), + None + ); + } + + #[test] + fn test_cargo_code_fence() { + assert_eq!( + smm!( + r#"This is a manifest: + +```cargo +dependencies = { time = "*" } +``` +"# + ) + .unwrap(), + Some( + r#"dependencies = { time = "*" } +"# + .into() + ) + ); + } + + #[test] + fn test_mixed_code_fence() { + assert_eq!( + smm!( + r#"This is *not* a manifest: + +``` +He's lying, I'm *totally* a manifest! +``` + +This *is*: + +```cargo +dependencies = { time = "*" } +``` +"# + ) + .unwrap(), + Some( + r#"dependencies = { time = "*" } +"# + .into() + ) + ); + } + + #[test] + fn test_two_cargo_code_fence() { + assert!(smm!( + r#"This is a manifest: + +```cargo +dependencies = { time = "*" } +``` + +So is this, but it doesn't count: + +```cargo +dependencies = { explode = true } +``` +"# + ) + .is_err()); + } +} diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 2c213b7f5fa..a9bcb46926e 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -33,6 +33,7 @@ use crate::util::{ self, config::ConfigRelativePath, validate_package_name, Config, IntoUrl, VersionReqExt, }; +pub mod embedded; mod targets; use self::targets::targets; diff --git a/tests/testsuite/script.rs b/tests/testsuite/script.rs index 169e08278a1..2c746f16453 100644 --- a/tests/testsuite/script.rs +++ b/tests/testsuite/script.rs @@ -1,3 +1,6 @@ +use cargo_test_support::basic_manifest; +use cargo_test_support::registry::Package; + const ECHO_SCRIPT: &str = r#"#!/usr/bin/env cargo fn main() { @@ -9,6 +12,7 @@ fn main() { } "#; +#[cfg(unix)] fn path() -> Vec { std::env::split_paths(&std::env::var_os("PATH").unwrap_or_default()).collect() } @@ -20,14 +24,20 @@ fn basic_rs() { .build(); p.cargo("-Zscript echo.rs") - .arg("--help") // An arg that, if processed by cargo, will cause problems .masquerade_as_nightly_cargo(&["script"]) - .with_status(101) - .with_stdout("") - .with_stderr("\ -thread 'main' panicked at 'not yet implemented: support for running manifest-commands is not yet implemented', [..] -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace -") + .with_stdout( + r#"bin: [ROOT]/home/.cargo/eval/target/eval/[..] +args: [] +"#, + ) + .with_stderr( + "\ +[WARNING] `package.edition` is unspecifiead, defaulting to `2021` +[COMPILING] echo v0.0.0 ([ROOT]/home/.cargo/eval/target/eval/[..]/echo) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]s +[RUNNING] `[ROOT]/home/.cargo/eval/target/eval/[..]/echo/target/debug/echo_[..]` +", + ) .run(); } @@ -38,14 +48,20 @@ fn basic_path() { .build(); p.cargo("-Zscript ./echo") - .arg("--help") // An arg that, if processed by cargo, will cause problems .masquerade_as_nightly_cargo(&["script"]) - .with_status(101) - .with_stdout("") - .with_stderr("\ -thread 'main' panicked at 'not yet implemented: support for running manifest-commands is not yet implemented', [..] -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace -") + .with_stdout( + r#"bin: [ROOT]/home/.cargo/eval/target/eval/[..] +args: [] +"#, + ) + .with_stderr( + "\ +[WARNING] `package.edition` is unspecifiead, defaulting to `2021` +[COMPILING] echo v0.0.0 ([ROOT]/home/.cargo/eval/target/eval/[..]/echo) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]s +[RUNNING] `[ROOT]/home/.cargo/eval/target/eval/[..]/echo/target/debug/echo_[..]` +", + ) .run(); } @@ -56,7 +72,6 @@ fn path_required() { .build(); p.cargo("-Zscript echo") - .arg("--help") // An arg that, if processed by cargo, will cause problems .masquerade_as_nightly_cargo(&["script"]) .with_status(101) .with_stdout("") @@ -86,15 +101,21 @@ fn manifest_precedence_over_plugins() { let path = std::env::join_paths(path.iter()).unwrap(); p.cargo("-Zscript echo.rs") - .arg("--help") // An arg that, if processed by cargo, will cause problems .env("PATH", &path) .masquerade_as_nightly_cargo(&["script"]) - .with_status(101) - .with_stdout("") - .with_stderr("\ -thread 'main' panicked at 'not yet implemented: support for running manifest-commands is not yet implemented', [..] -note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace -") + .with_stdout( + r#"bin: [ROOT]/home/.cargo/eval/target/eval/[..] +args: [] +"#, + ) + .with_stderr( + "\ +[WARNING] `package.edition` is unspecifiead, defaulting to `2021` +[COMPILING] echo v0.0.0 ([ROOT]/home/.cargo/eval/target/eval/[..]/echo) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]s +[RUNNING] `[ROOT]/home/.cargo/eval/target/eval/[..]/echo/target/debug/echo_[..]` +", + ) .run(); } @@ -111,7 +132,6 @@ fn warn_when_plugin_masks_manifest_on_stable() { let path = std::env::join_paths(path.iter()).unwrap(); p.cargo("echo.rs") - .arg("--help") // An arg that, if processed by cargo, will cause problems .env("PATH", &path) .with_stdout("") .with_stderr( @@ -131,7 +151,6 @@ fn requires_nightly() { .build(); p.cargo("echo.rs") - .arg("--help") // An arg that, if processed by cargo, will cause problems .with_status(101) .with_stdout("") .with_stderr( @@ -149,7 +168,6 @@ fn requires_z_flag() { .build(); p.cargo("echo.rs") - .arg("--help") // An arg that, if processed by cargo, will cause problems .masquerade_as_nightly_cargo(&["script"]) .with_status(101) .with_stdout("") @@ -160,3 +178,339 @@ error: running `echo.rs` requires `-Zscript` ) .run(); } + +#[cargo_test] +fn clean_output_with_edition() { + let script = r#"#!/usr/bin/env cargo + +//! ```cargo +//! [package] +//! edition = "2018" +//! ``` + +fn main() { + println!("Hello world!"); +}"#; + let p = cargo_test_support::project() + .file("script.rs", script) + .build(); + + p.cargo("-Zscript script.rs") + .masquerade_as_nightly_cargo(&["script"]) + .with_stdout( + r#"Hello world! +"#, + ) + .with_stderr( + "\ +[COMPILING] script v0.0.0 ([ROOT]/home/.cargo/eval/target/eval/[..]/script) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]s +[RUNNING] `[ROOT]/home/.cargo/eval/target/eval/[..]/script/target/debug/script_[..]` +", + ) + .run(); +} + +#[cargo_test] +fn warning_without_edition() { + let script = r#"#!/usr/bin/env cargo + +//! ```cargo +//! [package] +//! ``` + +fn main() { + println!("Hello world!"); +}"#; + let p = cargo_test_support::project() + .file("script.rs", script) + .build(); + + p.cargo("-Zscript script.rs") + .masquerade_as_nightly_cargo(&["script"]) + .with_stdout( + r#"Hello world! +"#, + ) + .with_stderr( + "\ +[WARNING] `package.edition` is unspecifiead, defaulting to `2021` +[COMPILING] script v0.0.0 ([ROOT]/home/.cargo/eval/target/eval/[..]/script) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]s +[RUNNING] `[ROOT]/home/.cargo/eval/target/eval/[..]/script/target/debug/script_[..]` +", + ) + .run(); +} + +#[cargo_test] +fn rebuild() { + let script = r#"#!/usr/bin/env cargo-eval + +fn main() { + let msg = option_env!("_MESSAGE").unwrap_or("undefined"); + println!("msg = {}", msg); +}"#; + let p = cargo_test_support::project() + .file("script.rs", script) + .build(); + + p.cargo("-Zscript script.rs") + .masquerade_as_nightly_cargo(&["script"]) + .with_stdout( + r#"msg = undefined +"#, + ) + .with_stderr( + "\ +[WARNING] `package.edition` is unspecifiead, defaulting to `2021` +[COMPILING] script v0.0.0 ([ROOT]/home/.cargo/eval/target/eval/[..]/script) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]s +[RUNNING] `[ROOT]/home/.cargo/eval/target/eval/[..]/script/target/debug/script_[..]` +", + ) + .run(); + + // Verify we don't rebuild + p.cargo("-Zscript script.rs") + .masquerade_as_nightly_cargo(&["script"]) + .with_stdout( + r#"msg = undefined +"#, + ) + .with_stderr( + "\ +[WARNING] `package.edition` is unspecifiead, defaulting to `2021` +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]s +[RUNNING] `[ROOT]/home/.cargo/eval/target/eval/[..]/script/target/debug/script_[..]` +", + ) + .run(); + + // Verify we do rebuild + p.cargo("-Zscript script.rs") + .env("_MESSAGE", "hello") + .masquerade_as_nightly_cargo(&["script"]) + .with_stdout( + r#"msg = hello +"#, + ) + .with_stderr( + "\ +[WARNING] `package.edition` is unspecifiead, defaulting to `2021` +[COMPILING] script v0.0.0 ([ROOT]/home/.cargo/eval/target/eval/[..]/script) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]s +[RUNNING] `[ROOT]/home/.cargo/eval/target/eval/[..]/script/target/debug/script_[..]` +", + ) + .run(); +} + +#[cargo_test] +fn test_line_numbering_preserved() { + let script = r#"#!/usr/bin/env cargo + +fn main() { + println!("line: {}", line!()); +} +"#; + let p = cargo_test_support::project() + .file("script.rs", script) + .build(); + + p.cargo("-Zscript script.rs") + .masquerade_as_nightly_cargo(&["script"]) + .with_stdout( + r#"line: 4 +"#, + ) + .with_stderr( + "\ +[WARNING] `package.edition` is unspecifiead, defaulting to `2021` +[COMPILING] script v0.0.0 ([ROOT]/home/.cargo/eval/target/eval/[..]/script) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]s +[RUNNING] `[ROOT]/home/.cargo/eval/target/eval/[..]/script/target/debug/script_[..]` +", + ) + .run(); +} + +#[cargo_test] +fn test_escaped_hyphen_arg() { + let script = ECHO_SCRIPT; + let p = cargo_test_support::project() + .file("script.rs", script) + .build(); + + p.cargo("-Zscript -- script.rs -NotAnArg") + .masquerade_as_nightly_cargo(&["script"]) + .with_stdout( + r#"bin: [ROOT]/home/.cargo/eval/target/eval/[..] +args: ["-NotAnArg"] +"#, + ) + .with_stderr( + "\ +[WARNING] `package.edition` is unspecifiead, defaulting to `2021` +[COMPILING] script v0.0.0 ([ROOT]/home/.cargo/eval/target/eval/[..]/script) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]s +[RUNNING] `[ROOT]/home/.cargo/eval/target/eval/[..]/script/target/debug/script_[..] -NotAnArg` +", + ) + .run(); +} + +#[cargo_test] +fn test_unescaped_hyphen_arg() { + let script = ECHO_SCRIPT; + let p = cargo_test_support::project() + .file("script.rs", script) + .build(); + + p.cargo("-Zscript script.rs -NotAnArg") + .masquerade_as_nightly_cargo(&["script"]) + .with_stdout( + r#"bin: [ROOT]/home/.cargo/eval/target/eval/[..] +args: ["-NotAnArg"] +"#, + ) + .with_stderr( + "\ +[WARNING] `package.edition` is unspecifiead, defaulting to `2021` +[COMPILING] script v0.0.0 ([ROOT]/home/.cargo/eval/target/eval/[..]/script) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]s +[RUNNING] `[ROOT]/home/.cargo/eval/target/eval/[..]/script/target/debug/script_[..] -NotAnArg` +", + ) + .run(); +} + +#[cargo_test] +fn test_same_flags() { + let script = ECHO_SCRIPT; + let p = cargo_test_support::project() + .file("script.rs", script) + .build(); + + p.cargo("-Zscript script.rs --help") + .masquerade_as_nightly_cargo(&["script"]) + .with_stdout( + r#"bin: [ROOT]/home/.cargo/eval/target/eval/[..] +args: ["--help"] +"#, + ) + .with_stderr( + "\ +[WARNING] `package.edition` is unspecifiead, defaulting to `2021` +[COMPILING] script v0.0.0 ([ROOT]/home/.cargo/eval/target/eval/[..]/script) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]s +[RUNNING] `[ROOT]/home/.cargo/eval/target/eval/[..]/script/target/debug/script_[..] --help` +", + ) + .run(); +} + +#[cargo_test] +fn test_name_has_weird_chars() { + let script = ECHO_SCRIPT; + let p = cargo_test_support::project() + .file("s-h.w§c!.rs", script) + .build(); + + p.cargo("-Zscript s-h.w§c!.rs") + .masquerade_as_nightly_cargo(&["script"]) + .with_stdout( + r#"bin: [ROOT]/home/.cargo/eval/target/eval/[..] +args: [] +"#, + ) + .with_stderr( + r#"[WARNING] `package.edition` is unspecifiead, defaulting to `2021` +[COMPILING] s-h_w_c_ v0.0.0 ([ROOT]/home/.cargo/eval/target/eval/[..]/s-h_w_c_) +[WARNING] crate `s_h_w_c__[..]` should have a snake case name + | + = help: convert the identifier to snake case: `s_h_w_c_[..]` + = note: `#[warn(non_snake_case)]` on by default + +[WARNING] `s-h_w_c_` (bin "s-h_w_c__[..]") generated 1 warning +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]s +[RUNNING] `[ROOT]/home/.cargo/eval/target/eval/[..]/s-h_w_c_/target/debug/s-h_w_c__[..]` +"#, + ) + .run(); +} + +#[cargo_test] +fn test_name_same_as_dependency() { + Package::new("script", "1.0.0").publish(); + let script = r#"#!/usr/bin/env cargo + +//! ```cargo +//! [dependencies] +//! script = "1.0.0" +//! ``` + +fn main() { + println!("Hello world!"); +}"#; + let p = cargo_test_support::project() + .file("script.rs", script) + .build(); + + p.cargo("-Zscript script.rs --help") + .masquerade_as_nightly_cargo(&["script"]) + .with_stdout( + r#"Hello world! +"#, + ) + .with_stderr( + "\ +[WARNING] `package.edition` is unspecifiead, defaulting to `2021` +[UPDATING] `dummy-registry` index +[DOWNLOADING] crates ... +[DOWNLOADED] script v1.0.0 (registry `dummy-registry`) +[COMPILING] script v1.0.0 +[COMPILING] script v0.0.0 ([ROOT]/home/.cargo/eval/target/eval/[..]/script) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]s +[RUNNING] `[ROOT]/home/.cargo/eval/target/eval/[..]/script/target/debug/script_[..] --help` +", + ) + .run(); +} + +#[cargo_test] +fn test_path_dep() { + let script = r#"#!/usr/bin/env cargo + +//! ```cargo +//! [dependencies] +//! bar.path = "./bar" +//! ``` + +fn main() { + println!("Hello world!"); +}"#; + let p = cargo_test_support::project() + .file("script.rs", script) + .file("src/lib.rs", "pub fn foo() {}") + .file("bar/Cargo.toml", &basic_manifest("bar", "0.0.1")) + .file("bar/src/lib.rs", "pub fn bar() {}") + .build(); + + p.cargo("-Zscript script.rs --help") + .masquerade_as_nightly_cargo(&["script"]) + .with_stdout( + r#"Hello world! +"#, + ) + .with_stderr( + "\ +[WARNING] `package.edition` is unspecifiead, defaulting to `2021` +[COMPILING] bar v0.0.1 ([ROOT]/foo/bar) +[COMPILING] script v0.0.0 ([ROOT]/home/.cargo/eval/target/eval/[..]/script) +[FINISHED] dev [unoptimized + debuginfo] target(s) in [..]s +[RUNNING] `[ROOT]/home/.cargo/eval/target/eval/[..]/script/target/debug/script_[..] --help` +", + ) + .run(); +} From 6b0b5a8a514ab7286ea619d89bffec561d5ea1d7 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 8 Jun 2023 20:15:44 -0500 Subject: [PATCH 10/10] docs(unstable): Expand on manifest commands so far This is written to reflect the current implementation though some parts might read a little weird because I didn't want to write throw-away documentation for when we change this. For example, single-file packages are currently only supported in `cargo ` and not as manifest paths but this will change. --- src/bin/cargo/cli.rs | 4 +- src/doc/src/reference/unstable.md | 95 +++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/src/bin/cargo/cli.rs b/src/bin/cargo/cli.rs index cd4f797010f..57cb31c38d4 100644 --- a/src/bin/cargo/cli.rs +++ b/src/bin/cargo/cli.rs @@ -490,9 +490,9 @@ pub fn cli() -> Command { #[allow(clippy::disallowed_methods)] let is_rustup = std::env::var_os("RUSTUP_HOME").is_some(); let usage = if is_rustup { - "cargo [+toolchain] [OPTIONS] [COMMAND]" + "cargo [+toolchain] [OPTIONS] [COMMAND]\n cargo [+toolchain] [OPTIONS] -Zscript [ARGS]..." } else { - "cargo [OPTIONS] [COMMAND]" + "cargo [OPTIONS] [COMMAND]\n cargo [OPTIONS] -Zscript [ARGS]..." }; Command::new("cargo") // Subcommands all count their args' display order independently (from 0), diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index 67c084de26d..50dfdd082b8 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -1397,6 +1397,101 @@ Valid operations are the following: * Tracking Issue: [#12207](https://github.com/rust-lang/cargo/issues/12207) +Cargo can directly run `.rs` files as: +```console +$ cargo -Zscript file.rs +``` +where `file.rs` can be as simple as: +```rust +fn main() {} +``` + +A user may optionally specify a manifest in a `cargo` code fence in a module-level comment, like: +```rust +#!/usr/bin/env cargo + +//! ```cargo +//! [dependencies] +//! clap = { version = "4.2", features = ["derive"] } +//! ``` + +use clap::Parser; + +#[derive(Parser, Debug)] +#[clap(version)] +struct Args { + #[clap(short, long, help = "Path to config")] + config: Option, +} + +fn main() { + let args = Args::parse(); + println!("{:?}", args); +} +``` + +#### Single-file packages + +In addition to today's multi-file packages (`Cargo.toml` file with other `.rs` +files), we are adding the concept of single-file packages which may contain an +embedded manifest. There is no required distinguishment for a single-file +`.rs` package from any other `.rs` file. + +A single-file package may contain an embedded manifest. An embedded manifest +is stored using `TOML` in a markdown code-fence with `cargo` at the start of the +infostring inside a target-level doc-comment. It is an error to have multiple +`cargo` code fences in the target-level doc-comment. We can relax this later, +either merging the code fences or ignoring later code fences. + +Supported forms of embedded manifest are: +``````rust +//! ```cargo +//! ``` +`````` +``````rust +/*! + * ```cargo + * ``` + */ +`````` + +Inferred / defaulted manifest fields: +- `package.name = ` +- `package.version = "0.0.0"` to [call attention to this crate being used in unexpected places](https://matklad.github.io/2021/08/22/large-rust-workspaces.html#Smaller-Tips) +- `package.publish = false` to avoid accidental publishes, particularly if we + later add support for including them in a workspace. +- `package.edition = ` to avoid always having to add an embedded + manifest at the cost of potentially breaking scripts on rust upgrades + - Warn when `edition` is unspecified. While with single-file packages this will be + silenced by default, users wanting stability are also likely to be using + other commands, like `cargo test` and will see it. + +Disallowed manifest fields: +- `[workspace]`, `[lib]`, `[[bin]]`, `[[example]]`, `[[test]]`, `[[bench]]` +- `package.workspace`, `package.build`, `package.links`, `package.autobins`, `package.autoexamples`, `package.autotests`, `package.autobenches` + +As the primary role for these files is exploratory programming which has a high +edit-to-run ratio, building should be fast. Therefore `CARGO_TARGET_DIR` will +be shared between single-file packages to allow reusing intermediate build +artifacts. + +The lockfile for single-file packages will be placed in `CARGO_TARGET_DIR`. In +the future, when workspaces are supported, that will allow a user to have a +persistent lockfile. + +#### Manifest-commands + +You may pass single-file packages directly to the `cargo` command, without subcommand. This is mostly intended for being put in `#!` lines. + +The precedence for how to interpret `cargo ` is +1. Built-in xor single-file packages +2. Aliases +3. External subcommands + +A parameter is identified as a single-file package if it has one of: +- Path separators +- A `.rs` extension + ### `[lints]` * Tracking Issue: [#12115](https://github.com/rust-lang/cargo/issues/12115)