Skip to content
This repository was archived by the owner on May 14, 2026. It is now read-only.

Commit 02f2b3b

Browse files
authored
Add GitHub Actions CI workflow (#26)
* Add GitHub Actions CI workflow Validates Rust, Node.js, and Python builds with tests and examples. Does not cache nanvix registry - release version not determinable from source. Signed-off-by: James Sturtevant <jsturtevant@gmail.com> * Fix Rust formatting Signed-off-by: James Sturtevant <jsturtevant@gmail.com> * Fix Clippy warnings Signed-off-by: James Sturtevant <jsturtevant@gmail.com> * Skip VM-requiring tests in CI GitHub Actions runners don't have KVM/hypervisor support needed for running the Nanvix microkernel VMs. Only run unit tests (--lib) and skip integration tests and example runs. Signed-off-by: James Sturtevant <jsturtevant@gmail.com> * Revert skip VM tests, enable KVM in CI GitHub Actions runners support KVM. Add udev rule to enable KVM access for all jobs that need to run VMs. Signed-off-by: James Sturtevant <jsturtevant@gmail.com> * Update Python version to 3.12 Signed-off-by: James Sturtevant <jsturtevant@gmail.com> --------- Signed-off-by: James Sturtevant <jsturtevant@gmail.com>
1 parent 66c1985 commit 02f2b3b

8 files changed

Lines changed: 208 additions & 33 deletions

File tree

.github/workflows/ci.yml

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
12+
jobs:
13+
rust:
14+
name: Rust Build & Test
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Enable KVM
20+
run: |
21+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
22+
sudo udevadm control --reload-rules
23+
sudo udevadm trigger --name-match=kvm
24+
sudo chmod 666 /dev/kvm
25+
26+
- name: Install Rust toolchain
27+
uses: dtolnay/rust-toolchain@nightly
28+
with:
29+
components: rustfmt, clippy
30+
31+
- name: Cache cargo
32+
uses: actions/cache@v4
33+
with:
34+
path: |
35+
~/.cargo/bin/
36+
~/.cargo/registry/index/
37+
~/.cargo/registry/cache/
38+
~/.cargo/git/db/
39+
target/
40+
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
41+
42+
- name: Check formatting
43+
run: cargo fmt --all -- --check
44+
45+
- name: Clippy
46+
run: cargo clippy --all-targets --all-features -- -D warnings
47+
48+
- name: Build
49+
run: cargo build --release
50+
51+
- name: Run tests
52+
run: cargo test --release
53+
54+
- name: Setup registry
55+
run: cargo run --release -- --setup-registry
56+
57+
- name: Run CLI with JavaScript
58+
run: cargo run --release -- guest-examples/hello.js
59+
60+
- name: Run CLI with Python
61+
run: cargo run --release -- guest-examples/hello.py
62+
63+
- name: Run syscall_interception example
64+
run: cargo run --release --example syscall_interception
65+
66+
node:
67+
name: Node.js Build
68+
runs-on: ubuntu-latest
69+
steps:
70+
- uses: actions/checkout@v4
71+
72+
- name: Enable KVM
73+
run: |
74+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
75+
sudo udevadm control --reload-rules
76+
sudo udevadm trigger --name-match=kvm
77+
sudo chmod 666 /dev/kvm
78+
79+
- name: Install Rust toolchain
80+
uses: dtolnay/rust-toolchain@nightly
81+
82+
- name: Setup Node.js
83+
uses: actions/setup-node@v4
84+
with:
85+
node-version: '20'
86+
87+
- name: Cache cargo
88+
uses: actions/cache@v4
89+
with:
90+
path: |
91+
~/.cargo/bin/
92+
~/.cargo/registry/index/
93+
~/.cargo/registry/cache/
94+
~/.cargo/git/db/
95+
target/
96+
key: ${{ runner.os }}-cargo-node-${{ hashFiles('**/Cargo.lock') }}
97+
98+
- name: Install dependencies
99+
run: npm install
100+
101+
- name: Build NAPI bindings
102+
run: npm run build
103+
104+
- name: Setup registry
105+
run: cargo run --release -- --setup-registry
106+
107+
- name: Run Node.js example
108+
run: node examples/napi.js
109+
110+
python:
111+
name: Python Build
112+
runs-on: ubuntu-latest
113+
steps:
114+
- uses: actions/checkout@v4
115+
116+
- name: Enable KVM
117+
run: |
118+
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
119+
sudo udevadm control --reload-rules
120+
sudo udevadm trigger --name-match=kvm
121+
sudo chmod 666 /dev/kvm
122+
123+
- name: Install Rust toolchain
124+
uses: dtolnay/rust-toolchain@nightly
125+
126+
- name: Setup Python
127+
uses: actions/setup-python@v5
128+
with:
129+
python-version: '3.12'
130+
131+
- name: Cache cargo
132+
uses: actions/cache@v4
133+
with:
134+
path: |
135+
~/.cargo/bin/
136+
~/.cargo/registry/index/
137+
~/.cargo/registry/cache/
138+
~/.cargo/git/db/
139+
target/
140+
key: ${{ runner.os }}-cargo-python-${{ hashFiles('**/Cargo.lock') }}
141+
142+
- name: Create virtual environment
143+
run: python3 -m venv .venv
144+
145+
- name: Install maturin
146+
run: .venv/bin/pip install maturin
147+
148+
- name: Build and install Python bindings
149+
run: .venv/bin/maturin develop --features python
150+
151+
- name: Setup registry
152+
run: cargo run --release -- --setup-registry
153+
154+
- name: Run Python example
155+
run: .venv/bin/python examples/python_sdk_example.py

build.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ fn main() {
44
use napi_build::setup;
55
setup();
66
}
7-
}
7+
}

