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
12 changes: 11 additions & 1 deletion docs/template-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Every field is optional except `schema` and `name`.
| `schema` | `string` | **yes** | Must be `dxon/v1` |
| `name` | `string` | **yes** | Short identifier shown in the registry, no spaces |
| `description` | `string` | no | One-line description of the environment |
| `base` | `string` | no | Suggested base distribution: `arch`, `debian`, or `alpine` |
| `base` | `string` | no | Pinned base distribution for this template: `arch`, `debian`, or `alpine` |
| `packages` | `map<distro, list<string>>` | no | Per-distro package lists installed before steps run |
| `env` | `map<string, string>` | no | Environment variables set inside the container |
| `run` | `list<string>` | no | Commands run after all steps complete |
Expand All @@ -44,6 +44,16 @@ dXon selects the list matching the chosen distribution and installs those packag

Supported keys: `arch`, `debian`, `alpine`.

If your template is intentionally distro-specific, set `base` and only define that distro key in `packages`.

```yaml
base: debian
packages:
debian: [curl, git, ca-certificates]
```

When `base` is set, `dxon create --template ...` automatically uses that distro.

---

## `env` — environment variables
Expand Down
12 changes: 0 additions & 12 deletions install.sh
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
#!/usr/bin/env sh
# dXon install script
# Usage: curl -sSfL https://raw.githubusercontent.com/P8labs/dxon/master/install.sh | sh
# Or: wget -qO- https://raw.githubusercontent.com/P8labs/dxon/master/install.sh | sh

set -e

REPO="P8labs/dxon"
BINARY="dxon"
INSTALL_DIR="${DXON_INSTALL_DIR:-/usr/local/bin}"

# ── helpers ────────────────────────────────────────────────────────────────────

info() { printf '\033[0;34m info\033[0m %s\n' "$*"; }
ok() { printf '\033[0;32m ok\033[0m %s\n' "$*"; }
Expand All @@ -22,7 +18,6 @@ need_cmd() {
fi
}

# ── detect OS ──────────────────────────────────────────────────────────────────

