diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6c0315934..f1faa339b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -13,7 +13,7 @@ ArcBox is a pure-Rust, high-performance container and VM runtime targeting macOS **Key layers (bottom-up):** ```text -arcbox (CLI) +abctl (CLI) ↓ gRPC over Unix socket arcbox-daemon ↓ diff --git a/app/AGENTS.md b/app/AGENTS.md index 2c1eb38eb..bba3a461a 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -6,9 +6,7 @@ Covers `arcbox-daemon` (startup/shutdown), `arcbox-core` (`vm_lifecycle`), `docs/daemon-lifecycle.md` (lock/handoff, residual-state tables) and `docs/data-directories.md` (filesystem paths) — point there, don't restate. -`arcbox-cli` ships two binaries: `abctl` (the real CLI) and `arcbox` (a -deprecated shim that `exec`s `abctl`, pending removal). User-facing strings -must name `abctl`. +`arcbox-cli` ships one binary, `abctl`. User-facing strings must name it. ## Startup & readiness contract diff --git a/app/arcbox-cli/Cargo.toml b/app/arcbox-cli/Cargo.toml index e7151cb2a..18999f2ae 100644 --- a/app/arcbox-cli/Cargo.toml +++ b/app/arcbox-cli/Cargo.toml @@ -16,10 +16,6 @@ path = "src/lib.rs" name = "abctl" path = "src/main.rs" -[[bin]] -name = "arcbox" -path = "src/bin/arcbox_placeholder.rs" - [dependencies] arcbox-core = { workspace = true } arcbox-docker = { workspace = true } diff --git a/app/arcbox-cli/src/bin/arcbox_placeholder.rs b/app/arcbox-cli/src/bin/arcbox_placeholder.rs deleted file mode 100644 index 1d724d89b..000000000 --- a/app/arcbox-cli/src/bin/arcbox_placeholder.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Placeholder binary that redirects users to `abctl`. -//! -//! If arguments are provided, attempts to exec `abctl` transparently so that -//! existing scripts and muscle-memory keep working. - -fn main() { - let args: Vec = std::env::args().skip(1).collect(); - - if args.is_empty() { - eprintln!("The ArcBox CLI has been renamed to `abctl`. Please use `abctl` instead."); - std::process::exit(0); - } - - // Try to exec `abctl` from the same directory as this binary. - let exe = std::env::current_exe().ok(); - let abctl_path = exe - .as_ref() - .and_then(|p| p.parent()) - .map(|dir| dir.join("abctl")); - - if let Some(ref path) = abctl_path { - if path.exists() { - exec_abctl(path, &args); - } - } - - // Fallback: try `abctl` from PATH. - exec_abctl(std::path::Path::new("abctl"), &args); -} - -#[cfg(unix)] -fn exec_abctl(path: &std::path::Path, args: &[String]) -> ! { - use std::os::unix::process::CommandExt; - // exec replaces the current process; on success this never returns. - let err = std::process::Command::new(path).args(args).exec(); - eprintln!("Failed to exec abctl: {err}"); - eprintln!( - "The ArcBox CLI has been renamed to `abctl`. Please install it or add it to your PATH." - ); - std::process::exit(1); -} - -#[cfg(not(unix))] -fn exec_abctl(path: &std::path::Path, args: &[String]) -> ! { - match std::process::Command::new(path).args(args).status() { - Ok(status) => std::process::exit(status.code().unwrap_or(1)), - Err(err) => { - eprintln!("Failed to run abctl: {err}"); - eprintln!( - "The ArcBox CLI has been renamed to `abctl`. Please install it or add it to your PATH." - ); - std::process::exit(1); - } - } -} diff --git a/app/arcbox-cli/src/commands/setup.rs b/app/arcbox-cli/src/commands/setup.rs index 2b965214d..918cb750e 100644 --- a/app/arcbox-cli/src/commands/setup.rs +++ b/app/arcbox-cli/src/commands/setup.rs @@ -124,20 +124,14 @@ async fn install(format: OutputFormat) -> Result<()> { tokio::fs::create_dir_all(comp.join("fish")).await?; // 2. Symlink current executable → ~/.arcbox/bin/abctl (primary). - // Also create ~/.arcbox/bin/arcbox → placeholder for backwards compat. let exe = std::env::current_exe().context("could not determine current executable path")?; - let exe_dir = exe - .parent() - .context("could not determine executable directory")?; let symlink_path = bin.join("abctl"); create_or_update_symlink(&exe, &symlink_path).await?; - // The placeholder binary lives next to the main binary. - let placeholder_exe = exe_dir.join("arcbox"); - let placeholder_symlink = bin.join("arcbox"); - if placeholder_exe.exists() { - create_or_update_symlink(&placeholder_exe, &placeholder_symlink).await?; - } + // Older installs linked ~/.arcbox/bin/arcbox to the rename shim, which no + // longer ships — the link would dangle on PATH. Drop it here rather than in + // `uninstall`, so an upgrade heals without the user removing anything. + remove_stale_shim_link(&bin.join("arcbox")).await; // 2b. Symlink Docker CLI tools → ~/.arcbox/bin/ if available. // Tools may be in the app bundle (xbin/) or ~/.arcbox/runtime/bin/. @@ -768,6 +762,19 @@ async fn create_or_update_symlink(target: &Path, link: &Path) -> Result<()> { Ok(()) } +/// Remove the retired `arcbox` shim link left by an older `setup install`. +/// +/// Only symlinks are touched: this directory holds nothing but links we +/// created, so a regular file here is the user's and is left alone. +async fn remove_stale_shim_link(link: &Path) { + let Ok(meta) = tokio::fs::symlink_metadata(link).await else { + return; + }; + if meta.file_type().is_symlink() { + let _ = tokio::fs::remove_file(link).await; + } +} + /// Remove a directory if it exists, ignoring errors. async fn remove_dir_if_exists(path: &Path) { let _ = tokio::fs::remove_dir_all(path).await; @@ -777,6 +784,28 @@ async fn remove_dir_if_exists(path: &Path) { mod tests { use super::*; + #[tokio::test] + async fn stale_shim_link_is_removed_but_a_real_file_is_kept() { + let dir = tempfile::tempdir().unwrap(); + + // A dangling link to the retired shim: the upgrade case. + let link = dir.path().join("arcbox"); + tokio::fs::symlink(dir.path().join("gone"), &link) + .await + .unwrap(); + remove_stale_shim_link(&link).await; + assert!(tokio::fs::symlink_metadata(&link).await.is_err()); + + // A regular file of the same name is not ours to delete. + let file = dir.path().join("arcbox-file"); + tokio::fs::write(&file, b"user data").await.unwrap(); + remove_stale_shim_link(&file).await; + assert_eq!(tokio::fs::read(&file).await.unwrap(), b"user data"); + + // Absent path is a no-op, not an error. + remove_stale_shim_link(&dir.path().join("absent")).await; + } + #[test] fn detect_zsh_from_env() { // detect_shell() reads $SHELL — just verify the function doesn't panic. diff --git a/app/arcbox-daemon/src/nfs_mount.rs b/app/arcbox-daemon/src/nfs_mount.rs index efb496d1e..affde601e 100644 --- a/app/arcbox-daemon/src/nfs_mount.rs +++ b/app/arcbox-daemon/src/nfs_mount.rs @@ -111,8 +111,8 @@ async fn reconcile( // The trigger is the lifecycle *ready* edge, never the restart generation: // that counter is bumped when the VM stops, so acting on it would send the // request into the gap where no guest exists and burn the retry budget on a - // VM that is still booting — or, after a plain `arcbox stop`, on one that is - // not coming back until the next on-demand start. + // VM that is still booting — or, after a plain `abctl machine stop`, on + // one that is not coming back until the next on-demand start. let mut state = runtime.subscribe_system_vm_state(); let mut first = true; diff --git a/docs/data-directories.md b/docs/data-directories.md index 6de60f099..d8c4d7315 100644 --- a/docs/data-directories.md +++ b/docs/data-directories.md @@ -92,7 +92,6 @@ execution. | Path | Purpose | Creator | |------|---------|---------| | `bin/abctl` | CLI symlink | cli (`abctl setup install`) | -| `bin/arcbox` | Compatibility symlink → abctl | cli (`abctl setup install`) | | `bin/arcbox-daemon` | Fallback daemon binary path | cli | | `bin/arcbox-agent` | Guest agent binary | daemon (bundle seed / boot cache) | | `bin/vm-agent` | Sandbox microVM init binary (guest sees `/arcbox/bin/vm-agent`) | daemon (bundle seed / boot cache) |