Skip to content

Commit 92de1a2

Browse files
authored
feat(toml): Allow adding/removing from cargo scripts (#14857)
### What does this PR try to resolve? This adds support for cargo script for more cargo commands and is a part of #12207 Unfortunately, there is a double-warning for unspecified editions for `cargo remove`. Rather than addressing that here, I'll be noting it in the tracking issue. ### How should we test and review this PR? ### Additional information
2 parents 10c255a + 26844a3 commit 92de1a2

32 files changed

+742
-101
lines changed

src/bin/cargo/commands/remove.rs

+16-16
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,7 @@ fn parse_section(args: &ArgMatches) -> DepTable {
161161
/// Clean up the workspace.dependencies, profile, patch, and replace sections of the root manifest
162162
/// by removing dependencies which no longer have a reference to them.
163163
fn gc_workspace(workspace: &Workspace<'_>) -> CargoResult<()> {
164-
let mut manifest: toml_edit::DocumentMut =
165-
cargo_util::paths::read(workspace.root_manifest())?.parse()?;
164+
let mut workspace_manifest = LocalManifest::try_new(workspace.root_manifest())?;
166165
let mut is_modified = true;
167166

168167
let members = workspace
@@ -177,8 +176,8 @@ fn gc_workspace(workspace: &Workspace<'_>) -> CargoResult<()> {
177176

178177
let mut dependencies = members
179178
.into_iter()
180-
.flat_map(|(manifest, unstable_features)| {
181-
manifest
179+
.flat_map(|(member_manifest, unstable_features)| {
180+
member_manifest
182181
.get_sections()
183182
.into_iter()
184183
.flat_map(move |(_, table)| {
@@ -190,7 +189,7 @@ fn gc_workspace(workspace: &Workspace<'_>) -> CargoResult<()> {
190189
Dependency::from_toml(
191190
workspace.gctx(),
192191
workspace.root(),
193-
&manifest.path,
192+
&member_manifest.path,
194193
&unstable_features,
195194
key,
196195
item,
@@ -203,7 +202,8 @@ fn gc_workspace(workspace: &Workspace<'_>) -> CargoResult<()> {
203202

204203
// Clean up the workspace.dependencies section and replace instances of
205204
// workspace dependencies with their definitions
206-
if let Some(toml_edit::Item::Table(deps_table)) = manifest
205+
if let Some(toml_edit::Item::Table(deps_table)) = workspace_manifest
206+
.data
207207
.get_mut("workspace")
208208
.and_then(|t| t.get_mut("dependencies"))
209209
{
@@ -246,7 +246,9 @@ fn gc_workspace(workspace: &Workspace<'_>) -> CargoResult<()> {
246246
// Example tables:
247247
// - profile.dev.package.foo
248248
// - profile.release.package."foo:2.1.0"
249-
if let Some(toml_edit::Item::Table(profile_section_table)) = manifest.get_mut("profile") {
249+
if let Some(toml_edit::Item::Table(profile_section_table)) =
250+
workspace_manifest.data.get_mut("profile")
251+
{
250252
profile_section_table.set_implicit(true);
251253

252254
for (_, item) in profile_section_table.iter_mut() {
@@ -280,7 +282,7 @@ fn gc_workspace(workspace: &Workspace<'_>) -> CargoResult<()> {
280282
}
281283

282284
// Clean up the replace section
283-
if let Some(toml_edit::Item::Table(table)) = manifest.get_mut("replace") {
285+
if let Some(toml_edit::Item::Table(table)) = workspace_manifest.data.get_mut("replace") {
284286
table.set_implicit(true);
285287

286288
for (key, item) in table.iter_mut() {
@@ -296,10 +298,7 @@ fn gc_workspace(workspace: &Workspace<'_>) -> CargoResult<()> {
296298
}
297299

298300
if is_modified {
299-
cargo_util::paths::write_atomic(
300-
workspace.root_manifest(),
301-
manifest.to_string().as_bytes(),
302-
)?;
301+
workspace_manifest.write()?;
303302
}
304303

305304
Ok(())
@@ -340,12 +339,13 @@ fn spec_has_match(
340339

341340
/// Removes unused patches from the manifest
342341
fn gc_unused_patches(workspace: &Workspace<'_>, resolve: &Resolve) -> CargoResult<bool> {
343-
let mut manifest: toml_edit::DocumentMut =
344-
cargo_util::paths::read(workspace.root_manifest())?.parse()?;
342+
let mut workspace_manifest = LocalManifest::try_new(workspace.root_manifest())?;
345343
let mut modified = false;
346344

347345
// Clean up the patch section
348-
if let Some(toml_edit::Item::Table(patch_section_table)) = manifest.get_mut("patch") {
346+
if let Some(toml_edit::Item::Table(patch_section_table)) =
347+
workspace_manifest.data.get_mut("patch")
348+
{
349349
patch_section_table.set_implicit(true);
350350

351351
for (_, item) in patch_section_table.iter_mut() {
@@ -383,7 +383,7 @@ fn gc_unused_patches(workspace: &Workspace<'_>, resolve: &Resolve) -> CargoResul
383383
}
384384

385385
if modified {
386-
cargo_util::paths::write(workspace.root_manifest(), manifest.to_string().as_bytes())?;
386+
workspace_manifest.write()?;
387387
}
388388

389389
Ok(modified)

src/cargo/util/toml/embedded.rs

+100-82
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ pub(super) fn expand_manifest(
2121
path: &std::path::Path,
2222
gctx: &GlobalContext,
2323
) -> CargoResult<String> {
24-
let source = split_source(content)?;
25-
if let Some(frontmatter) = source.frontmatter {
26-
match source.info {
24+
let source = ScriptSource::parse(content)?;
25+
if let Some(frontmatter) = source.frontmatter() {
26+
match source.info() {
2727
Some("cargo") | None => {}
2828
Some(other) => {
2929
if let Some(remainder) = other.strip_prefix("cargo,") {
@@ -50,15 +50,15 @@ pub(super) fn expand_manifest(
5050
)
5151
.into_path_unlocked();
5252
let mut hacked_source = String::new();
53-
if let Some(shebang) = source.shebang {
53+
if let Some(shebang) = source.shebang() {
5454
writeln!(hacked_source, "{shebang}")?;
5555
}
5656
writeln!(hacked_source)?; // open
5757
for _ in 0..frontmatter.lines().count() {
5858
writeln!(hacked_source)?;
5959
}
6060
writeln!(hacked_source)?; // close
61-
writeln!(hacked_source, "{}", source.content)?;
61+
writeln!(hacked_source, "{}", source.content())?;
6262
if let Some(parent) = hacked_path.parent() {
6363
cargo_util::paths::create_dir_all(parent)?;
6464
}
@@ -189,94 +189,112 @@ fn sanitize_name(name: &str) -> String {
189189
}
190190

191191
#[derive(Debug)]
192-
struct Source<'s> {
192+
pub struct ScriptSource<'s> {
193193
shebang: Option<&'s str>,
194194
info: Option<&'s str>,
195195
frontmatter: Option<&'s str>,
196196
content: &'s str,
197197
}
198198

199-
fn split_source(input: &str) -> CargoResult<Source<'_>> {
200-
let mut source = Source {
201-
shebang: None,
202-
info: None,
203-
frontmatter: None,
204-
content: input,
205-
};
199+
impl<'s> ScriptSource<'s> {
200+
pub fn parse(input: &'s str) -> CargoResult<Self> {
201+
let mut source = Self {
202+
shebang: None,
203+
info: None,
204+
frontmatter: None,
205+
content: input,
206+
};
207+
208+
// See rust-lang/rust's compiler/rustc_lexer/src/lib.rs's `strip_shebang`
209+
// Shebang must start with `#!` literally, without any preceding whitespace.
210+
// For simplicity we consider any line starting with `#!` a shebang,
211+
// regardless of restrictions put on shebangs by specific platforms.
212+
if let Some(rest) = source.content.strip_prefix("#!") {
213+
// Ok, this is a shebang but if the next non-whitespace token is `[`,
214+
// then it may be valid Rust code, so consider it Rust code.
215+
if rest.trim_start().starts_with('[') {
216+
return Ok(source);
217+
}
206218

207-
// See rust-lang/rust's compiler/rustc_lexer/src/lib.rs's `strip_shebang`
208-
// Shebang must start with `#!` literally, without any preceding whitespace.
209-
// For simplicity we consider any line starting with `#!` a shebang,
210-
// regardless of restrictions put on shebangs by specific platforms.
211-
if let Some(rest) = source.content.strip_prefix("#!") {
212-
// Ok, this is a shebang but if the next non-whitespace token is `[`,
213-
// then it may be valid Rust code, so consider it Rust code.
214-
if rest.trim_start().starts_with('[') {
215-
return Ok(source);
219+
// No other choice than to consider this a shebang.
220+
let newline_end = source
221+
.content
222+
.find('\n')
223+
.map(|pos| pos + 1)
224+
.unwrap_or(source.content.len());
225+
let (shebang, content) = source.content.split_at(newline_end);
226+
source.shebang = Some(shebang);
227+
source.content = content;
216228
}
217229

218-
// No other choice than to consider this a shebang.
219-
let newline_end = source
220-
.content
221-
.find('\n')
222-
.map(|pos| pos + 1)
230+
const FENCE_CHAR: char = '-';
231+
232+
let mut trimmed_content = source.content;
233+
while !trimmed_content.is_empty() {
234+
let c = trimmed_content;
235+
let c = c.trim_start_matches([' ', '\t']);
236+
let c = c.trim_start_matches(['\r', '\n']);
237+
if c == trimmed_content {
238+
break;
239+
}
240+
trimmed_content = c;
241+
}
242+
let fence_end = trimmed_content
243+
.char_indices()
244+
.find_map(|(i, c)| (c != FENCE_CHAR).then_some(i))
223245
.unwrap_or(source.content.len());
224-
let (shebang, content) = source.content.split_at(newline_end);
225-
source.shebang = Some(shebang);
246+
let (fence_pattern, rest) = match fence_end {
247+
0 => {
248+
return Ok(source);
249+
}
250+
1 | 2 => {
251+
anyhow::bail!(
252+
"found {fence_end} `{FENCE_CHAR}` in rust frontmatter, expected at least 3"
253+
)
254+
}
255+
_ => trimmed_content.split_at(fence_end),
256+
};
257+
let (info, content) = rest.split_once("\n").unwrap_or((rest, ""));
258+
let info = info.trim();
259+
if !info.is_empty() {
260+
source.info = Some(info);
261+
}
226262
source.content = content;
227-
}
228263

229-
const FENCE_CHAR: char = '-';
264+
let Some((frontmatter, content)) = source.content.split_once(fence_pattern) else {
265+
anyhow::bail!("no closing `{fence_pattern}` found for frontmatter");
266+
};
267+
source.frontmatter = Some(frontmatter);
268+
source.content = content;
230269

231-
let mut trimmed_content = source.content;
232-
while !trimmed_content.is_empty() {
233-
let c = trimmed_content;
234-
let c = c.trim_start_matches([' ', '\t']);
235-
let c = c.trim_start_matches(['\r', '\n']);
236-
if c == trimmed_content {
237-
break;
270+
let (line, content) = source
271+
.content
272+
.split_once("\n")
273+
.unwrap_or((source.content, ""));
274+
let line = line.trim();
275+
if !line.is_empty() {
276+
anyhow::bail!("unexpected trailing content on closing fence: `{line}`");
238277
}
239-
trimmed_content = c;
278+
source.content = content;
279+
280+
Ok(source)
240281
}
241-
let fence_end = trimmed_content
242-
.char_indices()
243-
.find_map(|(i, c)| (c != FENCE_CHAR).then_some(i))
244-
.unwrap_or(source.content.len());
245-
let (fence_pattern, rest) = match fence_end {
246-
0 => {
247-
return Ok(source);
248-
}
249-
1 | 2 => {
250-
anyhow::bail!(
251-
"found {fence_end} `{FENCE_CHAR}` in rust frontmatter, expected at least 3"
252-
)
253-
}
254-
_ => trimmed_content.split_at(fence_end),
255-
};
256-
let (info, content) = rest.split_once("\n").unwrap_or((rest, ""));
257-
let info = info.trim();
258-
if !info.is_empty() {
259-
source.info = Some(info);
282+
283+
pub fn shebang(&self) -> Option<&'s str> {
284+
self.shebang
260285
}
261-
source.content = content;
262286

263-
let Some((frontmatter, content)) = source.content.split_once(fence_pattern) else {
264-
anyhow::bail!("no closing `{fence_pattern}` found for frontmatter");
265-
};
266-
source.frontmatter = Some(frontmatter);
267-
source.content = content;
268-
269-
let (line, content) = source
270-
.content
271-
.split_once("\n")
272-
.unwrap_or((source.content, ""));
273-
let line = line.trim();
274-
if !line.is_empty() {
275-
anyhow::bail!("unexpected trailing content on closing fence: `{line}`");
287+
pub fn info(&self) -> Option<&'s str> {
288+
self.info
276289
}
277-
source.content = content;
278290

279-
Ok(source)
291+
pub fn frontmatter(&self) -> Option<&'s str> {
292+
self.frontmatter
293+
}
294+
295+
pub fn content(&self) -> &'s str {
296+
self.content
297+
}
280298
}
281299

282300
#[cfg(test)]
@@ -291,16 +309,16 @@ mod test_expand {
291309
fn assert_source(source: &str, expected: impl IntoData) {
292310
use std::fmt::Write as _;
293311

294-
let actual = match split_source(source) {
312+
let actual = match ScriptSource::parse(source) {
295313
Ok(actual) => actual,
296314
Err(err) => panic!("unexpected err: {err}"),
297315
};
298316

299317
let mut rendered = String::new();
300-
write_optional_field(&mut rendered, "shebang", actual.shebang);
301-
write_optional_field(&mut rendered, "info", actual.info);
302-
write_optional_field(&mut rendered, "frontmatter", actual.frontmatter);
303-
writeln!(&mut rendered, "content: {:?}", actual.content).unwrap();
318+
write_optional_field(&mut rendered, "shebang", actual.shebang());
319+
write_optional_field(&mut rendered, "info", actual.info());
320+
write_optional_field(&mut rendered, "frontmatter", actual.frontmatter());
321+
writeln!(&mut rendered, "content: {:?}", actual.content()).unwrap();
304322
assert_data_eq!(rendered, expected.raw());
305323
}
306324

@@ -497,7 +515,7 @@ content: "\nfn main() {}"
497515
#[test]
498516
fn split_too_few_dashes() {
499517
assert_err(
500-
split_source(
518+
ScriptSource::parse(
501519
r#"#!/usr/bin/env cargo
502520
--
503521
[dependencies]
@@ -513,7 +531,7 @@ fn main() {}
513531
#[test]
514532
fn split_mismatched_dashes() {
515533
assert_err(
516-
split_source(
534+
ScriptSource::parse(
517535
r#"#!/usr/bin/env cargo
518536
---
519537
[dependencies]
@@ -529,7 +547,7 @@ fn main() {}
529547
#[test]
530548
fn split_missing_close() {
531549
assert_err(
532-
split_source(
550+
ScriptSource::parse(
533551
r#"#!/usr/bin/env cargo
534552
---
535553
[dependencies]

src/cargo/util/toml/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ mod targets;
3939

4040
use self::targets::to_targets;
4141

42+
pub use embedded::ScriptSource;
43+
4244
/// See also `bin/cargo/commands/run.rs`s `is_manifest_command`
4345
pub fn is_embedded(path: &Path) -> bool {
4446
let ext = path.extension();

src/cargo/util/toml_mut/dependency.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,8 @@ mod tests {
12521252
let mut local = LocalManifest {
12531253
path: crate_root.clone(),
12541254
manifest,
1255+
embedded: None,
1256+
raw: toml.to_owned(),
12551257
};
12561258
assert_eq!(local.manifest.to_string(), toml);
12571259
let gctx = GlobalContext::default().unwrap();

0 commit comments

Comments
 (0)