Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e0d5f16
fix(export): stop shipping the guest rootfs disk in box archives
G4614 Jul 28, 2026
c364203
feat(export): ship the box disk as content-addressed layers
G4614 Jul 29, 2026
35b94fb
fix(export): restore the ArchiveManifest doc comment
G4614 Jul 29, 2026
192d170
style(libkrun-sys): rustfmt build.rs
G4614 Jul 29, 2026
1543227
fix(import): harden layered import against crafted archives
G4614 Jul 30, 2026
920bd0c
perf(export): cache the image disk's digest beside it
G4614 Jul 30, 2026
ae96c8e
fix(export): refuse an export the guest would not freeze for
G4614 Jul 30, 2026
988fd98
feat(archive): identify the image layer by its image digest
G4614 Jul 30, 2026
ef31ff3
feat(export): directory-form archive, incremental by construction
G4614 Jul 31, 2026
8c3c865
chore: keep the MinIO round-trip harness out of the PR
G4614 Jul 31, 2026
9b17523
feat(archive): shared layer store with reference-counted sweep
G4614 Jul 31, 2026
b7e5751
Merge remote-tracking branch 'origin/main' into feat/layered-archive
G4614 Jul 31, 2026
82f6876
fix: adapt to main's sha2 0.11 and satisfy workspace clippy
G4614 Jul 31, 2026
52d5b14
Merge branch 'feat/layered-archive' into feat/archive-store
G4614 Jul 31, 2026
331c6a4
fix(export): give a loaded host more freeze headroom
G4614 Jul 31, 2026
e0d861a
fix(export): give a loaded host more freeze headroom
G4614 Jul 31, 2026
e110cca
fix(node): give the options test its new field
G4614 Jul 31, 2026
ad2f36f
Merge branch 'feat/layered-archive' into feat/archive-store
G4614 Jul 31, 2026
855ae57
style(node): format the options test initializer
G4614 Jul 31, 2026
3261fa8
fix(export): stop shipping the guest rootfs disk in box archives
G4614 Jul 28, 2026
b16b171
feat(export): ship the box disk as content-addressed layers
G4614 Jul 29, 2026
b9ba606
fix(export): restore the ArchiveManifest doc comment
G4614 Jul 29, 2026
5e614e8
fix(import): harden layered import against crafted archives
G4614 Jul 30, 2026
0ef6623
perf(export): cache the image disk's digest beside it
G4614 Jul 30, 2026
2c244a7
fix(export): refuse an export the guest would not freeze for
G4614 Jul 30, 2026
607abd0
feat(archive): identify the image layer by its image digest
G4614 Jul 30, 2026
766b6e3
feat(export): directory-form archive, incremental by construction
G4614 Jul 31, 2026
8661ba1
chore: keep the MinIO round-trip harness out of the PR
G4614 Jul 31, 2026
c5e4b9c
fix: adapt to main's sha2 0.11 and satisfy workspace clippy
G4614 Jul 31, 2026
8752f64
fix(export): give a loaded host more freeze headroom
G4614 Jul 31, 2026
c6bf5cc
fix(import): retain refs when ownership handoff fails
G4614 Jul 31, 2026
69a4e6f
fix(archive): harden layered import and export
G4614 Jul 31, 2026
78ec792
fix(import): validate archive layer digests
G4614 Jul 31, 2026
84172e8
fix(import): bound layered archive extraction
G4614 Jul 31, 2026
4b9534a
fix(import): synchronize layer adoption with GC
G4614 Jul 31, 2026
0a2ae99
Merge commit '4b9534a2' into feat/archive-store
G4614 Jul 31, 2026
4bdbadb
test(archive): hash MinIO round trips in process
G4614 Jul 31, 2026
c660511
chore(archive): narrow export import PR surface
G4614 Jul 31, 2026
32b2fcd
feat(sdk): expose box archive import export
G4614 Jul 31, 2026
2cafe56
fix(sdks): default new export options
G4614 Jul 31, 2026
69b1e9e
fix(sdk): align archive bindings with drain contract
G4614 Jul 31, 2026
395dee6
fix(archive): give racing layer-object writers unique staging names
G4614 Aug 4, 2026
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
19 changes: 15 additions & 4 deletions sdks/c/include/boxlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,14 @@ typedef struct FFIError {
char *message;
} FFIError;