detect_os() {
OS="$(uname -s 2>/dev/null | tr '[:upper:]' '[:lower:]')"
Expand All @@ -33,7 +28,6 @@ detect_os() {
esac
}

# ── detect architecture ────────────────────────────────────────────────────────

detect_arch() {
ARCH="$(uname -m 2>/dev/null)"
Expand All @@ -45,7 +39,6 @@ detect_arch() {
esac
}

# ── fetch latest release tag ───────────────────────────────────────────────────

latest_version() {
if command -v curl >/dev/null 2>&1; then
Expand All @@ -63,7 +56,6 @@ latest_version() {
fi
}

# ── download binary ────────────────────────────────────────────────────────────

download() {
ASSET="${BINARY}-${VERSION}-${OS}-${ARCH}"
Expand All @@ -86,7 +78,6 @@ download() {
echo "$TMP"
}

# ── install binary ───────────────────────────────────────────download/v0.3.0/dxon-linux-x86_64──────────────────

install_bin() {
TMPFILE="$1"
Expand All @@ -106,7 +97,6 @@ install_bin() {
fi
}

# ── check systemd-nspawn ────────────────────────────────────────────────────────

check_nspawn() {
if ! command -v systemd-nspawn >/dev/null 2>&1; then
Expand All @@ -120,13 +110,11 @@ check_nspawn() {
fi
}

# ── main ───────────────────────────────────────────────────────────────────────

main() {
detect_os
detect_arch

# Allow pinning a version via env var
if [ -n "$DXON_VERSION" ]; then
VERSION="$DXON_VERSION"
else
Expand Down
3 changes: 0 additions & 3 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,8 @@ pub enum Commands {
packages: Vec<String>,
#[arg(long, short = 'y')]
trust: bool,
/// Shell to install: bash, zsh, fish.
#[arg(long, value_name = "SHELL")]
shell: Option<String>,
/// Copy or bind-mount host shell config into the container.
/// Valid values: copy, bind.
#[arg(long, value_name = "MODE")]
shell_config: Option<String>,
},
Expand Down
84 changes: 43 additions & 41 deletions src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,36 +46,6 @@ pub fn run(store: &ContainerStore, cfg: &mut Config, args: CreateArgs) -> Result
return Err(DxonError::ContainerExists(name).into());
}

let distro_choices = &["arch", "debian", "alpine"];

let distro_str: String = match args.distro {
Some(d) => d,
None => {
let default_idx = cfg
.default_distro
.as_deref()
.and_then(|d| distro_choices.iter().position(|&c| c == d))
.unwrap_or(0);

let idx = Select::with_theme(&theme)
.with_prompt("Base distribution")
.items(distro_choices)
.default(default_idx)
.interact()?;
distro_choices[idx].to_string()
}
};
let distro = Distro::parse(&distro_str)?;

let (bootstrap_tool, hint) = host.bootstrap_tool_for(&distro_str);
if !user::command_available(bootstrap_tool) {
return Err(DxonError::MissingTool {
tool: bootstrap_tool.to_string(),
hint: format!("on {}: {hint}", host.pretty_name),
}
.into());
}

let (tmpl, tmpl_name): (Option<DxTemplate>, Option<String>) = match args.template {
Some(ref t) => {
let (loaded, source) = template::resolve(t, cfg.effective_registry_url())?;
Expand Down Expand Up @@ -112,6 +82,49 @@ pub fn run(store: &ContainerStore, cfg: &mut Config, args: CreateArgs) -> Result
}
};

let distro_choices = &["arch", "debian", "alpine"];
let template_distro = tmpl.as_ref().and_then(|t| t.pinned_distro());

let distro_str: String = match (args.distro, template_distro) {
(Some(requested), Some(pinned)) => {
if requested != pinned {
anyhow::bail!(
"template '{}' is pinned to distro '{}' but '--distro {}' was provided",
tmpl_name.as_deref().unwrap_or("<template>"),
pinned,
requested
);
}
requested
}
(Some(requested), None) => requested,
(None, Some(pinned)) => pinned.to_string(),
(None, None) => {
let default_idx = cfg
.default_distro
.as_deref()
.and_then(|d| distro_choices.iter().position(|&c| c == d))
.unwrap_or(0);

let idx = Select::with_theme(&theme)
.with_prompt("Base distribution")
.items(distro_choices)
.default(default_idx)
.interact()?;
distro_choices[idx].to_string()
}
};
let distro = Distro::parse(&distro_str)?;

let (bootstrap_tool, hint) = host.bootstrap_tool_for(&distro_str);
if !user::command_available(bootstrap_tool) {
return Err(DxonError::MissingTool {
tool: bootstrap_tool.to_string(),
hint: format!("on {}: {hint}", host.pretty_name),
}
.into());
}

let mut answers: HashMap<String, String> = HashMap::new();
if let Some(ref t) = tmpl {
for prompt in &t.prompts {
Expand Down Expand Up @@ -284,7 +297,6 @@ pub fn run(store: &ContainerStore, cfg: &mut Config, args: CreateArgs) -> Result
let host_home = user::resolve_home();
let shell_name = chosen_shell.as_deref().unwrap_or("bash");

// Resolve the container user's home directory (e.g. /home/priyanshu or /root).
let container_home_abs = if host_user.uid == 0 {
std::path::PathBuf::from("/root")
} else {
Expand Down Expand Up @@ -394,8 +406,6 @@ fn provision(
println!("{} bootstrapping {} rootfs…", "→".cyan(), distro_str.bold());
bootstrap(distro, rootfs)?;

// Create a matching user/group inside the container so that file
// ownership inside /workspace matches the host user.
ensure_container_user(rootfs, &host_user.username, host_user.uid, host_user.gid)?;

let mut installed_packages: Vec<String> = Vec::new();
Expand Down Expand Up @@ -466,13 +476,11 @@ fn provision(

let shell_path = format!("/bin/{shell}");
let shell_bin = format!("/usr/{shell}");
// Set the default shell for root.
let chsh_root = format!(
"chsh -s {shell_path} root 2>/dev/null || chsh -s {shell_bin} root 2>/dev/null || true"
);
let _ = run_command(rootfs, &chsh_root, &HashMap::new());

// Also set the default shell for the mapped container user.
if host_user.uid != 0 {
let chsh_user = format!(
"chsh -s {shell_path} {username} 2>/dev/null || chsh -s {shell_bin} {username} 2>/dev/null || true",
Expand All @@ -482,13 +490,9 @@ fn provision(
}
}

// Create /workspace and assign it to the mapped user so the host user
// can write project files without permission errors.
run_command(rootfs, "mkdir -p /workspace", &HashMap::new())?;

let final_repo = if let Some(url) = repo {
// Derive the target directory name from the URL, stripping a trailing
// ".git" suffix if present (e.g. "https://github.com/foo/bar.git" → "bar").
let repo_name = url
.trim_end_matches('/')
.rsplit('/')
Expand All @@ -513,8 +517,6 @@ fn provision(
None
};

// Recursively chown /workspace to the mapped UID/GID (covers both the
// empty directory case and any files placed there by git clone above).
let chown_cmd = format!("chown -R {}:{} /workspace", host_user.uid, host_user.gid);
let _ = run_command(rootfs, &chown_cmd, &HashMap::new());

Expand Down
12 changes: 0 additions & 12 deletions src/commands/enter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,23 +290,13 @@ fn sanitize_workspace_subpath(raw: &str) -> Result<PathBuf> {
Ok(out)
}

/// Remove a stale `/run/systemd/nspawn/unix-export/<machine_name>` mount point
/// left behind by a container that exited uncleanly (crash, OOM, SIGKILL, etc.).
///
/// When systemd-nspawn starts with `--machine=<name>` it bind-mounts a socket
/// under that path. Normally nspawn cleans it up on exit, but after an unclean
/// shutdown the mount point persists and the next `systemd-nspawn --machine=<name>`
/// invocation refuses to start with "Mount point … exists already, refusing."
///
/// This is only called after confirming the machine is NOT currently running.
fn cleanup_stale_nspawn_machine(machine_name: &str) {
let socket_path = format!("/run/systemd/nspawn/unix-export/{machine_name}");

if !std::path::Path::new(&socket_path).exists() {
return;
}

// 1. Ask machinectl to terminate — it knows how to clean up registered machines.
if crate::user::command_available("machinectl") {
let _ = crate::user::privileged_command("machinectl")
.arg("terminate")
Expand All @@ -316,7 +306,6 @@ fn cleanup_stale_nspawn_machine(machine_name: &str) {
.status();
}

// 2. If still present it is likely a stale bind-mount; unmount it lazily.
if std::path::Path::new(&socket_path).exists() {
let _ = crate::user::privileged_command("umount")
.arg("--lazy")
Expand All @@ -326,7 +315,6 @@ fn cleanup_stale_nspawn_machine(machine_name: &str) {
.status();
}

// 3. Last resort: plain removal (works when the path is only a dead socket file).
if std::path::Path::new(&socket_path).exists() {
let _ = crate::user::privileged_command("rm")
.arg("-f")
Expand Down
1 change: 0 additions & 1 deletion src/commands/open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,6 @@ mod tests {
fs::create_dir_all(&vscode).unwrap();

let jsonc = r#"{
// User preference
"editor.tabSize": 4
}"#;
fs::write(vscode.join("settings.json"), jsonc).unwrap();
Expand Down
4 changes: 0 additions & 4 deletions src/container/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,12 @@ pub struct ContainerConfig {
pub shell: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shell_config_mode: Option<String>,
/// Username mapped from the host into the container (e.g. "priyanshu").
#[serde(default, skip_serializing_if = "Option::is_none")]
pub container_user: Option<String>,
/// UID of the mapped user inside the container.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub container_uid: Option<u32>,
/// GID of the mapped user inside the container.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub container_gid: Option<u32>,
/// Default working directory when entering the container (e.g. "/workspace").
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workspace_dir: Option<String>,
}
Expand Down
8 changes: 0 additions & 8 deletions src/runtime/nspawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,18 +138,13 @@ fn prompt_package_failure(
}
}

/// Ensure a user with the given `username`, `uid`, and `gid` exists inside the
/// container rootfs, creating the group and user if necessary.
///
/// This is a no-op when `uid == 0` (root always exists).
pub fn ensure_container_user(rootfs: &Path, username: &str, uid: u32, gid: u32) -> Result<()> {
if uid == 0 {
return Ok(());
}

let env = HashMap::new();

// Check whether the user already exists.
let check_cmd = format!("id {username} >/dev/null 2>&1");
if run_command(rootfs, &check_cmd, &env).is_ok() {
return Ok(());
Expand All @@ -161,14 +156,11 @@ pub fn ensure_container_user(rootfs: &Path, username: &str, uid: u32, gid: u32)
username.bold()
);

// Create the group if it doesn't already exist (gracefully ignore errors
// from distros where the group was created as part of an earlier step).
let group_cmd = format!(
"getent group {gid} >/dev/null 2>&1 || groupadd -g {gid} {username} 2>/dev/null || true"
);
let _ = run_command(rootfs, &group_cmd, &env);

// Create the user with a matching UID/GID and a real home directory.
let user_cmd = format!("useradd -u {uid} -g {gid} -m -s /bin/sh {username}");
run_command(rootfs, &user_cmd, &env)?;

Expand Down
9 changes: 0 additions & 9 deletions src/shell_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,6 @@ fn config_files_for(shell: &str, host_home: &Path) -> Vec<(PathBuf, PathBuf)> {
.collect()
}

/// Copy shell config files from the host into the container.
///
/// `container_home_abs` is the absolute path of the user's home directory
/// *inside* the container, e.g. `/home/priyanshu` or `/root`.
pub fn apply_copy(
rootfs: &Path,
host_home: &Path,
Expand Down Expand Up @@ -196,11 +192,6 @@ fn copy_file_with_path_sub(
user::privileged_write(dst, &content)
}

/// Build `--bind=` argument strings for systemd-nspawn to live-mount shell
/// config files from the host into `container_home_abs` inside the container.
///
/// `container_home_abs` is the absolute path of the user's home directory
/// *inside* the container, e.g. `/home/priyanshu` or `/root`.
pub fn bind_args(host_home: &Path, shell: &str, container_home_abs: &Path) -> Vec<String> {
let files = config_files_for(shell, host_home);

Expand Down
Loading