use std::{
fs, io,
path::{Path, PathBuf},
process::{Command, Stdio},
sync::{Mutex, OnceLock},
};
pub fn output(command: &mut Command) -> io::Result<std::process::Output> {
command.output()
}
pub fn python_program() -> &'static str {
"python3"
}
const FIXTURE_PYTHON: &str = "ARTIFACTIZE_TEST_PYTHON";
fn fixture_python() -> &'static Path {
static PYTHON: OnceLock<PathBuf> = OnceLock::new();
PYTHON.get_or_init(|| {
if let Some(program) = std::env::var_os(FIXTURE_PYTHON) {
return PathBuf::from(program);
}
let output = Command::new(python_program())
.args(["-I", "-c", "import sys; print(sys.executable)"])
.stdin(Stdio::null())
.output()
.expect("resolve the fixture infrastructure's Python before changing its environment");
assert!(output.status.success(), "fixture Python: {output:?}");
let program = PathBuf::from(
String::from_utf8(output.stdout)
.expect("fixture Python path is Unicode")
.trim_end(),
);
assert!(program.is_absolute(), "fixture Python path is absolute");
program
})
}
pub(super) fn prepare_probe(command: &mut Command) {
command.env(FIXTURE_PYTHON, fixture_python());
}
pub fn path_text(path: &Path) -> String {
let text = path.to_string_lossy();
#[cfg(windows)]
{
text.replace('\\', "/")
}
#[cfg(not(windows))]
{
text.into_owned()
}
}
#[derive(serde::Deserialize)]
#[serde(tag = "reason")]
enum CargoMessage {
#[serde(rename = "compiler-artifact")]
Artifact {
target: CargoTarget,
executable: Option<PathBuf>,
},
#[serde(other)]
Other,
}
#[derive(serde::Deserialize)]
struct CargoTarget {
name: String,
kind: Vec<String>,
}
pub struct Executable {
path: PathBuf,
_directory: tempfile::TempDir,
}
impl Executable {
pub fn copy_from(source: &Path) -> Result<Self, String> {
let root =
option_env!("CARGO_TARGET_TMPDIR").map_or_else(std::env::temp_dir, PathBuf::from);
fs::create_dir_all(&root).map_err(|error| error.to_string())?;
let directory = tempfile::Builder::new()
.prefix("artifactize-worker-")
.tempdir_in(root)
.map_err(|error| error.to_string())?;
let path = directory.path().join(
source
.file_name()
.ok_or_else(|| "Fixture executable has no filename".to_owned())?,
);
let copied = Command::new(fixture_python())
.args([
"-I",
"-c",
"import shutil, sys; shutil.copy(sys.argv[1], sys.argv[2])",
])
.arg(source)
.arg(&path)
.stdin(Stdio::null())
.output()
.map_err(|error| format!("Starting the fixture executable copier: {error}"))?;
if !copied.status.success() {
return Err(format!(
"Fixture executable copy failed ({}): {}",
copied.status,
String::from_utf8_lossy(&copied.stderr)
));
}
Ok(Self {
path,
_directory: directory,
})
}
pub fn path(&self) -> &Path {
&self.path
}
}
static OWNED: Mutex<Option<Executable>> = Mutex::new(None);
#[cfg(unix)]
extern "C" fn cleanup() {
let executable = OWNED
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
drop(executable);
}
#[cfg(windows)]
pub fn cleanup_after_exit(directory: &Path) -> Result<(), String> {
use std::io::BufRead;
use windows_sys::Win32::{
Foundation::FILETIME,
System::Threading::{GetCurrentProcess, GetProcessTimes},
};
let mut creation = FILETIME::default();
let (mut exit, mut kernel, mut user) = (creation, creation, creation);
if unsafe {
GetProcessTimes(
GetCurrentProcess(),
&mut creation,
&mut exit,
&mut kernel,
&mut user,
)
} == 0
{
return Err(format!(
"Reading fixture owner identity: {}",
io::Error::last_os_error()
));
}
let birth = (u64::from(creation.dwHighDateTime) << 32) | u64::from(creation.dwLowDateTime);
const CLEANUP: &str = r#"
import ctypes
from ctypes import wintypes
import shutil
import sys
kernel = ctypes.WinDLL('kernel32', use_last_error=True)
open_process = kernel.OpenProcess
open_process.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
open_process.restype = wintypes.HANDLE
wait = kernel.WaitForSingleObject
wait.argtypes = [wintypes.HANDLE, wintypes.DWORD]
wait.restype = wintypes.DWORD
close = kernel.CloseHandle
close.argtypes = [wintypes.HANDLE]
close.restype = wintypes.BOOL
times = kernel.GetProcessTimes
times.argtypes = [wintypes.HANDLE] + [ctypes.POINTER(wintypes.FILETIME)] * 4
times.restype = wintypes.BOOL
SYNCHRONIZE = 0x00100000
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
INFINITE = 0xffffffff
WAIT_OBJECT_0 = 0
try:
parent = open_process(SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, False, int(sys.argv[1]))
if not parent:
raise ctypes.WinError(ctypes.get_last_error())
try:
creation, exit, kernel_time, user = [wintypes.FILETIME() for _ in range(4)]
if not times(parent, ctypes.byref(creation), ctypes.byref(exit), ctypes.byref(kernel_time), ctypes.byref(user)):
raise ctypes.WinError(ctypes.get_last_error())
birth = (creation.dwHighDateTime << 32) | creation.dwLowDateTime
if birth != int(sys.argv[2]):
raise RuntimeError('Fixture owner process identity changed before registration')
# Publish readiness only after pinning the exact parent identity. Its native
# handle cannot be retargeted by PID reuse, including after an abrupt exit.
print('READY', flush=True)
if wait(parent, INFINITE) != WAIT_OBJECT_0:
raise ctypes.WinError(ctypes.get_last_error())
finally:
close(parent)
try:
shutil.rmtree(sys.argv[3])
except FileNotFoundError as error:
# Initialization can unwind after registration but before publication; its
# local TempDir then removes this exact root, not a missing file inside it.
if error.filename != sys.argv[3]:
raise
except Exception:
print('Fixture exit cleanup failed; retained root: ' + sys.argv[3], file=sys.stderr)
raise
"#;
let directory = fs::canonicalize(directory).map_err(|error| error.to_string())?;
let mut child = Command::new(fixture_python())
.args(["-I", "-c", CLEANUP])
.arg(std::process::id().to_string())
.arg(birth.to_string())
.arg(&directory)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.map_err(|error| format!("Starting fixture exit cleanup: {error}"))?;
let mut ready = String::new();
let read = std::io::BufReader::new(child.stdout.take().expect("cleanup readiness pipe"))
.read_line(&mut ready);
if read.is_err() || ready.trim_end() != "READY" {
let _ = child.kill();
let status = child.wait();
return Err(format!(
"Fixture exit cleanup registration failed for {}: {read:?}, {ready:?}, {status:?}",
path_text(&directory)
));
}
Ok(())
}
pub fn executable() -> Result<&'static Path, String> {
static BUILT: OnceLock<Result<PathBuf, String>> = OnceLock::new();
BUILT
.get_or_init(|| {
let candidate = Executable::copy_from(&source_executable()?)?;
#[cfg(unix)]
let registered = unsafe { libc::atexit(cleanup) };
#[cfg(unix)]
if registered != 0 {
return Err("Fixture executable exit cleanup could not be registered".into());
}
#[cfg(windows)]
cleanup_after_exit(candidate.path().parent().expect("owned executable root"))?;
let path = candidate.path().to_owned();
*OWNED.lock().unwrap() = Some(candidate);
Ok(path)
})
.as_deref()
.map_err(Clone::clone)
}
fn source_executable() -> Result<PathBuf, String> {
if let Some(path) = option_env!("CARGO_BIN_EXE_artifactize") {
return Ok(PathBuf::from(path));
}
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("workspace crate parent")
.join("artifactize")
.join("Cargo.toml");
let built = Command::new(env!("CARGO"))
.args([
"build",
"--quiet",
"--locked",
"--bin",
"artifactize",
"--message-format=json-render-diagnostics",
])
.arg("--manifest-path")
.arg(manifest)
.stdin(Stdio::null())
.stderr(Stdio::inherit())
.output()
.map_err(|error| error.to_string())?;
if !built.status.success() {
return Err(format!("Fixture CLI build failed: {}", built.status));
}
executable_from_receipt(&built.stdout, "artifactize")
}
pub fn executable_from_receipt(receipt: &[u8], binary: &str) -> Result<PathBuf, String> {
for line in receipt
.split(|byte| *byte == b'\n')
.filter(|line| !line.is_empty())
{
if let CargoMessage::Artifact {
target,
executable: Some(path),
} = serde_json::from_slice(line).map_err(|error| error.to_string())?
&& target.name == binary
&& target.kind.iter().any(|kind| kind == "bin")
{
return Ok(path);
}
}
Err(format!(
"Cargo did not report the {binary} fixture executable"
))
}