From fe7e5738cba5d5ecbe0b6b2998e305457a9d130d Mon Sep 17 00:00:00 2001 From: Pistonight Date: Tue, 9 Jun 2026 19:46:51 -0700 Subject: [PATCH 1/4] working on sbin --- packages/corelib/src/hmgr/paths.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/corelib/src/hmgr/paths.rs b/packages/corelib/src/hmgr/paths.rs index 1272120..37a52a1 100644 --- a/packages/corelib/src/hmgr/paths.rs +++ b/packages/corelib/src/hmgr/paths.rs @@ -66,6 +66,8 @@ pub(crate) fn home() -> &'static Path { home! { bin_root: "bin", binary: bin_root / file, + sbin_root: "sbin", + system_binary: sbin_root / file, config_root: "config", config_toml: config_root / "core.toml", install_root: "install", From d87c3f5c6cc97fadb46773f3042b4e6f56484b4b Mon Sep 17 00:00:00 2001 From: Michael Zhao <44533763+Pistonight@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:53:02 -0700 Subject: [PATCH 2/4] ms coreutils --- Taskfile.yml | 2 - packages/corelib/src/epkg/cargo.rs | 11 +- packages/corelib/src/hmgr/env/windows.rs | 1 + packages/corelib/src/hmgr/environment.rs | 2 +- packages/corelib/src/hmgr/item.rs | 71 ++++- packages/corelib/src/opfs/fs_util.rs | 12 +- packages/registry/metadata.toml | 11 +- .../registry/src/packages/cargo-binstall.rs | 2 +- .../src/packages/coreutils/config.toml | 31 +- .../registry/src/packages/coreutils/eza.rs | 37 ++- .../packages/coreutils/mod_linux-pacman.rs | 1 + .../src/packages/coreutils/mod_win.rs | 290 ++++++++++++------ .../coreutils/pwsh-install-template.ps1 | 218 +++++++++++++ .../registry/src/packages/coreutils/sed.rs | 18 ++ packages/registry/src/packages/git-cfg/mod.rs | 2 +- .../registry/src/packages/pwsh/mod_win.rs | 7 +- .../src/packages/shellutils/mod_win.rs | 6 +- .../registry/src/packages/starship/mod.rs | 2 +- packages/registry/src/util/macros/verify.rs | 22 ++ packages/registry/update/packages.mts | 2 +- 20 files changed, 568 insertions(+), 180 deletions(-) create mode 100644 packages/registry/src/packages/coreutils/pwsh-install-template.ps1 create mode 100644 packages/registry/src/packages/coreutils/sed.rs diff --git a/Taskfile.yml b/Taskfile.yml index a47f5c2..54ba5f9 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -96,7 +96,5 @@ tasks: echo "$new_version" > VERSION; echo "bumped version: $current -> $new_version"; - - upgrade: - cargo upgrade diff --git a/packages/corelib/src/epkg/cargo.rs b/packages/corelib/src/epkg/cargo.rs index f2f4ca3..f9a816d 100644 --- a/packages/corelib/src/epkg/cargo.rs +++ b/packages/corelib/src/epkg/cargo.rs @@ -84,6 +84,7 @@ pub fn install_git_commit( package: &str, git: &str, rev: &str, + root: Option<&str>, bar: Option<&Arc>, ) -> cu::Result<()> { let mut state = cargo::instance()?; @@ -93,6 +94,10 @@ pub fn install_git_commit( // rust-toolchain file, which will override the rust toolchain being used .current_dir(hmgr::paths::home()) .args(["install", package, "--git", git, "--rev", rev, "--locked"]); + let command = match root { + None => command, + Some(root) => command.args(["--force", "-q", "--root", root]) + }; let command = add_platform_build_args(command); let (child, bar) = command .preset( @@ -109,12 +114,16 @@ pub fn install_git_commit( /// Install a package using `cargo install` #[cu::context("failed to install '{package}' with cargo")] -pub fn install(package: &str, bar: Option<&Arc>) -> cu::Result<()> { +pub fn install(package: &str, root: Option<&str>, bar: Option<&Arc>) -> cu::Result<()> { let mut state = cargo::instance()?; let command = cu::which("cargo")? .command() .current_dir(hmgr::paths::home()) .args(["install", package, "--locked"]); + let command = match root { + None => command, + Some(root) => command.args(["-q", "--root", root]) + }; let command = add_platform_build_args(command); let (child, bar) = command .preset( diff --git a/packages/corelib/src/hmgr/env/windows.rs b/packages/corelib/src/hmgr/env/windows.rs index a890ac3..37c8864 100644 --- a/packages/corelib/src/hmgr/env/windows.rs +++ b/packages/corelib/src/hmgr/env/windows.rs @@ -203,6 +203,7 @@ impl Env { Item::Pwsh(_) => self.pwsh_dirty = true, Item::Cmd(_) => self.cmd_dirty = true, Item::LinkBin(_, _, _) => {} + Item::LinkSysBin(_, _) => {} Item::ShimBin(_, _) => {} Item::Bash(_) => {} Item::Zsh(_) => {} diff --git a/packages/corelib/src/hmgr/environment.rs b/packages/corelib/src/hmgr/environment.rs index cd0a7b5..c139f53 100644 --- a/packages/corelib/src/hmgr/environment.rs +++ b/packages/corelib/src/hmgr/environment.rs @@ -9,7 +9,7 @@ pub mod windows { use std::collections::BTreeMap; use std::sync::Mutex; - pub use win_envedit::get_user; + pub use win_envedit::{get_user, get_system}; static USER_THIS_SESSION: Mutex> = Mutex::new(BTreeMap::new()); /// Get user environment variable at the start of the session, /// before any set calls diff --git a/packages/corelib/src/hmgr/item.rs b/packages/corelib/src/hmgr/item.rs index 40530ad..7dfcad2 100644 --- a/packages/corelib/src/hmgr/item.rs +++ b/packages/corelib/src/hmgr/item.rs @@ -70,6 +70,7 @@ impl ItemMgr { #[cfg(target_os = "linux")] Item::SessionEnvVar(_, _, _) => {} Item::LinkBin(_, _, _) => self.link_dirty = true, + Item::LinkSysBin(_, _) => self.link_dirty = true, Item::ShimBin(_, _) => self.shim_dirty = true, Item::Pwsh(_) => {} Item::Bash(_) => {} @@ -94,6 +95,7 @@ impl ItemMgr { package: Option<&str>, // none for removing all ) -> cu::Result<()> { let mut bin_to_remove = vec![]; + let mut sbin_to_remove = vec![]; #[cfg(windows)] let mut env_to_remove = BTreeMap::new(); #[cfg(windows)] @@ -128,6 +130,10 @@ impl ItemMgr { bin_to_remove.push(bin.to_string()); // removing a link does not make links dirty } + Item::LinkSysBin(bin, _) => { + sbin_to_remove.push(bin.to_string()); + // removing a link does not make links dirty + } Item::ShimBin(bin, _) => { bin_to_remove.push(bin.to_string()); self.shim_dirty = true; @@ -150,6 +156,14 @@ impl ItemMgr { } } } + if !sbin_to_remove.is_empty() { + let bin_root = hmgr::paths::sbin_root(); + for bin in sbin_to_remove { + if let Err(e) = opfs::safe_remove_link(&bin_root.join(bin)) { + cu::warn!("failed to remove old link: {e}"); + } + } + } #[cfg(windows)] { @@ -215,25 +229,40 @@ impl ItemMgr { #[cu::context("failed to build binary links")] fn rebuild_links(&mut self) -> cu::Result<()> { let bin_root = hmgr::paths::bin_root(); + let sbin_root = hmgr::paths::sbin_root(); cu::fs::make_dir(&bin_root)?; + cu::fs::make_dir(&sbin_root)?; let mut link_paths = vec![]; + let mut sys_link_paths = vec![]; for entry in &self.items { - let Item::LinkBin(from, to, non_exe) = &entry.item else { - continue; - }; - let link_path = bin_root.join(from); - if link_path.exists() { - // assume existing file is from linking previously - continue; + match &entry.item { + Item::LinkBin(from, to, non_exe) => { + let link_path = bin_root.join(from); + if link_path.exists() { + // assume existing file is from linking previously + continue; + } + link_paths.push((link_path, to, non_exe)); + } + Item::LinkSysBin(from, to) => { + let link_path = sbin_root.join(from); + if link_path.exists() { + // assume existing file is from linking previously + continue; + } + sys_link_paths.push((link_path, to)); + } + _ => {} } - link_paths.push((link_path, to, non_exe)); } - let link_paths2: Vec<(&Path, &Path)> = link_paths + let link_paths_iter = link_paths .iter() - .map(|(x, y, _)| (x.as_path(), y.as_ref())) - .collect(); - - opfs::hardlink_files(&link_paths2)?; + .map(|(x, y, _)| (x.as_path(), Path::new(y))); + opfs::hardlink_files(link_paths_iter)?; + let sys_link_paths_iter = sys_link_paths + .iter() + .map(|(x, y)| (x.as_path(), Path::new(y))); + opfs::hardlink_files(sys_link_paths_iter)?; #[cfg(not(windows))] { @@ -315,7 +344,7 @@ impl ItemMgr { let shim_binary_old = hmgr::paths::shim_binary_old(); if shim_binary.exists() { // hardlink the old binary, so we can start deleting the old links - opfs::hardlink_files(&[(&shim_binary_old, &shim_binary)])?; + opfs::hardlink_files([(&shim_binary_old, &shim_binary)].into_iter())?; } // the old binary could be in use, which will not allow us to copy it, @@ -333,10 +362,9 @@ impl ItemMgr { } let link_paths = link_paths .iter() - .map(|x| (x.as_path(), shim_binary.as_path())) - .collect::>(); + .map(|x| (x.as_path(), shim_binary.as_path())); cu::check!( - opfs::hardlink_files(&link_paths), + opfs::hardlink_files(link_paths), "failed to create hardlinks for shim binaries" )?; @@ -382,6 +410,10 @@ pub enum Item { /// in the install directory. LinkBin(String, String, bool /* non_executable */), + /// Link a system binary (in the HOME/sbin directory) to a location + /// in the install directory. + LinkSysBin(String, String), + /// Create a shim binary that invokes a command. /// /// This is useful in 2 scenarios: @@ -436,6 +468,11 @@ impl Item { Self::LinkBin(name.into(), target.into(), false) } + #[inline(always)] + pub fn link_sys_bin(name: impl Into, target: impl Into) -> Self { + Self::LinkSysBin(name.into(), target.into()) + } + #[inline(always)] #[cfg(not(windows))] pub fn link_non_exe(name: impl Into, target: impl Into) -> Self { diff --git a/packages/corelib/src/opfs/fs_util.rs b/packages/corelib/src/opfs/fs_util.rs index ddafdd8..fd264c8 100644 --- a/packages/corelib/src/opfs/fs_util.rs +++ b/packages/corelib/src/opfs/fs_util.rs @@ -60,9 +60,13 @@ pub fn symlink_files(paths: &[(&Path, &Path)]) -> cu::Result<()> { /// Create hardlinks. `from` is where the link will be and `to` is the target of the link #[cfg(windows)] #[cu::context("failed to create hard links")] -pub fn hardlink_files(paths: &[(&Path, &Path)]) -> cu::Result<()> { +pub fn hardlink_files(paths: impl Iterator, impl AsRef)>) -> cu::Result<()> { let mut script = String::new(); + let mut empty = true; for (from, to) in paths { + empty = false; + let from = from.as_ref(); + let to = to.as_ref(); safe_remove_link(from)?; let from_abs = from.normalize()?; let to_abs = to.normalize()?; @@ -70,6 +74,9 @@ pub fn hardlink_files(paths: &[(&Path, &Path)]) -> cu::Result<()> { let to_str = to_abs.as_utf8()?; build_link_powershell(&mut script, "HardLink", from_str, to_str); } + if empty { + return Ok(()); + } cu::which("powershell")? .command() .args(["-NoLogo", "-NoProfile", "-c", &script]) @@ -90,8 +97,9 @@ fn build_link_powershell(out: &mut String, link_type: &str, from: &str, to: &str /// Create hardlinks. `from` is where the link will be and `to` is the target of the link #[cfg(not(windows))] #[cu::context("failed to create hard links")] -pub fn hardlink_files(paths: &[(&Path, &Path)]) -> cu::Result<()> { +pub fn hardlink_files(paths: impl Iterator, impl AsRef)>) -> cu::Result<()> { for (from, to) in paths { + let from = from.as_ref(); cu::fs::remove(from)?; std::fs::hard_link(to, from)?; } diff --git a/packages/registry/metadata.toml b/packages/registry/metadata.toml index 8dd4d85..ed5753d 100644 --- a/packages/registry/metadata.toml +++ b/packages/registry/metadata.toml @@ -21,16 +21,19 @@ SHA.'opfs::CpuArch::X64' = "9965153b4d3308dd5b3cb3d1a800b89b7b93a345b09f208e0f9b VERSION = "1.19.1" [coreutils] -ALIAS_VERSION = "13" +ALIAS_VERSION = "14" eza.VERSION = "0.23.4" -uutils.VERSION = "0.9.0" zip.VERSION = "3.0" unzip.VERSION = "6.0" tar.VERSION = "1.35" which.VERSION = "2.25" -bash.VERSION = "5.3.9" +bash.VERSION = "5.3.12" bash_cmp.VERSION = "2.17.0" -yay.VERSION = "12.5.7" +yay.VERSION = "12.6.0" +ms_coreutils.VERSION = "2026.5.29" +ms_coreutils.REPO = "https://github.com/microsoft/coreutils" +ms_coreutils.COMMIT = "e0dea0bffc58dd697f3ae60315f8579e2bf9b86f" # -- MUST BE MANUALLY PICKED! -- new utils/breaking changes likely to the setup process +uutils_sed.VERSION = "0.1.1" [shellutils] ALIAS_VERSION = "8" diff --git a/packages/registry/src/packages/cargo-binstall.rs b/packages/registry/src/packages/cargo-binstall.rs index 7f87f4a..497a4a0 100644 --- a/packages/registry/src/packages/cargo-binstall.rs +++ b/packages/registry/src/packages/cargo-binstall.rs @@ -14,7 +14,7 @@ pub fn install(ctx: &Context) -> cu::Result<()> { if cu::which("cargo-binstall").is_ok() { epkg::cargo::binstall("cargo-binstall", ctx.bar_ref()) } else { - epkg::cargo::install("cargo-binstall", ctx.bar_ref()) + epkg::cargo::install("cargo-binstall", None, ctx.bar_ref()) } } diff --git a/packages/registry/src/packages/coreutils/config.toml b/packages/registry/src/packages/coreutils/config.toml index 596f35c..6b53c39 100644 --- a/packages/registry/src/packages/coreutils/config.toml +++ b/packages/registry/src/packages/coreutils/config.toml @@ -3,33 +3,11 @@ key = ["windows"] comment = "Options for Windows-specific settings" children = [ { - key = ["exclude-coreutils"], - value = [], + key = ["prefer-gnu"], + value = true, comment = """ -Exclude creating shim executables for coreutils listed. -You would have to run coreutils to run the util. -Set it to the following to avoid conflicts with CMD and -native Windows utilities with the same names: -exclude-coreutils = [ - # System32 - 'expand', - 'hostname', - 'more', - 'whoami', - 'sort', - 'find', - # CMD - 'rmdir', - 'dir', - 'type', -] - -'diff' and 'find' can also be excluded (despite not being actually coreutils) - -The following utils are usually not used, and often conflicts, -so they are always removed: -- `link.exe` - conflicts with MSVC - +For commands with compatibility for both DOS and GNU, +prefer GNU when ambiguous """ }, { @@ -38,7 +16,6 @@ so they are always removed: comment = """ Add a doskey macro to make the mkdir command in CMD GNU-compatible. (Essentially alias mkdir in CMD to mkdir -p) -This will work even if mkdir is excluded in exclude-coreutils """ } ] diff --git a/packages/registry/src/packages/coreutils/eza.rs b/packages/registry/src/packages/coreutils/eza.rs index 824b942..a6f2600 100644 --- a/packages/registry/src/packages/coreutils/eza.rs +++ b/packages/registry/src/packages/coreutils/eza.rs @@ -1,34 +1,37 @@ use crate::pre::*; pub fn verify() -> cu::Result { - let v = check_cargo!("eza"); - check_outdated!(&v.version, metadata[coreutils::eza]::VERSION); + check_in_shaft!("eza"); + let version = get_version()?; + check_outdated!(&version, metadata[coreutils::eza]::VERSION); Ok(Verified::UpToDate) } +fn get_version() -> cu::Result { + let output = command_output!("eza", ["--version"]); + let output = cu::check!(output.lines().find(|l|l.starts_with("v")), "failed to parse eza --version output: failed to find version line")?; + let version = output.strip_prefix('v').unwrap_or(output); + let version = version.split_once(' ').map(|a| a.0).unwrap_or(version); + Ok(version.trim().to_string()) +} + pub fn install(ctx: &Context) -> cu::Result<()> { if let Ok(Verified::UpToDate) = verify() { return Ok(()); } - epkg::cargo::install("eza", ctx.bar_ref()) -} - -pub fn uninstall() -> cu::Result<()> { - let eza_path = hmgr::paths::binary(bin_name!("eza")); - cu::fs::remove(&eza_path)?; - epkg::cargo::uninstall("eza") + let install_dir = cu::path!((ctx.install_dir()) / "eza").into_utf8()?; + epkg::cargo::install("eza", Some(&install_dir), ctx.bar_ref()) } pub fn configure(ctx: &Context) -> cu::Result<()> { - // Delete existing eza binary to find the original - let eza_path = hmgr::paths::binary(bin_name!("eza")); - cu::fs::remove(&eza_path)?; - let eza_src = cu::which("eza")?; - cu::fs::copy(&eza_src, &eza_path)?; - - ctx.add_item(Item::shim_bin( + let bin = cu::path!((ctx.install_dir()) / "eza" / "bin" / bin_name!("eza")).into_utf8()?; + ctx.add_item(Item::link_bin( bin_name!("ls"), - ShimCommand::target(eza_path.into_utf8()?), + bin.clone(), + ))?; + ctx.add_item(Item::link_bin( + bin_name!("eza"), + bin, ))?; Ok(()) } diff --git a/packages/registry/src/packages/coreutils/mod_linux-pacman.rs b/packages/registry/src/packages/coreutils/mod_linux-pacman.rs index dc20075..2d8f9b1 100644 --- a/packages/registry/src/packages/coreutils/mod_linux-pacman.rs +++ b/packages/registry/src/packages/coreutils/mod_linux-pacman.rs @@ -9,6 +9,7 @@ register_binaries!( "ls", "diff", "find", + "xargs", "gzip", "sed", "grep", diff --git a/packages/registry/src/packages/coreutils/mod_win.rs b/packages/registry/src/packages/coreutils/mod_win.rs index 6e6c25f..967564b 100644 --- a/packages/registry/src/packages/coreutils/mod_win.rs +++ b/packages/registry/src/packages/coreutils/mod_win.rs @@ -1,164 +1,262 @@ //! GNU Coreutils, Diffutils, and other basic commands for common workflows -use std::collections::BTreeSet; - use crate::pre::*; mod common; mod eza; +mod sed; mod which; -register_binaries!("ls", "diff", "find", "gzip", "sed", "grep", "tar"); +register_binaries!( + "tar", // builtin + "ls", // eza + // uutils/sed + "sed", + // ms-coreutils + "coreutils", + "find", + "sort", + "grep", + "xargs", + // mingw + "diff", + "gzip" +); binary_dependencies!(Git, CargoBinstall); static PS_ALIASES: &[&str] = &[ "cat", "cp", "dir", "echo", "ls", "mv", "pwd", "rm", "rmdir", "sort", "sleep", "tee", ]; -static SYSTEM32_EXES: &[&str] = &["expand", "hostname", "more", "sort", "whoami"]; -static PS_FUNCTIONS: &[&str] = &["mkdir"]; +// keep in sync with pwsh-install-template.ps1 +#[rustfmt::skip] +static MS_COREUTILS_LIST: &[&str] = &[ + "arch", "b2sum", "base32", "base64", "basename", + "basenc", "cat", "cksum", "comm", "cp", + "csplit", "cut", "df", "dirname", + "du", "echo", "env", "expr", "factor", + "false", "find", "fmt", "fold", "grep", + "head", "hostname", "join", "la", + "ln", "md5sum", "mkdir", "mktemp", + "mv", "nl", "nproc", "numfmt", "od", + "pathchk", "pr", "printenv", "printf", "ptx", + "pwd", "readlink", "realpath", "rm", + "seq", "sha1sum", "sha224sum", "sha256sum", "sha384sum", + "sha512sum", "shuf", "sleep", "sort", "split", + "stat", "sum", "tac", "tail", "tee", + "test", "touch", "tr", "true", "truncate", + "tsort", "unexpand", "uniq", "unlink", "uptime", + "wc", "xargs", "yes" +]; +// these are compatible with DOS and will be put in sbin/ to superceed System32/ ones +static MS_COREUTILS_SBIN_LIST: &[&str] = &[ + "find", "sort", "hostname" +]; +// static PS_REMOVE_SYSTEM32_EXES: &[&str] = &["expand", "hostname", "sort", ]; + +// static PS_FUNCTIONS: &[&str] = &["mkdir"]; + +version_cache!(static MS_COREUTILS_COMMIT = metadata::coreutils::ms_coreutils::COMMIT); pub fn verify(_: &Context) -> cu::Result { - eza::verify()?; - check_in_shaft!("diff"); - check_in_shaft!("diff3"); - check_in_shaft!("cmp"); - // not checking find because of System32\find.exe - check_in_shaft!("gzip"); - check_in_shaft!("sed"); - check_in_shaft!("grep"); + check_sbin_path()?; + cu::check!( cu::which("tar"), "tar.exe is bundled in Windows; your Windows version might be too low" )?; - let v = check_cargo!("coreutils"); - check_outdated!(&v.version, metadata[coreutils::uutils]::VERSION); - check_verified!(which::verify()?); + eza::verify()?; + sed::verify()?; + + // ms-coreutils: uutils/coreutils + findutils + grep + check_verified!(verify_coreutils_version()?); + check_in_shaft!(#[sbin] "find"); + check_in_shaft!(#[sbin] "sort"); + check_in_shaft!("grep"); + + // mingw + check_in_shaft!("diff"); + check_in_shaft!("diff3"); + check_in_shaft!("cmp"); + check_in_shaft!("gzip"); + check_config_version_cache!(common::ALIAS_VERSION); Ok(Verified::UpToDate) } +fn check_sbin_path() -> cu::Result<()> { + // this is best effort check and does not cover every whacky + // way your PATH is messed up + let system_paths = hmgr::windows::get_system("PATH")?; + let system_path_expected = { + let mut system_root = cu::env_var("SystemRoot")?; + system_root.make_ascii_lowercase(); + if !system_root.ends_with('\\') { + system_root.push('\\'); + } + system_root.push_str("system32"); + system_root + }; + let mut sbin_path = hmgr::paths::sbin_root().into_utf8()?; + sbin_path.make_ascii_lowercase(); + for p in system_paths.split(';') { + let mut p = p.to_lowercase(); + while p.ends_with('\\') || p.ends_with('/') { + p.pop(); + } + cu::debug!("{p}"); + if p == sbin_path { + // sbin path verified to exist and be before system_root/system32 + return Ok(()); + } + if p == system_path_expected || p == "%systemroot%\\system32" + || p == "%systemdrive%\\windows\\system32" { + break; + } + } + cu::bail!("path check failed: $SHAFT_HOME\\sbin should appear before {system_path_expected} for coreutils to work. Please fix the SYSTEM PATH"); +} + +fn verify_coreutils_version() -> cu::Result { + check_version_cache!(MS_COREUTILS_COMMIT); + check_in_shaft!("coreutils"); + let v = get_coreutils_version()?; + check_outdated!(&v, metadata[coreutils::ms_coreutils]::VERSION); + Ok(Verified::UpToDate) +} + +fn get_coreutils_version() -> cu::Result { + let output = command_output!("coreutils", ["--version"]); + let output = output.strip_prefix("coreutils ").unwrap_or(&output); + let output = output.split_once(' ').map(|a|a.0).unwrap_or(output); + Ok(output.trim().to_string()) +} + pub fn install(ctx: &Context) -> cu::Result<()> { - eza::install(ctx)?; - epkg::cargo::binstall("coreutils", ctx.bar_ref())?; which::install(ctx)?; + eza::install(ctx)?; + sed::install(ctx)?; + let coreutils_dir = cu::path!((ctx.install_dir()) / "coreutils"); + let coreutils_bin = cu::path!(&coreutils_dir / "bin" / bin_name!("coreutils")); + if coreutils_bin.exists() && let Ok(Verified::UpToDate) = verify_coreutils_version() { + return Ok(()); + } + + epkg::cargo::install_git_commit("coreutils", + metadata::coreutils::ms_coreutils::REPO, + metadata::coreutils::ms_coreutils::COMMIT + , Some(coreutils_dir.as_utf8()?), ctx.bar_ref())?; + + MS_COREUTILS_COMMIT.update()?; Ok(()) } pub fn uninstall(ctx: &Context) -> cu::Result<()> { - eza::uninstall()?; - let coreutils_path = hmgr::paths::binary(bin_name!("coreutils")); - cu::fs::remove(&coreutils_path)?; - epkg::cargo::uninstall("coreutils")?; which::uninstall(ctx)?; + sed::uninstall(ctx)?; Ok(()) } pub fn configure(ctx: &Context) -> cu::Result<()> { eza::configure(ctx)?; // configure coreutils - // we need to copy installed coreutils to bin to ensure - // it's on the same drive, so it can be hardlinked - let old_coreutils_path = hmgr::paths::binary(bin_name!("coreutils")); - cu::fs::remove(&old_coreutils_path)?; - let coreutils_path = ctx.install_dir().join(bin_name!("coreutils")); - let coreutils_src = cu::which("coreutils")?; - cu::fs::copy(&coreutils_src, &coreutils_path)?; - let coreutils_path = coreutils_path.into_utf8()?; - + // see https://github.com/microsoft/coreutils/blob/main/coreutils.iss let config = ctx.load_config(CONFIG)?; - let all_utils = command_output!("coreutils", ["--list"]); - // ^ shadowed, but need to keep alive - let all_utils: BTreeSet<_> = all_utils - .lines() - .map(|s| s.trim()) - .filter(|s| { - if s.is_empty() { - return false; - } - if !s.chars().all(|c| c.is_alphanumeric()) { - return false; - } - // excluded by config: - if config.windows.exclude_coreutils.contains(*s) { - return false; - } - // always-exclude: - if *s == "link" { - // link conflicts with MSVC - return false; - } - - true - }) - .collect(); - - // link utils - for util in &all_utils { - ctx.add_item(Item::link_bin(bin_name!(util), coreutils_path.clone()))?; + let curr_prefer_gnu_find = get_ms_coreutils_reg_value("DefaultFind").unwrap_or_default(); + let curr_prefer_gnu_find = curr_prefer_gnu_find.trim() == "1"; + let curr_prefer_gnu_sort = get_ms_coreutils_reg_value("DefaultSort").unwrap_or_default(); + let curr_prefer_gnu_sort = curr_prefer_gnu_sort.trim() == "1"; + + let need_set_reg = curr_prefer_gnu_find != config.windows.prefer_gnu + || curr_prefer_gnu_sort != config.windows.prefer_gnu; + if need_set_reg { + cu::debug!("setting registry for ms-coreutils"); + let script = format!(concat!( +r"New-Item -Path 'HKLM:\SOFTWARE\Microsoft\coreutils' -Force | Out-Null;", +r"New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\coreutils' -Name 'DefaultFind' -PropertyType DWord -Value {0} -Force | Out-Null;", +r"New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\coreutils' -Name 'DefaultSort' -PropertyType DWord -Value {0} -Force | Out-Null;", + ), if config.windows.prefer_gnu { "1" } else { "0" }); + + opfs::sudo("powershell", "setting registry values for ms-coreutils")? + .args(["-NoLogo", "-NoProfile", "-Command", &script]) + .stdoe(cu::lv::D) + .stdin_null() + .wait_nz()?; } - // remove PS aliases and functions - for util in PS_ALIASES { - if all_utils.contains(util) { - ctx.add_item(Item::pwsh(format!("Remove-Item Alias:{util} -Force")))?; + + let coreutils_dir = cu::path!((ctx.install_dir()) / "coreutils"); + let coreutils_bin = cu::path!(&coreutils_dir / "bin" / bin_name!("coreutils")).into_utf8()?; + // make the .cmd links - see pwsh-install-template.ps1 why + let coreutils_cmd_dir = { + let mut p = coreutils_dir.join("cmd").into_utf8()?; + if !p.ends_with('\\') { + p.push('\\') } + p + }; + cu::fs::make_dir_empty(&coreutils_cmd_dir)?; + let mut link_paths = Vec::with_capacity(MS_COREUTILS_LIST.len()); + for util in MS_COREUTILS_LIST { + let link_target = format!("{coreutils_cmd_dir}\\{util}.cmd"); + link_paths.push((link_target, &coreutils_bin)); } - for util in PS_FUNCTIONS { - if all_utils.contains(util) { - ctx.add_item(Item::pwsh(format!("Remove-Item Function:{util} -Force")))?; - } + cu::check!(opfs::hardlink_files(link_paths.into_iter()), "failed to create cmd links for ms-coreutils")?; + // link bins + ctx.add_item(Item::link_bin(bin_name!("coreutils"), coreutils_bin.clone()))?; + for util in MS_COREUTILS_LIST { + ctx.add_item(Item::link_bin(bin_name!(util), coreutils_bin.clone()))?; } - // override System32 binaries by setting alias/doskey - for util in SYSTEM32_EXES { - let link_path = hmgr::paths::binary(bin_name!(&util)).into_utf8()?; - if all_utils.contains(util) { - ctx.add_item(Item::pwsh(format!( - "Set-Alias -Name {util} -Value '{link_path}'" - )))?; - ctx.add_item(Item::cmd(format!("doskey {util}=\"{link_path}\" $*")))?; - } + for util in MS_COREUTILS_SBIN_LIST { + ctx.add_item(Item::link_sys_bin(bin_name!(util), coreutils_bin.clone()))?; } + // add the pwsh injection script + // need to refer to https://github.com/microsoft/coreutils/blob/main/src/pwsh-install.ps1 + // if changes in the future + let pwsh_injection_script = include_str!("pwsh-install-template.ps1").replace("!!CMDDIR!!", &coreutils_cmd_dir); + ctx.add_item(Item::pwsh(pwsh_injection_script))?; if config.windows.cmd_mkdir { let link_path = hmgr::paths::binary(bin_name!("mkdir")).into_utf8()?; ctx.add_item(Item::cmd(format!("doskey mkdir=\"{link_path}\" -p $*")))?; } + for util in MS_COREUTILS_SBIN_LIST { + let util_path = hmgr::paths::binary(bin_name!(util)).into_utf8()?; + ctx.add_item(Item::cmd(format!("doskey {util_path}=\"{util_path}\" $*")))?; + } - // configure utils from mingw - let exe_path = opfs::find_in_wingit("usr/bin/grep.exe")?; - ctx.add_item(Item::shim_bin( - bin_name!("grep"), - ShimCommand::target(exe_path.into_utf8()?).args(["--color=auto"]), - ))?; - const MINGW_UTILS: &[&str] = &["diff", "diff3", "cmp", "find", "gzip", "sed"]; + const MINGW_UTILS: &[&str] = &["diff", "diff3", "cmp", "gzip"]; for util in MINGW_UTILS { - if config.windows.exclude_coreutils.contains(*util) { - continue; - } let exe_path = opfs::find_in_wingit(format!("usr/bin/{util}.exe"))?; ctx.add_item(Item::shim_bin( bin_name!(util), ShimCommand::target(exe_path.into_utf8()?), ))?; } - if !config.windows.exclude_coreutils.contains("find") { - let findutil_path = hmgr::paths::binary(bin_name!("find")).into_utf8()?; - ctx.add_item(Item::pwsh(format!( - "Set-Alias -Name find -Value '{findutil_path}'" - )))?; - ctx.add_item(Item::cmd(format!("doskey find=\"{findutil_path}\" $*")))?; - } common::ALIAS_VERSION.update()?; Ok(()) } +fn get_ms_coreutils_reg_value(value: &str) -> cu::Result { + let script = format!("(Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\coreutils').{value}"); + let (child, output) = cu::which("powershell")? + .command() + .args(["-NoLogo", "-NoProfile", "-Command", &script]) + .stdout(cu::pio::string()) + .stderr(cu::lv::D) + .stdin_null() + .spawn()?; + child.wait_nz()?; + let output = output.join()??; + Ok(output) +} + config_file! { static CONFIG: Config = { template: include_str!("config.toml"), - migration: [] + migration: [""] } } @@ -170,6 +268,6 @@ struct Config { #[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case")] struct ConfigWindows { - exclude_coreutils: BTreeSet, + prefer_gnu: bool, cmd_mkdir: bool, } diff --git a/packages/registry/src/packages/coreutils/pwsh-install-template.ps1 b/packages/registry/src/packages/coreutils/pwsh-install-template.ps1 new file mode 100644 index 0000000..1f9b5fa --- /dev/null +++ b/packages/registry/src/packages/coreutils/pwsh-install-template.ps1 @@ -0,0 +1,218 @@ +# https://github.com/microsoft/coreutils/blob/main/src/pwsh-install-template.ps1 +# # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +# Inlining the template into the profile shaves off ~10ms (25%). +# SHAFT_PATCH: take out { ls, date, rmdir, link } +$script:__COREUTILS__ = [System.Collections.Generic.HashSet[string]]::new( + [string[]]@( + 'arch', 'b2sum', 'base32', 'base64', 'basename', + 'basenc', 'cat', 'cksum', 'comm', 'cp', + 'csplit', 'cut', 'df', 'dirname', + 'du', 'echo', 'env', 'expr', 'factor', + 'false', 'find', 'fmt', 'fold', 'grep', + 'head', 'hostname', 'join', 'la', + 'ln', 'md5sum', 'mkdir', 'mktemp', + 'mv', 'nl', 'nproc', 'numfmt', 'od', + 'pathchk', 'pr', 'printenv', 'printf', 'ptx', + 'pwd', 'readlink', 'realpath', 'rm', + 'seq', 'sha1sum', 'sha224sum', 'sha256sum', 'sha384sum', + 'sha512sum', 'shuf', 'sleep', 'sort', 'split', + 'stat', 'sum', 'tac', 'tail', 'tee', + 'test', 'touch', 'tr', 'true', 'truncate', + 'tsort', 'unexpand', 'uniq', 'unlink', 'uptime', + 'wc', 'xargs', 'yes' + ), + [System.StringComparer]::OrdinalIgnoreCase +) + +$script:__COREUTILS_FAST_SKIP__ = [regex]::new( + '\b(?:' + ($script:__COREUTILS__ -join '|') + ')\b', + [System.Text.RegularExpressions.RegexOptions]::Compiled -bor ` + [System.Text.RegularExpressions.RegexOptions]::IgnoreCase +) + +# Casting the scriptblock to Func once and reusing it avoids the +# per-FindAll scriptblock-to-delegate wrapping overhead (~1.7x faster). +$script:__COREUTILS_CMD_PREDICATE__ = [System.Func[System.Management.Automation.Language.Ast, bool]] { + param($n) $n -is [System.Management.Automation.Language.CommandAst] +} + +$script:__COREUTILS_ARG_SPECIAL__ = [char[]] @("'", '"', '`', '$') + +# Wrap arguments into quotes. By being a function we can properly handle $variables. +# As per MSVCRT, any `\` before `"` must be doubled to escape them. +function global:__coreutils_q { + param($s) + '"' + (([string]$s) -replace '(\\*)"', '$1$1\"' -replace '(\\+)$', '$1$1') + '"' +} + +# PowerShell tokenizes `*"a"*` as [BareWord] instead of the expected [DoubleQuoted, BareWord, DoubleQuoted]. +# To work around that we use... regex. Group 1 = 'single', 2 = "double", 3 = `escape, 4 = bare run. +$script:__COREUTILS_ARG_RX__ = [regex]::new( + "'((?:[^']|'')*)'|""((?:[^""``]|""""|``.)*)""|``(.)|([^'""``]+)", + [System.Text.RegularExpressions.RegexOptions]::Compiled +) +$script:__COREUTILS_ARG_EVAL__ = [System.Text.RegularExpressions.MatchEvaluator] { + param($m) + if ($m.Groups[1].Success) { + # Single-quoted: literal. PS '' -> ', then MSVCRT-quote. + $body = $m.Groups[1].Value.Replace("''", "'") + if ($body -match '^(.*?)(\\+)$') { + return '"' + ($matches[1] -replace '(\\*)"', '$1$1\"') + '"' + $matches[2] + } + return '"' + ($body -replace '(\\*)"', '$1$1\"') + '"' + } + if ($m.Groups[2].Success) { + # Double-quoted: collapse PS quote-escapes to raw " / ', let ExpandString + # resolve `n / `t / $var, then MSVCRT-quote. + $body = $m.Groups[2].Value. + Replace('`"', '"'). + Replace("``'", "'"). + Replace('""', '"') + $body = $ExecutionContext.InvokeCommand.ExpandString($body) + if ($body -match '^(.*?)(\\+)$') { + return '"' + ($matches[1] -replace '(\\*)"', '$1$1\"') + '"' + $matches[2] + } + return '"' + ($body -replace '(\\*)"', '$1$1\"') + '"' + } + if ($m.Groups[3].Success) { + # Backtick-escaped char outside a string: " -> \"; everything else + # becomes a one-char quoted region so glob metas stay literal. + $c = $m.Groups[3].Value + if ($c -eq '"') { + return '\"' + } + return '"' + $c + '"' + } + # Bare run: passed through unquoted so coreutils can glob it; expand $vars. + return $ExecutionContext.InvokeCommand.ExpandString($m.Groups[4].Value) +} + +# 0: not tested, 1: coreutils not installed, 2: coreutils installed. +$script:__COREUTILS_CMD_DIR_TEST__ = 0 + +# PSConsoleHostReadLine override that rewrites coreutils command names to their +# .cmd equivalents after PSReadLine returns (history keeps the original). +# +# Why .cmd over .exe: PSNativeCommandArgumentPassing = 'Windows' results in a behavior +# where passing bare quotes to CreateProcess() is impossible. This prevents us from +# passing "*" as "*" to coreutils and instead will be given as a bare *. +# This causes it to treat it as a glob pattern. "*.cmd" files however are automatically +# treated as PSNativeCommandArgumentPassing = 'Legacy', which preserves quotes. +# It is the only possible workaround and the only way coreutils can work at all. +function PSConsoleHostReadLine { + [System.Diagnostics.DebuggerHidden()] + param() + + $lastRunStatus = $? + Microsoft.PowerShell.Core\Set-StrictMode -Off + $line = [Microsoft.PowerShell.PSConsoleReadLine]::ReadLine($host.Runspace, $ExecutionContext, $lastRunStatus) + + # If the line contains no coreutils name, we don't need to parse the AST at all. + if (-not $script:__COREUTILS_FAST_SKIP__.IsMatch($line)) { + return $line + } + + # Roamed/synced profiles can load this snippet on machines where coreutils is not installed. + # Test for the existence of the command directory once and remember the result. + if ($script:__COREUTILS_CMD_DIR_TEST__ -eq 0) { + $script:__COREUTILS_CMD_DIR_TEST__ = 1 + if (Test-Path -LiteralPath '!!CMDDIR!!' -PathType Container -ErrorAction Ignore) { + $script:__COREUTILS_CMD_DIR_TEST__ = 2 + } + } + if ($script:__COREUTILS_CMD_DIR_TEST__ -ne 2) { + return $line + } + + $ast = [System.Management.Automation.Language.Parser]::ParseInput($line, [ref]$null, [ref]$null) + $commands = $ast.FindAll($script:__COREUTILS_CMD_PREDICATE__, $true) + + # Process right-to-left so earlier offsets stay valid after each splice. + # In-place reverse beats Sort-Object for the typical 1-command line. + if ($commands.Count -gt 1) { + $commands = [System.Collections.Generic.List[object]]::new($commands) + $commands.Reverse() + } + + foreach ($cmd in $commands) { + $name = $cmd.GetCommandName() + if (!$name) { + continue + } + + $baseName = $name + if ($name.EndsWith('.exe') -or $name.EndsWith('.cmd')) { + $baseName = $name.Substring(0, $name.Length - 4) + } + if (!$script:__COREUTILS__.Contains($baseName)) { + continue + } + + # ls/la get colour + listing flags injected; la also rewrites to ls. + $cmdElement = $cmd.CommandElements[0] + $start = $cmdElement.Extent.StartOffset + $end = $cmdElement.Extent.EndOffset + $replacement = "& '!!CMDDIR!!" + + switch ($baseName) { + 'la' { $replacement += "ls.cmd' --color=auto -AFhl" } + # SHAFT_PATCH: 'ls' { $replacement += "ls.cmd' --color=auto" } + default { $replacement += "$baseName.cmd'" } + } + + # Walk command elements, merging adjacent ones whose extents touch + # (e.g. `'a'*` parses as [SingleQuoted, BareWord] but is one shell word). + # The inverse case `*'a'*` parses as a single BareWord whose text + # contains the embedded quotes, which is why AST-only analysis + # isn't enough and we still need to re-tokenize the source span. + $argsStart = $end + $argsEnd = $cmd.Extent.EndOffset + $rewrittenArgs = '' + $elements = $cmd.CommandElements + $count = $elements.Count + $i = 1 + while ($i -lt $count) { + $first = $elements[$i] + $wordStart = $first.Extent.StartOffset + $wordEnd = $first.Extent.EndOffset + $merged = $false + while ($i + 1 -lt $count -and $elements[$i + 1].Extent.StartOffset -eq $wordEnd) { + $i++ + $wordEnd = $elements[$i].Extent.EndOffset + $merged = $true + } + $source = $line.Substring($wordStart, $wordEnd - $wordStart) + $rewrittenArgs += $line.Substring($argsStart, $wordStart - $argsStart) + $argsStart = $wordEnd + # IndexOfAny beats running the regex per arg. + if ($source.IndexOfAny($script:__COREUTILS_ARG_SPECIAL__) -lt 0) { + $rewrittenArgs += $source + $i++ + continue + } + # A single un-merged PS expression that needs $var resolution + # (bare $var, "...$var...", $x.Member, $($expr), etc.). + # Defer evaluation to runtime so the value reaches coreutils as a literal arg. + # This matches POSIX behaviour where variable expansions don't result in globbing. + if (-not $merged -and + ($first -is [System.Management.Automation.Language.VariableExpressionAst] -or + $first -is [System.Management.Automation.Language.ExpandableStringExpressionAst] -or + $first -is [System.Management.Automation.Language.MemberExpressionAst])) { + $rewrittenArgs += '(__coreutils_q ' + $source + ')' + $i++ + continue + } + # Slow path: re-tokenise and re-emit as MSVCRT-style quoting, + # then wrap in PS single quotes so PS hands the body verbatim. + $windowsQuoted = $script:__COREUTILS_ARG_RX__.Replace($source, $script:__COREUTILS_ARG_EVAL__) + $rewrittenArgs += "'" + $windowsQuoted.Replace("'", "''") + "'" + $i++ + } + $rewrittenArgs += $line.Substring($argsStart, $argsEnd - $argsStart) + + $line = $line.Substring(0, $start) + $replacement + $rewrittenArgs + $line.Substring($argsEnd) + } + + return $line +} +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/packages/registry/src/packages/coreutils/sed.rs b/packages/registry/src/packages/coreutils/sed.rs new file mode 100644 index 0000000..c36d1a4 --- /dev/null +++ b/packages/registry/src/packages/coreutils/sed.rs @@ -0,0 +1,18 @@ +use crate::pre::*; + +pub fn verify() -> cu::Result { + let v = check_cargo!("sed"); + check_outdated!(&v.version, metadata[coreutils::uutils_sed]::VERSION); + Ok(Verified::UpToDate) +} +pub fn install(ctx: &Context) -> cu::Result<()> { + if let Ok(Verified::UpToDate) = verify() { + return Ok(()); + } + epkg::cargo::binstall("sed", ctx.bar_ref())?; + Ok(()) +} +pub fn uninstall(_: &Context) -> cu::Result<()> { + epkg::cargo::uninstall("sed")?; + Ok(()) +} diff --git a/packages/registry/src/packages/git-cfg/mod.rs b/packages/registry/src/packages/git-cfg/mod.rs index d7b9008..84091fd 100644 --- a/packages/registry/src/packages/git-cfg/mod.rs +++ b/packages/registry/src/packages/git-cfg/mod.rs @@ -13,7 +13,7 @@ pub fn verify(_: &Context) -> cu::Result { Ok(Verified::UpToDate) } pub fn install(ctx: &Context) -> cu::Result<()> { - epkg::cargo::install("git-delta", ctx.bar_ref())?; + epkg::cargo::install("git-delta", None, ctx.bar_ref())?; Ok(()) } pub fn uninstall(_: &Context) -> cu::Result<()> { diff --git a/packages/registry/src/packages/pwsh/mod_win.rs b/packages/registry/src/packages/pwsh/mod_win.rs index 27baa39..ad7fcc2 100644 --- a/packages/registry/src/packages/pwsh/mod_win.rs +++ b/packages/registry/src/packages/pwsh/mod_win.rs @@ -32,12 +32,7 @@ pub fn verify(ctx: &Context) -> cu::Result { "$PSVersionTable.PSVersion.ToString()" ] ); - let is_preview = version.contains("preview"); - if !is_preview { - cu::debug!("preview version of pwsh is required for tilde expansion"); - return Ok(Verified::NotUpToDate); - } - check_outdated!(&version, metadata[pwsh]::VERSION); + check_outdated!(version.trim(), metadata[pwsh]::VERSION); Ok(Verified::UpToDate) } diff --git a/packages/registry/src/packages/shellutils/mod_win.rs b/packages/registry/src/packages/shellutils/mod_win.rs index 62b480d..642b42f 100644 --- a/packages/registry/src/packages/shellutils/mod_win.rs +++ b/packages/registry/src/packages/shellutils/mod_win.rs @@ -109,10 +109,10 @@ pub fn install(ctx: &Context) -> cu::Result<()> { epkg::cargo::binstall("bat", ctx.bar_ref())?; epkg::cargo::binstall("du-dust", ctx.bar_ref())?; - epkg::cargo::install("fd-find", ctx.bar_ref())?; + epkg::cargo::install("fd-find", None, ctx.bar_ref())?; epkg::cargo::binstall("ripgrep", ctx.bar_ref())?; - epkg::cargo::install("websocat", ctx.bar_ref())?; - epkg::cargo::install("zoxide", ctx.bar_ref())?; + epkg::cargo::install("websocat", None, ctx.bar_ref())?; + epkg::cargo::install("zoxide", None, ctx.bar_ref())?; n::install(ctx)?; viopen::install(ctx)?; diff --git a/packages/registry/src/packages/starship/mod.rs b/packages/registry/src/packages/starship/mod.rs index 4b9d347..eb899b8 100644 --- a/packages/registry/src/packages/starship/mod.rs +++ b/packages/registry/src/packages/starship/mod.rs @@ -17,7 +17,7 @@ pub fn verify(_: &Context) -> cu::Result { pub fn install(ctx: &Context) -> cu::Result<()> { let config = ctx.load_config(CONFIG)?; if config.build_from_source { - epkg::cargo::install("starship", ctx.bar_ref())?; + epkg::cargo::install("starship", None, ctx.bar_ref())?; } else { epkg::cargo::binstall("starship", ctx.bar_ref())?; } diff --git a/packages/registry/src/util/macros/verify.rs b/packages/registry/src/util/macros/verify.rs index ce40d0a..9efdbef 100644 --- a/packages/registry/src/util/macros/verify.rs +++ b/packages/registry/src/util/macros/verify.rs @@ -19,6 +19,28 @@ pub(crate) use check_in_path; /// /// Optionally, takes the name of the system package that can be used as alternative macro_rules! check_in_shaft { + (#[sbin] $bin:literal) => {{ + match cu::which($bin) { + Err(e) => { + cu::error!("verify: not found in PATH: '{}'", $bin); + cu::debug!("check_in_shaft failed: {e:?}"); + return Ok(Verified::NotInstalled); + } + Ok(path) => { + if path != hmgr::paths::system_binary(bin_name!($bin)) { + // ignore system32 ones since we are checking sbin + if cfg!(not(windows)) || !path.as_os_str().to_string_lossy().to_lowercase().ends_with(&format!("system32\\{}.exe", $bin)) { + cu::bail!( + "found existing '{}' installed outside of shaft, please uninstall it first (at '{}'), or ensure the shaft bin has higher priority in PATH", + $bin, + path.display() + ); + } + } + path + } + } + }}; ($bin:literal) => {{ match cu::which($bin) { Err(e) => { diff --git a/packages/registry/update/packages.mts b/packages/registry/update/packages.mts index ab1ea12..5ba1162 100644 --- a/packages/registry/update/packages.mts +++ b/packages/registry/update/packages.mts @@ -62,7 +62,6 @@ export const pkg_pwsh: PackageFn = (meta) => export const pkg_cargo_binstall = default_cratesio_fetcher("cargo-binstall"); export const pkg_coreutils: PackageFn = () => [ fetch_from_cratesio({ crate: "eza", query: (v) => ({ "eza.VERSION": v }) }), - fetch_from_cratesio({ crate: "coreutils", query: (v) => ({ "uutils.VERSION": v }) }), fetch_from_arch_linux({ package: "zip", query: (version) => ({ "zip.VERSION": version }) }), fetch_from_arch_linux({ package: "unzip", query: (version) => ({ "unzip.VERSION": version }) }), fetch_from_arch_linux({ package: "tar", query: (version) => ({ "tar.VERSION": version }) }), @@ -70,6 +69,7 @@ export const pkg_coreutils: PackageFn = () => [ fetch_from_arch_linux({ package: "bash", query: (version) => ({ "bash.VERSION": version }) }), fetch_from_arch_linux({ package: "bash-completion", query: (version) => ({ "bash_cmp.VERSION": version }) }), fetch_from_aur({ package: "yay-bin", query: (version) => ({ "yay.VERSION": version })}), + fetch_from_cratesio({ crate: "sed", query: (v) => ({ "uutils_sed.VERSION": v }) }), ]; export const pkg_shellutils: PackageFn = async () => { return [ From a3805a7b21d6262f1ab82c0bc1439918f8791249 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Wed, 10 Jun 2026 21:58:02 -0700 Subject: [PATCH 3/4] run fixes --- VERSION | 2 +- packages/corelib/src/epkg/cargo.rs | 10 +- packages/corelib/src/hmgr/env/linux.rs | 1 + packages/corelib/src/hmgr/environment.rs | 2 +- packages/corelib/src/opfs/fs_util.rs | 8 +- .../registry/src/packages/coreutils/eza.rs | 15 +- .../packages/coreutils/mod_linux-pacman.rs | 1 - .../src/packages/coreutils/mod_win.rs | 72 ++- .../coreutils/pwsh-install-template.ps1 | 436 +++++++++--------- .../registry/src/packages/coreutils/sed.rs | 36 +- .../packages/shellutils/mod_linux-pacman.rs | 6 +- 11 files changed, 309 insertions(+), 280 deletions(-) diff --git a/VERSION b/VERSION index 0bfccb0..ef52a64 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.5 +0.4.6 diff --git a/packages/corelib/src/epkg/cargo.rs b/packages/corelib/src/epkg/cargo.rs index f9a816d..db9f352 100644 --- a/packages/corelib/src/epkg/cargo.rs +++ b/packages/corelib/src/epkg/cargo.rs @@ -96,7 +96,7 @@ pub fn install_git_commit( .args(["install", package, "--git", git, "--rev", rev, "--locked"]); let command = match root { None => command, - Some(root) => command.args(["--force", "-q", "--root", root]) + Some(root) => command.args(["--force", "-q", "--root", root]), }; let command = add_platform_build_args(command); let (child, bar) = command @@ -114,7 +114,11 @@ pub fn install_git_commit( /// Install a package using `cargo install` #[cu::context("failed to install '{package}' with cargo")] -pub fn install(package: &str, root: Option<&str>, bar: Option<&Arc>) -> cu::Result<()> { +pub fn install( + package: &str, + root: Option<&str>, + bar: Option<&Arc>, +) -> cu::Result<()> { let mut state = cargo::instance()?; let command = cu::which("cargo")? .command() @@ -122,7 +126,7 @@ pub fn install(package: &str, root: Option<&str>, bar: Option<&Arc command, - Some(root) => command.args(["-q", "--root", root]) + Some(root) => command.args(["-q", "--root", root]), }; let command = add_platform_build_args(command); let (child, bar) = command diff --git a/packages/corelib/src/hmgr/env/linux.rs b/packages/corelib/src/hmgr/env/linux.rs index ea7fa93..b25a611 100644 --- a/packages/corelib/src/hmgr/env/linux.rs +++ b/packages/corelib/src/hmgr/env/linux.rs @@ -187,6 +187,7 @@ impl Env { Item::Bash(_) => self.bash_dirty = true, Item::Zsh(_) => self.zsh_dirty = true, Item::LinkBin(_, _, _) => {} + Item::LinkSysBin(_, _) => {} Item::ShimBin(_, _) => {} Item::Pwsh(_) => {} Item::Cmd(_) => {} diff --git a/packages/corelib/src/hmgr/environment.rs b/packages/corelib/src/hmgr/environment.rs index c139f53..793d25f 100644 --- a/packages/corelib/src/hmgr/environment.rs +++ b/packages/corelib/src/hmgr/environment.rs @@ -9,7 +9,7 @@ pub mod windows { use std::collections::BTreeMap; use std::sync::Mutex; - pub use win_envedit::{get_user, get_system}; + pub use win_envedit::{get_system, get_user}; static USER_THIS_SESSION: Mutex> = Mutex::new(BTreeMap::new()); /// Get user environment variable at the start of the session, /// before any set calls diff --git a/packages/corelib/src/opfs/fs_util.rs b/packages/corelib/src/opfs/fs_util.rs index fd264c8..4b72ac1 100644 --- a/packages/corelib/src/opfs/fs_util.rs +++ b/packages/corelib/src/opfs/fs_util.rs @@ -60,7 +60,9 @@ pub fn symlink_files(paths: &[(&Path, &Path)]) -> cu::Result<()> { /// Create hardlinks. `from` is where the link will be and `to` is the target of the link #[cfg(windows)] #[cu::context("failed to create hard links")] -pub fn hardlink_files(paths: impl Iterator, impl AsRef)>) -> cu::Result<()> { +pub fn hardlink_files( + paths: impl Iterator, impl AsRef)>, +) -> cu::Result<()> { let mut script = String::new(); let mut empty = true; for (from, to) in paths { @@ -97,7 +99,9 @@ fn build_link_powershell(out: &mut String, link_type: &str, from: &str, to: &str /// Create hardlinks. `from` is where the link will be and `to` is the target of the link #[cfg(not(windows))] #[cu::context("failed to create hard links")] -pub fn hardlink_files(paths: impl Iterator, impl AsRef)>) -> cu::Result<()> { +pub fn hardlink_files( + paths: impl Iterator, impl AsRef)>, +) -> cu::Result<()> { for (from, to) in paths { let from = from.as_ref(); cu::fs::remove(from)?; diff --git a/packages/registry/src/packages/coreutils/eza.rs b/packages/registry/src/packages/coreutils/eza.rs index a6f2600..475e0d2 100644 --- a/packages/registry/src/packages/coreutils/eza.rs +++ b/packages/registry/src/packages/coreutils/eza.rs @@ -9,7 +9,10 @@ pub fn verify() -> cu::Result { fn get_version() -> cu::Result { let output = command_output!("eza", ["--version"]); - let output = cu::check!(output.lines().find(|l|l.starts_with("v")), "failed to parse eza --version output: failed to find version line")?; + let output = cu::check!( + output.lines().find(|l| l.starts_with("v")), + "failed to parse eza --version output: failed to find version line" + )?; let version = output.strip_prefix('v').unwrap_or(output); let version = version.split_once(' ').map(|a| a.0).unwrap_or(version); Ok(version.trim().to_string()) @@ -25,13 +28,7 @@ pub fn install(ctx: &Context) -> cu::Result<()> { pub fn configure(ctx: &Context) -> cu::Result<()> { let bin = cu::path!((ctx.install_dir()) / "eza" / "bin" / bin_name!("eza")).into_utf8()?; - ctx.add_item(Item::link_bin( - bin_name!("ls"), - bin.clone(), - ))?; - ctx.add_item(Item::link_bin( - bin_name!("eza"), - bin, - ))?; + ctx.add_item(Item::link_bin(bin_name!("ls"), bin.clone()))?; + ctx.add_item(Item::link_bin(bin_name!("eza"), bin))?; Ok(()) } diff --git a/packages/registry/src/packages/coreutils/mod_linux-pacman.rs b/packages/registry/src/packages/coreutils/mod_linux-pacman.rs index 2d8f9b1..03676e7 100644 --- a/packages/registry/src/packages/coreutils/mod_linux-pacman.rs +++ b/packages/registry/src/packages/coreutils/mod_linux-pacman.rs @@ -63,7 +63,6 @@ pub fn install(ctx: &Context) -> cu::Result<()> { } pub fn uninstall(_: &Context) -> cu::Result<()> { - eza::uninstall()?; cu::warn!("not uninstalling the essential packages for your sanity"); Ok(()) } diff --git a/packages/registry/src/packages/coreutils/mod_win.rs b/packages/registry/src/packages/coreutils/mod_win.rs index 967564b..5c13df4 100644 --- a/packages/registry/src/packages/coreutils/mod_win.rs +++ b/packages/registry/src/packages/coreutils/mod_win.rs @@ -9,7 +9,7 @@ mod which; register_binaries!( "tar", // builtin - "ls", // eza + "ls", // eza // uutils/sed "sed", // ms-coreutils @@ -48,9 +48,7 @@ static MS_COREUTILS_LIST: &[&str] = &[ "wc", "xargs", "yes" ]; // these are compatible with DOS and will be put in sbin/ to superceed System32/ ones -static MS_COREUTILS_SBIN_LIST: &[&str] = &[ - "find", "sort", "hostname" -]; +static MS_COREUTILS_SBIN_LIST: &[&str] = &["find", "sort", "hostname"]; // static PS_REMOVE_SYSTEM32_EXES: &[&str] = &["expand", "hostname", "sort", ]; // static PS_FUNCTIONS: &[&str] = &["mkdir"]; @@ -71,8 +69,14 @@ pub fn verify(_: &Context) -> cu::Result { // ms-coreutils: uutils/coreutils + findutils + grep check_verified!(verify_coreutils_version()?); - check_in_shaft!(#[sbin] "find"); - check_in_shaft!(#[sbin] "sort"); + check_in_shaft!( + #[sbin] + "find" + ); + check_in_shaft!( + #[sbin] + "sort" + ); check_in_shaft!("grep"); // mingw @@ -110,12 +114,16 @@ fn check_sbin_path() -> cu::Result<()> { // sbin path verified to exist and be before system_root/system32 return Ok(()); } - if p == system_path_expected || p == "%systemroot%\\system32" - || p == "%systemdrive%\\windows\\system32" { + if p == system_path_expected + || p == "%systemroot%\\system32" + || p == "%systemdrive%\\windows\\system32" + { break; } } - cu::bail!("path check failed: $SHAFT_HOME\\sbin should appear before {system_path_expected} for coreutils to work. Please fix the SYSTEM PATH"); + cu::bail!( + "path check failed: $SHAFT_HOME\\sbin should appear before {system_path_expected} for coreutils to work. Please fix the SYSTEM PATH" + ); } fn verify_coreutils_version() -> cu::Result { @@ -129,7 +137,7 @@ fn verify_coreutils_version() -> cu::Result { fn get_coreutils_version() -> cu::Result { let output = command_output!("coreutils", ["--version"]); let output = output.strip_prefix("coreutils ").unwrap_or(&output); - let output = output.split_once(' ').map(|a|a.0).unwrap_or(output); + let output = output.split_once(' ').map(|a| a.0).unwrap_or(output); Ok(output.trim().to_string()) } @@ -139,14 +147,19 @@ pub fn install(ctx: &Context) -> cu::Result<()> { sed::install(ctx)?; let coreutils_dir = cu::path!((ctx.install_dir()) / "coreutils"); let coreutils_bin = cu::path!(&coreutils_dir / "bin" / bin_name!("coreutils")); - if coreutils_bin.exists() && let Ok(Verified::UpToDate) = verify_coreutils_version() { + if coreutils_bin.exists() + && let Ok(Verified::UpToDate) = verify_coreutils_version() + { return Ok(()); } - epkg::cargo::install_git_commit("coreutils", - metadata::coreutils::ms_coreutils::REPO, - metadata::coreutils::ms_coreutils::COMMIT - , Some(coreutils_dir.as_utf8()?), ctx.bar_ref())?; + epkg::cargo::install_git_commit( + "coreutils", + metadata::coreutils::ms_coreutils::REPO, + metadata::coreutils::ms_coreutils::COMMIT, + Some(coreutils_dir.as_utf8()?), + ctx.bar_ref(), + )?; MS_COREUTILS_COMMIT.update()?; Ok(()) @@ -173,11 +186,14 @@ pub fn configure(ctx: &Context) -> cu::Result<()> { || curr_prefer_gnu_sort != config.windows.prefer_gnu; if need_set_reg { cu::debug!("setting registry for ms-coreutils"); - let script = format!(concat!( -r"New-Item -Path 'HKLM:\SOFTWARE\Microsoft\coreutils' -Force | Out-Null;", -r"New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\coreutils' -Name 'DefaultFind' -PropertyType DWord -Value {0} -Force | Out-Null;", -r"New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\coreutils' -Name 'DefaultSort' -PropertyType DWord -Value {0} -Force | Out-Null;", - ), if config.windows.prefer_gnu { "1" } else { "0" }); + let script = format!( + concat!( + r"New-Item -Path 'HKLM:\SOFTWARE\Microsoft\coreutils' -Force | Out-Null;", + r"New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\coreutils' -Name 'DefaultFind' -PropertyType DWord -Value {0} -Force | Out-Null;", + r"New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\coreutils' -Name 'DefaultSort' -PropertyType DWord -Value {0} -Force | Out-Null;", + ), + if config.windows.prefer_gnu { "1" } else { "0" } + ); opfs::sudo("powershell", "setting registry values for ms-coreutils")? .args(["-NoLogo", "-NoProfile", "-Command", &script]) @@ -202,9 +218,15 @@ r"New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\coreutils' -Name 'DefaultSort let link_target = format!("{coreutils_cmd_dir}\\{util}.cmd"); link_paths.push((link_target, &coreutils_bin)); } - cu::check!(opfs::hardlink_files(link_paths.into_iter()), "failed to create cmd links for ms-coreutils")?; + cu::check!( + opfs::hardlink_files(link_paths.into_iter()), + "failed to create cmd links for ms-coreutils" + )?; // link bins - ctx.add_item(Item::link_bin(bin_name!("coreutils"), coreutils_bin.clone()))?; + ctx.add_item(Item::link_bin( + bin_name!("coreutils"), + coreutils_bin.clone(), + ))?; for util in MS_COREUTILS_LIST { ctx.add_item(Item::link_bin(bin_name!(util), coreutils_bin.clone()))?; } @@ -214,7 +236,8 @@ r"New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\coreutils' -Name 'DefaultSort // add the pwsh injection script // need to refer to https://github.com/microsoft/coreutils/blob/main/src/pwsh-install.ps1 // if changes in the future - let pwsh_injection_script = include_str!("pwsh-install-template.ps1").replace("!!CMDDIR!!", &coreutils_cmd_dir); + let pwsh_injection_script = + include_str!("pwsh-install-template.ps1").replace("!!CMDDIR!!", &coreutils_cmd_dir); ctx.add_item(Item::pwsh(pwsh_injection_script))?; if config.windows.cmd_mkdir { let link_path = hmgr::paths::binary(bin_name!("mkdir")).into_utf8()?; @@ -240,7 +263,8 @@ r"New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\coreutils' -Name 'DefaultSort } fn get_ms_coreutils_reg_value(value: &str) -> cu::Result { - let script = format!("(Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\coreutils').{value}"); + let script = + format!("(Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\coreutils').{value}"); let (child, output) = cu::which("powershell")? .command() .args(["-NoLogo", "-NoProfile", "-Command", &script]) diff --git a/packages/registry/src/packages/coreutils/pwsh-install-template.ps1 b/packages/registry/src/packages/coreutils/pwsh-install-template.ps1 index 1f9b5fa..eab7825 100644 --- a/packages/registry/src/packages/coreutils/pwsh-install-template.ps1 +++ b/packages/registry/src/packages/coreutils/pwsh-install-template.ps1 @@ -1,218 +1,218 @@ -# https://github.com/microsoft/coreutils/blob/main/src/pwsh-install-template.ps1 -# # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv -# Inlining the template into the profile shaves off ~10ms (25%). -# SHAFT_PATCH: take out { ls, date, rmdir, link } -$script:__COREUTILS__ = [System.Collections.Generic.HashSet[string]]::new( - [string[]]@( - 'arch', 'b2sum', 'base32', 'base64', 'basename', - 'basenc', 'cat', 'cksum', 'comm', 'cp', - 'csplit', 'cut', 'df', 'dirname', - 'du', 'echo', 'env', 'expr', 'factor', - 'false', 'find', 'fmt', 'fold', 'grep', - 'head', 'hostname', 'join', 'la', - 'ln', 'md5sum', 'mkdir', 'mktemp', - 'mv', 'nl', 'nproc', 'numfmt', 'od', - 'pathchk', 'pr', 'printenv', 'printf', 'ptx', - 'pwd', 'readlink', 'realpath', 'rm', - 'seq', 'sha1sum', 'sha224sum', 'sha256sum', 'sha384sum', - 'sha512sum', 'shuf', 'sleep', 'sort', 'split', - 'stat', 'sum', 'tac', 'tail', 'tee', - 'test', 'touch', 'tr', 'true', 'truncate', - 'tsort', 'unexpand', 'uniq', 'unlink', 'uptime', - 'wc', 'xargs', 'yes' - ), - [System.StringComparer]::OrdinalIgnoreCase -) - -$script:__COREUTILS_FAST_SKIP__ = [regex]::new( - '\b(?:' + ($script:__COREUTILS__ -join '|') + ')\b', - [System.Text.RegularExpressions.RegexOptions]::Compiled -bor ` - [System.Text.RegularExpressions.RegexOptions]::IgnoreCase -) - -# Casting the scriptblock to Func once and reusing it avoids the -# per-FindAll scriptblock-to-delegate wrapping overhead (~1.7x faster). -$script:__COREUTILS_CMD_PREDICATE__ = [System.Func[System.Management.Automation.Language.Ast, bool]] { - param($n) $n -is [System.Management.Automation.Language.CommandAst] -} - -$script:__COREUTILS_ARG_SPECIAL__ = [char[]] @("'", '"', '`', '$') - -# Wrap arguments into quotes. By being a function we can properly handle $variables. -# As per MSVCRT, any `\` before `"` must be doubled to escape them. -function global:__coreutils_q { - param($s) - '"' + (([string]$s) -replace '(\\*)"', '$1$1\"' -replace '(\\+)$', '$1$1') + '"' -} - -# PowerShell tokenizes `*"a"*` as [BareWord] instead of the expected [DoubleQuoted, BareWord, DoubleQuoted]. -# To work around that we use... regex. Group 1 = 'single', 2 = "double", 3 = `escape, 4 = bare run. -$script:__COREUTILS_ARG_RX__ = [regex]::new( - "'((?:[^']|'')*)'|""((?:[^""``]|""""|``.)*)""|``(.)|([^'""``]+)", - [System.Text.RegularExpressions.RegexOptions]::Compiled -) -$script:__COREUTILS_ARG_EVAL__ = [System.Text.RegularExpressions.MatchEvaluator] { - param($m) - if ($m.Groups[1].Success) { - # Single-quoted: literal. PS '' -> ', then MSVCRT-quote. - $body = $m.Groups[1].Value.Replace("''", "'") - if ($body -match '^(.*?)(\\+)$') { - return '"' + ($matches[1] -replace '(\\*)"', '$1$1\"') + '"' + $matches[2] - } - return '"' + ($body -replace '(\\*)"', '$1$1\"') + '"' - } - if ($m.Groups[2].Success) { - # Double-quoted: collapse PS quote-escapes to raw " / ', let ExpandString - # resolve `n / `t / $var, then MSVCRT-quote. - $body = $m.Groups[2].Value. - Replace('`"', '"'). - Replace("``'", "'"). - Replace('""', '"') - $body = $ExecutionContext.InvokeCommand.ExpandString($body) - if ($body -match '^(.*?)(\\+)$') { - return '"' + ($matches[1] -replace '(\\*)"', '$1$1\"') + '"' + $matches[2] - } - return '"' + ($body -replace '(\\*)"', '$1$1\"') + '"' - } - if ($m.Groups[3].Success) { - # Backtick-escaped char outside a string: " -> \"; everything else - # becomes a one-char quoted region so glob metas stay literal. - $c = $m.Groups[3].Value - if ($c -eq '"') { - return '\"' - } - return '"' + $c + '"' - } - # Bare run: passed through unquoted so coreutils can glob it; expand $vars. - return $ExecutionContext.InvokeCommand.ExpandString($m.Groups[4].Value) -} - -# 0: not tested, 1: coreutils not installed, 2: coreutils installed. -$script:__COREUTILS_CMD_DIR_TEST__ = 0 - -# PSConsoleHostReadLine override that rewrites coreutils command names to their -# .cmd equivalents after PSReadLine returns (history keeps the original). -# -# Why .cmd over .exe: PSNativeCommandArgumentPassing = 'Windows' results in a behavior -# where passing bare quotes to CreateProcess() is impossible. This prevents us from -# passing "*" as "*" to coreutils and instead will be given as a bare *. -# This causes it to treat it as a glob pattern. "*.cmd" files however are automatically -# treated as PSNativeCommandArgumentPassing = 'Legacy', which preserves quotes. -# It is the only possible workaround and the only way coreutils can work at all. -function PSConsoleHostReadLine { - [System.Diagnostics.DebuggerHidden()] - param() - - $lastRunStatus = $? - Microsoft.PowerShell.Core\Set-StrictMode -Off - $line = [Microsoft.PowerShell.PSConsoleReadLine]::ReadLine($host.Runspace, $ExecutionContext, $lastRunStatus) - - # If the line contains no coreutils name, we don't need to parse the AST at all. - if (-not $script:__COREUTILS_FAST_SKIP__.IsMatch($line)) { - return $line - } - - # Roamed/synced profiles can load this snippet on machines where coreutils is not installed. - # Test for the existence of the command directory once and remember the result. - if ($script:__COREUTILS_CMD_DIR_TEST__ -eq 0) { - $script:__COREUTILS_CMD_DIR_TEST__ = 1 - if (Test-Path -LiteralPath '!!CMDDIR!!' -PathType Container -ErrorAction Ignore) { - $script:__COREUTILS_CMD_DIR_TEST__ = 2 - } - } - if ($script:__COREUTILS_CMD_DIR_TEST__ -ne 2) { - return $line - } - - $ast = [System.Management.Automation.Language.Parser]::ParseInput($line, [ref]$null, [ref]$null) - $commands = $ast.FindAll($script:__COREUTILS_CMD_PREDICATE__, $true) - - # Process right-to-left so earlier offsets stay valid after each splice. - # In-place reverse beats Sort-Object for the typical 1-command line. - if ($commands.Count -gt 1) { - $commands = [System.Collections.Generic.List[object]]::new($commands) - $commands.Reverse() - } - - foreach ($cmd in $commands) { - $name = $cmd.GetCommandName() - if (!$name) { - continue - } - - $baseName = $name - if ($name.EndsWith('.exe') -or $name.EndsWith('.cmd')) { - $baseName = $name.Substring(0, $name.Length - 4) - } - if (!$script:__COREUTILS__.Contains($baseName)) { - continue - } - - # ls/la get colour + listing flags injected; la also rewrites to ls. - $cmdElement = $cmd.CommandElements[0] - $start = $cmdElement.Extent.StartOffset - $end = $cmdElement.Extent.EndOffset - $replacement = "& '!!CMDDIR!!" - - switch ($baseName) { - 'la' { $replacement += "ls.cmd' --color=auto -AFhl" } - # SHAFT_PATCH: 'ls' { $replacement += "ls.cmd' --color=auto" } - default { $replacement += "$baseName.cmd'" } - } - - # Walk command elements, merging adjacent ones whose extents touch - # (e.g. `'a'*` parses as [SingleQuoted, BareWord] but is one shell word). - # The inverse case `*'a'*` parses as a single BareWord whose text - # contains the embedded quotes, which is why AST-only analysis - # isn't enough and we still need to re-tokenize the source span. - $argsStart = $end - $argsEnd = $cmd.Extent.EndOffset - $rewrittenArgs = '' - $elements = $cmd.CommandElements - $count = $elements.Count - $i = 1 - while ($i -lt $count) { - $first = $elements[$i] - $wordStart = $first.Extent.StartOffset - $wordEnd = $first.Extent.EndOffset - $merged = $false - while ($i + 1 -lt $count -and $elements[$i + 1].Extent.StartOffset -eq $wordEnd) { - $i++ - $wordEnd = $elements[$i].Extent.EndOffset - $merged = $true - } - $source = $line.Substring($wordStart, $wordEnd - $wordStart) - $rewrittenArgs += $line.Substring($argsStart, $wordStart - $argsStart) - $argsStart = $wordEnd - # IndexOfAny beats running the regex per arg. - if ($source.IndexOfAny($script:__COREUTILS_ARG_SPECIAL__) -lt 0) { - $rewrittenArgs += $source - $i++ - continue - } - # A single un-merged PS expression that needs $var resolution - # (bare $var, "...$var...", $x.Member, $($expr), etc.). - # Defer evaluation to runtime so the value reaches coreutils as a literal arg. - # This matches POSIX behaviour where variable expansions don't result in globbing. - if (-not $merged -and - ($first -is [System.Management.Automation.Language.VariableExpressionAst] -or - $first -is [System.Management.Automation.Language.ExpandableStringExpressionAst] -or - $first -is [System.Management.Automation.Language.MemberExpressionAst])) { - $rewrittenArgs += '(__coreutils_q ' + $source + ')' - $i++ - continue - } - # Slow path: re-tokenise and re-emit as MSVCRT-style quoting, - # then wrap in PS single quotes so PS hands the body verbatim. - $windowsQuoted = $script:__COREUTILS_ARG_RX__.Replace($source, $script:__COREUTILS_ARG_EVAL__) - $rewrittenArgs += "'" + $windowsQuoted.Replace("'", "''") + "'" - $i++ - } - $rewrittenArgs += $line.Substring($argsStart, $argsEnd - $argsStart) - - $line = $line.Substring(0, $start) + $replacement + $rewrittenArgs + $line.Substring($argsEnd) - } - - return $line -} -# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# https://github.com/microsoft/coreutils/blob/main/src/pwsh-install-template.ps1 +# # vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv +# Inlining the template into the profile shaves off ~10ms (25%). +# SHAFT_PATCH: take out { ls, date, rmdir, link } +$script:__COREUTILS__ = [System.Collections.Generic.HashSet[string]]::new( + [string[]]@( + 'arch', 'b2sum', 'base32', 'base64', 'basename', + 'basenc', 'cat', 'cksum', 'comm', 'cp', + 'csplit', 'cut', 'df', 'dirname', + 'du', 'echo', 'env', 'expr', 'factor', + 'false', 'find', 'fmt', 'fold', 'grep', + 'head', 'hostname', 'join', 'la', + 'ln', 'md5sum', 'mkdir', 'mktemp', + 'mv', 'nl', 'nproc', 'numfmt', 'od', + 'pathchk', 'pr', 'printenv', 'printf', 'ptx', + 'pwd', 'readlink', 'realpath', 'rm', + 'seq', 'sha1sum', 'sha224sum', 'sha256sum', 'sha384sum', + 'sha512sum', 'shuf', 'sleep', 'sort', 'split', + 'stat', 'sum', 'tac', 'tail', 'tee', + 'test', 'touch', 'tr', 'true', 'truncate', + 'tsort', 'unexpand', 'uniq', 'unlink', 'uptime', + 'wc', 'xargs', 'yes' + ), + [System.StringComparer]::OrdinalIgnoreCase +) + +$script:__COREUTILS_FAST_SKIP__ = [regex]::new( + '\b(?:' + ($script:__COREUTILS__ -join '|') + ')\b', + [System.Text.RegularExpressions.RegexOptions]::Compiled -bor ` + [System.Text.RegularExpressions.RegexOptions]::IgnoreCase +) + +# Casting the scriptblock to Func once and reusing it avoids the +# per-FindAll scriptblock-to-delegate wrapping overhead (~1.7x faster). +$script:__COREUTILS_CMD_PREDICATE__ = [System.Func[System.Management.Automation.Language.Ast, bool]] { + param($n) $n -is [System.Management.Automation.Language.CommandAst] +} + +$script:__COREUTILS_ARG_SPECIAL__ = [char[]] @("'", '"', '`', '$') + +# Wrap arguments into quotes. By being a function we can properly handle $variables. +# As per MSVCRT, any `\` before `"` must be doubled to escape them. +function global:__coreutils_q { + param($s) + '"' + (([string]$s) -replace '(\\*)"', '$1$1\"' -replace '(\\+)$', '$1$1') + '"' +} + +# PowerShell tokenizes `*"a"*` as [BareWord] instead of the expected [DoubleQuoted, BareWord, DoubleQuoted]. +# To work around that we use... regex. Group 1 = 'single', 2 = "double", 3 = `escape, 4 = bare run. +$script:__COREUTILS_ARG_RX__ = [regex]::new( + "'((?:[^']|'')*)'|""((?:[^""``]|""""|``.)*)""|``(.)|([^'""``]+)", + [System.Text.RegularExpressions.RegexOptions]::Compiled +) +$script:__COREUTILS_ARG_EVAL__ = [System.Text.RegularExpressions.MatchEvaluator] { + param($m) + if ($m.Groups[1].Success) { + # Single-quoted: literal. PS '' -> ', then MSVCRT-quote. + $body = $m.Groups[1].Value.Replace("''", "'") + if ($body -match '^(.*?)(\\+)$') { + return '"' + ($matches[1] -replace '(\\*)"', '$1$1\"') + '"' + $matches[2] + } + return '"' + ($body -replace '(\\*)"', '$1$1\"') + '"' + } + if ($m.Groups[2].Success) { + # Double-quoted: collapse PS quote-escapes to raw " / ', let ExpandString + # resolve `n / `t / $var, then MSVCRT-quote. + $body = $m.Groups[2].Value. + Replace('`"', '"'). + Replace("``'", "'"). + Replace('""', '"') + $body = $ExecutionContext.InvokeCommand.ExpandString($body) + if ($body -match '^(.*?)(\\+)$') { + return '"' + ($matches[1] -replace '(\\*)"', '$1$1\"') + '"' + $matches[2] + } + return '"' + ($body -replace '(\\*)"', '$1$1\"') + '"' + } + if ($m.Groups[3].Success) { + # Backtick-escaped char outside a string: " -> \"; everything else + # becomes a one-char quoted region so glob metas stay literal. + $c = $m.Groups[3].Value + if ($c -eq '"') { + return '\"' + } + return '"' + $c + '"' + } + # Bare run: passed through unquoted so coreutils can glob it; expand $vars. + return $ExecutionContext.InvokeCommand.ExpandString($m.Groups[4].Value) +} + +# 0: not tested, 1: coreutils not installed, 2: coreutils installed. +$script:__COREUTILS_CMD_DIR_TEST__ = 0 + +# PSConsoleHostReadLine override that rewrites coreutils command names to their +# .cmd equivalents after PSReadLine returns (history keeps the original). +# +# Why .cmd over .exe: PSNativeCommandArgumentPassing = 'Windows' results in a behavior +# where passing bare quotes to CreateProcess() is impossible. This prevents us from +# passing "*" as "*" to coreutils and instead will be given as a bare *. +# This causes it to treat it as a glob pattern. "*.cmd" files however are automatically +# treated as PSNativeCommandArgumentPassing = 'Legacy', which preserves quotes. +# It is the only possible workaround and the only way coreutils can work at all. +function PSConsoleHostReadLine { + [System.Diagnostics.DebuggerHidden()] + param() + + $lastRunStatus = $? + Microsoft.PowerShell.Core\Set-StrictMode -Off + $line = [Microsoft.PowerShell.PSConsoleReadLine]::ReadLine($host.Runspace, $ExecutionContext, $lastRunStatus) + + # If the line contains no coreutils name, we don't need to parse the AST at all. + if (-not $script:__COREUTILS_FAST_SKIP__.IsMatch($line)) { + return $line + } + + # Roamed/synced profiles can load this snippet on machines where coreutils is not installed. + # Test for the existence of the command directory once and remember the result. + if ($script:__COREUTILS_CMD_DIR_TEST__ -eq 0) { + $script:__COREUTILS_CMD_DIR_TEST__ = 1 + if (Test-Path -LiteralPath '!!CMDDIR!!' -PathType Container -ErrorAction Ignore) { + $script:__COREUTILS_CMD_DIR_TEST__ = 2 + } + } + if ($script:__COREUTILS_CMD_DIR_TEST__ -ne 2) { + return $line + } + + $ast = [System.Management.Automation.Language.Parser]::ParseInput($line, [ref]$null, [ref]$null) + $commands = $ast.FindAll($script:__COREUTILS_CMD_PREDICATE__, $true) + + # Process right-to-left so earlier offsets stay valid after each splice. + # In-place reverse beats Sort-Object for the typical 1-command line. + if ($commands.Count -gt 1) { + $commands = [System.Collections.Generic.List[object]]::new($commands) + $commands.Reverse() + } + + foreach ($cmd in $commands) { + $name = $cmd.GetCommandName() + if (!$name) { + continue + } + + $baseName = $name + if ($name.EndsWith('.exe') -or $name.EndsWith('.cmd')) { + $baseName = $name.Substring(0, $name.Length - 4) + } + if (!$script:__COREUTILS__.Contains($baseName)) { + continue + } + + # ls/la get colour + listing flags injected; la also rewrites to ls. + $cmdElement = $cmd.CommandElements[0] + $start = $cmdElement.Extent.StartOffset + $end = $cmdElement.Extent.EndOffset + $replacement = "& '!!CMDDIR!!" + + switch ($baseName) { + 'la' { $replacement += "ls.cmd' --color=auto -AFhl" } + # SHAFT_PATCH: 'ls' { $replacement += "ls.cmd' --color=auto" } + default { $replacement += "$baseName.cmd'" } + } + + # Walk command elements, merging adjacent ones whose extents touch + # (e.g. `'a'*` parses as [SingleQuoted, BareWord] but is one shell word). + # The inverse case `*'a'*` parses as a single BareWord whose text + # contains the embedded quotes, which is why AST-only analysis + # isn't enough and we still need to re-tokenize the source span. + $argsStart = $end + $argsEnd = $cmd.Extent.EndOffset + $rewrittenArgs = '' + $elements = $cmd.CommandElements + $count = $elements.Count + $i = 1 + while ($i -lt $count) { + $first = $elements[$i] + $wordStart = $first.Extent.StartOffset + $wordEnd = $first.Extent.EndOffset + $merged = $false + while ($i + 1 -lt $count -and $elements[$i + 1].Extent.StartOffset -eq $wordEnd) { + $i++ + $wordEnd = $elements[$i].Extent.EndOffset + $merged = $true + } + $source = $line.Substring($wordStart, $wordEnd - $wordStart) + $rewrittenArgs += $line.Substring($argsStart, $wordStart - $argsStart) + $argsStart = $wordEnd + # IndexOfAny beats running the regex per arg. + if ($source.IndexOfAny($script:__COREUTILS_ARG_SPECIAL__) -lt 0) { + $rewrittenArgs += $source + $i++ + continue + } + # A single un-merged PS expression that needs $var resolution + # (bare $var, "...$var...", $x.Member, $($expr), etc.). + # Defer evaluation to runtime so the value reaches coreutils as a literal arg. + # This matches POSIX behaviour where variable expansions don't result in globbing. + if (-not $merged -and + ($first -is [System.Management.Automation.Language.VariableExpressionAst] -or + $first -is [System.Management.Automation.Language.ExpandableStringExpressionAst] -or + $first -is [System.Management.Automation.Language.MemberExpressionAst])) { + $rewrittenArgs += '(__coreutils_q ' + $source + ')' + $i++ + continue + } + # Slow path: re-tokenise and re-emit as MSVCRT-style quoting, + # then wrap in PS single quotes so PS hands the body verbatim. + $windowsQuoted = $script:__COREUTILS_ARG_RX__.Replace($source, $script:__COREUTILS_ARG_EVAL__) + $rewrittenArgs += "'" + $windowsQuoted.Replace("'", "''") + "'" + $i++ + } + $rewrittenArgs += $line.Substring($argsStart, $argsEnd - $argsStart) + + $line = $line.Substring(0, $start) + $replacement + $rewrittenArgs + $line.Substring($argsEnd) + } + + return $line +} +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/packages/registry/src/packages/coreutils/sed.rs b/packages/registry/src/packages/coreutils/sed.rs index c36d1a4..8a44cc3 100644 --- a/packages/registry/src/packages/coreutils/sed.rs +++ b/packages/registry/src/packages/coreutils/sed.rs @@ -1,18 +1,18 @@ -use crate::pre::*; - -pub fn verify() -> cu::Result { - let v = check_cargo!("sed"); - check_outdated!(&v.version, metadata[coreutils::uutils_sed]::VERSION); - Ok(Verified::UpToDate) -} -pub fn install(ctx: &Context) -> cu::Result<()> { - if let Ok(Verified::UpToDate) = verify() { - return Ok(()); - } - epkg::cargo::binstall("sed", ctx.bar_ref())?; - Ok(()) -} -pub fn uninstall(_: &Context) -> cu::Result<()> { - epkg::cargo::uninstall("sed")?; - Ok(()) -} +use crate::pre::*; + +pub fn verify() -> cu::Result { + let v = check_cargo!("sed"); + check_outdated!(&v.version, metadata[coreutils::uutils_sed]::VERSION); + Ok(Verified::UpToDate) +} +pub fn install(ctx: &Context) -> cu::Result<()> { + if let Ok(Verified::UpToDate) = verify() { + return Ok(()); + } + epkg::cargo::binstall("sed", ctx.bar_ref())?; + Ok(()) +} +pub fn uninstall(_: &Context) -> cu::Result<()> { + epkg::cargo::uninstall("sed")?; + Ok(()) +} diff --git a/packages/registry/src/packages/shellutils/mod_linux-pacman.rs b/packages/registry/src/packages/shellutils/mod_linux-pacman.rs index a85eaaa..6493b22 100644 --- a/packages/registry/src/packages/shellutils/mod_linux-pacman.rs +++ b/packages/registry/src/packages/shellutils/mod_linux-pacman.rs @@ -82,10 +82,10 @@ pub fn install(ctx: &Context) -> cu::Result<()> { epkg::cargo::binstall("bat", ctx.bar_ref())?; epkg::cargo::binstall("du-dust", ctx.bar_ref())?; - epkg::cargo::install("fd-find", ctx.bar_ref())?; + epkg::cargo::install("fd-find", None, ctx.bar_ref())?; epkg::cargo::binstall("ripgrep", ctx.bar_ref())?; - epkg::cargo::install("websocat", ctx.bar_ref())?; - epkg::cargo::install("zoxide", ctx.bar_ref())?; + epkg::cargo::install("websocat", None, ctx.bar_ref())?; + epkg::cargo::install("zoxide", None, ctx.bar_ref())?; n::install(ctx)?; viopen::install(ctx)?; From 0f99bf35c333aed46aabf8b1c44b9880f6215d00 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Wed, 10 Jun 2026 22:20:48 -0700 Subject: [PATCH 4/4] clean up --- packages/registry/src/packages/coreutils/mod_win.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/registry/src/packages/coreutils/mod_win.rs b/packages/registry/src/packages/coreutils/mod_win.rs index 5c13df4..2947c8c 100644 --- a/packages/registry/src/packages/coreutils/mod_win.rs +++ b/packages/registry/src/packages/coreutils/mod_win.rs @@ -24,9 +24,9 @@ register_binaries!( ); binary_dependencies!(Git, CargoBinstall); -static PS_ALIASES: &[&str] = &[ - "cat", "cp", "dir", "echo", "ls", "mv", "pwd", "rm", "rmdir", "sort", "sleep", "tee", -]; +// static PS_ALIASES: &[&str] = &[ +// "cat", "cp", "dir", "echo", "ls", "mv", "pwd", "rm", "rmdir", "sort", "sleep", "tee", +// ]; // keep in sync with pwsh-install-template.ps1 #[rustfmt::skip] static MS_COREUTILS_LIST: &[&str] = &[