diff --git a/Cargo.lock b/Cargo.lock index b6dca243..a6b20fb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4618,7 +4618,7 @@ checksum = "32ac00cd3f8ec9c1d33fb3e7958a82df6989c42d747bd326c822b1d625283547" [[package]] name = "xtask" -version = "0.1.0" +version = "0.0.0" dependencies = [ "anyhow", "flate2", @@ -4629,6 +4629,14 @@ dependencies = [ "zip", ] +[[package]] +name = "xtask_codegen" +version = "0.0.0" +dependencies = [ + "bpaf", + "xtask", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 14b6d5ee..57617cbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = [ "crates/*", "lib/*", - "xtask/" + "xtask/codegen" ] resolver = "2" diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..6dae5352 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,2 @@ +allow-dbg-in-tests = true + diff --git a/justfile b/justfile new file mode 100644 index 00000000..b990b371 --- /dev/null +++ b/justfile @@ -0,0 +1,152 @@ +_default: + just --list -u + +alias f := format +alias t := test +# alias r := ready +# alias l := lint +# alias qt := test-quick + +# Installs the tools needed to develop +# install-tools: +# cargo install cargo-binstall +# cargo binstall cargo-insta taplo-cli wasm-pack wasm-tools knope + +# Upgrades the tools needed to develop +# upgrade-tools: +# cargo install cargo-binstall --force +# cargo binstall cargo-insta taplo-cli wasm-pack wasm-tools knope --force + +# Generate all files across crates and tools. You rarely want to use it locally. +# gen-all: +# cargo run -p xtask_codegen -- all +# cargo codegen-configuration +# cargo codegen-migrate +# just gen-bindings +# just format + +# Generates TypeScript types and JSON schema of the configuration +# gen-bindings: +# cargo codegen-schema +# cargo codegen-bindings + +# Generates code generated files for the linter +# gen-lint: +# cargo run -p xtask_codegen -- analyzer +# cargo codegen-configuration +# cargo codegen-migrate +# just gen-bindings +# cargo run -p rules_check +# just format + +# Generates the initial files for all formatter crates +# gen-formatter: +# cargo run -p xtask_codegen -- formatter + +# Generates the Tailwind CSS preset for utility class sorting (requires Bun) +# gen-tw: +# bun packages/tailwindcss-config-analyzer/src/generate-tailwind-preset.ts + +# Generates the code of the grammars available in Biome +# gen-grammar *args='': +# cargo run -p xtask_codegen -- grammar {{args}} + +# Generates the linter documentation and Rust documentation +# documentation: +# RUSTDOCFLAGS='-D warnings' cargo documentation + +# Creates a new lint rule in the given path, with the given name. Name has to be camel case. +# new-js-lintrule rulename: +# cargo run -p xtask_codegen -- new-lintrule --kind=js --category=lint --name={{rulename}} +# just gen-lint +# just documentation + +# Creates a new lint rule in the given path, with the given name. Name has to be camel case. +# new-js-assistrule rulename: +# cargo run -p xtask_codegen -- new-lintrule --kind=js --category=assist --name={{rulename}} +# just gen-lint +# just documentation + +# Promotes a rule from the nursery group to a new group +# promote-rule rulename group: +# cargo run -p xtask_codegen -- promote-rule --name={{rulename}} --group={{group}} +# just gen-lint +# just documentation +# -cargo test -p biome_js_analyze -- {{snakecase(rulename)}} +# cargo insta accept + + +# Format Rust files and TOML files +format: + cargo format + # taplo format + +[unix] +_touch file: + touch {{file}} + +[windows] +_touch file: + (gci {{file}}).LastWriteTime = Get-Date + +# Run tests of all crates +test: + cargo test run --no-fail-fast + +# Run tests for the crate passed as argument e.g. just test-create pg_cli +test-crate name: + cargo test run -p {{name}} --no-fail-fast + +# Run doc tests +test-doc: + cargo test --doc + +# Tests a lint rule. The name of the rule needs to be camel case +# test-lintrule name: +# just _touch crates/biome_js_analyze/tests/spec_tests.rs +# just _touch crates/biome_json_analyze/tests/spec_tests.rs +# just _touch crates/biome_css_analyze/tests/spec_tests.rs +# just _touch crates/biome_graphql_analyze/tests/spec_tests.rs +# cargo test -p biome_js_analyze -- {{snakecase(name)}} --show-output +# cargo test -p biome_json_analyze -- {{snakecase(name)}} --show-output +# cargo test -p biome_css_analyze -- {{snakecase(name)}} --show-output +# cargo test -p biome_graphql_analyze -- {{snakecase(name)}} --show-output + +# Tests a lint rule. The name of the rule needs to be camel case +# test-transformation name: +# just _touch crates/biome_js_transform/tests/spec_tests.rs +# cargo test -p biome_js_transform -- {{snakecase(name)}} --show-output + +# Run the quick_test for the given package. +# test-quick package: +# cargo test -p {{package}} --test quick_test -- quick_test --nocapture --ignored + + +# Alias for `cargo clippy`, it runs clippy on the whole codebase +lint: + cargo clippy + +# When you finished coding, run this command to run the same commands in the CI. +# ready: +# git diff --exit-code --quiet +# just gen-all +# just documentation +# #just format # format is already run in `just gen-all` +# just lint +# just test +# just test-doc +# git diff --exit-code --quiet + +# Creates a new crate +new-crate name: + cargo new --lib crates/{{snakecase(name)}} + cargo run -p xtask_codegen -- new-crate --name={{snakecase(name)}} + +# Creates a new changeset for the final changelog +# new-changeset: +# knope document-change + +# Dry-run of the release +# dry-run-release *args='': +# knope release --dry-run {{args}} + diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..d6970839 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +newline_style = "Unix" + diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 7a34617e..9118df95 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xtask" -version = "0.1.0" +version = "0.0.0" publish = false license = "MIT OR Apache-2.0" edition = "2021" diff --git a/xtask/codegen/Cargo.toml b/xtask/codegen/Cargo.toml new file mode 100644 index 00000000..65d71e99 --- /dev/null +++ b/xtask/codegen/Cargo.toml @@ -0,0 +1,11 @@ +[package] +edition = "2021" +name = "xtask_codegen" +publish = false +version = "0.0.0" + +[dependencies] +bpaf = { workspace = true, features = ["derive"] } +xtask = { path = '../', version = "0.0" } + + diff --git a/xtask/codegen/src/generate_crate.rs b/xtask/codegen/src/generate_crate.rs new file mode 100644 index 00000000..5fc46488 --- /dev/null +++ b/xtask/codegen/src/generate_crate.rs @@ -0,0 +1,64 @@ +use std::fs; +use xtask::*; + +fn cargo_template(name: &str) -> String { + format!( + r#" +[package] +authors.workspace = true +categories.workspace = true +description = "" +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +name = "{name}" +repository.workspace = true +version = "0.0.0" + +[lints] +workspace = true +"# + ) +} + +// fn knope_template(name: &str) -> String { +// format!( +// r#" +// [packages.{name}] +// versioned_files = ["crates/{name}/Cargo.toml"] +// changelog = "crates/{name}/CHANGELOG.md" +// "# +// ) +// } + +pub fn generate_crate(crate_name: String) -> Result<()> { + let crate_root = project_root().join("crates").join(crate_name.as_str()); + let cargo_file = crate_root.join("Cargo.toml"); + // let knope_config = project_root().join("knope.toml"); + + // let mut knope_contents = fs::read_to_string(&knope_config)?; + fs::write(cargo_file, cargo_template(crate_name.as_str()))?; + // let start_content = "## Rust crates. DO NOT CHANGE!\n"; + // let end_content = "\n## End of crates. DO NOT CHANGE!"; + // debug_assert!( + // knope_contents.contains(start_content), + // "The file knope.toml must contains `{start_content}`" + // ); + // debug_assert!( + // knope_contents.contains(end_content), + // "The file knope.toml must contains `{end_content}`" + // ); + + // let file_start_index = knope_contents.find(start_content).unwrap() + start_content.len(); + // let file_end_index = knope_contents.find(end_content).unwrap(); + // let crates_text = &knope_contents[file_start_index..file_end_index]; + // let template = knope_template(crate_name.as_str()); + // let new_crates_text: Vec<_> = crates_text.lines().chain(Some(&template[..])).collect(); + // let new_crates_text = new_crates_text.join("\n"); + // + // knope_contents.replace_range(file_start_index..file_end_index, &new_crates_text); + // fs::write(knope_config, knope_contents)?; + Ok(()) +} + diff --git a/xtask/codegen/src/lib.rs b/xtask/codegen/src/lib.rs new file mode 100644 index 00000000..59c83805 --- /dev/null +++ b/xtask/codegen/src/lib.rs @@ -0,0 +1,20 @@ +//! Codegen tools. Derived from Biome's codegen + +mod generate_crate; + +use bpaf::Bpaf; +pub use self::generate_crate::generate_crate; + +#[derive(Debug, Clone, Bpaf)] +#[bpaf(options)] +pub enum TaskCommand { + /// Creates a new crate + #[bpaf(command, long("new-crate"))] + NewCrate { + /// The name of the crate + #[bpaf(long("name"), argument("STRING"))] + name: String, + }, +} + + diff --git a/xtask/codegen/src/main.rs b/xtask/codegen/src/main.rs new file mode 100644 index 00000000..ff7b871c --- /dev/null +++ b/xtask/codegen/src/main.rs @@ -0,0 +1,19 @@ +use xtask::{project_root, pushd, Result}; + +use xtask_codegen::{ + generate_crate, task_command, TaskCommand, +}; + +fn main() -> Result<()> { + let _d = pushd(project_root()); + let result = task_command().fallback_to_usage().run(); + + match result { + TaskCommand::NewCrate { name } => { + generate_crate(name)?; + } + } + + Ok(()) +} + diff --git a/xtask/src/glue.rs b/xtask/src/glue.rs new file mode 100644 index 00000000..b09285c6 --- /dev/null +++ b/xtask/src/glue.rs @@ -0,0 +1,214 @@ +//! A shell but bad, some cross platform glue code + +use std::{ + cell::RefCell, + env, + ffi::OsString, + io::Write, + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; + +use anyhow::{bail, Context, Result}; + +pub mod fs2 { + use std::{fs, path::Path}; + + use anyhow::{Context, Result}; + + pub fn read_dir>(path: P) -> Result { + let path = path.as_ref(); + fs::read_dir(path).with_context(|| format!("Failed to read {}", path.display())) + } + + pub fn read_to_string>(path: P) -> Result { + let path = path.as_ref(); + fs::read_to_string(path).with_context(|| format!("Failed to read {}", path.display())) + } + + pub fn write, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> { + let path = path.as_ref(); + fs::write(path, contents).with_context(|| format!("Failed to write {}", path.display())) + } + + pub fn copy, Q: AsRef>(from: P, to: Q) -> Result { + let from = from.as_ref(); + let to = to.as_ref(); + fs::copy(from, to) + .with_context(|| format!("Failed to copy {} to {}", from.display(), to.display())) + } + + pub fn remove_file>(path: P) -> Result<()> { + let path = path.as_ref(); + fs::remove_file(path).with_context(|| format!("Failed to remove file {}", path.display())) + } + + pub fn remove_dir_all>(path: P) -> Result<()> { + let path = path.as_ref(); + fs::remove_dir_all(path).with_context(|| format!("Failed to remove dir {}", path.display())) + } + + pub fn create_dir_all>(path: P) -> Result<()> { + let path = path.as_ref(); + fs::create_dir_all(path).with_context(|| format!("Failed to create dir {}", path.display())) + } +} + +#[macro_export] +macro_rules! run { + ($($expr:expr),*) => { + run!($($expr),*; echo = true) + }; + ($($expr:expr),* ; echo = $echo:expr) => { + $crate::glue::run_process(format!($($expr),*), $echo, None) + }; + ($($expr:expr),* ; <$stdin:expr) => { + $crate::glue::run_process(format!($($expr),*), false, Some($stdin)) + }; +} +pub use crate::run; + +pub struct Pushd { + _p: (), +} + +pub fn pushd(path: impl Into) -> Pushd { + Env::with(|env| env.pushd(path.into())); + Pushd { _p: () } +} + +impl Drop for Pushd { + fn drop(&mut self) { + Env::with(|env| env.popd()) + } +} + +pub struct Pushenv { + _p: (), +} + +pub fn pushenv(var: &str, value: &str) -> Pushenv { + Env::with(|env| env.pushenv(var.into(), value.into())); + Pushenv { _p: () } +} + +impl Drop for Pushenv { + fn drop(&mut self) { + Env::with(|env| env.popenv()) + } +} + +pub fn rm_rf(path: impl AsRef) -> Result<()> { + let path = path.as_ref(); + if !path.exists() { + return Ok(()); + } + if path.is_file() { + fs2::remove_file(path) + } else { + fs2::remove_dir_all(path) + } +} + +#[doc(hidden)] +pub fn run_process(cmd: String, echo: bool, stdin: Option<&[u8]>) -> Result { + run_process_inner(&cmd, echo, stdin).with_context(|| format!("process `{cmd}` failed")) +} + +pub fn date_iso() -> Result { + run!("date --iso --utc") +} + +fn run_process_inner(cmd: &str, echo: bool, stdin: Option<&[u8]>) -> Result { + let mut args = shelx(cmd); + let binary = args.remove(0); + let current_dir = Env::with(|it| it.cwd().to_path_buf()); + + if echo { + println!("> {cmd}") + } + + let mut command = Command::new(binary); + command + .args(args) + .current_dir(current_dir) + .stderr(Stdio::inherit()); + let output = match stdin { + None => command.stdin(Stdio::null()).output(), + Some(stdin) => { + command.stdin(Stdio::piped()).stdout(Stdio::piped()); + let mut process = command.spawn()?; + process.stdin.take().unwrap().write_all(stdin)?; + process.wait_with_output() + } + }?; + let stdout = String::from_utf8(output.stdout)?; + + if echo { + print!("{stdout}") + } + + if !output.status.success() { + bail!("{}", output.status) + } + + Ok(stdout.trim().to_string()) +} + +fn shelx(cmd: &str) -> Vec { + let mut res = Vec::new(); + for (string_piece, in_quotes) in cmd.split('\'').zip([false, true].iter().copied().cycle()) { + if in_quotes { + res.push(string_piece.to_string()) + } else if !string_piece.is_empty() { + res.extend( + string_piece + .split_ascii_whitespace() + .map(|it| it.to_string()), + ) + } + } + res +} + +struct Env { + pushd_stack: Vec, + pushenv_stack: Vec<(OsString, Option)>, +} + +impl Env { + fn with T, T>(f: F) -> T { + thread_local! { + static ENV: RefCell = RefCell::new(Env { + pushd_stack: vec![env::current_dir().unwrap()], + pushenv_stack: vec![], + }); + } + ENV.with(|it| f(&mut it.borrow_mut())) + } + + fn pushd(&mut self, dir: PathBuf) { + let dir = self.cwd().join(dir); + self.pushd_stack.push(dir); + env::set_current_dir(self.cwd()).unwrap(); + } + fn popd(&mut self) { + self.pushd_stack.pop().unwrap(); + env::set_current_dir(self.cwd()).unwrap(); + } + fn pushenv(&mut self, var: OsString, value: OsString) { + self.pushenv_stack.push((var.clone(), env::var_os(&var))); + env::set_var(var, value) + } + fn popenv(&mut self) { + let (var, value) = self.pushenv_stack.pop().unwrap(); + match value { + None => env::remove_var(var), + Some(value) => env::set_var(var, value), + } + } + fn cwd(&self) -> &Path { + self.pushd_stack.last().unwrap() + } +} + diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs new file mode 100644 index 00000000..7ad1ef14 --- /dev/null +++ b/xtask/src/lib.rs @@ -0,0 +1,78 @@ +//! Codegen tools mostly used to generate ast and syntax definitions. Adapted from rust analyzer's codegen + +pub mod glue; + +use std::{ + env, + fmt::Display, + path::{Path, PathBuf}, +}; + +pub use crate::glue::{pushd, pushenv}; + +pub use anyhow::{anyhow, bail, ensure, Context as _, Error, Result}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum Mode { + Overwrite, + Verify, +} + +pub fn project_root() -> PathBuf { + Path::new( + &env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| env!("CARGO_MANIFEST_DIR").to_owned()), + ) + .ancestors() + .nth(2) + .unwrap() + .to_path_buf() +} + +pub fn run_rustfmt(mode: Mode) -> Result<()> { + let _dir = pushd(project_root()); + let _e = pushenv("RUSTUP_TOOLCHAIN", "stable"); + ensure_rustfmt()?; + match mode { + Mode::Overwrite => run!("cargo fmt"), + Mode::Verify => run!("cargo fmt -- --check"), + }?; + Ok(()) +} + +pub fn reformat(text: impl Display) -> Result { + reformat_without_preamble(text).map(prepend_generated_preamble) +} + +pub fn reformat_with_command(text: impl Display, command: impl Display) -> Result { + reformat_without_preamble(text).map(|formatted| { + format!("//! This is a generated file. Don't modify it by hand! Run '{command}' to re-generate the file.\n\n{formatted}") + }) +} + +pub const PREAMBLE: &str = "Generated file, do not edit by hand, see `xtask/codegen`"; +pub fn prepend_generated_preamble(content: impl Display) -> String { + format!("//! {PREAMBLE}\n\n{content}") +} + +pub fn reformat_without_preamble(text: impl Display) -> Result { + let _e = pushenv("RUSTUP_TOOLCHAIN", "stable"); + ensure_rustfmt()?; + let output = run!( + "rustfmt --config newline_style=Unix"; + Result<()> { + let out = run!("rustfmt --version")?; + if !out.contains("stable") { + bail!( + "Failed to run rustfmt from toolchain 'stable'. \ + Please run `rustup component add rustfmt --toolchain stable` to install it.", + ) + } + Ok(()) +} +