From c28afe80dc93d003997a62179127536d8d4e0c1f Mon Sep 17 00:00:00 2001 From: omnitrix Date: Sun, 7 Dec 2025 23:59:42 +0000 Subject: [PATCH 1/4] refactor(xtask): migrate project bootstrap to rust implementation --- xtask/Cargo.toml | 7 + xtask/src/bootstrap.rs | 315 +++++++++++++++++++++++++++++++++++++++++ xtask/src/main.rs | 33 +++++ 3 files changed, 355 insertions(+) create mode 100644 xtask/src/bootstrap.rs diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index f3086ba..dab021a 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -26,8 +26,15 @@ rust-version.workspace = true [package.metadata.release] release = false +[features] +bootstrap = [] +default = ["bootstrap"] + [dependencies] clap = { version = "4.5.49", features = ["derive"] } +colored = { version = "3.0.0" } +dialoguer = { version = "0.12.0" } +toml_edit = { version = "0.24" } which = { version = "8.0.0" } [lints] diff --git a/xtask/src/bootstrap.rs b/xtask/src/bootstrap.rs new file mode 100644 index 0000000..7e9d324 --- /dev/null +++ b/xtask/src/bootstrap.rs @@ -0,0 +1,315 @@ +// Copyright 2025 FastLabs Developers +// +// 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. + +use std::error::Error; +use std::path::Path; +use std::path::PathBuf; + +use colored::Colorize; +use dialoguer::Confirm; +use dialoguer::Input; + +fn workspace_dir() -> &'static Path { + Path::new(env!("CARGO_WORKSPACE_DIR")) +} + +fn workspace_path(relative: &str) -> PathBuf { + workspace_dir().join(relative) +} + +pub fn bootstrap_project() { + println!("\n{}", "🚀 Starting project bootstrap...".yellow().bold()); + + let project_name = get_valid_input( + "Enter your project name (e.g., my-awesome-project)", + parse_project_name, + ); + let github_username = get_valid_input( + "Enter your GitHub username (e.g., torvalds)", + parse_github_username, + ); + + let confirmation = Confirm::new() + .with_prompt( + format!("Bootstrap project '{project_name}' for user '{github_username}'?") + .blue() + .to_string(), + ) + .default(false) + .interact() + .unwrap(); + + if !confirmation { + println!("\n{}", "Cancelled.".yellow()); + return; + } + + println!("\n{}", "Bootstrapping...".cyan()); + execute_bootstrap(&project_name, &github_username); + + println!("\n{}", "🎉 Bootstrap complete!".green().bold()); + println!( + " {}: {}", + "You can now delete this script".dimmed(), + "cargo x bootstrap --cleanup".cyan().bold(), + ); +} + +pub fn cleanup_bootstrap() { + println!("\n{}", "🧹 Starting bootstrap cleanup...".yellow().bold()); + let bootstrap_file = workspace_path("xtask/src/bootstrap.rs"); + let cargo_toml = workspace_path("xtask/Cargo.toml"); + remove_bootstrap_file(&bootstrap_file); + cleanup_cargo_toml(&cargo_toml).unwrap(); + println!("\n{}", "🧹 Bootstrap cleanup complete!".green().bold()); +} + +fn remove_bootstrap_file(bootstrap_file: &Path) { + if bootstrap_file.exists() { + println!("Deleting bootstrap.rs..."); + std::fs::remove_file(bootstrap_file).unwrap(); + } else { + println!("{}", "bootstrap.rs already deleted".dimmed()); + } +} + +fn cleanup_cargo_toml(cargo_toml_path: &Path) -> Result<(), Box> { + use toml_edit::DocumentMut; + + let content = std::fs::read_to_string(cargo_toml_path)?; + let mut doc = content.parse::()?; + + disable_bootstrap_feature(&mut doc); + remove_bootstrap_dependencies(&mut doc); + + std::fs::write(cargo_toml_path, doc.to_string())?; + Ok(()) +} + +fn disable_bootstrap_feature(doc: &mut toml_edit::DocumentMut) { + if let Some(features) = doc.get_mut("features").and_then(|f| f.as_table_mut()) { + println!("Disabling bootstrap feature..."); + if let Some(default) = features.get_mut("default").and_then(|d| d.as_array_mut()) { + let index_to_remove = default + .iter() + .position(|feature| feature.as_str() == Some("bootstrap")); + if let Some(idx) = index_to_remove { + default.remove(idx); + } + } + } +} + +fn remove_bootstrap_dependencies(doc: &mut toml_edit::DocumentMut) { + if let Some(dependencies) = doc.get_mut("dependencies").and_then(|d| d.as_table_mut()) { + println!("Removing unnecessary dependencies..."); + dependencies.remove("toml_edit"); + dependencies.remove("colored"); + dependencies.remove("dialoguer"); + } +} + +/// Validates a project name according to Cargo's naming conventions. +/// +/// Adapted from Cargo's [`restricted_names`] validation. +/// +/// [`restricted_names`]: https://github.com/rust-lang/cargo/blob/master/crates/cargo-util-schemas/src/restricted_names.rs +/// +/// See also: +pub fn parse_project_name(name: &str) -> Result { + let name = name.trim(); + + if name.is_empty() { + return Err("project name cannot be empty".into()); + } + + let mut chars = name.chars(); + if let Some(ch) = chars.next() { + if ch.is_ascii_digit() { + return Err(format!("the name cannot start with a digit: '{}'", ch)); + } + if !(ch.is_ascii_alphabetic() || ch == '_') { + return Err(format!( + "the first character must be a letter or `_`, found: '{}'", + ch + )); + } + } + + for ch in chars { + if !(ch.is_ascii_alphanumeric() || ch == '-' || ch == '_') { + return Err(format!( + "invalid character '{}': only letters, numbers, `-`, or `_` are allowed", + ch + )); + } + } + + Ok(name.to_owned()) +} + +pub fn parse_github_username(account_name: &str) -> Result { + let account_name = account_name.trim(); + if account_name.is_empty() { + return Err("GitHub account name cannot be empty".into()); + } + Ok(account_name.to_owned()) +} + +fn get_valid_input(prompt: &str, validator: F) -> String +where + F: Fn(&str) -> Result, +{ + loop { + let input: String = Input::new().with_prompt(prompt).interact_text().unwrap(); + match validator(&input) { + Ok(value) => return value, + Err(e) => eprintln!("{}", format!("ERROR: {e}").red()), + } + } +} + +fn execute_bootstrap(project_name: &str, github_username: &str) { + update_readme(project_name, github_username); + update_root_cargo_toml(project_name, github_username); + update_template_cargo_toml(project_name); + update_semantic_yml(project_name, github_username); + update_cargo_lock(project_name); + update_project_dir(project_name); +} + +fn replace_in_file(file: &std::path::Path, old: &str, new: &str) -> Result<(), Box> { + let content = std::fs::read_to_string(file)?; + + if !content.contains(old) { + return Ok(()); + } + let content = content.replace(old, new); + + std::fs::write(file, content)?; + Ok(()) +} + +fn print_task(task: impl AsRef) { + print!("{:.<60}", task.as_ref()); +} + +fn print_update_result(result: Result<(), Box>) { + match result { + Ok(_) => println!("{}", "[OK]".green()), + Err(e) => println!("{}", format!("[ERROR] {e}").red()), + } +} + +fn update_readme(project_name: &str, github_username: &str) { + let file = workspace_path("README.md"); + print_task(format!("Updating {}...", file.display())); + let result = replace_in_file( + &file, + "fast/template", + &format!("{}/{}", github_username, project_name), + ) + .and_then(|_| replace_in_file(&file, "${projectName}", project_name)); + print_update_result(result); +} + +fn update_root_cargo_toml(project_name: &str, github_username: &str) { + let file = workspace_path("Cargo.toml"); + print_task(format!("Updating {}...", file.display())); + let result = replace_in_file( + &file, + "fast/template", + &format!("{}/{}", github_username, project_name), + ) + .and_then(|_| replace_in_file(&file, "template", project_name)); + + print_update_result(result); +} + +fn update_template_cargo_toml(project_name: &str) { + let file = workspace_path("template/Cargo.toml"); + print_task(format!("Updating {}...", file.display())); + let result = replace_in_file(&file, "template", project_name); + print_update_result(result); +} + +fn update_semantic_yml(project_name: &str, github_username: &str) { + let file = workspace_path(".github/semantic.yml"); + print_task(format!("Updating {}...", file.display())); + let result = replace_in_file( + &file, + "fast/template", + &format!("{}/{}", github_username, project_name), + ); + print_update_result(result); +} + +fn update_cargo_lock(project_name: &str) { + let file = workspace_path("Cargo.lock"); + print_task(format!("Updating {}...", file.display())); + let result = replace_in_file(&file, "template", project_name); + print_update_result(result); +} + +fn update_project_dir(project_name: &str) { + print_task(format!( + "Renaming directory \"template\" to \"{project_name}\" ..." + )); + let template_dir = Path::new(env!("CARGO_WORKSPACE_DIR")).join("template"); + let target_dir = Path::new(env!("CARGO_WORKSPACE_DIR")).join(project_name); + let result = if target_dir.exists() { + Err(format!("Directory '{project_name}' already exists").into()) + } else { + std::fs::rename(template_dir, target_dir).map_err(|e| e.into()) + }; + print_update_result(result); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_project_name() { + // valid names + assert_eq!(parse_project_name("myproject"), Ok("myproject".into())); + assert_eq!(parse_project_name("my-project"), Ok("my-project".into())); + assert_eq!(parse_project_name("my_project"), Ok("my_project".into())); + assert_eq!(parse_project_name("project123"), Ok("project123".into())); + assert_eq!(parse_project_name("_private"), Ok("_private".into())); + assert_eq!(parse_project_name("MyProject"), Ok("MyProject".into())); + assert_eq!(parse_project_name(" myproject "), Ok("myproject".into())); + + // invalid names + assert!(parse_project_name("").is_err()); + assert!(parse_project_name(" ").is_err()); + assert!(parse_project_name("123project").is_err()); + assert!(parse_project_name("-project").is_err()); + assert!(parse_project_name("my@project").is_err()); + assert!(parse_project_name("my project").is_err()); + assert!(parse_project_name("my.project").is_err()); + } + + #[test] + fn test_parse_github_username() { + // valid accounts + assert_eq!(parse_github_username("myuser"), Ok("myuser".into())); + assert_eq!(parse_github_username("my-org"), Ok("my-org".into())); + assert_eq!(parse_github_username(" myuser "), Ok("myuser".into())); + + // invalid accounts + assert!(parse_github_username("").is_err()); + assert!(parse_github_username(" ").is_err()); + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index b8a8de8..b3aaf81 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -19,6 +19,9 @@ use std::process::Command as StdCommand; use clap::Parser; use clap::Subcommand; +#[cfg(feature = "bootstrap")] +mod bootstrap; + #[derive(Parser)] struct Command { #[clap(subcommand)] @@ -29,6 +32,7 @@ impl Command { fn run(self) { match self.sub { SubCommand::Build(cmd) => cmd.run(), + SubCommand::Bootstrap(cmd) => cmd.run(), SubCommand::Lint(cmd) => cmd.run(), SubCommand::Test(cmd) => cmd.run(), } @@ -39,6 +43,8 @@ impl Command { enum SubCommand { #[clap(about = "Compile workspace packages.")] Build(CommandBuild), + #[clap(about = "Bootstrap a new project from this template.")] + Bootstrap(CommandBootstrap), #[clap(about = "Run format and clippy checks.")] Lint(CommandLint), #[clap(about = "Run unit tests.")] @@ -57,6 +63,33 @@ impl CommandBuild { } } +#[derive(Parser)] +struct CommandBootstrap { + #[arg( + long, + help = "Clean up bootstrap files and disable the bootstrap feature" + )] + cleanup: bool, +} + +impl CommandBootstrap { + fn run(self) { + #[cfg(not(feature = "bootstrap"))] + { + println!("\nâš ī¸ This project has already been bootstrapped!"); + return; + } + #[cfg(feature = "bootstrap")] + { + if self.cleanup { + bootstrap::cleanup_bootstrap(); + } else { + bootstrap::bootstrap_project(); + } + } + } +} + #[derive(Parser)] struct CommandTest { #[arg(long, help = "Run tests serially and do not capture output.")] From 2a2fb080b835ef5964d6a5da35de24bda612e6a9 Mon Sep 17 00:00:00 2001 From: tison Date: Sun, 4 Jan 2026 19:26:41 +0800 Subject: [PATCH 2/4] chore: fine tune Signed-off-by: tison --- Cargo.lock | 300 +++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 4 +- template/src/lib.rs | 1 + 3 files changed, 300 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1798052..1dd0cc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,7 +38,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -49,7 +49,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -58,6 +58,12 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + [[package]] name = "clap" version = "4.5.53" @@ -104,12 +110,58 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "console" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.61.2", +] + +[[package]] +name = "dialoguer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96" +dependencies = [ + "console", + "shell-words", + "tempfile", + "zeroize", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "env_home" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" +[[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" @@ -117,15 +169,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[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", + "wasip2", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -144,6 +230,18 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "once_cell_polyfill" version = "1.70.2" @@ -168,6 +266,12 @@ 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 = "rustix" version = "1.1.2" @@ -178,9 +282,35 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "strsim" version = "0.11.1" @@ -198,22 +328,87 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "template" version = "0.1.0" +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.24.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c740b185920170a6d9191122cafef7010bd6270a3824594bff6784c04d7f09e" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "which" version = "8.0.0" @@ -231,6 +426,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -240,16 +444,104 @@ 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", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + [[package]] name = "winsafe" version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "x" version = "0.0.0" dependencies = [ "clap", + "colored", + "dialoguer", + "toml_edit", "which", ] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" diff --git a/Cargo.toml b/Cargo.toml index 6d03c32..21ed69b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,12 +25,14 @@ repository = "https://github.com/fast/template" rust-version = "1.85.0" [workspace.lints.rust] -missing_docs = "deny" unknown_lints = "deny" +unsafe_code = "deny" unused_must_use = "deny" [workspace.lints.clippy] dbg_macro = "deny" +too_many_arguments = "allow" +type_complexity = "allow" [workspace.metadata.release] pre-release-commit-message = "chore: release v{{version}}" diff --git a/template/src/lib.rs b/template/src/lib.rs index ac6e38e..5a120da 100644 --- a/template/src/lib.rs +++ b/template/src/lib.rs @@ -15,6 +15,7 @@ //! A template library. #![cfg_attr(docsrs, feature(doc_cfg))] +#![deny(missing_docs)] /// A placeholder function. pub fn hello() { From 3a13d39e65a13f5a581a36c36024107cef4b9d5e Mon Sep 17 00:00:00 2001 From: tison Date: Sun, 4 Jan 2026 20:04:09 +0800 Subject: [PATCH 3/4] fine tune Signed-off-by: tison --- .github/workflows/ci-bootstrap.yml | 38 +++++++++++++++ xtask/Cargo.toml | 2 - xtask/src/bootstrap-done.rs | 17 +++++++ xtask/src/bootstrap.rs | 78 +++++++++++++++--------------- xtask/src/main.rs | 20 +------- 5 files changed, 96 insertions(+), 59 deletions(-) create mode 100644 .github/workflows/ci-bootstrap.yml create mode 100644 xtask/src/bootstrap-done.rs diff --git a/.github/workflows/ci-bootstrap.yml b/.github/workflows/ci-bootstrap.yml new file mode 100644 index 0000000..547eaf9 --- /dev/null +++ b/.github/workflows/ci-bootstrap.yml @@ -0,0 +1,38 @@ +# Copyright 2025 FastLabs Developers +# +# 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. + +name: CI Bootstrap +on: + pull_request: + branches: [ main ] + push: + branches: [ main ] + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.number || github.run_id }} + cancel-in-progress: true + +jobs: + bootstrap: + name: Bootstrap + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v6 + - name: Install toolchain + uses: dtolnay/rust-toolchain@nightly + - uses: Swatinem/rust-cache@v2 + - name: Bootstrap cleanup + run: cargo x bootstrap --cleanup + - name: Bootstrap cleanup (After) + run: cargo x bootstrap --cleanup diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 6f55fbf..389a82f 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -23,8 +23,6 @@ rust-version.workspace = true release = false [features] -bootstrap = [] -default = ["bootstrap"] [dependencies] clap = { version = "4.5.49", features = ["derive"] } diff --git a/xtask/src/bootstrap-done.rs b/xtask/src/bootstrap-done.rs new file mode 100644 index 0000000..0f68bdb --- /dev/null +++ b/xtask/src/bootstrap-done.rs @@ -0,0 +1,17 @@ +// Copyright 2025 FastLabs Developers +// +// 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. + +pub fn bootstrap(_cleanup: bool) { + println!("\nThis project has already been bootstrapped!"); +} diff --git a/xtask/src/bootstrap.rs b/xtask/src/bootstrap.rs index 1633909..9b553c6 100644 --- a/xtask/src/bootstrap.rs +++ b/xtask/src/bootstrap.rs @@ -18,10 +18,19 @@ use std::path::Path; use colored::Colorize; use dialoguer::Confirm; use dialoguer::Input; +use toml_edit::DocumentMut; use super::workspace_dir; -pub fn bootstrap_project() { +pub fn bootstrap(cleanup: bool) { + if cleanup { + cleanup_bootstrap(); + } else { + bootstrap_project(); + } +} + +fn bootstrap_project() { println!("\n{}", "🚀 Starting project bootstrap...".yellow().bold()); let project_name = get_valid_input( @@ -29,7 +38,7 @@ pub fn bootstrap_project() { parse_project_name, ); let github_username = get_valid_input( - "Enter your GitHub username (e.g., torvalds)", + "Enter your GitHub username (e.g., tisonkun)", parse_github_username, ); @@ -59,57 +68,48 @@ pub fn bootstrap_project() { ); } -pub fn cleanup_bootstrap() { +fn cleanup_bootstrap() { println!("\n{}", "🧹 Starting bootstrap cleanup...".yellow().bold()); - let bootstrap_file = workspace_dir().join("xtask/src/bootstrap.rs"); - let cargo_toml = workspace_dir().join("xtask/Cargo.toml"); - remove_bootstrap_file(&bootstrap_file); - cleanup_cargo_toml(&cargo_toml).unwrap(); + remove_ci_workflows(); + override_bootstrap_file(); + cleanup_cargo_toml(); println!("\n{}", "🧹 Bootstrap cleanup complete!".green().bold()); } -fn remove_bootstrap_file(bootstrap_file: &Path) { - if bootstrap_file.exists() { - println!("Deleting bootstrap.rs..."); - std::fs::remove_file(bootstrap_file).unwrap(); +fn remove_ci_workflows() { + let workflows_dir = workspace_dir().join(".github/workflows/ci-bootstrap.yml"); + if workflows_dir.exists() { + println!("Removing CI Bootstrap workflows..."); + std::fs::remove_dir_all(workflows_dir).unwrap(); } else { - println!("{}", "bootstrap.rs already deleted".dimmed()); + panic!("Broken bootstrap cleanup state: '.github/workflows/ci-bootstrap.yml' not found"); } } -fn cleanup_cargo_toml(cargo_toml_path: &Path) -> Result<(), Box> { - use toml_edit::DocumentMut; - - let content = std::fs::read_to_string(cargo_toml_path)?; - let mut doc = content.parse::()?; - - disable_bootstrap_feature(&mut doc); - remove_bootstrap_dependencies(&mut doc); - - std::fs::write(cargo_toml_path, doc.to_string())?; - Ok(()) -} - -fn disable_bootstrap_feature(doc: &mut toml_edit::DocumentMut) { - if let Some(features) = doc.get_mut("features").and_then(|f| f.as_table_mut()) { - println!("Disabling bootstrap feature..."); - if let Some(default) = features.get_mut("default").and_then(|d| d.as_array_mut()) { - let index_to_remove = default - .iter() - .position(|feature| feature.as_str() == Some("bootstrap")); - if let Some(idx) = index_to_remove { - default.remove(idx); - } - } +fn override_bootstrap_file() { + let old_bootstrap_file = workspace_dir().join("xtask/src/bootstrap.rs"); + let new_bootstrap_file = workspace_dir().join("xtask/src/bootstrap-done.rs"); + if new_bootstrap_file.exists() { + println!("Overriding bootstrap file..."); + std::fs::rename(new_bootstrap_file, old_bootstrap_file).unwrap(); + } else { + panic!("Broken bootstrap cleanup state: 'bootstrap-done.rs' not found"); } } -fn remove_bootstrap_dependencies(doc: &mut toml_edit::DocumentMut) { +fn cleanup_cargo_toml() { + let cargo_toml = workspace_dir().join("xtask/Cargo.toml"); + + let content = std::fs::read_to_string(&cargo_toml).unwrap(); + let mut doc = content.parse::().unwrap(); if let Some(dependencies) = doc.get_mut("dependencies").and_then(|d| d.as_table_mut()) { println!("Removing unnecessary dependencies..."); dependencies.remove("toml_edit"); dependencies.remove("colored"); dependencies.remove("dialoguer"); + std::fs::write(&cargo_toml, doc.to_string()).unwrap(); + } else { + panic!("Broken bootstrap cleanup state: 'dependencies' section not found"); } } @@ -297,9 +297,9 @@ mod tests { #[test] fn test_parse_github_username() { // valid accounts - assert_eq!(parse_github_username("myuser"), Ok("myuser".into())); + assert_eq!(parse_github_username("my-user"), Ok("my-user".into())); assert_eq!(parse_github_username("my-org"), Ok("my-org".into())); - assert_eq!(parse_github_username(" myuser "), Ok("myuser".into())); + assert_eq!(parse_github_username(" my-user "), Ok("my-user".into())); // invalid accounts assert!(parse_github_username("").is_err()); diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 6f808ca..a890e65 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -20,7 +20,6 @@ use std::process::Command as StdCommand; use clap::Parser; use clap::Subcommand; -#[cfg(feature = "bootstrap")] mod bootstrap; fn workspace_dir() -> &'static Path { @@ -70,28 +69,13 @@ impl CommandBuild { #[derive(Parser)] struct CommandBootstrap { - #[arg( - long, - help = "Clean up bootstrap files and disable the bootstrap feature" - )] + #[arg(long, help = "Clean up the bootstrap scaffold.")] cleanup: bool, } impl CommandBootstrap { fn run(self) { - #[cfg(not(feature = "bootstrap"))] - { - println!("\nâš ī¸ This project has already been bootstrapped!"); - return; - } - #[cfg(feature = "bootstrap")] - { - if self.cleanup { - bootstrap::cleanup_bootstrap(); - } else { - bootstrap::bootstrap_project(); - } - } + bootstrap::bootstrap(self.cleanup); } } From fe8cd8b8d23463e6dcc3ca9745cbedd76acc5404 Mon Sep 17 00:00:00 2001 From: tison Date: Sun, 4 Jan 2026 20:08:53 +0800 Subject: [PATCH 4/4] more Signed-off-by: tison --- README.md | 4 +- bootstrap.py | 119 ----------------------------------------- xtask/src/bootstrap.rs | 6 +-- xtask/src/main.rs | 2 +- 4 files changed, 6 insertions(+), 125 deletions(-) delete mode 100755 bootstrap.py diff --git a/README.md b/README.md index c6fd11e..0f38aca 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ Use this repository as a GitHub template to quickly start a new Rust project. ## Getting Started 1. Create a new repository using this template; -2. Clone your repository and run the bootstrap script: `./bootstrap.py`; -3. Follow the prompts, review changes, and commit; +2. Clone your repository and run the bootstrap script: `cargo x bootstrap`; +3. Cleanup the bootstrap scaffolding: `cargo x bootstrap --cleanup`; 4. Start building your project! ## Minimum Rust version policy diff --git a/bootstrap.py b/bootstrap.py deleted file mode 100755 index c6d4b68..0000000 --- a/bootstrap.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025 FastLabs Developers -# -# 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. - -import os -import sys - -def main(): - print("Welcome to the project bootstrap script!") - - # 1. Get user input - try: - project_name = input("Enter your project name (e.g., my-awesome-project): ").strip() - if not project_name: - print("Error: Project name cannot be empty.") - sys.exit(1) - - github_username = input("Enter your GitHub username (e.g., torvalds): ").strip() - if not github_username: - print("Error: GitHub username cannot be empty.") - sys.exit(1) - except KeyboardInterrupt: - print("\nOperation cancelled.") - sys.exit(0) - - print(f"\nBootstrapping project '{project_name}' for user '{github_username}'...\n") - - # 2. Update README.md - # Replaces: - # - fast/template -> username/project_name - # - ${projectName} -> project_name - readme_path = "README.md" - if os.path.exists(readme_path): - with open(readme_path, "r", encoding="utf-8") as f: - content = f.read() - - new_content = content.replace("fast/template", f"{github_username}/{project_name}") - new_content = new_content.replace("${projectName}", project_name) - - if content != new_content: - with open(readme_path, "w", encoding="utf-8") as f: - f.write(new_content) - print(f"✅ Updated {readme_path}") - else: - print(f"â„šī¸ No changes needed in {readme_path}") - else: - print(f"âš ī¸ Warning: {readme_path} not found.") - - # 3. Update Cargo.toml (Workspace Root) - # Replaces: - # - fast/template -> username/project_name - # - "template" (in members) -> "project_name" - root_cargo_path = "Cargo.toml" - if os.path.exists(root_cargo_path): - with open(root_cargo_path, "r", encoding="utf-8") as f: - content = f.read() - - new_content = content.replace("fast/template", f"{github_username}/{project_name}") - # Identify workspace member "template" specifically to avoid false positives - new_content = new_content.replace('"template"', f'"{project_name}"') - - if content != new_content: - with open(root_cargo_path, "w", encoding="utf-8") as f: - f.write(new_content) - print(f"✅ Updated {root_cargo_path}") - else: - print(f"â„šī¸ No changes needed in {root_cargo_path}") - else: - print(f"âš ī¸ Warning: {root_cargo_path} not found.") - - # 4. Update template/Cargo.toml (Package Name) - # Replaces: - # - name = "template" -> name = "project_name" - # Note: We edit the file inside the directory *before* renaming the directory - template_cargo_path = "template/Cargo.toml" - if os.path.exists(template_cargo_path): - with open(template_cargo_path, "r", encoding="utf-8") as f: - content = f.read() - - new_content = content.replace('name = "template"', f'name = "{project_name}"') - - if content != new_content: - with open(template_cargo_path, "w", encoding="utf-8") as f: - f.write(new_content) - print(f"✅ Updated {template_cargo_path}") - else: - print(f"â„šī¸ No changes needed in {template_cargo_path}") - else: - # If the directory was already renamed in a previous run, we might want to check the new name - # but for a simple bootstrap script, assuming standard state is fine. - print(f"âš ī¸ Warning: {template_cargo_path} not found (Did you already run this script?)") - - # 5. Rename template directory - if os.path.exists("template"): - os.rename("template", project_name) - print(f"✅ Renamed directory 'template' to '{project_name}'") - else: - if os.path.exists(project_name): - print(f"â„šī¸ Directory '{project_name}' already exists.") - else: - print("âš ī¸ Warning: Directory 'template' not found.") - - print("\n🎉 Bootstrap complete!") - print(f"You can now delete this script: rm {os.path.basename(__file__)}") - -if __name__ == "__main__": - main() diff --git a/xtask/src/bootstrap.rs b/xtask/src/bootstrap.rs index 9b553c6..ef41888 100644 --- a/xtask/src/bootstrap.rs +++ b/xtask/src/bootstrap.rs @@ -77,10 +77,10 @@ fn cleanup_bootstrap() { } fn remove_ci_workflows() { - let workflows_dir = workspace_dir().join(".github/workflows/ci-bootstrap.yml"); - if workflows_dir.exists() { + let ci_bootstrap = workspace_dir().join(".github/workflows/ci-bootstrap.yml"); + if ci_bootstrap.exists() { println!("Removing CI Bootstrap workflows..."); - std::fs::remove_dir_all(workflows_dir).unwrap(); + std::fs::remove_file(ci_bootstrap).unwrap(); } else { panic!("Broken bootstrap cleanup state: '.github/workflows/ci-bootstrap.yml' not found"); } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index a890e65..0b077c3 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -69,7 +69,7 @@ impl CommandBuild { #[derive(Parser)] struct CommandBootstrap { - #[arg(long, help = "Clean up the bootstrap scaffold.")] + #[arg(long, help = "Clean up the bootstrap scaffolding.")] cleanup: bool, }