Skip to content

Commit 25dc3bd

Browse files
committed
Auto merge of #12681 - epage:frontmatter, r=Muscraft
feat(embedded): Hack in code fence support ### What does this PR try to resolve? This is to allow us to get feedback on the design proposed [on zulip](https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/Embedding.20cargo.20manifests.20in.20rust.20source/near/391427092) to verify we want to make an RFC for this syntax. ````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<std::path::PathBuf>, } fn main() { let args = Args::parse(); println!("{:?}", args); } ```` ### How should we test and review this PR? The tests were updated in a separate commit to ensure there was no regression while then migrating to the new syntax to make sure it worked. This involves some future work - Removing doc comment support - Getting the syntax approved and implemented - Migrating to rustc support for the syntax #12207 was updated to record these items so we don't lose track of them
2 parents 3ea3c3a + b3bc676 commit 25dc3bd

File tree

3 files changed

+177
-81
lines changed

3 files changed

+177
-81
lines changed

src/cargo/util/toml/embedded.rs

Lines changed: 154 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,72 @@ pub fn expand_manifest(
1515
path: &std::path::Path,
1616
config: &Config,
1717
) -> CargoResult<String> {
18-
let comment = match extract_comment(content) {
19-
Ok(comment) => Some(comment),
20-
Err(err) => {
21-
tracing::trace!("failed to extract doc comment: {err}");
22-
None
18+
let source = split_source(content)?;
19+
if let Some(frontmatter) = source.frontmatter {
20+
match source.info {
21+
Some("cargo") => {}
22+
None => {
23+
anyhow::bail!("frontmatter is missing an infostring; specify `cargo` for embedding a manifest");
24+
}
25+
Some(other) => {
26+
if let Some(remainder) = other.strip_prefix("cargo,") {
27+
anyhow::bail!("cargo does not support frontmatter infostring attributes like `{remainder}` at this time")
28+
} else {
29+
anyhow::bail!("frontmatter infostring `{other}` is unsupported by cargo; specify `cargo` for embedding a manifest")
30+
}
31+
}
2332
}
24-
}
25-
.unwrap_or_default();
26-
let manifest = match extract_manifest(&comment)? {
27-
Some(manifest) => Some(manifest),
28-
None => {
29-
tracing::trace!("failed to extract manifest");
30-
None
33+
34+
// HACK: until rustc has native support for this syntax, we have to remove it from the
35+
// source file
36+
use std::fmt::Write as _;
37+
let hash = crate::util::hex::short_hash(&path.to_string_lossy());
38+
let mut rel_path = std::path::PathBuf::new();
39+
rel_path.push("target");
40+
rel_path.push(&hash[0..2]);
41+
rel_path.push(&hash[2..]);
42+
let target_dir = config.home().join(rel_path);
43+
let hacked_path = target_dir
44+
.join(
45+
path.file_name()
46+
.expect("always a name for embedded manifests"),
47+
)
48+
.into_path_unlocked();
49+
let mut hacked_source = String::new();
50+
if let Some(shebang) = source.shebang {
51+
writeln!(hacked_source, "{shebang}")?;
52+
}
53+
writeln!(hacked_source)?; // open
54+
for _ in 0..frontmatter.lines().count() {
55+
writeln!(hacked_source)?;
3156
}
57+
writeln!(hacked_source)?; // close
58+
writeln!(hacked_source, "{}", source.content)?;
59+
if let Some(parent) = hacked_path.parent() {
60+
cargo_util::paths::create_dir_all(parent)?;
61+
}
62+
cargo_util::paths::write_if_changed(&hacked_path, hacked_source)?;
63+
64+
let manifest = expand_manifest_(&frontmatter, &hacked_path, config)
65+
.with_context(|| format!("failed to parse manifest at {}", path.display()))?;
66+
let manifest = toml::to_string_pretty(&manifest)?;
67+
Ok(manifest)
68+
} else {
69+
// Legacy doc-comment support; here only for transitional purposes
70+
let comment = extract_comment(content)?.unwrap_or_default();
71+
let manifest = match extract_manifest(&comment)? {
72+
Some(manifest) => Some(manifest),
73+
None => {
74+
tracing::trace!("failed to extract manifest");
75+
None
76+
}
77+
}
78+
.unwrap_or_default();
79+
let manifest = expand_manifest_(&manifest, path, config)
80+
.with_context(|| format!("failed to parse manifest at {}", path.display()))?;
81+
let manifest = toml::to_string_pretty(&manifest)?;
82+
Ok(manifest)
3283
}
33-
.unwrap_or_default();
34-
let manifest = expand_manifest_(&manifest, path, config)
35-
.with_context(|| format!("failed to parse manifest at {}", path.display()))?;
36-
let manifest = toml::to_string_pretty(&manifest)?;
37-
Ok(manifest)
3884
}
3985

4086
fn expand_manifest_(
@@ -66,10 +112,8 @@ fn expand_manifest_(
66112
anyhow::bail!("`package.{key}` is not allowed in embedded manifests")
67113
}
68114
}
69-
let file_name = path
70-
.file_name()
71-
.ok_or_else(|| anyhow::format_err!("no file name"))?
72-
.to_string_lossy();
115+
// HACK: Using an absolute path while `hacked_path` is in use
116+
let bin_path = path.to_string_lossy().into_owned();
73117
let file_stem = path
74118
.file_stem()
75119
.ok_or_else(|| anyhow::format_err!("no file name"))?
@@ -103,10 +147,7 @@ fn expand_manifest_(
103147

104148
let mut bin = toml::Table::new();
105149
bin.insert("name".to_owned(), toml::Value::String(bin_name));
106-
bin.insert(
107-
"path".to_owned(),
108-
toml::Value::String(file_name.into_owned()),
109-
);
150+
bin.insert("path".to_owned(), toml::Value::String(bin_path));
110151
manifest.insert(
111152
"bin".to_owned(),
112153
toml::Value::Array(vec![toml::Value::Table(bin)]),
@@ -159,8 +200,82 @@ fn sanitize_name(name: &str) -> String {
159200
name
160201
}
161202

203+
struct Source<'s> {
204+
shebang: Option<&'s str>,
205+
info: Option<&'s str>,
206+
frontmatter: Option<&'s str>,
207+
content: &'s str,
208+
}
209+
210+
fn split_source(input: &str) -> CargoResult<Source<'_>> {
211+
let mut source = Source {
212+
shebang: None,
213+
info: None,
214+
frontmatter: None,
215+
content: input,
216+
};
217+
218+
// See rust-lang/rust's compiler/rustc_lexer/src/lib.rs's `strip_shebang`
219+
// Shebang must start with `#!` literally, without any preceding whitespace.
220+
// For simplicity we consider any line starting with `#!` a shebang,
221+
// regardless of restrictions put on shebangs by specific platforms.
222+
if let Some(rest) = source.content.strip_prefix("#!") {
223+
// Ok, this is a shebang but if the next non-whitespace token is `[`,
224+
// then it may be valid Rust code, so consider it Rust code.
225+
if rest.trim_start().starts_with('[') {
226+
return Ok(source);
227+
}
228+
229+
// No other choice than to consider this a shebang.
230+
let (shebang, content) = source
231+
.content
232+
.split_once('\n')
233+
.unwrap_or((source.content, ""));
234+
source.shebang = Some(shebang);
235+
source.content = content;
236+
}
237+
238+
let tick_end = source
239+
.content
240+
.char_indices()
241+
.find_map(|(i, c)| (c != '`').then_some(i))
242+
.unwrap_or(source.content.len());
243+
let (fence_pattern, rest) = match tick_end {
244+
0 => {
245+
return Ok(source);
246+
}
247+
1 | 2 => {
248+
anyhow::bail!("found {tick_end} backticks in rust frontmatter, expected at least 3")
249+
}
250+
_ => source.content.split_at(tick_end),
251+
};
252+
let (info, content) = rest.split_once("\n").unwrap_or((rest, ""));
253+
if !info.is_empty() {
254+
source.info = Some(info.trim_end());
255+
}
256+
source.content = content;
257+
258+
let Some((frontmatter, content)) = source.content.split_once(fence_pattern) else {
259+
anyhow::bail!("no closing `{fence_pattern}` found for frontmatter");
260+
};
261+
source.frontmatter = Some(frontmatter);
262+
source.content = content;
263+
264+
let (line, content) = source
265+
.content
266+
.split_once("\n")
267+
.unwrap_or((source.content, ""));
268+
let line = line.trim();
269+
if !line.is_empty() {
270+
anyhow::bail!("unexpected trailing content on closing fence: `{line}`");
271+
}
272+
source.content = content;
273+
274+
Ok(source)
275+
}
276+
162277
/// Locates a "code block manifest" in Rust source.
163-
fn extract_comment(input: &str) -> CargoResult<String> {
278+
fn extract_comment(input: &str) -> CargoResult<Option<String>> {
164279
let mut doc_fragments = Vec::new();
165280
let file = syn::parse_file(input)?;
166281
// HACK: `syn` doesn't tell us what kind of comment was used, so infer it from how many
@@ -181,7 +296,7 @@ fn extract_comment(input: &str) -> CargoResult<String> {
181296
}
182297
}
183298
if doc_fragments.is_empty() {
184-
anyhow::bail!("no doc-comment found");
299+
return Ok(None);
185300
}
186301
unindent_doc_fragments(&mut doc_fragments);
187302

@@ -190,7 +305,7 @@ fn extract_comment(input: &str) -> CargoResult<String> {
190305
add_doc_fragment(&mut doc_comment, frag);
191306
}
192307

193-
Ok(doc_comment)
308+
Ok(Some(doc_comment))
194309
}
195310

196311
/// A `#[doc]`
@@ -496,7 +611,7 @@ mod test_expand {
496611
snapbox::assert_eq(
497612
r#"[[bin]]
498613
name = "test-"
499-
path = "test.rs"
614+
path = "/home/me/test.rs"
500615
501616
[package]
502617
autobenches = false
@@ -523,7 +638,7 @@ strip = true
523638
snapbox::assert_eq(
524639
r#"[[bin]]
525640
name = "test-"
526-
path = "test.rs"
641+
path = "/home/me/test.rs"
527642
528643
[dependencies]
529644
time = "0.1.25"
@@ -561,38 +676,38 @@ mod test_comment {
561676

562677
macro_rules! ec {
563678
($s:expr) => {
564-
extract_comment($s).unwrap_or_else(|err| panic!("{}", err))
679+
extract_comment($s)
680+
.unwrap_or_else(|err| panic!("{}", err))
681+
.unwrap()
565682
};
566683
}
567684

568685
#[test]
569686
fn test_no_comment() {
570-
snapbox::assert_eq(
571-
"no doc-comment found",
687+
assert_eq!(
688+
None,
572689
extract_comment(
573690
r#"
574691
fn main () {
575692
}
576693
"#,
577694
)
578-
.unwrap_err()
579-
.to_string(),
695+
.unwrap()
580696
);
581697
}
582698

583699
#[test]
584700
fn test_no_comment_she_bang() {
585-
snapbox::assert_eq(
586-
"no doc-comment found",
701+
assert_eq!(
702+
None,
587703
extract_comment(
588704
r#"#!/usr/bin/env cargo-eval
589705
590706
fn main () {
591707
}
592708
"#,
593709
)
594-
.unwrap_err()
595-
.to_string(),
710+
.unwrap()
596711
);
597712
}
598713

src/doc/src/reference/unstable.md

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,13 +1191,12 @@ fn main() {}
11911191
```
11921192

11931193
A user may optionally specify a manifest in a `cargo` code fence in a module-level comment, like:
1194-
```rust
1194+
````rust
11951195
#!/usr/bin/env -S cargo +nightly -Zscript
1196-
1197-
//! ```cargo
1198-
//! [dependencies]
1199-
//! clap = { version = "4.2", features = ["derive"] }
1200-
//! ```
1196+
```cargo
1197+
[dependencies]
1198+
clap = { version = "4.2", features = ["derive"] }
1199+
```
12011200

12021201
use clap::Parser;
12031202

@@ -1212,7 +1211,7 @@ fn main() {
12121211
let args = Args::parse();
12131212
println!("{:?}", args);
12141213
}
1215-
```
1214+
````
12161215

12171216
### Single-file packages
12181217

@@ -1225,22 +1224,8 @@ Single-file packages may be selected via `--manifest-path`, like
12251224
`cargo test --manifest-path foo.rs`. Unlike `Cargo.toml`, these files cannot be auto-discovered.
12261225

12271226
A single-file package may contain an embedded manifest. An embedded manifest
1228-
is stored using `TOML` in a markdown code-fence with `cargo` at the start of the
1229-
infostring inside a target-level doc-comment. It is an error to have multiple
1230-
`cargo` code fences in the target-level doc-comment. We can relax this later,
1231-
either merging the code fences or ignoring later code fences.
1232-
1233-
Supported forms of embedded manifest are:
1234-
``````rust
1235-
//! ```cargo
1236-
//! ```
1237-
``````
1238-
``````rust
1239-
/*!
1240-
* ```cargo
1241-
* ```
1242-
*/
1243-
``````
1227+
is stored using `TOML` in rust "frontmatter", a markdown code-fence with `cargo`
1228+
at the start of the infostring at the top of the file.
12441229

12451230
Inferred / defaulted manifest fields:
12461231
- `package.name = <slugified file stem>`

tests/testsuite/script.rs

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -208,11 +208,10 @@ error: running `echo.rs` requires `-Zscript`
208208
#[cargo_test]
209209
fn clean_output_with_edition() {
210210
let script = r#"#!/usr/bin/env cargo
211-
212-
//! ```cargo
213-
//! [package]
214-
//! edition = "2018"
215-
//! ```
211+
```cargo
212+
[package]
213+
edition = "2018"
214+
```
216215
217216
fn main() {
218217
println!("Hello world!");
@@ -240,10 +239,9 @@ fn main() {
240239
#[cargo_test]
241240
fn warning_without_edition() {
242241
let script = r#"#!/usr/bin/env cargo
243-
244-
//! ```cargo
245-
//! [package]
246-
//! ```
242+
```cargo
243+
[package]
244+
```
247245
248246
fn main() {
249247
println!("Hello world!");
@@ -625,11 +623,10 @@ fn missing_script_rs() {
625623
fn test_name_same_as_dependency() {
626624
Package::new("script", "1.0.0").publish();
627625
let script = r#"#!/usr/bin/env cargo
628-
629-
//! ```cargo
630-
//! [dependencies]
631-
//! script = "1.0.0"
632-
//! ```
626+
```cargo
627+
[dependencies]
628+
script = "1.0.0"
629+
```
633630
634631
fn main() {
635632
println!("Hello world!");
@@ -662,11 +659,10 @@ fn main() {
662659
#[cargo_test]
663660
fn test_path_dep() {
664661
let script = r#"#!/usr/bin/env cargo
665-
666-
//! ```cargo
667-
//! [dependencies]
668-
//! bar.path = "./bar"
669-
//! ```
662+
```cargo
663+
[dependencies]
664+
bar.path = "./bar"
665+
```
670666
671667
fn main() {
672668
println!("Hello world!");

0 commit comments

Comments
 (0)