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
11 changes: 7 additions & 4 deletions src/nix/rebuild.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ use crate::runtime::Runtime;
pub async fn nixos_rebuild(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()> {
println!("Running nixos-rebuild switch...");

let sudo = runtime.sudo_prefix();
let result = runtime
.exec_cmd(sandbox_name, &["bash", "-lc", "nixos-rebuild switch"], false)
.exec_cmd(sandbox_name, &["bash", "-lc", &format!("{sudo}nixos-rebuild switch")], false)
.await?;

if result.exit_code != 0 {
Expand All @@ -18,7 +19,7 @@ pub async fn nixos_rebuild(runtime: &dyn Runtime, sandbox_name: &str) -> Result<
let rollback = runtime
.exec_cmd(
sandbox_name,
&["bash", "-lc", "nixos-rebuild switch --rollback"],
&["bash", "-lc", &format!("{sudo}nixos-rebuild switch --rollback")],
false,
)
.await;
Expand Down Expand Up @@ -50,13 +51,14 @@ pub async fn write_state_toml(
toml_content: &str,
) -> Result<()> {
// Use tee to write the file as root
let sudo = runtime.sudo_prefix();
let result = runtime
.exec_cmd(
sandbox_name,
&[
"bash", "-lc",
&format!(
"mkdir -p /etc/devbox && cat > /etc/devbox/devbox-state.toml << 'DEVBOX_EOF'\n{toml_content}\nDEVBOX_EOF"
"{sudo}mkdir -p /etc/devbox && {sudo}tee /etc/devbox/devbox-state.toml > /dev/null << 'DEVBOX_EOF'\n{toml_content}\nDEVBOX_EOF"
),
],
false,
Expand All @@ -80,13 +82,14 @@ pub async fn write_nix_file(
filename: &str,
content: &str,
) -> Result<()> {
let sudo = runtime.sudo_prefix();
let result = runtime
.exec_cmd(
sandbox_name,
&[
"bash", "-lc",
&format!(
"mkdir -p /etc/devbox/sets && cat > /etc/devbox/sets/{filename} << 'DEVBOX_EOF'\n{content}\nDEVBOX_EOF"
"{sudo}mkdir -p /etc/devbox/sets && {sudo}tee /etc/devbox/sets/{filename} > /dev/null << 'DEVBOX_EOF'\n{content}\nDEVBOX_EOF"
),
],
false,
Expand Down
4 changes: 4 additions & 0 deletions src/runtime/incus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ impl Runtime for IncusRuntime {
30
}

fn exec_runs_as_root(&self) -> bool {
true
}

async fn create(&self, opts: &CreateOpts) -> Result<SandboxInfo> {
let vm = Self::vm_name(&opts.name);

Expand Down
12 changes: 12 additions & 0 deletions src/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,16 @@ pub trait Runtime: Send + Sync {
async fn exec_as_user(&self, name: &str, cmd: &[&str]) -> Result<ExecResult> {
self.exec_cmd(name, cmd, true).await
}

/// Whether exec_cmd runs as root by default.
/// Incus: true (incus exec defaults to root)
/// Lima: false (limactl shell runs as the configured user)
fn exec_runs_as_root(&self) -> bool {
false
}

/// Returns "sudo " if exec_cmd runs as user (needs elevation), "" if already root.
fn sudo_prefix(&self) -> &str {
if self.exec_runs_as_root() { "" } else { "sudo " }
}
}
50 changes: 32 additions & 18 deletions src/sandbox/overlay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ const STASH_DIR: &str = "/var/devbox/overlay/stash";
/// Returns a list of (status, path) tuples.
pub async fn diff(runtime: &dyn Runtime, sandbox_name: &str) -> Result<Vec<OverlayChange>> {
// List all files in the upper directory
let sudo = runtime.sudo_prefix();
let result = runtime
.exec_cmd(
sandbox_name,
&[
"bash", "-lc",
&format!("find {UPPER} -not -path {UPPER} -printf '%y %P\\n'"),
&format!("{sudo}find {UPPER} -not -path {UPPER} -printf '%y %P\\n'"),
],
false,
)
Expand Down Expand Up @@ -161,6 +162,7 @@ pub async fn commit(

// Sync: rsync from upper to lower for each changed file
// We need to handle additions, modifications, and deletions
let sudo = runtime.sudo_prefix();
let mut committed = 0;

for change in &filtered {
Expand All @@ -170,8 +172,9 @@ pub async fn commit(
match change.status {
ChangeStatus::Added | ChangeStatus::Modified => {
if change.is_dir {
let cmd = format!("{sudo}mkdir -p {lower_path}");
let result = runtime
.exec_cmd(sandbox_name, &["mkdir", "-p", &lower_path], false)
.exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false)
.await?;
if result.exit_code != 0 {
eprintln!(
Expand All @@ -191,17 +194,15 @@ pub async fn commit(
.unwrap_or_default()
);
if !parent.is_empty() && parent != LOWER {
let cmd = format!("{sudo}mkdir -p {parent}");
let _ = runtime
.exec_cmd(sandbox_name, &["mkdir", "-p", &parent], false)
.exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false)
.await;
}

let cmd = format!("{sudo}cp -a {upper_path} {lower_path}");
let result = runtime
.exec_cmd(
sandbox_name,
&["cp", "-a", &upper_path, &lower_path],
false,
)
.exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false)
.await?;
if result.exit_code != 0 {
eprintln!(
Expand All @@ -214,8 +215,9 @@ pub async fn commit(
}
}
ChangeStatus::Deleted => {
let cmd = format!("{sudo}rm -rf {lower_path}");
let result = runtime
.exec_cmd(sandbox_name, &["rm", "-rf", &lower_path], false)
.exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false)
.await?;
if result.exit_code != 0 {
eprintln!(
Expand Down Expand Up @@ -248,12 +250,14 @@ pub async fn discard(
sandbox_name: &str,
paths: Option<&[String]>,
) -> Result<usize> {
let sudo = runtime.sudo_prefix();
if let Some(filter_paths) = paths {
let mut discarded = 0;
for path in filter_paths {
let upper_path = format!("{UPPER}/{}", path.trim_start_matches('/'));
let cmd = format!("{sudo}rm -rf {upper_path}");
let result = runtime
.exec_cmd(sandbox_name, &["rm", "-rf", &upper_path], false)
.exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false)
.await?;
if result.exit_code == 0 {
println!(" Discarded: {path}");
Expand All @@ -272,7 +276,7 @@ pub async fn discard(
&[
"bash",
"-lc",
&format!("rm -rf {UPPER}/* {UPPER}/.[!.]* 2>/dev/null; true"),
&format!("{sudo}rm -rf {UPPER}/* {UPPER}/.[!.]* 2>/dev/null; true"),
],
false,
)
Expand All @@ -294,18 +298,22 @@ pub async fn stash(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()> {
bail!("A stash already exists. Pop or discard it first (`devbox layer stash-pop`).");
}

let sudo = runtime.sudo_prefix();

// Move upper to stash
let cmd = format!("{sudo}mv {UPPER} {STASH_DIR}");
let result = runtime
.exec_cmd(sandbox_name, &["mv", UPPER, STASH_DIR], false)
.exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false)
.await?;

if result.exit_code != 0 {
bail!("Failed to stash overlay: {}", result.stderr.trim());
}

// Recreate empty upper directory
let cmd = format!("{sudo}mkdir -p {UPPER}");
let result = runtime
.exec_cmd(sandbox_name, &["mkdir", "-p", UPPER], false)
.exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false)
.await?;

if result.exit_code != 0 {
Expand All @@ -325,9 +333,11 @@ pub async fn stash_pop(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()>
bail!("No stash found. Nothing to pop.");
}

let sudo = runtime.sudo_prefix();

// Merge stash back into upper (copy hidden and regular files)
let merge_cmd = format!(
"cp -a {STASH_DIR}/* {UPPER}/ 2>/dev/null; cp -a {STASH_DIR}/.[!.]* {UPPER}/ 2>/dev/null; true"
"{sudo}cp -a {STASH_DIR}/* {UPPER}/ 2>/dev/null; {sudo}cp -a {STASH_DIR}/.[!.]* {UPPER}/ 2>/dev/null; true"
);
let result = runtime
.exec_cmd(sandbox_name, &["bash", "-lc", &merge_cmd], false)
Expand All @@ -338,8 +348,9 @@ pub async fn stash_pop(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()>
}

// Remove the stash directory
let cmd = format!("{sudo}rm -rf {STASH_DIR}");
let result = runtime
.exec_cmd(sandbox_name, &["rm", "-rf", STASH_DIR], false)
.exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false)
.await?;

if result.exit_code != 0 {
Expand Down Expand Up @@ -367,11 +378,13 @@ pub async fn has_stash(runtime: &dyn Runtime, sandbox_name: &str) -> Result<bool
/// Newer kernels don't allow `mount -o remount` on OverlayFS, so we
/// unmount and remount with the same options instead.
pub async fn refresh(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()> {
let sudo = runtime.sudo_prefix();

// Try simple remount first (works on older kernels)
let result = runtime
.exec_cmd(
sandbox_name,
&["bash", "-lc", &format!("mount -o remount {WORKSPACE}")],
&["bash", "-lc", &format!("{sudo}mount -o remount {WORKSPACE}")],
false,
)
.await?;
Expand All @@ -384,7 +397,7 @@ pub async fn refresh(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()> {
// Remount not supported — unmount and remount manually.
// The upper layer is on disk, so nothing is lost.
let remount_cmd = format!(
"umount {WORKSPACE} && mount -t overlay overlay \
"{sudo}umount {WORKSPACE} && {sudo}mount -t overlay overlay \
-o lowerdir={LOWER},upperdir={UPPER},workdir={WORK} {WORKSPACE}"
);
let result = runtime
Expand Down Expand Up @@ -455,8 +468,9 @@ pub async fn lower_layer_changes(runtime: &dyn Runtime, sandbox_name: &str) -> R
// Compare the lower layer mtime against a timestamp file we create on mount.
// If no timestamp exists, we can't detect changes — just check for stale handles.
// Simpler approach: find files in lower newer than the overlay work dir (created at mount time).
let sudo = runtime.sudo_prefix();
let cmd = format!(
"find {} -newer {} -not -path {} -type f -printf '%P\\n' 2>/dev/null | head -50",
"{sudo}find {} -newer {} -not -path {} -type f -printf '%P\\n' 2>/dev/null | head -50",
LOWER, WORK, LOWER
);
let result = runtime
Expand Down
Loading
Loading