Repository navigation
Expand file tree
/
Copy pathversion.rs
More file actions
149 lines (136 loc) · 5.72 KB
/
Copy pathversion.rs
File metadata and controls
149 lines (136 loc) · 5.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//! `cargo xtask version [set X.Y.Z[-pre]]`: the workspace version, the single source of truth.
//!
//! The version lives in `[workspace.package] version` in the root `Cargo.toml`; every crate
//! inherits it with `version.workspace = true`, and the packaging scripts read it from here.
use std::path::Path;
/// The `version = "…"` value inside `[workspace.package]`.
pub fn read(manifest: &str) -> Result<String, String> {
let (_, line) = find_line(manifest)?;
let v = line.split_once('=').map(|(_, v)| v.trim().trim_matches('"').to_string()).unwrap_or_default();
if v.is_empty() {
return Err("empty [workspace.package] version".into());
}
Ok(v)
}
/// `manifest` with the workspace version replaced; everything else is byte-for-byte unchanged.
pub fn replace(manifest: &str, new: &str) -> Result<String, String> {
validate(new)?;
let (idx, _) = find_line(manifest)?;
let mut out = String::with_capacity(manifest.len() + 8);
for (i, line) in manifest.split_inclusive('\n').enumerate() {
if i == idx {
let eol = if line.ends_with("\r\n") {
"\r\n"
} else if line.ends_with('\n') {
"\n"
} else {
""
};
out.push_str(&format!("version = \"{new}\"{eol}"));
} else {
out.push_str(line);
}
}
Ok(out)
}
/// Semver without build metadata: `MAJOR.MINOR.PATCH` plus an optional `-pre.release` tag.
pub fn validate(v: &str) -> Result<(), String> {
let bad = || Err(format!("`{v}` is not a version like 1.2.3 or 1.2.3-rc.1"));
let (core, pre) = match v.split_once('-') {
Some((c, p)) => (c, Some(p)),
None => (v, None),
};
let nums: Vec<&str> = core.split('.').collect();
if nums.len() != 3 || nums.iter().any(|n| n.is_empty() || !n.bytes().all(|b| b.is_ascii_digit()) || (n.len() > 1 && n.starts_with('0'))) {
return bad();
}
if let Some(p) = pre
&& (p.is_empty() || p.split('.').any(|id| id.is_empty() || !id.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')))
{
return bad();
}
Ok(())
}
/// Index and text of the `version` line in `[workspace.package]`.
fn find_line(manifest: &str) -> Result<(usize, &str), String> {
let mut in_section = false;
for (i, line) in manifest.lines().enumerate() {
let t = line.trim();
if t.starts_with('[') {
in_section = t == "[workspace.package]";
continue;
}
if in_section
&& let Some(rest) = t.strip_prefix("version")
&& rest.trim_start().starts_with('=')
{
return Ok((i, line));
}
}
Err("no `version = \"…\"` in [workspace.package] of the root Cargo.toml".into())
}
pub fn run(root: &Path, args: &[&str]) -> Result<(), String> {
let path = root.join("Cargo.toml");
let text = std::fs::read_to_string(&path).map_err(|e| format!("read {}: {e}", path.display()))?;
match args {
[] => {
println!("{}", read(&text)?);
Ok(())
}
["set", new] => {
let old = read(&text)?;
let updated = replace(&text, new)?;
// Write atomically: a half-written root manifest breaks every build in the workspace.
let tmp = root.join("target").join("Cargo.toml.xtask-version");
std::fs::create_dir_all(root.join("target")).map_err(|e| format!("create target/: {e}"))?;
std::fs::write(&tmp, updated).map_err(|e| format!("write {}: {e}", tmp.display()))?;
std::fs::rename(&tmp, &path).map_err(|e| format!("replace {}: {e}", path.display()))?;
// Refresh the workspace members' entries in Cargo.lock (no dependency upgrades).
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
if crate::run(std::process::Command::new(&cargo).args(["update", "--workspace", "--offline"])).is_err() {
crate::run(std::process::Command::new(&cargo).args(["update", "--workspace"]))?;
}
println!("version: {old} -> {new}");
Ok(())
}
_ => Err("usage: cargo xtask version [set X.Y.Z[-pre]]".into()),
}
}
#[cfg(test)]
mod tests {
use super::*;
const MANIFEST: &str = "[workspace]\nmembers = [\"a\"]\n\n[workspace.package]\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[workspace.dependencies]\nfoo = { version = \"1\" }\n";
#[test]
fn reads_the_workspace_version() {
assert_eq!(read(MANIFEST).unwrap(), "0.1.0");
assert!(read("[package]\nversion = \"1.0.0\"\n").is_err());
}
#[test]
fn replaces_only_the_workspace_version() {
let out = replace(MANIFEST, "1.2.3-rc.1").unwrap();
assert_eq!(read(&out).unwrap(), "1.2.3-rc.1");
assert_eq!(out, MANIFEST.replace("version = \"0.1.0\"", "version = \"1.2.3-rc.1\""));
assert!(out.contains("foo = { version = \"1\" }"));
}
#[test]
fn keeps_crlf_line_endings() {
let crlf = MANIFEST.replace('\n', "\r\n");
let out = replace(&crlf, "2.0.0").unwrap();
assert_eq!(out, crlf.replace("\"0.1.0\"", "\"2.0.0\""));
}
#[test]
fn validates_versions() {
for ok in ["0.1.0", "1.20.300", "1.0.0-rc.1", "1.0.0-alpha", "1.0.0-x-y.2"] {
assert!(validate(ok).is_ok(), "{ok}");
}
for bad in ["1.0", "1.0.0.0", "v1.0.0", "01.0.0", "1.0.0-", "1.0.0-a..b", "1.0.0+meta", "1.a.0", ""] {
assert!(validate(bad).is_err(), "{bad}");
}
assert!(replace(MANIFEST, "nope").is_err());
}
#[test]
fn the_real_manifest_has_a_valid_version() {
let text = std::fs::read_to_string(crate::root().join("Cargo.toml")).unwrap();
validate(&read(&text).unwrap()).unwrap();
}
}