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
51 changes: 48 additions & 3 deletions src/runtime/incus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,33 @@ impl IncusRuntime {
println!("Image '{alias}' imported successfully.");
Ok(())
}
/// Detect the UID of the first non-root user in the VM.
async fn detect_vm_uid(vm: &str) -> Option<String> {
// Find first user with UID >= 1000 (standard non-system user)
let result = run_cmd(
"incus",
&["exec", vm, "--", "bash", "-c",
"awk -F: '$3 >= 1000 && $3 < 65534 { print $3; exit }' /etc/passwd"],
).await.ok()?;
let uid = result.stdout.trim().to_string();
if uid.is_empty() { None } else { Some(uid) }
}

/// Detect the HOME directory for a given UID in the VM.
async fn detect_vm_home(vm: &str, uid: &str) -> String {
let result = run_cmd(
"incus",
&["exec", vm, "--", "bash", "-c",
&format!("getent passwd {uid} | cut -d: -f6")],
).await;
match result {
Ok(r) if !r.stdout.trim().is_empty() => {
format!("HOME={}", r.stdout.trim())
}
_ => format!("HOME=/home/dev"),
}
}

/// Wait for the Incus VM agent to become ready (up to 120 seconds).
/// The agent starts after the guest OS boots and runs incus-agent.
async fn wait_for_agent(vm: &str) -> Result<()> {
Expand Down Expand Up @@ -190,9 +217,27 @@ impl Runtime for IncusRuntime {
let vm = Self::vm_name(name);

if interactive {
let mut args = vec!["exec", &vm, "--"];
args.extend_from_slice(cmd);
run_interactive("incus", &args).await
// For interactive sessions (shell attach, etc.), run as the
// non-root user with correct HOME and working directory.
let uid_str = Self::detect_vm_uid(&vm).await.unwrap_or("1000".to_string());
let home_env = Self::detect_vm_home(&vm, &uid_str).await;

let mut args = vec![
"exec".to_string(),
vm,
"--user".to_string(),
uid_str,
"--cwd".to_string(),
"/workspace".to_string(),
"--env".to_string(),
home_env,
"--".to_string(),
];
for c in cmd {
args.push(c.to_string());
}
let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
run_interactive("incus", &arg_refs).await
} else {
let mut args = vec!["exec", &vm, "--"];
args.extend_from_slice(cmd);
Expand Down
18 changes: 13 additions & 5 deletions src/sandbox/provision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,10 +291,16 @@ async fn provision_nixos(
}

// 8. Run nixos-rebuild switch (interactive so user sees progress)
// We must set NIX_PATH explicitly because:
// - incus exec doesn't source /etc/profile (no login shell)
// - images:nixos/* may not have channels in the default NIX_PATH
// - After nix-channel --update, nixpkgs lives at the channel profile path
println!("Installing packages via nixos-rebuild (this may take a few minutes)...");
let rebuild_cmd = format!(
"{NIXOS_PATH_PREFIX}\
export NIX_PATH=\"nixos-config=/etc/nixos/configuration.nix:$NIX_PATH\" && \
export NIX_PATH=\"nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos:\
nixos-config=/etc/nixos/configuration.nix:\
/nix/var/nix/profiles/per-user/root/channels\" && \
export NIXPKGS_ALLOW_UNFREE=1 && \
sudo -E nixos-rebuild switch"
);
Expand Down Expand Up @@ -1010,16 +1016,18 @@ async fn wait_for_network(runtime: &dyn Runtime, name: &str) -> Result<()> {
/// `nixos-rebuild` to fail with "file 'nixpkgs/nixos' was not found in the Nix search path".
/// We check if the nixos channel exists and add it if missing.
async fn ensure_nixos_channel(runtime: &dyn Runtime, name: &str) -> Result<()> {
// Check if nixpkgs is already in NIX_PATH
// Check if the nixos channel is already available for root.
// We check the channel profile path directly since NIX_PATH may not be set
// in the non-login incus exec shell.
let check = run_in_vm(
runtime,
name,
"nix-instantiate --eval -E '<nixpkgs>' 2>/dev/null && echo found",
"test -d /nix/var/nix/profiles/per-user/root/channels/nixos && echo found",
false,
)
.await?;

if check.stdout.contains("found") {
if check.stdout.trim() == "found" {
return Ok(());
}

Expand All @@ -1028,7 +1036,7 @@ async fn ensure_nixos_channel(runtime: &dyn Runtime, name: &str) -> Result<()> {
"sudo nix-channel --add https://nixos.org/channels/nixos-25.05 nixos && ",
"sudo nix-channel --update"
);
let result = run_in_vm(runtime, name, channel_cmd, false).await?;
let result = run_in_vm(runtime, name, channel_cmd, true).await?;
if result.exit_code != 0 {
eprintln!(
"Warning: failed to set up NixOS channel: {}",
Expand Down
Loading