Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/microvm-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,22 @@ jobs:
shell: pwsh
run: |
$binDir = Join-Path $env:GITHUB_WORKSPACE "src\target\x86_64-pc-windows-msvc\debug"
$required = @("wxc-exec.exe", "nanvixd.exe", "kernel.elf", "python3.12", "nanvix_rootfs.img")
$required = @(
"wxc-exec.exe",
"nanvixd.exe",
"nanvix_rootfs.img",
"python3.initrd",
"bin\kernel.elf",
"snapshots\kernel.vmem",
"snapshots\kernel.whp.cbor"
)
$missing = $required | Where-Object { -not (Test-Path (Join-Path $binDir $_)) }
if ($missing) {
Write-Host "::error::Missing binaries: $($missing -join ', ')"
exit 1
}
Get-ChildItem $binDir -Include $required -Recurse | Format-Table Name, Length
$leaves = $required | ForEach-Object { Split-Path $_ -Leaf }
Get-ChildItem $binDir -Include $leaves -Recurse | Format-Table FullName, Length

- name: Diagnose hypervisor environment
shell: pwsh
Expand Down
1 change: 1 addition & 0 deletions src/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

166 changes: 151 additions & 15 deletions src/nanvix_binaries/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@ fn main() {
}

verify_checksums(&all_binaries, &bin_dir, &checksums);
verify_bin_subdir_checksums(&bin_dir, &checksums);

// Generate host-local WHP snapshots at build time so even the first
// runtime execution uses warm start. The runtime fallback in
// nanvix_runner.rs handles the case where snapshots are missing.
let snapshots_dir = bin_dir.join(nanvix_common::SNAPSHOTS_SUBDIR);
let snapshots_present = nanvix_common::SNAPSHOT_FILES
.iter()
.all(|name| snapshots_dir.join(name).exists());
if !snapshots_present {
fs::create_dir_all(&snapshots_dir).expect("failed to create snapshots dir");
eprintln!("nanvix_binaries: generating host-local snapshots (cold boot)...");
generate_snapshots_locally(&bin_dir);
} else {
eprintln!("nanvix_binaries: host-local snapshots already present");
}

