arcbox-constants 0.4.13

Shared protocol and runtime constants for ArcBox
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
/// Guest mount point for dockerd persistent state (`/var/lib/docker`).
///
/// Backed by the Btrfs `@docker` subvolume on `/dev/vdb`.
pub const DOCKER_DATA_MOUNT_POINT: &str = "/var/lib/docker";

/// Guest mount point for containerd persistent state (`/var/lib/containerd`).
///
/// Backed by the Btrfs `@containerd` subvolume on `/dev/vdb`.
pub const CONTAINERD_DATA_MOUNT_POINT: &str = "/var/lib/containerd";

/// Guest mount point for K3s persistent state (`/var/lib/rancher/k3s`).
pub const K3S_DATA_MOUNT_POINT: &str = "/var/lib/rancher/k3s";

/// Guest mount point for kubelet persistent state (`/var/lib/kubelet`).
pub const KUBELET_DATA_MOUNT_POINT: &str = "/var/lib/kubelet";

/// Guest mount point for CNI persistent state (`/var/lib/cni`).
pub const CNI_DATA_MOUNT_POINT: &str = "/var/lib/cni";

/// Docker Engine API Unix socket path in guest.
pub const DOCKER_API_UNIX_SOCKET: &str = "/var/run/docker.sock";

/// containerd gRPC socket path in guest.
pub const CONTAINERD_SOCKET: &str = "/run/containerd/containerd.sock";

/// K3s-generated kubeconfig path inside the guest.
pub const K3S_KUBECONFIG_PATH: &str = "/var/lib/rancher/k3s/k3s.yaml";

/// K3s-managed CNI config directory for the kubelet/containerd stack.
pub const K3S_CNI_CONF_DIR: &str = "/var/lib/rancher/k3s/agent/etc/cni/net.d";

/// K3s-managed CNI plugin directory used by current releases.
pub const K3S_CNI_BIN_DIR: &str = "/var/lib/rancher/k3s/data/cni";

/// Directory where runtime binaries (containerd, dockerd, runc, …) are
/// accessed via VirtioFS live execution.
pub const ARCBOX_RUNTIME_BIN_DIR: &str = "/arcbox/runtime/bin";

/// Host-side privileged paths (require root to write).
pub mod privileged {
    /// Installed helper binary path.
    pub const HELPER_BINARY: &str = "/usr/local/libexec/arcbox-helper";
    /// Helper launchd plist path.
    pub const HELPER_PLIST: &str = "/Library/LaunchDaemons/com.arcboxlabs.desktop.helper.plist";
    /// Helper socket-activation socket path.
    pub const HELPER_SOCKET: &str = "/var/run/arcbox-helper.sock";
    /// Docker socket symlink path.
    pub const DOCKER_SOCKET: &str = "/var/run/docker.sock";
}

/// launchd service labels.
pub mod labels {
    /// Daemon (user-level LaunchAgent).
    pub const DAEMON: &str = "com.arcboxlabs.desktop.daemon";
    /// Development daemon (user-level LaunchAgent).
    pub const DEVELOPMENT_DAEMON: &str = "com.arcboxlabs.desktop.dev.daemon";
    /// Helper (system-level LaunchDaemon).
    pub const HELPER: &str = "com.arcboxlabs.desktop.helper";
}

/// Runtime profile names and derived host identity.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ArcboxProfile {
    /// Production profile using `~/.arcbox` and the `arcbox` Docker context.
    #[default]
    Production,
    /// Development profile using `~/.arcbox-dev` and the `arcbox-dev` Docker context.
    Development,
}