src/bin/hyperlight-nanvix.rs

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,66 +10,74 @@ const DEFAULT_LOG_LEVEL: &str = "info";
1010

1111
async fn setup_registry_command() -> Result<()> {
1212
println!("Setting up Nanvix registry...");
13-
13+
1414
// Check cache status first using shared cache utilities
1515
let kernel_cached = cache::is_binary_cached("kernel.elf");
1616
let qjs_cached = cache::is_binary_cached("qjs");
1717
let python_cached = cache::is_binary_cached("python3");
18-
18+
1919
if kernel_cached && qjs_cached && python_cached {
2020
println!("Registry already set up at ~/.cache/nanvix-registry/");
2121
} else {
2222
// Trigger registry download by requesting key binaries
2323
let registry = Registry::new(None);
24-
24+
2525
if !kernel_cached {
2626
print!("Downloading kernel.elf... ");
27-
let _kernel = registry.get_cached_binary("hyperlight", "single-process", "kernel.elf").await?;
27+
let _kernel = registry
28+
.get_cached_binary("hyperlight", "single-process", "kernel.elf")
29+
.await?;
2830
println!("done");
2931
} else {
3032
println!("kernel.elf already cached");
3133
}
32-
34+
3335
if !qjs_cached {
3436
print!("Downloading qjs binary... ");
35-
let _qjs = registry.get_cached_binary("hyperlight", "single-process", "qjs").await?;
37+
let _qjs = registry
38+
.get_cached_binary("hyperlight", "single-process", "qjs")
39+
.await?;
3640
println!("done");
3741
} else {
3842
println!("qjs already cached");
3943
}
40-
44+
4145
if !python_cached {
4246
print!("Downloading python3 binary... ");
43-
let _python = registry.get_cached_binary("hyperlight", "single-process", "python3").await?;
47+
let _python = registry
48+
.get_cached_binary("hyperlight", "single-process", "python3")
49+
.await?;
4450
println!("done");
4551
} else {
4652
println!("python3 already cached");
4753
}
48-
54+
4955
println!("\nRegistry setup complete at ~/.cache/nanvix-registry/");
5056
}
51-
57+
5258
println!("\nTo compile and run C/C++ programs, see the README:");
53-
println!("https://github.com/hyperlight-dev/hyperlight-nanvix?tab=readme-ov-file#c--c-programs");
54-
59+
println!(
60+
"https://github.com/hyperlight-dev/hyperlight-nanvix?tab=readme-ov-file#c--c-programs"
61+
);
62+
5563
Ok(())
5664
}
5765

5866
async fn clear_registry_command() -> Result<()> {
5967
println!("Clearing Nanvix registry cache...");
60-
68+
6169
// Create a minimal config to instantiate the Sandbox for cache clearing
6270
let config = RuntimeConfig::new();
6371
let sandbox = Sandbox::new(config)?;
64-
72+
6573
match sandbox.clear_cache().await {
6674
Ok(()) => println!("Cache cleared successfully"),
6775
Err(e) => {
6876
eprintln!("Error clearing cache: {}", e);
6977
std::process::exit(1);
7078
}
7179
}
72-
80+
7381
println!("Run 'cargo run -- --setup-registry' to re-download if needed.");
7482
Ok(())
7583
}

src/cache.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ pub async fn get_cached_binary_path(binary_name: &str) -> Option<String> {
2828
pub fn is_binary_cached(binary_name: &str) -> bool {
2929
let cache_path = get_binary_cache_directory().join(binary_name);
3030
cache_path.exists()
31-
}
31+
}

src/python.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
use pyo3::prelude::*;
1+
#![allow(non_local_definitions)]
2+
23
use pyo3::exceptions::PyRuntimeError;
4+
use pyo3::prelude::*;
35
use std::sync::Arc;
46

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

