| use anyhow::Context as _; |
| |
| use cargo_util_schemas::manifest::PackageName; |
| |
| use crate::util::restricted_names; |
| use crate::CargoResult; |
| use crate::Config; |
| |
| const DEFAULT_EDITION: crate::core::features::Edition = |
| crate::core::features::Edition::LATEST_STABLE; |
| const AUTO_FIELDS: &[&str] = &["autobins", "autoexamples", "autotests", "autobenches"]; |
| |
| pub(super) fn expand_manifest( |
| content: &str, |
| path: &std::path::Path, |
| config: &Config, |
| ) -> CargoResult<String> { |
| let source = split_source(content)?; |
| if let Some(frontmatter) = source.frontmatter { |
| match source.info { |
| Some("cargo") | None => {} |
| Some(other) => { |
| if let Some(remainder) = other.strip_prefix("cargo,") { |
| anyhow::bail!("cargo does not support frontmatter infostring attributes like `{remainder}` at this time") |
| } else { |
| anyhow::bail!("frontmatter infostring `{other}` is unsupported by cargo; specify `cargo` for embedding a manifest") |
| } |
| } |
| } |
| |
| // HACK: until rustc has native support for this syntax, we have to remove it from the |
| // source file |
| use std::fmt::Write as _; |
| let hash = crate::util::hex::short_hash(&path.to_string_lossy()); |
| let mut rel_path = std::path::PathBuf::new(); |
| rel_path.push("target"); |
| rel_path.push(&hash[0..2]); |
| rel_path.push(&hash[2..]); |
| let target_dir = config.home().join(rel_path); |
| let hacked_path = target_dir |
| .join( |
| path.file_name() |
| .expect("always a name for embedded manifests"), |
| ) |
| .into_path_unlocked(); |
| let mut hacked_source = String::new(); |
| if let Some(shebang) = source.shebang { |
| writeln!(hacked_source, "{shebang}")?; |
| } |
| writeln!(hacked_source)?; // open |
| for _ in 0..frontmatter.lines().count() { |
| writeln!(hacked_source)?; |
| } |
| writeln!(hacked_source)?; // close |
| writeln!(hacked_source, "{}", source.content)?; |
| if let Some(parent) = hacked_path.parent() { |
| cargo_util::paths::create_dir_all(parent)?; |
| } |
| cargo_util::paths::write_if_changed(&hacked_path, hacked_source)?; |
| |
| let manifest = expand_manifest_(&frontmatter, &hacked_path, config) |
| .with_context(|| format!("failed to parse manifest at {}", path.display()))?; |
| let manifest = toml::to_string_pretty(&manifest)?; |
| Ok(manifest) |
| } else { |
| let frontmatter = ""; |
| let manifest = expand_manifest_(frontmatter, path, config) |
| .with_context(|| format!("failed to parse manifest at {}", path.display()))?; |
| let manifest = toml::to_string_pretty(&manifest)?; |
| Ok(manifest) |
| } |
| } |
| |
| fn expand_manifest_( |
| manifest: &str, |
| path: &std::path::Path, |
| config: &Config, |
| ) -> CargoResult<toml::Table> { |
| let mut manifest: toml::Table = toml::from_str(&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"] |
| .iter() |
| .chain(AUTO_FIELDS.iter()) |
| { |
| if package.contains_key(*key) { |
| anyhow::bail!("`package.{key}` is not allowed in embedded manifests") |
| } |
| } |
| // HACK: Using an absolute path while `hacked_path` is in use |
| let bin_path = path.to_string_lossy().into_owned(); |
| let file_stem = path |
| .file_stem() |
| .ok_or_else(|| anyhow::format_err!("no file name"))? |
| .to_string_lossy(); |
| let name = sanitize_name(file_stem.as_ref()); |
| let bin_name = name.clone(); |
| package |
| .entry("name".to_owned()) |
| .or_insert(toml::Value::String(name)); |
| package.entry("edition".to_owned()).or_insert_with(|| { |
| let _ = config.shell().warn(format_args!( |
| "`package.edition` is unspecified, defaulting to `{}`", |
| DEFAULT_EDITION |
| )); |
| toml::Value::String(DEFAULT_EDITION.to_string()) |
| }); |
| package |
| .entry("build".to_owned()) |
| .or_insert_with(|| toml::Value::Boolean(false)); |
| for field in AUTO_FIELDS { |
| package |
| .entry(field.to_owned()) |
| .or_insert_with(|| toml::Value::Boolean(false)); |
| } |
| |
| let mut bin = toml::Table::new(); |
| bin.insert("name".to_owned(), toml::Value::String(bin_name)); |
| bin.insert("path".to_owned(), toml::Value::String(bin_path)); |
| 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) |
| } |
| |
| /// Ensure the package name matches the validation from `ops::cargo_new::check_name` |
| fn sanitize_name(name: &str) -> String { |
| let placeholder = if name.contains('_') { |
| '_' |
| } else { |
| // Since embedded manifests only support `[[bin]]`s, prefer arrow-case as that is the |
| // more common convention for CLIs |
| '-' |
| }; |
| |
| let mut name = PackageName::sanitize(name, placeholder).into_inner(); |
| |
| loop { |
| if restricted_names::is_keyword(&name) { |
| name.push(placeholder); |
| } else if restricted_names::is_conflicting_artifact_name(&name) { |
| // Being an embedded manifest, we always assume it is a `[[bin]]` |
| name.push(placeholder); |
| } else if name == "test" { |
| name.push(placeholder); |
| } else if restricted_names::is_windows_reserved(&name) { |
| // Go ahead and be consistent across platforms |
| name.push(placeholder); |
| } else { |
| break; |
| } |
| } |
| |
| name |
| } |
| |
| struct Source<'s> { |
| shebang: Option<&'s str>, |
| info: Option<&'s str>, |
| frontmatter: Option<String>, |
| content: &'s str, |
| } |
| |
| fn split_source(input: &str) -> CargoResult<Source<'_>> { |
| let mut source = Source { |
| shebang: None, |
| info: None, |
| frontmatter: None, |
| content: input, |
| }; |
| |
| // See rust-lang/rust's compiler/rustc_lexer/src/lib.rs's `strip_shebang` |
| // Shebang must start with `#!` literally, without any preceding whitespace. |
| // For simplicity we consider any line starting with `#!` a shebang, |
| // regardless of restrictions put on shebangs by specific platforms. |
| if let Some(rest) = source.content.strip_prefix("#!") { |
| // Ok, this is a shebang but if the next non-whitespace token is `[`, |
| // then it may be valid Rust code, so consider it Rust code. |
| if rest.trim_start().starts_with('[') { |
| return Ok(source); |
| } |
| |
| // No other choice than to consider this a shebang. |
| let (shebang, content) = source |
| .content |
| .split_once('\n') |
| .unwrap_or((source.content, "")); |
| source.shebang = Some(shebang); |
| source.content = content; |
| } |
| |
| // Experiment: let us try which char works better |
| let tick_char = source |
| .content |
| .chars() |
| .filter(|c| ['`', '#', '-'].contains(c)) |
| .next() |
| .unwrap_or('`'); |
| |
| let tick_end = source |
| .content |
| .char_indices() |
| .find_map(|(i, c)| (c != tick_char).then_some(i)) |
| .unwrap_or(source.content.len()); |
| let (fence_pattern, rest) = match tick_end { |
| 0 => { |
| return Ok(source); |
| } |
| 1 if tick_char == '#' => { |
| // Attribute |
| return Ok(source); |
| } |
| 2 if tick_char == '#' => { |
| return split_prefix_source(source, "##"); |
| } |
| 1 | 2 => { |
| anyhow::bail!("found {tick_end} `{tick_char}` in rust frontmatter, expected at least 3") |
| } |
| _ => source.content.split_at(tick_end), |
| }; |
| let (info, content) = rest.split_once("\n").unwrap_or((rest, "")); |
| if !info.is_empty() { |
| source.info = Some(info.trim_end()); |
| } |
| source.content = content; |
| |
| let Some((frontmatter, content)) = source.content.split_once(fence_pattern) else { |
| anyhow::bail!("no closing `{fence_pattern}` found for frontmatter"); |
| }; |
| source.frontmatter = Some(frontmatter.to_owned()); |
| source.content = content; |
| |
| let (line, content) = source |
| .content |
| .split_once("\n") |
| .unwrap_or((source.content, "")); |
| let line = line.trim(); |
| if !line.is_empty() { |
| anyhow::bail!("unexpected trailing content on closing fence: `{line}`"); |
| } |
| source.content = content; |
| |
| Ok(source) |
| } |
| |
| fn split_prefix_source<'s>(mut source: Source<'s>, prefix: &str) -> CargoResult<Source<'s>> { |
| let mut frontmatter = String::new(); |
| while let Some(rest) = source.content.strip_prefix(prefix) { |
| if !rest.is_empty() && !rest.starts_with(' ') { |
| anyhow::bail!("frontmatter must have a space between `##` and the content"); |
| } |
| let (line, rest) = rest.split_once('\n').unwrap_or((rest, "")); |
| frontmatter.push_str(" "); |
| frontmatter.push_str(line); |
| frontmatter.push('\n'); |
| source.content = rest; |
| } |
| source.frontmatter = Some(frontmatter); |
| Ok(source) |
| } |
| |
| #[cfg(test)] |
| mod test_expand { |
| use super::*; |
| |
| macro_rules! si { |
| ($i:expr) => {{ |
| expand_manifest( |
| $i, |
| std::path::Path::new("/home/me/test.rs"), |
| &Config::default().unwrap(), |
| ) |
| .unwrap_or_else(|err| panic!("{}", err)) |
| }}; |
| } |
| |
| #[test] |
| fn test_default() { |
| snapbox::assert_eq( |
| r#"[[bin]] |
| name = "test-" |
| path = "/home/me/test.rs" |
| |
| [package] |
| autobenches = false |
| autobins = false |
| autoexamples = false |
| autotests = false |
| build = false |
| edition = "2021" |
| name = "test-" |
| |
| [profile.release] |
| strip = true |
| |
| [workspace] |
| "#, |
| si!(r#"fn main() {}"#), |
| ); |
| } |
| |
| #[test] |
| fn test_dependencies() { |
| snapbox::assert_matches( |
| r#"[[bin]] |
| name = "test-" |
| path = [..] |
| |
| [dependencies] |
| time = "0.1.25" |
| |
| [package] |
| autobenches = false |
| autobins = false |
| autoexamples = false |
| autotests = false |
| build = false |
| edition = "2021" |
| name = "test-" |
| |
| [profile.release] |
| strip = true |
| |
| [workspace] |
| "#, |
| si!(r#"```cargo |
| [dependencies] |
| time="0.1.25" |
| ``` |
| fn main() {} |
| "#), |
| ); |
| } |
| |
| #[test] |
| fn test_no_infostring() { |
| snapbox::assert_matches( |
| r#"[[bin]] |
| name = "test-" |
| path = [..] |
| |
| [dependencies] |
| time = "0.1.25" |
| |
| [package] |
| autobenches = false |
| autobins = false |
| autoexamples = false |
| autotests = false |
| build = false |
| edition = "2021" |
| name = "test-" |
| |
| [profile.release] |
| strip = true |
| |
| [workspace] |
| "#, |
| si!(r#"``` |
| [dependencies] |
| time="0.1.25" |
| ``` |
| fn main() {} |
| "#), |
| ); |
| } |
| |
| #[test] |
| fn test_dash_fence() { |
| snapbox::assert_matches( |
| r#"[[bin]] |
| name = "test-" |
| path = [..] |
| |
| [dependencies] |
| time = "0.1.25" |
| |
| [package] |
| autobenches = false |
| autobins = false |
| autoexamples = false |
| autotests = false |
| build = false |
| edition = "2021" |
| name = "test-" |
| |
| [profile.release] |
| strip = true |
| |
| [workspace] |
| "#, |
| si!(r#"--- |
| [dependencies] |
| time="0.1.25" |
| --- |
| fn main() {} |
| "#), |
| ); |
| } |
| |
| #[test] |
| fn test_hash_fence() { |
| snapbox::assert_matches( |
| r#"[[bin]] |
| name = "test-" |
| path = [..] |
| |
| [dependencies] |
| time = "0.1.25" |
| |
| [package] |
| autobenches = false |
| autobins = false |
| autoexamples = false |
| autotests = false |
| build = false |
| edition = "2021" |
| name = "test-" |
| |
| [profile.release] |
| strip = true |
| |
| [workspace] |
| "#, |
| si!(r#"### |
| [dependencies] |
| time="0.1.25" |
| ### |
| fn main() {} |
| "#), |
| ); |
| } |
| |
| #[test] |
| fn test_hash_prefix() { |
| snapbox::assert_matches( |
| r#"[[bin]] |
| name = "test-" |
| path = [..] |
| |
| [dependencies] |
| time = "0.1.25" |
| |
| [package] |
| autobenches = false |
| autobins = false |
| autoexamples = false |
| autotests = false |
| build = false |
| edition = "2021" |
| name = "test-" |
| |
| [profile.release] |
| strip = true |
| |
| [workspace] |
| "#, |
| si!(r#"## [dependencies] |
| ## time="0.1.25" |
| fn main() {} |
| "#), |
| ); |
| } |
| } |