impl ArcboxProfile {
    /// Returns the canonical profile name used in environment variables and CLIs.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Production => "production",
            Self::Development => "development",
        }
    }

    /// Returns the Docker context name for this profile.
    #[must_use]
    pub const fn docker_context_name(self) -> &'static str {
        match self {
            Self::Production => "arcbox",
            Self::Development => "arcbox-dev",
        }
    }

    /// Returns the launchd daemon label for this profile.
    #[must_use]
    pub const fn daemon_label(self) -> &'static str {
        match self {
            Self::Production => labels::DAEMON,
            Self::Development => labels::DEVELOPMENT_DAEMON,
        }
    }

    /// Returns the profile selected by `ARCBOX_PROFILE`, defaulting to production.
    #[cfg(feature = "std")]
    #[must_use]
    pub fn from_env_or_default() -> Self {
        std::env::var(crate::env::PROFILE)
            .ok()
            .and_then(|value| value.parse().ok())
            .unwrap_or_default()
    }

    /// Returns this profile's default data directory.
    #[cfg(feature = "std")]
    #[must_use]
    pub fn default_data_dir(self) -> std::path::PathBuf {
        dirs::home_dir().map_or_else(
            || std::path::PathBuf::from("/var/lib/arcbox"),
            |home| match self {
                Self::Production => home.join(".arcbox"),
                Self::Development => home.join(".arcbox-dev"),
            },
        )
    }
}

impl core::fmt::Display for ArcboxProfile {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str(self.as_str())
    }
}

impl core::str::FromStr for ArcboxProfile {
    type Err = String;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match value.trim().to_ascii_lowercase().as_str() {
            "production" | "prod" => Ok(Self::Production),
            "development" | "dev" => Ok(Self::Development),
            other => Err(format!(
                "unknown ArcBox profile '{other}' (expected production or development)"
            )),
        }
    }
}

/// Docker CLI tool names managed by the helper's cli_link.
pub const DOCKER_CLI_TOOLS: &[&str] = &[
    "docker",
    "docker-buildx",
    "docker-compose",
    "docker-credential-osxkeychain",
];

/// Subset of `DOCKER_CLI_TOOLS` that are Docker CLI plugins — binaries that
/// upstream `docker` discovers via its plugin mechanism and invokes as
/// `docker <name>` (e.g. `docker compose`, `docker buildx`).
///
/// Upstream Docker CLI searches these paths in order for plugin binaries:
///   1. entries listed in `cliPluginsExtraDirs` in `~/.docker/config.json`
///   2. `~/.docker/cli-plugins/`
///   3. `/usr/local/lib/docker/cli-plugins/`
///   4. `/usr/lib/docker/cli-plugins/`
///
/// Putting a plugin only in `$PATH` (as a standalone `docker-compose` binary)
/// is *not* enough — `docker compose` (with a space) will fail to find it.
/// Excludes the `docker` host binary itself and credential helpers (which
/// are discovered via a different, credsStore-based mechanism).
pub const DOCKER_CLI_PLUGINS: &[&str] = &["docker-buildx", "docker-compose"];

/// Returns true if a symlink target looks like it belongs to an ArcBox app bundle.
///
/// Used by multiple subsystems (privileged helper, brew hooks, setup install)
/// to decide whether an existing `/usr/local/bin/` symlink can be safely replaced.
///
/// Gated on the `std` feature: it takes a `std::path::Path`, so it is
/// unavailable (and unusable) in `no_std` builds.
#[cfg(feature = "std")]
pub fn is_arcbox_owned(target: &std::path::Path) -> bool {
    target
        .to_string_lossy()
        .contains(".app/Contents/MacOS/xbin/")
}

/// Host-side subdirectory names within the profile data directory.
pub mod host {
    /// Runtime state (sockets, PID files, ephemeral markers).
    pub const RUN: &str = "run";
    /// Centralized log directory.
    pub const LOG: &str = "log";
    /// Persistent data aggregation (images, containers, volumes, …).
    pub const DATA: &str = "data";

    /// Log file names (written by each component's tracing-appender).
    pub const DAEMON_LOG: &str = "daemon.log";
    pub const AGENT_LOG: &str = "agent.log";

