diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3678fa7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/build.rs b/build.rs index 789500e..0be2cdb 100644 --- a/build.rs +++ b/build.rs @@ -4,4 +4,4 @@ fn main() { use napi_build::setup; setup(); } -} \ No newline at end of file +} diff --git a/src/bin/hyperlight-nanvix.rs b/src/bin/hyperlight-nanvix.rs index fe4233f..7ffd2a1 100644 --- a/src/bin/hyperlight-nanvix.rs +++ b/src/bin/hyperlight-nanvix.rs @@ -10,58 +10,66 @@ 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) => { @@ -69,7 +77,7 @@ async fn clear_registry_command() -> Result<()> { std::process::exit(1); } } - + println!("Run 'cargo run -- --setup-registry' to re-download if needed."); Ok(()) } diff --git a/src/cache.rs b/src/cache.rs index e302e19..1523e56 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -28,4 +28,4 @@ pub async fn get_cached_binary_path(binary_name: &str) -> Option { pub fn is_binary_cached(binary_name: &str) -> bool { let cache_path = get_binary_cache_directory().join(binary_name); cache_path.exists() -} \ No newline at end of file +} diff --git a/src/python.rs b/src/python.rs index ac7c339..60ec823 100644 --- a/src/python.rs +++ b/src/python.rs @@ -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}; @@ -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 @@ -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 { @@ -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) }) diff --git a/src/runtime.rs b/src/runtime.rs index ac76095..89a1947 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -38,7 +38,7 @@ impl WorkloadType { /// Detect workload type from file extension pub fn from_path>(path: P) -> Option { 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() { @@ -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..."); @@ -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(), @@ -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 { @@ -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 { @@ -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(), @@ -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), diff --git a/src/unit_tests.rs b/src/unit_tests.rs index 8854135..7efa297 100644 --- a/src/unit_tests.rs +++ b/src/unit_tests.rs @@ -1,5 +1,5 @@ #[cfg(test)] -mod unit_tests { +mod tests { use crate::runtime::{Runtime, WorkloadType}; use crate::*; use std::sync::Arc; diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 14985b9..75c14e9 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -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");