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
//! §6 `bl install` argv — the parse layer, a sibling of the run-wiring (the
//! [`crate::checkout_args`] convention; bl-4c45).
//!
//! `bl install [<path>] [--from <ref>] [--to <ref>] [--bin <name>=<path>]…
//! [--as ID]`. The §6 defaults: `<path>` is the recommended bundle
//! ([`DEFAULT_PATH`] — all of `config/`, never the store) and must stay inside
//! the checkout (relative, no `..`); `--to` defaults to the landing; `--from`
//! defaults to the CONFIGURED UPSTREAM — parse leaves it `None` and the run
//! wiring resolves it through the `install.pre` fetch chain (core itself still
//! reaches no remote, §0). Two guards are parse-time because both legs belong
//! to the LANDING-TARGETED direction (§6): a store-targeted install names its
//! `--from` explicitly (the upstream default adopts config INTO the landing),
//! and `--bin` feeds the landing-side binary resolution — an explicit flag
//! silently dropped would be the bl-cf93 sin, so each is refused, not ignored.
use std::collections::BTreeMap;
use std::io;
use std::path::{Path, PathBuf};
use crate::checkout;
use crate::install::DEFAULT_PATH;
use crate::LANDING_BRANCH;
/// Parsed `bl install` argv. `from: None` = the §6 configured-upstream
/// default, resolved by the run wiring; `bins` maps a plugin name to the
/// explicit `--bin <name>=<path>` candidate that outranks the machine lookup.
#[derive(Debug)]
pub(crate) struct Opts {
pub(crate) path: String,
/// Was `<path>` given EXPLICITLY (vs defaulted to [`DEFAULT_PATH`])? The
/// bl-cfe3 bind-only signal: `--bin` with neither an explicit `<path>` nor a
/// `--from` copies NOTHING — it just binds, so re-binding a plugin binary can
/// never silently mirror the stale upstream config. Config adoption stays
/// opt-in via a named `<path>` or `--from`.
pub(crate) explicit_path: bool,
pub(crate) from: Option<String>,
pub(crate) to: String,
pub(crate) actor: String,
pub(crate) bins: BTreeMap<String, PathBuf>,
}
/// Parse install's argv (see the module doc for the §6 defaults and guards).
pub(crate) fn parse(args: &[String], default_actor: &str) -> io::Result<Opts> {
let (mut path, mut from, mut to) = (None, None, None);
let mut bins = BTreeMap::new();
let mut actor = default_actor.to_string();
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--from" => from = Some(checkout::value(args, &mut i, "--from")?),
"--to" => to = Some(checkout::value(args, &mut i, "--to")?),
"--as" => actor = checkout::value(args, &mut i, "--as")?,
"--bin" => {
let v = checkout::value(args, &mut i, "--bin")?;
let (name, bin) = v
.split_once('=')
.ok_or_else(|| crate::usage(format!("install: --bin wants <name>=<path>, got '{v}'")))?;
bins.insert(name.to_string(), PathBuf::from(bin));
}
flag if flag.starts_with('-') => {
return Err(crate::usage(format!("install: unexpected flag '{flag}'")));
}
p => {
if path.replace(p.to_string()).is_some() {
return Err(crate::usage("install: at most one path"));
}
}
}
i += 1;
}
let explicit_path = path.is_some();
let path = path.unwrap_or_else(|| DEFAULT_PATH.to_string());
if Path::new(&path).is_absolute() || path.split('/').any(|c| c == "..") {
return Err(crate::usage(format!("install: path must be checkout-relative: '{path}'")));
}
let to = to.unwrap_or_else(|| LANDING_BRANCH.to_string());
if to != LANDING_BRANCH {
if from.is_none() {
return Err(crate::usage(
"install: --from is required when --to is not the landing (the configured-upstream default adopts into the landing, §6)",
));
}
if !bins.is_empty() {
return Err(crate::usage(
"install: --bin applies only to a landing-targeted install (§6 local binary resolution)",
));
}
}
Ok(Opts { path, explicit_path, from, to, actor, bins })
}
#[cfg(test)]
#[path = "install_args_tests.rs"]
mod tests;