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
155 changes: 155 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

env:
CARGO_TERM_COLOR: always

jobs:
rust:
name: Rust Build & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
sudo chmod 666 /dev/kvm

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt, clippy

- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

- name: Check formatting
run: cargo fmt --all -- --check

- name: Clippy
run: cargo clippy --all-targets --all-features -- -D warnings

- name: Build
run: cargo build --release

- name: Run tests
run: cargo test --release

- name: Setup registry
run: cargo run --release -- --setup-registry

- name: Run CLI with JavaScript
run: cargo run --release -- guest-examples/hello.js

- name: Run CLI with Python
run: cargo run --release -- guest-examples/hello.py

- name: Run syscall_interception example
run: cargo run --release --example syscall_interception

node:
name: Node.js Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
sudo chmod 666 /dev/kvm

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@nightly

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-node-${{ hashFiles('**/Cargo.lock') }}

- name: Install dependencies
run: npm install

- name: Build NAPI bindings
run: npm run build

- name: Setup registry
run: cargo run --release -- --setup-registry

- name: Run Node.js example
run: node examples/napi.js

python:
name: Python Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
sudo chmod 666 /dev/kvm

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@nightly

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-python-${{ hashFiles('**/Cargo.lock') }}

- name: Create virtual environment
run: python3 -m venv .venv

- name: Install maturin
run: .venv/bin/pip install maturin

- name: Build and install Python bindings
run: .venv/bin/maturin develop --features python

- name: Setup registry
run: cargo run --release -- --setup-registry

- name: Run Python example
run: .venv/bin/python examples/python_sdk_example.py
2 changes: 1 addition & 1 deletion build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ fn main() {
use napi_build::setup;
setup();
}
}
}
38 changes: 23 additions & 15 deletions src/bin/hyperlight-nanvix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,66 +10,74 @@ const DEFAULT_LOG_LEVEL: &str = "info";

async fn setup_registry_command() -> Result<()> {
println!("Setting up Nanvix registry...");

// Check cache status first using shared cache utilities
let kernel_cached = cache::is_binary_cached("kernel.elf");
let qjs_cached = cache::is_binary_cached("qjs");
let python_cached = cache::is_binary_cached("python3");

if kernel_cached && qjs_cached && python_cached {
println!("Registry already set up at ~/.cache/nanvix-registry/");
} else {
// Trigger registry download by requesting key binaries
let registry = Registry::new(None);

if !kernel_cached {
print!("Downloading kernel.elf... ");
let _kernel = registry.get_cached_binary("hyperlight", "single-process", "kernel.elf").await?;
let _kernel = registry
.get_cached_binary("hyperlight", "single-process", "kernel.elf")
.await?;
println!("done");
} else {
println!("kernel.elf already cached");
}

if !qjs_cached {
print!("Downloading qjs binary... ");
let _qjs = registry.get_cached_binary("hyperlight", "single-process", "qjs").await?;
let _qjs = registry
.get_cached_binary("hyperlight", "single-process", "qjs")
.await?;
println!("done");
} else {
println!("qjs already cached");
}

if !python_cached {
print!("Downloading python3 binary... ");
let _python = registry.get_cached_binary("hyperlight", "single-process", "python3").await?;
let _python = registry
.get_cached_binary("hyperlight", "single-process", "python3")
.await?;
println!("done");
} else {
println!("python3 already cached");
}

println!("\nRegistry setup complete at ~/.cache/nanvix-registry/");
}

println!("\nTo compile and run C/C++ programs, see the README:");
println!("https://github.com/hyperlight-dev/hyperlight-nanvix?tab=readme-ov-file#c--c-programs");

println!(
"https://github.com/hyperlight-dev/hyperlight-nanvix?tab=readme-ov-file#c--c-programs"
);

Ok(())
}

async fn clear_registry_command() -> Result<()> {
println!("Clearing Nanvix registry cache...");

// Create a minimal config to instantiate the Sandbox for cache clearing
let config = RuntimeConfig::new();
let sandbox = Sandbox::new(config)?;

match sandbox.clear_cache().await {
Ok(()) => println!("Cache cleared successfully"),
Err(e) => {
eprintln!("Error clearing cache: {}", e);
std::process::exit(1);
}
}

println!("Run 'cargo run -- --setup-registry' to re-download if needed.");
Ok(())
}
Expand Down
2 changes: 1 addition & 1 deletion src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ pub async fn get_cached_binary_path(binary_name: &str) -> Option<String> {
pub fn is_binary_cached(binary_name: &str) -> bool {
let cache_path = get_binary_cache_directory().join(binary_name);
cache_path.exists()
}
}
16 changes: 11 additions & 5 deletions src/python.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use pyo3::prelude::*;
#![allow(non_local_definitions)]

use pyo3::exceptions::PyRuntimeError;
use pyo3::prelude::*;
use std::sync::Arc;

