diff --git a/Cargo.lock b/Cargo.lock index 806123dab495..b8d4d9d30568 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 17f5af414b65..d6d7e76d669e 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 89d08eacc8f8..71d74cb5b8a8 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 117a7c8e67c4..f5d970b94b09 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 000000000000..26540492557c --- /dev/null +++ b/src/cargo/util/toml/embedded.rs @@ -0,0 +1,863 @@ +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) + .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)?; + 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 = 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 2c213b7f5fa8..a9bcb46926e6 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 169e08278a1a..2c746f16453a 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(); +}