From 47e84453508d0375124bde1e219946c5226486b1 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Sat, 30 May 2026 21:17:53 -0700 Subject: [PATCH 1/3] feat: path macro and cargo preset prints crates in order --- packages/copper/Cargo.toml | 14 +- .../copper/src/process/pio/cargo_preset.rs | 7 +- packages/copper/src/str/mod.rs | 3 + packages/copper/src/str/path_macro.rs | 371 ++++++++++++++++++ 4 files changed, 384 insertions(+), 11 deletions(-) create mode 100644 packages/copper/src/str/path_macro.rs diff --git a/packages/copper/Cargo.toml b/packages/copper/Cargo.toml index 2ddaa58..84fe269 100644 --- a/packages/copper/Cargo.toml +++ b/packages/copper/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pistonite-cu" -version = "0.8.2" +version = "0.8.3" edition = "2024" description = "Battery-included common utils to speed up development of rust tools" repository = "https://github.com/Pistonite/cu" @@ -15,7 +15,7 @@ pistonite-cu-proc-macros = { version = "0.2.8", path = "../copper-proc-macros" } # --- Always enabled --- anyhow = "1.0.102" -log = "0.4.29" +log = "0.4.30" # --- Command Line Interface --- oneshot = { version = "0.2.1", optional = true, features = ["std"] } @@ -28,7 +28,7 @@ ctrlc = { version = "3.5.2", optional = true } # --- Coroutine --- num_cpus = { version = "1.17.0", optional = true } -tokio = { version = "1.52.1", optional = true, features = [ +tokio = { version = "1.52.3", optional = true, features = [ "macros", "rt-multi-thread" ] } @@ -36,13 +36,13 @@ tokio = { version = "1.52.1", optional = true, features = [ dunce = {version="1.0.5", optional = true} which = {version = "8.0.2", optional = true } pathdiff = {version = "0.2.3", optional=true} -filetime = { version = "0.2.27", optional = true} +filetime = { version = "0.2.29", optional = true} glob = { version = "0.3.3", optional = true } -spin = {version = "0.10.0", optional = true} # for PIO +spin = {version = "0.12.0", optional = true} # for PIO # serde serde = { version = "1.0.228", features = ["derive"], optional = true } -serde_json = { version = "1.0.149", optional = true } +serde_json = { version = "1.0.150", optional = true } serde_yaml_ng = { version = "0.10.0", optional = true } toml = { version = "1.1.2", optional = true } @@ -58,7 +58,7 @@ windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_Syste [dev-dependencies] [dev-dependencies.tokio] -version = "1.52.1" +version = "1.52.3" features = [ "macros", "rt-multi-thread", "time" ] [features] diff --git a/packages/copper/src/process/pio/cargo_preset.rs b/packages/copper/src/process/pio/cargo_preset.rs index e58783b..229e119 100644 --- a/packages/copper/src/process/pio/cargo_preset.rs +++ b/packages/copper/src/process/pio/cargo_preset.rs @@ -1,4 +1,3 @@ -use std::collections::BTreeSet; use std::process::Stdio; use std::sync::{Arc, LazyLock}; @@ -266,7 +265,7 @@ struct PrintState { other_lv: Lv, bar: Arc, done_count: usize, - in_progress: BTreeSet, + in_progress: Vec, // using a vec to preserve order buf: String, diagnostic_hook: Option, stderr_printing_message_lv: Option, @@ -316,7 +315,7 @@ impl PrintState { return; } self.done_count += 1; - self.in_progress.remove(target.name); + self.in_progress.retain(|x| x != target.name); self.update_bar(); } "compiler-message" => { @@ -409,7 +408,7 @@ impl PrintState { None => line, Some(i) => &line[..i], }; - self.in_progress.insert(crate_name.replace('-', "_")); + self.in_progress.push(crate_name.replace('-', "_")); self.update_bar(); } diff --git a/packages/copper/src/str/mod.rs b/packages/copper/src/str/mod.rs index 692a261..715e20f 100644 --- a/packages/copper/src/str/mod.rs +++ b/packages/copper/src/str/mod.rs @@ -14,3 +14,6 @@ pub use osstring::{OsStrExtension, OsStrExtensionOwned}; mod path; #[cfg(feature = "fs")] pub use path::PathExtension; + +// path macro just depends on the std path functions and does not require fs +mod path_macro; diff --git a/packages/copper/src/str/path_macro.rs b/packages/copper/src/str/path_macro.rs new file mode 100644 index 0000000..f121274 --- /dev/null +++ b/packages/copper/src/str/path_macro.rs @@ -0,0 +1,371 @@ +/// Efficient Path-join macro +/// +/// **The macro rules above is only for illustration purpose, see source code for implementation** +/// +/// ## Usage +/// The macro efficiently creates joined paths from either a owned `PathBuf` +/// or a borrowed Path reference (`impl AsRef`), and one or more path segments reference +/// to join. The OS separator is used (i.e. `\` on Windows). +/// +/// The format of the macro in pseudocode is: +/// ```rust,ignore +/// cu::path!( FIRST_SEG $( / NEXT_SEG )* ) +/// ``` +/// +/// `FIRST_SEG` can be: +/// - A owned `PathBuf` ident +/// - e.g. `cu::path!(my_path_buf / ...)` +/// - A borrowed `&Path` ident: +/// - e.g. `cu::path!(&my_path / ...)` +/// - Here `&` is the macro rule to indicate you don't want to borrow the path, so you need +/// it even when `my_path` is already a borrowed path +/// - A literal string, which you can use without `&` +/// - e.g. `cu::path!("my_path" / ...)` +/// - An expression that evaluates to a owned `PathBuf` +/// - e.g. `cu::path!( (get_path()) / ...)` +/// - Expression needs to be parenthesized because `/` cannot follow an expression in macro +/// rules. `{ }` also works +/// - An expression that evaludates to a borrowed `&Path` +/// - e.g. `cu::path!( &(my.path) / ... )` +/// - Expression needs to be parenthesized because `/` cannot follow an expression in macro +/// rules. `{ }` also works +/// - Here `&` is the macro rule to indicate you don't want to borrow the path, so you need +/// it even when `my_path` is already a borrowed path +/// +/// Each `NEXT_SEG` can be: +/// - A literal string +/// - An ident (the macro will not take ownership of the variable) +/// - An expression wrapped with either `( )` or `{ }`. The last expression doesn't need to be +/// wrapped +/// +/// ## Examples +/// ```rust +/// # use pistonite_cu as cu; +/// use std::path::{Path, PathBuf}; +/// +/// // From a literal string +/// let p1 = cu::path!("home" / "user"); +/// let p2 = cu::path!("home" / "user" / "docs"); +/// assert_eq!(p1, PathBuf::from("home").join("user")); +/// assert_eq!(p2, PathBuf::from("home").join("user").join("docs")); +/// +/// // From an owned PathBuf ident (base is moved) +/// let base = PathBuf::from("usr").join("local"); +/// let p = cu::path!(base / "bin" / "tool"); +/// assert_eq!(p, PathBuf::from("usr").join("local").join("bin").join("tool")); +/// +/// // From a borrowed &Path ident (use `&` even if already a reference) +/// let base = PathBuf::from("etc"); +/// let base_ref: &Path = base.as_path(); +/// let p = cu::path!(&base_ref / "nginx" / "nginx.conf"); +/// assert_eq!(p, PathBuf::from("etc").join("nginx").join("nginx.conf")); +/// +/// // From an expression returning PathBuf (must be parenthesized) +/// let p = cu::path!((PathBuf::from("usr").join("local")) / "bin"); +/// assert_eq!(p, PathBuf::from("usr").join("local").join("bin")); +/// +/// // From an expression returning &Path (must be parenthesized, and needs `&`) +/// let owned = PathBuf::from("var"); +/// let p = cu::path!(&(owned.as_path()) / "log"); +/// assert_eq!(p, PathBuf::from("var").join("log")); +/// +/// // NEXT_SEG can be an ident — not moved, still usable after +/// let dir = "subdir"; +/// let file = "file.txt"; +/// let p = cu::path!("root" / dir / file); +/// assert_eq!(p, PathBuf::from("root").join(dir).join(file)); +/// let _ = (dir, file); // still accessible +/// +/// // NEXT_SEG can be an expression (must be parenthesized) +/// let sub = String::from("sub"); +/// let p = cu::path!("root" / (sub.as_str()) / "output.log"); +/// assert_eq!(p, PathBuf::from("root").join("sub").join("output.log")); +/// ``` +/// +/// ## Implementation +/// Currently this uses the same implementation as the standard library (as of 1.95.0) +/// that does not do any probing to pre-allocate the path based on the input iterator. +/// Each segment is `.push()`-ed onto the initial buffer in a loop. +/// +#[cfg(doc)] +#[macro_export] +macro_rules! path { + ($(&)? ident / $(ident_or_literal_or_expr) / * ) => {}; + (literal / $(ident_or_literal_or_expr) / * ) => {}; + ($(&)? ( path_expression ) / $(ident_or_literal_or_expr) / *) => {}; +} + +#[cfg(not(doc))] +#[macro_export] +macro_rules! path { + ($first:ident / $($rest_segs:tt)* ) => {{ + let mut x = $first; + $crate::__path_internal!(x / $($rest_segs)*) + }}; + ( & $first:ident / $($rest_segs:tt)* ) => {{ + let mut x = std::path::PathBuf::from($first.to_owned()); + $crate::__path_internal!(x / $($rest_segs)*) + }}; + ($first:literal / $($rest_segs:tt)* ) => {{ + let mut x = std::path::PathBuf::from($first); + $crate::__path_internal!(x / $($rest_segs)*) + }}; + (( $first:expr ) / $($rest_segs:tt)* ) => {{ + let mut x: ::std::path::PathBuf = { $first }; + $crate::__path_internal!(x / $($rest_segs)*) + }}; + ( & ( $first:expr ) / $($rest_segs:tt)* ) => {{ + let x: &::std::path::Path = {$first}.as_ref(); + let mut x = x.to_path_buf(); + $crate::__path_internal!(x / $($rest_segs)*) + }}; + ( $first:block / $($rest_segs:tt)* ) => {{ + let mut x: ::std::path::PathBuf = $first; + $crate::__path_internal!(x / $($rest_segs)*) + }}; + ( & $first:block / $($rest_segs:tt)* ) => {{ + let x: &::std::path::Path = $first.as_ref(); + let mut x = x.to_path_buf(); + $crate::__path_internal!(x / $($rest_segs)*) + }}; +} + +#[macro_export] +#[doc(hidden)] +macro_rules! __path_internal { + // Non-expression terminals (1 or 2 remaining) + ($first:ident / $second:literal) => {{ + $first.push($second); $first + }}; + ($first:ident / $second:ident) => {{ + let x: &::std::path::Path = $second.as_ref(); + $first.push(x); $first + }}; + ($first:ident / $second:literal / $third:literal) => {{ + $first.push($second); $first.push($third); $first + }}; + ($first:ident / $second:literal / $third:ident) => {{ + $first.push($second); + let x: &::std::path::Path = $third.as_ref(); + $first.push(x); $first + }}; + ($first:ident / $second:ident / $third:literal) => {{ + let x: &::std::path::Path = $second.as_ref(); + $first.push(x); + $first.push($third); $first + }}; + ($first:ident / $second:ident / $third:ident) => {{ + let x: &::std::path::Path = $second.as_ref(); + $first.push(x); + let x: &::std::path::Path = $third.as_ref(); + $first.push(x); $first + }}; + + // non-terminal muchering (2 at a time) + ($first:ident / $second:literal / $third:literal / $($rest_segs:tt)* ) => {{ + $first.push($second); $first.push($third); + $crate::__path_internal!($first / $($rest_segs)* ) + }}; + ($first:ident / $second:literal / $third:ident / $($rest_segs:tt)* ) => {{ + $first.push($second); + let x: &::std::path::Path = $third.as_ref(); + $first.push(x); + $crate::__path_internal!($first / $($rest_segs)* ) + }}; + ($first:ident / $second:literal / ( $third:expr ) / $($rest_segs:tt)* ) => {{ + $first.push($second); + let x: &::std::path::Path = {$third}.as_ref(); + $first.push(x); + $crate::__path_internal!($first / $($rest_segs)* ) + }}; + ($first:ident / $second:literal / $third:block / $($rest_segs:tt)* ) => {{ + $first.push($second); + let x: &::std::path::Path = $third.as_ref(); + $first.push(x); + $crate::__path_internal!($first / $($rest_segs)* ) + }}; + ($first:ident / $second:ident / $third:literal / $($rest_segs:tt)* ) => {{ + let x: &::std::path::Path = $second.as_ref(); + $first.push(x); + $first.push($third); + $crate::__path_internal!($first / $($rest_segs)* ) + }}; + ($first:ident / $second:ident / $third:ident / $($rest_segs:tt)* ) => {{ + let x: &::std::path::Path = $second.as_ref(); + $first.push(x); + let x: &::std::path::Path = $third.as_ref(); + $first.push(x); + $crate::__path_internal!($first / $($rest_segs)* ) + }}; + ($first:ident / $second:ident / ( $third:expr ) / $($rest_segs:tt)* ) => {{ + let x: &::std::path::Path = $second.as_ref(); + $first.push(x); + let x: &::std::path::Path = {$third}.as_ref(); + $first.push(x); + $crate::__path_internal!($first / $($rest_segs)* ) + }}; + ($first:ident / $second:ident / $third:block / $($rest_segs:tt)* ) => {{ + let x: &::std::path::Path = $second.as_ref(); + $first.push(x); + let x: &::std::path::Path = $third.as_ref(); + $first.push(x); + $crate::__path_internal!($first / $($rest_segs)* ) + }}; + // expression muchering (1 at a time) + ($first:ident / ( $second:expr ) / $($rest_segs:tt)* ) => {{ + let x: &::std::path::Path = {$second}.as_ref(); + $first.push(x); + $crate::__path_internal!($first / $($rest_segs)* ) + }}; + ($first:ident / $second:block / $($rest_segs:tt)* ) => {{ + let x: &::std::path::Path = $second.as_ref(); + $first.push(x); + $crate::__path_internal!($first / $($rest_segs)* ) + }}; + + // expression muchering, must be after the non-expression rules + // so the `/` doesn't get interpreted as an operator + + ($first:ident / $second:literal / $third:expr) => {{ + $first.push($second); + let x: &::std::path::Path = {$third}.as_ref(); + $first.push(x); $first + }}; + ($first:ident / $second:ident / $third:expr) => {{ + let x: &::std::path::Path = $second.as_ref(); + $first.push(x); + let x: &::std::path::Path = {$third}.as_ref(); + $first.push(x); $first + }}; + ($first:ident / $second:expr) => {{ + let x: &::std::path::Path = {$second}.as_ref(); + $first.push(x); $first + }}; + +} + +#[cfg(test)] +mod tests { + use std::path::{Path, PathBuf}; + + fn long_expected() -> PathBuf { + PathBuf::from("a") + .join("b") + .join("c") + .join("d") + .join("e") + .join("f") + .join("g") + .join("h") + .join("i") + .join("j") + } + + // literal first, mix of literal / ident / expr segments + #[test] + fn long_path_from_literal() { + let c = "c"; + let e = String::from("e"); + let h = "h"; + let p = crate::path!("a" / "b" / (c) / "d" / (e.as_str()) / "f" / "g" / h / "i" / "j"); + assert_eq!(p, long_expected()); + let _ = (c, h); // idents still accessible + } + + // owned PathBuf first, mix of ident / expr / literal segments + #[test] + fn long_path_from_owned() { + let base = PathBuf::from("a"); + let b = "b"; + let d = String::from("d"); + let g = "g"; + let p = crate::path!(base / (b) / "c" / (d.as_str()) / "e" / "f" / g / "h" / "i" / "j"); + assert_eq!(p, long_expected()); + let _ = (b, g); + } + + // borrowed &Path first, mix of expr / literal / ident segments + #[test] + fn long_path_from_borrowed() { + let base = PathBuf::from("a"); + let base_ref: &Path = base.as_path(); + let c = String::from("c"); + let f = "f"; + let i = "i"; + let p = crate::path!(&base_ref / "b" / (c.as_str()) / "d" / "e" / f / "g" / "h" / i / "j"); + assert_eq!(p, long_expected()); + let _ = (f, i); + } + + // expr-owned first, mix of literal / ident / expr segments + #[test] + fn long_path_from_expr_owned() { + let d = "d"; + let e = String::from("e"); + let f = String::from("f"); + let p = crate::path!( + (PathBuf::from("a")) / "b" / "c" / d / (&e) / (f.as_str()) / "g" / "h" / "i" / "j" + ); + assert_eq!(p, long_expected()); + let _ = d; + } + + // expr-borrowed first, mix of ident / expr / literal segments + #[test] + fn long_path_from_expr_borrowed() { + let base = PathBuf::from("a"); + let b = "b"; + let e = String::from("e"); + let j = "j"; + let p = crate::path!( + &(base.as_path()) / b / "c" / "d" / (e.as_str()) / "f" / "g" / "h" / "i" / j + ); + assert_eq!(p, long_expected()); + let _ = (b, j); + } + + // no two consecutive segments share a type: cycles ident / expr / literal throughout + // 10 segments (even) — exercises even-count terminal arm + #[test] + fn long_path_no_consecutive_same_type_even() { + let b = "b"; + let c = String::from("c"); + let e = "e"; + let f = String::from("f"); + let h = "h"; + let i = String::from("i"); + let p = crate::path!( + "a" / b / (c.as_str()) / "d" / e / (f.as_str()) / "g" / h / (i.as_str()) / "j" + ); + assert_eq!(p, long_expected()); + let _ = (b, e, h); + } + + fn nine_expected() -> PathBuf { + PathBuf::from("a") + .join("b") + .join("c") + .join("d") + .join("e") + .join("f") + .join("g") + .join("h") + .join("i") + } + + // no two consecutive segments share a type: cycles literal / ident / expr throughout + // 9 segments (odd) — exercises odd-count terminal arm + #[test] + fn long_path_no_consecutive_same_type_odd() { + let b = "b"; + let c = String::from("c"); + let e = "e"; + let f = String::from("f"); + let h = "h"; + let i = String::from("i"); + let p = + crate::path!("a" / b / (c.as_str()) / "d" / e / (f.as_str()) / "g" / h / i.as_str()); + assert_eq!(p, nine_expected()); + let _ = (b, e, h); + } +} From d1f668b6cd47263916ee5dc0d23f4a3b50c01ff7 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Sat, 30 May 2026 21:19:29 -0700 Subject: [PATCH 2/3] run tests on windows as well --- .github/workflows/ci.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a7f556..7e6f935 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,13 +14,20 @@ jobs: with: rust: stable - run: task check - test: + test-linux: runs-on: blacksmith-4vcpu-ubuntu-2404 steps: - uses: Pistonight/mono-dev/actions/setup@main with: rust: stable - run: task test + test-windows: + runs-on: blacksmith-4vcpu-windows-2025 + steps: + - uses: Pistonight/mono-dev/actions/setup@main + with: + rust: stable + - run: task test doc: runs-on: blacksmith-4vcpu-ubuntu-2404 steps: From 4a34f52e1099602016cea125d23c41e8d8a904f5 Mon Sep 17 00:00:00 2001 From: Michael Zhao <44533763+Pistonight@users.noreply.github.com> Date: Sat, 30 May 2026 21:28:56 -0700 Subject: [PATCH 3/3] fix windows test issues --- packages/copper/src/process/pio/read.rs | 4 ++++ packages/copper/src/process/pio/write.rs | 1 + packages/terminal-tests/Taskfile.yml | 1 + 3 files changed, 6 insertions(+) diff --git a/packages/copper/src/process/pio/read.rs b/packages/copper/src/process/pio/read.rs index 62f8092..19bea85 100644 --- a/packages/copper/src/process/pio/read.rs +++ b/packages/copper/src/process/pio/read.rs @@ -24,6 +24,7 @@ use super::{ChildOutConfig, ChildOutTask}; /// /// assert_eq!(b"Hello, world!\n".to_vec(), out.join()??); /// # Ok(()) } +/// # #[cfg(not(unix))] fn main() {} /// ``` #[inline(always)] pub fn buffer() -> Buffer { @@ -87,6 +88,7 @@ impl ChildOutTask for BufferTask { /// /// assert_eq!("Hello, world!\n", out.join()??); /// # Ok(()) } +/// # #[cfg(not(unix))] fn main() {} /// ``` #[inline(always)] pub fn string() -> BufferString { @@ -175,6 +177,7 @@ async fn read_to_end(r: Result) -> crate::Result CoLines { diff --git a/packages/copper/src/process/pio/write.rs b/packages/copper/src/process/pio/write.rs index 381562b..d39aca0 100644 --- a/packages/copper/src/process/pio/write.rs +++ b/packages/copper/src/process/pio/write.rs @@ -27,6 +27,7 @@ use super::{ChildInConfig, ChildInTask}; /// /// assert_eq!(b"foobar\n".to_vec(), out.join()??); /// # Ok(()) } +/// # #[cfg(not(unix))] fn main() {} /// ``` #[inline(always)] pub fn write + Send + 'static>(buf: B) -> Write { diff --git a/packages/terminal-tests/Taskfile.yml b/packages/terminal-tests/Taskfile.yml index b141f5e..d9fbe2a 100644 --- a/packages/terminal-tests/Taskfile.yml +++ b/packages/terminal-tests/Taskfile.yml @@ -8,6 +8,7 @@ includes: tasks: run: + platforms: [linux, darwin] desc: Run terminal tests cmds: - cargo run --features bin -- {{.CLI_ARGS}}