From df015f34eb5f07c4897891f4fde2b7a57ec06458 Mon Sep 17 00:00:00 2001 From: busticated Date: Sun, 26 Nov 2023 14:26:39 -0800 Subject: [PATCH 01/31] ignore TODOs in README --- xtask/src/git.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/xtask/src/git.rs b/xtask/src/git.rs index b026c9a..b6a9a85 100644 --- a/xtask/src/git.rs +++ b/xtask/src/git.rs @@ -127,6 +127,7 @@ impl<'a> Git<'a> { "--", ":!./target/*", ":!./tmp/*", + ":!./README.md", ], [""], ) @@ -218,7 +219,8 @@ mod tests { "--line-number", "--", ":!./target/*", - ":!./tmp/*" + ":!./tmp/*", + ":!./README.md", ] ); } From 9b8ad31ce3b2042ceb005b7936a058fd1e5affaf Mon Sep 17 00:00:00 2001 From: busticated Date: Sun, 26 Nov 2023 14:27:14 -0800 Subject: [PATCH 02/31] tune xtask help output --- xtask/src/tasks.rs | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/xtask/src/tasks.rs b/xtask/src/tasks.rs index a12b885..f6b7c8e 100644 --- a/xtask/src/tasks.rs +++ b/xtask/src/tasks.rs @@ -90,18 +90,16 @@ impl Tasks { for task in self.map.values() { let char_count = task.name.char_indices().count(); let spaces = separator.repeat(max_col_width - char_count + padding); - let line = format!(">> {}{}{}\n", task.name, spaces, task.description); + let line = format!(" {}{}{}\n", task.name, spaces, task.description); lines.push_str(&line); for (name, description) in task.flags.iter() { let separator = " ".to_string(); let spaces = separator.repeat(max_col_width + padding); - let line = format!("\n{} >> --{} | {}\n", spaces, name, description); + let line = format!("{} ⮑ --{} | {}\n", spaces, name, description); lines.push_str(&line); } - - lines.push('\n'); } Ok(lines) @@ -192,16 +190,11 @@ mod tests { assert_eq!( tasks.help().unwrap(), [ - ">> one....task 01", - "", - " >> --bar | enables bar", - "", - " >> --foo | does the foo", - "", - ">> two....task 02", - "", - " >> --baz | invokes a baz", - "", + " one....task 01", + " ⮑ --bar | enables bar", + " ⮑ --foo | does the foo", + " two....task 02", + " ⮑ --baz | invokes a baz", "", ] .join("\n") From 9e7318ac3f9928fe982248f24c0158b80ffd3b9a Mon Sep 17 00:00:00 2001 From: busticated Date: Tue, 11 Aug 2026 19:59:40 -0700 Subject: [PATCH 03/31] [node-js-release-info] fix tokio for doctests, serde for serialization tests --- crates/node-js-release-info/Cargo.toml | 2 ++ crates/node-js-release-info/README.md | 2 +- crates/node-js-release-info/src/arch.rs | 1 + crates/node-js-release-info/src/ext.rs | 1 + crates/node-js-release-info/src/lib.rs | 1 + crates/node-js-release-info/src/os.rs | 1 + 6 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/node-js-release-info/Cargo.toml b/crates/node-js-release-info/Cargo.toml index 346469c..fc778e8 100644 --- a/crates/node-js-release-info/Cargo.toml +++ b/crates/node-js-release-info/Cargo.toml @@ -30,6 +30,8 @@ tokio = { version = "1.*", default-features = false, features = ["macros", "net" [dev-dependencies] mockito = "1.*" serde_json = "1.*" +# doc examples use `#[tokio::main]`, which requires the multi-threaded runtime +tokio = { version = "1.*", default-features = false, features = ["rt-multi-thread"] } [features] json = ["dep:serde"] diff --git a/crates/node-js-release-info/README.md b/crates/node-js-release-info/README.md index 2a65e43..98bcf30 100644 --- a/crates/node-js-release-info/README.md +++ b/crates/node-js-release-info/README.md @@ -49,7 +49,7 @@ Full `json` serialization + deserialization is available via the `json` feature. cargo add node-js-release-info --features json ``` -```rust +```rust,ignore use node_js_release_info::{NodeJSRelInfo, NodeJSRelInfoError}; #[tokio::main] diff --git a/crates/node-js-release-info/src/arch.rs b/crates/node-js-release-info/src/arch.rs index 0c80dc5..28fce75 100644 --- a/crates/node-js-release-info/src/arch.rs +++ b/crates/node-js-release-info/src/arch.rs @@ -177,6 +177,7 @@ mod tests { } #[test] + #[cfg(feature = "json")] fn it_serializes_and_deserializes() { let arch_json = serde_json::to_string(&NodeJSArch::X64).unwrap(); let arch: NodeJSArch = serde_json::from_str(&arch_json).unwrap(); diff --git a/crates/node-js-release-info/src/ext.rs b/crates/node-js-release-info/src/ext.rs index c9979bf..353c0ea 100644 --- a/crates/node-js-release-info/src/ext.rs +++ b/crates/node-js-release-info/src/ext.rs @@ -130,6 +130,7 @@ mod tests { } #[test] + #[cfg(feature = "json")] fn it_serializes_and_deserializes() { let ext_json = serde_json::to_string(&NodeJSPkgExt::Tarxz).unwrap(); let ext: NodeJSPkgExt = serde_json::from_str(&ext_json).unwrap(); diff --git a/crates/node-js-release-info/src/lib.rs b/crates/node-js-release-info/src/lib.rs index 316244f..61e02dc 100644 --- a/crates/node-js-release-info/src/lib.rs +++ b/crates/node-js-release-info/src/lib.rs @@ -557,6 +557,7 @@ mod tests { } #[test] + #[cfg(feature = "json")] fn it_serializes_and_deserializes() { let version = "20.6.1".to_string(); let filename = "node-v20.6.1-darwin-arm64.tar.gz".to_string(); diff --git a/crates/node-js-release-info/src/os.rs b/crates/node-js-release-info/src/os.rs index 5d8b823..b106bbd 100644 --- a/crates/node-js-release-info/src/os.rs +++ b/crates/node-js-release-info/src/os.rs @@ -137,6 +137,7 @@ mod tests { } #[test] + #[cfg(feature = "json")] fn it_serializes_and_deserializes() { let os_json = serde_json::to_string(&NodeJSOS::Darwin).unwrap(); let os: NodeJSOS = serde_json::from_str(&os_json).unwrap(); From 9dc62edfe27b01592c5426e68881ad1c65589012 Mon Sep 17 00:00:00 2001 From: busticated Date: Thu, 13 Aug 2026 10:27:03 -0700 Subject: [PATCH 04/31] fix lint issues --- xtask/src/changelog.rs | 6 +++--- xtask/src/git.rs | 6 +++--- xtask/src/krate.rs | 2 +- xtask/src/main.rs | 12 ++++++------ xtask/src/tasks.rs | 4 ++-- xtask/src/workspace.rs | 3 +++ 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/xtask/src/changelog.rs b/xtask/src/changelog.rs index 6ea80f3..3042e05 100644 --- a/xtask/src/changelog.rs +++ b/xtask/src/changelog.rs @@ -52,7 +52,7 @@ impl Changelog { pub fn render>(&self, name: N, version: &Version) -> String { let name = name.as_ref(); - let lines = vec![ + let lines = [ format!("# `{}` Changelog", name), MARKER_START.to_string(), MARKER_END.to_string(), @@ -70,10 +70,10 @@ impl Changelog { } self.load()?; let mut changes = format!("{}\n{}\n", MARKER_START, MARKER_END); - changes.push_str(format!("## v{}\n\n", &krate.version).as_str()); + changes.push_str(format!("## v{}\n\n", krate.version).as_str()); for msg in log.iter() { if !msg.is_empty() { - changes.push_str(format!("* {}\n", &msg).as_str()); + changes.push_str(format!("* {}\n", msg).as_str()); } } changes.push('\n'); diff --git a/xtask/src/git.rs b/xtask/src/git.rs index b6a9a85..764593b 100644 --- a/xtask/src/git.rs +++ b/xtask/src/git.rs @@ -140,10 +140,10 @@ impl<'a> Git<'a> { } fn get_changelog_params(&self, krate: &Krate) -> (String, Vec) { - let range = format!("{}@{}..HEAD", &krate.name, &krate.version); - let query = format!(r"--grep=\[{}\]", &krate.name); + let range = format!("{}@{}..HEAD", krate.name, krate.version); + let query = format!(r"--grep=\[{}\]", krate.name); let fmt = String::from("--pretty=format:%B"); - let prefix = format!("[{}]", &krate.name); + let prefix = format!("[{}]", krate.name); let args = self.build_args(["log"], [range, query, fmt]); (prefix, args) } diff --git a/xtask/src/krate.rs b/xtask/src/krate.rs index 1165c53..5874013 100644 --- a/xtask/src/krate.rs +++ b/xtask/src/krate.rs @@ -106,7 +106,7 @@ impl Krate { } pub fn id(&self) -> String { - format!("{}@{}", &self.name, self.version) + format!("{}@{}", self.name, self.version) } pub fn set_version(&mut self, version: Version) -> Result<(), DynError> { diff --git a/xtask/src/main.rs b/xtask/src/main.rs index e05ceb1..1518275 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -38,7 +38,7 @@ fn try_main() -> Result<(), DynError> { args.remove(0); // drop executable path - let cmd = match args.get(0) { + let cmd = match args.first() { Some(x) => x.clone(), None => "".to_string(), }; @@ -104,11 +104,11 @@ fn init_tasks() -> Tasks { tags.insert(name, version); } - for (name, _version) in tags.iter() { + for name in tags.keys() { let krate = krates.get(name).unwrap_or_else(|| panic!("Could Not Find Crate: `{}`!", name)); let log = git.get_changelog(krate)?; - println!(":::: {} [changes: {}]", &krate.name, log.len()); + println!(":::: {} [changes: {}]", krate.name, log.len()); if log.is_empty() { println!("\t--- n/a ---"); @@ -193,7 +193,7 @@ fn init_tasks() -> Tasks { println!(); let coverage_root = String::from("tmp/coverage"); - let report = format!("{}/html/index.html", &coverage_root); + let report = format!("{}/html/index.html", coverage_root); tasks.get("clean").unwrap().exec(vec![], tasks)?; cargo.coverage(&coverage_root).run()?; @@ -345,8 +345,8 @@ fn init_tasks() -> Tasks { for tag in tags { let (name, _ver) = tag.split_once('@').unwrap_or_else(|| panic!("Invalid Tag: `{}`!", tag)); let krate = krates.get(name).unwrap_or_else(|| panic!("Could Not Find Crate: `{}`!", name)); - let message = format!("Publishing: {} at v{}", &krate.name, &krate.version); - println!("{}", &message); + let message = format!("Publishing: {} at v{}", krate.name, krate.version); + println!("{}", message); cargo.publish_package(&krate.name).run()?; } diff --git a/xtask/src/tasks.rs b/xtask/src/tasks.rs index f6b7c8e..16b98ea 100644 --- a/xtask/src/tasks.rs +++ b/xtask/src/tasks.rs @@ -16,7 +16,7 @@ type TaskRunner = fn( tasks: &Tasks, ) -> Result<(), DynError>; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct Task { pub name: String, pub description: String, @@ -51,7 +51,7 @@ impl Task { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct Tasks { map: BTreeMap, } diff --git a/xtask/src/workspace.rs b/xtask/src/workspace.rs index 81d7b45..26d6967 100644 --- a/xtask/src/workspace.rs +++ b/xtask/src/workspace.rs @@ -15,6 +15,9 @@ const CRATES_DIRNAME: &str = "crates"; pub struct Workspace { pub path: PathBuf, pub readme: Readme, + // NOTE: unread for now, but loading it validates the workspace manifest + // parses on every task run - and mirrors `Krate`'s shape + #[allow(dead_code)] pub toml: Toml, } From a54db0a90f8fb3d38bc1b03e2260142ad5cdf005 Mon Sep 17 00:00:00 2001 From: busticated Date: Thu, 13 Aug 2026 10:27:32 -0700 Subject: [PATCH 05/31] [node-js-release-info] fix lint issues --- crates/node-js-release-info/src/specs.rs | 149 +++++++++++------------ 1 file changed, 73 insertions(+), 76 deletions(-) diff --git a/crates/node-js-release-info/src/specs.rs b/crates/node-js-release-info/src/specs.rs index b807383..7fb6276 100644 --- a/crates/node-js-release-info/src/specs.rs +++ b/crates/node-js-release-info/src/specs.rs @@ -9,11 +9,9 @@ use std::str::FromStr; pub fn validate_version>(semver: T) -> Result { match Version::parse(semver.as_ref()) { Ok(v) => Ok(v.to_string()), - Err(_) => { - return Err(NodeJSRelInfoError::InvalidVersion( - semver.as_ref().to_owned(), - )) - } + Err(_) => Err(NodeJSRelInfoError::InvalidVersion( + semver.as_ref().to_owned(), + )), } } @@ -52,7 +50,7 @@ pub fn parse(version: &String, specs: String) -> Option { continue; } - if !filename.starts_with(format!("node-v{}", &version).as_str()) { + if !filename.starts_with(format!("node-v{}", version).as_str()) { continue; } @@ -105,10 +103,79 @@ pub fn parse(version: &String, specs: String) -> Option { Some(all) } +#[cfg(test)] +use mockito::{Mock, Server}; + +#[cfg(test)] +pub fn setup_server_mock( + version: &str, + url_fmt: &mut NodeJSURLFormatter, + server: &mut Server, +) -> Mock { + url_fmt.host = server.host_with_port(); + url_fmt.protocol = "http:".to_string(); + server.mock("GET", url_fmt.info_pathname(version).as_str()) +} + +#[cfg(test)] +pub fn get_fake_specs() -> &'static str { + "ea52b4feaf917e08cd2c729c1186585fcacef07c261a01310c91333b9e41d93c node-v20.6.1-aix-ppc64.tar.gz + 9471bd6dc491e09c31b0f831f5953284b8a6842ed4ccb98f5c62d13e6086c471 node-v20.6.1-arm64.msi + d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46 node-v20.6.1-darwin-arm64.tar.gz + 9c61b0d60fce962244d5e54549dc912e28b3c5f5e23149bfd15f66f8f7269129 node-v20.6.1-darwin-arm64.tar.xz + 365ec544c6596f194afff9a613554abfc68d4a2274181b7651386d9a11cf5862 node-v20.6.1-darwin-x64.tar.gz + 9b10c16670781e3a5af722656d28f264cdd8ebb3140f62692b33813100391349 node-v20.6.1-darwin-x64.tar.xz + d8271461ced2887f65af413949caee19db3e80d22bbefdaf01252ca998570052 node-v20.6.1-headers.tar.gz + 60963e3ee60b6739e97e0c7b8ffb25848a82649c0c277af728400c570fd9db6d node-v20.6.1-headers.tar.xz + d38fe2e41e3fe8ae81b517b4cf49521f500e181e54f4c3d05e2b2d691a57b2ca node-v20.6.1-linux-arm64.tar.gz + 6823720796b287465bb4aa8e7611143322ffd6cbdb9c6e3b149576f6d87953bf node-v20.6.1-linux-arm64.tar.xz + 459510281ea51cf5d89fc666e36fbba80793ae4b90c3a7f89dd6666c65c825b3 node-v20.6.1-linux-armv7l.tar.gz + 9dbd4fd7f804a28de91ffb8792df6e89bbb4f934fccd013624b3dabf8bf809ac node-v20.6.1-linux-armv7l.tar.xz + ca00f1aa8b2535fa167258cf5f2cfce4b79d83c442dd5e46f5e17d6a5749ec0f node-v20.6.1-linux-ppc64le.tar.gz + 27884935b025b6676e4b8737f334673ee825947d0baef61aa0326374597aeb05 node-v20.6.1-linux-ppc64le.tar.xz + 4a3f29cfc8a7ed1e9e44fcacb78e2fbaa3ce01be1efc4971a42710ad1e9e45d1 node-v20.6.1-linux-s390x.tar.gz + 3968d629989b6de16b8872b6d7ee6e6cdf1204def99c43412a6ee28203ed0022 node-v20.6.1-linux-s390x.tar.xz + 26dd13a6f7253f0ab9bcab561353985a297d927840771d905566735b792868da node-v20.6.1-linux-x64.tar.gz + 591f9f274104f266a8cf085d2c7d5d2848ba73b98ae323d501db2d4c4b7026e5 node-v20.6.1-linux-x64.tar.xz + d9acf82d9576dd0350c8e66b55f6fc2750fa9f4aa23d6453ffc58e32af995894 node-v20.6.1.pkg + 0053c09a01b1b355bca5af82927cae376124c13d74fa53567f08f4cfb085e6aa node-v20.6.1.tar.gz + 3aec5e728daa38800c343b129221d3488064a2529a39bb5467bc55be226c6a2b node-v20.6.1.tar.xz + 337549faf397deb0da3bccd4e27db45a619d89de4ea12830d16d9dfaded8e92c node-v20.6.1-win-arm64.7z + 0e62045bfc9d7c38360bd7da152c75ed82087242d5e4b401fa23a439588d36f6 node-v20.6.1-win-arm64.zip + c6cfe7824770a266a30bee8c33f485d0e89b94254c682250a239d83adfb7ce77 node-v20.6.1-win-x64.7z + 88371914f1f75d594bb367570e163cf5ecebeb514fd54cc765093819ebb0ed48 node-v20.6.1-win-x64.zip + 87d631b294a25386400d0f44d227330da62a1326e2a4fbb98bda3d7c431257f1 node-v20.6.1-win-x86.7z + 578cff623601aa8878a035f06edbf69190338ee3b345e7a096e804cb80c4ce24 node-v20.6.1-win-x86.zip + 5c2616da46728dd1326645c7db114e78ad87138a258c0724a035269258c23509 node-v20.6.1-x64.msi + cb83586af83182187e760b7e01aa7c7b2bacb521d60ceefed3ac6fc62c222449 node-v20.6.1-x86.msi + 7cc3240fd7ce7926eef1cbbad33b033f7c5d97b3f3e527d65ff1e2c3f7638a11 win-arm64/node.exe + deb027ded744371657811cfe52e774881ea928d36779924af84aa9a7a31104d2 win-arm64/node.lib + dcb6b4bc6f2a78bf0f759853b59e94ddbe9ad6b9f32d24fdcf590d74c6350bc2 win-arm64/node_pdb.7z + bdcd574e99646ec4a03bb13b3661c957f5a7ca837f5c33827075c4262d449689 win-arm64/node_pdb.zip + 5b824f3a375cca06dfd7dc70fa341a6ef8bb0b2e912358d8602a0c7ad273b9a4 win-x64/node.exe + d275cfc4d637d2feaf4c39e1a5f5cd84f5b474fa713c15013e940c329feed13b win-x64/node.lib + fea6c0fcff45739a6e5af9843ec45455c97ff8677167bd649fd48cbef59ca52d win-x64/node_pdb.7z + bc13f5e63c1510cd41f82dc20725f40bbfa378252e09a00a8531cddabbf1b106 win-x64/node_pdb.zip + 837db0d8fb7fa194ebe23cd34ac7bedc02d1132de67cf4f147d694574be5cc4e win-x86/node.exe + a0738dec64427ae73eeb1d036081652c1c0223a679a63e0459c2af667f284f58 win-x86/node.lib + 516ac820f05eb8478be541ac12386c3b5b5c07624f73934bcf0b11a3fcdb1c95 win-x86/node_pdb.7z + 9b68f3e1f1717a2f6a090e1679f8cc627566ed064c657c35eddd0dba9484e310 win-x86/node_pdb.zip" +} + #[cfg(test)] mod tests { use super::*; + fn assert_is_darwin_arm64_targz_specs(specs: ParsedSpecs) { + assert_eq!(specs.len(), 1); + let (os, arch, ext, sha256, filename) = &specs[0]; + assert_eq!(*os, NodeJSOS::Darwin); + assert_eq!(*arch, NodeJSArch::ARM64); + assert_eq!(*ext, NodeJSPkgExt::Targz); + assert_eq!(filename, "node-v20.6.1-darwin-arm64.tar.gz"); + assert_eq!(sha256, "FAKESHA"); + } + #[test] fn it_validates_a_version_string() { let version = validate_version("20.6.1").unwrap(); @@ -269,73 +336,3 @@ mod tests { mock.assert_async().await; } } - -#[cfg(test)] -use mockito::{Mock, Server}; - -#[cfg(test)] -fn assert_is_darwin_arm64_targz_specs(specs: ParsedSpecs) { - assert_eq!(specs.len(), 1); - let (os, arch, ext, sha256, filename) = &specs[0]; - assert_eq!(*os, NodeJSOS::Darwin); - assert_eq!(*arch, NodeJSArch::ARM64); - assert_eq!(*ext, NodeJSPkgExt::Targz); - assert_eq!(filename, "node-v20.6.1-darwin-arm64.tar.gz"); - assert_eq!(sha256, "FAKESHA"); -} - -#[cfg(test)] -pub fn setup_server_mock( - version: &str, - url_fmt: &mut NodeJSURLFormatter, - server: &mut Server, -) -> Mock { - url_fmt.host = server.host_with_port(); - url_fmt.protocol = "http:".to_string(); - server.mock("GET", url_fmt.info_pathname(version).as_str()) -} - -#[cfg(test)] -pub fn get_fake_specs() -> &'static str { - "ea52b4feaf917e08cd2c729c1186585fcacef07c261a01310c91333b9e41d93c node-v20.6.1-aix-ppc64.tar.gz - 9471bd6dc491e09c31b0f831f5953284b8a6842ed4ccb98f5c62d13e6086c471 node-v20.6.1-arm64.msi - d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46 node-v20.6.1-darwin-arm64.tar.gz - 9c61b0d60fce962244d5e54549dc912e28b3c5f5e23149bfd15f66f8f7269129 node-v20.6.1-darwin-arm64.tar.xz - 365ec544c6596f194afff9a613554abfc68d4a2274181b7651386d9a11cf5862 node-v20.6.1-darwin-x64.tar.gz - 9b10c16670781e3a5af722656d28f264cdd8ebb3140f62692b33813100391349 node-v20.6.1-darwin-x64.tar.xz - d8271461ced2887f65af413949caee19db3e80d22bbefdaf01252ca998570052 node-v20.6.1-headers.tar.gz - 60963e3ee60b6739e97e0c7b8ffb25848a82649c0c277af728400c570fd9db6d node-v20.6.1-headers.tar.xz - d38fe2e41e3fe8ae81b517b4cf49521f500e181e54f4c3d05e2b2d691a57b2ca node-v20.6.1-linux-arm64.tar.gz - 6823720796b287465bb4aa8e7611143322ffd6cbdb9c6e3b149576f6d87953bf node-v20.6.1-linux-arm64.tar.xz - 459510281ea51cf5d89fc666e36fbba80793ae4b90c3a7f89dd6666c65c825b3 node-v20.6.1-linux-armv7l.tar.gz - 9dbd4fd7f804a28de91ffb8792df6e89bbb4f934fccd013624b3dabf8bf809ac node-v20.6.1-linux-armv7l.tar.xz - ca00f1aa8b2535fa167258cf5f2cfce4b79d83c442dd5e46f5e17d6a5749ec0f node-v20.6.1-linux-ppc64le.tar.gz - 27884935b025b6676e4b8737f334673ee825947d0baef61aa0326374597aeb05 node-v20.6.1-linux-ppc64le.tar.xz - 4a3f29cfc8a7ed1e9e44fcacb78e2fbaa3ce01be1efc4971a42710ad1e9e45d1 node-v20.6.1-linux-s390x.tar.gz - 3968d629989b6de16b8872b6d7ee6e6cdf1204def99c43412a6ee28203ed0022 node-v20.6.1-linux-s390x.tar.xz - 26dd13a6f7253f0ab9bcab561353985a297d927840771d905566735b792868da node-v20.6.1-linux-x64.tar.gz - 591f9f274104f266a8cf085d2c7d5d2848ba73b98ae323d501db2d4c4b7026e5 node-v20.6.1-linux-x64.tar.xz - d9acf82d9576dd0350c8e66b55f6fc2750fa9f4aa23d6453ffc58e32af995894 node-v20.6.1.pkg - 0053c09a01b1b355bca5af82927cae376124c13d74fa53567f08f4cfb085e6aa node-v20.6.1.tar.gz - 3aec5e728daa38800c343b129221d3488064a2529a39bb5467bc55be226c6a2b node-v20.6.1.tar.xz - 337549faf397deb0da3bccd4e27db45a619d89de4ea12830d16d9dfaded8e92c node-v20.6.1-win-arm64.7z - 0e62045bfc9d7c38360bd7da152c75ed82087242d5e4b401fa23a439588d36f6 node-v20.6.1-win-arm64.zip - c6cfe7824770a266a30bee8c33f485d0e89b94254c682250a239d83adfb7ce77 node-v20.6.1-win-x64.7z - 88371914f1f75d594bb367570e163cf5ecebeb514fd54cc765093819ebb0ed48 node-v20.6.1-win-x64.zip - 87d631b294a25386400d0f44d227330da62a1326e2a4fbb98bda3d7c431257f1 node-v20.6.1-win-x86.7z - 578cff623601aa8878a035f06edbf69190338ee3b345e7a096e804cb80c4ce24 node-v20.6.1-win-x86.zip - 5c2616da46728dd1326645c7db114e78ad87138a258c0724a035269258c23509 node-v20.6.1-x64.msi - cb83586af83182187e760b7e01aa7c7b2bacb521d60ceefed3ac6fc62c222449 node-v20.6.1-x86.msi - 7cc3240fd7ce7926eef1cbbad33b033f7c5d97b3f3e527d65ff1e2c3f7638a11 win-arm64/node.exe - deb027ded744371657811cfe52e774881ea928d36779924af84aa9a7a31104d2 win-arm64/node.lib - dcb6b4bc6f2a78bf0f759853b59e94ddbe9ad6b9f32d24fdcf590d74c6350bc2 win-arm64/node_pdb.7z - bdcd574e99646ec4a03bb13b3661c957f5a7ca837f5c33827075c4262d449689 win-arm64/node_pdb.zip - 5b824f3a375cca06dfd7dc70fa341a6ef8bb0b2e912358d8602a0c7ad273b9a4 win-x64/node.exe - d275cfc4d637d2feaf4c39e1a5f5cd84f5b474fa713c15013e940c329feed13b win-x64/node.lib - fea6c0fcff45739a6e5af9843ec45455c97ff8677167bd649fd48cbef59ca52d win-x64/node_pdb.7z - bc13f5e63c1510cd41f82dc20725f40bbfa378252e09a00a8531cddabbf1b106 win-x64/node_pdb.zip - 837db0d8fb7fa194ebe23cd34ac7bedc02d1132de67cf4f147d694574be5cc4e win-x86/node.exe - a0738dec64427ae73eeb1d036081652c1c0223a679a63e0459c2af667f284f58 win-x86/node.lib - 516ac820f05eb8478be541ac12386c3b5b5c07624f73934bcf0b11a3fcdb1c95 win-x86/node_pdb.7z - 9b68f3e1f1717a2f6a090e1679f8cc627566ed064c657c35eddd0dba9484e310 win-x86/node_pdb.zip" -} From 1953460e061821fb7b2fee2ff9f65c5511adf027 Mon Sep 17 00:00:00 2001 From: busticated Date: Tue, 11 Aug 2026 20:30:10 -0700 Subject: [PATCH 06/31] update to actions/checkout@v7, trigger on pull-requests, remove path filtering --- .github/workflows/ci.yaml | 7 ++----- .github/workflows/publish.yaml | 2 +- .github/workflows/test.yaml | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d7d8ee8..ffc1c15 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,10 +6,7 @@ on: - '**' tags-ignore: - '**' - paths: - - ".github/workflows/**.yml" - - "**/Cargo.toml" - - "**.rs" + pull_request: workflow_dispatch: jobs: @@ -20,7 +17,7 @@ jobs: tags: ${{ steps.info.outputs.tags }} steps: - name: Checkout Source Code - uses: actions/checkout@v3 + uses: actions/checkout@v7 with: fetch-tags: true - name: Set Info diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 1e57119..65f8fc0 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Source Code - uses: actions/checkout@v3 + uses: actions/checkout@v7 with: fetch-tags: true - name: Install Rust Toolchain (stable) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 489793a..1e5d17b 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,7 +22,7 @@ jobs: if: matrix.os == 'windows-latest' run: git config --global core.symlinks true - name: Checkout Source Code - uses: actions/checkout@v3 + uses: actions/checkout@v7 - name: Install Rust Toolchain (stable) uses: dtolnay/rust-toolchain@stable - name: Setup Project From eb6dbc67bdaed731988ada175ce24f98648b9425 Mon Sep 17 00:00:00 2001 From: busticated Date: Thu, 13 Aug 2026 10:29:24 -0700 Subject: [PATCH 07/31] set minimum supported rust version to v1.86 --- Cargo.toml | 1 + README.md | 2 +- rust-toolchain.toml | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 rust-toolchain.toml diff --git a/Cargo.toml b/Cargo.toml index bf0ffa8..17a92f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = ["xtask", "crates/*"] [workspace.package] edition = "2021" +rust-version = "1.86" license = "MIT OR Apache-2.0" authors = ["Busticated "] repository = "https://github.com/busticated/rusty" diff --git a/README.md b/README.md index 301d583..a2de822 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Rusty -[![CI Status](https://github.com/busticated/rusty/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/busticated/rusty/actions) [![Rust Version Support](https://img.shields.io/badge/rust%20version-%3E%3D1.72.1-orange)](https://releases.rs/) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/busticated/rusty/blob/master/LICENSE) +[![CI Status](https://github.com/busticated/rusty/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/busticated/rusty/actions) [![Rust Version Support](https://img.shields.io/badge/rust%20version-%3E%3D1.86-orange)](https://releases.rs/) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/busticated/rusty/blob/main/LICENSE) A `cargo` workspace ([docs](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html)) monorepo ([info](https://en.wikipedia.org/wiki/Monorepo)) hosting a collection of Rust utility crates. diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..61563f9 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,8 @@ +# see: https://rust-lang.github.io/rustup/overrides.html#the-toolchain-file +# +# pins the toolchain used for local development and CI so everyone gets the +# same `rustc` + tooling. note this is *not* the project's MSRV - that is +# declared via `rust-version` in `Cargo.toml` +[toolchain] +channel = "stable" +components = ["clippy", "llvm-tools-preview", "rustfmt"] From 123d67421a4fba8df7dbb031aa4dbdb8c098663f Mon Sep 17 00:00:00 2001 From: busticated Date: Thu, 13 Aug 2026 10:30:17 -0700 Subject: [PATCH 08/31] [detect-newline-style] set minimum supported rust version to v1.86 --- crates/detect-newline-style/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/detect-newline-style/Cargo.toml b/crates/detect-newline-style/Cargo.toml index 0a2ada5..7407d48 100644 --- a/crates/detect-newline-style/Cargo.toml +++ b/crates/detect-newline-style/Cargo.toml @@ -14,6 +14,7 @@ categories = [ "text-processing" ] edition.workspace = true +rust-version.workspace = true license.workspace = true authors.workspace = true repository.workspace = true From a57d491bb090218dc25efd78c33aa21add4be3d0 Mon Sep 17 00:00:00 2001 From: busticated Date: Thu, 13 Aug 2026 10:30:35 -0700 Subject: [PATCH 09/31] [node-js-release-info] set minimum supported rust version to v1.86 --- crates/node-js-release-info/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/node-js-release-info/Cargo.toml b/crates/node-js-release-info/Cargo.toml index fc778e8..2039927 100644 --- a/crates/node-js-release-info/Cargo.toml +++ b/crates/node-js-release-info/Cargo.toml @@ -15,6 +15,7 @@ categories = [ "web-programming::http-client" ] edition.workspace = true +rust-version.workspace = true license.workspace = true authors.workspace = true repository.workspace = true From e10cd59281aa5f2a854bc773d654f6f383cb3a45 Mon Sep 17 00:00:00 2001 From: busticated Date: Thu, 13 Aug 2026 10:33:06 -0700 Subject: [PATCH 10/31] update cargo dependencies to latest --- xtask/Cargo.toml | 6 +++--- xtask/src/toml.rs | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index fcaf193..96f116c 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -7,8 +7,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -duct = "0.13.*" -inquire = "0.6.*" +duct = "1.*" +inquire = "0.9.*" regex = "1.*" semver = "1.*" -toml_edit = "0.20.*" +toml_edit = "0.25.*" diff --git a/xtask/src/toml.rs b/xtask/src/toml.rs index 4d10e59..ef5c4e6 100644 --- a/xtask/src/toml.rs +++ b/xtask/src/toml.rs @@ -4,7 +4,7 @@ use semver::Version; use std::error::Error; use std::fs; use std::path::{Path, PathBuf}; -use toml_edit::{value as toml_value, Document}; +use toml_edit::{value as toml_value, DocumentMut}; type DynError = Box; @@ -13,7 +13,7 @@ const CARGO_TOML: &str = "Cargo.toml"; #[derive(Clone, Debug, Default)] pub struct Toml { pub path: PathBuf, - data: Document, + data: DocumentMut, } impl Toml { @@ -29,10 +29,10 @@ impl Toml { toml.load() } - pub fn read(&self) -> Result { + pub fn read(&self) -> Result { // TODO (busticated): pull into FS wrapper? let text = fs::read_to_string(&self.path)?; - Ok(text.parse::()?) + Ok(text.parse::()?) } pub fn load(&mut self) -> Result { @@ -42,7 +42,7 @@ impl Toml { pub fn create(&mut self, fs: &FS, krate: &Krate) -> Result<(), DynError> { let text = self.render(&krate.name, &krate.description); - self.data = text.parse::()?; + self.data = text.parse::()?; self.save(fs) } From 2a1d3de05643c126c17b08bd54cf0b5032326b71 Mon Sep 17 00:00:00 2001 From: busticated Date: Thu, 13 Aug 2026 10:33:35 -0700 Subject: [PATCH 11/31] [node-js-release-info] update cargo dependencies to latest --- crates/node-js-release-info/Cargo.toml | 2 +- crates/node-js-release-info/src/error.rs | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/node-js-release-info/Cargo.toml b/crates/node-js-release-info/Cargo.toml index 2039927..dbd050c 100644 --- a/crates/node-js-release-info/Cargo.toml +++ b/crates/node-js-release-info/Cargo.toml @@ -23,7 +23,7 @@ repository.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -reqwest = { version = "0.11.*" } +reqwest = { version = "0.13.*" } semver = "1.*" serde = { version = "1.*", features = ["derive"], optional = true } tokio = { version = "1.*", default-features = false, features = ["macros", "net", "time"] } diff --git a/crates/node-js-release-info/src/error.rs b/crates/node-js-release-info/src/error.rs index f77128a..924c693 100644 --- a/crates/node-js-release-info/src/error.rs +++ b/crates/node-js-release-info/src/error.rs @@ -118,15 +118,13 @@ mod tests { #[tokio::test] async fn it_prints_expected_message_upon_http_error() { - let err = fake_http_error().await.unwrap_err(); - assert_eq!( - format!("{err}"), - "builder error: relative URL without a base" - ); - } + let source = reqwest::get("not-a-url").await.unwrap_err(); + // NOTE: `HttpError` delegates to the wrapped `reqwest::Error` verbatim + // so assert on that rather than on reqwest's exact wording, which + // changes between releases + let expected = source.to_string(); + let err = NodeJSRelInfoError::from(source); - async fn fake_http_error() -> std::result::Result<(), NodeJSRelInfoError> { - let error = reqwest::get("not-a-url").await.unwrap_err(); - Err(NodeJSRelInfoError::from(error)) + assert_eq!(format!("{err}"), expected); } } From 2a5e7a4890081c58d45d11e640c053f2bb8bac51 Mon Sep 17 00:00:00 2001 From: busticated Date: Tue, 11 Aug 2026 21:02:08 -0700 Subject: [PATCH 12/31] add formatting xtask, ci verifies minimum supported rust version --- .github/workflows/test.yaml | 26 ++++++++++++++++++++++++++ README.md | 21 +++++++++++++++++++++ xtask/src/cargo.rs | 30 +++++++++++++++++++++++++++++- xtask/src/main.rs | 29 +++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1e5d17b..39beae9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -30,3 +30,29 @@ jobs: - name: Run Tests & Coverage run: cargo xtask ci + msrv: + name: Verify Minimum Supported Rust Version + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout Source Code + uses: actions/checkout@v7 + - name: Read MSRV from Cargo.toml + id: msrv + run: | + version="$(grep -m1 '^rust-version' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')" + echo ":::: MSRV: ${version}" + echo "version=${version}" >> "$GITHUB_OUTPUT" + - name: Install Rust Toolchain (MSRV) + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ steps.msrv.outputs.version }} + - name: Check Workspace Builds on MSRV + # NOTE: `rust-toolchain.toml` pins `stable`, which takes precedence over + # the toolchain installed above - `RUSTUP_TOOLCHAIN` overrides both, so + # without it this job would silently verify the wrong toolchain + env: + RUSTUP_TOOLCHAIN: ${{ steps.msrv.outputs.version }} + run: | + rustc --version + cargo check --workspace --all-features --all-targets diff --git a/README.md b/README.md index a2de822..0be56dd 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,27 @@ Run `cargo xtask help` to see any other test-related commands that are available

+
+How to format source code +

+ +To format source code across the workspace, run: + +``` +cargo xtask format +``` + +To check formatting _without_ making changes - as CI does - run: + +``` +cargo xtask format --check +``` + +Run `cargo xtask help` to see any other formatting-related commands that are available. + +

+
+
How to create docs

diff --git a/xtask/src/cargo.rs b/xtask/src/cargo.rs index e5cd171..7ef9b9f 100644 --- a/xtask/src/cargo.rs +++ b/xtask/src/cargo.rs @@ -161,6 +161,22 @@ impl<'a> Cargo<'a> { (args, Some(envs)) } + pub fn format(&self, check: bool) -> Expression { + let (args, envs) = self.format_params(check); + self.exec_safe(args, envs) + } + + fn format_params(&self, check: bool) -> (Vec, EnvVars) { + let mut arguments = vec![OsString::from("--all")]; + + if check { + arguments.push("--check".into()); + } + + let args = self.build_args([OsString::from("fmt")], arguments); + (args, None) + } + pub fn lint(&self) -> Expression { let (args, envs) = self.lint_params(); self.exec_safe(args, envs) @@ -172,7 +188,6 @@ impl<'a> Cargo<'a> { ["--all-targets", "--all-features", "--no-deps"], ); let envs = HashMap::from([("RUSTFLAGS".into(), "-Dwarnings".into())]); - (args, Some(envs)) } @@ -290,6 +305,19 @@ mod tests { assert_eq!(envs, Some(expected_envs)); } + #[test] + fn it_builds_args_for_the_format_subcommand() { + let opts = Options::new(vec![], task_flags! {}).unwrap(); + let cargo = Cargo::new(&opts); + let (args, envs) = cargo.format_params(false); + assert_eq!(args, ["fmt", "--all"]); + assert_eq!(envs, None); + + let (args, envs) = cargo.format_params(true); + assert_eq!(args, ["fmt", "--all", "--check"]); + assert_eq!(envs, None); + } + #[test] fn it_builds_args_for_the_lint_subcommand() { let opts = Options::new(vec![], task_flags! {}).unwrap(); diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 1518275..2c0e58a 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -139,6 +139,10 @@ fn init_tasks() -> Tasks { println!(":::::::::::::::::::::::::::::::::"); println!(); + tasks + .get("format") + .unwrap() + .exec(vec!["--check".into()], tasks)?; tasks .get("spellcheck") .unwrap() @@ -475,6 +479,31 @@ fn init_tasks() -> Tasks { Ok(()) }, }, + Task { + name: "format".into(), + description: "format source code (rustfmt)".into(), + flags: task_flags! { + "check" => "check formatting without making changes" + }, + run: |opts, _fs, _git, cargo, _workspace, _tasks| { + let check = opts.has("check"); + + println!("::::::::::::::::::::::::::::"); + if check { + println!(":::: Checking Formatting ::::"); + } else { + println!(":::: Formatting Project ::::"); + } + println!("::::::::::::::::::::::::::::"); + println!(); + + cargo.format(check).run()?; + + println!(":::: Done!"); + println!(); + Ok(()) + }, + }, Task { name: "lint".into(), description: "run the linter (clippy)".into(), From 58d988457ffe946ba2c6baeb4342142f30398761 Mon Sep 17 00:00:00 2001 From: busticated Date: Tue, 11 Aug 2026 21:08:05 -0700 Subject: [PATCH 13/31] fix ci os + architecture matrix --- .github/workflows/test.yaml | 13 +++++++------ xtask/src/main.rs | 6 +++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 39beae9..fbc5e8d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,12 +11,13 @@ jobs: strategy: # fail-fast: false matrix: - os: - - ubuntu-latest - - macos-latest - - windows-latest - architecture: - - x64 + include: + - os: ubuntu-latest + architecture: x64 + - os: macos-latest + architecture: arm64 + - os: windows-latest + architecture: x64 steps: - name: Git Symlink Setup for Windows if: matrix.os == 'windows-latest' diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 2c0e58a..c9241f7 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -536,10 +536,10 @@ fn init_tasks() -> Tasks { // to 'C:\Users\runneradmin\.cargo\bin\cargo.exe'" // see: https://github.com/rust-lang/rustup/issues/1367 //cmd!("rustup", "update").run()?; + // NOTE: the toolchain and its components (clippy, rustfmt, + // llvm-tools-preview) are installed by `rustup` automatically + // per `rust-toolchain.toml` cmd!("rustup", "toolchain", "list", "--verbose").run()?; - // TODO (busticated): is there a way to includes these in Cargo.toml or similar? - cmd!("rustup", "component", "add", "clippy").run()?; - cmd!("rustup", "component", "add", "llvm-tools-preview").run()?; cargo.install(["grcov"]).run()?; cargo.install(["typos-cli"]).run()?; From 13912d65d8cd4a56e545a99107aa2bbe0249341a Mon Sep 17 00:00:00 2001 From: busticated Date: Thu, 13 Aug 2026 10:34:44 -0700 Subject: [PATCH 14/31] clarify licensing --- LICENSE-APACHE | 202 +++++++++++++++++++++++++++++++++++++++++ LICENSE => LICENSE-MIT | 0 README.md | 16 +++- 3 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 LICENSE-APACHE rename LICENSE => LICENSE-MIT (100%) diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..8e78c1c --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Rusty Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE b/LICENSE-MIT similarity index 100% rename from LICENSE rename to LICENSE-MIT diff --git a/README.md b/README.md index 0be56dd..eab70b1 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Rusty -[![CI Status](https://github.com/busticated/rusty/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/busticated/rusty/actions) [![Rust Version Support](https://img.shields.io/badge/rust%20version-%3E%3D1.86-orange)](https://releases.rs/) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/busticated/rusty/blob/main/LICENSE) +[![CI Status](https://github.com/busticated/rusty/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/busticated/rusty/actions) [![Rust Version Support](https://img.shields.io/badge/rust%20version-%3E%3D1.86-orange)](https://releases.rs/) [![License](https://img.shields.io/badge/license-MIT_OR_Apache--2.0-blue.svg)](https://github.com/busticated/rusty#license) A `cargo` workspace ([docs](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html)) monorepo ([info](https://en.wikipedia.org/wiki/Monorepo)) hosting a collection of Rust utility crates. -[Installation](#installation) | [Crates](#crates) | [Development](#development) | [Docs](#docs--resources) +[Installation](#installation) | [Crates](#crates) | [Development](#development) | [Docs](#docs--resources) | [License](#license) ## Installation @@ -244,3 +244,15 @@ Any `todo!()` macros in the source code will also be reported. * [Reqwest](https://github.com/seanmonstar/reqwest) * [Mockito](https://github.com/lipanski/mockito) + +## License + +Licensed under either of + +* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or [apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0)) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or [opensource.org/licenses/MIT](https://opensource.org/licenses/MIT)) + +at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this repository by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + From 99c316316194b7d4a783b6617fdfdb9fac446ddc Mon Sep 17 00:00:00 2001 From: busticated Date: Thu, 13 Aug 2026 10:35:03 -0700 Subject: [PATCH 15/31] [detect-newline-style] clarify licensing --- crates/detect-newline-style/LICENSE-APACHE | 202 +++++++++++++++++++++ crates/detect-newline-style/LICENSE-MIT | 21 +++ 2 files changed, 223 insertions(+) create mode 100644 crates/detect-newline-style/LICENSE-APACHE create mode 100644 crates/detect-newline-style/LICENSE-MIT diff --git a/crates/detect-newline-style/LICENSE-APACHE b/crates/detect-newline-style/LICENSE-APACHE new file mode 100644 index 0000000..8e78c1c --- /dev/null +++ b/crates/detect-newline-style/LICENSE-APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Rusty Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/detect-newline-style/LICENSE-MIT b/crates/detect-newline-style/LICENSE-MIT new file mode 100644 index 0000000..67113f0 --- /dev/null +++ b/crates/detect-newline-style/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Rusty Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 14cc6b160378ce09a7ddef7914d4bbb18d57bf79 Mon Sep 17 00:00:00 2001 From: busticated Date: Thu, 13 Aug 2026 10:35:24 -0700 Subject: [PATCH 16/31] [node-js-release-info] clarify licensing --- crates/node-js-release-info/LICENSE-APACHE | 202 +++++++++++++++++++++ crates/node-js-release-info/LICENSE-MIT | 21 +++ 2 files changed, 223 insertions(+) create mode 100644 crates/node-js-release-info/LICENSE-APACHE create mode 100644 crates/node-js-release-info/LICENSE-MIT diff --git a/crates/node-js-release-info/LICENSE-APACHE b/crates/node-js-release-info/LICENSE-APACHE new file mode 100644 index 0000000..8e78c1c --- /dev/null +++ b/crates/node-js-release-info/LICENSE-APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Rusty Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/node-js-release-info/LICENSE-MIT b/crates/node-js-release-info/LICENSE-MIT new file mode 100644 index 0000000..67113f0 --- /dev/null +++ b/crates/node-js-release-info/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Rusty Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 7def01264a8db35bb0213afdc6e37ff7f02133e3 Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 12 Aug 2026 11:17:12 -0700 Subject: [PATCH 17/31] add guidance for agents --- AGENTS.md | 26 ++++++++++++++++++++++++++ CLAUDE.md | 7 +++++++ 2 files changed, 33 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4c756df --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,26 @@ +# AGENTS.md + +Instructions for AI coding agents working on this repository. **Read [README.md](README.md) first** — it covers setup, conventions, and the full command list, and takes precedence if this document conflicts with it. + +## Hard rules — require explicit approval + +- **Never** run `cargo add`. Ask; the user must say yes explicitly. Do not infer approval from a request to implement a feature that needs a dependency. +- **Never** `git commit`. Make changes locally, then prompt the user to review and commit. +- **Never** `git push`. Prompt the user, and explain why the push is needed. +- **Never** publish a crate. See "[How to publish crates](README.md#develop-publish-crate)". + +## Commands + +All local dev commands are `xtask` scripts — run `cargo xtask help` for the full list. + +- `cargo xtask test` — fast inner loop while iterating. +- `cargo xtask ci` — run before prompting the user to review. Chains `format --check`, `spellcheck`, `lint` (clippy), and `coverage` (which runs the tests). Coverage is known to be flaky, so don't reach for it mid-iteration. Report results honestly. +- `cargo xtask crate:add` — the only supported way to add a crate. Never hand-create one under `crates/`. + +## Conventions + +- **Toolchain is pinned** to Rust 1.86 (`rust-toolchain.toml`, `Cargo.toml`). CI verifies this MSRV — don't use newer language or std features. +- **Commit messages** are `[] ` (e.g. `[node-js-release-info] update docs`) and crate changes are staged separately from workspace changes. This drives automated changelogs. Workspace-level changes (including `xtask`) get no prefix. +- **Don't hand-edit** the crate list in `README.md` — it's generated between the `` / `` markers. +- **Document public interfaces** with inline rustdoc annotations. +- **TODO comments** are formatted `// TODO (): `. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6daf6cd --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,7 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +The instructions live in [AGENTS.md](AGENTS.md) — shared by every AI coding agent used here — and are imported below so they load automatically. + +@AGENTS.md From 9f92a77ff096a457b31050152338d428700a9ad8 Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 12 Aug 2026 11:44:19 -0700 Subject: [PATCH 18/31] tune workspace configuration: add cargo lockfile, update to latest stable edition and resolver, clarify formatting + linting rules --- .gitignore | 6 +- Cargo.lock | 2323 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 32 +- rustfmt.toml | 4 + xtask/Cargo.toml | 20 +- xtask/src/cargo.rs | 36 +- xtask/src/changelog.rs | 33 +- xtask/src/exec.rs | 6 +- xtask/src/fs.rs | 18 +- xtask/src/git.rs | 18 +- xtask/src/krate.rs | 26 +- xtask/src/main.rs | 32 +- xtask/src/options.rs | 15 +- xtask/src/readme.rs | 22 +- xtask/src/semver.rs | 22 +- xtask/src/tasks.rs | 18 +- xtask/src/toml.rs | 34 +- xtask/src/workspace.rs | 21 +- 18 files changed, 2543 insertions(+), 143 deletions(-) create mode 100644 Cargo.lock diff --git a/.gitignore b/.gitignore index d788f26..b2d866b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,9 @@ debug/ target/ -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock +# NOTE: `Cargo.lock` is intentionally committed. current guidance is to commit +# it for *all* projects - libraries included - so CI builds are reproducible +# see: https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..892caaa --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2323 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "aws-lc-rs" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core 0.10.1", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "detect-newline-style" +version = "0.1.2" +dependencies = [ + "regex", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "duct" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e66e9c0c03d094e1a0ba1be130b849034aa80c3a2ab8ee94316bc809f3fa684" +dependencies = [ + "libc", + "os_pipe", + "shared_child", + "shared_thread", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inquire" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756" +dependencies = [ + "bitflags", + "crossterm", + "dyn-clone", + "fuzzy-matcher", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mockito" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90820618712cab19cfc46b274c6c22546a82affcb3c3bdf0f29e3db8e1bb92c0" +dependencies = [ + "assert-json-diff", + "bytes", + "colored", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "log", + "pin-project-lite", + "rand 0.9.5", + "regex", + "serde_json", + "serde_urlencoded", + "similar", + "tokio", +] + +[[package]] +name = "node-js-release-info" +version = "1.1.1" +dependencies = [ + "mockito", + "reqwest", + "semver", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shared_thread" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b86057fcb5423f5018e331ac04623e32d6b5ce85e33300f92c79a1973928b0" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "duct", + "inquire", + "regex", + "semver", + "toml_edit", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 17a92f4..dae0380 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,42 @@ [workspace] -resolver = "2" +# resolver 3 is MSRV-aware - it will not select dependency versions that +# require a newer `rustc` than `workspace.package.rust-version` below. note +# this must be set explicitly: virtual manifests have no edition to infer from +resolver = "3" members = ["xtask", "crates/*"] [workspace.package] -edition = "2021" +edition = "2024" rust-version = "1.86" license = "MIT OR Apache-2.0" authors = ["Busticated "] repository = "https://github.com/busticated/rusty" +# declare every dependency here, then reference it from a crate via +# `.workspace = true` so versions cannot drift between crates +[workspace.dependencies] +duct = "1" +inquire = "0.9" +mockito = "1" +regex = "1" +reqwest = "0.13" +semver = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", default-features = false } +toml_edit = "0.25" + +# lints are configured once here, then inherited by each crate via +# `[lints] workspace = true` - see: https://doc.rust-lang.org/cargo/reference/manifest.html#the-lints-section +[workspace.lints.rust] +unsafe_code = "forbid" +missing_docs = "warn" +missing_debug_implementations = "warn" +unreachable_pub = "warn" + +[workspace.lints.clippy] +uninlined_format_args = "warn" + [profile.dev] # Faster builds, disable if you need debug = 0 diff --git a/rustfmt.toml b/rustfmt.toml index eda3988..d4e8a88 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,8 @@ # see: https://rust-lang.github.io/rustfmt/ # +# pin formatting rules to the 2024 style edition so output does not shift when +# the toolchain updates. this is independent of the crates' `edition` +style_edition = "2024" + # reorder_modules = false # reorder_imports = false diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 96f116c..16da364 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -2,13 +2,21 @@ name = "xtask" description = "internal-only crate used to orchestrate repo tasks" version = "0.1.0" -edition = "2021" +publish = false +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -duct = "1.*" -inquire = "0.9.*" -regex = "1.*" -semver = "1.*" -toml_edit = "0.25.*" +duct.workspace = true +inquire.workspace = true +regex.workspace = true +semver.workspace = true +toml_edit.workspace = true + +[lints] +workspace = true diff --git a/xtask/src/cargo.rs b/xtask/src/cargo.rs index 7ef9b9f..b938bd2 100644 --- a/xtask/src/cargo.rs +++ b/xtask/src/cargo.rs @@ -10,7 +10,7 @@ use std::path::PathBuf; type DynError = Box; #[derive(Clone, Debug, PartialEq)] -pub struct Cargo<'a> { +pub(crate) struct Cargo<'a> { pub bin: String, opts: &'a Options, } @@ -26,12 +26,12 @@ impl<'a> Execute for Cargo<'a> { } impl<'a> Cargo<'a> { - pub fn new(opts: &'a Options) -> Cargo<'a> { + pub(crate) fn new(opts: &'a Options) -> Cargo<'a> { let bin = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); Cargo { bin, opts } } - pub fn workspace_path(&self) -> Result { + pub(crate) fn workspace_path(&self) -> Result { let (args, envs) = self.workspace_path_params(); let stdout = self.exec_safe(args, envs).read()?; Ok(PathBuf::from(stdout.replace("Cargo.toml", "").trim())) @@ -45,7 +45,7 @@ impl<'a> Cargo<'a> { (args, None) } - pub fn create(&self, path: P, arguments: U) -> Expression + pub(crate) fn create(&self, path: P, arguments: U) -> Expression where P: Into, U: IntoIterator, @@ -65,7 +65,7 @@ impl<'a> Cargo<'a> { (args, None) } - pub fn install(&self, arguments: U) -> Expression + pub(crate) fn install(&self, arguments: U) -> Expression where U: IntoIterator, U::Item: Into, @@ -79,11 +79,17 @@ impl<'a> Cargo<'a> { U: IntoIterator, U::Item: Into, { - let args = self.build_args([OsString::from("install")], arguments); + // NOTE: `--locked` makes the tool build from its own committed + // `Cargo.lock` - without it, an unrelated upstream release can break + // `setup` on a repo where nothing changed + let args = self.build_args( + [OsString::from("install"), OsString::from("--locked")], + arguments, + ); (args, None) } - pub fn build(&self, arguments: U) -> Expression + pub(crate) fn build(&self, arguments: U) -> Expression where U: IntoIterator, U::Item: Into, @@ -101,7 +107,7 @@ impl<'a> Cargo<'a> { (args, None) } - pub fn clean(&self, arguments: U) -> Expression + pub(crate) fn clean(&self, arguments: U) -> Expression where U: IntoIterator, U::Item: Into, @@ -119,7 +125,7 @@ impl<'a> Cargo<'a> { (args, None) } - pub fn test(&self, arguments: U) -> Expression + pub(crate) fn test(&self, arguments: U) -> Expression where U: IntoIterator, U::Item: Into, @@ -137,7 +143,7 @@ impl<'a> Cargo<'a> { (args, None) } - pub fn coverage

(&self, path: P) -> Expression + pub(crate) fn coverage

(&self, path: P) -> Expression where P: Into, { @@ -161,7 +167,7 @@ impl<'a> Cargo<'a> { (args, Some(envs)) } - pub fn format(&self, check: bool) -> Expression { + pub(crate) fn format(&self, check: bool) -> Expression { let (args, envs) = self.format_params(check); self.exec_safe(args, envs) } @@ -177,7 +183,7 @@ impl<'a> Cargo<'a> { (args, None) } - pub fn lint(&self) -> Expression { + pub(crate) fn lint(&self) -> Expression { let (args, envs) = self.lint_params(); self.exec_safe(args, envs) } @@ -191,7 +197,7 @@ impl<'a> Cargo<'a> { (args, Some(envs)) } - pub fn doc(&self, arguments: U) -> Expression + pub(crate) fn doc(&self, arguments: U) -> Expression where U: IntoIterator, U::Item: Into, @@ -209,7 +215,7 @@ impl<'a> Cargo<'a> { (args, None) } - pub fn publish_package>(&self, name: N) -> Expression { + pub(crate) fn publish_package>(&self, name: N) -> Expression { let (args, envs) = self.publish_package_params(name); self.exec_unsafe(args, envs) } @@ -255,7 +261,7 @@ mod tests { let opts = Options::new(vec![], task_flags! {}).unwrap(); let cargo = Cargo::new(&opts); let (args, envs) = cargo.install_params(["grcov"]); - assert_eq!(args, ["install", "grcov"]); + assert_eq!(args, ["install", "--locked", "grcov"]); assert_eq!(envs, None); } diff --git a/xtask/src/changelog.rs b/xtask/src/changelog.rs index 3042e05..98e8323 100644 --- a/xtask/src/changelog.rs +++ b/xtask/src/changelog.rs @@ -13,50 +13,50 @@ const MARKER_START: &str = ""; const MARKER_END: &str = ""; #[derive(Clone, Debug, Default, PartialEq)] -pub struct Changelog { +pub(crate) struct Changelog { pub path: PathBuf, text: String, } impl Changelog { - pub fn new(crate_root: PathBuf) -> Self { + pub(crate) fn new(crate_root: PathBuf) -> Self { Changelog { text: String::new(), path: crate_root.join(CHANGELOG_MD), } } - pub fn from_path(crate_root: PathBuf) -> Result { + pub(crate) fn from_path(crate_root: PathBuf) -> Result { let mut changelog = Changelog::new(crate_root); changelog.load() } - pub fn read(&self) -> Result { + pub(crate) fn read(&self) -> Result { // TODO (busticated): pull into FS wrapper? Ok(fs::read_to_string(&self.path)?) } - pub fn load(&mut self) -> Result { + pub(crate) fn load(&mut self) -> Result { self.text = self.read()?; Ok(self.clone()) } - pub fn create(&mut self, fs: &FS, krate: &Krate) -> Result<(), DynError> { + pub(crate) fn create(&mut self, fs: &FS, krate: &Krate) -> Result<(), DynError> { self.text = self.render(&krate.name, &krate.version); self.save(fs) } - pub fn save(&self, fs: &FS) -> Result<(), DynError> { + pub(crate) fn save(&self, fs: &FS) -> Result<(), DynError> { Ok(fs.write(&self.path, &self.text)?) } - pub fn render>(&self, name: N, version: &Version) -> String { + pub(crate) fn render>(&self, name: N, version: &Version) -> String { let name = name.as_ref(); let lines = [ - format!("# `{}` Changelog", name), + format!("# `{name}` Changelog"), MARKER_START.to_string(), MARKER_END.to_string(), - format!("## v{}", version), + format!("## v{version}"), "".to_string(), "* Initial release 🎊🎉".to_string(), "".to_string(), @@ -64,20 +64,25 @@ impl Changelog { lines.join("\n") } - pub fn update(&mut self, fs: &FS, krate: &Krate, log: Vec) -> Result<(), DynError> { + pub(crate) fn update( + &mut self, + fs: &FS, + krate: &Krate, + log: Vec, + ) -> Result<(), DynError> { if log.is_empty() { return Ok(()); } self.load()?; - let mut changes = format!("{}\n{}\n", MARKER_START, MARKER_END); + let mut changes = format!("{MARKER_START}\n{MARKER_END}\n"); changes.push_str(format!("## v{}\n\n", krate.version).as_str()); for msg in log.iter() { if !msg.is_empty() { - changes.push_str(format!("* {}\n", msg).as_str()); + changes.push_str(format!("* {msg}\n").as_str()); } } changes.push('\n'); - let ptn = format!(r"{}[\s\S]*?{}", MARKER_START, MARKER_END); + let ptn = format!(r"{MARKER_START}[\s\S]*?{MARKER_END}"); let re = RegexBuilder::new(ptn.as_str()) .case_insensitive(true) .multi_line(true) diff --git a/xtask/src/exec.rs b/xtask/src/exec.rs index a2d571e..75b3442 100644 --- a/xtask/src/exec.rs +++ b/xtask/src/exec.rs @@ -1,11 +1,11 @@ use crate::options::Options; -use duct::{cmd, Expression}; +use duct::{Expression, cmd}; use std::collections::HashMap; use std::ffi::OsString; -pub type EnvVars = Option>; +pub(crate) type EnvVars = Option>; -pub trait Execute { +pub(crate) trait Execute { fn bin(&self) -> String; fn opts(&self) -> &Options; diff --git a/xtask/src/fs.rs b/xtask/src/fs.rs index 28f83b5..01aedf5 100644 --- a/xtask/src/fs.rs +++ b/xtask/src/fs.rs @@ -5,46 +5,46 @@ use std::path::Path; type IOResult = std::io::Result<()>; #[derive(Clone, Debug, PartialEq)] -pub struct FS<'a> { +pub(crate) struct FS<'a> { opts: &'a Options, } impl<'a> FS<'a> { - pub fn new(opts: &'a Options) -> FS<'a> { + pub(crate) fn new(opts: &'a Options) -> FS<'a> { FS { opts } } - pub fn write, D: AsRef<[u8]>>(&self, path: P, data: D) -> IOResult { + pub(crate) fn write, D: AsRef<[u8]>>(&self, path: P, data: D) -> IOResult { if self.opts.has("dry-run") { let path = path.as_ref().to_string_lossy(); - println!("Skipping: write {}", path); + println!("Skipping: write {path}"); return Ok(()); } fs::write(path, data) } - pub fn remove_dir_all>(&self, path: P) -> IOResult { + pub(crate) fn remove_dir_all>(&self, path: P) -> IOResult { if self.opts.has("dry-run") { let path = path.as_ref().to_string_lossy(); - println!("Skipping: remove_dir_all {}", path); + println!("Skipping: remove_dir_all {path}"); return Ok(()); } fs::remove_dir_all(path) } - pub fn create_dir_all>(&self, path: P) -> IOResult { + pub(crate) fn create_dir_all>(&self, path: P) -> IOResult { if self.opts.has("dry-run") { let path = path.as_ref().to_string_lossy(); - println!("Skipping: create_dir_all {}", path); + println!("Skipping: create_dir_all {path}"); return Ok(()); } fs::create_dir_all(path) } - pub fn read_dir>(&self, path: P) -> std::io::Result { + pub(crate) fn read_dir>(&self, path: P) -> std::io::Result { fs::read_dir(path) } } diff --git a/xtask/src/git.rs b/xtask/src/git.rs index 764593b..8f5c5ec 100644 --- a/xtask/src/git.rs +++ b/xtask/src/git.rs @@ -1,6 +1,6 @@ +use crate::Krate; use crate::exec::Execute; use crate::options::Options; -use crate::Krate; use duct::Expression; use std::error::Error; use std::ffi::OsString; @@ -9,7 +9,7 @@ use std::path::Path; type DynError = Box; #[derive(Clone, Debug, PartialEq)] -pub struct Git<'a> { +pub(crate) struct Git<'a> { pub bin: String, opts: &'a Options, } @@ -25,12 +25,12 @@ impl<'a> Execute for Git<'a> { } impl<'a> Git<'a> { - pub fn new(opts: &'a Options) -> Git<'a> { + pub(crate) fn new(opts: &'a Options) -> Git<'a> { let bin = "git".to_string(); Git { bin, opts } } - pub fn add(&self, path: P, arguments: U) -> Expression + pub(crate) fn add(&self, path: P, arguments: U) -> Expression where P: AsRef, U: IntoIterator, @@ -52,7 +52,7 @@ impl<'a> Git<'a> { ) } - pub fn commit(&self, message: M, arguments: U) -> Expression + pub(crate) fn commit(&self, message: M, arguments: U) -> Expression where M: AsRef, U: IntoIterator, @@ -71,7 +71,7 @@ impl<'a> Git<'a> { self.build_args(["commit", "--message", message.as_ref()], arguments) } - pub fn tag(&self, arguments: U) -> Expression + pub(crate) fn tag(&self, arguments: U) -> Expression where U: IntoIterator, U::Item: Into, @@ -88,7 +88,7 @@ impl<'a> Git<'a> { self.build_args(["tag"], arguments) } - pub fn create_tag(&self, tag: T) -> Expression + pub(crate) fn create_tag(&self, tag: T) -> Expression where T: AsRef, { @@ -103,7 +103,7 @@ impl<'a> Git<'a> { self.tag_params([tag.as_ref(), "--message", tag.as_ref()]) } - pub fn todos(&self) -> Expression { + pub(crate) fn todos(&self) -> Expression { let args = self.todos_params(); self.exec_safe(args, None) } @@ -133,7 +133,7 @@ impl<'a> Git<'a> { ) } - pub fn get_changelog(&self, krate: &Krate) -> Result, DynError> { + pub(crate) fn get_changelog(&self, krate: &Krate) -> Result, DynError> { let (prefix, args) = self.get_changelog_params(krate); let history = self.exec_safe(args, None).read()?; Ok(self.fmt_changelog(prefix, history)) diff --git a/xtask/src/krate.rs b/xtask/src/krate.rs index 5874013..55e79c7 100644 --- a/xtask/src/krate.rs +++ b/xtask/src/krate.rs @@ -16,7 +16,7 @@ const SRC_DIRNAME: &str = "src"; const LIB_FILENAME: &str = "lib.rs"; #[derive(Clone, Debug)] -pub struct Krate { +pub(crate) struct Krate { pub kind: KrateKind, pub version: Version, pub name: String, @@ -57,7 +57,7 @@ impl Default for Krate { } impl Krate { - pub fn new, V: AsRef, N: AsRef, D: AsRef>( + pub(crate) fn new, V: AsRef, N: AsRef, D: AsRef>( kind: K, version: V, name: N, @@ -83,7 +83,7 @@ impl Krate { } } - pub fn from_path(path: PathBuf) -> Result { + pub(crate) fn from_path(path: PathBuf) -> Result { let toml = Toml::from_path(path.clone())?; let readme = Readme::from_path(path.clone())?; let changelog = Changelog::from_path(path.clone())?; @@ -105,17 +105,17 @@ impl Krate { Ok(krate) } - pub fn id(&self) -> String { + pub(crate) fn id(&self) -> String { format!("{}@{}", self.name, self.version) } - pub fn set_version(&mut self, version: Version) -> Result<(), DynError> { + pub(crate) fn set_version(&mut self, version: Version) -> Result<(), DynError> { self.version = version; self.toml.set_version(&self.version)?; Ok(()) } - pub fn clean(&self, fs: &FS) -> Result<(), DynError> { + pub(crate) fn clean(&self, fs: &FS) -> Result<(), DynError> { use std::io::ErrorKind; match fs.remove_dir_all(self.tmp_path()) { @@ -125,12 +125,12 @@ impl Krate { } } - pub fn create_dirs(&self, fs: &FS) -> Result<(), DynError> { + pub(crate) fn create_dirs(&self, fs: &FS) -> Result<(), DynError> { Ok(fs.create_dir_all(self.coverage_path())?) } } -pub trait KratePaths { +pub(crate) trait KratePaths { fn path(&self) -> PathBuf; fn tmp_path(&self) -> PathBuf { @@ -143,14 +143,14 @@ pub trait KratePaths { } #[derive(Clone, Debug, Default, PartialEq)] -pub enum KrateKind { +pub(crate) enum KrateKind { #[default] Library, Binary, } impl KrateKind { - pub fn new>(kind: K) -> KrateKind { + pub(crate) fn new>(kind: K) -> KrateKind { let kind = KrateKind::from_str(kind.as_ref()); if kind.is_err() { @@ -160,7 +160,7 @@ impl KrateKind { kind.unwrap() } - pub fn from_path(path: PathBuf) -> Result { + pub(crate) fn from_path(path: PathBuf) -> Result { let path = path.join(SRC_DIRNAME).join(LIB_FILENAME); if !path.is_file() { @@ -178,7 +178,7 @@ impl Display for KrateKind { KrateKind::Library => "--lib", }; - write!(f, "{}", arch) + write!(f, "{arch}") } } @@ -189,7 +189,7 @@ impl FromStr for KrateKind { match s.to_lowercase().trim() { "binary" | "bin" | "--bin" => Ok(KrateKind::Binary), "library" | "lib" | "--lib" => Ok(KrateKind::Library), - _ => Err(format!("Unrecognized input: {}", s).into()), + _ => Err(format!("Unrecognized input: {s}").into()), } } } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index c9241f7..66523c2 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,3 +1,9 @@ +//! Internal-only task runner for this workspace - see `cargo xtask help` +//! +//! This crate is not published. Each repo task (test, lint, coverage, +//! changelog, release, ...) is declared as a [`Task`](crate::tasks::Task) in +//! [`init_tasks`] and dispatched by name from the command line. + mod cargo; mod changelog; mod exec; @@ -28,7 +34,7 @@ type DynError = Box; fn main() { if let Err(e) = try_main() { - eprintln!("{:?}", e); + eprintln!("{e:?}"); std::process::exit(-1); } } @@ -50,8 +56,8 @@ fn try_main() -> Result<(), DynError> { println!("::::::::::::::::::::::"); println!(":::: Running Task ::::"); println!("::::::::::::::::::::::"); - println!("Name: {}", cmd); - println!("Args: {:?}", args); + println!("Name: {cmd}"); + println!("Args: {args:?}"); println!(); let tasks = init_tasks(); @@ -70,7 +76,7 @@ fn print_help(cmd: String, _args: Vec, tasks: Tasks) -> Result<(), DynEr println!(); if !(cmd.is_empty() || cmd == "help" || cmd == "--help") { - let msg = format!("Unrecognized Command! Received: '{}'", cmd); + let msg = format!("Unrecognized Command! Received: '{cmd}'"); return Err(msg.into()); } @@ -97,7 +103,7 @@ fn init_tasks() -> Tasks { for tag in tags_text.lines() { let (name, version) = match tag.split_once('@') { - None => return Err(format!("Invalid tag: {}", tag).into()), + None => return Err(format!("Invalid tag: {tag}").into()), Some((n, v)) => (n.trim().to_string(), v.trim().to_string()), }; @@ -105,7 +111,7 @@ fn init_tasks() -> Tasks { } for name in tags.keys() { - let krate = krates.get(name).unwrap_or_else(|| panic!("Could Not Find Crate: `{}`!", name)); + let krate = krates.get(name).unwrap_or_else(|| panic!("Could Not Find Crate: `{name}`!")); let log = git.get_changelog(krate)?; println!(":::: {} [changes: {}]", krate.name, log.len()); @@ -118,7 +124,7 @@ fn init_tasks() -> Tasks { for l in log.iter() { - println!("* {}", l); + println!("* {l}"); } println!(); @@ -197,7 +203,7 @@ fn init_tasks() -> Tasks { println!(); let coverage_root = String::from("tmp/coverage"); - let report = format!("{}/html/index.html", coverage_root); + let report = format!("{coverage_root}/html/index.html"); tasks.get("clean").unwrap().exec(vec![], tasks)?; cargo.coverage(&coverage_root).run()?; @@ -237,7 +243,7 @@ fn init_tasks() -> Tasks { cmd!("open", &report).run()?; } - println!(":::: Report: {}", report); + println!(":::: Report: {report}"); println!(":::: Done!"); println!(); Ok(()) @@ -347,10 +353,10 @@ fn init_tasks() -> Tasks { } for tag in tags { - let (name, _ver) = tag.split_once('@').unwrap_or_else(|| panic!("Invalid Tag: `{}`!", tag)); - let krate = krates.get(name).unwrap_or_else(|| panic!("Could Not Find Crate: `{}`!", name)); + let (name, _ver) = tag.split_once('@').unwrap_or_else(|| panic!("Invalid Tag: `{tag}`!")); + let krate = krates.get(name).unwrap_or_else(|| panic!("Could Not Find Crate: `{name}`!")); let message = format!("Publishing: {} at v{}", krate.name, krate.version); - println!("{}", message); + println!("{message}"); cargo.publish_package(&krate.name).run()?; } @@ -468,7 +474,7 @@ fn init_tasks() -> Tasks { workspace.readme.update_crates_list(&fs, krates)?; - println!(":::: Updated: {:?}", readme_path); + println!(":::: Updated: {readme_path:?}"); if opts.has("open") { cmd!("open", readme_path.to_str().unwrap()).run()?; diff --git a/xtask/src/options.rs b/xtask/src/options.rs index 33f51bb..ee36de9 100644 --- a/xtask/src/options.rs +++ b/xtask/src/options.rs @@ -6,13 +6,13 @@ type DynError = Box; type TaskFlags = BTreeMap; #[derive(Clone, Debug, Default, PartialEq)] -pub struct Options { +pub(crate) struct Options { pub args: Vec, pub flags: TaskFlags, } impl Options { - pub fn new(args: Vec, flags: TaskFlags) -> Result { + pub(crate) fn new(args: Vec, flags: TaskFlags) -> Result { let re = Regex::new(r"^-*")?; let args = args .iter() @@ -21,14 +21,14 @@ impl Options { for arg in &args { if !flags.contains_key(arg) { - return Err(format!("Unrecognized argument! {}", arg).into()); + return Err(format!("Unrecognized argument! {arg}").into()); } } Ok(Options { args, flags }) } - pub fn has>(&self, flag: F) -> bool { + pub(crate) fn has>(&self, flag: F) -> bool { let flag = flag.as_ref().trim().to_lowercase(); for arg in &self.args { if arg == &flag { @@ -40,6 +40,13 @@ impl Options { } } +/// Builds the flag name / description map used when declaring a [`Task`](crate::tasks::Task) +/// +/// ```ignore +/// task_flags! { +/// "dry-run" => "run thru steps but do not publish", +/// } +/// ``` #[macro_export] macro_rules! task_flags { ($($k:expr => $v:expr),* $(,)?) => {{ diff --git a/xtask/src/readme.rs b/xtask/src/readme.rs index 8e695e3..4634988 100644 --- a/xtask/src/readme.rs +++ b/xtask/src/readme.rs @@ -11,44 +11,44 @@ type DynError = Box; const README_MD: &str = "README.md"; #[derive(Clone, Debug, Default, PartialEq)] -pub struct Readme { +pub(crate) struct Readme { pub path: PathBuf, text: String, } impl Readme { - pub fn new(crate_root: PathBuf) -> Self { + pub(crate) fn new(crate_root: PathBuf) -> Self { Readme { text: String::new(), path: crate_root.join(README_MD), } } - pub fn from_path(crate_root: PathBuf) -> Result { + pub(crate) fn from_path(crate_root: PathBuf) -> Result { let mut readme = Readme::new(crate_root); readme.load() } - pub fn read(&self) -> Result { + pub(crate) fn read(&self) -> Result { // TODO (busticated): pull into FS wrapper? Ok(fs::read_to_string(&self.path)?) } - pub fn load(&mut self) -> Result { + pub(crate) fn load(&mut self) -> Result { self.text = self.read()?; Ok(self.clone()) } - pub fn create(&mut self, fs: &FS, krate: &Krate) -> Result<(), DynError> { + pub(crate) fn create(&mut self, fs: &FS, krate: &Krate) -> Result<(), DynError> { self.text = self.render(&krate.name, &krate.description); self.save(fs) } - pub fn save(&self, fs: &FS) -> Result<(), DynError> { + pub(crate) fn save(&self, fs: &FS) -> Result<(), DynError> { Ok(fs.write(&self.path, &self.text)?) } - pub fn render, D: AsRef>(&self, name: N, description: D) -> String { + pub(crate) fn render, D: AsRef>(&self, name: N, description: D) -> String { let name = name.as_ref(); let description = description.as_ref(); let lines = vec![ @@ -69,7 +69,7 @@ impl Readme { lines.join("\n") } - pub fn update_crates_list( + pub(crate) fn update_crates_list( &mut self, fs: &FS, mut krates: BTreeMap, @@ -78,7 +78,7 @@ impl Readme { let marker_start = ""; let marker_end = ""; let mut entries = String::from(marker_start); - let ptn = format!(r"{}[\s\S]*?{}", marker_start, marker_end); + let ptn = format!(r"{marker_start}[\s\S]*?{marker_end}"); let re = RegexBuilder::new(ptn.as_str()) .case_insensitive(true) .multi_line(true) @@ -88,7 +88,7 @@ impl Readme { krate.toml.load()?; let name = krate.toml.get_name()?; let description = krate.toml.get_description()?; - let entry = format!("\n* [{}](crates/{})\n\t* {}", name, name, description); + let entry = format!("\n* [{name}](crates/{name})\n\t* {description}"); entries.push_str(&entry); } diff --git a/xtask/src/semver.rs b/xtask/src/semver.rs index 96fea8a..8f69dbe 100644 --- a/xtask/src/semver.rs +++ b/xtask/src/semver.rs @@ -2,7 +2,7 @@ use semver::{BuildMetadata, Prerelease, Version}; use std::fmt::{Display, Formatter}; #[derive(Clone, Debug, PartialEq)] -pub enum VersionChoice { +pub(crate) enum VersionChoice { Major(Version), Minor(Version), Patch(Version), @@ -11,17 +11,17 @@ pub enum VersionChoice { impl Display for VersionChoice { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { let msg = match self { - VersionChoice::Major(v) => format!("Major: {}", v), - VersionChoice::Minor(v) => format!("Minor: {}", v), - VersionChoice::Patch(v) => format!("Patch: {}", v), + VersionChoice::Major(v) => format!("Major: {v}"), + VersionChoice::Minor(v) => format!("Minor: {v}"), + VersionChoice::Patch(v) => format!("Patch: {v}"), }; - write!(f, "{}", msg) + write!(f, "{msg}") } } impl VersionChoice { - pub fn options(version: &Version) -> Vec { + pub(crate) fn options(version: &Version) -> Vec { vec![ VersionChoice::Major(increment_major(version)), VersionChoice::Minor(increment_minor(version)), @@ -29,7 +29,7 @@ impl VersionChoice { ] } - pub fn get_version(&self) -> Version { + pub(crate) fn get_version(&self) -> Version { match self { VersionChoice::Major(v) => v.clone(), VersionChoice::Minor(v) => v.clone(), @@ -38,7 +38,7 @@ impl VersionChoice { } } -pub fn increment_major(version: &Version) -> Version { +pub(crate) fn increment_major(version: &Version) -> Version { let mut v = version.clone(); v.major += 1; v.minor = 0; @@ -48,7 +48,7 @@ pub fn increment_major(version: &Version) -> Version { v } -pub fn increment_minor(version: &Version) -> Version { +pub(crate) fn increment_minor(version: &Version) -> Version { let mut v = version.clone(); v.minor += 1; v.patch = 0; @@ -57,7 +57,7 @@ pub fn increment_minor(version: &Version) -> Version { v } -pub fn increment_patch(version: &Version) -> Version { +pub(crate) fn increment_patch(version: &Version) -> Version { let mut v = version.clone(); v.patch += 1; v.pre = Prerelease::EMPTY; @@ -88,7 +88,7 @@ mod tests { #[test] fn it_displays_version_choice_text() { let choice = VersionChoice::Major(Version::new(1, 0, 0)); - assert_eq!(format!("{}", choice), "Major: 1.0.0"); + assert_eq!(format!("{choice}"), "Major: 1.0.0"); } #[test] diff --git a/xtask/src/tasks.rs b/xtask/src/tasks.rs index 16b98ea..16cabd4 100644 --- a/xtask/src/tasks.rs +++ b/xtask/src/tasks.rs @@ -17,7 +17,7 @@ type TaskRunner = fn( ) -> Result<(), DynError>; #[derive(Clone, Debug)] -pub struct Task { +pub(crate) struct Task { pub name: String, pub description: String, pub flags: BTreeMap, @@ -26,7 +26,7 @@ pub struct Task { impl Task { #[allow(dead_code)] - pub fn new, D: AsRef>( + pub(crate) fn new, D: AsRef>( name: N, description: D, flags: BTreeMap, @@ -40,7 +40,7 @@ impl Task { } } - pub fn exec(&self, args: Vec, tasks: &Tasks) -> Result<(), DynError> { + pub(crate) fn exec(&self, args: Vec, tasks: &Tasks) -> Result<(), DynError> { let opts = Options::new(args, self.flags.clone())?; let cargo = Cargo::new(&opts); let git = Git::new(&opts); @@ -52,28 +52,28 @@ impl Task { } #[derive(Clone, Debug)] -pub struct Tasks { +pub(crate) struct Tasks { map: BTreeMap, } impl Tasks { - pub fn new() -> Self { + pub(crate) fn new() -> Self { Tasks { map: BTreeMap::new(), } } - pub fn add(&mut self, tasks: Vec) { + pub(crate) fn add(&mut self, tasks: Vec) { for task in tasks.iter() { self.map.insert(task.name.clone(), task.clone()); } } - pub fn get>(&self, name: T) -> Option<&Task> { + pub(crate) fn get>(&self, name: T) -> Option<&Task> { self.map.get(name.as_ref()) } - pub fn help(&self) -> Result { + pub(crate) fn help(&self) -> Result { let separator = ".".to_string(); let mut lines = String::new(); let mut max_col_width = 0; @@ -97,7 +97,7 @@ impl Tasks { for (name, description) in task.flags.iter() { let separator = " ".to_string(); let spaces = separator.repeat(max_col_width + padding); - let line = format!("{} ⮑ --{} | {}\n", spaces, name, description); + let line = format!("{spaces} ⮑ --{name} | {description}\n"); lines.push_str(&line); } } diff --git a/xtask/src/toml.rs b/xtask/src/toml.rs index ef5c4e6..991453b 100644 --- a/xtask/src/toml.rs +++ b/xtask/src/toml.rs @@ -4,53 +4,53 @@ use semver::Version; use std::error::Error; use std::fs; use std::path::{Path, PathBuf}; -use toml_edit::{value as toml_value, DocumentMut}; +use toml_edit::{DocumentMut, value as toml_value}; type DynError = Box; const CARGO_TOML: &str = "Cargo.toml"; #[derive(Clone, Debug, Default)] -pub struct Toml { +pub(crate) struct Toml { pub path: PathBuf, data: DocumentMut, } impl Toml { - pub fn new(crate_root: PathBuf) -> Self { + pub(crate) fn new(crate_root: PathBuf) -> Self { Toml { path: crate_root.join(CARGO_TOML), ..Default::default() } } - pub fn from_path(crate_root: PathBuf) -> Result { + pub(crate) fn from_path(crate_root: PathBuf) -> Result { let mut toml = Toml::new(crate_root); toml.load() } - pub fn read(&self) -> Result { + pub(crate) fn read(&self) -> Result { // TODO (busticated): pull into FS wrapper? let text = fs::read_to_string(&self.path)?; Ok(text.parse::()?) } - pub fn load(&mut self) -> Result { + pub(crate) fn load(&mut self) -> Result { self.data = self.read()?; Ok(self.clone()) } - pub fn create(&mut self, fs: &FS, krate: &Krate) -> Result<(), DynError> { + pub(crate) fn create(&mut self, fs: &FS, krate: &Krate) -> Result<(), DynError> { let text = self.render(&krate.name, &krate.description); self.data = text.parse::()?; self.save(fs) } - pub fn save(&self, fs: &FS) -> Result<(), DynError> { + pub(crate) fn save(&self, fs: &FS) -> Result<(), DynError> { Ok(fs.write(&self.path, self.data.to_string())?) } - pub fn render, D: AsRef>(&self, name: N, description: D) -> String { + pub(crate) fn render, D: AsRef>(&self, name: N, description: D) -> String { let name = name.as_ref(); let description = description.as_ref(); let lines = vec![ @@ -59,16 +59,20 @@ impl Toml { format!("description = \"{}\"", description), "version = \"0.1.0\"".to_string(), "edition.workspace = true".to_string(), + "rust-version.workspace = true".to_string(), "license.workspace = true".to_string(), "authors.workspace = true".to_string(), "repository.workspace = true".to_string(), "".to_string(), "[dependencies]".to_string(), + "".to_string(), + "[lints]".to_string(), + "workspace = true".to_string(), ]; lines.join("\n") } - pub fn get_version(&self) -> Result { + pub(crate) fn get_version(&self) -> Result { let pkg = self .data .get("package") @@ -82,12 +86,12 @@ impl Toml { Ok(Version::parse(version)?) } - pub fn set_version(&mut self, version: &Version) -> Result<(), DynError> { + pub(crate) fn set_version(&mut self, version: &Version) -> Result<(), DynError> { self.data["package"]["version"] = toml_value(version.to_string()); Ok(()) } - pub fn get_name(&self) -> Result { + pub(crate) fn get_name(&self) -> Result { let pkg = self .data .get("package") @@ -101,7 +105,7 @@ impl Toml { Ok(name.to_string()) } - pub fn get_description(&self) -> Result { + pub(crate) fn get_description(&self) -> Result { let pkg = self .data .get("package") @@ -165,11 +169,15 @@ mod tests { "description = \"my-crate description\"", "version = \"0.1.0\"", "edition.workspace = true", + "rust-version.workspace = true", "license.workspace = true", "authors.workspace = true", "repository.workspace = true", "", "[dependencies]", + "", + "[lints]", + "workspace = true", ] .join("\n") ); diff --git a/xtask/src/workspace.rs b/xtask/src/workspace.rs index 26d6967..2e56be6 100644 --- a/xtask/src/workspace.rs +++ b/xtask/src/workspace.rs @@ -12,7 +12,7 @@ type DynError = Box; const CRATES_DIRNAME: &str = "crates"; #[derive(Clone, Debug, Default)] -pub struct Workspace { +pub(crate) struct Workspace { pub path: PathBuf, pub readme: Readme, // NOTE: unread for now, but loading it validates the workspace manifest @@ -29,25 +29,25 @@ impl KratePaths for Workspace { impl Workspace { #[allow(dead_code)] - pub fn new>(path: P) -> Self { + pub(crate) fn new>(path: P) -> Self { let path = path.as_ref().to_owned(); let readme = Readme::new(path.clone()); let toml = Toml::new(path.clone()); Workspace { path, readme, toml } } - pub fn from_path>(path: P) -> Result { + pub(crate) fn from_path>(path: P) -> Result { let path = path.as_ref().to_owned(); let readme = Readme::from_path(path.clone())?; let toml = Toml::from_path(path.clone())?; Ok(Workspace { path, readme, toml }) } - pub fn krates_path(&self) -> PathBuf { + pub(crate) fn krates_path(&self) -> PathBuf { self.path().join(CRATES_DIRNAME) } - pub fn krates(&self, fs: &FS) -> Result, DynError> { + pub(crate) fn krates(&self, fs: &FS) -> Result, DynError> { let mut krates = BTreeMap::new(); for entry in fs.read_dir(self.krates_path())? { @@ -62,7 +62,12 @@ impl Workspace { Ok(krates) } - pub fn add_krate(&self, fs: &FS, cargo: &Cargo, mut krate: Krate) -> Result { + pub(crate) fn add_krate( + &self, + fs: &FS, + cargo: &Cargo, + mut krate: Krate, + ) -> Result { let kind = krate.kind.to_string(); let args = ["--name", &krate.name, &kind]; let krate_copy = krate.clone(); // TODO (mirande): deal w/ "cannot borrow as mutable because it is also borrowed as immutable" errors @@ -73,7 +78,7 @@ impl Workspace { Ok(krate) } - pub fn clean(&self, fs: &FS, cargo: &Cargo) -> Result<(), DynError> { + pub(crate) fn clean(&self, fs: &FS, cargo: &Cargo) -> Result<(), DynError> { use std::io::ErrorKind; match fs.remove_dir_all(self.tmp_path()) { @@ -92,7 +97,7 @@ impl Workspace { Ok(()) } - pub fn create_dirs(&self, fs: &FS) -> Result<(), DynError> { + pub(crate) fn create_dirs(&self, fs: &FS) -> Result<(), DynError> { fs.create_dir_all(self.coverage_path())?; let krates = self.krates(fs)?; From 241063631c41ea49e4aac1b6f5fbc03b449e7c74 Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 12 Aug 2026 11:49:06 -0700 Subject: [PATCH 19/31] [detect-newline-style] inherit dependencies and lint settings from workspace --- crates/detect-newline-style/Cargo.toml | 5 ++++- crates/detect-newline-style/src/lib.rs | 6 ++++-- crates/detect-newline-style/tests/integration.rs | 2 ++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/detect-newline-style/Cargo.toml b/crates/detect-newline-style/Cargo.toml index 7407d48..11db05e 100644 --- a/crates/detect-newline-style/Cargo.toml +++ b/crates/detect-newline-style/Cargo.toml @@ -20,4 +20,7 @@ authors.workspace = true repository.workspace = true [dependencies] -regex = "1.*" +regex.workspace = true + +[lints] +workspace = true diff --git a/crates/detect-newline-style/src/lib.rs b/crates/detect-newline-style/src/lib.rs index 874fd4b..e4c1be0 100644 --- a/crates/detect-newline-style/src/lib.rs +++ b/crates/detect-newline-style/src/lib.rs @@ -9,6 +9,8 @@ const CR: &str = "\r"; const LF: &str = "\n"; const CRLF: &str = "\r\n"; +/// A newline style - see [`find`](LineEnding::find) to detect which style a +/// given string prefers #[derive(Clone, Debug, Default, PartialEq)] pub enum LineEnding { /// CR-style line ending (`"\r"`) rarely used, mostly on older systems @@ -166,7 +168,7 @@ impl Display for LineEnding { LineEnding::CRLF => CRLF, }; - write!(f, "{}", eol) + write!(f, "{eol}") } } @@ -178,7 +180,7 @@ impl FromStr for LineEnding { CR => Ok(LineEnding::CR), LF => Ok(LineEnding::LF), CRLF => Ok(LineEnding::CRLF), - _ => Err(format!("Unrecognized input: {}", s).into()), + _ => Err(format!("Unrecognized input: {s}").into()), } } } diff --git a/crates/detect-newline-style/tests/integration.rs b/crates/detect-newline-style/tests/integration.rs index d5b7ad4..21a34d0 100644 --- a/crates/detect-newline-style/tests/integration.rs +++ b/crates/detect-newline-style/tests/integration.rs @@ -1,3 +1,5 @@ +//! Integration tests exercising the crate's public API as a consumer would + use detect_newline_style::*; #[test] From 52915badaa5676037fa1d7ff0dacb82df0fc8dd9 Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 12 Aug 2026 11:51:00 -0700 Subject: [PATCH 20/31] [node-js-release-info] inherit dependencies and lint settings from workspace --- crates/node-js-release-info/Cargo.toml | 17 +++++++------ crates/node-js-release-info/src/arch.rs | 25 ++++++++++++++++++- crates/node-js-release-info/src/error.rs | 17 +++++++------ crates/node-js-release-info/src/ext.rs | 16 +++++++++++- crates/node-js-release-info/src/lib.rs | 7 ++++++ crates/node-js-release-info/src/os.rs | 22 +++++++++++++++- crates/node-js-release-info/src/specs.rs | 14 +++++------ crates/node-js-release-info/src/url.rs | 16 +++++++----- .../node-js-release-info/tests/integration.rs | 4 +++ 9 files changed, 107 insertions(+), 31 deletions(-) diff --git a/crates/node-js-release-info/Cargo.toml b/crates/node-js-release-info/Cargo.toml index dbd050c..c6bdde0 100644 --- a/crates/node-js-release-info/Cargo.toml +++ b/crates/node-js-release-info/Cargo.toml @@ -23,16 +23,19 @@ repository.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -reqwest = { version = "0.13.*" } -semver = "1.*" -serde = { version = "1.*", features = ["derive"], optional = true } -tokio = { version = "1.*", default-features = false, features = ["macros", "net", "time"] } +reqwest.workspace = true +semver.workspace = true +serde = { workspace = true, optional = true } +tokio = { workspace = true, features = ["macros", "net", "time"] } [dev-dependencies] -mockito = "1.*" -serde_json = "1.*" +mockito.workspace = true +serde_json.workspace = true # doc examples use `#[tokio::main]`, which requires the multi-threaded runtime -tokio = { version = "1.*", default-features = false, features = ["rt-multi-thread"] } +tokio = { workspace = true, features = ["rt-multi-thread"] } [features] json = ["dep:serde"] + +[lints] +workspace = true diff --git a/crates/node-js-release-info/src/arch.rs b/crates/node-js-release-info/src/arch.rs index 28fce75..9075161 100644 --- a/crates/node-js-release-info/src/arch.rs +++ b/crates/node-js-release-info/src/arch.rs @@ -7,19 +7,27 @@ use std::str::FromStr; #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] +/// The CPU architecture a Node.js distributable targets pub enum NodeJSArch { + /// 64-bit x86 (`x64`) #[cfg_attr(feature = "json", serde(rename = "x64"))] X64, + /// 32-bit x86 (`x86`) #[cfg_attr(feature = "json", serde(rename = "x86"))] X86, + /// 64-bit ARM (`arm64`) #[cfg_attr(feature = "json", serde(rename = "arm64"))] ARM64, + /// 32-bit ARMv7 with hardware floating point (`armv7l`) #[cfg_attr(feature = "json", serde(rename = "armv7l"))] ARMV7L, + /// 64-bit PowerPC, big-endian (`ppc64`) #[cfg_attr(feature = "json", serde(rename = "ppc64"))] PPC64, + /// 64-bit PowerPC, little-endian (`ppc64le`) #[cfg_attr(feature = "json", serde(rename = "ppc64le"))] PPC64LE, + /// 64-bit IBM Z (`s390x`) #[cfg_attr(feature = "json", serde(rename = "s390x"))] S390X, } @@ -31,10 +39,25 @@ impl Default for NodeJSArch { } impl NodeJSArch { + /// Creates a new instance using the default architecture ([`X64`](NodeJSArch::X64)) + /// + /// # Examples + /// + /// ```rust + /// use node_js_release_info::NodeJSArch; + /// assert_eq!(NodeJSArch::new(), NodeJSArch::X64); + /// ``` pub fn new() -> NodeJSArch { NodeJSArch::X64 } + /// Determines the architecture of the current environment via + /// [`std::env::consts::ARCH`] + /// + /// # Errors + /// + /// Returns [`NodeJSRelInfoError::UnrecognizedArch`] when the current + /// architecture has no corresponding Node.js distributable pub fn from_env() -> Result { NodeJSArch::from_str(ARCH) } @@ -52,7 +75,7 @@ impl Display for NodeJSArch { NodeJSArch::S390X => "s390x", }; - write!(f, "{}", arch) + write!(f, "{arch}") } } diff --git a/crates/node-js-release-info/src/error.rs b/crates/node-js-release-info/src/error.rs index 924c693..bd556b0 100644 --- a/crates/node-js-release-info/src/error.rs +++ b/crates/node-js-release-info/src/error.rs @@ -1,6 +1,7 @@ use std::error::Error; use std::fmt::{Display, Formatter, Result}; +/// The error type returned by all fallible operations in this crate #[derive(Debug)] pub enum NodeJSRelInfoError { /// The operating system for the Node.js distributable you are targeting is @@ -28,27 +29,27 @@ impl Display for NodeJSRelInfoError { fn fmt(&self, f: &mut Formatter<'_>) -> Result { let message = match self { NodeJSRelInfoError::UnrecognizedOs(input) => { - format!("Unrecognized OS! Received: '{}'", input) + format!("Unrecognized OS! Received: '{input}'") } NodeJSRelInfoError::UnrecognizedArch(input) => { - format!("Unrecognized Arch! Received: '{}'", input) + format!("Unrecognized Arch! Received: '{input}'") } NodeJSRelInfoError::UnrecognizedExt(input) => { - format!("Unrecognized File Extension! Received: '{}'", input) + format!("Unrecognized File Extension! Received: '{input}'") } NodeJSRelInfoError::InvalidVersion(input) => { - format!("Invalid Version! Received: '{}'", input) + format!("Invalid Version! Received: '{input}'") } NodeJSRelInfoError::UnrecognizedVersion(input) => { - format!("Unrecognized Version! Received: '{}'", input) + format!("Unrecognized Version! Received: '{input}'") } NodeJSRelInfoError::UnrecognizedConfiguration(input) => { - format!("Unrecognized Configuration! Received: '{}'", input) + format!("Unrecognized Configuration! Received: '{input}'") } - NodeJSRelInfoError::HttpError(e) => return write!(f, "{}", e), + NodeJSRelInfoError::HttpError(e) => return write!(f, "{e}"), }; - write!(f, "Error: {}", message) + write!(f, "Error: {message}") } } diff --git a/crates/node-js-release-info/src/ext.rs b/crates/node-js-release-info/src/ext.rs index 353c0ea..cd5de6c 100644 --- a/crates/node-js-release-info/src/ext.rs +++ b/crates/node-js-release-info/src/ext.rs @@ -6,15 +6,21 @@ use std::str::FromStr; #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] +/// The file extension of a Node.js distributable pub enum NodeJSPkgExt { + /// gzip-compressed tarball (`tar.gz`) #[cfg_attr(feature = "json", serde(rename = "tar.gz"))] Targz, + /// xz-compressed tarball (`tar.xz`) #[cfg_attr(feature = "json", serde(rename = "tar.xz"))] Tarxz, + /// zip archive (`zip`) - Windows only #[cfg_attr(feature = "json", serde(rename = "zip"))] Zip, + /// Windows installer package (`msi`) #[cfg_attr(feature = "json", serde(rename = "msi"))] Msi, + /// 7-Zip archive (`7z`) - Windows only #[cfg_attr(feature = "json", serde(rename = "7z"))] S7z, // can't start w/ a number (X_x) } @@ -26,6 +32,14 @@ impl Default for NodeJSPkgExt { } impl NodeJSPkgExt { + /// Creates a new instance using the default extension ([`Targz`](NodeJSPkgExt::Targz)) + /// + /// # Examples + /// + /// ```rust + /// use node_js_release_info::NodeJSPkgExt; + /// assert_eq!(NodeJSPkgExt::new(), NodeJSPkgExt::Targz); + /// ``` pub fn new() -> NodeJSPkgExt { NodeJSPkgExt::Targz } @@ -40,7 +54,7 @@ impl Display for NodeJSPkgExt { NodeJSPkgExt::S7z => "7z", }; - write!(f, "{}", arch) + write!(f, "{arch}") } } diff --git a/crates/node-js-release-info/src/lib.rs b/crates/node-js-release-info/src/lib.rs index 61e02dc..db5ea5b 100644 --- a/crates/node-js-release-info/src/lib.rs +++ b/crates/node-js-release-info/src/lib.rs @@ -16,6 +16,13 @@ use crate::url::NodeJSURLFormatter; use serde::{Deserialize, Serialize}; use std::string::ToString; +/// Metadata describing a single Node.js distributable +/// +/// Build one with [`new`](NodeJSRelInfo::new) or +/// [`from_env`](NodeJSRelInfo::from_env), narrow it with the builder methods +/// (e.g. [`macos`](NodeJSRelInfo::macos), [`arm64`](NodeJSRelInfo::arm64)), +/// then call [`fetch`](NodeJSRelInfo::fetch) to populate `filename`, `sha256` +/// and `url` from the downloads server #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] pub struct NodeJSRelInfo { diff --git a/crates/node-js-release-info/src/os.rs b/crates/node-js-release-info/src/os.rs index b106bbd..f9a85b1 100644 --- a/crates/node-js-release-info/src/os.rs +++ b/crates/node-js-release-info/src/os.rs @@ -7,13 +7,18 @@ use std::str::FromStr; #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] +/// The operating system a Node.js distributable targets pub enum NodeJSOS { + /// Linux (`linux`) #[cfg_attr(feature = "json", serde(rename = "linux"))] Linux, + /// macOS (`darwin`) #[cfg_attr(feature = "json", serde(rename = "darwin"))] Darwin, + /// Windows (`win`) #[cfg_attr(feature = "json", serde(rename = "win"))] Windows, + /// IBM AIX (`aix`) #[cfg_attr(feature = "json", serde(rename = "aix"))] AIX, } @@ -25,10 +30,25 @@ impl Default for NodeJSOS { } impl NodeJSOS { + /// Creates a new instance using the default OS ([`Linux`](NodeJSOS::Linux)) + /// + /// # Examples + /// + /// ```rust + /// use node_js_release_info::NodeJSOS; + /// assert_eq!(NodeJSOS::new(), NodeJSOS::Linux); + /// ``` pub fn new() -> NodeJSOS { NodeJSOS::Linux } + /// Determines the OS of the current environment via + /// [`std::env::consts::OS`] + /// + /// # Errors + /// + /// Returns [`NodeJSRelInfoError::UnrecognizedOs`] when the current OS has + /// no corresponding Node.js distributable pub fn from_env() -> Result { NodeJSOS::from_str(OS) } @@ -43,7 +63,7 @@ impl Display for NodeJSOS { NodeJSOS::AIX => "aix", }; - write!(f, "{}", os) + write!(f, "{os}") } } diff --git a/crates/node-js-release-info/src/specs.rs b/crates/node-js-release-info/src/specs.rs index 7fb6276..0fca5f3 100644 --- a/crates/node-js-release-info/src/specs.rs +++ b/crates/node-js-release-info/src/specs.rs @@ -6,7 +6,7 @@ use crate::url::NodeJSURLFormatter; use semver::Version; use std::str::FromStr; -pub fn validate_version>(semver: T) -> Result { +pub(crate) fn validate_version>(semver: T) -> Result { match Version::parse(semver.as_ref()) { Ok(v) => Ok(v.to_string()), Err(_) => Err(NodeJSRelInfoError::InvalidVersion( @@ -15,7 +15,7 @@ pub fn validate_version>(semver: T) -> Result Result { @@ -36,9 +36,9 @@ pub async fn fetch( } } -pub type ParsedSpecs = Vec<(NodeJSOS, NodeJSArch, NodeJSPkgExt, String, String)>; +pub(crate) type ParsedSpecs = Vec<(NodeJSOS, NodeJSArch, NodeJSPkgExt, String, String)>; -pub fn parse(version: &String, specs: String) -> Option { +pub(crate) fn parse(version: &String, specs: String) -> Option { let mut all: ParsedSpecs = vec![]; for line in specs.lines() { let (sha256, filename) = match line.trim().split_once(' ') { @@ -50,7 +50,7 @@ pub fn parse(version: &String, specs: String) -> Option { continue; } - if !filename.starts_with(format!("node-v{}", version).as_str()) { + if !filename.starts_with(format!("node-v{version}").as_str()) { continue; } @@ -107,7 +107,7 @@ pub fn parse(version: &String, specs: String) -> Option { use mockito::{Mock, Server}; #[cfg(test)] -pub fn setup_server_mock( +pub(crate) fn setup_server_mock( version: &str, url_fmt: &mut NodeJSURLFormatter, server: &mut Server, @@ -118,7 +118,7 @@ pub fn setup_server_mock( } #[cfg(test)] -pub fn get_fake_specs() -> &'static str { +pub(crate) fn get_fake_specs() -> &'static str { "ea52b4feaf917e08cd2c729c1186585fcacef07c261a01310c91333b9e41d93c node-v20.6.1-aix-ppc64.tar.gz 9471bd6dc491e09c31b0f831f5953284b8a6842ed4ccb98f5c62d13e6086c471 node-v20.6.1-arm64.msi d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46 node-v20.6.1-darwin-arm64.tar.gz diff --git a/crates/node-js-release-info/src/url.rs b/crates/node-js-release-info/src/url.rs index 7f5281d..6292b99 100644 --- a/crates/node-js-release-info/src/url.rs +++ b/crates/node-js-release-info/src/url.rs @@ -1,5 +1,5 @@ #[derive(Clone, Debug, PartialEq)] -pub struct NodeJSURLFormatter { +pub(crate) struct NodeJSURLFormatter { pub protocol: String, pub host: String, pub pathname: String, @@ -12,7 +12,7 @@ impl Default for NodeJSURLFormatter { } impl NodeJSURLFormatter { - pub fn new() -> NodeJSURLFormatter { + pub(crate) fn new() -> NodeJSURLFormatter { NodeJSURLFormatter { protocol: String::from("https:"), host: String::from("nodejs.org"), @@ -20,7 +20,7 @@ impl NodeJSURLFormatter { } } - pub fn info>(&self, version: V) -> String { + pub(crate) fn info>(&self, version: V) -> String { format!( "{}//{}{}", self.protocol, @@ -29,7 +29,7 @@ impl NodeJSURLFormatter { ) } - pub fn info_pathname>(&self, version: V) -> String { + pub(crate) fn info_pathname>(&self, version: V) -> String { format!( "{}/v{}/SHASUMS256.txt", self.pathname, @@ -37,7 +37,7 @@ impl NodeJSURLFormatter { ) } - pub fn pkg, F: AsRef>(&self, version: V, filename: F) -> String { + pub(crate) fn pkg, F: AsRef>(&self, version: V, filename: F) -> String { format!( "{}//{}{}", self.protocol, @@ -46,7 +46,11 @@ impl NodeJSURLFormatter { ) } - pub fn pkg_pathname, F: AsRef>(&self, version: V, filename: F) -> String { + pub(crate) fn pkg_pathname, F: AsRef>( + &self, + version: V, + filename: F, + ) -> String { format!( "{}/v{}/{}", self.pathname, diff --git a/crates/node-js-release-info/tests/integration.rs b/crates/node-js-release-info/tests/integration.rs index f99da0d..11530b9 100644 --- a/crates/node-js-release-info/tests/integration.rs +++ b/crates/node-js-release-info/tests/integration.rs @@ -1,3 +1,7 @@ +//! Integration tests exercising the crate's public API as a consumer would +//! +//! NOTE: these hit the live Node.js downloads server + use node_js_release_info::*; const VERSION: &str = "20.7.0"; From 052065e8480a5e53efd6d4a2ccd223d6f53ace56 Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 12 Aug 2026 12:44:29 -0700 Subject: [PATCH 21/31] expand ci checks: move to cargo-llvm-cov coverage, add dependency + semver audits --- .github/dependabot.yml | 25 ++++++++ .github/workflows/test.yaml | 36 +++++++++++ AGENTS.md | 5 +- README.md | 23 +++++++ deny.toml | 41 +++++++++++++ xtask/src/cargo.rs | 114 +++++++++++++++++++++++++---------- xtask/src/krate.rs | 6 +- xtask/src/main.rs | 116 ++++++++++++++++++++++++++---------- xtask/src/options.rs | 6 +- xtask/src/workspace.rs | 4 ++ 10 files changed, 307 insertions(+), 69 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 deny.toml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..7615fec --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,25 @@ +# see: https://docs.github.com/code-security/dependabot/dependabot-version-updates +version: 2 + +updates: + - package-ecosystem: cargo + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + # workspace crates share dependencies - one PR per batch keeps CI runs and + # changelog noise down. commit messages match this repo's convention: + # workspace-level changes carry no crate-name prefix + groups: + cargo-dependencies: + patterns: + - "*" + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + github-actions: + patterns: + - "*" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fbc5e8d..71d942a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -31,6 +31,42 @@ jobs: - name: Run Tests & Coverage run: cargo xtask ci + deny: + name: Audit Dependencies + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout Source Code + uses: actions/checkout@v7 + - name: Install Rust Toolchain (stable) + uses: dtolnay/rust-toolchain@stable + - name: Install cargo-deny + uses: taiki-e/install-action@v2 + with: + tool: cargo-deny + - name: Check Advisories, Licenses & Bans + run: cargo deny --all-features check + + semver: + name: Check for Breaking API Changes + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout Source Code + uses: actions/checkout@v7 + - name: Install Rust Toolchain (stable) + uses: dtolnay/rust-toolchain@stable + - name: Install cargo-semver-checks + uses: taiki-e/install-action@v2 + with: + tool: cargo-semver-checks + # NOTE: baselines come from the last published version of each crate on + # crates.io. crates marked `publish = false` are skipped automatically. + # this only reports - bumping the version is still a deliberate step via + # `cargo xtask crate:release` + - name: Compare Public API Against Published Baseline + run: cargo semver-checks --workspace + msrv: name: Verify Minimum Supported Rust Version runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index 4c756df..22498d6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,10 @@ Instructions for AI coding agents working on this repository. **Read [README.md] All local dev commands are `xtask` scripts — run `cargo xtask help` for the full list. - `cargo xtask test` — fast inner loop while iterating. -- `cargo xtask ci` — run before prompting the user to review. Chains `format --check`, `spellcheck`, `lint` (clippy), and `coverage` (which runs the tests). Coverage is known to be flaky, so don't reach for it mid-iteration. Report results honestly. +- `cargo xtask ci` — run before prompting the user to review. Chains `format --check`, `spellcheck`, `lint` (clippy), `doc --check` (doc tests + rustdoc warnings), and `coverage` (which runs the tests). Report results honestly. +- `cargo xtask audit` and `cargo xtask semver` — dependency audit and public-API breakage check. Both need network access and run as their own CI jobs, so they are **not** part of `cargo xtask ci`. + +Task naming follows one rule: `--check` is a non-mutating mode of a task that otherwise writes (`format`, `doc`); `name:sub` is a family of distinct operations on one noun (`crate:add`, `crate:list`, ...); everything else is a bare verb. - `cargo xtask crate:add` — the only supported way to add a crate. Never hand-create one under `crates/`. ## Conventions diff --git a/README.md b/README.md index eab70b1..efc8b41 100644 --- a/README.md +++ b/README.md @@ -107,11 +107,34 @@ To see code coverage stats for _all_ crates: cargo xtask coverage --open ``` +Coverage is collected with [cargo-llvm-cov](https://github.com/taiki-e/cargo-llvm-cov) and written to `tmp/coverage` as both an html report and `lcov.info`. The `xtask` crate and `tests/` directories are excluded. Note `cargo-llvm-cov` does _not_ run doc tests - those run via `cargo xtask doc --check`. + Run `cargo xtask help` to see any other coverage-related commands that are available.

+
+How to audit dependencies +

+ +To check dependencies for security advisories, disallowed licenses, and untrusted sources, run: + +``` +cargo xtask audit +``` + +This runs [cargo-deny](https://github.com/EmbarkStudios/cargo-deny) - policy lives in [deny.toml](deny.toml). To check whether your changes break the public API of an already-published crate, run: + +``` +cargo xtask semver +``` + +Both commands need network access - the first to fetch the advisory database, the second to fetch each crate's last published version as a baseline. For that reason neither is part of `cargo xtask ci`; they run as their own CI jobs instead. + +

+
+
How to check for spelling errors

diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..3de9222 --- /dev/null +++ b/deny.toml @@ -0,0 +1,41 @@ +# see: https://embarkstudios.github.io/cargo-deny/ +# +# run via `cargo xtask deny` + +[graph] +all-features = true + +[advisories] +# fail on any crate with a known security advisory or that is unmaintained +version = 2 +yanked = "deny" + +[licenses] +version = 2 +# this workspace publishes under `MIT OR Apache-2.0` - only accept dependency +# licenses that are compatible with distributing under those terms +allow = [ + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-3-Clause", + # covers the Mozilla CA root certificate *data* bundled by + # `webpki-root-certs` (via `reqwest` -> `rustls`), not code. permissive, + # no copyleft - see: https://cdla.dev/permissive-2-0/ + "CDLA-Permissive-2.0", + "ISC", + "MIT", + "MIT-0", + "Unicode-3.0", +] +confidence-threshold = 0.93 + +[bans] +# duplicate versions of the same crate bloat build times - warn so they stay +# visible without blocking CI on a transitive dependency we do not control +multiple-versions = "warn" +wildcards = "deny" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] diff --git a/xtask/src/cargo.rs b/xtask/src/cargo.rs index b938bd2..074d686 100644 --- a/xtask/src/cargo.rs +++ b/xtask/src/cargo.rs @@ -143,28 +143,53 @@ impl<'a> Cargo<'a> { (args, None) } - pub(crate) fn coverage

(&self, path: P) -> Expression + pub(crate) fn coverage_clean(&self) -> Expression { + let (args, envs) = self.coverage_clean_params(); + self.exec_unsafe(args, envs) + } + + fn coverage_clean_params(&self) -> (Vec, EnvVars) { + // NOTE: profiling data accumulates across runs - without this, stale + // data from a previous run is folded into the report + let args = self.build_args([OsString::from("llvm-cov")], ["clean", "--workspace"]); + (args, None) + } + + pub(crate) fn coverage(&self) -> Expression { + let (args, envs) = self.coverage_params(); + self.exec_unsafe(args, envs) + } + + fn coverage_params(&self) -> (Vec, EnvVars) { + // NOTE: `--no-report` runs the tests and collects profiling data but + // renders nothing - `coverage_report()` below turns that single + // collection run into both html and lcov output + let args = self.build_args( + [OsString::from("llvm-cov")], + ["--workspace", "--all-features", "--no-report"], + ); + (args, None) + } + + pub(crate) fn coverage_report(&self, arguments: U) -> Expression where - P: Into, + U: IntoIterator, + U::Item: Into, { - let (args, envs) = self.coverage_params(path); + let (args, envs) = self.coverage_report_params(arguments); self.exec_unsafe(args, envs) } - fn coverage_params

(&self, path: P) -> (Vec, EnvVars) + fn coverage_report_params(&self, arguments: U) -> (Vec, EnvVars) where - P: Into, + U: IntoIterator, + U::Item: Into, { - let mut profile_ptn: OsString = path.into(); - profile_ptn.push("/cargo-test-%p-%m.profraw"); - let args = self.build_args([OsString::from("test")], ["--all-features"]); - let envs = HashMap::from([ - ("CARGO_INCREMENTAL".into(), "0".into()), - ("RUSTFLAGS".into(), "-Cinstrument-coverage".into()), - ("LLVM_PROFILE_FILE".into(), profile_ptn), - ]); - - (args, Some(envs)) + let args = self.build_args( + [OsString::from("llvm-cov"), OsString::from("report")], + arguments, + ); + (args, None) } pub(crate) fn format(&self, check: bool) -> Expression { @@ -212,7 +237,10 @@ impl<'a> Cargo<'a> { U::Item: Into, { let args = self.build_args([OsString::from("doc")], arguments); - (args, None) + // NOTE: rustdoc only *warns* on problems like broken intra-doc links, + // so they slip by unnoticed - this promotes them to build failures + let envs = HashMap::from([("RUSTDOCFLAGS".into(), "-Dwarnings".into())]); + (args, Some(envs)) } pub(crate) fn publish_package>(&self, name: N) -> Expression { @@ -296,19 +324,43 @@ mod tests { fn it_builds_args_for_the_coverage_subcommand() { let opts = Options::new(vec![], task_flags! {}).unwrap(); let cargo = Cargo::new(&opts); - let path = PathBuf::from("fake-coverage-path"); - let (args, envs) = cargo.coverage_params(path); - let expected_envs = HashMap::from([ - ("CARGO_INCREMENTAL".into(), "0".into()), - ("RUSTFLAGS".into(), "-Cinstrument-coverage".into()), - ( - "LLVM_PROFILE_FILE".into(), - "fake-coverage-path/cargo-test-%p-%m.profraw".into(), - ), - ]); - - assert_eq!(args, ["test", "--all-features"]); - assert_eq!(envs, Some(expected_envs)); + let (args, envs) = cargo.coverage_params(); + + assert_eq!( + args, + ["llvm-cov", "--workspace", "--all-features", "--no-report"] + ); + assert_eq!(envs, None); + } + + #[test] + fn it_builds_args_for_the_coverage_clean_subcommand() { + let opts = Options::new(vec![], task_flags! {}).unwrap(); + let cargo = Cargo::new(&opts); + let (args, envs) = cargo.coverage_clean_params(); + + assert_eq!(args, ["llvm-cov", "clean", "--workspace"]); + assert_eq!(envs, None); + } + + #[test] + fn it_builds_args_for_the_coverage_report_subcommand() { + let opts = Options::new(vec![], task_flags! {}).unwrap(); + let cargo = Cargo::new(&opts); + let (args, envs) = + cargo.coverage_report_params(["--html", "--output-dir", "fake-coverage-path"]); + + assert_eq!( + args, + [ + "llvm-cov", + "report", + "--html", + "--output-dir", + "fake-coverage-path" + ] + ); + assert_eq!(envs, None); } #[test] @@ -342,8 +394,10 @@ mod tests { let opts = Options::new(vec![], task_flags! {}).unwrap(); let cargo = Cargo::new(&opts); let (args, envs) = cargo.doc_params(["--workspace", "--no-deps"]); + let expected_envs = HashMap::from([("RUSTDOCFLAGS".into(), "-Dwarnings".into())]); + assert_eq!(args, ["doc", "--workspace", "--no-deps"]); - assert_eq!(envs, None); + assert_eq!(envs, Some(expected_envs)); } #[test] diff --git a/xtask/src/krate.rs b/xtask/src/krate.rs index 55e79c7..6b2b2f0 100644 --- a/xtask/src/krate.rs +++ b/xtask/src/krate.rs @@ -252,11 +252,9 @@ mod tests { } #[test] - #[should_panic( - expected = "called `Result::unwrap()` on an `Err` value: \"Unrecognized input: NOPE!\"" - )] fn it_fails_to_initialize_when_krate_kind_cannot_be_determined_from_str() { - KrateKind::from_str("NOPE!").unwrap(); + let err = KrateKind::from_str("NOPE!").unwrap_err(); + assert_eq!(err.to_string(), "Unrecognized input: NOPE!"); } #[test] diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 66523c2..a3cf1ea 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,7 +1,7 @@ //! Internal-only task runner for this workspace - see `cargo xtask help` //! //! This crate is not published. Each repo task (test, lint, coverage, -//! changelog, release, ...) is declared as a [`Task`](crate::tasks::Task) in +//! changelog, release, ...) is declared as a [`Task`] in //! [`init_tasks`] and dispatched by name from the command line. mod cargo; @@ -157,6 +157,10 @@ fn init_tasks() -> Tasks { .get("lint") .unwrap() .exec(vec![], tasks)?; + tasks + .get("doc") + .unwrap() + .exec(vec!["--check".into()], tasks)?; tasks .get("coverage") .unwrap() @@ -186,11 +190,6 @@ fn init_tasks() -> Tasks { }, }, Task { - // TODO (busticated): oof. coverage is a bit h0rked atm - see: - // https://github.com/mozilla/grcov/issues/1103 - // https://github.com/mozilla/grcov/issues/556 - // https://github.com/mozilla/grcov/issues/802 - // https://github.com/mozilla/grcov/issues/1042 name: "coverage".into(), description: "run tests and generate html code coverage report".into(), flags: task_flags! { @@ -204,9 +203,15 @@ fn init_tasks() -> Tasks { let coverage_root = String::from("tmp/coverage"); let report = format!("{coverage_root}/html/index.html"); + let lcov = format!("{coverage_root}/lcov.info"); + // NOTE: `xtask` is tooling and `tests/` holds the tests + // themselves - neither belongs in the crates' coverage numbers + let ignore = r"(^|/)(xtask|tests)/"; + // NOTE: `clean` wipes stale profiling data, which would + // otherwise be folded into this run's report tasks.get("clean").unwrap().exec(vec![], tasks)?; - cargo.coverage(&coverage_root).run()?; + cargo.coverage().run()?; println!(":::: Done!"); println!(); @@ -215,28 +220,22 @@ fn init_tasks() -> Tasks { println!(":::::::::::::::::::::::::::"); println!(); - cmd!( - "grcov", - ".", - "--binary-path", - "./target/debug/deps", - "--source-dir", - ".", - "--output-types", - "html,lcov", - "--branch", - "--ignore-not-existing", - "--ignore", - "../*", - "--ignore", - "/*", - "--ignore", - "xtask/*", - "--ignore", - "*/tests/*", - "--output-path", + cargo.coverage_report([ + "--html", + "--output-dir", &coverage_root, - ) + "--ignore-filename-regex", + ignore, + ]) + .run()?; + + cargo.coverage_report([ + "--lcov", + "--output-path", + &lcov, + "--ignore-filename-regex", + ignore, + ]) .run()?; if opts.has("open"){ @@ -442,17 +441,26 @@ fn init_tasks() -> Tasks { name: "doc".into(), description: "build project documentation".into(), flags: task_flags! { + "check" => "test examples and render docs without updating the README", "dry-run" => "run thru steps but do not generate docs", "open" => "open rendered docs for viewing" }, run: |opts, fs, _git, cargo, mut workspace, _tasks| { + let check = opts.has("check"); + println!(":::::::::::::::::::::::::::"); - println!(":::: Building All Docs ::::"); + if check { + println!(":::: Checking All Docs ::::"); + } else { + println!(":::: Building All Docs ::::"); + } println!(":::::::::::::::::::::::::::"); println!(); println!(":::: Testing Examples..."); println!(); + // NOTE: `cargo llvm-cov` does not run doc tests, so the + // `coverage` task does not cover them - they run here cargo.test(["--doc", "--all-features"]).run()?; println!(":::: Rendering Docs..."); @@ -466,6 +474,15 @@ fn init_tasks() -> Tasks { cargo.doc(args).run()?; + // NOTE: `--check` verifies only - rewriting the README here + // would mutate the tree mid-CI + if check { + println!(); + println!(":::: Done!"); + println!(); + return Ok(()); + } + println!(); println!(":::: Updating Workspace README..."); @@ -485,6 +502,43 @@ fn init_tasks() -> Tasks { Ok(()) }, }, + Task { + name: "audit".into(), + description: "audit dependencies for advisories, licenses & bans".into(), + flags: task_flags! {}, + run: |_opts, _fs, _git, _cargo, _workspace, _tasks| { + println!("::::::::::::::::::::::::::::::"); + println!(":::: Auditing Dependencies ::::"); + println!("::::::::::::::::::::::::::::::"); + println!(); + + cmd!("cargo", "deny", "--all-features", "check").run()?; + + println!(":::: Done!"); + println!(); + Ok(()) + }, + }, + Task { + name: "semver".into(), + description: "check public APIs for accidental breaking changes".into(), + flags: task_flags! {}, + run: |_opts, _fs, _git, _cargo, _workspace, _tasks| { + println!(":::::::::::::::::::::::::::"); + println!(":::: Checking Semver ::::"); + println!(":::::::::::::::::::::::::::"); + println!(); + + // NOTE: baselines are the last published version of each + // crate, so this needs network access. crates marked + // `publish = false` (e.g. `xtask`) are skipped automatically + cmd!("cargo", "semver-checks", "--workspace").run()?; + + println!(":::: Done!"); + println!(); + Ok(()) + }, + }, Task { name: "format".into(), description: "format source code (rustfmt)".into(), @@ -546,7 +600,9 @@ fn init_tasks() -> Tasks { // llvm-tools-preview) are installed by `rustup` automatically // per `rust-toolchain.toml` cmd!("rustup", "toolchain", "list", "--verbose").run()?; - cargo.install(["grcov"]).run()?; + cargo.install(["cargo-llvm-cov"]).run()?; + cargo.install(["cargo-deny"]).run()?; + cargo.install(["cargo-semver-checks"]).run()?; cargo.install(["typos-cli"]).run()?; println!(":::: Done!"); diff --git a/xtask/src/options.rs b/xtask/src/options.rs index ee36de9..7729861 100644 --- a/xtask/src/options.rs +++ b/xtask/src/options.rs @@ -68,13 +68,11 @@ mod tests { } #[test] - #[should_panic( - expected = "called `Result::unwrap()` on an `Err` value: \"Unrecognized argument! nope\"" - )] fn it_fails_to_initialize_when_args_has_unrecognized_items() { let flags = task_flags! {}; let args = vec!["nope".into()]; - Options::new(args, flags).unwrap(); + let err = Options::new(args, flags).unwrap_err(); + assert_eq!(err.to_string(), "Unrecognized argument! nope"); } #[test] diff --git a/xtask/src/workspace.rs b/xtask/src/workspace.rs index 2e56be6..b02ed16 100644 --- a/xtask/src/workspace.rs +++ b/xtask/src/workspace.rs @@ -94,6 +94,10 @@ impl Workspace { } cargo.clean(["--release"]).run()?; + // NOTE: `cargo-llvm-cov` keeps its instrumented build and `.profraw` + // data under `target/llvm-cov-target`, which neither the `tmp/` wipe + // above nor `cargo clean --release` touches - it is hundreds of MB + cargo.coverage_clean().run()?; Ok(()) } From ad045104fd69f3f0e1e8ac739c181318fe5a0513 Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 12 Aug 2026 12:46:19 -0700 Subject: [PATCH 22/31] [node-js-release-info] assert on returned errors instead of panic text --- crates/node-js-release-info/src/arch.rs | 6 ++--- crates/node-js-release-info/src/ext.rs | 8 +++---- crates/node-js-release-info/src/lib.rs | 30 +++++++++++------------- crates/node-js-release-info/src/os.rs | 6 ++--- crates/node-js-release-info/src/specs.rs | 7 +++--- 5 files changed, 24 insertions(+), 33 deletions(-) diff --git a/crates/node-js-release-info/src/arch.rs b/crates/node-js-release-info/src/arch.rs index 9075161..2159fbe 100644 --- a/crates/node-js-release-info/src/arch.rs +++ b/crates/node-js-release-info/src/arch.rs @@ -192,11 +192,9 @@ mod tests { } #[test] - #[should_panic( - expected = "called `Result::unwrap()` on an `Err` value: UnrecognizedArch(\"NOPE!\")" - )] fn it_fails_when_arch_is_unrecognized() { - NodeJSArch::from_str("NOPE!").unwrap(); + let err = NodeJSArch::from_str("NOPE!").unwrap_err(); + assert!(matches!(err, NodeJSRelInfoError::UnrecognizedArch(x) if x == "NOPE!")); } #[test] diff --git a/crates/node-js-release-info/src/ext.rs b/crates/node-js-release-info/src/ext.rs index cd5de6c..e046aa9 100644 --- a/crates/node-js-release-info/src/ext.rs +++ b/crates/node-js-release-info/src/ext.rs @@ -136,11 +136,9 @@ mod tests { } #[test] - #[should_panic( - expected = "called `Result::unwrap()` on an `Err` value: UnrecognizedExt(\"NOPE!\")" - )] - fn it_fails_when_arch_is_unrecognized() { - NodeJSPkgExt::from_str("NOPE!").unwrap(); + fn it_fails_when_ext_is_unrecognized() { + let err = NodeJSPkgExt::from_str("NOPE!").unwrap_err(); + assert!(matches!(err, NodeJSRelInfoError::UnrecognizedExt(x) if x == "NOPE!")); } #[test] diff --git a/crates/node-js-release-info/src/lib.rs b/crates/node-js-release-info/src/lib.rs index db5ea5b..091b6e5 100644 --- a/crates/node-js-release-info/src/lib.rs +++ b/crates/node-js-release-info/src/lib.rs @@ -603,18 +603,14 @@ mod tests { } #[tokio::test] - #[should_panic( - expected = "called `Result::unwrap()` on an `Err` value: InvalidVersion(\"NOPE!\")" - )] async fn it_fails_to_fetch_info_when_version_is_invalid() { let mut info = NodeJSRelInfo::new("NOPE!"); - info.fetch().await.unwrap(); + let err = info.fetch().await.unwrap_err(); + + assert!(matches!(err, NodeJSRelInfoError::InvalidVersion(x) if x == "NOPE!")); } #[tokio::test] - #[should_panic( - expected = "called `Result::unwrap()` on an `Err` value: UnrecognizedVersion(\"1.0.0\")" - )] async fn it_fails_to_fetch_info_when_version_is_unrecognized() { let mut info = NodeJSRelInfo::new("1.0.0"); let mut server = Server::new_async().await; @@ -624,14 +620,13 @@ mod tests { .create_async() .await; - info.fetch().await.unwrap(); + let err = info.fetch().await.unwrap_err(); mock.assert_async().await; + + assert!(matches!(err, NodeJSRelInfoError::UnrecognizedVersion(x) if x == "1.0.0")); } #[tokio::test] - #[should_panic( - expected = "called `Result::unwrap()` on an `Err` value: UnrecognizedConfiguration(\"node-v20.6.1-linux-x64.zip\")" - )] async fn it_fails_to_fetch_info_when_configuration_is_unrecognized() { let mut server = Server::new_async().await; let mut info = NodeJSRelInfo::new("20.6.1").linux().zip().to_owned(); @@ -640,8 +635,12 @@ mod tests { .create_async() .await; - info.fetch().await.unwrap(); + let err = info.fetch().await.unwrap_err(); mock.assert_async().await; + + assert!( + matches!(err, NodeJSRelInfoError::UnrecognizedConfiguration(x) if x == "node-v20.6.1-linux-x64.zip") + ); } #[tokio::test] @@ -727,9 +726,6 @@ mod tests { } #[tokio::test] - #[should_panic( - expected = "called `Result::unwrap()` on an `Err` value: UnrecognizedVersion(\"1.0.0\")" - )] async fn it_fails_to_fetch_all_supported_node_js_configurations_when_version_is_unrecognized() { let mut info = NodeJSRelInfo::new("1.0.0"); let mut server = Server::new_async().await; @@ -738,7 +734,9 @@ mod tests { .create_async() .await; - info.fetch_all().await.unwrap(); + let err = info.fetch_all().await.unwrap_err(); mock.assert_async().await; + + assert!(matches!(err, NodeJSRelInfoError::UnrecognizedVersion(x) if x == "1.0.0")); } } diff --git a/crates/node-js-release-info/src/os.rs b/crates/node-js-release-info/src/os.rs index f9a85b1..4b964f3 100644 --- a/crates/node-js-release-info/src/os.rs +++ b/crates/node-js-release-info/src/os.rs @@ -149,11 +149,9 @@ mod tests { } #[test] - #[should_panic( - expected = "called `Result::unwrap()` on an `Err` value: UnrecognizedOs(\"NOPE!\")" - )] fn it_fails_when_os_cannot_be_determined_from_str() { - NodeJSOS::from_str("NOPE!").unwrap(); + let err = NodeJSOS::from_str("NOPE!").unwrap_err(); + assert!(matches!(err, NodeJSRelInfoError::UnrecognizedOs(x) if x == "NOPE!")); } #[test] diff --git a/crates/node-js-release-info/src/specs.rs b/crates/node-js-release-info/src/specs.rs index 0fca5f3..74d9c2e 100644 --- a/crates/node-js-release-info/src/specs.rs +++ b/crates/node-js-release-info/src/specs.rs @@ -319,9 +319,6 @@ mod tests { } #[tokio::test] - #[should_panic( - expected = "called `Result::unwrap()` on an `Err` value: UnrecognizedVersion(\"1.0.0\")" - )] async fn it_fails_to_fetch_node_js_specs_when_version_is_unrecognized() { let version = String::from("1.0.0"); let mut url_fmt = NodeJSURLFormatter::new(); @@ -332,7 +329,9 @@ mod tests { .create_async() .await; - fetch(&version, &url_fmt).await.unwrap(); + let err = fetch(&version, &url_fmt).await.unwrap_err(); mock.assert_async().await; + + assert!(matches!(err, NodeJSRelInfoError::UnrecognizedVersion(x) if x == "1.0.0")); } } From d2172ea9f8fed10a925b182cdb0fa6b75d9414bc Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 12 Aug 2026 18:15:25 -0700 Subject: [PATCH 23/31] speed up ci: install prebuilt tooling, make setup skip installed tools --- .github/workflows/test.yaml | 12 +++++++ xtask/src/main.rs | 67 ++++++++++++++++++++++++++++++++++--- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 71d942a..9a49dce 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -26,6 +26,18 @@ jobs: uses: actions/checkout@v7 - name: Install Rust Toolchain (stable) uses: dtolnay/rust-toolchain@stable + # NOTE: `cargo install` always builds from source - crates.io ships no + # binaries. these tools publish prebuilt binaries via GitHub Releases, + # which this action fetches in seconds rather than minutes. installing + # them here makes the `setup` step below a no-op, since it skips any + # tool that is already available + # NOTE: these are `install-action` tool names, not crate names - they + # match one-to-one here except `typos`, whose crate is `typos-cli` (the + # name `cargo xtask setup` installs) and whose binary is `typos` + - name: Install Prebuilt Tooling + uses: taiki-e/install-action@v2 + with: + tool: cargo-llvm-cov,cargo-deny,cargo-semver-checks,typos - name: Setup Project run: cargo xtask setup - name: Run Tests & Coverage diff --git a/xtask/src/main.rs b/xtask/src/main.rs index a3cf1ea..ae2d3aa 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -67,6 +67,19 @@ fn try_main() -> Result<(), DynError> { } } +/// Reports whether a tool is already usable by running a probe command (e.g. +/// `cargo llvm-cov --version`). Returns `false` when the binary is missing or +/// the probe exits non-zero - never errors, so a failed probe just means +/// "install it". +fn is_tool_available(bin: &str, args: &[&str]) -> bool { + cmd(bin, args) + .stdout_null() + .stderr_null() + .unchecked() + .run() + .is_ok_and(|out| out.status.success()) +} + fn print_help(cmd: String, _args: Vec, tasks: Tasks) -> Result<(), DynError> { println!(":::::::::::::::::::::::::"); println!(":::: Tasks & Options ::::"); @@ -600,10 +613,30 @@ fn init_tasks() -> Tasks { // llvm-tools-preview) are installed by `rustup` automatically // per `rust-toolchain.toml` cmd!("rustup", "toolchain", "list", "--verbose").run()?; - cargo.install(["cargo-llvm-cov"]).run()?; - cargo.install(["cargo-deny"]).run()?; - cargo.install(["cargo-semver-checks"]).run()?; - cargo.install(["typos-cli"]).run()?; + + // NOTE: each entry is (crate to install, command that proves + // it is usable). `cargo install` decides whether to skip by + // consulting its own ledger, so it rebuilds from source for a + // binary put in place by anything else - probing the tool + // itself makes this a no-op wherever it is already available + // (e.g. CI, where prebuilt binaries are fetched beforehand) + let bin = cargo.bin.as_str(); + let tools = [ + ("cargo-llvm-cov", bin, vec!["llvm-cov", "--version"]), + ("cargo-deny", bin, vec!["deny", "--version"]), + ("cargo-semver-checks", bin, vec!["semver-checks", "--version"]), + ("typos-cli", "typos", vec!["--version"]), + ]; + + for (krate, probe_bin, probe_args) in tools { + if is_tool_available(probe_bin, &probe_args) { + println!(":::: Found: {krate} - skipping"); + continue; + } + + println!(":::: Installing: {krate}..."); + cargo.install([krate]).run()?; + } println!(":::: Done!"); println!(); @@ -665,3 +698,29 @@ fn init_tasks() -> Tasks { tasks } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_detects_an_available_tool() { + // `cargo` is always present - we are running under it + assert!(is_tool_available("cargo", &["--version"])); + } + + #[test] + fn it_detects_a_missing_tool() { + assert!(!is_tool_available( + "xtask-definitely-not-a-real-binary", + &["--version"] + )); + } + + #[test] + fn it_detects_a_tool_whose_probe_fails() { + // binary exists but the subcommand does not, so the probe exits + // non-zero - treated as "not available" rather than erroring + assert!(!is_tool_available("cargo", &["xtask-not-a-subcommand"])); + } +} From adea4559ea623f1954913f7608ee33f192ea2cba Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 12 Aug 2026 18:41:56 -0700 Subject: [PATCH 24/31] [detect-newline-style] remove regex dependency --- Cargo.lock | 3 -- crates/detect-newline-style/Cargo.toml | 1 - crates/detect-newline-style/src/lib.rs | 63 +++++++++++++++++--------- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 892caaa..0e6d489 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -233,9 +233,6 @@ dependencies = [ [[package]] name = "detect-newline-style" version = "0.1.2" -dependencies = [ - "regex", -] [[package]] name = "displaydoc" diff --git a/crates/detect-newline-style/Cargo.toml b/crates/detect-newline-style/Cargo.toml index 11db05e..2c20302 100644 --- a/crates/detect-newline-style/Cargo.toml +++ b/crates/detect-newline-style/Cargo.toml @@ -20,7 +20,6 @@ authors.workspace = true repository.workspace = true [dependencies] -regex.workspace = true [lints] workspace = true diff --git a/crates/detect-newline-style/src/lib.rs b/crates/detect-newline-style/src/lib.rs index e4c1be0..da72bf3 100644 --- a/crates/detect-newline-style/src/lib.rs +++ b/crates/detect-newline-style/src/lib.rs @@ -1,6 +1,5 @@ #![doc = include_str!("../README.md")] -use regex::RegexBuilder; use std::error::Error; use std::fmt::{Display, Formatter}; use std::str::FromStr; @@ -11,7 +10,7 @@ const CRLF: &str = "\r\n"; /// A newline style - see [`find`](LineEnding::find) to detect which style a /// given string prefers -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] pub enum LineEnding { /// CR-style line ending (`"\r"`) rarely used, mostly on older systems /// (e.g. classic MacOS - OS-X before 10.0) @@ -63,28 +62,31 @@ impl LineEnding { /// assert_eq!(eol, LineEnding::LF); /// ``` pub fn find>(text: S, default: LineEnding) -> LineEnding { - let text = text.as_ref(); - let ptn = r"(?:\r\n?|\n)"; - let re = RegexBuilder::new(ptn) - .case_insensitive(true) - .multi_line(true) - .build() - .unwrap(); - - let matches = re.find_iter(text); + // NOTE: `\r` and `\n` are ASCII, so they can never appear inside a + // multi-byte UTF-8 sequence - scanning bytes is safe and avoids + // pulling in (and re-compiling, on every call) a regex + let bytes = text.as_ref().as_bytes(); let mut crlf_count = 0; let mut cr_count = 0; let mut lf_count = 0; - - for item in matches { - let x = item.as_str(); - - if x == CRLF { - crlf_count += 1; - } else if x == LF { - lf_count += 1; - } else if x == CR { - cr_count += 1; + let mut i = 0; + + while i < bytes.len() { + match bytes[i] { + b'\r' => { + if bytes.get(i + 1) == Some(&b'\n') { + crlf_count += 1; + i += 2; + } else { + cr_count += 1; + i += 1; + } + } + b'\n' => { + lf_count += 1; + i += 1; + } + _ => i += 1, } } @@ -245,6 +247,25 @@ mod tests { assert_eq!(eol, LineEnding::LF); } + #[test] + fn it_counts_a_lone_cr_followed_by_crlf_separately() { + // "\r\r\n" is one CR then one CRLF - not two CRs, and not a CR plus + // an LF. + let eol = LineEnding::find("a\r\r\nb\r\nc\r\nd", LineEnding::LF); + assert_eq!(eol, LineEnding::CRLF); + + // ...and with the CRLFs removed, the lone CR wins + let eol = LineEnding::find("a\r\r\nb", LineEnding::LF); + assert_eq!(eol, LineEnding::LF); + } + + #[test] + fn it_counts_line_breaks_around_multi_byte_characters() { + // `\r` / `\n` are ASCII and cannot appear inside a UTF-8 sequence + let eol = LineEnding::find("日本\r\n語\r\n🦀\n", LineEnding::LF); + assert_eq!(eol, LineEnding::CRLF); + } + #[test] fn it_uses_default_when_text_has_no_line_breaks() { let input = "no line breaks"; From 8869f14f458812797f73d0f95ae48e3a6900ad87 Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 12 Aug 2026 18:45:57 -0700 Subject: [PATCH 25/31] [node-js-release-info] expose error source, derive Eq + Hash, document json feature --- crates/node-js-release-info/Cargo.toml | 5 +++++ crates/node-js-release-info/src/arch.rs | 11 +++-------- crates/node-js-release-info/src/error.rs | 12 +++++++++++- crates/node-js-release-info/src/ext.rs | 11 +++-------- crates/node-js-release-info/src/lib.rs | 2 +- crates/node-js-release-info/src/os.rs | 11 +++-------- crates/node-js-release-info/src/url.rs | 2 +- 7 files changed, 27 insertions(+), 27 deletions(-) diff --git a/crates/node-js-release-info/Cargo.toml b/crates/node-js-release-info/Cargo.toml index c6bdde0..22841b8 100644 --- a/crates/node-js-release-info/Cargo.toml +++ b/crates/node-js-release-info/Cargo.toml @@ -37,5 +37,10 @@ tokio = { workspace = true, features = ["rt-multi-thread"] } [features] json = ["dep:serde"] +# NOTE: docs.rs builds with default features unless told otherwise, which +# would hide everything the `json` feature adds +[package.metadata.docs.rs] +all-features = true + [lints] workspace = true diff --git a/crates/node-js-release-info/src/arch.rs b/crates/node-js-release-info/src/arch.rs index 2159fbe..d2b1890 100644 --- a/crates/node-js-release-info/src/arch.rs +++ b/crates/node-js-release-info/src/arch.rs @@ -5,11 +5,12 @@ use std::env::consts::ARCH; use std::fmt::{Display, Formatter}; use std::str::FromStr; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] /// The CPU architecture a Node.js distributable targets pub enum NodeJSArch { /// 64-bit x86 (`x64`) + #[default] #[cfg_attr(feature = "json", serde(rename = "x64"))] X64, /// 32-bit x86 (`x86`) @@ -32,12 +33,6 @@ pub enum NodeJSArch { S390X, } -impl Default for NodeJSArch { - fn default() -> Self { - NodeJSArch::new() - } -} - impl NodeJSArch { /// Creates a new instance using the default architecture ([`X64`](NodeJSArch::X64)) /// @@ -48,7 +43,7 @@ impl NodeJSArch { /// assert_eq!(NodeJSArch::new(), NodeJSArch::X64); /// ``` pub fn new() -> NodeJSArch { - NodeJSArch::X64 + NodeJSArch::default() } /// Determines the architecture of the current environment via diff --git a/crates/node-js-release-info/src/error.rs b/crates/node-js-release-info/src/error.rs index bd556b0..22ed79c 100644 --- a/crates/node-js-release-info/src/error.rs +++ b/crates/node-js-release-info/src/error.rs @@ -23,7 +23,17 @@ pub enum NodeJSRelInfoError { HttpError(reqwest::Error), } -impl Error for NodeJSRelInfoError {} +impl Error for NodeJSRelInfoError { + /// Exposes the underlying [`reqwest::Error`] behind + /// [`HttpError`](NodeJSRelInfoError::HttpError) so callers (and error + /// reporters like `anyhow`) can walk the full cause chain + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + NodeJSRelInfoError::HttpError(e) => Some(e), + _ => None, + } + } +} impl Display for NodeJSRelInfoError { fn fmt(&self, f: &mut Formatter<'_>) -> Result { diff --git a/crates/node-js-release-info/src/ext.rs b/crates/node-js-release-info/src/ext.rs index e046aa9..32a9621 100644 --- a/crates/node-js-release-info/src/ext.rs +++ b/crates/node-js-release-info/src/ext.rs @@ -4,11 +4,12 @@ use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::str::FromStr; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] /// The file extension of a Node.js distributable pub enum NodeJSPkgExt { /// gzip-compressed tarball (`tar.gz`) + #[default] #[cfg_attr(feature = "json", serde(rename = "tar.gz"))] Targz, /// xz-compressed tarball (`tar.xz`) @@ -25,12 +26,6 @@ pub enum NodeJSPkgExt { S7z, // can't start w/ a number (X_x) } -impl Default for NodeJSPkgExt { - fn default() -> Self { - NodeJSPkgExt::new() - } -} - impl NodeJSPkgExt { /// Creates a new instance using the default extension ([`Targz`](NodeJSPkgExt::Targz)) /// @@ -41,7 +36,7 @@ impl NodeJSPkgExt { /// assert_eq!(NodeJSPkgExt::new(), NodeJSPkgExt::Targz); /// ``` pub fn new() -> NodeJSPkgExt { - NodeJSPkgExt::Targz + NodeJSPkgExt::default() } } impl Display for NodeJSPkgExt { diff --git a/crates/node-js-release-info/src/lib.rs b/crates/node-js-release-info/src/lib.rs index 091b6e5..4525a52 100644 --- a/crates/node-js-release-info/src/lib.rs +++ b/crates/node-js-release-info/src/lib.rs @@ -23,7 +23,7 @@ use std::string::ToString; /// (e.g. [`macos`](NodeJSRelInfo::macos), [`arm64`](NodeJSRelInfo::arm64)), /// then call [`fetch`](NodeJSRelInfo::fetch) to populate `filename`, `sha256` /// and `url` from the downloads server -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] pub struct NodeJSRelInfo { /// The operating system for the Node.js distributable you are targeting diff --git a/crates/node-js-release-info/src/os.rs b/crates/node-js-release-info/src/os.rs index 4b964f3..d2dbdab 100644 --- a/crates/node-js-release-info/src/os.rs +++ b/crates/node-js-release-info/src/os.rs @@ -5,11 +5,12 @@ use std::env::consts::OS; use std::fmt::{Display, Formatter}; use std::str::FromStr; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] /// The operating system a Node.js distributable targets pub enum NodeJSOS { /// Linux (`linux`) + #[default] #[cfg_attr(feature = "json", serde(rename = "linux"))] Linux, /// macOS (`darwin`) @@ -23,12 +24,6 @@ pub enum NodeJSOS { AIX, } -impl Default for NodeJSOS { - fn default() -> Self { - NodeJSOS::new() - } -} - impl NodeJSOS { /// Creates a new instance using the default OS ([`Linux`](NodeJSOS::Linux)) /// @@ -39,7 +34,7 @@ impl NodeJSOS { /// assert_eq!(NodeJSOS::new(), NodeJSOS::Linux); /// ``` pub fn new() -> NodeJSOS { - NodeJSOS::Linux + NodeJSOS::default() } /// Determines the OS of the current environment via diff --git a/crates/node-js-release-info/src/url.rs b/crates/node-js-release-info/src/url.rs index 6292b99..f319e34 100644 --- a/crates/node-js-release-info/src/url.rs +++ b/crates/node-js-release-info/src/url.rs @@ -1,4 +1,4 @@ -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub(crate) struct NodeJSURLFormatter { pub protocol: String, pub host: String, From e34d6b2cd0588f2aef335ee01c0a7f24bbb4d78b Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 12 Aug 2026 19:17:43 -0700 Subject: [PATCH 26/31] [detect-newline-style] rename variants per RFC 430, add concrete error type BREAKING: `LineEnding::{CR,LF,CRLF}` are now `{Cr,Lf,Crlf}` BREAKING: `FromStr::Err` is now `ParseLineEndingError`, not `Box` derive `Copy` on `LineEnding` --- crates/detect-newline-style/README.md | 14 +- crates/detect-newline-style/src/lib.rs | 160 ++++++++++-------- .../detect-newline-style/tests/integration.rs | 18 +- 3 files changed, 108 insertions(+), 84 deletions(-) diff --git a/crates/detect-newline-style/README.md b/crates/detect-newline-style/README.md index dc0ccf1..629a8dc 100644 --- a/crates/detect-newline-style/README.md +++ b/crates/detect-newline-style/README.md @@ -19,23 +19,23 @@ use detect_newline_style::LineEnding; fn main() { let text = "one\rtwo\r\nthree\nfour\n"; - let eol = LineEnding::find(text, LineEnding::LF); + let eol = LineEnding::find(text, LineEnding::Lf); - assert_eq!(eol, LineEnding::LF); + assert_eq!(eol, LineEnding::Lf); let text = "one\rtwo\r\nthree\n"; let eol = LineEnding::find_or_use_lf(text); - assert_eq!(eol, LineEnding::LF); + assert_eq!(eol, LineEnding::Lf); let text = "one\rtwo\r\nthree\n"; let eol = LineEnding::find_or_use_crlf(text); - assert_eq!(eol, LineEnding::CRLF); + assert_eq!(eol, LineEnding::Crlf); - assert_eq!(format!("{}", LineEnding::CR), "\r"); - assert_eq!(format!("{}", LineEnding::LF), "\n"); - assert_eq!(format!("{}", LineEnding::CRLF), "\r\n"); + assert_eq!(format!("{}", LineEnding::Cr), "\r"); + assert_eq!(format!("{}", LineEnding::Lf), "\n"); + assert_eq!(format!("{}", LineEnding::Crlf), "\r\n"); } ``` diff --git a/crates/detect-newline-style/src/lib.rs b/crates/detect-newline-style/src/lib.rs index da72bf3..b5b96a0 100644 --- a/crates/detect-newline-style/src/lib.rs +++ b/crates/detect-newline-style/src/lib.rs @@ -4,22 +4,47 @@ use std::error::Error; use std::fmt::{Display, Formatter}; use std::str::FromStr; +/// Returned by [`LineEnding::from_str`] when the input is not one of `"\r"`, +/// `"\n"` or `"\r\n"` +/// +/// This is a concrete type rather than `Box` so it is `Send + Sync` +/// and can cross thread boundaries +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct ParseLineEndingError { + input: String, +} + +impl ParseLineEndingError { + /// The unrecognized input that produced this error + pub fn input(&self) -> &str { + &self.input + } +} + +impl Display for ParseLineEndingError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "unrecognized line ending - received: '{}'", self.input) + } +} + +impl Error for ParseLineEndingError {} + const CR: &str = "\r"; const LF: &str = "\n"; const CRLF: &str = "\r\n"; /// A newline style - see [`find`](LineEnding::find) to detect which style a /// given string prefers -#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] pub enum LineEnding { /// CR-style line ending (`"\r"`) rarely used, mostly on older systems /// (e.g. classic MacOS - OS-X before 10.0) - CR, + Cr, /// LF-style line ending (`"\n"`) typically used on *nix and MacOS #[default] - LF, + Lf, /// CRLF-style line ending (`"\r\n"`) typically used on Windows - CRLF, + Crlf, } impl LineEnding { @@ -35,16 +60,13 @@ impl LineEnding { /// ```rust /// use detect_newline_style::LineEnding; /// let eol = LineEnding::new("\n"); - /// assert_eq!(eol, LineEnding::LF); + /// assert_eq!(eol, LineEnding::Lf); /// ``` pub fn new>(kind: K) -> LineEnding { - let kind = LineEnding::from_str(kind.as_ref()); - - if kind.is_err() { - return LineEnding::LF; - } - - kind.unwrap() + // NOTE: unrecognized input falls back to the default (`Lf`) rather + // than failing - use [`from_str`](LineEnding::from_str) if you need + // to know the input was invalid + LineEnding::from_str(kind.as_ref()).unwrap_or_default() } /// Determines which newline style a given string uses (CR, LF, or CRLF) @@ -58,8 +80,8 @@ impl LineEnding { /// /// ```rust /// use detect_newline_style::LineEnding; - /// let eol = LineEnding::find("one\ntwo\r\nthree\n", LineEnding::CRLF); - /// assert_eq!(eol, LineEnding::LF); + /// let eol = LineEnding::find("one\ntwo\r\nthree\n", LineEnding::Crlf); + /// assert_eq!(eol, LineEnding::Lf); /// ``` pub fn find>(text: S, default: LineEnding) -> LineEnding { // NOTE: `\r` and `\n` are ASCII, so they can never appear inside a @@ -91,11 +113,11 @@ impl LineEnding { } if crlf_count > lf_count && crlf_count > cr_count { - return LineEnding::CRLF; + return LineEnding::Crlf; } else if lf_count > crlf_count && lf_count > cr_count { - return LineEnding::LF; + return LineEnding::Lf; } else if cr_count > lf_count && cr_count > crlf_count { - return LineEnding::CR; + return LineEnding::Cr; } default @@ -113,12 +135,12 @@ impl LineEnding { /// ```rust /// use detect_newline_style::LineEnding; /// let eol = LineEnding::find_or_use_crlf("one\ntwo\r\nthree\n"); - /// assert_eq!(eol, LineEnding::LF); + /// assert_eq!(eol, LineEnding::Lf); /// let eol = LineEnding::find_or_use_crlf("one\ntwo\r\nthree\r"); - /// assert_eq!(eol, LineEnding::CRLF); + /// assert_eq!(eol, LineEnding::Crlf); /// ``` pub fn find_or_use_crlf>(s: S) -> LineEnding { - LineEnding::find(s, LineEnding::CRLF) + LineEnding::find(s, LineEnding::Crlf) } /// Determines which newline style a given string uses (CR, LF, or CRLF) @@ -133,12 +155,12 @@ impl LineEnding { /// ```rust /// use detect_newline_style::LineEnding; /// let eol = LineEnding::find_or_use_lf("one\r\ntwo\nthree\r\n"); - /// assert_eq!(eol, LineEnding::CRLF); + /// assert_eq!(eol, LineEnding::Crlf); /// let eol = LineEnding::find_or_use_lf("one\ntwo\r\nthree\r"); - /// assert_eq!(eol, LineEnding::LF); + /// assert_eq!(eol, LineEnding::Lf); /// ``` pub fn find_or_use_lf>(s: S) -> LineEnding { - LineEnding::find(s, LineEnding::LF) + LineEnding::find(s, LineEnding::Lf) } /// Determines which newline style a given string uses (CR, LF, or CRLF) @@ -153,21 +175,21 @@ impl LineEnding { /// ```rust /// use detect_newline_style::LineEnding; /// let eol = LineEnding::find_or_use_cr("one\ntwo\r\nthree\n"); - /// assert_eq!(eol, LineEnding::LF); + /// assert_eq!(eol, LineEnding::Lf); /// let eol = LineEnding::find_or_use_cr("one\ntwo\r\nthree\r"); - /// assert_eq!(eol, LineEnding::CR); + /// assert_eq!(eol, LineEnding::Cr); /// ``` pub fn find_or_use_cr>(s: S) -> LineEnding { - LineEnding::find(s, LineEnding::CR) + LineEnding::find(s, LineEnding::Cr) } } impl Display for LineEnding { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let eol = match self { - LineEnding::CR => CR, - LineEnding::LF => LF, - LineEnding::CRLF => CRLF, + LineEnding::Cr => CR, + LineEnding::Lf => LF, + LineEnding::Crlf => CRLF, }; write!(f, "{eol}") @@ -175,14 +197,16 @@ impl Display for LineEnding { } impl FromStr for LineEnding { - type Err = Box; - - fn from_str(s: &str) -> Result> { - match s.to_lowercase().as_str() { - CR => Ok(LineEnding::CR), - LF => Ok(LineEnding::LF), - CRLF => Ok(LineEnding::CRLF), - _ => Err(format!("Unrecognized input: {s}").into()), + type Err = ParseLineEndingError; + + fn from_str(s: &str) -> Result { + match s { + CR => Ok(LineEnding::Cr), + LF => Ok(LineEnding::Lf), + CRLF => Ok(LineEnding::Crlf), + _ => Err(ParseLineEndingError { + input: s.to_owned(), + }), } } } @@ -195,89 +219,89 @@ mod tests { fn it_initializes_a_line_ending() { let eol = LineEnding::new("\r"); - assert_eq!(eol, LineEnding::CR); + assert_eq!(eol, LineEnding::Cr); let eol = LineEnding::new("\n"); - assert_eq!(eol, LineEnding::LF); + assert_eq!(eol, LineEnding::Lf); let eol = LineEnding::new("\r\n"); - assert_eq!(eol, LineEnding::CRLF); + assert_eq!(eol, LineEnding::Crlf); } #[test] fn it_uses_lf_line_ending_when_kind_is_unrecognized() { let eol = LineEnding::new("NOPE!"); - assert_eq!(eol, LineEnding::LF); + assert_eq!(eol, LineEnding::Lf); } #[test] fn it_serializes_a_line_ending() { - assert_eq!("\r", format!("{}", LineEnding::CR)); - assert_eq!("\n", format!("{}", LineEnding::LF)); - assert_eq!("\r\n", format!("{}", LineEnding::CRLF)); + assert_eq!("\r", format!("{}", LineEnding::Cr)); + assert_eq!("\n", format!("{}", LineEnding::Lf)); + assert_eq!("\r\n", format!("{}", LineEnding::Crlf)); } #[test] fn it_finds_preferred_line_ending_when_input_prefers_unix_style_endings() { let input = "\nthis\nprefers\nunix-style endings\r\n"; - let eol = LineEnding::find(input, LineEnding::CRLF); - assert_eq!(eol, LineEnding::LF); + let eol = LineEnding::find(input, LineEnding::Crlf); + assert_eq!(eol, LineEnding::Lf); } #[test] fn it_finds_preferred_line_ending_when_input_prefers_windows_style_endings() { let input = "\r\nthis\r\nprefers\r\nwindows-style endings\n"; - let eol = LineEnding::find(input, LineEnding::CRLF); - assert_eq!(eol, LineEnding::CRLF); + let eol = LineEnding::find(input, LineEnding::Crlf); + assert_eq!(eol, LineEnding::Crlf); } #[test] fn it_finds_preferred_line_ending_when_input_prefers_obsolete_style_endings() { let input = "\rthis\rprefers\r\nobsolete endings\n"; - let eol = LineEnding::find(input, LineEnding::CRLF); - assert_eq!(eol, LineEnding::CR); + let eol = LineEnding::find(input, LineEnding::Crlf); + assert_eq!(eol, LineEnding::Cr); } #[test] fn it_uses_default_when_preference_cannot_be_determined() { let input = "\r\nthis\r\nis\nambiguous\n?\r\r"; - let eol = LineEnding::find(input, LineEnding::LF); - assert_eq!(eol, LineEnding::LF); + let eol = LineEnding::find(input, LineEnding::Lf); + assert_eq!(eol, LineEnding::Lf); } #[test] fn it_counts_a_lone_cr_followed_by_crlf_separately() { // "\r\r\n" is one CR then one CRLF - not two CRs, and not a CR plus // an LF. - let eol = LineEnding::find("a\r\r\nb\r\nc\r\nd", LineEnding::LF); - assert_eq!(eol, LineEnding::CRLF); + let eol = LineEnding::find("a\r\r\nb\r\nc\r\nd", LineEnding::Lf); + assert_eq!(eol, LineEnding::Crlf); // ...and with the CRLFs removed, the lone CR wins - let eol = LineEnding::find("a\r\r\nb", LineEnding::LF); - assert_eq!(eol, LineEnding::LF); + let eol = LineEnding::find("a\r\r\nb", LineEnding::Lf); + assert_eq!(eol, LineEnding::Lf); } #[test] fn it_counts_line_breaks_around_multi_byte_characters() { // `\r` / `\n` are ASCII and cannot appear inside a UTF-8 sequence - let eol = LineEnding::find("日本\r\n語\r\n🦀\n", LineEnding::LF); - assert_eq!(eol, LineEnding::CRLF); + let eol = LineEnding::find("日本\r\n語\r\n🦀\n", LineEnding::Lf); + assert_eq!(eol, LineEnding::Crlf); } #[test] fn it_uses_default_when_text_has_no_line_breaks() { let input = "no line breaks"; - let eol = LineEnding::find(input, LineEnding::LF); - assert_eq!(eol, LineEnding::LF); + let eol = LineEnding::find(input, LineEnding::Lf); + assert_eq!(eol, LineEnding::Lf); } #[test] fn it_uses_default_when_text_is_empty() { let input = ""; - let eol = LineEnding::find(input, LineEnding::LF); - assert_eq!(eol, LineEnding::LF); + let eol = LineEnding::find(input, LineEnding::Lf); + assert_eq!(eol, LineEnding::Lf); } #[test] @@ -285,12 +309,12 @@ mod tests { let input = "\rthis\rprefers\r\nobsolete endings\n"; let eol = LineEnding::find_or_use_cr(input); - assert_eq!(eol, LineEnding::CR); + assert_eq!(eol, LineEnding::Cr); let input = "\r\nthis\r\nis\nambiguous\n?\r\r"; let eol = LineEnding::find_or_use_cr(input); - assert_eq!(eol, LineEnding::CR); + assert_eq!(eol, LineEnding::Cr); } #[test] @@ -298,12 +322,12 @@ mod tests { let input = "\nthis\nprefers\nunix-style endings\r\n"; let eol = LineEnding::find_or_use_lf(input); - assert_eq!(eol, LineEnding::LF); + assert_eq!(eol, LineEnding::Lf); let input = "\r\nthis\r\nis\nambiguous\n?\r\r"; let eol = LineEnding::find_or_use_lf(input); - assert_eq!(eol, LineEnding::LF); + assert_eq!(eol, LineEnding::Lf); } #[test] @@ -311,11 +335,11 @@ mod tests { let input = "\r\nthis\r\nprefers\r\nwindows-style endings\n"; let eol = LineEnding::find_or_use_crlf(input); - assert_eq!(eol, LineEnding::CRLF); + assert_eq!(eol, LineEnding::Crlf); let input = "\r\nthis\r\nis\nambiguous\n?\r\r"; let eol = LineEnding::find_or_use_crlf(input); - assert_eq!(eol, LineEnding::CRLF); + assert_eq!(eol, LineEnding::Crlf); } } diff --git a/crates/detect-newline-style/tests/integration.rs b/crates/detect-newline-style/tests/integration.rs index 21a34d0..b503108 100644 --- a/crates/detect-newline-style/tests/integration.rs +++ b/crates/detect-newline-style/tests/integration.rs @@ -6,59 +6,59 @@ use detect_newline_style::*; fn it_detects_cr_style_endings_while_defaulting_to_cr_endings() { let input = "one\rtwo\r\nthree\rfour\n"; let eol = LineEnding::find_or_use_cr(input); - assert_eq!(eol, LineEnding::CR); + assert_eq!(eol, LineEnding::Cr); } #[test] fn it_detects_lf_style_endings_while_defaulting_to_cr_endings() { let input = "one\rtwo\r\nthree\nfour\n"; let eol = LineEnding::find_or_use_cr(input); - assert_eq!(eol, LineEnding::LF); + assert_eq!(eol, LineEnding::Lf); } #[test] fn it_detects_crlf_style_endings_while_defaulting_to_cr_endings() { let input = "one\rtwo\r\nthree\nfour\r\n"; let eol = LineEnding::find_or_use_cr(input); - assert_eq!(eol, LineEnding::CRLF); + assert_eq!(eol, LineEnding::Crlf); } #[test] fn it_detects_cr_style_endings_while_defaulting_to_lf_endings() { let input = "one\rtwo\r\nthree\rfour\n"; let eol = LineEnding::find_or_use_lf(input); - assert_eq!(eol, LineEnding::CR); + assert_eq!(eol, LineEnding::Cr); } #[test] fn it_detects_lf_style_endings_while_defaulting_to_lf_endings() { let input = "one\rtwo\r\nthree\nfour\n"; let eol = LineEnding::find_or_use_lf(input); - assert_eq!(eol, LineEnding::LF); + assert_eq!(eol, LineEnding::Lf); } #[test] fn it_detects_crlf_style_endings_while_defaulting_to_lf_endings() { let input = "one\rtwo\r\nthree\nfour\r\n"; let eol = LineEnding::find_or_use_lf(input); - assert_eq!(eol, LineEnding::CRLF); + assert_eq!(eol, LineEnding::Crlf); } #[test] fn it_detects_cr_style_endings_while_defaulting_to_crlf_endings() { let input = "one\rtwo\r\nthree\rfour\n"; let eol = LineEnding::find_or_use_crlf(input); - assert_eq!(eol, LineEnding::CR); + assert_eq!(eol, LineEnding::Cr); } #[test] fn it_detects_lf_style_endings_while_defaulting_to_crlf_endings() { let input = "one\rtwo\r\nthree\nfour\n"; let eol = LineEnding::find_or_use_crlf(input); - assert_eq!(eol, LineEnding::LF); + assert_eq!(eol, LineEnding::Lf); } #[test] fn it_detects_crlf_style_endings_while_defaulting_to_crlf_endings() { let input = "one\rtwo\r\nthree\nfour\r\n"; let eol = LineEnding::find_or_use_crlf(input); - assert_eq!(eol, LineEnding::CRLF); + assert_eq!(eol, LineEnding::Crlf); } From c01a7b46b57b4272270997eef6cdb0ea958487ac Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 12 Aug 2026 19:26:31 -0700 Subject: [PATCH 27/31] [node-js-release-info] rename types + variants per RFC 430, consume self in builders BREAKING: types are now `NodeJsRelInfo`, `NodeJsOs`, `NodeJsArch`, `NodeJsPkgExt`, `NodeJsRelInfoError` BREAKING: variants are now UpperCamelCase - e.g. `AIX` -> `Aix`, `PPC64LE` -> `Ppc64le` BREAKING: builder methods consume and return `Self`, so `to_owned()` is gone BREAKING: `fetch()` takes `self` and returns the populated value BREAKING: os/arch/ext/error enums are `#[non_exhaustive]` BREAKING: error messages drop the `Error: ` prefix and are lowercased add `NodeJsOs::SunOs` and `NodeJsArch::Armv6l` for older Node.js releases derive `Copy` on the os/arch/ext enums return an error instead of panicking on a malformed checksum line target Node.js v24.19.0 in integration tests and doc examples --- crates/node-js-release-info/README.md | 20 +- crates/node-js-release-info/src/arch.rs | 169 ++++--- crates/node-js-release-info/src/error.rs | 80 ++-- crates/node-js-release-info/src/ext.rs | 102 ++-- crates/node-js-release-info/src/lib.rs | 435 ++++++++---------- crates/node-js-release-info/src/os.rs | 127 ++--- crates/node-js-release-info/src/specs.rs | 59 ++- crates/node-js-release-info/src/url.rs | 22 +- .../node-js-release-info/tests/integration.rs | 50 +- 9 files changed, 530 insertions(+), 534 deletions(-) diff --git a/crates/node-js-release-info/README.md b/crates/node-js-release-info/README.md index 98bcf30..9c84ebe 100644 --- a/crates/node-js-release-info/README.md +++ b/crates/node-js-release-info/README.md @@ -21,20 +21,20 @@ cargo add tokio --features full ``` ```rust -use node_js_release_info::{NodeJSRelInfo, NodeJSRelInfoError}; +use node_js_release_info::{NodeJsRelInfo, NodeJsRelInfoError}; #[tokio::main] -async fn main() -> Result<(), NodeJSRelInfoError> { +async fn main() -> Result<(), NodeJsRelInfoError> { // get a specific configuration - let info = NodeJSRelInfo::new("20.6.1").macos().arm64().fetch().await?; - assert_eq!(info.version, "20.6.1"); - assert_eq!(info.filename, "node-v20.6.1-darwin-arm64.tar.gz"); - assert_eq!(info.sha256, "d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46"); - assert_eq!(info.url, "https://nodejs.org/download/release/v20.6.1/node-v20.6.1-darwin-arm64.tar.gz"); + let info = NodeJsRelInfo::new("24.19.0").macos().arm64().fetch().await?; + assert_eq!(info.version, "24.19.0"); + assert_eq!(info.filename, "node-v24.19.0-darwin-arm64.tar.gz"); + assert_eq!(info.sha256, "8294b7aa9b03997481c06babf1e8b270c859358f27da57a11509afe537ac381d"); + assert_eq!(info.url, "https://nodejs.org/download/release/v24.19.0/node-v24.19.0-darwin-arm64.tar.gz"); // get all supported configurations let all = info.fetch_all().await?; - assert_eq!(all.len(), 24); + assert_eq!(all.len(), 19); assert_eq!(all[2], info); println!("{:?}", all); Ok(()) @@ -50,11 +50,11 @@ cargo add node-js-release-info --features json ``` ```rust,ignore -use node_js_release_info::{NodeJSRelInfo, NodeJSRelInfoError}; +use node_js_release_info::NodeJsRelInfo; #[tokio::main] async fn main() { - let info = NodeJSRelInfo::new("20.6.1").macos().arm64().to_owned(); + let info = NodeJsRelInfo::new("24.19.0").macos().arm64(); let json = serde_json::to_string(&info).unwrap(); let info_deserialized = serde_json::from_str(&json).unwrap(); assert_eq!(info, info_deserialized); diff --git a/crates/node-js-release-info/src/arch.rs b/crates/node-js-release-info/src/arch.rs index d2b1890..96ad36e 100644 --- a/crates/node-js-release-info/src/arch.rs +++ b/crates/node-js-release-info/src/arch.rs @@ -1,14 +1,18 @@ -use crate::error::NodeJSRelInfoError; +use crate::error::NodeJsRelInfoError; #[cfg(feature = "json")] use serde::{Deserialize, Serialize}; use std::env::consts::ARCH; use std::fmt::{Display, Formatter}; use std::str::FromStr; -#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "json", derive(Deserialize, Serialize))] /// The CPU architecture a Node.js distributable targets -pub enum NodeJSArch { +/// +/// Non-exhaustive: Node.js has added and removed target architectures over +/// time, so new variants may appear in a minor release +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "json", derive(Deserialize, Serialize))] +#[non_exhaustive] +pub enum NodeJsArch { /// 64-bit x86 (`x64`) #[default] #[cfg_attr(feature = "json", serde(rename = "x64"))] @@ -18,32 +22,37 @@ pub enum NodeJSArch { X86, /// 64-bit ARM (`arm64`) #[cfg_attr(feature = "json", serde(rename = "arm64"))] - ARM64, - /// 32-bit ARMv7 with hardware floating point (`armv7l`) + Arm64, + /// 32-bit ARMv6 with hardware floating point (`armv6l`) - shipped up to + /// Node.js v11 + #[cfg_attr(feature = "json", serde(rename = "armv6l"))] + Armv6l, + /// 32-bit ARMv7 with hardware floating point (`armv7l`) - shipped up to + /// Node.js v23 #[cfg_attr(feature = "json", serde(rename = "armv7l"))] - ARMV7L, + Armv7l, /// 64-bit PowerPC, big-endian (`ppc64`) #[cfg_attr(feature = "json", serde(rename = "ppc64"))] - PPC64, + Ppc64, /// 64-bit PowerPC, little-endian (`ppc64le`) #[cfg_attr(feature = "json", serde(rename = "ppc64le"))] - PPC64LE, + Ppc64le, /// 64-bit IBM Z (`s390x`) #[cfg_attr(feature = "json", serde(rename = "s390x"))] - S390X, + S390x, } -impl NodeJSArch { - /// Creates a new instance using the default architecture ([`X64`](NodeJSArch::X64)) +impl NodeJsArch { + /// Creates a new instance using the default architecture ([`X64`](NodeJsArch::X64)) /// /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSArch; - /// assert_eq!(NodeJSArch::new(), NodeJSArch::X64); + /// use node_js_release_info::NodeJsArch; + /// assert_eq!(NodeJsArch::new(), NodeJsArch::X64); /// ``` - pub fn new() -> NodeJSArch { - NodeJSArch::default() + pub fn new() -> NodeJsArch { + NodeJsArch::default() } /// Determines the architecture of the current environment via @@ -51,42 +60,44 @@ impl NodeJSArch { /// /// # Errors /// - /// Returns [`NodeJSRelInfoError::UnrecognizedArch`] when the current + /// Returns [`NodeJsRelInfoError::UnrecognizedArch`] when the current /// architecture has no corresponding Node.js distributable - pub fn from_env() -> Result { - NodeJSArch::from_str(ARCH) + pub fn from_env() -> Result { + NodeJsArch::from_str(ARCH) } } -impl Display for NodeJSArch { +impl Display for NodeJsArch { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let arch = match self { - NodeJSArch::X64 => "x64", - NodeJSArch::X86 => "x86", - NodeJSArch::ARM64 => "arm64", - NodeJSArch::ARMV7L => "armv7l", - NodeJSArch::PPC64 => "ppc64", - NodeJSArch::PPC64LE => "ppc64le", - NodeJSArch::S390X => "s390x", + NodeJsArch::X64 => "x64", + NodeJsArch::X86 => "x86", + NodeJsArch::Arm64 => "arm64", + NodeJsArch::Armv6l => "armv6l", + NodeJsArch::Armv7l => "armv7l", + NodeJsArch::Ppc64 => "ppc64", + NodeJsArch::Ppc64le => "ppc64le", + NodeJsArch::S390x => "s390x", }; write!(f, "{arch}") } } -impl FromStr for NodeJSArch { - type Err = NodeJSRelInfoError; +impl FromStr for NodeJsArch { + type Err = NodeJsRelInfoError; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { match s { - "x64" | "x86_64" => Ok(NodeJSArch::X64), - "x86" => Ok(NodeJSArch::X86), - "arm64" | "aarch64" => Ok(NodeJSArch::ARM64), - "arm" | "armv7l" => Ok(NodeJSArch::ARMV7L), - "ppc64" | "powerpc64" => Ok(NodeJSArch::PPC64), - "ppc64le" => Ok(NodeJSArch::PPC64LE), - "s390x" => Ok(NodeJSArch::S390X), - _ => Err(NodeJSRelInfoError::UnrecognizedArch(s.to_string())), + "x64" | "x86_64" => Ok(NodeJsArch::X64), + "x86" => Ok(NodeJsArch::X86), + "arm64" | "aarch64" => Ok(NodeJsArch::Arm64), + "armv6l" => Ok(NodeJsArch::Armv6l), + "arm" | "armv7l" => Ok(NodeJsArch::Armv7l), + "ppc64" | "powerpc64" => Ok(NodeJsArch::Ppc64), + "ppc64le" => Ok(NodeJsArch::Ppc64le), + "s390x" => Ok(NodeJsArch::S390x), + _ => Err(NodeJsRelInfoError::UnrecognizedArch(s.to_string())), } } } @@ -97,106 +108,114 @@ mod tests { #[test] fn it_initializes() { - let arch = NodeJSArch::new(); - assert_eq!(arch, NodeJSArch::X64); + let arch = NodeJsArch::new(); + assert_eq!(arch, NodeJsArch::X64); } #[test] fn it_initializes_with_defaults() { - let arch = NodeJSArch::default(); - assert_eq!(arch, NodeJSArch::X64); + let arch = NodeJsArch::default(); + assert_eq!(arch, NodeJsArch::X64); } #[test] fn it_initializes_from_str() { - let arch = NodeJSArch::from_str("x64").unwrap(); + let arch = NodeJsArch::from_str("x64").unwrap(); + + assert_eq!(arch, NodeJsArch::X64); - assert_eq!(arch, NodeJSArch::X64); + let arch = NodeJsArch::from_str("x86_64").unwrap(); - let arch = NodeJSArch::from_str("x86_64").unwrap(); + assert_eq!(arch, NodeJsArch::X64); - assert_eq!(arch, NodeJSArch::X64); + let arch = NodeJsArch::from_str("x86").unwrap(); - let arch = NodeJSArch::from_str("x86").unwrap(); + assert_eq!(arch, NodeJsArch::X86); - assert_eq!(arch, NodeJSArch::X86); + let arch = NodeJsArch::from_str("arm64").unwrap(); - let arch = NodeJSArch::from_str("arm64").unwrap(); + assert_eq!(arch, NodeJsArch::Arm64); - assert_eq!(arch, NodeJSArch::ARM64); + let arch = NodeJsArch::from_str("aarch64").unwrap(); - let arch = NodeJSArch::from_str("aarch64").unwrap(); + assert_eq!(arch, NodeJsArch::Arm64); - assert_eq!(arch, NodeJSArch::ARM64); + let arch = NodeJsArch::from_str("arm").unwrap(); - let arch = NodeJSArch::from_str("arm").unwrap(); + assert_eq!(arch, NodeJsArch::Armv7l); - assert_eq!(arch, NodeJSArch::ARMV7L); + let arch = NodeJsArch::from_str("armv6l").unwrap(); - let arch = NodeJSArch::from_str("ppc64").unwrap(); + assert_eq!(arch, NodeJsArch::Armv6l); - assert_eq!(arch, NodeJSArch::PPC64); + let arch = NodeJsArch::from_str("ppc64").unwrap(); - let arch = NodeJSArch::from_str("ppc64le").unwrap(); + assert_eq!(arch, NodeJsArch::Ppc64); - assert_eq!(arch, NodeJSArch::PPC64LE); + let arch = NodeJsArch::from_str("ppc64le").unwrap(); - let arch = NodeJSArch::from_str("powerpc64").unwrap(); + assert_eq!(arch, NodeJsArch::Ppc64le); - assert_eq!(arch, NodeJSArch::PPC64); + let arch = NodeJsArch::from_str("powerpc64").unwrap(); - let arch = NodeJSArch::from_str("s390x").unwrap(); + assert_eq!(arch, NodeJsArch::Ppc64); - assert_eq!(arch, NodeJSArch::S390X); + let arch = NodeJsArch::from_str("s390x").unwrap(); + + assert_eq!(arch, NodeJsArch::S390x); } #[test] fn it_serializes_to_str() { - let text = format!("{}", NodeJSArch::X64); + let text = format!("{}", NodeJsArch::X64); assert_eq!(text, "x64"); - let text = format!("{}", NodeJSArch::X86); + let text = format!("{}", NodeJsArch::X86); assert_eq!(text, "x86"); - let text = format!("{}", NodeJSArch::ARM64); + let text = format!("{}", NodeJsArch::Arm64); assert_eq!(text, "arm64"); - let text = format!("{}", NodeJSArch::ARMV7L); + let text = format!("{}", NodeJsArch::Armv7l); assert_eq!(text, "armv7l"); - let text = format!("{}", NodeJSArch::PPC64); + let text = format!("{}", NodeJsArch::Armv6l); + + assert_eq!(text, "armv6l"); + + let text = format!("{}", NodeJsArch::Ppc64); assert_eq!(text, "ppc64"); - let text = format!("{}", NodeJSArch::PPC64LE); + let text = format!("{}", NodeJsArch::Ppc64le); assert_eq!(text, "ppc64le"); - let text = format!("{}", NodeJSArch::S390X); + let text = format!("{}", NodeJsArch::S390x); assert_eq!(text, "s390x"); } #[test] fn it_initializes_using_current_environment() { - NodeJSArch::from_env().unwrap(); + NodeJsArch::from_env().unwrap(); } #[test] fn it_fails_when_arch_is_unrecognized() { - let err = NodeJSArch::from_str("NOPE!").unwrap_err(); - assert!(matches!(err, NodeJSRelInfoError::UnrecognizedArch(x) if x == "NOPE!")); + let err = NodeJsArch::from_str("NOPE!").unwrap_err(); + assert!(matches!(err, NodeJsRelInfoError::UnrecognizedArch(x) if x == "NOPE!")); } #[test] #[cfg(feature = "json")] fn it_serializes_and_deserializes() { - let arch_json = serde_json::to_string(&NodeJSArch::X64).unwrap(); - let arch: NodeJSArch = serde_json::from_str(&arch_json).unwrap(); - assert_eq!(arch, NodeJSArch::X64); + let arch_json = serde_json::to_string(&NodeJsArch::X64).unwrap(); + let arch: NodeJsArch = serde_json::from_str(&arch_json).unwrap(); + assert_eq!(arch, NodeJsArch::X64); } } diff --git a/crates/node-js-release-info/src/error.rs b/crates/node-js-release-info/src/error.rs index 22ed79c..e1e9993 100644 --- a/crates/node-js-release-info/src/error.rs +++ b/crates/node-js-release-info/src/error.rs @@ -2,16 +2,19 @@ use std::error::Error; use std::fmt::{Display, Formatter, Result}; /// The error type returned by all fallible operations in this crate +/// +/// Non-exhaustive: new variants may appear in a minor release #[derive(Debug)] -pub enum NodeJSRelInfoError { +#[non_exhaustive] +pub enum NodeJsRelInfoError { /// The operating system for the Node.js distributable you are targeting is - /// unrecognized - see: [`NodeJSOS`](crate::NodeJSOS) for options + /// unrecognized - see: [`NodeJsOs`](crate::NodeJsOs) for options UnrecognizedOs(String), /// The CPU architecture for the Node.js distributable you are targeting is - /// unrecognized - see: [`NodeJSArch`](crate::NodeJSArch) for options + /// unrecognized - see: [`NodeJsArch`](crate::NodeJsArch) for options UnrecognizedArch(String), /// The file extension of the Node.js distributable you are targeting is - /// unrecognized - see: [`NodeJSPkgExt`](crate::NodeJSPkgExt) for options + /// unrecognized - see: [`NodeJsPkgExt`](crate::NodeJsPkgExt) for options UnrecognizedExt(String), /// The version string provided is invalid - see: [semver](https://semver.org) InvalidVersion(String), @@ -23,49 +26,49 @@ pub enum NodeJSRelInfoError { HttpError(reqwest::Error), } -impl Error for NodeJSRelInfoError { +impl Error for NodeJsRelInfoError { /// Exposes the underlying [`reqwest::Error`] behind - /// [`HttpError`](NodeJSRelInfoError::HttpError) so callers (and error + /// [`HttpError`](NodeJsRelInfoError::HttpError) so callers (and error /// reporters like `anyhow`) can walk the full cause chain fn source(&self) -> Option<&(dyn Error + 'static)> { match self { - NodeJSRelInfoError::HttpError(e) => Some(e), + NodeJsRelInfoError::HttpError(e) => Some(e), _ => None, } } } -impl Display for NodeJSRelInfoError { +impl Display for NodeJsRelInfoError { fn fmt(&self, f: &mut Formatter<'_>) -> Result { let message = match self { - NodeJSRelInfoError::UnrecognizedOs(input) => { - format!("Unrecognized OS! Received: '{input}'") + NodeJsRelInfoError::UnrecognizedOs(input) => { + format!("unrecognized os - received: '{input}'") } - NodeJSRelInfoError::UnrecognizedArch(input) => { - format!("Unrecognized Arch! Received: '{input}'") + NodeJsRelInfoError::UnrecognizedArch(input) => { + format!("unrecognized arch - received: '{input}'") } - NodeJSRelInfoError::UnrecognizedExt(input) => { - format!("Unrecognized File Extension! Received: '{input}'") + NodeJsRelInfoError::UnrecognizedExt(input) => { + format!("unrecognized file extension - received: '{input}'") } - NodeJSRelInfoError::InvalidVersion(input) => { - format!("Invalid Version! Received: '{input}'") + NodeJsRelInfoError::InvalidVersion(input) => { + format!("invalid version - received: '{input}'") } - NodeJSRelInfoError::UnrecognizedVersion(input) => { - format!("Unrecognized Version! Received: '{input}'") + NodeJsRelInfoError::UnrecognizedVersion(input) => { + format!("unrecognized version - received: '{input}'") } - NodeJSRelInfoError::UnrecognizedConfiguration(input) => { - format!("Unrecognized Configuration! Received: '{input}'") + NodeJsRelInfoError::UnrecognizedConfiguration(input) => { + format!("unrecognized configuration - received: '{input}'") } - NodeJSRelInfoError::HttpError(e) => return write!(f, "{e}"), + NodeJsRelInfoError::HttpError(e) => return write!(f, "{e}"), }; - write!(f, "Error: {message}") + write!(f, "{message}") } } -impl From for NodeJSRelInfoError { +impl From for NodeJsRelInfoError { fn from(e: reqwest::Error) -> Self { - NodeJSRelInfoError::HttpError(e) + NodeJsRelInfoError::HttpError(e) } } @@ -75,55 +78,52 @@ mod tests { #[test] fn it_prints_expected_message_when_os_is_unrecognized() { - let err = NodeJSRelInfoError::UnrecognizedOs("unknown-os".to_string()); - assert_eq!( - format!("{err}"), - "Error: Unrecognized OS! Received: 'unknown-os'" - ); + let err = NodeJsRelInfoError::UnrecognizedOs("unknown-os".to_string()); + assert_eq!(format!("{err}"), "unrecognized os - received: 'unknown-os'"); } #[test] fn it_prints_expected_message_when_arch_is_unrecognized() { - let err = NodeJSRelInfoError::UnrecognizedArch("unknown-arch".to_string()); + let err = NodeJsRelInfoError::UnrecognizedArch("unknown-arch".to_string()); assert_eq!( format!("{err}"), - "Error: Unrecognized Arch! Received: 'unknown-arch'" + "unrecognized arch - received: 'unknown-arch'" ); } #[test] fn it_prints_expected_message_when_extension_is_unrecognized() { - let err = NodeJSRelInfoError::UnrecognizedExt("unknown-ext".to_string()); + let err = NodeJsRelInfoError::UnrecognizedExt("unknown-ext".to_string()); assert_eq!( format!("{err}"), - "Error: Unrecognized File Extension! Received: 'unknown-ext'" + "unrecognized file extension - received: 'unknown-ext'" ); } #[test] fn it_prints_expected_message_when_version_is_invalid() { - let err = NodeJSRelInfoError::InvalidVersion("invalid-ver".to_string()); + let err = NodeJsRelInfoError::InvalidVersion("invalid-ver".to_string()); assert_eq!( format!("{err}"), - "Error: Invalid Version! Received: 'invalid-ver'" + "invalid version - received: 'invalid-ver'" ); } #[test] fn it_prints_expected_message_when_version_is_unrecognized() { - let err = NodeJSRelInfoError::UnrecognizedVersion("unknown-ver".to_string()); + let err = NodeJsRelInfoError::UnrecognizedVersion("unknown-ver".to_string()); assert_eq!( format!("{err}"), - "Error: Unrecognized Version! Received: 'unknown-ver'" + "unrecognized version - received: 'unknown-ver'" ); } #[test] fn it_prints_expected_message_when_configuration_is_unrecognized() { - let err = NodeJSRelInfoError::UnrecognizedConfiguration("unknown-cfg".to_string()); + let err = NodeJsRelInfoError::UnrecognizedConfiguration("unknown-cfg".to_string()); assert_eq!( format!("{err}"), - "Error: Unrecognized Configuration! Received: 'unknown-cfg'" + "unrecognized configuration - received: 'unknown-cfg'" ); } @@ -134,7 +134,7 @@ mod tests { // so assert on that rather than on reqwest's exact wording, which // changes between releases let expected = source.to_string(); - let err = NodeJSRelInfoError::from(source); + let err = NodeJsRelInfoError::from(source); assert_eq!(format!("{err}"), expected); } diff --git a/crates/node-js-release-info/src/ext.rs b/crates/node-js-release-info/src/ext.rs index 32a9621..bf94715 100644 --- a/crates/node-js-release-info/src/ext.rs +++ b/crates/node-js-release-info/src/ext.rs @@ -1,13 +1,17 @@ -use crate::error::NodeJSRelInfoError; +use crate::error::NodeJsRelInfoError; #[cfg(feature = "json")] use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::str::FromStr; -#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "json", derive(Deserialize, Serialize))] /// The file extension of a Node.js distributable -pub enum NodeJSPkgExt { +/// +/// Non-exhaustive: Node.js has added and removed package formats over time, +/// so new variants may appear in a minor release +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "json", derive(Deserialize, Serialize))] +#[non_exhaustive] +pub enum NodeJsPkgExt { /// gzip-compressed tarball (`tar.gz`) #[default] #[cfg_attr(feature = "json", serde(rename = "tar.gz"))] @@ -26,44 +30,44 @@ pub enum NodeJSPkgExt { S7z, // can't start w/ a number (X_x) } -impl NodeJSPkgExt { - /// Creates a new instance using the default extension ([`Targz`](NodeJSPkgExt::Targz)) +impl NodeJsPkgExt { + /// Creates a new instance using the default extension ([`Targz`](NodeJsPkgExt::Targz)) /// /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSPkgExt; - /// assert_eq!(NodeJSPkgExt::new(), NodeJSPkgExt::Targz); + /// use node_js_release_info::NodeJsPkgExt; + /// assert_eq!(NodeJsPkgExt::new(), NodeJsPkgExt::Targz); /// ``` - pub fn new() -> NodeJSPkgExt { - NodeJSPkgExt::default() + pub fn new() -> NodeJsPkgExt { + NodeJsPkgExt::default() } } -impl Display for NodeJSPkgExt { +impl Display for NodeJsPkgExt { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let arch = match self { - NodeJSPkgExt::Targz => "tar.gz", - NodeJSPkgExt::Tarxz => "tar.xz", - NodeJSPkgExt::Zip => "zip", - NodeJSPkgExt::Msi => "msi", - NodeJSPkgExt::S7z => "7z", + NodeJsPkgExt::Targz => "tar.gz", + NodeJsPkgExt::Tarxz => "tar.xz", + NodeJsPkgExt::Zip => "zip", + NodeJsPkgExt::Msi => "msi", + NodeJsPkgExt::S7z => "7z", }; write!(f, "{arch}") } } -impl FromStr for NodeJSPkgExt { - type Err = NodeJSRelInfoError; +impl FromStr for NodeJsPkgExt { + type Err = NodeJsRelInfoError; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { match s { - "tar.gz" => Ok(NodeJSPkgExt::Targz), - "tar.xz" => Ok(NodeJSPkgExt::Tarxz), - "zip" => Ok(NodeJSPkgExt::Zip), - "msi" => Ok(NodeJSPkgExt::Msi), - "7z" => Ok(NodeJSPkgExt::S7z), - _ => Err(NodeJSRelInfoError::UnrecognizedExt(s.to_string())), + "tar.gz" => Ok(NodeJsPkgExt::Targz), + "tar.xz" => Ok(NodeJsPkgExt::Tarxz), + "zip" => Ok(NodeJsPkgExt::Zip), + "msi" => Ok(NodeJsPkgExt::Msi), + "7z" => Ok(NodeJsPkgExt::S7z), + _ => Err(NodeJsRelInfoError::UnrecognizedExt(s.to_string())), } } } @@ -74,73 +78,73 @@ mod tests { #[test] fn it_initializes() { - let ext = NodeJSPkgExt::new(); - assert_eq!(ext, NodeJSPkgExt::Targz); + let ext = NodeJsPkgExt::new(); + assert_eq!(ext, NodeJsPkgExt::Targz); } #[test] fn it_initializes_with_defaults() { - let ext = NodeJSPkgExt::default(); - assert_eq!(ext, NodeJSPkgExt::Targz); + let ext = NodeJsPkgExt::default(); + assert_eq!(ext, NodeJsPkgExt::Targz); } #[test] fn it_initializes_from_str() { - let ext = NodeJSPkgExt::from_str("tar.gz").unwrap(); + let ext = NodeJsPkgExt::from_str("tar.gz").unwrap(); - assert_eq!(ext, NodeJSPkgExt::Targz); + assert_eq!(ext, NodeJsPkgExt::Targz); - let ext = NodeJSPkgExt::from_str("tar.xz").unwrap(); + let ext = NodeJsPkgExt::from_str("tar.xz").unwrap(); - assert_eq!(ext, NodeJSPkgExt::Tarxz); + assert_eq!(ext, NodeJsPkgExt::Tarxz); - let ext = NodeJSPkgExt::from_str("zip").unwrap(); + let ext = NodeJsPkgExt::from_str("zip").unwrap(); - assert_eq!(ext, NodeJSPkgExt::Zip); + assert_eq!(ext, NodeJsPkgExt::Zip); - let ext = NodeJSPkgExt::from_str("msi").unwrap(); + let ext = NodeJsPkgExt::from_str("msi").unwrap(); - assert_eq!(ext, NodeJSPkgExt::Msi); + assert_eq!(ext, NodeJsPkgExt::Msi); - let ext = NodeJSPkgExt::from_str("7z").unwrap(); + let ext = NodeJsPkgExt::from_str("7z").unwrap(); - assert_eq!(ext, NodeJSPkgExt::S7z); + assert_eq!(ext, NodeJsPkgExt::S7z); } #[test] fn it_serializes_to_str() { - let text = format!("{}", NodeJSPkgExt::Targz); + let text = format!("{}", NodeJsPkgExt::Targz); assert_eq!(text, "tar.gz"); - let text = format!("{}", NodeJSPkgExt::Tarxz); + let text = format!("{}", NodeJsPkgExt::Tarxz); assert_eq!(text, "tar.xz"); - let text = format!("{}", NodeJSPkgExt::Zip); + let text = format!("{}", NodeJsPkgExt::Zip); assert_eq!(text, "zip"); - let text = format!("{}", NodeJSPkgExt::Msi); + let text = format!("{}", NodeJsPkgExt::Msi); assert_eq!(text, "msi"); - let text = format!("{}", NodeJSPkgExt::S7z); + let text = format!("{}", NodeJsPkgExt::S7z); assert_eq!(text, "7z"); } #[test] fn it_fails_when_ext_is_unrecognized() { - let err = NodeJSPkgExt::from_str("NOPE!").unwrap_err(); - assert!(matches!(err, NodeJSRelInfoError::UnrecognizedExt(x) if x == "NOPE!")); + let err = NodeJsPkgExt::from_str("NOPE!").unwrap_err(); + assert!(matches!(err, NodeJsRelInfoError::UnrecognizedExt(x) if x == "NOPE!")); } #[test] #[cfg(feature = "json")] fn it_serializes_and_deserializes() { - let ext_json = serde_json::to_string(&NodeJSPkgExt::Tarxz).unwrap(); - let ext: NodeJSPkgExt = serde_json::from_str(&ext_json).unwrap(); - assert_eq!(ext, NodeJSPkgExt::Tarxz); + let ext_json = serde_json::to_string(&NodeJsPkgExt::Tarxz).unwrap(); + let ext: NodeJsPkgExt = serde_json::from_str(&ext_json).unwrap(); + assert_eq!(ext, NodeJsPkgExt::Tarxz); } } diff --git a/crates/node-js-release-info/src/lib.rs b/crates/node-js-release-info/src/lib.rs index 4525a52..7da6362 100644 --- a/crates/node-js-release-info/src/lib.rs +++ b/crates/node-js-release-info/src/lib.rs @@ -7,31 +7,31 @@ mod os; mod specs; mod url; -pub use crate::arch::NodeJSArch; -pub use crate::error::NodeJSRelInfoError; -pub use crate::ext::NodeJSPkgExt; -pub use crate::os::NodeJSOS; -use crate::url::NodeJSURLFormatter; +pub use crate::arch::NodeJsArch; +pub use crate::error::NodeJsRelInfoError; +pub use crate::ext::NodeJsPkgExt; +pub use crate::os::NodeJsOs; +use crate::url::NodeJsUrlFormatter; #[cfg(feature = "json")] use serde::{Deserialize, Serialize}; use std::string::ToString; /// Metadata describing a single Node.js distributable /// -/// Build one with [`new`](NodeJSRelInfo::new) or -/// [`from_env`](NodeJSRelInfo::from_env), narrow it with the builder methods -/// (e.g. [`macos`](NodeJSRelInfo::macos), [`arm64`](NodeJSRelInfo::arm64)), -/// then call [`fetch`](NodeJSRelInfo::fetch) to populate `filename`, `sha256` +/// Build one with [`new`](NodeJsRelInfo::new) or +/// [`from_env`](NodeJsRelInfo::from_env), narrow it with the builder methods +/// (e.g. [`macos`](NodeJsRelInfo::macos), [`arm64`](NodeJsRelInfo::arm64)), +/// then call [`fetch`](NodeJsRelInfo::fetch) to populate `filename`, `sha256` /// and `url` from the downloads server #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] #[cfg_attr(feature = "json", derive(Deserialize, Serialize))] -pub struct NodeJSRelInfo { +pub struct NodeJsRelInfo { /// The operating system for the Node.js distributable you are targeting - pub os: NodeJSOS, + pub os: NodeJsOs, /// The CPU architecture for the Node.js distributable you are targeting - pub arch: NodeJSArch, + pub arch: NodeJsArch, /// The file extension for the Node.js distributable you are targeting - pub ext: NodeJSPkgExt, + pub ext: NodeJsPkgExt, /// The version of Node.js you are targeting as a [semver](https://semver.org) string pub version: String, /// The filename of the Node.js distributable (populated after fetching) @@ -41,10 +41,10 @@ pub struct NodeJSRelInfo { /// The fully qualified url for the Node.js distributable (populated after fetching) pub url: String, #[cfg_attr(feature = "json", serde(skip))] - url_fmt: NodeJSURLFormatter, + url_fmt: NodeJsUrlFormatter, } -impl NodeJSRelInfo { +impl NodeJsRelInfo { /// Creates a new instance using default settings /// /// # Arguments @@ -54,11 +54,11 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::new("20.6.1"); + /// use node_js_release_info::NodeJsRelInfo; + /// let info = NodeJsRelInfo::new("24.19.0"); /// ``` pub fn new>(semver: T) -> Self { - NodeJSRelInfo { + NodeJsRelInfo { version: semver.as_ref().to_owned(), ..Default::default() } @@ -73,16 +73,16 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::from_env("20.6.1"); + /// use node_js_release_info::NodeJsRelInfo; + /// let info = NodeJsRelInfo::from_env("24.19.0"); /// ``` - pub fn from_env>(semver: T) -> Result { - let mut info = NodeJSRelInfo::new(semver); - info.os = NodeJSOS::from_env()?; - info.arch = NodeJSArch::from_env()?; + pub fn from_env>(semver: T) -> Result { + let mut info = NodeJsRelInfo::new(semver); + info.os = NodeJsOs::from_env()?; + info.arch = NodeJsArch::from_env()?; info.ext = match info.os { - NodeJSOS::Windows => NodeJSPkgExt::Zip, - _ => NodeJSPkgExt::Targz, + NodeJsOs::Windows => NodeJsPkgExt::Zip, + _ => NodeJsPkgExt::Targz, }; Ok(info) } @@ -92,11 +92,11 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::new("20.6.1").macos(); + /// use node_js_release_info::NodeJsRelInfo; + /// let info = NodeJsRelInfo::new("24.19.0").macos(); /// ``` - pub fn macos(&mut self) -> &mut Self { - self.os = NodeJSOS::Darwin; + pub fn macos(mut self) -> Self { + self.os = NodeJsOs::Darwin; self } @@ -105,11 +105,11 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::new("20.6.1").linux(); + /// use node_js_release_info::NodeJsRelInfo; + /// let info = NodeJsRelInfo::new("24.19.0").linux(); /// ``` - pub fn linux(&mut self) -> &mut Self { - self.os = NodeJSOS::Linux; + pub fn linux(mut self) -> Self { + self.os = NodeJsOs::Linux; self } @@ -118,11 +118,11 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::new("20.6.1").windows(); + /// use node_js_release_info::NodeJsRelInfo; + /// let info = NodeJsRelInfo::new("24.19.0").windows(); /// ``` - pub fn windows(&mut self) -> &mut Self { - self.os = NodeJSOS::Windows; + pub fn windows(mut self) -> Self { + self.os = NodeJsOs::Windows; self } @@ -131,11 +131,11 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::new("20.6.1").aix(); + /// use node_js_release_info::NodeJsRelInfo; + /// let info = NodeJsRelInfo::new("24.19.0").aix(); /// ``` - pub fn aix(&mut self) -> &mut Self { - self.os = NodeJSOS::AIX; + pub fn aix(mut self) -> Self { + self.os = NodeJsOs::Aix; self } @@ -144,11 +144,11 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::new("20.6.1").x64(); + /// use node_js_release_info::NodeJsRelInfo; + /// let info = NodeJsRelInfo::new("24.19.0").x64(); /// ``` - pub fn x64(&mut self) -> &mut Self { - self.arch = NodeJSArch::X64; + pub fn x64(mut self) -> Self { + self.arch = NodeJsArch::X64; self } @@ -157,11 +157,11 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::new("20.6.1").x86(); + /// use node_js_release_info::NodeJsRelInfo; + /// let info = NodeJsRelInfo::new("24.19.0").x86(); /// ``` - pub fn x86(&mut self) -> &mut Self { - self.arch = NodeJSArch::X86; + pub fn x86(mut self) -> Self { + self.arch = NodeJsArch::X86; self } @@ -170,11 +170,11 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::new("20.6.1").arm64(); + /// use node_js_release_info::NodeJsRelInfo; + /// let info = NodeJsRelInfo::new("24.19.0").arm64(); /// ``` - pub fn arm64(&mut self) -> &mut Self { - self.arch = NodeJSArch::ARM64; + pub fn arm64(mut self) -> Self { + self.arch = NodeJsArch::Arm64; self } @@ -183,11 +183,11 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::new("20.6.1").armv7l(); + /// use node_js_release_info::NodeJsRelInfo; + /// let info = NodeJsRelInfo::new("24.19.0").armv7l(); /// ``` - pub fn armv7l(&mut self) -> &mut Self { - self.arch = NodeJSArch::ARMV7L; + pub fn armv7l(mut self) -> Self { + self.arch = NodeJsArch::Armv7l; self } @@ -196,11 +196,11 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::new("20.6.1").ppc64(); + /// use node_js_release_info::NodeJsRelInfo; + /// let info = NodeJsRelInfo::new("24.19.0").ppc64(); /// ``` - pub fn ppc64(&mut self) -> &mut Self { - self.arch = NodeJSArch::PPC64; + pub fn ppc64(mut self) -> Self { + self.arch = NodeJsArch::Ppc64; self } @@ -209,11 +209,11 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::new("20.6.1").ppc64le(); + /// use node_js_release_info::NodeJsRelInfo; + /// let info = NodeJsRelInfo::new("24.19.0").ppc64le(); /// ``` - pub fn ppc64le(&mut self) -> &mut Self { - self.arch = NodeJSArch::PPC64LE; + pub fn ppc64le(mut self) -> Self { + self.arch = NodeJsArch::Ppc64le; self } @@ -222,11 +222,11 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::new("20.6.1").s390x(); + /// use node_js_release_info::NodeJsRelInfo; + /// let info = NodeJsRelInfo::new("24.19.0").s390x(); /// ``` - pub fn s390x(&mut self) -> &mut Self { - self.arch = NodeJSArch::S390X; + pub fn s390x(mut self) -> Self { + self.arch = NodeJsArch::S390x; self } @@ -235,11 +235,11 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::new("20.6.1").tar_gz(); + /// use node_js_release_info::NodeJsRelInfo; + /// let info = NodeJsRelInfo::new("24.19.0").tar_gz(); /// ``` - pub fn tar_gz(&mut self) -> &mut Self { - self.ext = NodeJSPkgExt::Targz; + pub fn tar_gz(mut self) -> Self { + self.ext = NodeJsPkgExt::Targz; self } @@ -248,11 +248,11 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::new("20.6.1").tar_xz(); + /// use node_js_release_info::NodeJsRelInfo; + /// let info = NodeJsRelInfo::new("24.19.0").tar_xz(); /// ``` - pub fn tar_xz(&mut self) -> &mut Self { - self.ext = NodeJSPkgExt::Tarxz; + pub fn tar_xz(mut self) -> Self { + self.ext = NodeJsPkgExt::Tarxz; self } @@ -261,11 +261,11 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::new("20.6.1").zip(); + /// use node_js_release_info::NodeJsRelInfo; + /// let info = NodeJsRelInfo::new("24.19.0").zip(); /// ``` - pub fn zip(&mut self) -> &mut Self { - self.ext = NodeJSPkgExt::Zip; + pub fn zip(mut self) -> Self { + self.ext = NodeJsPkgExt::Zip; self } @@ -274,11 +274,11 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::new("20.6.1").s7z(); + /// use node_js_release_info::NodeJsRelInfo; + /// let info = NodeJsRelInfo::new("24.19.0").s7z(); /// ``` - pub fn s7z(&mut self) -> &mut Self { - self.ext = NodeJSPkgExt::S7z; + pub fn s7z(mut self) -> Self { + self.ext = NodeJsPkgExt::S7z; self } @@ -287,59 +287,62 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::new("20.6.1").msi(); + /// use node_js_release_info::NodeJsRelInfo; + /// let info = NodeJsRelInfo::new("24.19.0").msi(); /// ``` - pub fn msi(&mut self) -> &mut Self { - self.ext = NodeJSPkgExt::Msi; + pub fn msi(mut self) -> Self { + self.ext = NodeJsPkgExt::Msi; self } /// Creates owned data from reference for convenience when chaining /// - /// # Examples - /// - /// ```rust - /// use node_js_release_info::NodeJSRelInfo; - /// let info = NodeJSRelInfo::new("20.6.1").windows().x64().zip().to_owned(); - /// ``` - pub fn to_owned(&self) -> Self { - self.clone() - } - /// Fetches Node.js metadata for specified configuration from the /// [releases download server](https://nodejs.org/download/release/) /// + /// # Errors + /// + /// Returns [`InvalidVersion`](NodeJsRelInfoError::InvalidVersion) when + /// `version` is not valid semver, + /// [`UnrecognizedVersion`](NodeJsRelInfoError::UnrecognizedVersion) when + /// the release does not exist, + /// [`UnrecognizedConfiguration`](NodeJsRelInfoError::UnrecognizedConfiguration) + /// when the release exists but ships no such os/arch/ext combination, and + /// [`HttpError`](NodeJsRelInfoError::HttpError) when the request fails + /// /// # Examples /// /// ```rust - /// use node_js_release_info::{NodeJSRelInfo, NodeJSRelInfoError}; + /// use node_js_release_info::{NodeJsRelInfo, NodeJsRelInfoError}; /// /// #[tokio::main] - /// async fn main() -> Result<(), NodeJSRelInfoError> { - /// let info = NodeJSRelInfo::new("20.6.1").macos().arm64().fetch().await?; - /// assert_eq!(info.version, "20.6.1"); - /// assert_eq!(info.filename, "node-v20.6.1-darwin-arm64.tar.gz"); - /// assert_eq!(info.sha256, "d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46"); - /// assert_eq!(info.url, "https://nodejs.org/download/release/v20.6.1/node-v20.6.1-darwin-arm64.tar.gz"); + /// async fn main() -> Result<(), NodeJsRelInfoError> { + /// let info = NodeJsRelInfo::new("24.19.0").macos().arm64().fetch().await?; + /// assert_eq!(info.version, "24.19.0"); + /// assert_eq!(info.filename, "node-v24.19.0-darwin-arm64.tar.gz"); + /// assert_eq!(info.sha256, "8294b7aa9b03997481c06babf1e8b270c859358f27da57a11509afe537ac381d"); + /// assert_eq!(info.url, "https://nodejs.org/download/release/v24.19.0/node-v24.19.0-darwin-arm64.tar.gz"); /// Ok(()) /// } /// ``` - pub async fn fetch(&mut self) -> Result { + pub async fn fetch(mut self) -> Result { let version = specs::validate_version(self.version.as_str())?; let specs = specs::fetch(&version, &self.url_fmt).await?; let filename = self.filename(); let info = specs.lines().find(|&line| line.contains(filename.as_str())); - let mut specs = match info { - None => return Err(NodeJSRelInfoError::UnrecognizedConfiguration(filename))?, - Some(s) => s.split_whitespace(), + let Some(line) = info else { + return Err(NodeJsRelInfoError::UnrecognizedConfiguration(filename)); + }; + + let Some(sha256) = line.split_whitespace().next() else { + return Err(NodeJsRelInfoError::UnrecognizedConfiguration(filename)); }; self.filename = filename; - self.sha256 = specs.nth(0).unwrap().to_string(); + self.sha256 = sha256.to_string(); self.url = self.url_fmt.pkg(&self.version, &self.filename); - Ok(self.to_owned()) + Ok(self) } /// Fetches Node.js metadata for all supported configurations from the @@ -348,34 +351,34 @@ impl NodeJSRelInfo { /// # Examples /// /// ```rust - /// use node_js_release_info::{NodeJSRelInfo, NodeJSRelInfoError}; + /// use node_js_release_info::{NodeJsRelInfo, NodeJsRelInfoError}; /// /// #[tokio::main] - /// async fn main() -> Result<(), NodeJSRelInfoError> { - /// let info = NodeJSRelInfo::new("20.6.1"); + /// async fn main() -> Result<(), NodeJsRelInfoError> { + /// let info = NodeJsRelInfo::new("24.19.0"); /// let all = info.fetch_all().await?; - /// assert_eq!(all.len(), 24); - /// assert_eq!(all[2].version, "20.6.1"); - /// assert_eq!(all[2].filename, "node-v20.6.1-darwin-arm64.tar.gz"); - /// assert_eq!(all[2].sha256, "d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46"); - /// assert_eq!(all[2].url, "https://nodejs.org/download/release/v20.6.1/node-v20.6.1-darwin-arm64.tar.gz"); + /// assert_eq!(all.len(), 19); + /// assert_eq!(all[2].version, "24.19.0"); + /// assert_eq!(all[2].filename, "node-v24.19.0-darwin-arm64.tar.gz"); + /// assert_eq!(all[2].sha256, "8294b7aa9b03997481c06babf1e8b270c859358f27da57a11509afe537ac381d"); + /// assert_eq!(all[2].url, "https://nodejs.org/download/release/v24.19.0/node-v24.19.0-darwin-arm64.tar.gz"); /// Ok(()) /// } /// ``` - pub async fn fetch_all(&self) -> Result, NodeJSRelInfoError> { + pub async fn fetch_all(&self) -> Result, NodeJsRelInfoError> { let version = specs::validate_version(self.version.as_str())?; let specs = specs::fetch(&version, &self.url_fmt).await?; let specs = match specs::parse(&version, specs) { Some(s) => s, None => { - return Err(NodeJSRelInfoError::UnrecognizedVersion(version.clone())); + return Err(NodeJsRelInfoError::UnrecognizedVersion(version.clone())); } }; - let mut all: Vec = vec![]; + let mut all: Vec = vec![]; for (os, arch, ext, sha256, filename) in specs.into_iter() { let version = version.clone(); - let mut info = NodeJSRelInfo { + let mut info = NodeJsRelInfo { os, arch, version, @@ -396,7 +399,7 @@ impl NodeJSRelInfo { let arch = self.arch.to_string(); let ext = self.ext.to_string(); - if self.ext == NodeJSPkgExt::Msi { + if self.ext == NodeJsPkgExt::Msi { return format!("node-v{}-{}.{}", self.version, arch, ext); } @@ -415,23 +418,23 @@ mod tests { #[test] fn it_initializes() { - let info = NodeJSRelInfo::new("1.0.0"); - assert_eq!(info.os, NodeJSOS::Linux); - assert_eq!(info.arch, NodeJSArch::X64); - assert_eq!(info.ext, NodeJSPkgExt::Targz); + let info = NodeJsRelInfo::new("1.0.0"); + assert_eq!(info.os, NodeJsOs::Linux); + assert_eq!(info.arch, NodeJsArch::X64); + assert_eq!(info.ext, NodeJsPkgExt::Targz); assert_eq!(info.version, "1.0.0".to_string()); assert_eq!(info.filename, "".to_string()); assert_eq!(info.sha256, "".to_string()); assert_eq!(info.url, "".to_string()); - is_thread_safe::(); + is_thread_safe::(); } #[test] fn it_initializes_with_defaults() { - let info = NodeJSRelInfo::default(); - assert_eq!(info.os, NodeJSOS::Linux); - assert_eq!(info.arch, NodeJSArch::X64); - assert_eq!(info.ext, NodeJSPkgExt::Targz); + let info = NodeJsRelInfo::default(); + assert_eq!(info.os, NodeJsOs::Linux); + assert_eq!(info.arch, NodeJsArch::X64); + assert_eq!(info.ext, NodeJsPkgExt::Targz); assert_eq!(info.version, "".to_string()); assert_eq!(info.filename, "".to_string()); assert_eq!(info.sha256, "".to_string()); @@ -441,124 +444,76 @@ mod tests { #[test] #[cfg_attr(not(target_os = "macos"), ignore)] fn it_initializes_using_current_environment_on_macos() { - let info = NodeJSRelInfo::from_env("1.0.0").unwrap(); - assert_eq!(info.ext, NodeJSPkgExt::Targz); + let info = NodeJsRelInfo::from_env("1.0.0").unwrap(); + assert_eq!(info.ext, NodeJsPkgExt::Targz); } #[test] #[cfg_attr(not(target_os = "linux"), ignore)] fn it_initializes_using_current_environment_on_linux() { - let info = NodeJSRelInfo::from_env("1.0.0").unwrap(); - assert_eq!(info.ext, NodeJSPkgExt::Targz); + let info = NodeJsRelInfo::from_env("1.0.0").unwrap(); + assert_eq!(info.ext, NodeJsPkgExt::Targz); } #[test] #[cfg_attr(not(target_os = "windows"), ignore)] fn it_initializes_using_current_environment_on_windows() { - let info = NodeJSRelInfo::from_env("1.0.0").unwrap(); - assert_eq!(info.ext, NodeJSPkgExt::Zip); + let info = NodeJsRelInfo::from_env("1.0.0").unwrap(); + assert_eq!(info.ext, NodeJsPkgExt::Zip); } #[test] fn it_sets_os() { - let mut info = NodeJSRelInfo::new("1.0.0"); - - assert_eq!(info.os, NodeJSOS::Linux); + let info = NodeJsRelInfo::new("1.0.0"); - info.windows(); - - assert_eq!(info.os, NodeJSOS::Windows); - - info.macos(); - - assert_eq!(info.os, NodeJSOS::Darwin); - - info.linux(); - - assert_eq!(info.os, NodeJSOS::Linux); - - info.aix(); - - assert_eq!(info.os, NodeJSOS::AIX); + assert_eq!(info.os, NodeJsOs::Linux); + assert_eq!(info.clone().windows().os, NodeJsOs::Windows); + assert_eq!(info.clone().macos().os, NodeJsOs::Darwin); + assert_eq!(info.clone().linux().os, NodeJsOs::Linux); + assert_eq!(info.clone().aix().os, NodeJsOs::Aix); } #[test] fn it_sets_arch() { - let mut info = NodeJSRelInfo::new("1.0.0"); - - info.x86(); - - assert_eq!(info.arch, NodeJSArch::X86); - - info.x64(); + let info = NodeJsRelInfo::new("1.0.0"); - assert_eq!(info.arch, NodeJSArch::X64); - - info.arm64(); - - assert_eq!(info.arch, NodeJSArch::ARM64); - - info.armv7l(); - - assert_eq!(info.arch, NodeJSArch::ARMV7L); - - info.ppc64(); - - assert_eq!(info.arch, NodeJSArch::PPC64); - - info.ppc64le(); - - assert_eq!(info.arch, NodeJSArch::PPC64LE); - - info.s390x(); - - assert_eq!(info.arch, NodeJSArch::S390X); + assert_eq!(info.clone().x86().arch, NodeJsArch::X86); + assert_eq!(info.clone().x64().arch, NodeJsArch::X64); + assert_eq!(info.clone().arm64().arch, NodeJsArch::Arm64); + assert_eq!(info.clone().armv7l().arch, NodeJsArch::Armv7l); + assert_eq!(info.clone().ppc64().arch, NodeJsArch::Ppc64); + assert_eq!(info.clone().ppc64le().arch, NodeJsArch::Ppc64le); + assert_eq!(info.clone().s390x().arch, NodeJsArch::S390x); } #[test] fn it_sets_ext() { - let mut info = NodeJSRelInfo::new("1.0.0"); - - info.zip(); + let info = NodeJsRelInfo::new("1.0.0"); - assert_eq!(info.ext, NodeJSPkgExt::Zip); - - info.tar_gz(); - - assert_eq!(info.ext, NodeJSPkgExt::Targz); - - info.tar_xz(); - - assert_eq!(info.ext, NodeJSPkgExt::Tarxz); - - info.msi(); - - assert_eq!(info.ext, NodeJSPkgExt::Msi); - - info.s7z(); - - assert_eq!(info.ext, NodeJSPkgExt::S7z); + assert_eq!(info.clone().zip().ext, NodeJsPkgExt::Zip); + assert_eq!(info.clone().tar_gz().ext, NodeJsPkgExt::Targz); + assert_eq!(info.clone().tar_xz().ext, NodeJsPkgExt::Tarxz); + assert_eq!(info.clone().msi().ext, NodeJsPkgExt::Msi); + assert_eq!(info.clone().s7z().ext, NodeJsPkgExt::S7z); } #[test] - fn it_gets_owned_copy() { - let mut info1 = NodeJSRelInfo::new("1.0.0"); - let info2 = info1.to_owned(); + fn it_clones() { + let info1 = NodeJsRelInfo::new("1.0.0"); + let info2 = info1.clone(); assert_eq!(info1, info2); - - info1.windows(); - - assert_ne!(info1, info2); + // builders consume, so the clone is unaffected by further chaining + assert_ne!(info1.windows(), info2); } #[test] fn it_formats_filename() { - let info = NodeJSRelInfo::new("1.0.0").macos().x64().zip().to_owned(); + let info = NodeJsRelInfo::new("1.0.0").macos().x64().zip(); assert_eq!(info.filename(), "node-v1.0.0-darwin-x64.zip"); - let info = NodeJSRelInfo::new("1.0.0").windows().x64().msi().to_owned(); + let info = NodeJsRelInfo::new("1.0.0").windows().x64().msi(); assert_eq!(info.filename(), "node-v1.0.0-x64.msi"); } @@ -571,10 +526,10 @@ mod tests { let sha256 = "d8ba8018d45b294429b1a7646ccbeaeb2af3cdf45b5c91dabbd93e2a2035cb46".to_string(); let url = "https://nodejs.org/download/release/v20.6.1/node-v20.6.1-darwin-arm64.tar.gz" .to_string(); - let info_orig = NodeJSRelInfo { - os: NodeJSOS::Darwin, - arch: NodeJSArch::ARM64, - ext: NodeJSPkgExt::Targz, + let info_orig = NodeJsRelInfo { + os: NodeJsOs::Darwin, + arch: NodeJsArch::Arm64, + ext: NodeJsPkgExt::Targz, version: version.clone(), filename: filename.clone(), sha256: sha256.clone(), @@ -582,10 +537,10 @@ mod tests { ..Default::default() }; let info_json = serde_json::to_string(&info_orig).unwrap(); - let info: NodeJSRelInfo = serde_json::from_str(&info_json).unwrap(); - assert_eq!(info.os, NodeJSOS::Darwin); - assert_eq!(info.arch, NodeJSArch::ARM64); - assert_eq!(info.ext, NodeJSPkgExt::Targz); + let info: NodeJsRelInfo = serde_json::from_str(&info_json).unwrap(); + assert_eq!(info.os, NodeJsOs::Darwin); + assert_eq!(info.arch, NodeJsArch::Arm64); + assert_eq!(info.ext, NodeJsPkgExt::Targz); assert_eq!(info.version, "20.6.1".to_string()); assert_eq!( info.filename, @@ -604,15 +559,15 @@ mod tests { #[tokio::test] async fn it_fails_to_fetch_info_when_version_is_invalid() { - let mut info = NodeJSRelInfo::new("NOPE!"); + let info = NodeJsRelInfo::new("NOPE!"); let err = info.fetch().await.unwrap_err(); - assert!(matches!(err, NodeJSRelInfoError::InvalidVersion(x) if x == "NOPE!")); + assert!(matches!(err, NodeJsRelInfoError::InvalidVersion(x) if x == "NOPE!")); } #[tokio::test] async fn it_fails_to_fetch_info_when_version_is_unrecognized() { - let mut info = NodeJSRelInfo::new("1.0.0"); + let mut info = NodeJsRelInfo::new("1.0.0"); let mut server = Server::new_async().await; let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server) .with_body(specs::get_fake_specs()) @@ -623,13 +578,13 @@ mod tests { let err = info.fetch().await.unwrap_err(); mock.assert_async().await; - assert!(matches!(err, NodeJSRelInfoError::UnrecognizedVersion(x) if x == "1.0.0")); + assert!(matches!(err, NodeJsRelInfoError::UnrecognizedVersion(x) if x == "1.0.0")); } #[tokio::test] async fn it_fails_to_fetch_info_when_configuration_is_unrecognized() { let mut server = Server::new_async().await; - let mut info = NodeJSRelInfo::new("20.6.1").linux().zip().to_owned(); + let mut info = NodeJsRelInfo::new("20.6.1").linux().zip(); let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server) .with_body(specs::get_fake_specs()) .create_async() @@ -639,20 +594,20 @@ mod tests { mock.assert_async().await; assert!( - matches!(err, NodeJSRelInfoError::UnrecognizedConfiguration(x) if x == "node-v20.6.1-linux-x64.zip") + matches!(err, NodeJsRelInfoError::UnrecognizedConfiguration(x) if x == "node-v20.6.1-linux-x64.zip") ); } #[tokio::test] async fn it_fetches_node_js_release_info() { - let mut info = NodeJSRelInfo::new("20.6.1"); + let mut info = NodeJsRelInfo::new("20.6.1"); let mut server = Server::new_async().await; let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server) .with_body(specs::get_fake_specs()) .create_async() .await; - info.fetch().await.unwrap(); + let info = info.fetch().await.unwrap(); mock.assert_async().await; assert_eq!(info.filename, "node-v20.6.1-linux-x64.tar.gz"); @@ -672,14 +627,14 @@ mod tests { #[tokio::test] async fn it_fetches_node_js_release_info_when_ext_is_msi() { - let mut info = NodeJSRelInfo::new("20.6.1").arm64().msi().to_owned(); + let mut info = NodeJsRelInfo::new("20.6.1").arm64().msi(); let mut server = Server::new_async().await; let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server) .with_body(specs::get_fake_specs()) .create_async() .await; - info.fetch().await.unwrap(); + let info = info.fetch().await.unwrap(); mock.assert_async().await; assert_eq!(info.filename, "node-v20.6.1-arm64.msi"); @@ -699,7 +654,7 @@ mod tests { #[tokio::test] async fn it_fetches_all_supported_node_js_configurations() { - let mut info = NodeJSRelInfo::new("20.6.1"); + let mut info = NodeJsRelInfo::new("20.6.1"); let mut server = Server::new_async().await; let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server) .with_body(specs::get_fake_specs()) @@ -711,9 +666,9 @@ mod tests { assert_eq!(all.len(), 24); assert_eq!(all[2].version, "20.6.1"); - assert_eq!(all[2].os, NodeJSOS::Darwin); - assert_eq!(all[2].arch, NodeJSArch::ARM64); - assert_eq!(all[2].ext, NodeJSPkgExt::Targz); + assert_eq!(all[2].os, NodeJsOs::Darwin); + assert_eq!(all[2].arch, NodeJsArch::Arm64); + assert_eq!(all[2].ext, NodeJsPkgExt::Targz); assert_eq!(all[2].filename, "node-v20.6.1-darwin-arm64.tar.gz"); assert_eq!( all[2].sha256, @@ -727,7 +682,7 @@ mod tests { #[tokio::test] async fn it_fails_to_fetch_all_supported_node_js_configurations_when_version_is_unrecognized() { - let mut info = NodeJSRelInfo::new("1.0.0"); + let mut info = NodeJsRelInfo::new("1.0.0"); let mut server = Server::new_async().await; let mock = specs::setup_server_mock(&info.version, &mut info.url_fmt, &mut server) .with_body(String::from("")) @@ -737,6 +692,6 @@ mod tests { let err = info.fetch_all().await.unwrap_err(); mock.assert_async().await; - assert!(matches!(err, NodeJSRelInfoError::UnrecognizedVersion(x) if x == "1.0.0")); + assert!(matches!(err, NodeJsRelInfoError::UnrecognizedVersion(x) if x == "1.0.0")); } } diff --git a/crates/node-js-release-info/src/os.rs b/crates/node-js-release-info/src/os.rs index d2dbdab..eff1447 100644 --- a/crates/node-js-release-info/src/os.rs +++ b/crates/node-js-release-info/src/os.rs @@ -1,14 +1,18 @@ -use crate::error::NodeJSRelInfoError; +use crate::error::NodeJsRelInfoError; #[cfg(feature = "json")] use serde::{Deserialize, Serialize}; use std::env::consts::OS; use std::fmt::{Display, Formatter}; use std::str::FromStr; -#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "json", derive(Deserialize, Serialize))] /// The operating system a Node.js distributable targets -pub enum NodeJSOS { +/// +/// Non-exhaustive: Node.js has added and removed target platforms over time, +/// so new variants may appear in a minor release +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "json", derive(Deserialize, Serialize))] +#[non_exhaustive] +pub enum NodeJsOs { /// Linux (`linux`) #[default] #[cfg_attr(feature = "json", serde(rename = "linux"))] @@ -21,20 +25,23 @@ pub enum NodeJSOS { Windows, /// IBM AIX (`aix`) #[cfg_attr(feature = "json", serde(rename = "aix"))] - AIX, + Aix, + /// illumos / Solaris (`sunos`) - shipped up to Node.js v14 + #[cfg_attr(feature = "json", serde(rename = "sunos"))] + SunOs, } -impl NodeJSOS { - /// Creates a new instance using the default OS ([`Linux`](NodeJSOS::Linux)) +impl NodeJsOs { + /// Creates a new instance using the default OS ([`Linux`](NodeJsOs::Linux)) /// /// # Examples /// /// ```rust - /// use node_js_release_info::NodeJSOS; - /// assert_eq!(NodeJSOS::new(), NodeJSOS::Linux); + /// use node_js_release_info::NodeJsOs; + /// assert_eq!(NodeJsOs::new(), NodeJsOs::Linux); /// ``` - pub fn new() -> NodeJSOS { - NodeJSOS::default() + pub fn new() -> NodeJsOs { + NodeJsOs::default() } /// Determines the OS of the current environment via @@ -42,36 +49,38 @@ impl NodeJSOS { /// /// # Errors /// - /// Returns [`NodeJSRelInfoError::UnrecognizedOs`] when the current OS has + /// Returns [`NodeJsRelInfoError::UnrecognizedOs`] when the current OS has /// no corresponding Node.js distributable - pub fn from_env() -> Result { - NodeJSOS::from_str(OS) + pub fn from_env() -> Result { + NodeJsOs::from_str(OS) } } -impl Display for NodeJSOS { +impl Display for NodeJsOs { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let os = match self { - NodeJSOS::Linux => "linux", - NodeJSOS::Darwin => "darwin", - NodeJSOS::Windows => "win", - NodeJSOS::AIX => "aix", + NodeJsOs::Linux => "linux", + NodeJsOs::Darwin => "darwin", + NodeJsOs::Windows => "win", + NodeJsOs::Aix => "aix", + NodeJsOs::SunOs => "sunos", }; write!(f, "{os}") } } -impl FromStr for NodeJSOS { - type Err = NodeJSRelInfoError; +impl FromStr for NodeJsOs { + type Err = NodeJsRelInfoError; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { match s { - "linux" => Ok(NodeJSOS::Linux), - "darwin" | "macos" => Ok(NodeJSOS::Darwin), - "windows" | "win" => Ok(NodeJSOS::Windows), - "aix" => Ok(NodeJSOS::AIX), - _ => Err(NodeJSRelInfoError::UnrecognizedOs(s.to_string())), + "linux" => Ok(NodeJsOs::Linux), + "darwin" | "macos" => Ok(NodeJsOs::Darwin), + "windows" | "win" => Ok(NodeJsOs::Windows), + "sunos" | "solaris" | "illumos" => Ok(NodeJsOs::SunOs), + "aix" => Ok(NodeJsOs::Aix), + _ => Err(NodeJsRelInfoError::UnrecognizedOs(s.to_string())), } } } @@ -82,78 +91,90 @@ mod tests { #[test] fn it_initializes() { - let os = NodeJSOS::new(); - assert_eq!(os, NodeJSOS::Linux); + let os = NodeJsOs::new(); + assert_eq!(os, NodeJsOs::Linux); } #[test] fn it_initializes_with_defaults() { - let os = NodeJSOS::default(); - assert_eq!(os, NodeJSOS::Linux); + let os = NodeJsOs::default(); + assert_eq!(os, NodeJsOs::Linux); } #[test] fn it_initializes_from_str() { - let os = NodeJSOS::from_str("linux").unwrap(); + let os = NodeJsOs::from_str("linux").unwrap(); + + assert_eq!(os, NodeJsOs::Linux); + + let os = NodeJsOs::from_str("darwin").unwrap(); - assert_eq!(os, NodeJSOS::Linux); + assert_eq!(os, NodeJsOs::Darwin); - let os = NodeJSOS::from_str("darwin").unwrap(); + let os = NodeJsOs::from_str("macos").unwrap(); - assert_eq!(os, NodeJSOS::Darwin); + assert_eq!(os, NodeJsOs::Darwin); - let os = NodeJSOS::from_str("macos").unwrap(); + let os = NodeJsOs::from_str("windows").unwrap(); - assert_eq!(os, NodeJSOS::Darwin); + assert_eq!(os, NodeJsOs::Windows); - let os = NodeJSOS::from_str("windows").unwrap(); + let os = NodeJsOs::from_str("win").unwrap(); - assert_eq!(os, NodeJSOS::Windows); + assert_eq!(os, NodeJsOs::Windows); - let os = NodeJSOS::from_str("win").unwrap(); + let os = NodeJsOs::from_str("aix").unwrap(); - assert_eq!(os, NodeJSOS::Windows); + assert_eq!(os, NodeJsOs::Aix); - let os = NodeJSOS::from_str("aix").unwrap(); + let os = NodeJsOs::from_str("sunos").unwrap(); - assert_eq!(os, NodeJSOS::AIX); + assert_eq!(os, NodeJsOs::SunOs); + + let os = NodeJsOs::from_str("solaris").unwrap(); + + assert_eq!(os, NodeJsOs::SunOs); } #[test] fn it_serializes_to_str() { - let text = format!("{}", NodeJSOS::Linux); + let text = format!("{}", NodeJsOs::Linux); assert_eq!(text, "linux"); - let text = format!("{}", NodeJSOS::Darwin); + let text = format!("{}", NodeJsOs::Darwin); assert_eq!(text, "darwin"); - let text = format!("{}", NodeJSOS::Windows); + let text = format!("{}", NodeJsOs::Windows); assert_eq!(text, "win"); - let text = format!("{}", NodeJSOS::AIX); + let text = format!("{}", NodeJsOs::Aix); assert_eq!(text, "aix"); + + let text = format!("{}", NodeJsOs::SunOs); + + assert_eq!(text, "sunos"); } #[test] fn it_initializes_using_current_environment() { - NodeJSOS::from_env().unwrap(); + NodeJsOs::from_env().unwrap(); } #[test] fn it_fails_when_os_cannot_be_determined_from_str() { - let err = NodeJSOS::from_str("NOPE!").unwrap_err(); - assert!(matches!(err, NodeJSRelInfoError::UnrecognizedOs(x) if x == "NOPE!")); + let err = NodeJsOs::from_str("NOPE!").unwrap_err(); + assert!(matches!(err, NodeJsRelInfoError::UnrecognizedOs(x) if x == "NOPE!")); } #[test] #[cfg(feature = "json")] fn it_serializes_and_deserializes() { - let os_json = serde_json::to_string(&NodeJSOS::Darwin).unwrap(); - let os: NodeJSOS = serde_json::from_str(&os_json).unwrap(); - assert_eq!(os, NodeJSOS::Darwin); + let os_json = serde_json::to_string(&NodeJsOs::Darwin).unwrap(); + let os: NodeJsOs = serde_json::from_str(&os_json).unwrap(); + assert_eq!(os, NodeJsOs::Darwin); } } diff --git a/crates/node-js-release-info/src/specs.rs b/crates/node-js-release-info/src/specs.rs index 74d9c2e..6218726 100644 --- a/crates/node-js-release-info/src/specs.rs +++ b/crates/node-js-release-info/src/specs.rs @@ -1,15 +1,15 @@ -use crate::arch::NodeJSArch; -use crate::error::NodeJSRelInfoError; -use crate::ext::NodeJSPkgExt; -use crate::os::NodeJSOS; -use crate::url::NodeJSURLFormatter; +use crate::arch::NodeJsArch; +use crate::error::NodeJsRelInfoError; +use crate::ext::NodeJsPkgExt; +use crate::os::NodeJsOs; +use crate::url::NodeJsUrlFormatter; use semver::Version; use std::str::FromStr; -pub(crate) fn validate_version>(semver: T) -> Result { +pub(crate) fn validate_version>(semver: T) -> Result { match Version::parse(semver.as_ref()) { Ok(v) => Ok(v.to_string()), - Err(_) => Err(NodeJSRelInfoError::InvalidVersion( + Err(_) => Err(NodeJsRelInfoError::InvalidVersion( semver.as_ref().to_owned(), )), } @@ -17,26 +17,26 @@ pub(crate) fn validate_version>(semver: T) -> Result Result { + url_fmt: &NodeJsUrlFormatter, +) -> Result { let info_url = url_fmt.info(version); let res = match reqwest::get(info_url.as_str()).await { - Err(e) => return Err(NodeJSRelInfoError::HttpError(e)), + Err(e) => return Err(NodeJsRelInfoError::HttpError(e)), Ok(r) => r, }; // TODO (busticated): handle 5xx errors if res.status().as_u16() >= 400 { - return Err(NodeJSRelInfoError::UnrecognizedVersion(version.clone())); + return Err(NodeJsRelInfoError::UnrecognizedVersion(version.clone())); } match res.text().await { - Err(e) => Err(NodeJSRelInfoError::HttpError(e)), + Err(e) => Err(NodeJsRelInfoError::HttpError(e)), Ok(b) => Ok(b), } } -pub(crate) type ParsedSpecs = Vec<(NodeJSOS, NodeJSArch, NodeJSPkgExt, String, String)>; +pub(crate) type ParsedSpecs = Vec<(NodeJsOs, NodeJsArch, NodeJsPkgExt, String, String)>; pub(crate) fn parse(version: &String, specs: String) -> Option { let mut all: ParsedSpecs = vec![]; @@ -63,7 +63,7 @@ pub(crate) fn parse(version: &String, specs: String) -> Option { } let os = if is_msi { "win" } else { parts[2] }; - let os = match NodeJSOS::from_str(os) { + let os = match NodeJsOs::from_str(os) { Ok(os) => os, Err(_) => { continue; @@ -77,14 +77,14 @@ pub(crate) fn parse(version: &String, specs: String) -> Option { } }; - let arch = match NodeJSArch::from_str(arch) { + let arch = match NodeJsArch::from_str(arch) { Ok(a) => a, Err(_) => { continue; } }; - let ext = match NodeJSPkgExt::from_str(ext) { + let ext = match NodeJsPkgExt::from_str(ext) { Ok(ext) => ext, Err(_) => { continue; @@ -109,7 +109,7 @@ use mockito::{Mock, Server}; #[cfg(test)] pub(crate) fn setup_server_mock( version: &str, - url_fmt: &mut NodeJSURLFormatter, + url_fmt: &mut NodeJsUrlFormatter, server: &mut Server, ) -> Mock { url_fmt.host = server.host_with_port(); @@ -169,9 +169,9 @@ mod tests { fn assert_is_darwin_arm64_targz_specs(specs: ParsedSpecs) { assert_eq!(specs.len(), 1); let (os, arch, ext, sha256, filename) = &specs[0]; - assert_eq!(*os, NodeJSOS::Darwin); - assert_eq!(*arch, NodeJSArch::ARM64); - assert_eq!(*ext, NodeJSPkgExt::Targz); + assert_eq!(*os, NodeJsOs::Darwin); + assert_eq!(*arch, NodeJsArch::Arm64); + assert_eq!(*ext, NodeJsPkgExt::Targz); assert_eq!(filename, "node-v20.6.1-darwin-arm64.tar.gz"); assert_eq!(sha256, "FAKESHA"); } @@ -184,14 +184,11 @@ mod tests { let error = validate_version("NOPE").unwrap_err(); - assert_eq!( - format!("{error}"), - "Error: Invalid Version! Received: 'NOPE'" - ); + assert_eq!(format!("{error}"), "invalid version - received: 'NOPE'"); let error = validate_version("").unwrap_err(); - assert_eq!(format!("{error}"), "Error: Invalid Version! Received: ''"); + assert_eq!(format!("{error}"), "invalid version - received: ''"); } #[test] @@ -201,9 +198,9 @@ mod tests { let specs = parse(&version, specs_raw).unwrap(); assert_eq!(specs.len(), 24); let (os, arch, ext, sha256, filename) = &specs[2]; - assert_eq!(*os, NodeJSOS::Darwin); - assert_eq!(*arch, NodeJSArch::ARM64); - assert_eq!(*ext, NodeJSPkgExt::Targz); + assert_eq!(*os, NodeJsOs::Darwin); + assert_eq!(*arch, NodeJsArch::Arm64); + assert_eq!(*ext, NodeJsPkgExt::Targz); assert_eq!(filename, "node-v20.6.1-darwin-arm64.tar.gz"); assert_eq!( sha256, @@ -306,7 +303,7 @@ mod tests { #[tokio::test] async fn it_fetches_node_js_specs() { let version = String::from("20.6.1"); - let mut url_fmt = NodeJSURLFormatter::new(); + let mut url_fmt = NodeJsUrlFormatter::new(); let mut server = Server::new_async().await; let mock = setup_server_mock(&version, &mut url_fmt, &mut server) .with_body(get_fake_specs()) @@ -321,7 +318,7 @@ mod tests { #[tokio::test] async fn it_fails_to_fetch_node_js_specs_when_version_is_unrecognized() { let version = String::from("1.0.0"); - let mut url_fmt = NodeJSURLFormatter::new(); + let mut url_fmt = NodeJsUrlFormatter::new(); let mut server = Server::new_async().await; let mock = setup_server_mock(&version, &mut url_fmt, &mut server) .with_body(get_fake_specs()) @@ -332,6 +329,6 @@ mod tests { let err = fetch(&version, &url_fmt).await.unwrap_err(); mock.assert_async().await; - assert!(matches!(err, NodeJSRelInfoError::UnrecognizedVersion(x) if x == "1.0.0")); + assert!(matches!(err, NodeJsRelInfoError::UnrecognizedVersion(x) if x == "1.0.0")); } } diff --git a/crates/node-js-release-info/src/url.rs b/crates/node-js-release-info/src/url.rs index f319e34..f119e80 100644 --- a/crates/node-js-release-info/src/url.rs +++ b/crates/node-js-release-info/src/url.rs @@ -1,19 +1,19 @@ #[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub(crate) struct NodeJSURLFormatter { +pub(crate) struct NodeJsUrlFormatter { pub protocol: String, pub host: String, pub pathname: String, } -impl Default for NodeJSURLFormatter { +impl Default for NodeJsUrlFormatter { fn default() -> Self { - NodeJSURLFormatter::new() + NodeJsUrlFormatter::new() } } -impl NodeJSURLFormatter { - pub(crate) fn new() -> NodeJSURLFormatter { - NodeJSURLFormatter { +impl NodeJsUrlFormatter { + pub(crate) fn new() -> NodeJsUrlFormatter { + NodeJsUrlFormatter { protocol: String::from("https:"), host: String::from("nodejs.org"), pathname: String::from("/download/release"), @@ -66,7 +66,7 @@ mod tests { #[test] fn it_initializes() { - let url_fmt = NodeJSURLFormatter::new(); + let url_fmt = NodeJsUrlFormatter::new(); assert_eq!(url_fmt.protocol, "https:"); assert_eq!(url_fmt.host, "nodejs.org"); assert_eq!(url_fmt.pathname, "/download/release"); @@ -74,13 +74,13 @@ mod tests { #[test] fn it_initializes_with_defaults() { - let url_fmt = NodeJSURLFormatter::default(); - assert_eq!(url_fmt, NodeJSURLFormatter::new()); + let url_fmt = NodeJsUrlFormatter::default(); + assert_eq!(url_fmt, NodeJsUrlFormatter::new()); } #[test] fn it_formats_url_for_node_js_release_info() { - let url_fmt = NodeJSURLFormatter::new(); + let url_fmt = NodeJsUrlFormatter::new(); assert_eq!( url_fmt.info("1.0.0"), "https://nodejs.org/download/release/v1.0.0/SHASUMS256.txt" @@ -89,7 +89,7 @@ mod tests { #[test] fn it_formats_url_for_node_js_package() { - let url_fmt = NodeJSURLFormatter::new(); + let url_fmt = NodeJsUrlFormatter::new(); assert_eq!( url_fmt.pkg("1.0.0", "fake-filename"), "https://nodejs.org/download/release/v1.0.0/fake-filename" diff --git a/crates/node-js-release-info/tests/integration.rs b/crates/node-js-release-info/tests/integration.rs index 11530b9..792ef4d 100644 --- a/crates/node-js-release-info/tests/integration.rs +++ b/crates/node-js-release-info/tests/integration.rs @@ -4,14 +4,17 @@ use node_js_release_info::*; -const VERSION: &str = "20.7.0"; +const VERSION: &str = "24.19.0"; +const DARWIN_X64_URL: &str = + "https://nodejs.org/download/release/v24.19.0/node-v24.19.0-darwin-x64.tar.gz"; +const DARWIN_X64_SHA256: &str = "d1b5e999db158c62fe8f7267a4476b035d8bd93b1a605bac24a3f0dd166e3316"; #[test] fn it_provides_expected_resources() { - let info = NodeJSRelInfo::new(VERSION); - let os = NodeJSOS::Linux; - let arch = NodeJSArch::X64; - let ext = NodeJSPkgExt::Targz; + let info = NodeJsRelInfo::new(VERSION); + let os = NodeJsOs::Linux; + let arch = NodeJsArch::X64; + let ext = NodeJsPkgExt::Targz; assert_eq!(info.version, VERSION); assert_eq!(info.os, os); assert_eq!(info.arch, arch); @@ -20,29 +23,26 @@ fn it_provides_expected_resources() { #[tokio::test] async fn it_fetches_node_js_release_info_for_a_given_configuration() { - let mut info = NodeJSRelInfo::new(VERSION); - let result = info.macos().x64().tar_gz().fetch().await.unwrap(); - assert_eq!( - result.url, - "https://nodejs.org/download/release/v20.7.0/node-v20.7.0-darwin-x64.tar.gz" - ); - assert_eq!( - result.sha256, - "ceeba829f44e7573949f2ce2ad5def27f1d6daa55f2860bea82964851fae01bc" - ); + let result = NodeJsRelInfo::new(VERSION) + .macos() + .x64() + .tar_gz() + .fetch() + .await + .unwrap(); + + assert_eq!(result.url, DARWIN_X64_URL); + assert_eq!(result.sha256, DARWIN_X64_SHA256); } #[tokio::test] async fn it_fetches_node_js_release_info_for_all_supported_configurations() { - let info = NodeJSRelInfo::new(VERSION); + let info = NodeJsRelInfo::new(VERSION); let result = info.fetch_all().await.unwrap(); - assert_eq!(result.len(), 24); - assert_eq!( - result[4].url, - "https://nodejs.org/download/release/v20.7.0/node-v20.7.0-darwin-x64.tar.gz" - ); - assert_eq!( - result[4].sha256, - "ceeba829f44e7573949f2ce2ad5def27f1d6daa55f2860bea82964851fae01bc" - ); + + // NOTE: v24 dropped `linux-armv7l` and all three 32-bit Windows builds, + // taking the recognized configuration count from 24 (as of v20) down to 19 + assert_eq!(result.len(), 19); + assert_eq!(result[4].url, DARWIN_X64_URL); + assert_eq!(result[4].sha256, DARWIN_X64_SHA256); } From 09583030dd8d7c81b5e50ce2f1910c5a2b5ea2d0 Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 12 Aug 2026 19:47:58 -0700 Subject: [PATCH 28/31] cargo xtask crate:release now runs the semver check --- .github/workflows/test.yaml | 23 +++-------------------- AGENTS.md | 3 ++- README.md | 6 ++++-- xtask/src/main.rs | 15 +++++++++++++++ 4 files changed, 24 insertions(+), 23 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9a49dce..f3796f7 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -34,6 +34,9 @@ jobs: # NOTE: these are `install-action` tool names, not crate names - they # match one-to-one here except `typos`, whose crate is `typos-cli` (the # name `cargo xtask setup` installs) and whose binary is `typos` + # NOTE: `cargo-semver-checks` is listed even though no job here runs it - + # `setup` installs it for `crate:release`, and fetching the prebuilt + # binary is far cheaper than letting `setup` build it from source - name: Install Prebuilt Tooling uses: taiki-e/install-action@v2 with: @@ -59,26 +62,6 @@ jobs: - name: Check Advisories, Licenses & Bans run: cargo deny --all-features check - semver: - name: Check for Breaking API Changes - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Checkout Source Code - uses: actions/checkout@v7 - - name: Install Rust Toolchain (stable) - uses: dtolnay/rust-toolchain@stable - - name: Install cargo-semver-checks - uses: taiki-e/install-action@v2 - with: - tool: cargo-semver-checks - # NOTE: baselines come from the last published version of each crate on - # crates.io. crates marked `publish = false` are skipped automatically. - # this only reports - bumping the version is still a deliberate step via - # `cargo xtask crate:release` - - name: Compare Public API Against Published Baseline - run: cargo semver-checks --workspace - msrv: name: Verify Minimum Supported Rust Version runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index 22498d6..4e5a7af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,8 @@ All local dev commands are `xtask` scripts — run `cargo xtask help` for the fu - `cargo xtask test` — fast inner loop while iterating. - `cargo xtask ci` — run before prompting the user to review. Chains `format --check`, `spellcheck`, `lint` (clippy), `doc --check` (doc tests + rustdoc warnings), and `coverage` (which runs the tests). Report results honestly. -- `cargo xtask audit` and `cargo xtask semver` — dependency audit and public-API breakage check. Both need network access and run as their own CI jobs, so they are **not** part of `cargo xtask ci`. +- `cargo xtask audit` — dependency audit (advisories, licenses, sources). Needs network access and runs as its own CI job, so it is **not** part of `cargo xtask ci`. +- `cargo xtask semver` — checks the public API against the last published version. Versions are bumped at release time, not alongside the code, so breaking changes legitimately sit on `main` un-bumped — this must **not** gate CI. `crate:release` runs it automatically before prompting for the new version. Task naming follows one rule: `--check` is a non-mutating mode of a task that otherwise writes (`format`, `doc`); `name:sub` is a family of distinct operations on one noun (`crate:add`, `crate:list`, ...); everything else is a bare verb. - `cargo xtask crate:add` — the only supported way to add a crate. Never hand-create one under `crates/`. diff --git a/README.md b/README.md index efc8b41..e9e3854 100644 --- a/README.md +++ b/README.md @@ -124,13 +124,15 @@ To check dependencies for security advisories, disallowed licenses, and untruste cargo xtask audit ``` -This runs [cargo-deny](https://github.com/EmbarkStudios/cargo-deny) - policy lives in [deny.toml](deny.toml). To check whether your changes break the public API of an already-published crate, run: +This runs [cargo-deny](https://github.com/EmbarkStudios/cargo-deny) - policy lives in [deny.toml](deny.toml). It needs network access to fetch the advisory database, so it is not part of `cargo xtask ci`; it runs as its own CI job instead. + +To check whether your changes break the public API of an already-published crate, run: ``` cargo xtask semver ``` -Both commands need network access - the first to fetch the advisory database, the second to fetch each crate's last published version as a baseline. For that reason neither is part of `cargo xtask ci`; they run as their own CI jobs instead. +This compares each crate against its last published version on [crates.io](https://crates.io). Because versions are assigned at release time rather than alongside the code, breaking changes sit on `main` un-bumped until released - so this reports "requires new major version" as a normal state and deliberately does **not** gate CI. [`cargo xtask crate:release`](#develop-publish-crate) runs it for you before asking which version to assign.

diff --git a/xtask/src/main.rs b/xtask/src/main.rs index ae2d3aa..005e127 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -407,6 +407,21 @@ fn init_tasks() -> Tasks { for mut krate in krates.values().cloned() { let log = git.get_changelog(&krate)?; let version = krate.toml.get_version()?; + + println!(); + println!(":::: Checking `{}` for breaking API changes...", krate.name); + println!(); + + // NOTE: advisory only - "requires new major version" is the + // *expected* result here, since versions are bumped below + // rather than alongside the code. `.unchecked()` keeps that + // non-zero exit from aborting the release + cmd!("cargo", "semver-checks", "--package", &krate.name) + .unchecked() + .run()?; + + println!(); + let options = VersionChoice::options(&version); let message = format!("Version for `{}` [current: {}]", krate.name, version); let question = InquireSelect::new(&message, options); From e72207470a16f0b79cdf55520f764ca3b8dcf860 Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 12 Aug 2026 19:48:51 -0700 Subject: [PATCH 29/31] [detect-newline-style] add migrations section to README with v0.x -> v1.x notes --- crates/detect-newline-style/README.md | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/crates/detect-newline-style/README.md b/crates/detect-newline-style/README.md index 629a8dc..fe10595 100644 --- a/crates/detect-newline-style/README.md +++ b/crates/detect-newline-style/README.md @@ -39,3 +39,43 @@ fn main() { } ``` + +## Migrations + +
+0.x -> 1.x +

+ +**Variant names now follow [RFC 430](https://rust-lang.github.io/rfcs/0430-finalizing-naming-conventions.html)** + +| before | after | +| --- | --- | +| `LineEnding::CR` | `LineEnding::Cr` | +| `LineEnding::LF` | `LineEnding::Lf` | +| `LineEnding::CRLF` | `LineEnding::Crlf` | + +**`FromStr` returns a concrete error type** + +`LineEnding::from_str` now fails with `ParseLineEndingError` instead of `Box`. The old type was neither `Send` nor `Sync`, so the error could not cross a thread boundary or compose with most application error types. + +Code that propagates with `?` into a function returning `Box` keeps working unchanged. Code that names the error type explicitly needs updating: + +```rust,ignore +// before +let eol: Result> = LineEnding::from_str("\n"); +// after +let eol: Result = LineEnding::from_str("\n"); +``` + +`from_str` also no longer lowercases its input. This has no observable effect, since the only valid inputs are `"\r"`, `"\n"` and `"\r\n"`. + +**`LineEnding` is now `Copy`** + +Existing code is unaffected - values that were previously moved are now copied. + +**The `regex` dependency is gone** + +This crate now has no dependencies at all. Behaviour is unchanged; see the tests covering `"\r\r\n"` and multi-byte input for the edge cases. + +

+
From 54266803f089da227b57063a148325f173d08c25 Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 12 Aug 2026 19:49:45 -0700 Subject: [PATCH 30/31] [node-js-release-info] add migrations section to README with v1.x -> v2.x notes --- crates/node-js-release-info/README.md | 79 +++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/crates/node-js-release-info/README.md b/crates/node-js-release-info/README.md index 9c84ebe..ff3ffa2 100644 --- a/crates/node-js-release-info/README.md +++ b/crates/node-js-release-info/README.md @@ -61,3 +61,82 @@ async fn main() { } ``` + +## Migrations + +
+1.x -> 2.x +

+ +**Type and variant names now follow [RFC 430](https://rust-lang.github.io/rfcs/0430-finalizing-naming-conventions.html)** + +| before | after | +| --- | --- | +| `NodeJSRelInfo` | `NodeJsRelInfo` | +| `NodeJSRelInfoError` | `NodeJsRelInfoError` | +| `NodeJSOS` | `NodeJsOs` | +| `NodeJSArch` | `NodeJsArch` | +| `NodeJSPkgExt` | `NodeJsPkgExt` | +| `NodeJSOS::AIX` | `NodeJsOs::Aix` | +| `NodeJSArch::ARM64` | `NodeJsArch::Arm64` | +| `NodeJSArch::ARMV7L` | `NodeJsArch::Armv7l` | +| `NodeJSArch::PPC64` | `NodeJsArch::Ppc64` | +| `NodeJSArch::PPC64LE` | `NodeJsArch::Ppc64le` | +| `NodeJSArch::S390X` | `NodeJsArch::S390x` | + +**Builder methods consume `self`** + +They now return an owned value directly, so the trailing `to_owned()` is no longer needed - and `NodeJsRelInfo::to_owned()` has been removed. Use `.clone()` if you want a copy. + +```rust,ignore +// before +let info = NodeJSRelInfo::new("24.19.0").macos().arm64().to_owned(); +// after +let info = NodeJsRelInfo::new("24.19.0").macos().arm64(); +``` + +Calling a builder as a statement no longer works, since it takes `self`: + +```rust,ignore +// before +let mut info = NodeJSRelInfo::new("24.19.0"); +info.macos(); +// after +let info = NodeJsRelInfo::new("24.19.0").macos(); +``` + +**`fetch()` consumes `self`** + +It previously mutated in place *and* returned a clone. It now takes `self` and returns the populated value: + +```rust,ignore +// before +let mut info = NodeJSRelInfo::new("24.19.0"); +info.fetch().await?; // `info` updated in place +// after +let info = NodeJsRelInfo::new("24.19.0").fetch().await?; +``` + +**Enums are `#[non_exhaustive]`** + +`NodeJsOs`, `NodeJsArch`, `NodeJsPkgExt` and `NodeJsRelInfoError` may gain variants in a minor release, so a `match` over them needs a `_` arm. Node.js adds and removes target platforms over time, and this keeps that from being a breaking change. + +**Error messages changed** + +The `Error: ` prefix is gone and messages are lowercased, per [API guideline C-GOOD-ERR](https://rust-lang.github.io/api-guidelines/interoperability.html#error-types-are-meaningful-and-well-behaved). Previously they composed into chains as `Error: Error: ...`. + +```text +before: Error: Invalid Version! Received: 'x' +after: invalid version - received: 'x' +``` + +`NodeJsRelInfoError` also implements `Error::source()` now, exposing the underlying `reqwest::Error` behind `HttpError`. + +**New variants** + +`NodeJsOs::SunOs` (`sunos`) and `NodeJsArch::Armv6l` (`armv6l`) were missing. Both appear in older Node.js releases, so fetching those versions previously failed with `UnrecognizedOs` / `UnrecognizedArch` on valid artifacts. + +Note that Node.js v24 *dropped* `linux-armv7l` and all 32-bit Windows builds, so `fetch_all` returns 19 configurations for v24 where v20 returned 24. + +

+
From 02f8bace4ab7ed751a4a24dd25e98199477bc075 Mon Sep 17 00:00:00 2001 From: busticated Date: Wed, 12 Aug 2026 20:07:54 -0700 Subject: [PATCH 31/31] ci publish workflow fetches prebuilt tooling --- .github/workflows/publish.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 65f8fc0..5ba249f 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -20,6 +20,14 @@ jobs: fetch-tags: true - name: Install Rust Toolchain (stable) uses: dtolnay/rust-toolchain@stable + # NOTE: publishing needs none of these, but `cargo xtask setup` installs + # them and `cargo install` always builds from source. fetching the + # prebuilt binaries makes `setup` below a no-op instead - see the same + # step in `test.yaml` for details + - name: Install Prebuilt Tooling + uses: taiki-e/install-action@v2 + with: + tool: cargo-llvm-cov,cargo-deny,cargo-semver-checks,typos - name: Setup Project run: cargo xtask setup - name: Publish to Crates.io