89-
Ok(Self { runtime: Arc::new(runtime) })
91+
Ok(Self {
92+
runtime: Arc::new(runtime),
93+
})
9094
}
9195

9296
/// Run a workload in the sandbox
@@ -103,7 +107,7 @@ impl NanvixSandbox {
103107
/// ... print("Success!")
104108
fn run<'py>(&self, py: Python<'py>, workload_path: String) -> PyResult<&'py PyAny> {
105109
let runtime = Arc::clone(&self.runtime);
106-
110+
107111
pyo3_asyncio::tokio::future_into_py(py, async move {
108112
match runtime.run(&workload_path).await {
109113
Ok(()) => Ok(WorkloadResult {
@@ -127,9 +131,11 @@ impl NanvixSandbox {
127131
/// >>> success = await sandbox.clear_cache()
128132
fn clear_cache<'py>(&self, py: Python<'py>) -> PyResult<&'py PyAny> {
129133
let runtime = Arc::clone(&self.runtime);
130-
134+
131135
pyo3_asyncio::tokio::future_into_py(py, async move {
132-
runtime.clear_cache().await
136+
runtime
137+
.clear_cache()
138+
.await
133139
.map_err(|e| PyRuntimeError::new_err(format!("Failed to clear cache: {}", e)))?;
134140
Ok(true)
135141
})

src/runtime.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ impl WorkloadType {
3838
/// Detect workload type from file extension
3939
pub fn from_path<P: AsRef<Path>>(path: P) -> Option<Self> {
4040
let path_ref = path.as_ref();
41-
41+
4242
if let Some(extension) = path_ref.extension() {
4343
let ext_str = extension.to_str()?.to_lowercase();
4444
match ext_str.as_str() {
@@ -134,8 +134,6 @@ impl Runtime {
134134
cache::get_cached_binary_path(binary_name).await
135135
}
136136

137-
138-
139137
/// Clear the nanvix registry cache to force fresh downloads
140138
pub async fn clear_cache(&self) -> Result<()> {
141139
log::info!("Clearing nanvix registry cache...");
@@ -161,7 +159,10 @@ impl Runtime {
161159
let binary_path = if matches!(workload_type, WorkloadType::Binary) {
162160
// For binary workloads, we don't need an interpreter
163161
String::new()
164-
} else if let Some(cached_path) = self.get_cached_binary_path(workload_type.binary_name()).await {
162+
} else if let Some(cached_path) = self
163+
.get_cached_binary_path(workload_type.binary_name())
164+
.await
165+
{
165166
log::info!(
166167
"Using cached {} binary: {}",
167168
workload_type.binary_name(),
@@ -179,7 +180,8 @@ impl Runtime {
179180
};
180181

181182
// Get kernel path for terminal configuration
182-
let kernel_path = if let Some(cached_path) = self.get_cached_binary_path("kernel.elf").await {
183+
let kernel_path = if let Some(cached_path) = self.get_cached_binary_path("kernel.elf").await
184+
{
183185
log::info!("Using cached kernel binary: {}", cached_path);
184186
cached_path
185187
} else {
@@ -228,7 +230,10 @@ impl Runtime {
228230
log::info!("Changed working directory to: {}", base_path.display());
229231
}
230232
} else {
231-
log::warn!("Could not determine registry base directory from binary path: {}", binary_path);
233+
log::warn!(
234+
"Could not determine registry base directory from binary path: {}",
235+
binary_path
236+
);
232237
}
233238
current_dir
234239
} else {
@@ -258,7 +263,8 @@ impl Runtime {
258263
let mut terminal: Terminal<()> = Terminal::new(sandbox_cache_config);
259264

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

285291
// Execute workload
286-
let _execution_result = terminal
292+
terminal
287293
.run(
288294
Some(&script_name),
289295
Some(&unique_app_name),

src/unit_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#[cfg(test)]
2-
mod unit_tests {
2+
mod tests {
33
use crate::runtime::{Runtime, WorkloadType};
44
use crate::*;
55
use std::sync::Arc;

tests/integration_tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ async fn test_syscall_interception() {
6262
.as_nanos();
6363
let config = RuntimeConfig::new()
6464
.with_syscall_table(Arc::new(syscall_table))
65-
.with_log_directory(&format!("/tmp/hyperlight-syscall-test-{}", timestamp))
66-
.with_tmp_directory(&format!("/tmp/hyperlight-syscall-tmp-{}", timestamp));
65+
.with_log_directory(format!("/tmp/hyperlight-syscall-test-{}", timestamp))
66+
.with_tmp_directory(format!("/tmp/hyperlight-syscall-tmp-{}", timestamp));
6767

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

0 commit comments

Comments
 (0)