This commit is contained in:
Sebastian Moser
2026-10-23 15:27:31 +02:00
parent 9141fc5441
commit 37cb94d1d4
11 changed files with 482 additions and 63 deletions

View File

@@ -21,6 +21,57 @@ pub fn write_to_mize_stdout(args: std::fmt::Arguments) -> MizeResult<()> {
Ok(())
}
/// Mize Shell (msh) macro - wraps command execution with nixpkgs executable resolution.
///
/// If the executable is given as a bare name (identifier), it is resolved via
/// [`msh_get_nix_bin`] to a nixpkgs store path. If it is given as a string literal
/// containing `/`, it is treated as a path and used as-is.
///
/// # Examples
/// ```ignore
/// msh!(git status)?; // resolves `git` from nixpkgs
/// msh!(curl https://example.com)?;
/// msh!("/usr/bin/git" status)?; // uses path directly
/// ```
#[macro_export]
macro_rules! msh {
// First token is a string literal — check if it's a path at runtime
($cmd:literal $($rest:tt)*) => {{
let cmd_str: &str = $cmd;
let resolved: String = if cmd_str.contains('/') {
cmd_str.to_string()
} else {
$crate::msh_get_nix_bin(cmd_str)
};
let mut cmd = ::std::process::Command::new(resolved);
$(cmd.arg(::std::stringify!($rest));)*
let status = cmd.status()?;
if status.success() {
::std::result::Result::Ok(())
} else {
::std::result::Result::Err(::std::io::Error::new(
::std::io::ErrorKind::Other,
::std::format!("msh: command exited with {}", status),
))
}
}};
// First token is an identifier — always resolve via nixpkgs
($cmd:ident $($rest:tt)*) => {{
let resolved: String = $crate::msh_get_nix_bin(::std::stringify!($cmd));
let mut cmd = ::std::process::Command::new(resolved);
$(cmd.arg(::std::stringify!($rest));)*
let status = cmd.status()?;
if status.success() {
::std::result::Result::Ok(())
} else {
::std::result::Result::Err(::std::io::Error::new(
::std::io::ErrorKind::Other,
::std::format!("msh: command exited with {}", status),
))
}
}};
}
pub mod prelude {
pub use super::*;
pub use instance::Mize;
@@ -97,6 +148,99 @@ pub extern "C" fn mize_rust_hack_set_global(mize: Arc<Mutex<Mize>>) {
let _ = GLOBAL_MIZE.set(mize);
}
/// Resolve an executable name to its full path in the nixpkgs store.
///
/// Reads `nix.pkgs.ref` (nixpkgs commit hash) and `nix.bin` (path to the `nix`
/// binary) from the Mize config, then runs:
///
/// ```text
/// <nix.bin> eval --raw nixpkgs/<hash>#<pkg>
/// ```
///
/// and returns `<outPath>/bin/<executable>`.
///
/// Certain executable names are mapped to different nixpkgs attribute names
/// via a hardcoded match (e.g. `npm` → `nodejs`).
///
/// # Panics
/// Panics if the global Mize instance is not set, the config values are missing,
/// or `nix eval` fails.
pub fn msh_get_nix_bin(name: &str) -> String {
// Hardcoded mapping of executable names to nixpkgs attribute names
let pkg = match name {
"npm" => "nodejs",
"node" => "nodejs",
"yarn" => "yarn",
"pnpm" => "nodePackages.pnpm",
_ => name,
};
let mut mize = get_global_mize().expect("msh_get_nix_bin: no global Mize instance set");
let nixpkgs_ref = mize
.get_config("nix.pkgs.ref")
.expect("msh_get_nix_bin: failed to get config nix.pkgs.ref")
.value_string()
.expect("msh_get_nix_bin: nix.pkgs.ref is not a string");
let nix_bin = mize
.get_config("nix.bin")
.expect("msh_get_nix_bin: failed to get config nix.bin")
.value_string()
.expect("msh_get_nix_bin: nix.bin is not a string");
// Run: nix eval --raw nixpkgs/<hash>#<pkg>
// --raw outputs just the store path without quotes
let attr = format!("nixpkgs/{}#{}", nixpkgs_ref, pkg);
let output = std::process::Command::new(&nix_bin)
.args(["eval", "--raw", &attr])
.output()
.expect("msh_get_nix_bin: failed to spawn nix eval");
if !output.status.success() {
panic!(
"msh_get_nix_bin: nix eval failed for {}: {}",
attr,
String::from_utf8_lossy(&output.stderr)
);
}
let out_path = String::from_utf8(output.stdout)
.expect("msh_get_nix_bin: nix eval output is not valid UTF-8")
.trim()
.to_string();
format!("{}/bin/{}", out_path, name)
}
/// Run a command with Mize Shell executable resolution.
///
/// This is the function-based equivalent of the [`msh!`] macro, with proper
/// argument handling (no `stringify!` limitations).
///
/// If `cmd` contains `/` it is treated as a path and used as-is.
/// Otherwise it is resolved via [`msh_get_nix_bin`].
///
/// Returns `Ok(())` if the command exits successfully, or an `io::Error` otherwise.
pub fn msh_run(cmd: &str, args: &[&str]) -> std::io::Result<()> {
let resolved = if cmd.contains('/') {
cmd.to_string()
} else {
msh_get_nix_bin(cmd)
};
let mut command = std::process::Command::new(resolved);
command.args(args);
let status = command.status()?;
if status.success() {
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("msh: command exited with {}", status),
))
}
}
// platform specific stuff
pub mod platform {
use crate::{Mize, MizeResult};