Skip to content

Commit 6c6bace

Browse files
Merge pull request #18 from ethannortharc/fix/lima-run-as-root
refactor: replace sudo_prefix with run_as_root abstraction
2 parents 9e27f1c + b98bdef commit 6c6bace

File tree

4 files changed

+76
-152
lines changed

4 files changed

+76
-152
lines changed

src/nix/rebuild.rs

Lines changed: 10 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,16 @@ use crate::runtime::Runtime;
77
pub async fn nixos_rebuild(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()> {
88
println!("Running nixos-rebuild switch...");
99

10-
let sudo = runtime.sudo_prefix();
1110
let result = runtime
12-
.exec_cmd(sandbox_name, &["bash", "-lc", &format!("{sudo}nixos-rebuild switch")], false)
11+
.run_as_root(sandbox_name, "nixos-rebuild switch", false)
1312
.await?;
1413

1514
if result.exit_code != 0 {
1615
eprintln!("nixos-rebuild failed:\n{}", result.stderr.trim());
1716
eprintln!("Attempting rollback...");
1817

1918
let rollback = runtime
20-
.exec_cmd(
21-
sandbox_name,
22-
&["bash", "-lc", &format!("{sudo}nixos-rebuild switch --rollback")],
23-
false,
24-
)
19+
.run_as_root(sandbox_name, "nixos-rebuild switch --rollback", false)
2520
.await;
2621

2722
match rollback {
@@ -50,20 +45,10 @@ pub async fn write_state_toml(
5045
sandbox_name: &str,
5146
toml_content: &str,
5247
) -> Result<()> {
53-
// Use tee to write the file as root
54-
let sudo = runtime.sudo_prefix();
55-
let result = runtime
56-
.exec_cmd(
57-
sandbox_name,
58-
&[
59-
"bash", "-lc",
60-
&format!(
61-
"{sudo}mkdir -p /etc/devbox && {sudo}tee /etc/devbox/devbox-state.toml > /dev/null << 'DEVBOX_EOF'\n{toml_content}\nDEVBOX_EOF"
62-
),
63-
],
64-
false,
65-
)
66-
.await?;
48+
let cmd = format!(
49+
"mkdir -p /etc/devbox && tee /etc/devbox/devbox-state.toml > /dev/null << 'DEVBOX_EOF'\n{toml_content}\nDEVBOX_EOF"
50+
);
51+
let result = runtime.run_as_root(sandbox_name, &cmd, false).await?;
6752

6853
if result.exit_code != 0 {
6954
bail!(
@@ -82,19 +67,10 @@ pub async fn write_nix_file(
8267
filename: &str,
8368
content: &str,
8469
) -> Result<()> {
85-
let sudo = runtime.sudo_prefix();
86-
let result = runtime
87-
.exec_cmd(
88-
sandbox_name,
89-
&[
90-
"bash", "-lc",
91-
&format!(
92-
"{sudo}mkdir -p /etc/devbox/sets && {sudo}tee /etc/devbox/sets/{filename} > /dev/null << 'DEVBOX_EOF'\n{content}\nDEVBOX_EOF"
93-
),
94-
],
95-
false,
96-
)
97-
.await?;
70+
let cmd = format!(
71+
"mkdir -p /etc/devbox/sets && tee /etc/devbox/sets/{filename} > /dev/null << 'DEVBOX_EOF'\n{content}\nDEVBOX_EOF"
72+
);
73+
let result = runtime.run_as_root(sandbox_name, &cmd, false).await?;
9874

9975
if result.exit_code != 0 {
10076
bail!("Failed to write {filename}: {}", result.stderr.trim());

src/runtime/mod.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,20 @@ pub trait Runtime: Send + Sync {
141141
false
142142
}
143143

144-
/// Returns "sudo " if exec_cmd runs as user (needs elevation), "" if already root.
145-
fn sudo_prefix(&self) -> &str {
146-
if self.exec_runs_as_root() { "" } else { "sudo " }
144+
/// Execute a shell command as root with a login shell.
145+
///
146+
/// This is the correct abstraction for running privileged commands:
147+
/// - Incus: `bash -lc <cmd>` (already root, login shell for PATH)
148+
/// - Lima: `sudo bash -lc <cmd>` (elevate, login shell for PATH)
149+
///
150+
/// Unlike a simple `sudo` prefix, this wraps the ENTIRE command inside
151+
/// the sudo boundary, so environment variables set within `cmd` (like
152+
/// `export NIX_PATH=...`) are preserved for the privileged process.
153+
async fn run_as_root(&self, name: &str, cmd: &str, interactive: bool) -> Result<ExecResult> {
154+
if self.exec_runs_as_root() {
155+
self.exec_cmd(name, &["bash", "-lc", cmd], interactive).await
156+
} else {
157+
self.exec_cmd(name, &["sudo", "bash", "-lc", cmd], interactive).await
158+
}
147159
}
148160
}

src/sandbox/overlay.rs

Lines changed: 30 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,9 @@ const STASH_DIR: &str = "/var/devbox/overlay/stash";
1414
/// List files changed in the overlay upper layer.
1515
/// Returns a list of (status, path) tuples.
1616
pub async fn diff(runtime: &dyn Runtime, sandbox_name: &str) -> Result<Vec<OverlayChange>> {
17-
// List all files in the upper directory
18-
let sudo = runtime.sudo_prefix();
19-
let result = runtime
20-
.exec_cmd(
21-
sandbox_name,
22-
&[
23-
"bash", "-lc",
24-
&format!("{sudo}find {UPPER} -not -path {UPPER} -printf '%y %P\\n'"),
25-
],
26-
false,
27-
)
28-
.await?;
17+
// List all files in the upper directory (needs root for overlay dirs)
18+
let cmd = format!("find {UPPER} -not -path {UPPER} -printf '%y %P\\n'");
19+
let result = runtime.run_as_root(sandbox_name, &cmd, false).await?;
2920

3021
if result.exit_code != 0 {
3122
bail!("Failed to scan overlay changes: {}", result.stderr.trim());
@@ -160,9 +151,7 @@ pub async fn commit(
160151
return Ok(filtered.len());
161152
}
162153

163-
// Sync: rsync from upper to lower for each changed file
164-
// We need to handle additions, modifications, and deletions
165-
let sudo = runtime.sudo_prefix();
154+
// Sync: copy from upper to lower for each changed file
166155
let mut committed = 0;
167156

168157
for change in &filtered {
@@ -172,10 +161,8 @@ pub async fn commit(
172161
match change.status {
173162
ChangeStatus::Added | ChangeStatus::Modified => {
174163
if change.is_dir {
175-
let cmd = format!("{sudo}mkdir -p {lower_path}");
176-
let result = runtime
177-
.exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false)
178-
.await?;
164+
let cmd = format!("mkdir -p {lower_path}");
165+
let result = runtime.run_as_root(sandbox_name, &cmd, false).await?;
179166
if result.exit_code != 0 {
180167
eprintln!(
181168
"Warning: failed to create dir {}: {}",
@@ -194,16 +181,12 @@ pub async fn commit(
194181
.unwrap_or_default()
195182
);
196183
if !parent.is_empty() && parent != LOWER {
197-
let cmd = format!("{sudo}mkdir -p {parent}");
198-
let _ = runtime
199-
.exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false)
200-
.await;
184+
let cmd = format!("mkdir -p {parent}");
185+
let _ = runtime.run_as_root(sandbox_name, &cmd, false).await;
201186
}
202187

203-
let cmd = format!("{sudo}cp -a {upper_path} {lower_path}");
204-
let result = runtime
205-
.exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false)
206-
.await?;
188+
let cmd = format!("cp -a {upper_path} {lower_path}");
189+
let result = runtime.run_as_root(sandbox_name, &cmd, false).await?;
207190
if result.exit_code != 0 {
208191
eprintln!(
209192
"Warning: failed to commit {}: {}",
@@ -215,10 +198,8 @@ pub async fn commit(
215198
}
216199
}
217200
ChangeStatus::Deleted => {
218-
let cmd = format!("{sudo}rm -rf {lower_path}");
219-
let result = runtime
220-
.exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false)
221-
.await?;
201+
let cmd = format!("rm -rf {lower_path}");
202+
let result = runtime.run_as_root(sandbox_name, &cmd, false).await?;
222203
if result.exit_code != 0 {
223204
eprintln!(
224205
"Warning: failed to delete {}: {}",
@@ -250,15 +231,12 @@ pub async fn discard(
250231
sandbox_name: &str,
251232
paths: Option<&[String]>,
252233
) -> Result<usize> {
253-
let sudo = runtime.sudo_prefix();
254234
if let Some(filter_paths) = paths {
255235
let mut discarded = 0;
256236
for path in filter_paths {
257237
let upper_path = format!("{UPPER}/{}", path.trim_start_matches('/'));
258-
let cmd = format!("{sudo}rm -rf {upper_path}");
259-
let result = runtime
260-
.exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false)
261-
.await?;
238+
let cmd = format!("rm -rf {upper_path}");
239+
let result = runtime.run_as_root(sandbox_name, &cmd, false).await?;
262240
if result.exit_code == 0 {
263241
println!(" Discarded: {path}");
264242
discarded += 1;
@@ -270,17 +248,8 @@ pub async fn discard(
270248
Ok(discarded)
271249
} else {
272250
// Clear entire upper layer
273-
let result = runtime
274-
.exec_cmd(
275-
sandbox_name,
276-
&[
277-
"bash",
278-
"-lc",
279-
&format!("{sudo}rm -rf {UPPER}/* {UPPER}/.[!.]* 2>/dev/null; true"),
280-
],
281-
false,
282-
)
283-
.await?;
251+
let cmd = format!("rm -rf {UPPER}/* {UPPER}/.[!.]* 2>/dev/null; true");
252+
let result = runtime.run_as_root(sandbox_name, &cmd, false).await?;
284253

285254
if result.exit_code != 0 {
286255
bail!("Failed to clear overlay: {}", result.stderr.trim());
@@ -298,23 +267,17 @@ pub async fn stash(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()> {
298267
bail!("A stash already exists. Pop or discard it first (`devbox layer stash-pop`).");
299268
}
300269

301-
let sudo = runtime.sudo_prefix();
302-
303270
// Move upper to stash
304-
let cmd = format!("{sudo}mv {UPPER} {STASH_DIR}");
305-
let result = runtime
306-
.exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false)
307-
.await?;
271+
let cmd = format!("mv {UPPER} {STASH_DIR}");
272+
let result = runtime.run_as_root(sandbox_name, &cmd, false).await?;
308273

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

313278
// Recreate empty upper directory
314-
let cmd = format!("{sudo}mkdir -p {UPPER}");
315-
let result = runtime
316-
.exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false)
317-
.await?;
279+
let cmd = format!("mkdir -p {UPPER}");
280+
let result = runtime.run_as_root(sandbox_name, &cmd, false).await?;
318281

319282
if result.exit_code != 0 {
320283
bail!(
@@ -333,25 +296,19 @@ pub async fn stash_pop(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()>
333296
bail!("No stash found. Nothing to pop.");
334297
}
335298

336-
let sudo = runtime.sudo_prefix();
337-
338299
// Merge stash back into upper (copy hidden and regular files)
339300
let merge_cmd = format!(
340-
"{sudo}cp -a {STASH_DIR}/* {UPPER}/ 2>/dev/null; {sudo}cp -a {STASH_DIR}/.[!.]* {UPPER}/ 2>/dev/null; true"
301+
"cp -a {STASH_DIR}/* {UPPER}/ 2>/dev/null; cp -a {STASH_DIR}/.[!.]* {UPPER}/ 2>/dev/null; true"
341302
);
342-
let result = runtime
343-
.exec_cmd(sandbox_name, &["bash", "-lc", &merge_cmd], false)
344-
.await?;
303+
let result = runtime.run_as_root(sandbox_name, &merge_cmd, false).await?;
345304

346305
if result.exit_code != 0 {
347306
bail!("Failed to restore stash: {}", result.stderr.trim());
348307
}
349308

350309
// Remove the stash directory
351-
let cmd = format!("{sudo}rm -rf {STASH_DIR}");
352-
let result = runtime
353-
.exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false)
354-
.await?;
310+
let cmd = format!("rm -rf {STASH_DIR}");
311+
let result = runtime.run_as_root(sandbox_name, &cmd, false).await?;
355312

356313
if result.exit_code != 0 {
357314
bail!("Failed to clean up stash: {}", result.stderr.trim());
@@ -378,16 +335,9 @@ pub async fn has_stash(runtime: &dyn Runtime, sandbox_name: &str) -> Result<bool
378335
/// Newer kernels don't allow `mount -o remount` on OverlayFS, so we
379336
/// unmount and remount with the same options instead.
380337
pub async fn refresh(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()> {
381-
let sudo = runtime.sudo_prefix();
382-
383338
// Try simple remount first (works on older kernels)
384-
let result = runtime
385-
.exec_cmd(
386-
sandbox_name,
387-
&["bash", "-lc", &format!("{sudo}mount -o remount {WORKSPACE}")],
388-
false,
389-
)
390-
.await?;
339+
let cmd = format!("mount -o remount {WORKSPACE}");
340+
let result = runtime.run_as_root(sandbox_name, &cmd, false).await?;
391341

392342
if result.exit_code == 0 {
393343
println!("Overlay refreshed — host changes are now visible.");
@@ -397,12 +347,10 @@ pub async fn refresh(runtime: &dyn Runtime, sandbox_name: &str) -> Result<()> {
397347
// Remount not supported — unmount and remount manually.
398348
// The upper layer is on disk, so nothing is lost.
399349
let remount_cmd = format!(
400-
"{sudo}umount {WORKSPACE} && {sudo}mount -t overlay overlay \
350+
"umount {WORKSPACE} && mount -t overlay overlay \
401351
-o lowerdir={LOWER},upperdir={UPPER},workdir={WORK} {WORKSPACE}"
402352
);
403-
let result = runtime
404-
.exec_cmd(sandbox_name, &["bash", "-lc", &remount_cmd], false)
405-
.await?;
353+
let result = runtime.run_as_root(sandbox_name, &remount_cmd, false).await?;
406354

407355
if result.exit_code != 0 {
408356
bail!("Failed to refresh overlay: {}", result.stderr.trim());
@@ -468,14 +416,11 @@ pub async fn lower_layer_changes(runtime: &dyn Runtime, sandbox_name: &str) -> R
468416
// Compare the lower layer mtime against a timestamp file we create on mount.
469417
// If no timestamp exists, we can't detect changes — just check for stale handles.
470418
// Simpler approach: find files in lower newer than the overlay work dir (created at mount time).
471-
let sudo = runtime.sudo_prefix();
472419
let cmd = format!(
473-
"{sudo}find {} -newer {} -not -path {} -type f -printf '%P\\n' 2>/dev/null | head -50",
420+
"find {} -newer {} -not -path {} -type f -printf '%P\\n' 2>/dev/null | head -50",
474421
LOWER, WORK, LOWER
475422
);
476-
let result = runtime
477-
.exec_cmd(sandbox_name, &["bash", "-lc", &cmd], false)
478-
.await?;
423+
let result = runtime.run_as_root(sandbox_name, &cmd, false).await?;
479424

480425
if result.exit_code != 0 {
481426
return Ok(vec![]);

0 commit comments

Comments
 (0)