typedef struct RuntimeHandle CBoxliteRuntime;

typedef struct OptionsHandle CBoxliteOptions;

typedef struct BoxHandle CBoxHandle;

typedef struct FFIError CBoxliteError;

typedef struct RuntimeHandle CBoxliteRuntime;

typedef struct OptionsHandle CBoxliteOptions;

// Box creation completion.
typedef void (*CBoxCreateBoxCb)(CBoxHandle*, CBoxliteError*, void*);

Expand Down Expand Up @@ -469,6 +469,17 @@ enum BoxliteErrorCode boxlite_advanced_options_set_capabilities_drop(CAdvancedBo
const char *const *capabilities,
int count);

enum BoxliteErrorCode boxlite_box_export(CBoxHandle *handle,
const char *dest_path,
char **out_path,
CBoxliteError *out_error);

enum BoxliteErrorCode boxlite_runtime_import_box(CBoxliteRuntime *runtime,
const char *archive_path,
const char *name,
CBoxHandle **out_handle,
CBoxliteError *out_error);

enum BoxliteErrorCode boxlite_create_box(CBoxliteRuntime *runtime,
CBoxliteOptions *opts,
CBoxCreateBoxCb cb,
Expand Down
145 changes: 145 additions & 0 deletions sdks/c/src/archive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
//! Box archive export/import operations for the BoxLite C SDK.

use std::os::raw::c_char;
use std::path::PathBuf;
use std::sync::Arc;

use boxlite::BoxliteError;
use boxlite::runtime::options::{BoxArchive, ExportOptions};

use crate::box_handle::BoxHandle;
use crate::error::{BoxliteErrorCode, FFIError, null_pointer_error, write_error};
use crate::runtime::RuntimeHandle;
use crate::util::{alloc_c_string, c_str_to_string};
use crate::{CBoxHandle, CBoxliteError, CBoxliteRuntime};

#[unsafe(no_mangle)]
pub unsafe extern "C" fn boxlite_box_export(
handle: *mut CBoxHandle,
dest_path: *const c_char,
out_path: *mut *mut c_char,
out_error: *mut CBoxliteError,
) -> BoxliteErrorCode {
box_export(handle, dest_path, out_path, out_error)
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn boxlite_runtime_import_box(
runtime: *mut CBoxliteRuntime,
archive_path: *const c_char,
name: *const c_char,
out_handle: *mut *mut CBoxHandle,
out_error: *mut CBoxliteError,
) -> BoxliteErrorCode {
runtime_import_box(runtime, archive_path, name, out_handle, out_error)
}

unsafe fn box_export(
handle: *mut BoxHandle,
dest_path: *const c_char,
out_path: *mut *mut c_char,
out_error: *mut FFIError,
) -> BoxliteErrorCode {
unsafe {
if handle.is_null() {
write_error(out_error, null_pointer_error("handle"));
return BoxliteErrorCode::InvalidArgument;
}
if out_path.is_null() {
write_error(out_error, null_pointer_error("out_path"));
return BoxliteErrorCode::InvalidArgument;
}
let dest = match c_str_to_string(dest_path) {
Ok(s) => PathBuf::from(s),
Err(e) => {
write_error(out_error, e);
return BoxliteErrorCode::InvalidArgument;
}
};

let handle_ref = &*handle;
let lite = handle_ref.handle.clone();
match handle_ref
.tokio_rt
.block_on(lite.export(ExportOptions::default(), &dest))
{
Ok(archive) => {
let path = archive.path().to_string_lossy().into_owned();
let c_path = alloc_c_string(&path);
if c_path.is_null() {
write_error(
out_error,
BoxliteError::Internal("archive path contains interior NUL".into()),
);
return BoxliteErrorCode::Internal;
}
*out_path = c_path;
BoxliteErrorCode::Ok
}
Err(e) => {
write_error(out_error, e);
BoxliteErrorCode::Internal
}
}
}
}

unsafe fn runtime_import_box(
runtime: *mut RuntimeHandle,
archive_path: *const c_char,
name: *const c_char,
out_handle: *mut *mut CBoxHandle,
out_error: *mut FFIError,
) -> BoxliteErrorCode {
unsafe {
if runtime.is_null() {
write_error(out_error, null_pointer_error("runtime"));
return BoxliteErrorCode::InvalidArgument;
}
if out_handle.is_null() {
write_error(out_error, null_pointer_error("out_handle"));
return BoxliteErrorCode::InvalidArgument;
}
let archive_path = match c_str_to_string(archive_path) {
Ok(s) => PathBuf::from(s),
Err(e) => {
write_error(out_error, e);
return BoxliteErrorCode::InvalidArgument;
}
};
let name = if name.is_null() {
None
} else {
match c_str_to_string(name) {
Ok(s) => Some(s),
Err(e) => {
write_error(out_error, e);
return BoxliteErrorCode::InvalidArgument;
}
}
};

let runtime_ref = &*runtime;
let runtime_clone = runtime_ref.runtime.clone();
let tokio_rt = runtime_ref.tokio_rt.clone();
let task_tokio_rt = tokio_rt.clone();

match tokio_rt.block_on(runtime_clone.import_box(BoxArchive::new(archive_path), name)) {
Ok(handle) => {
let box_id = handle.id().clone();
let boxed = Box::new(BoxHandle {
handle: Arc::new(handle),
box_id,
tokio_rt: task_tokio_rt,
queue: runtime_ref.queue.clone(),
});
*out_handle = Box::into_raw(boxed);
BoxliteErrorCode::Ok
}
Err(e) => {
write_error(out_error, e);
BoxliteErrorCode::Internal
}
}
}
}
2 changes: 2 additions & 0 deletions sdks/c/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#![allow(clippy::too_many_arguments)]

mod advanced_options;
mod archive;
mod box_handle;
mod copy;
mod error;
Expand Down Expand Up @@ -67,6 +68,7 @@ pub type BoxliteCommand = exec::BoxliteCommand;
pub type CAdvancedBoxOptions = advanced_options::AdvancedBoxOptionsHandle;

pub use advanced_options::*;
pub use archive::*;
pub use box_handle::*;
pub use copy::*;
pub use error::*;
Expand Down
62 changes: 62 additions & 0 deletions sdks/go/archive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package boxlite

/*
#include "bridge.h"
#include <stdlib.h>
*/
import "C"
import (
"context"
"unsafe"
)

// Export writes the box archive into dest and returns the archive path.
func (b *Box) Export(ctx context.Context, dest string) (string, error) {
if err := ctx.Err(); err != nil {
return "", err
}

cDest := toCString(dest)
defer C.free(unsafe.Pointer(cDest))

var outPath *C.char
var cerr C.CBoxliteError
code := C.boxlite_box_export(b.handle, cDest, &outPath, &cerr)
if code != C.Ok {
return "", freeError(&cerr)
}
defer freeBoxliteString(outPath)

return cString(outPath), ctx.Err()
}

// Import restores an archive into this runtime. If name is empty, the archive's
// recorded box name is used.
func (r *Runtime) Import(ctx context.Context, archivePath, name string) (*Box, error) {
if err := ctx.Err(); err != nil {
return nil, err
}

cArchive := toCString(archivePath)
defer C.free(unsafe.Pointer(cArchive))
var cName *C.char
if name != "" {
cName = toCString(name)
defer C.free(unsafe.Pointer(cName))
}

var outHandle *C.CBoxHandle
var cerr C.CBoxliteError
code := C.boxlite_runtime_import_box(r.handle, cArchive, cName, &outHandle, &cerr)
if code != C.Ok {
return nil, freeError(&cerr)
}

if err := ctx.Err(); err != nil {
if outHandle != nil {
C.boxlite_box_free(outHandle)
}
return nil, err
}
return newBoxFromHandle(r, outHandle, name), nil
}
2 changes: 1 addition & 1 deletion sdks/node/src/snapshot_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub struct JsExportOptions {}

impl From<JsExportOptions> for ExportOptions {
fn from(_js: JsExportOptions) -> Self {
ExportOptions {}
ExportOptions::default()
}
}

Expand Down
2 changes: 1 addition & 1 deletion sdks/python/src/snapshot_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl PyExportOptions {

impl From<PyExportOptions> for ExportOptions {
fn from(_py: PyExportOptions) -> Self {
ExportOptions {}
ExportOptions::default()
}
}

Expand Down
38 changes: 36 additions & 2 deletions src/boxlite/src/db/base_disk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ impl BaseDiskStore {
let conn = self.db.conn();
db_err!(conn.execute(
"INSERT INTO base_disk \
(id, source_box_id, name, kind, base_path, created_at, json) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
(id, source_box_id, name, kind, base_path, created_at, json, digest) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
rusqlite::params![
&disk.id,
&disk.source_box_id,
Expand All @@ -106,11 +106,43 @@ impl BaseDiskStore {
&disk.disk_info.base_path,
disk.created_at,
json,
&disk.digest,
],
))?;
Ok(())
}

/// Find a base disk by its content digest.
///
/// Only layers whose digest has already been computed are visible here;
/// see [`BaseDisk::digest`] for why it is filled in lazily.
pub(crate) fn find_by_digest(&self, digest: &str) -> BoxliteResult<Option<BaseDiskInfo>> {
let conn = self.db.conn();
let result = db_err!(
conn.query_row(
"SELECT id, source_box_id, name, kind, base_path, \
created_at, json FROM base_disk WHERE digest = ?1",
rusqlite::params![digest],
row_to_record,
)
.optional()
)?;
Ok(result)
}

/// Record a layer's content digest, in both the indexed column and the
/// JSON blob so the two cannot drift.
pub(crate) fn set_digest(&self, id: &BaseDiskID, digest: &str) -> BoxliteResult<()> {
let conn = self.db.conn();
db_err!(conn.execute(
"UPDATE base_disk \
SET digest = ?2, json = json_set(json, '$.digest', ?2) \
WHERE id = ?1",
rusqlite::params![id, digest],
))?;
Ok(())
}

/// Find a base disk by its ID.
#[allow(dead_code)] // used in lineage.rs tests
pub(crate) fn find_by_id(&self, id: &BaseDiskID) -> BoxliteResult<Option<BaseDiskInfo>> {
Expand Down Expand Up @@ -330,6 +362,7 @@ mod tests {
size_bytes: 512,
},
created_at: chrono::Utc::now().timestamp(),
digest: None,
}
}

Expand Down Expand Up @@ -686,6 +719,7 @@ mod tests {
size_bytes: 1024,
},
created_at: 1700000000,
digest: None,
};
store.insert(&disk).unwrap();

Expand Down
2 changes: 2 additions & 0 deletions src/boxlite/src/db/migration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod v5_to_v6;
mod v6_to_v7;
mod v7_to_v8;
mod v8_to_v9;
mod v9_to_v10;

use std::path::Path;

Expand Down Expand Up @@ -81,5 +82,6 @@ fn all_migrations() -> Vec<Box<dyn Migration>> {
Box::new(v6_to_v7::MoveDisksAndAddBaseDisk),
Box::new(v7_to_v8::RenameNetworkSpec),
Box::new(v8_to_v9::PreservePublishedPorts),
Box::new(v9_to_v10::AddBaseDiskDigest),
]
}
Loading
Loading