app: every WSL script runs from a file, never inline on the command line (src/wslhost.rs: write_script, bash_line, command)

5 October 2026, PC 2 on 0.3.5: the prover probe's inline script (double quotes, a path with a space) reached bash
mangled and the app said proving needed the WSL2 setup while /opt/igneum held the host. Now the probe, run_tool, the
setup launch, igneum-prove-verify (probe and run; the wslpath round trip dropped) and jobrun's six inline scripts
write a UTF-8, LF, no-BOM file under %LOCALAPPDATA%\igneum\wsl and run wsl -d <distro> -- bash [-l] '<file>' '<arg>'...
with every word single-quoted, placed raw on the command line; arguments reach the script as $1, $2. Unit tests pin
that the line never carries a double quote or a newline and that the file has LF endings and no BOM.
This commit is contained in:
igneum-labs 2026-10-05 08:47:40 +00:00
parent 090affa08a
commit 97e53c1d00
4 changed files with 233 additions and 45 deletions

View file

@ -48,22 +48,22 @@ fn quiet(cmd: &mut Command) -> &mut Command {
cmd
}
fn shell_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', "'\\''"))
/// The drive-letter mapping (src/wslhost.rs). A `wslpath -a` round trip through wsl.exe was dropped on 5 October
/// 2026: a path with a space on that command line is split by the shell inside the distribution.
fn to_wsl(p: &Path) -> String {
wslhost::wsl_path(p)
}
/// `wslpath -a <windows path>` inside the distribution; the drive-letter mapping when wslpath did not answer.
fn to_wsl(p: &Path) -> String {
let out = quiet(&mut Command::new(wsl_exe())).args(["-d", wslhost::DISTRO, "--", "wslpath", "-a"]).arg(p).stdin(Stdio::null()).output();
if let Ok(o) = out {
if o.status.success() {
let s = String::from_utf8_lossy(&o.stdout).trim().to_string();
if s.starts_with('/') {
return s;
}
/// The script file: runs the probe or the host (`write_script` under %LOCALAPPDATA%\igneum\wsl, removed after the
/// run); exits 2 when the file cannot be written.
fn script_file(stem: &str, body: &str) -> wslhost::ScriptFile {
match wslhost::write_script(stem, body) {
Ok(f) => f,
Err(e) => {
eprintln!("igneum-prove-verify: cannot write the {stem} script under {}: {e}", wslhost::script_dir().display());
std::process::exit(NO_HOST);
}
}
wslhost::wsl_path(p)
}
/// The host's arguments with `--proof <path>` rewritten for WSL. Pure, so it has a test.
@ -91,12 +91,10 @@ pub fn rewrite_args<F: Fn(&Path) -> String>(args: &[String], to_wsl: F) -> Vec<S
/// The script that finds the host and replaces the shell with it: the host's exit code is the script's. With no
/// host, a line on stderr naming the places looked at, and exit 2.
pub fn run_script(bin_dir: &Path, host_args: &[String]) -> String {
pub fn run_script(bin_dir: &Path) -> String {
let lookup = wslhost::lookup_script(bin_dir);
let args: Vec<String> = host_args.iter().map(|a| shell_quote(a)).collect();
format!(
"h=$({lookup}); if [ -n \"$h\" ]; then exec \"$h\" {}; fi; echo 'igneum-prove-verify: no igneum-prove-host in WSL2 (looked at: {})' >&2; exit {NO_HOST}",
args.join(" "),
"h=$({lookup}); if [ -n \"$h\" ]; then exec \"$h\" \"$@\"; fi; echo 'igneum-prove-verify: no igneum-prove-host in WSL2 (looked at: {})' >&2; exit {NO_HOST}",
wslhost::candidates_text(bin_dir).replace('\'', "'\\''")
)
}
@ -109,7 +107,8 @@ fn main() {
return;
}
if args.iter().any(|a| a == "--probe") {
let out = quiet(&mut Command::new(wsl_exe())).args(["-d", wslhost::DISTRO, "--", "bash", "-lc", &wslhost::lookup_script(&bin_dir)]).stdin(Stdio::null()).output();
let file = script_file("verify-probe", &wslhost::lookup_script(&bin_dir));
let out = quiet(&mut wslhost::command(&wsl_exe(), wslhost::DISTRO, None, &file.path, true, &[])).stdin(Stdio::null()).output();
let host = out.ok().filter(|o| o.status.success()).map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()).unwrap_or_default();
if host.is_empty() {
eprintln!("igneum-prove-verify: no igneum-prove-host in WSL2 ({}) (looked at: {})", wslhost::DISTRO, wslhost::candidates_text(&bin_dir));
@ -119,8 +118,9 @@ fn main() {
return;
}
let host_args = rewrite_args(&args, to_wsl);
let script = run_script(&bin_dir, &host_args);
let status = quiet(&mut Command::new(wsl_exe())).args(["-d", wslhost::DISTRO, "--", "bash", "-lc", &script]).stdin(Stdio::null()).status();
let file = script_file("verify-run", &run_script(&bin_dir));
let argv: Vec<&str> = host_args.iter().map(|a| a.as_str()).collect();
let status = quiet(&mut wslhost::command(&wsl_exe(), wslhost::DISTRO, None, &file.path, true, &argv)).stdin(Stdio::null()).status();
match status {
Ok(st) => std::process::exit(st.code().unwrap_or(1)),
Err(e) => {
@ -145,9 +145,12 @@ mod tests {
#[test]
fn the_script_execs_the_first_host_and_exits_2_without_one() {
let s = run_script(Path::new("C:\\Igneum"), &["--mode".into(), "verify".into(), "--statement".into(), "0xab".into()]);
let s = run_script(Path::new("C:\\Igneum"));
assert!(s.starts_with("h=$(for f in '/mnt/c/Igneum/wsl2/bin/igneum-prove-host' ~/igneum-prove/proving/igneum-prove/target/release/igneum-prove-host ~/igneum-prove/target/release/igneum-prove-host '/opt/igneum/igneum-prove-host'; do"), "{s}");
assert!(s.contains("exec \"$h\" '--mode' 'verify' '--statement' '0xab'; fi;"));
assert!(s.contains("exec \"$h\" \"$@\"; fi;"), "{s}");
// the arguments travel on the command line as $1, $2... with no double quote anywhere
let line = wslhost::bash_line(Path::new("C:\\Users\\x\\AppData\\Local\\igneum\\wsl\\verify-run-1-0.sh"), true, &["--mode", "verify", "--proof", "/mnt/c/Users/x/p.bin", "--statement", "0xab"]);
assert_eq!(line, "bash -l '/mnt/c/Users/x/AppData/Local/igneum/wsl/verify-run-1-0.sh' '--mode' 'verify' '--proof' '/mnt/c/Users/x/p.bin' '--statement' '0xab'");
assert!(s.ends_with("exit 2"));
assert!(s.contains("looked at: /mnt/c/Igneum/wsl2/bin/igneum-prove-host, ~/igneum-prove/proving/"));
}

View file

@ -833,12 +833,8 @@ fn probe(req: &str, wsl_user: &str) -> Result<String, String> {
users.push(DEFAULT_WSL_USER);
users.push("");
for u in users {
let mut c = Command::new(&wsl);
c.args(["-d", DEFAULT_DISTRO]);
if !u.is_empty() {
c.args(["-u", u]);
}
c.args(["--", "bash", "-lc", PROVER_PROBE]);
let file = crate::wslhost::write_script("prover-probe", PROVER_PROBE).map_err(|e| format!("cannot write the WSL probe script: {e}"))?;
let mut c = crate::wslhost::command(&wsl, DEFAULT_DISTRO, Some(u), &file.path, true, &[]);
let (code, out) = run_capture(&mut c, Duration::from_secs(90));
if code == Some(0) {
return Ok(format!("toolchain in {DEFAULT_DISTRO}{}: {}", if u.is_empty() { " (default user)".to_string() } else { format!(" as {u}") }, short_out(&out)));
@ -1387,7 +1383,8 @@ fn run_shard_benchmark(shared: &Arc<Shared>, job: &Job, sink: &Sink, data_root:
let shard = fixtures[0].clone();
let blocks = fixtures[1..].join(" ");
// what this run sees inside WSL: the account's own Ubuntu, so say who we are there
let (ccode, cout) = run_capture(Command::new(crate::platform::tool("wsl")).args(["-d", &distro, "-u", &user, "--", "bash", "-c", "echo \"wsl user $(id -un) uid $(id -u) home $HOME\"; nvidia-smi -L 2>&1 | head -1; ls -d \"$HOME/.sp1\" \"$HOME/igneum-prove\" 2>&1 | head -2"]), Duration::from_secs(60));
let ctx = crate::wslhost::write_script("wsl-context", "echo \"wsl user $(id -un) uid $(id -u) home $HOME\"; nvidia-smi -L 2>&1 | head -1; ls -d \"$HOME/.sp1\" \"$HOME/igneum-prove\" 2>&1 | head -2").map_err(|e| format!("cannot write the WSL context script: {e}"))?;
let (ccode, cout) = run_capture(&mut crate::wslhost::command(&crate::platform::tool("wsl"), &distro, Some(&user), &ctx.path, false, &[]), Duration::from_secs(60));
sink.line(&format!("wsl context (-u {user}): exit {ccode:?}: {}", short_out(&cout)));
// the payload ships the Linux host next to the app (wsl2\bin): no cargo, no toolchain, run it fixture by fixture;
// params.build = true forces the package's prove-shard.sh (cargo build inside the distro) instead
@ -1411,8 +1408,8 @@ fn run_shard_benchmark(shared: &Arc<Shared>, job: &Job, sink: &Sink, data_root:
}
let mode = if i == 0 { "shard --shard 0" } else { "block" };
let line = format!("export PATH=\"/usr/local/cuda/bin:$PATH\"; CUDA_DIR=$(ls -d /usr/local/cuda-12.* 2>/dev/null | sort -V | tail -1); export LD_LIBRARY_PATH=\"/usr/lib/wsl/lib:${{CUDA_DIR:+$CUDA_DIR/lib64:}}${{LD_LIBRARY_PATH:-}}\"; echo \"=== GPU run: {f} --mode {mode} (SP1_PROVER=cuda) ===\"; SP1_PROVER=cuda RUST_LOG=info '{host_wsl}' '{fixdir_wsl}/{f}.json' --mode {mode} --out '{results_wsl}/{f}-cuda-{started}.json'; rc=$?; echo \"run exit $rc at $(date -u +%FT%TZ)\"; exit $rc");
let mut cmd = Command::new(crate::platform::tool("wsl"));
cmd.args(["-d", &distro, "-u", &user, "--", "bash", "-c", &line]);
let run = crate::wslhost::write_script("gpu-run", &line).map_err(|e| format!("cannot write the WSL run script: {e}"))?;
let mut cmd = crate::wslhost::command(&crate::platform::tool("wsl"), &distro, Some(&user), &run.path, false, &[]);
cmd.current_dir(&pkg);
let r = run_streamed(&mut cmd, sink, ctl, left, shared, job, started, "shard benchmark running")?;
if r.code != Some(0) {
@ -1434,7 +1431,9 @@ fn run_shard_benchmark(shared: &Arc<Shared>, job: &Job, sink: &Sink, data_root:
};
if ran.code.is_none() {
// the Linux side outlives wsl.exe: end the prover there too
let _ = run_capture(Command::new(crate::platform::tool("wsl")).args(["-d", &distro, "-u", &user, "--", "bash", "-c", "pkill -f igneum-prove-host; pkill -f prove-shard.sh; true"]), Duration::from_secs(30));
if let Ok(kill) = crate::wslhost::write_script("prover-kill", "pkill -f igneum-prove-host; pkill -f prove-shard.sh; true") {
let _ = run_capture(&mut crate::wslhost::command(&crate::platform::tool("wsl"), &distro, Some(&user), &kill.path, false, &[]), Duration::from_secs(30));
}
}
sink.stage("uploading the results");
let mut uploaded = Vec::new();
@ -1485,7 +1484,9 @@ fn build_stage(shared: &Arc<Shared>, job: &Job, sink: &Sink, ctl: &Ctl, p: &jb::
cmd.current_dir(dir);
let ran = run_streamed(&mut cmd, sink, ctl, cap, shared, job, started, &format!("build: {stage}"))?;
if ran.code.is_none() {
let _ = run_capture(Command::new(crate::platform::tool("wsl")).args(["-d", &p.distro, "-u", &p.wsl_user, "--", "bash", "-c", jb::kill_script()]), Duration::from_secs(30));
if let Ok(kill) = crate::wslhost::write_script("build-kill", jb::kill_script()) {
let _ = run_capture(&mut crate::wslhost::command(&crate::platform::tool("wsl"), &p.distro, Some(&p.wsl_user), &kill.path, false, &[]), Duration::from_secs(30));
}
}
sink.line(&format!("STAGE {stage} {} {} {} s exit {}", if ran.timed_out { "timeout" } else { "end" }, jobs::format_time(crate::platform::unix_now()), t0.elapsed().as_secs(), ran.code.map(|c| c.to_string()).unwrap_or_else(|| "none".into())));
Ok(ran)
@ -1562,7 +1563,8 @@ fn run_build(shared: &Arc<Shared>, job: &Job, sink: &Sink, jobs_dir: &Path, ctl:
let ps = format!("[math]::Floor(([IO.DriveInfo]::new('{drive}')).AvailableFreeSpace/1GB)");
let (wc, wo) = run_capture(Command::new(crate::platform::tool("powershell")).args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &ps]), Duration::from_secs(40));
let win_free = if wc == Some(0) { jb::parse_free_gb(&wo) } else { None };
let (lc, lo) = run_capture(Command::new(&wsl).args(["-d", &p.distro, "-u", &p.wsl_user, "--", "bash", "-c", jb::free_gb_script()]), Duration::from_secs(120));
let free = crate::wslhost::write_script("free-gb", jb::free_gb_script()).map_err(|e| format!("cannot write the WSL free-space script: {e}"))?;
let (lc, lo) = run_capture(&mut crate::wslhost::command(&wsl, &p.distro, Some(&p.wsl_user), &free.path, false, &[]), Duration::from_secs(120));
let wsl_free = if lc == Some(0) { jb::parse_free_gb(&lo) } else { None };
sink.line(&format!("RESULT check {} drive {drive}: {} GB free, {} /root: {} GB free, floor {} GB", now(), win_free.map(|g| g.to_string()).unwrap_or_else(|| format!("unknown (powershell exit {wc:?}: {})", short_out(&wo))), p.distro, wsl_free.map(|g| g.to_string()).unwrap_or_else(|| format!("unknown (wsl exit {lc:?}: {})", short_out(&lo))), p.min_free_gb));
let Some(wf) = win_free else { return Err("cannot read the free space of the Windows drive".into()) };

View file

@ -154,8 +154,11 @@ fn find_tools(bin_dir: &Path) -> Result<Tools, String> {
// the SP1 host runs inside WSL2 (Ubuntu-24.04): the Linux binaries the payload ships under wsl2\bin\ (seen
// from Ubuntu as /mnt/<drive>/.../wsl2/bin), else one built there by setup-wsl.sh, else a hand install
// (the order and the list are src/wslhost.rs, shared with igneum-prove-verify.exe)
let script = format!("{}; command -v nvidia-smi >/dev/null && echo cuda", crate::wslhost::lookup_script(bin_dir));
let probe = crate::platform::quiet(&mut Command::new(crate::platform::tool("wsl"))).args(["-d", crate::wslhost::DISTRO, "--", "bash", "-lc", &script]).output();
// from a file, never inline (src/wslhost.rs: an inline script with double quotes and a path with a space
// reached bash mangled on 5 October 2026 and the app said the setup was missing)
let body = format!("{}\ncommand -v nvidia-smi >/dev/null && echo cuda\ntrue", crate::wslhost::lookup_script(bin_dir));
let file = crate::wslhost::write_script("prove-probe", &body).map_err(|e| format!("cannot write the WSL probe script: {e}"))?;
let probe = crate::platform::quiet(&mut crate::wslhost::command(&crate::platform::tool("wsl"), crate::wslhost::DISTRO, None, &file.path, true, &[])).output();
let answered = probe.is_ok();
let text = probe.map(|o| String::from_utf8_lossy(&o.stdout).to_string()).unwrap_or_default();
let host = text.lines().find(|l| l.contains("igneum-prove-host")).map(|l| PathBuf::from(l.trim()));
@ -218,19 +221,25 @@ fn read_verifier(shared: &Shared) {
/// every second and killed when the app quits, the setting goes off or `limit` passes (a proof must never outlive
/// the app). Returns (exit ok, output).
fn run_tool(shared: &Shared, t: &Tools, exe: &Path, args: &[String], env: &[(&str, &str)], limit: Duration, log: &Path) -> (bool, String) {
let mut cmd = if t.wsl {
let mut c = Command::new(crate::platform::tool("wsl"));
let envs: String = env.iter().map(|(k, v)| format!("{k}={v} ")).collect();
let line = format!("{envs}{} {}", exe.display(), args.iter().map(|a| format!("'{a}'")).collect::<Vec<_>>().join(" "));
c.args(["-d", crate::wslhost::DISTRO, "--", "bash", "-lc", &line]);
c
// Windows: a script file exports the environment and execs the host; the arguments travel as $1, $2... (the
// single-quoted rule of src/wslhost.rs). The file lives until the run ends.
let (mut cmd, _script) = if t.wsl {
let mut body: String = env.iter().map(|(k, v)| format!("export {k}={}\n", crate::wslhost::sq(v))).collect();
body.push_str(&format!("exec {} \"$@\"", crate::wslhost::sq(&exe.display().to_string())));
let file = match crate::wslhost::write_script("prove-run", &body) {
Ok(f) => f,
Err(e) => return (false, format!("cannot write the WSL run script: {e}")),
};
let argv: Vec<&str> = args.iter().map(|a| a.as_str()).collect();
let c = crate::wslhost::command(&crate::platform::tool("wsl"), crate::wslhost::DISTRO, None, &file.path, true, &argv);
(c, Some(file))
} else {
let mut c = Command::new(exe);
c.args(args);
for (k, v) in env {
c.env(k, v);
}
c
(c, None)
};
crate::platform::quiet(&mut cmd);
let Ok(file) = std::fs::File::create(log) else { return (false, format!("cannot write {}", log.display())) };
@ -512,9 +521,17 @@ pub fn setup(shared: &Shared) -> Result<Value, String> {
if !script.exists() {
return Err(format!("setup script missing: {}", script.display()));
}
let line = format!("bash {}", wsl_path(&script));
// cmd's `start` opens the window; the tail after `--` follows the single-quoted rule of src/wslhost.rs (the
// payload path has a space) and goes on the line as written
let line = format!("/c start \"\" {} -d {} -- {}", crate::platform::tool("wsl").display(), crate::wslhost::DISTRO, crate::wslhost::bash_line(&script, true, &[]));
let mut c = Command::new(crate::platform::tool("cmd"));
c.args(["/c", "start", "", &crate::platform::tool("wsl").display().to_string(), "-d", crate::wslhost::DISTRO, "--", "bash", "-lc", &line]);
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
c.raw_arg(&line);
}
#[cfg(not(windows))]
c.arg(&line);
crate::platform::quiet(&mut c); // cmd itself hidden; `start` still opens the setup's own window
c.spawn().map_err(|e| e.to_string())?;
shared.event("proving", "WSL2 prover setup started in its own window");

View file

@ -10,7 +10,9 @@
//! 3. `~/igneum-prove/target/release/igneum-prove-host`, the layout before 5 October 2026
//! 4. `/opt/igneum/igneum-prove-host`, a hand install (the devnet jobs put the CUDA host there)
use std::path::Path;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
/// The WSL distribution the host runs in.
pub const DISTRO: &str = "Ubuntu-24.04";
@ -50,10 +52,174 @@ pub fn lookup_script(bin_dir: &Path) -> String {
format!("for f in {}; do [ -x \"$f\" ] && {{ echo \"$f\"; break; }}; done", list.join(" "))
}
// ---- scripts run from a file, never inline on the command line ------------------------------------------------------
//
// 5 October 2026, PC 2 on 0.3.5: `wsl -d Ubuntu-24.04 -- bash -lc "<script>"` with `[ -x "$f" ]` and a path with a
// space ("Igneum Miner") printed nothing, so the app said "proving needs the WSL2 setup" while /opt/igneum held the
// host; the same script written to a file and run as `bash /mnt/c/.../probe.sh` found it. Rust's Command wraps the
// argument in double quotes and escapes the inner ones with backslashes; wsl.exe hands the line to the shell inside
// the distribution, which reads it differently. The rule from here: every script goes to a file (UTF-8, LF, no BOM),
// the command line after `--` is `bash [-l] '<file>' '<arg>'...` with every word single-quoted and never a double
// quote or a newline, and on Windows that tail is placed on the command line as written (CommandExt::raw_arg), so
// neither the C runtime's quoting nor the shell's re-reading can change it. Arguments reach the script as $1, $2...
/// Where the script files go: `%LOCALAPPDATA%\igneum\wsl` on Windows (`IGNEUM_APP_DATA` first, as the engine's data
/// root); a temp folder elsewhere (tests).
pub fn script_dir() -> PathBuf {
if let Some(p) = std::env::var_os("IGNEUM_APP_DATA") {
return PathBuf::from(p).join("wsl");
}
#[cfg(windows)]
{
std::env::var_os("LOCALAPPDATA")
.map(PathBuf::from)
.unwrap_or_else(|| std::env::temp_dir())
.join("igneum")
.join("wsl")
}
#[cfg(not(windows))]
{
std::env::temp_dir().join("igneum-wsl")
}
}
/// Single-quoted for a POSIX shell: `a b` becomes `'a b'`, a quote inside becomes `'\''`.
pub fn sq(s: &str) -> String {
format!("'{}'", s.replace('\'', "'\\''"))
}
/// A script file written for `bash <file>`; removed when dropped unless `keep()` was called.
pub struct ScriptFile {
pub path: PathBuf,
keep: bool,
}
impl ScriptFile {
/// Leaves the file in place (a run that outlives the engine, such as the setup window).
pub fn keep(mut self) -> PathBuf {
self.keep = true;
self.path.clone()
}
/// The file as the distribution sees it.
pub fn wsl_path(&self) -> String {
wsl_path(&self.path)
}
}
impl Drop for ScriptFile {
fn drop(&mut self) {
if !self.keep {
let _ = std::fs::remove_file(&self.path);
}
}
}
static SCRIPT_SEQ: AtomicU64 = AtomicU64::new(0);
/// Writes `body` as `<script_dir>/<stem>-<pid>-<n>.sh`: a `#!/bin/bash` first line, UTF-8, LF line endings, no BOM,
/// a final newline. The name is unique per process and call, so two runs never share a file.
pub fn write_script(stem: &str, body: &str) -> std::io::Result<ScriptFile> {
let dir = script_dir();
std::fs::create_dir_all(&dir)?;
let n = SCRIPT_SEQ.fetch_add(1, Ordering::Relaxed);
let path = dir.join(format!("{stem}-{}-{n}.sh", std::process::id()));
std::fs::write(&path, script_text(body).as_bytes())?;
Ok(ScriptFile { path, keep: false })
}
/// The bytes a script file holds: the shebang line, the body with LF endings, one final newline.
pub fn script_text(body: &str) -> String {
let body = body.replace("\r\n", "\n").replace('\r', "\n");
let body = body.trim_start_matches('\u{feff}');
format!("#!/bin/bash\n{}\n", body.trim_end_matches('\n'))
}
/// The words after `--`: `bash [-l] '<wsl path of file>' '<arg>'...`, every word single-quoted. No double quote and no
/// newline can appear, whatever the paths and arguments hold (a newline inside an argument is replaced by a space).
pub fn bash_line(file: &Path, login: bool, args: &[&str]) -> String {
let mut words = vec!["bash".to_string()];
if login {
words.push("-l".to_string());
}
words.push(sq(&wsl_path(file)));
for a in args {
words.push(sq(&a.replace(['\r', '\n'], " ")));
}
words.join(" ")
}
/// `wsl.exe -d <distro> [-u <user>] -- bash [-l] '<file>' '<arg>'...`. On Windows the tail after `--` goes on the
/// command line exactly as `bash_line` wrote it; elsewhere the words are ordinary arguments (nothing runs wsl there).
/// The caller adds stdio, the hidden-window flag and the timeout.
pub fn command(wsl_exe: &Path, distro: &str, user: Option<&str>, file: &Path, login: bool, args: &[&str]) -> Command {
let mut c = Command::new(wsl_exe);
c.args(["-d", distro]);
if let Some(u) = user.filter(|u| !u.is_empty()) {
c.args(["-u", u]);
}
c.arg("--");
let line = bash_line(file, login, args);
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
c.raw_arg(line);
}
#[cfg(not(windows))]
{
let _ = line;
c.arg("bash");
if login {
c.arg("-l");
}
c.arg(wsl_path(file));
c.args(args);
}
c
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_command_line_after_the_dashes_never_carries_a_double_quote_or_a_newline() {
let file = Path::new("C:\\Users\\the project lead\\AppData\\Local\\igneum\\wsl\\probe-12-3.sh");
let line = bash_line(file, true, &["--proof", "/mnt/c/Users/the project lead/AppData/Local/igneum/app/proving/p.bin", "--statement", "0xab", "it's", "two\nlines"]);
assert_eq!(
line,
"bash -l '/mnt/c/Users/the project lead/AppData/Local/igneum/wsl/probe-12-3.sh' '--proof' '/mnt/c/Users/the project lead/AppData/Local/igneum/app/proving/p.bin' '--statement' '0xab' 'it'\\''s' 'two lines'"
);
assert!(!line.contains('"') && !line.contains('\n'), "{line}");
// the lookup script's own double quotes live in the file, never on the line
let body = format!("{}\ncommand -v nvidia-smi >/dev/null && echo cuda", lookup_script(Path::new("C:\\Program Files\\Igneum Miner")));
assert!(body.contains('"'));
assert!(!bash_line(file, false, &[]).contains('"'));
}
#[test]
fn script_files_are_lf_utf8_without_bom_and_removed_after_use() {
let text = script_text("\u{feff}echo \"$1\"\r\nfor f in '/mnt/c/Program Files/x'; do [ -x \"$f\" ] && echo \"$f\"; done\r\n");
assert_eq!(text, "#!/bin/bash\necho \"$1\"\nfor f in '/mnt/c/Program Files/x'; do [ -x \"$f\" ] && echo \"$f\"; done\n");
assert!(!text.contains('\r') && !text.starts_with('\u{feff}'));
let f = write_script("unit", "echo \"$1\"").unwrap();
let bytes = std::fs::read(&f.path).unwrap();
assert!(bytes.starts_with(b"#!/bin/bash\necho \"$1\"\n"), "{:?}", String::from_utf8_lossy(&bytes));
assert!(!bytes.contains(&b'\r') && !bytes.starts_with(&[0xef, 0xbb, 0xbf]));
let path = f.path.clone();
drop(f);
assert!(!path.exists());
let kept = write_script("unit-keep", "true").unwrap().keep();
assert!(kept.exists());
let _ = std::fs::remove_file(kept);
}
#[test]
fn shell_quoting() {
assert_eq!(sq("a b"), "'a b'");
assert_eq!(sq("it's"), "'it'\\''s'");
assert_eq!(sq(""), "''");
}
#[test]
fn candidate_order_is_shipped_then_setup_build_then_old_layout_then_opt() {
let c = candidates(Path::new("C:\\Program Files\\Igneum Miner"));