#![allow(dead_code)]
use std::path::{Path, PathBuf};
use std::process::Command;
pub fn repository_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.canonicalize()
.expect("the repository root must exist")
}
pub fn posix(path: &Path) -> String {
path.to_string_lossy().replace('\\', "/")
}
fn find_on_path(program: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
std::env::split_paths(&path)
.map(|directory| directory.join(program))
.find(|candidate| candidate.is_file())
}
pub fn bash_program() -> PathBuf {
if let Some(explicit) = std::env::var_os("RUNNER_MANAGER_BASH") {
return PathBuf::from(explicit);
}
if !cfg!(windows) {
return PathBuf::from("bash");
}
let mut tried: Vec<PathBuf> = Vec::new();
if let Some(git) = find_on_path("git.exe")
&& let Some(root) = git.parent().and_then(Path::parent)
{
let candidate = root.join("bin").join("bash.exe");
if candidate.is_file() {
return candidate;
}
tried.push(candidate);
}
let standard = PathBuf::from(r"C:\Program Files\Git\bin\bash.exe");
if standard.is_file() {
return standard;
}
tried.push(standard);
panic!(
"no usable bash found on Windows. Tried, in order: {tried:?}. `bash` on \
PATH is deliberately NOT a fallback: there it resolves to the WSL \
launcher, a different program that fails outright when no distribution \
is installed. Install Git for Windows, or set RUNNER_MANAGER_BASH."
);
}
pub fn run_bash(script: &Path, arguments: &[&str], envs: &[(&str, &str)]) -> (bool, String) {
let mut command = Command::new(bash_program());
command.arg(posix(script));
command.args(arguments);
command.current_dir(repository_root());
for (key, value) in envs {
command.env(key, value);
}
let output = command
.output()
.unwrap_or_else(|err| panic!("cannot run {}: {err}", posix(script)));
(
output.status.success(),
format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
),
)
}
pub fn release_script() -> PathBuf {
let path = repository_root()
.join(".github")
.join("scripts")
.join("release.sh");
assert!(path.is_file(), "{} must exist", path.display());
path
}
pub fn channels_script() -> PathBuf {
let path = repository_root()
.join(".github")
.join("scripts")
.join("channels.sh");
assert!(
path.is_file(),
"{} must exist: it is where step 8's decisions live",
path.display()
);
path
}
pub fn install_script(name: &str) -> PathBuf {
let path = repository_root().join("install").join(name);
assert!(
path.is_file(),
"{} must exist: it is a published release asset",
path.display()
);
path
}
pub fn checksum_line(file: &Path) -> String {
let (ok, output) = run_bash(&release_script(), &["sha256", &posix(file)], &[]);
assert!(
ok,
"release.sh sha256 failed for {}:\n{output}",
file.display()
);
output.trim().to_string()
}
pub const TARGETS: [(&str, &str, &str); 5] = [
("x86_64-pc-windows-msvc", "zip", "runner-manager.exe"),
("aarch64-apple-darwin", "tar.gz", "runner-manager"),
("x86_64-apple-darwin", "tar.gz", "runner-manager"),
("x86_64-unknown-linux-gnu", "tar.gz", "runner-manager"),
("aarch64-unknown-linux-gnu", "tar.gz", "runner-manager"),
];
pub struct FixtureRelease {
pub root: PathBuf,
pub assets: PathBuf,
pub version: String,
}
impl FixtureRelease {
pub fn sums(&self) -> PathBuf {
self.assets.join("SHA256SUMS")
}
pub fn staged(&self, target: &str) -> PathBuf {
self.root
.join("stage")
.join(format!("runner-manager-{}-{target}", self.version))
}
pub fn archive(&self, target: &str) -> PathBuf {
let (_, extension, _) = TARGETS
.iter()
.find(|(name, _, _)| *name == target)
.unwrap_or_else(|| panic!("unknown target {target}"));
self.assets.join(format!(
"runner-manager-{}-{target}.{extension}",
self.version
))
}
pub fn expected_output(&self, target: &str) -> String {
format!("runner-manager {} ({target})", self.version)
}
}
pub fn build_release(root: &Path, version: &str) -> FixtureRelease {
let stage = root.join("stage");
let assets = root.join("assets");
std::fs::create_dir_all(&stage).expect("fixture stage");
std::fs::create_dir_all(&assets).expect("fixture assets");
let mut sums = String::new();
for (target, extension, binary) in TARGETS {
let stem = format!("runner-manager-{version}-{target}");
let payload = stage.join(&stem);
std::fs::create_dir_all(&payload).expect("fixture payload directory");
let body = format!("#!/bin/sh\necho \"runner-manager {version} ({target})\"\n");
std::fs::write(payload.join(binary), body.as_bytes()).expect("fixture binary");
std::fs::write(payload.join("LICENSE"), b"MIT\n").expect("fixture licence");
let archive = assets.join(format!("{stem}.{extension}"));
pack(&stage, &stem, binary, extension, &archive, body.as_bytes());
sums.push_str(&checksum_line(&archive));
sums.push('\n');
}
let mut lines: Vec<&str> = sums.lines().collect();
lines.sort_by_key(|line| line.split_once(" ").map(|(_, name)| name).unwrap_or(line));
std::fs::write(assets.join("SHA256SUMS"), format!("{}\n", lines.join("\n")))
.expect("fixture SHA256SUMS");
FixtureRelease {
root: root.to_path_buf(),
assets,
version: version.to_string(),
}
}
fn run_tar(stage: &Path, stem: &str, archive: &Path) -> (bool, String) {
let name = archive
.file_name()
.expect("an archive file name")
.to_string_lossy();
let mut command = Command::new(bash_program());
command.arg("-c");
command.arg(format!(
"cd '{}' && tar -czf '../assets/{}' '{}'",
posix(stage),
name,
stem
));
let output = command.output().expect("cannot run tar through bash");
(
output.status.success(),
format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
),
)
}
fn crc32(data: &[u8]) -> u32 {
let mut table = [0u32; 256];
for (index, slot) in table.iter_mut().enumerate() {
let mut value = index as u32;
for _ in 0..8 {
value = if value & 1 != 0 {
0xEDB8_8320 ^ (value >> 1)
} else {
value >> 1
};
}
*slot = value;
}
let mut crc = 0xFFFF_FFFFu32;
for byte in data {
crc = table[((crc ^ u32::from(*byte)) & 0xFF) as usize] ^ (crc >> 8);
}
crc ^ 0xFFFF_FFFF
}
pub fn write_stored_zip(path: &Path, entries: &[(String, &[u8])]) {
let mut out: Vec<u8> = Vec::new();
let mut central: Vec<u8> = Vec::new();
const DOS_TIME: u16 = 0;
const DOS_DATE: u16 = 0x0021;
for (name, data) in entries {
let offset = out.len() as u32;
let crc = crc32(data);
let size = data.len() as u32;
let name_bytes = name.as_bytes();
out.extend_from_slice(&0x0403_4b50u32.to_le_bytes()); out.extend_from_slice(&20u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&DOS_TIME.to_le_bytes());
out.extend_from_slice(&DOS_DATE.to_le_bytes());
out.extend_from_slice(&crc.to_le_bytes());
out.extend_from_slice(&size.to_le_bytes()); out.extend_from_slice(&size.to_le_bytes()); out.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(name_bytes);
out.extend_from_slice(data);
central.extend_from_slice(&0x0201_4b50u32.to_le_bytes()); central.extend_from_slice(&0x031Eu16.to_le_bytes()); central.extend_from_slice(&20u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&DOS_TIME.to_le_bytes());
central.extend_from_slice(&DOS_DATE.to_le_bytes());
central.extend_from_slice(&crc.to_le_bytes());
central.extend_from_slice(&size.to_le_bytes());
central.extend_from_slice(&size.to_le_bytes());
central.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); let external = if name.ends_with('/') {
0x41ED_0010u32
} else {
0x81ED_0000u32
};
central.extend_from_slice(&external.to_le_bytes());
central.extend_from_slice(&offset.to_le_bytes());
central.extend_from_slice(name_bytes);
}
let central_offset = out.len() as u32;
let central_size = central.len() as u32;
out.extend_from_slice(¢ral);
out.extend_from_slice(&0x0605_4b50u32.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&(entries.len() as u16).to_le_bytes());
out.extend_from_slice(&(entries.len() as u16).to_le_bytes());
out.extend_from_slice(¢ral_size.to_le_bytes());
out.extend_from_slice(¢ral_offset.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("zip parent directory");
}
std::fs::write(path, &out)
.unwrap_or_else(|err| panic!("cannot write {}: {err}", path.display()));
}
fn pack(stage: &Path, stem: &str, binary: &str, extension: &str, archive: &Path, body: &[u8]) {
match extension {
"tar.gz" => {
let (ok, output) = run_tar(stage, stem, archive);
assert!(ok, "packing {stem}.{extension} failed:\n{output}");
}
"zip" => {
let licence = std::fs::read(stage.join(stem).join("LICENSE"))
.expect("the staged LICENSE that the tar path already packs");
write_stored_zip(
archive,
&[
(format!("{stem}/"), &[][..]),
(format!("{stem}/{binary}"), body),
(format!("{stem}/LICENSE"), licence.as_slice()),
],
);
}
other => panic!("unknown archive extension {other}"),
}
}
pub fn substitute_payload(release: &FixtureRelease, target: &str) {
let (_, extension, binary) = TARGETS
.iter()
.find(|(name, _, _)| *name == target)
.unwrap_or_else(|| panic!("unknown target {target}"));
let stem = format!("runner-manager-{}-{target}", release.version);
let body = format!("#!/bin/sh\necho \"substituted payload for {target}\"\n");
let staged = release.staged(target);
std::fs::write(staged.join(binary), body.as_bytes()).expect("the substituted binary");
let archive = release.archive(target);
std::fs::remove_file(&archive).expect("removing the original archive");
pack(
&release.root.join("stage"),
&stem,
binary,
extension,
&archive,
body.as_bytes(),
);
assert!(
archive.is_file(),
"the substituted archive was not written for {target}"
);
}