    /// Default daemon lock file name (inside `RUN`).
    pub const DAEMON_LOCK: &str = "daemon.lock";
    /// Default Docker API socket name (inside `RUN`).
    pub const DOCKER_SOCKET: &str = "docker.sock";
    /// Default gRPC API socket name (inside `RUN`).
    pub const GRPC_SOCKET: &str = "arcbox.sock";
}

/// Resolved host-side directory layout.
///
/// Single source of truth for every path derived from the ArcBox data
/// directory (`~/.arcbox` by default). Both the daemon and CLI should
/// construct a `HostLayout` once and pass it around instead of
/// recalculating paths independently.
#[cfg(feature = "std")]
#[derive(Debug, Clone)]
pub struct HostLayout {
    /// Root data directory (e.g. `~/.arcbox`).
    pub data_dir: std::path::PathBuf,
    /// `<data_dir>/run` — sockets, PID file, lock.
    pub run_dir: std::path::PathBuf,
    /// `<data_dir>/log` — daemon and agent logs.
    pub log_dir: std::path::PathBuf,
    /// `<data_dir>/data` — persistent VM/container data.
    pub data_subdir: std::path::PathBuf,
    /// `<run_dir>/docker.sock`
    pub docker_socket: std::path::PathBuf,
    /// `<run_dir>/arcbox.sock`
    pub grpc_socket: std::path::PathBuf,
    /// `<run_dir>/daemon.lock`
    pub lock_file: std::path::PathBuf,
    /// `<log_dir>/daemon.log`
    pub daemon_log: std::path::PathBuf,
}

#[cfg(feature = "std")]
impl HostLayout {
    /// Build a layout from an explicit data directory.
    #[must_use]
    pub fn new(data_dir: std::path::PathBuf) -> Self {
        let run_dir = data_dir.join(host::RUN);
        let log_dir = data_dir.join(host::LOG);
        let data_subdir = data_dir.join(host::DATA);
        let docker_socket = run_dir.join(host::DOCKER_SOCKET);
        let grpc_socket = run_dir.join(host::GRPC_SOCKET);
        let lock_file = run_dir.join(host::DAEMON_LOCK);
        let daemon_log = log_dir.join(host::DAEMON_LOG);
        Self {
            data_dir,
            run_dir,
            log_dir,
            data_subdir,
            docker_socket,
            grpc_socket,
            lock_file,
            daemon_log,
        }
    }

    /// Build a layout from a runtime profile.
    #[must_use]
    pub fn for_profile(profile: ArcboxProfile) -> Self {
        Self::new(profile.default_data_dir())
    }

    /// Resolve the data directory from an optional override, falling
    /// back to the production profile directory.
    #[must_use]
    pub fn resolve(data_dir: Option<&std::path::Path>) -> Self {
        Self::resolve_for_profile(ArcboxProfile::Production, data_dir)
    }

    /// Resolve the data directory from an optional override, falling back to
    /// the selected profile's default directory.
    #[must_use]
    pub fn resolve_for_profile(profile: ArcboxProfile, data_dir: Option<&std::path::Path>) -> Self {
        match data_dir {
            Some(d) => Self::new(d.to_path_buf()),
            None => Self::for_profile(profile),
        }
    }

    /// Resolve the data directory from an optional override, then
    /// `ARCBOX_DATA_DIR`, then the selected profile's default directory.
    #[must_use]
    pub fn resolve_for_profile_from_env(
        profile: ArcboxProfile,
        data_dir: Option<&std::path::Path>,
    ) -> Self {
        if let Some(data_dir) = data_dir {
            return Self::new(data_dir.to_path_buf());
        }

        if let Ok(data_dir) = std::env::var(crate::env::DATA_DIR) {
            if !data_dir.is_empty() {
                return Self::new(std::path::PathBuf::from(data_dir));
            }
        }

        Self::for_profile(profile)
    }

    /// Resolve the layout from environment (`ARCBOX_DATA_DIR`, then
    /// `ARCBOX_PROFILE`) or the production defaults.
    #[must_use]
    pub fn from_env_or_default() -> Self {
        Self::resolve_for_profile_from_env(ArcboxProfile::from_env_or_default(), None)
    }
}