println!("cargo:rustc-env=NANVIX_BIN_DIR={}", bin_dir.display());
println!("cargo:BIN_DIR={}", bin_dir.display());
Expand All @@ -89,7 +105,8 @@ fn needs_download(
bin_dir: &Path,
checksums: &HashMap<String, String>,
) -> bool {
config.binaries.iter().any(|name| {
// Check flat binaries.
let flat_missing = config.binaries.iter().any(|name| {
let path = bin_dir.join(name);
if !path.exists() {
return true;
Expand All @@ -99,41 +116,97 @@ fn needs_download(
} else {
false
}
})
});
if flat_missing {
return true;
}

// Check bin/ subdir files.
let bin_subdir = bin_dir.join(nanvix_common::BIN_SUBDIR);
for name in nanvix_common::BIN_SUBDIR_FILES {
let path = bin_subdir.join(name);
if !path.exists() {
return true;
}
if let Some(expected) = checksums.get(*name) {
if certutil_sha256(&path) != *expected {
return true;
}
}
}

false
}

fn download_and_extract(config: &RepoConfig, repo: &str, bin_dir: &Path) {
let url = github_download_url(repo, &config.tag, &config.asset);
let zip_path = bin_dir.join(&config.asset);
let binary_paths: Vec<PathBuf> = config.binaries.iter().map(|b| bin_dir.join(b)).collect();

// Cleanup helper: remove zip + any partially extracted binaries.
// Called before panicking so the filesystem isn't left in a dangling state.
let cleanup = |bin_dir_paths: &[PathBuf], zip: &Path| {
// Cleanup helper: remove zip on failure.
let cleanup = |zip: &Path| {
let _ = fs::remove_file(zip);
for p in bin_dir_paths {
let _ = fs::remove_file(p);
}
};

eprintln!(" downloading {}...", config.asset);
if let Err(msg) = try_curl_download(&url, &zip_path) {
cleanup(&binary_paths, &zip_path);
cleanup(&zip_path);
panic!("nanvix_binaries: {}", msg);
}

let size = zip_path.metadata().map(|m| m.len()).unwrap_or(0);
eprintln!(" downloaded {} bytes, extracting...", size);

let binaries: Vec<&str> = config.binaries.iter().map(|s| s.as_str()).collect();

// Extract flat binaries (nanvixd.exe from bin/, rootfs + initrd from root).
if let Err(msg) = try_tar_extract(&zip_path, bin_dir, &binaries) {
cleanup(&binary_paths, &zip_path);
cleanup(&zip_path);
panic!("nanvix_binaries: {}", msg);
}

// Extract bin/ subdir files (kernel.elf stays in bin/ as nanvixd expects).
let bin_subdir = bin_dir.join(nanvix_common::BIN_SUBDIR);
fs::create_dir_all(&bin_subdir).expect("failed to create bin subdir");
if let Err(msg) = try_tar_extract_bin_subdir(&zip_path, &bin_subdir) {
cleanup(&zip_path);
panic!("nanvix_binaries: {}", msg);
}

let _ = fs::remove_file(&zip_path);
}

// -- Snapshot generation -----------------------------------------------------

fn generate_snapshots_locally(bin_dir: &Path) {
let nanvixd = bin_dir.join("nanvixd.exe");
let ramfs = bin_dir.join("nanvix_rootfs.img");
let initrd = bin_dir.join("python3.initrd");
let bin_subdir = bin_dir.join(nanvix_common::BIN_SUBDIR);

if !nanvixd.exists() || !ramfs.exists() || !initrd.exists() {
panic!(
"nanvix_binaries: cannot generate snapshots β€” required binaries missing:\n\
\x20 nanvixd.exe: {}\n\
\x20 nanvix_rootfs.img: {}\n\
\x20 python3.initrd: {}",
nanvixd.exists(),
ramfs.exists(),
initrd.exists()
);
}

nanvix_common::generate_snapshot(bin_dir, &nanvixd, &bin_subdir, &ramfs, &initrd)
.unwrap_or_else(|e| panic!("nanvix_binaries: {}", e));

// Log generated file sizes.
let snapshots_dir = bin_dir.join(nanvix_common::SNAPSHOTS_SUBDIR);
for name in nanvix_common::SNAPSHOT_FILES {
let path = snapshots_dir.join(name);
let size = path.metadata().map(|m| m.len()).unwrap_or(0);
eprintln!(" snapshots/{} -- generated ({} bytes)", name, size);
}
}

// -- curl.exe ----------------------------------------------------------------

fn try_curl_download(url: &str, dest: &Path) -> Result<(), String> {
Expand Down Expand Up @@ -184,12 +257,11 @@ fn try_curl_download(url: &str, dest: &Path) -> Result<(), String> {

fn try_tar_extract(zip_path: &Path, dest_dir: &Path, files: &[&str]) -> Result<(), String> {
// The nanvix-python zip has a top-level directory with two sub-layouts:
// bin/nanvixd.exe, bin/kernel.elf, bin/python3.12 β†’ strip 2 components
// nanvix_rootfs.img β†’ strip 1 component
// We run two passes to handle both depths.
// bin/nanvixd.exe β†’ strip 2 components
// nanvix_rootfs.img, python3.initrd β†’ strip 1 component

const ARCHIVE_PREFIX: &str = "microvm-standalone-256mb";
const BIN_DIR_FILES: &[&str] = &["nanvixd.exe", "kernel.elf", "python3.12"];
const BIN_DIR_FILES: &[&str] = &["nanvixd.exe"];

let (bin_files, root_files): (Vec<&&str>, Vec<&&str>) =
files.iter().partition(|f| BIN_DIR_FILES.contains(f));
Expand Down Expand Up @@ -255,6 +327,37 @@ fn try_tar_extract(zip_path: &Path, dest_dir: &Path, files: &[&str]) -> Result<(
Ok(())
}

fn try_tar_extract_bin_subdir(zip_path: &Path, dest_dir: &Path) -> Result<(), String> {
const ARCHIVE_PREFIX: &str = "microvm-standalone-256mb";

for name in nanvix_common::BIN_SUBDIR_FILES {
let mut cmd = Command::new("tar");
cmd.arg("-xf").arg(zip_path).arg("-C").arg(dest_dir);
cmd.args(["--strip-components", "2"]);
cmd.arg(format!("{}/bin/{}", ARCHIVE_PREFIX, name));
let output = cmd
.output()
.map_err(|e| format!("tar.exe not found: {}", e))?;
if !output.status.success() {
return Err(format!(
"tar extraction failed (bin/{})\n exit code: {}\n stderr: {}",
name,
output.status,
String::from_utf8_lossy(&output.stderr)
));
}
let path = dest_dir.join(name);
if path.exists() {
let size = path.metadata().map(|m| m.len()).unwrap_or(0);
eprintln!(" bin/{} -- extracted ({} bytes)", name, size);
} else {
return Err(format!("'bin/{}' not found in zip after extraction", name));
}
}

Ok(())
}

// -- Helpers -----------------------------------------------------------------

fn github_token() -> Option<String> {
Expand Down Expand Up @@ -326,3 +429,36 @@ fn verify_checksums(binaries: &[&str], bin_dir: &Path, checksums: &HashMap<Strin
}
}
}

fn verify_bin_subdir_checksums(bin_dir: &Path, checksums: &HashMap<String, String>) {
let bin_subdir = bin_dir.join(nanvix_common::BIN_SUBDIR);
for name in nanvix_common::BIN_SUBDIR_FILES {
let path = bin_subdir.join(name);
if !path.exists() {
panic!(
"nanvix_binaries: bin/{} not found after download/extract",
name
);
}

if let Some(expected) = checksums.get(*name) {
let actual = certutil_sha256(&path);
if actual != *expected {
panic!(
"nanvix_binaries: SHA256 mismatch for 'bin/{}'!\n\
\x20 expected: {}\n\
\x20 actual: {}\n\
Update checksums.json with the new hashes.",
name, expected, actual
);
}
eprintln!(" bin/{} -- checksum OK", name);
} else {
panic!(
"nanvix_binaries: 'bin/{}' has no entry in checksums.json β€” \
every binary must be hash-verified",
name
);
}
}
}
8 changes: 4 additions & 4 deletions src/nanvix_binaries/checksums.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"nanvixd.exe": "375f21ea2cf148c0fc761779237c88921742890979b3389c6aa45409a4dd9c0c",
"kernel.elf": "034ce96131b727d65f728b98e617257ebb514d3318b599ab139ce7901c7cd4ba",
"python3.12": "46516dd7437c4e63470c3889bfb61cc3c195c792d697d5e137697620abe2af15",
"nanvix_rootfs.img": "7015e5280d1490958d13f8eff7ebe8ff6cf9e197167d23518985284a1d143309"
"nanvixd.exe": "9172542c250c7cc3eb1bba00fb027543f2a309e5b0152f5f9dee9e6484ed21ec",
"nanvix_rootfs.img": "0d45a1f0a29e0d6fc9f297016c02a798738d5288acacb46e770eb9103e1ca0b9",
"python3.initrd": "9e20583c70b2b6feb43ccbd477657d78f5cd1a061e962bacb7b13079fbdede92",
"kernel.elf": "4e75f32192b63c034846f7f802e3e3cfe7d6df57b3a0c8d6bd5927fc4ead0b2d"
}
8 changes: 6 additions & 2 deletions src/nanvix_binaries/versions.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
{
"nanvix_python": {
"tag": "3.12.3-nanvix-0.12.529-ee6cfe1",
"tag": "3.12.3-nanvix-0.15.4-ac76d40",
"asset": "microvm-standalone-256mb.zip",
"binaries": ["nanvixd.exe", "kernel.elf", "python3.12", "nanvix_rootfs.img"]
"binaries": [
"nanvixd.exe",
"nanvix_rootfs.img",
"python3.initrd"
]
}
}
Loading
Loading