feat: introduced nix packaging and path-based binary resolution
- Add comprehensive Nix flake definitions, derivations, modules, and CI workflows. - Update tests and executables to resolve binaries from PATH rather than absolute paths. - Ensure byte reproducibility and zeroed timestamps in embedded dashboard archives. - Add handling for Nix-managed installations in CLI update checks.
This commit is contained in:
@@ -1991,12 +1991,30 @@ mod tests {
|
||||
.expect("child did not enter expected executable");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn test_executable(name: &str) -> std::path::PathBuf {
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
|
||||
std::env::var_os("PATH")
|
||||
.and_then(|path| {
|
||||
std::env::split_paths(&path)
|
||||
.map(|directory| directory.join(name))
|
||||
.find(|candidate| {
|
||||
candidate.metadata().is_ok_and(|metadata| {
|
||||
metadata.is_file() && metadata.permissions().mode() & 0o111 != 0
|
||||
})
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| panic!("{name} executable on PATH"))
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn process_test_command(prefix: &str) -> (tempfile::TempDir, std::path::PathBuf, String) {
|
||||
let dir = tempfile::tempdir().expect("process test directory");
|
||||
let name = format!("{prefix}{}", std::process::id());
|
||||
let command = dir.path().join(&name);
|
||||
std::os::unix::fs::symlink("/bin/sleep", &command).expect("sleep symlink");
|
||||
let sleep = test_executable("sleep");
|
||||
std::os::unix::fs::symlink(sleep, &command).expect("sleep symlink");
|
||||
(dir, command, name)
|
||||
}
|
||||
|
||||
@@ -2881,13 +2899,14 @@ mod tests {
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn kill_builtin_refuses_ancestors_but_not_unrelated_processes() {
|
||||
let (result, output) = execute_captured(
|
||||
let sleep = test_executable("sleep");
|
||||
let command = format!(
|
||||
"parent=$(ps -o ppid= -p $$ | tr -d ' ')\nkill -CONT \"$parent\"; printf \
|
||||
'ancestor=%s\\n' \"$?\"\n/bin/sleep 30 &\nchild=$!\nkill -TERM \"$child\"; printf \
|
||||
'child=%s\\n' \"$?\"\nprintf 'survived\\n'"
|
||||
.to_string(),
|
||||
)
|
||||
.await;
|
||||
'ancestor=%s\\n' \"$?\"\n{} 30 &\nchild=$!\nkill -TERM \"$child\"; printf 'child=%s\\n' \
|
||||
\"$?\"\nprintf 'survived\\n'",
|
||||
quote_arg(sleep.to_str().expect("utf8 sleep path"))
|
||||
);
|
||||
let (result, output) = execute_captured(command).await;
|
||||
assert_eq!(result.exit_code, Some(0), "the shell must survive: {output:?}");
|
||||
assert!(output.contains("survived"), "{output:?}");
|
||||
assert!(
|
||||
@@ -3024,11 +3043,14 @@ mod tests {
|
||||
// An identity "compressor" that also proves it was started with the
|
||||
// shell's working directory and reaches the command's stderr.
|
||||
let shim = bin.join("pi-test-compress");
|
||||
std::fs::write(
|
||||
&shim,
|
||||
"#!/bin/sh\nprintf 'compressor cwd=%s\\n' \"$PWD\" >&2\nexec /bin/cat\n",
|
||||
)
|
||||
.expect("write shim");
|
||||
let shell = test_executable("sh");
|
||||
let cat = test_executable("cat");
|
||||
let shim_source = format!(
|
||||
"#!{}\nprintf 'compressor cwd=%s\\n' \"$PWD\" >&2\nexec {}\n",
|
||||
shell.display(),
|
||||
quote_arg(cat.to_str().expect("utf8 cat path"))
|
||||
);
|
||||
std::fs::write(&shim, shim_source).expect("write shim");
|
||||
std::fs::set_permissions(&shim, std::fs::Permissions::from_mode(0o755)).expect("chmod shim");
|
||||
|
||||
// Enough distinct lines that the 1K buffer forces spilling through the
|
||||
@@ -4476,9 +4498,8 @@ replace = [{ pattern = "hello", replacement = "HI" }]
|
||||
/// external background jobs and 1 while one is running. The host relies on
|
||||
/// this to retain a per-call shell whose `&`/`nohup` child is still alive
|
||||
/// instead of dropping it (which would SIGKILL the child via kill-on-drop).
|
||||
/// Path-qualified `/bin/sleep` is used so it spawns a real external process
|
||||
/// (the bare `sleep` builtin runs in-process and is intentionally not
|
||||
/// counted).
|
||||
/// `sh -c` forces an external process because the bare `sleep` builtin runs
|
||||
/// in-process and is intentionally not counted.
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn live_background_job_count_tracks_external_background_jobs() {
|
||||
@@ -4502,7 +4523,7 @@ replace = [{ pattern = "hello", replacement = "HI" }]
|
||||
// An external background process is tracked while it runs.
|
||||
shell
|
||||
.run(
|
||||
ShellRunOptions { command: "/bin/sleep 30 &".into(), ..Default::default() },
|
||||
ShellRunOptions { command: "sh -c 'sleep 30' &".into(), ..Default::default() },
|
||||
None,
|
||||
CancelToken::default(),
|
||||
)
|
||||
@@ -4640,7 +4661,7 @@ replace = [{ pattern = "^.+$", replacement = "PWD" }]
|
||||
let root = unique_temp_dir("heredoc-chain");
|
||||
let minimizer = printf_minimizer(&root.join("minimizer.toml"), None);
|
||||
let (result, output) = run_command_capture(
|
||||
"/bin/cat <<'PY'\nhello $USER\nPY\nprintf 'after\\n'",
|
||||
"cat <<'PY'\nhello $USER\nPY\nprintf 'after\\n'",
|
||||
None,
|
||||
Some(minimizer),
|
||||
CancelToken::default(),
|
||||
@@ -4845,7 +4866,7 @@ replace = [{ pattern = "^.+$", replacement = "PWD" }]
|
||||
// `printf '%d\n' "$$"` then `sleep 0.5`. Long enough for our `getsid`.
|
||||
let exec = session
|
||||
.shell
|
||||
.run_string("/bin/sh -c 'printf \"%d\\n\" \"$$\"; sleep 0.5'", &source_info, ¶ms)
|
||||
.run_string("sh -c 'printf \"%d\\n\" \"$$\"; sleep 0.5'", &source_info, ¶ms)
|
||||
.await
|
||||
.expect("run_string");
|
||||
drop(params);
|
||||
@@ -4910,7 +4931,7 @@ replace = [{ pattern = "^.+$", replacement = "PWD" }]
|
||||
shell_b
|
||||
.run(
|
||||
ShellRunOptions {
|
||||
command: "/bin/sh -c 'printf \"ready\\n\"; sleep 30'".into(),
|
||||
command: "sh -c 'printf \"ready\\n\"; sleep 30'".into(),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tx_b),
|
||||
@@ -4942,7 +4963,7 @@ replace = [{ pattern = "^.+$", replacement = "PWD" }]
|
||||
shell_a
|
||||
.run(
|
||||
ShellRunOptions {
|
||||
command: "/bin/sh -c 'printf \"%d\\n\" \"$$\"; sleep 2'".into(),
|
||||
command: "sh -c 'printf \"%d\\n\" \"$$\"; sleep 2'".into(),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tx_a),
|
||||
@@ -5003,9 +5024,7 @@ replace = [{ pattern = "^.+$", replacement = "PWD" }]
|
||||
let escaped_pid_path = pid_path.to_string_lossy().replace('\'', "'\\''");
|
||||
std::fs::write(
|
||||
&snapshot_path,
|
||||
format!(
|
||||
"/bin/sh -c 'printf \"%d\\n\" \"$$\" > \"$1\"; sleep 30' sh '{escaped_pid_path}'\n"
|
||||
),
|
||||
format!("sh -c 'printf \"%d\\n\" \"$$\" > \"$1\"; sleep 30' sh '{escaped_pid_path}'\n"),
|
||||
)
|
||||
.expect("write snapshot file");
|
||||
|
||||
@@ -5058,7 +5077,7 @@ replace = [{ pattern = "^.+$", replacement = "PWD" }]
|
||||
|
||||
let child_dead = time::timeout(Duration::from_secs(5), async {
|
||||
loop {
|
||||
// SAFETY: `child_pid` came from the foreground `/bin/sh` spawned by the
|
||||
// SAFETY: `child_pid` came from the foreground `sh` spawned by the
|
||||
// snapshot; `kill(pid, 0)` only probes whether that process still exists.
|
||||
let kill_result = unsafe { libc::kill(child_pid, 0) };
|
||||
if kill_result == -1 {
|
||||
@@ -5148,14 +5167,14 @@ replace = [{ pattern = "^.+$", replacement = "PWD" }]
|
||||
|
||||
let shell_handle = tokio::spawn(async move {
|
||||
let source_info = SourceInfo::from("pi-natives:test");
|
||||
// First stage prints its own PID and sleeps; `cat` forwards the PID
|
||||
// line to our reader and exits on EOF. The first stage leads the
|
||||
// pipeline's process group, the second (`cat`) is the join-or-detach
|
||||
// stage that would EPERM without the wiring fix.
|
||||
// First stage prints its own PID and sleeps; `sh -c cat` forwards
|
||||
// the PID line to our reader and exits on EOF. The first stage
|
||||
// leads the pipeline's process group, while the second stage is the
|
||||
// join-or-detach process that would EPERM without the wiring fix.
|
||||
let exec = session
|
||||
.shell
|
||||
.run_string(
|
||||
"/bin/sh -c 'printf \"%d\\n\" \"$$\"; sleep 1' | /bin/cat",
|
||||
"sh -c 'printf \"%d\\n\" \"$$\"; sleep 1' | sh -c cat",
|
||||
&source_info,
|
||||
¶ms,
|
||||
)
|
||||
@@ -5210,7 +5229,7 @@ replace = [{ pattern = "^.+$", replacement = "PWD" }]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn wait_accepts_last_background_process_id() {
|
||||
let options = ShellExecuteOptions {
|
||||
command: "/bin/sh -c 'exit 7' & mover=$!; wait \"$mover\"".to_string(),
|
||||
command: "sh -c 'exit 7' & mover=$!; wait \"$mover\"".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -5227,9 +5246,9 @@ replace = [{ pattern = "^.+$", replacement = "PWD" }]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn wait_n_p_records_completed_process_id() {
|
||||
let options = ShellExecuteOptions {
|
||||
command: "/bin/sh -c 'sleep 0.2; exit 42' & slow=$!; /bin/sh -c 'exit 13' & fast=$!; \
|
||||
wait -n -p hit \"$slow\" \"$fast\"; status=$?; wait \"$slow\"; [ \"$status\" \
|
||||
-eq 13 ] && [ \"$hit\" = \"$fast\" ]"
|
||||
command: "sh -c 'sleep 0.2; exit 42' & slow=$!; sh -c 'exit 13' & fast=$!; wait -n -p \
|
||||
hit \"$slow\" \"$fast\"; status=$?; wait \"$slow\"; [ \"$status\" -eq 13 ] && \
|
||||
[ \"$hit\" = \"$fast\" ]"
|
||||
.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -5247,7 +5266,7 @@ replace = [{ pattern = "^.+$", replacement = "PWD" }]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn wait_f_accepts_process_id() {
|
||||
let options = ShellExecuteOptions {
|
||||
command: "/bin/sh -c 'exit 5' & child=$!; wait -f \"$child\"".to_string(),
|
||||
command: "sh -c 'exit 5' & child=$!; wait -f \"$child\"".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -5390,13 +5409,9 @@ replace = [{ pattern = "^.+$", replacement = "PWD" }]
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn quoted_heredoc_without_trailing_newline_runs() {
|
||||
let (result, output) = run_command_capture(
|
||||
"/bin/cat <<'PY'\nhello $USER\nPY",
|
||||
None,
|
||||
None,
|
||||
CancelToken::default(),
|
||||
)
|
||||
.await;
|
||||
let (result, output) =
|
||||
run_command_capture("cat <<'PY'\nhello $USER\nPY", None, None, CancelToken::default())
|
||||
.await;
|
||||
|
||||
assert_eq!(result.exit_code, Some(0));
|
||||
assert_eq!(output, "hello $USER\n");
|
||||
@@ -5437,7 +5452,7 @@ replace = [{ pattern = "^.+$", replacement = "PWD" }]
|
||||
let command = if cfg!(windows) {
|
||||
"nohup cmd /C exit 7"
|
||||
} else {
|
||||
"nohup /bin/sh -c 'exit 7'"
|
||||
"nohup sh -c 'exit 7'"
|
||||
};
|
||||
let options = ShellExecuteOptions { command: command.to_string(), ..Default::default() };
|
||||
let result = execute_shell(options, None, CancelToken::default())
|
||||
@@ -5455,8 +5470,8 @@ replace = [{ pattern = "^.+$", replacement = "PWD" }]
|
||||
async fn nohup_background_captures_operand_pid() {
|
||||
let (tx, rx) = flume::unbounded::<String>();
|
||||
let options = ShellExecuteOptions {
|
||||
command: "nohup /bin/sh -c 'exit 0' >/dev/null 2>&1 & pid=$!; printf 'pid=%s\n' \
|
||||
\"$pid\"; test -n \"$pid\""
|
||||
command: "nohup sh -c 'exit 0' >/dev/null 2>&1 & pid=$!; printf 'pid=%s\n' \"$pid\"; \
|
||||
test -n \"$pid\""
|
||||
.to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user