/// Default data directory: `~/.arcbox`, falling back to `/var/lib/arcbox`
/// when the home directory cannot be resolved.
///
/// Uses `dirs::home_dir()` which handles edge cases (launchd, sudo,
/// non-interactive shells) that raw `$HOME` does not.
#[cfg(feature = "std")]
#[must_use]
pub fn default_data_dir() -> std::path::PathBuf {
    ArcboxProfile::Production.default_data_dir()
}

/// Privileged log directory (root-owned, for arcbox-helper).
pub mod privileged_log {
    /// Directory for helper logs (root-writable).
    pub const HELPER_LOG_DIR: &str = "/var/log/arcbox";
    /// Helper log file name.
    pub const HELPER_LOG: &str = "helper.log";
}

/// Guest-side subdirectory names within `/arcbox/`.
pub mod guest {
    /// Log directory inside the VirtioFS mount.
    pub const LOG: &str = "log";
}

#[cfg(all(test, feature = "std"))]
mod tests {
    use super::*;
    use std::path::PathBuf;

    #[test]
    fn host_layout_new_derives_all_paths() {
        let layout = HostLayout::new(PathBuf::from("/tmp/arcbox"));
        assert_eq!(layout.run_dir, PathBuf::from("/tmp/arcbox/run"));
        assert_eq!(layout.log_dir, PathBuf::from("/tmp/arcbox/log"));
        assert_eq!(layout.data_subdir, PathBuf::from("/tmp/arcbox/data"));
        assert_eq!(
            layout.docker_socket,
            PathBuf::from("/tmp/arcbox/run/docker.sock")
        );
        assert_eq!(
            layout.grpc_socket,
            PathBuf::from("/tmp/arcbox/run/arcbox.sock")
        );
        assert_eq!(
            layout.lock_file,
            PathBuf::from("/tmp/arcbox/run/daemon.lock")
        );
        assert_eq!(
            layout.daemon_log,
            PathBuf::from("/tmp/arcbox/log/daemon.log")
        );
    }

    #[test]
    fn host_layout_resolve_uses_explicit_dir() {
        let dir = PathBuf::from("/custom/dir");
        let layout = HostLayout::resolve(Some(&dir));
        assert_eq!(layout.data_dir, dir);
    }

    #[test]
    fn host_layout_resolve_uses_default_when_none() {
        let layout = HostLayout::resolve(None);
        assert_eq!(layout.data_dir, default_data_dir());
    }

    #[test]
    fn host_layout_resolve_for_profile_uses_explicit_dir() {
        let dir = PathBuf::from("/custom/dev");
        let layout = HostLayout::resolve_for_profile_from_env(
            ArcboxProfile::Development,
            Some(dir.as_path()),
        );
        assert_eq!(layout.data_dir, dir);
    }

    #[test]
    fn development_profile_uses_dev_data_dir_and_context() {
        let layout = HostLayout::for_profile(ArcboxProfile::Development);
        assert!(layout.data_dir.ends_with(".arcbox-dev"));
        assert_eq!(
            ArcboxProfile::Development.docker_context_name(),
            "arcbox-dev"
        );
        assert_eq!(
            ArcboxProfile::Development.daemon_label(),
            labels::DEVELOPMENT_DAEMON
        );
    }

    #[test]
    fn parses_profile_names_and_aliases() {
        assert_eq!(
            "production".parse::<ArcboxProfile>().unwrap(),
            ArcboxProfile::Production
        );
        assert_eq!(
            "dev".parse::<ArcboxProfile>().unwrap(),
            ArcboxProfile::Development
        );
    }

    #[test]
    fn default_data_dir_returns_home_based_path() {
        // When HOME is set (normal dev environment), the path should
        // end with ".arcbox" under the home directory.
        if dirs::home_dir().is_some() {
            let dir = default_data_dir();
            assert!(dir.ends_with(".arcbox"));
        }
    }
}