use std::path::{Path, PathBuf};
mod config;
mod devcontainer;
mod git_pre_commit;
mod github_action;
mod install_script;
mod task_docs;
mod task_stubs;
mod tool_stub;
#[derive(Debug, usage_rs::Args)]
#[usage(visible_alias = "gen", alias = "g")]
pub(crate) struct Generate {
#[usage(subcommand)]
command: Commands,
}
#[derive(Debug, usage_rs::Subcommands)]
enum Commands {
#[usage(hide)]
Bootstrap(install_script::InstallScript),
Config(config::Config),
Devcontainer(devcontainer::Devcontainer),
GitPreCommit(git_pre_commit::GitPreCommit),
GithubAction(github_action::GithubAction),
InstallScript(install_script::InstallScript),
TaskDocs(task_docs::TaskDocs),
TaskStubs(task_stubs::TaskStubs),
ToolStub(tool_stub::ToolStub),
}
impl Commands {
pub(crate) async fn run(self) -> eyre::Result<()> {
match self {
Self::Bootstrap(cmd) => {
deprecated_at!(
"2026.9.0",
"2027.9.0",
"cli.generate.bootstrap",
"`mise generate bootstrap` is deprecated. Use `mise generate install-script` instead."
);
cmd.run().await
}
Self::Config(cmd) => cmd.run().await,
Self::Devcontainer(cmd) => cmd.run().await,
Self::GitPreCommit(cmd) => cmd.run().await,
Self::GithubAction(cmd) => cmd.run().await,
Self::InstallScript(cmd) => cmd.run().await,
Self::TaskDocs(cmd) => cmd.run().await,
Self::TaskStubs(cmd) => cmd.run().await,
Self::ToolStub(cmd) => cmd.run().await,
}
}
}
impl Generate {
pub(crate) async fn run(self) -> eyre::Result<()> {
self.command.run().await
}
}
pub(super) fn windows_launcher_path(stub: &Path) -> Option<PathBuf> {
windows_launcher_path_with_ext(stub, "cmd")
}
pub(super) fn windows_exe_launcher_path(stub: &Path) -> Option<PathBuf> {
windows_launcher_path_with_ext(stub, "exe")
}
fn windows_launcher_path_with_ext(stub: &Path, launcher_ext: &str) -> Option<PathBuf> {
let name = stub.file_name()?.to_str()?;
let ext = name.rsplit_once('.').map(|(_, e)| e.to_ascii_lowercase());
if matches!(ext.as_deref(), Some("cmd" | "bat" | "exe")) {
return None;
}
Some(stub.with_file_name(format!("{name}.{launcher_ext}")))
}
pub(super) const WINDOWS_LAUNCHER_MARKER: &str = "rem generated by mise";
pub(super) fn windows_launcher_body(command: &str) -> String {
let raw = crate::env::LAUNCHER_RAW_CMDLINE_ENV;
let path = crate::env::LAUNCHER_PATH_ENV;
let sentinel = crate::env::LAUNCHER_ARGS_SENTINEL;
[
"@echo off",
WINDOWS_LAUNCHER_MARKER,
"setlocal DisableDelayedExpansion",
&format!("set \"{path}=%~f0\""),
"setlocal EnableDelayedExpansion",
&format!("set \"{raw}=!CMDCMDLINE!\""),
&format!("if \"!{raw}!\"==\"!{raw}:%{path}%=!\" goto mise_launcher_fallback"),
&format!("{command} {sentinel} %*"),
"exit !ERRORLEVEL!",
":mise_launcher_fallback",
&format!("set \"{raw}=\""),
&format!("set \"{path}=\""),
&format!("{command} {sentinel} %*"),
]
.join("\r\n")
+ "\r\n"
}
const WINDOWS_LAUNCHER_COMMAND_LINE: usize = 7;
pub(super) fn is_generated_launcher(contents: &str) -> bool {
let lines: Vec<&str> = contents.lines().collect();
if lines.first() != Some(&"@echo off") || lines.get(1) != Some(&WINDOWS_LAUNCHER_MARKER) {
return false;
}
if lines.len() == 3 {
return lines[2].ends_with(" %*");
}
let suffix = format!(" {} %*", crate::env::LAUNCHER_ARGS_SENTINEL);
let Some(command) = lines
.get(WINDOWS_LAUNCHER_COMMAND_LINE)
.copied()
.and_then(|line| line.strip_suffix(&suffix))
else {
return false;
};
windows_launcher_body(command).lines().eq(lines)
}
pub(super) fn cmd_quote(s: &str) -> String {
format!("\"{}\"", s.replace('%', "%%"))
}
#[cfg(test)]
mod windows_launcher_tests {
use super::*;
#[test]
fn launcher_sits_beside_the_stub() {
assert_eq!(
windows_launcher_path(Path::new("bin/hello")),
Some(PathBuf::from("bin/hello.cmd"))
);
assert_eq!(
windows_launcher_path(Path::new("my.tool")),
Some(PathBuf::from("my.tool.cmd"))
);
}
#[test]
fn a_name_that_is_already_executable_gets_none() {
for name in ["mytool.cmd", "mytool.BAT", "mytool.exe"] {
assert_eq!(windows_launcher_path(Path::new(name)), None, "{name}");
assert_eq!(windows_exe_launcher_path(Path::new(name)), None, "{name}");
}
}
#[test]
fn the_two_launcher_forms_share_a_name() {
let stub = Path::new("bin/hello");
assert_eq!(
windows_launcher_path(stub),
Some(PathBuf::from("bin/hello.cmd"))
);
assert_eq!(
windows_exe_launcher_path(stub),
Some(PathBuf::from("bin/hello.exe"))
);
}
#[test]
fn body_forwards_arguments() {
let body = windows_launcher_body("mise run hello");
let lines: Vec<&str> = body.lines().collect();
assert_eq!(lines[0], "@echo off");
assert_eq!(lines[1], WINDOWS_LAUNCHER_MARKER);
let expected = format!("mise run hello {} %*", crate::env::LAUNCHER_ARGS_SENTINEL);
assert_eq!(lines[WINDOWS_LAUNCHER_COMMAND_LINE], expected);
assert_eq!(lines[lines.len() - 1], expected);
assert!(body.ends_with("\r\n"), "{body:?}");
assert!(!body.contains("\n\n"), "{body:?}");
}
#[test]
fn the_shim_body_carries_the_same_recovery() {
fn recovery(body: &str, label: &str, command: &str) -> Vec<String> {
body.lines()
.skip_while(|l| !l.contains("CMDCMDLINE"))
.map(|l| l.replace(label, "LABEL").replace(command, "COMMAND"))
.collect()
}
assert_eq!(
recovery(
&crate::shims::windows_file_shim_body("hello"),
"mise_shim_fallback",
"mise x -- hello",
),
recovery(
&windows_launcher_body("mise run hello"),
"mise_launcher_fallback",
"mise run hello",
),
);
}
#[test]
fn the_command_line_index_matches_the_body() {
let body = windows_launcher_body("SOME-UNIQUE-COMMAND");
let lines: Vec<&str> = body.lines().collect();
assert!(
lines[WINDOWS_LAUNCHER_COMMAND_LINE].starts_with("SOME-UNIQUE-COMMAND "),
"{:?}",
lines[WINDOWS_LAUNCHER_COMMAND_LINE]
);
}
#[test]
fn the_guard_and_the_exit_are_present() {
let body = windows_launcher_body("mise run hello");
assert!(body.contains("goto mise_launcher_fallback"), "{body:?}");
assert!(body.contains(":mise_launcher_fallback"), "{body:?}");
assert!(body.contains("\r\nexit !ERRORLEVEL!\r\n"), "{body:?}");
assert!(body.contains("!CMDCMDLINE!"), "{body:?}");
assert!(!body.contains("%CMDCMDLINE%"), "{body:?}");
}
#[test]
fn every_body_carries_the_ownership_marker() {
for command in ["mise run hello", r#"mise tool-stub "%~dpn0""#] {
let body = windows_launcher_body(command);
assert!(
body.lines().nth(1) == Some(WINDOWS_LAUNCHER_MARKER),
"{body:?}"
);
}
}
#[test]
fn a_launcher_is_recognised_whatever_command_it_carries() {
for command in [
"mise run hello",
r#""C:\Program Files\mise.exe" run hello"#,
r#"mise tool-stub "%~dpn0""#,
"some-other-binary run build",
] {
assert!(
is_generated_launcher(&windows_launcher_body(command)),
"{command}"
);
}
}
#[test]
fn a_launcher_from_before_argument_recovery_is_still_ours() {
for command in [
"mise run hello",
r#""mise" run "build:all""#,
r#"mise tool-stub "%~dpn0""#,
] {
let old = format!("@echo off\r\n{WINDOWS_LAUNCHER_MARKER}\r\n{command} %*\r\n");
assert!(is_generated_launcher(&old), "{old:?}");
}
}
#[test]
fn a_launcher_with_a_line_added_to_it_is_not_ours() {
let body = windows_launcher_body("mise run hello");
assert!(is_generated_launcher(&body));
assert!(!is_generated_launcher(&format!("{body}echo done\r\n")));
let tampered = body.replace("exit !ERRORLEVEL!", "exit /b !ERRORLEVEL!");
assert_ne!(tampered, body);
assert!(!is_generated_launcher(&tampered), "{tampered:?}");
}
#[test]
fn a_launcher_survives_line_ending_normalisation() {
let body = windows_launcher_body("mise run hello");
assert!(is_generated_launcher(&body.replace("\r\n", "\n")));
}
#[test]
fn a_launcher_without_the_marker_is_not_ours() {
for contents in [
"@echo off\r\nmise run hello %*\r\n",
"@echo off\r\nrem hand written\r\nmise run hello %*\r\n",
"rem generated by mise\r\nmise run hello %*\r\n",
"@echo off\r\nrem generated by mise\r\nmise run hello %*\r\necho done\r\n",
"@echo off\r\nrem generated by mise\r\nmise run hello\r\n",
"",
] {
assert!(!is_generated_launcher(contents), "{contents:?}");
}
}
#[test]
fn quoting_survives_cmd_metacharacters() {
assert_eq!(cmd_quote("mise"), "\"mise\"");
assert_eq!(
cmd_quote(r"C:\Program Files\mise.exe"),
"\"C:\\Program Files\\mise.exe\""
);
assert_eq!(cmd_quote(r"C:\a&b\mise.exe"), "\"C:\\a&b\\mise.exe\"");
assert_eq!(cmd_quote(r"C:\p%c\mise.exe"), "\"C:\\p%%c\\mise.exe\"");
assert_eq!(cmd_quote("%PATH%"), "\"%%PATH%%\"");
}
}