- 1
//! Where a managed install lives, and how its pieces are addressed. - 2
//! - 3
//! Every `self` subcommand resolves the install root through - 4
//! [`InstallRoot::resolve`] so that `--prefix` means the same thing to - 5
//! `install`, `status`, `update`, `services-sync`, and `uninstall`. A - 6
//! prefix that only `install` understood was the source of installs that - 7
//! could not afterwards be inspected or removed. - 8
- 9
use std::path::{Path, PathBuf}; - 10
- 11
use vak_core::install as core_install; - 12
- 13
/// Overrides the install prefix without threading `--prefix` through - 14
/// every invocation. Useful for staging and for tests. - 15
pub const PREFIX_ENV: &str = core_install::PREFIX_ENV; - 16
- 17
const BIN_DIR: &str = "bin"; - 18
const STAGING_DIR: &str = ".staging"; - 19
const BACKUP_DIR: &str = ".backup"; - 20
- 21
/// A resolved install root together with the layout it implies. - 22
#[derive(Debug, Clone, PartialEq, Eq)] - 23
pub struct InstallRoot { - 24
prefix: PathBuf, - 25
bundle: bool, - 26
} - 27
- 28
impl InstallRoot { - 29
/// Resolve the install root, in precedence order: - 30
/// explicit `--prefix`, then `VAK_PREFIX`, then the platform - 31
/// default. The first two are taken literally so callers can install - 32
/// to a plain directory on any platform. - 33
pub fn resolve(explicit: Option<PathBuf>) -> Self { - 34
if let Some(p) = explicit { - 35
return Self::at(p); - 36
} - 37
if let Some(p) = std::env::var_os(PREFIX_ENV).filter(|v| !v.is_empty()) { - 38
return Self::at(PathBuf::from(p)); - 39
} - 40
Self::at(platform_default()) - 41
} - 42
- 43
/// Treat `prefix` as an install root, inferring the layout from it. - 44
pub fn at(prefix: PathBuf) -> Self { - 45
let bundle = is_bundle(&prefix); - 46
Self { prefix, bundle } - 47
} - 48
- 49
pub fn prefix(&self) -> &Path { - 50
&self.prefix - 51
} - 52
- 53
/// True when the root is a macOS application bundle, which puts - 54
/// executables under `Contents/MacOS` rather than `bin/`. - 55
pub fn is_bundle(&self) -> bool { - 56
self.bundle - 57
} - 58
- 59
pub fn bin_dir(&self) -> PathBuf { - 60
if self.bundle { - 61
self.prefix.join("Contents").join("MacOS") - 62
} else { - 63
self.prefix.join(BIN_DIR) - 64
} - 65
} - 66
- 67
pub fn manifest_path(&self) -> PathBuf { - 68
core_install::manifest_path_for_prefix(&self.prefix) - 69
} - 70
- 71
/// Frontend assets and the manifest live here in a bundle; plain - 72
/// prefixes keep them beside the binaries. - 73
pub fn resources_dir(&self) -> PathBuf { - 74
if self.bundle { - 75
self.prefix.join("Contents").join("Resources") - 76
} else { - 77
self.prefix.join("share") - 78
} - 79
} - 80
- 81
/// Scratch space for a transaction, on the same filesystem as the - 82
/// destination so the final move is an atomic rename rather than a - 83
/// cross-device copy. - 84
pub fn staging_dir(&self) -> PathBuf { - 85
self.prefix.join(STAGING_DIR) - 86
} - 87
- 88
/// Where displaced files wait until a transaction commits. - 89
pub fn backup_dir(&self) -> PathBuf { - 90
self.prefix.join(BACKUP_DIR) - 91
} - 92
- 93
/// True when something is already installed here. - 94
pub fn is_installed(&self) -> bool { - 95
self.manifest_path().exists() - 96
} - 97
} - 98
- 99
/// Platform default install root. macOS gets a real application bundle so - 100
/// the app is launchable from Finder; other platforms get a managed - 101
/// prefix under the data home. - 102
/// - 103
/// Delegates to `vak_core::install`, the single source of truth shared - 104
/// with `vak_core::health`'s "self version parity" check. - 105
fn platform_default() -> PathBuf { - 106
core_install::platform_default_prefix() - 107
} - 108
- 109
fn is_bundle(root: &Path) -> bool { - 110
core_install::is_bundle(root) - 111
} - 112
- 113
#[cfg(test)] - 114
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 115
mod tests { - 116
use super::*; - 117
- 118
#[test] - 119
fn explicit_prefix_wins_and_is_taken_literally() { - 120
let root = InstallRoot::resolve(Some(PathBuf::from("/tmp/vak-explicit"))); - 121
assert_eq!(root.prefix(), Path::new("/tmp/vak-explicit")); - 122
assert!(!root.is_bundle(), "a plain directory is not a bundle"); - 123
assert_eq!(root.bin_dir(), PathBuf::from("/tmp/vak-explicit/bin")); - 124
assert_eq!( - 125
root.manifest_path(), - 126
PathBuf::from("/tmp/vak-explicit/install.json") - 127
); - 128
} - 129
- 130
#[test] - 131
fn bundle_layout_puts_binaries_and_manifest_inside_contents() { - 132
let root = InstallRoot::at(PathBuf::from("/Applications/Vak.app")); - 133
assert!(root.is_bundle()); - 134
assert_eq!( - 135
root.bin_dir(), - 136
PathBuf::from("/Applications/Vak.app/Contents/MacOS") - 137
); - 138
assert_eq!( - 139
root.manifest_path(), - 140
PathBuf::from("/Applications/Vak.app/Contents/Resources/install.json") - 141
); - 142
} - 143
- 144
#[test] - 145
fn staging_and_backup_sit_inside_the_prefix() { - 146
// Same filesystem as the destination, so committing a - 147
// transaction is a rename and never a cross-device copy. - 148
let root = InstallRoot::at(PathBuf::from("/opt/vak")); - 149
assert!(root.staging_dir().starts_with(root.prefix())); - 150
assert!(root.backup_dir().starts_with(root.prefix())); - 151
} - 152
} - 153
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.