use crate::runtime::{Runtime, RuntimeConfig};
Expand Down Expand Up @@ -86,7 +88,9 @@ impl NanvixSandbox {
let runtime = Runtime::new(runtime_config)
.map_err(|e| PyRuntimeError::new_err(format!("Failed to create runtime: {}", e)))?;

Ok(Self { runtime: Arc::new(runtime) })
Ok(Self {
runtime: Arc::new(runtime),
})
}

/// Run a workload in the sandbox
Expand All @@ -103,7 +107,7 @@ impl NanvixSandbox {
/// ... print("Success!")
fn run<'py>(&self, py: Python<'py>, workload_path: String) -> PyResult<&'py PyAny> {
let runtime = Arc::clone(&self.runtime);

pyo3_asyncio::tokio::future_into_py(py, async move {
match runtime.run(&workload_path).await {
Ok(()) => Ok(WorkloadResult {
Expand All @@ -127,9 +131,11 @@ impl NanvixSandbox {
/// >>> success = await sandbox.clear_cache()
fn clear_cache<'py>(&self, py: Python<'py>) -> PyResult<&'py PyAny> {
let runtime = Arc::clone(&self.runtime);

pyo3_asyncio::tokio::future_into_py(py, async move {
runtime.clear_cache().await
runtime
.clear_cache()
.await
.map_err(|e| PyRuntimeError::new_err(format!("Failed to clear cache: {}", e)))?;
Ok(true)
})
Expand Down
22 changes: 14 additions & 8 deletions src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ impl WorkloadType {
/// Detect workload type from file extension
pub fn from_path<P: AsRef<Path>>(path: P) -> Option<Self> {
let path_ref = path.as_ref();

if let Some(extension) = path_ref.extension() {
let ext_str = extension.to_str()?.to_lowercase();
match ext_str.as_str() {
Expand Down Expand Up @@ -134,8 +134,6 @@ impl Runtime {
cache::get_cached_binary_path(binary_name).await
}



/// Clear the nanvix registry cache to force fresh downloads
pub async fn clear_cache(&self) -> Result<()> {
log::info!("Clearing nanvix registry cache...");
Expand All @@ -161,7 +159,10 @@ impl Runtime {
let binary_path = if matches!(workload_type, WorkloadType::Binary) {
// For binary workloads, we don't need an interpreter
String::new()
} else if let Some(cached_path) = self.get_cached_binary_path(workload_type.binary_name()).await {
} else if let Some(cached_path) = self
.get_cached_binary_path(workload_type.binary_name())
.await
{
log::info!(
"Using cached {} binary: {}",
workload_type.binary_name(),
Expand All @@ -179,7 +180,8 @@ impl Runtime {
};

// Get kernel path for terminal configuration
let kernel_path = if let Some(cached_path) = self.get_cached_binary_path("kernel.elf").await {
let kernel_path = if let Some(cached_path) = self.get_cached_binary_path("kernel.elf").await
{
log::info!("Using cached kernel binary: {}", cached_path);
cached_path
} else {
Expand Down Expand Up @@ -228,7 +230,10 @@ impl Runtime {
log::info!("Changed working directory to: {}", base_path.display());
}
} else {
log::warn!("Could not determine registry base directory from binary path: {}", binary_path);
log::warn!(
"Could not determine registry base directory from binary path: {}",
binary_path
);
}
current_dir
} else {
Expand Down Expand Up @@ -258,7 +263,8 @@ impl Runtime {
let mut terminal: Terminal<()> = Terminal::new(sandbox_cache_config);

// Prepare execution paths and metadata
let (script_args, script_name) = self.prepare_script_args(workload_type, Path::new(&absolute_workload_path))?;
let (script_args, script_name) =
self.prepare_script_args(workload_type, Path::new(&absolute_workload_path))?;
let effective_binary_path = match workload_type {
WorkloadType::Python => "bin/python3".to_string(),
WorkloadType::Binary => absolute_workload_path.clone(),
Expand All @@ -283,7 +289,7 @@ impl Runtime {
log::debug!("Script args: {}", effective_script_args);

// Execute workload
let _execution_result = terminal
terminal
.run(
Some(&script_name),
Some(&unique_app_name),
Expand Down
2 changes: 1 addition & 1 deletion src/unit_tests.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#[cfg(test)]
mod unit_tests {
mod tests {
use crate::runtime::{Runtime, WorkloadType};
use crate::*;
use std::sync::Arc;
Expand Down
4 changes: 2 additions & 2 deletions tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ async fn test_syscall_interception() {
.as_nanos();
let config = RuntimeConfig::new()
.with_syscall_table(Arc::new(syscall_table))
.with_log_directory(&format!("/tmp/hyperlight-syscall-test-{}", timestamp))
.with_tmp_directory(&format!("/tmp/hyperlight-syscall-tmp-{}", timestamp));
.with_log_directory(format!("/tmp/hyperlight-syscall-test-{}", timestamp))
.with_tmp_directory(format!("/tmp/hyperlight-syscall-tmp-{}", timestamp));

let mut sandbox = Sandbox::new(config).expect("Failed to create sandbox with syscall table");

Expand Down