Skip to content

Commit ba869d3

Browse files
committed
feat(embedded): Hack in code fence support
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.
1 parent 4638ef9 commit ba869d3

File tree

1 file changed

+142
-18
lines changed

1 file changed

+142
-18
lines changed

src/cargo/util/toml/embedded.rs

Lines changed: 142 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,72 @@ pub fn expand_manifest(
1515
path: &std::path::Path,
1616
config: &Config,
1717
) -> CargoResult<String> {
18-
let comment = extract_comment(content)?.unwrap_or_default();
19-
let manifest = match extract_manifest(&comment)? {
20-
Some(manifest) => Some(manifest),
21-
None => {
22-
tracing::trace!("failed to extract manifest");
23-
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+
}
32+
}
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)?;
2456
}
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)
2583
}
26-
.unwrap_or_default();
27-
let manifest = expand_manifest_(&manifest, path, config)
28-
.with_context(|| format!("failed to parse manifest at {}", path.display()))?;
29-
let manifest = toml::to_string_pretty(&manifest)?;
30-
Ok(manifest)
3184
}
3285

3386
fn expand_manifest_(
@@ -59,11 +112,8 @@ fn expand_manifest_(
59112
anyhow::bail!("`package.{key}` is not allowed in embedded manifests")
60113
}
61114
}
62-
let bin_path = path
63-
.file_name()
64-
.ok_or_else(|| anyhow::format_err!("no file name"))?
65-
.to_string_lossy()
66-
.into_owned();
115+
// HACK: Using an absolute path while `hacked_path` is in use
116+
let bin_path = path.to_string_lossy().into_owned();
67117
let file_stem = path
68118
.file_stem()
69119
.ok_or_else(|| anyhow::format_err!("no file name"))?
@@ -150,6 +200,80 @@ fn sanitize_name(name: &str) -> String {
150200
name
151201
}
152202

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+
153277
/// Locates a "code block manifest" in Rust source.
154278
fn extract_comment(input: &str) -> CargoResult<Option<String>> {
155279
let mut doc_fragments = Vec::new();
@@ -487,7 +611,7 @@ mod test_expand {
487611
snapbox::assert_eq(
488612
r#"[[bin]]
489613
name = "test-"
490-
path = "test.rs"
614+
path = "/home/me/test.rs"
491615
492616
[package]
493617
autobenches = false
@@ -514,7 +638,7 @@ strip = true
514638
snapbox::assert_eq(
515639
r#"[[bin]]
516640
name = "test-"
517-
path = "test.rs"
641+
path = "/home/me/test.rs"
518642
519643
[dependencies]
520644
time = "0.1.25"

0 commit comments

Comments
 (0)