| 17 |
17 |
|
use crate::events::{self, Event, EventTx};
|
| 18 |
18 |
|
use crate::ota::{OtaRegistry, PublishAuthority, Release};
|
| 19 |
19 |
|
use crate::state::ExecutorMap;
|
|
20 |
+ |
use crate::topology::Kind;
|
| 20 |
21 |
|
use anyhow::{Context as _, Result};
|
| 21 |
22 |
|
use ops_core::live_log::LiveLog;
|
| 22 |
23 |
|
use ops_exec::{Action, Executor, ObserveKind, Step as OpStep, SyncOpts};
|
| 34 |
35 |
|
/// recipe's `sh("mbp", …)` under `step("sign")` becomes an `Action::Sign`, gated
|
| 35 |
36 |
|
/// by the mac host's `sign` grant — so recipes stay unchanged while every command
|
| 36 |
37 |
|
/// is capability-checked at its transport. `Verify` is read-only (an observe).
|
| 37 |
|
- |
fn action_for(step: Step) -> Action {
|
|
38 |
+ |
/// The capability a step's commands are gated on.
|
|
39 |
+ |
///
|
|
40 |
+ |
/// `Verify` depends on what is being released, which is the one place this is
|
|
41 |
+ |
/// not a property of the step alone. An app's verify is a Gatekeeper check on a
|
|
42 |
+ |
/// signed bundle, and the `gatekeeper` observe is granted implicitly to hosts
|
|
43 |
+ |
/// that can `sign` (`CapabilitySet::from_tokens`) precisely so that pairing
|
|
44 |
+ |
/// holds. A library's verify is a crate preflight: it runs `cargo` on the build
|
|
45 |
+ |
/// host and asks the registry a question. Gating that on Gatekeeper asks a Linux
|
|
46 |
+ |
/// host for a macOS code-signing capability it can never honestly hold, and the
|
|
47 |
+ |
/// only way to satisfy it would be to declare the capability falsely.
|
|
48 |
+ |
fn action_for(step: Step, kind: Kind) -> Action {
|
| 38 |
49 |
|
match step {
|
| 39 |
50 |
|
Step::Checkout | Step::Prebuild | Step::Build => Action::Build,
|
| 40 |
51 |
|
Step::Sign => Action::Sign,
|
| 41 |
52 |
|
Step::Notarize => Action::Notarize,
|
| 42 |
53 |
|
Step::Staple => Action::Staple,
|
| 43 |
54 |
|
Step::Package => Action::Package,
|
| 44 |
|
- |
Step::Verify => Action::Observe(ObserveKind::Custom("gatekeeper".into())),
|
|
55 |
+ |
Step::Verify => match kind {
|
|
56 |
+ |
Kind::App => Action::Observe(ObserveKind::Custom("gatekeeper".into())),
|
|
57 |
+ |
// Running the build toolchain to inspect a crate, which is what
|
|
58 |
+ |
// `build` means on a host.
|
|
59 |
+ |
Kind::Library => Action::Build,
|
|
60 |
+ |
},
|
| 45 |
61 |
|
// Publish/Collect run on the daemon, not through a host executor; this
|
| 46 |
62 |
|
// label only applies if a recipe runs a bare `sh` while one is open.
|
| 47 |
63 |
|
Step::Publish | Step::Collect => Action::Package,
|
| 111 |
127 |
|
/// Cargo features this app's release builds enable (topology `features`).
|
| 112 |
128 |
|
/// Recipes read it via `feature_flags()`.
|
| 113 |
129 |
|
pub features: Vec<String>,
|
|
130 |
+ |
/// App or library. Decides which capability a `verify` step is gated on;
|
|
131 |
+ |
/// see [`action_for`].
|
|
132 |
+ |
pub kind: Kind,
|
| 114 |
133 |
|
pub target_run_id: i64,
|
| 115 |
134 |
|
/// Capability-scoped executor per build host. Recipe commands dispatch
|
| 116 |
135 |
|
/// through these — the transport (local / ssh / in-session agent) and the
|
| 192 |
211 |
|
build_host: String,
|
| 193 |
212 |
|
repo: String,
|
| 194 |
213 |
|
features: Vec<String>,
|
|
214 |
+ |
kind: Kind,
|
| 195 |
215 |
|
target_run_id: i64,
|
| 196 |
216 |
|
execs: Arc<ExecutorMap>,
|
| 197 |
217 |
|
syncs: Arc<ExecutorMap>,
|
| 210 |
230 |
|
build_host,
|
| 211 |
231 |
|
repo,
|
| 212 |
232 |
|
features,
|
|
233 |
+ |
kind,
|
| 213 |
234 |
|
target_run_id,
|
| 214 |
235 |
|
execs,
|
| 215 |
236 |
|
syncs,
|
| 474 |
495 |
|
let sink = self.ensure_step()?;
|
| 475 |
496 |
|
let exec = self.exec(host)?;
|
| 476 |
497 |
|
let cur = self.current_step();
|
| 477 |
|
- |
let step = OpStep::shell(action_for(cur), cmd.to_string());
|
|
498 |
+ |
let step = OpStep::shell(action_for(cur, self.kind), cmd.to_string());
|
| 478 |
499 |
|
// Bounded by the step's deadline and interruptible on supersession, so a
|
| 479 |
500 |
|
// hung command fails its step instead of running unbounded, and a
|
| 480 |
501 |
|
// superseded build stops mid-step rather than only at the next boundary.
|
| 1102 |
1123 |
|
// Ask the publishing host whether cargo has credentials, rather
|
| 1103 |
1124 |
|
// than moving the token anywhere. It stays in cargo's own 0600
|
| 1104 |
1125 |
|
// store; a shell line carrying it would be visible in `ps`.
|
| 1105 |
|
- |
let creds = ctx
|
| 1106 |
|
- |
.run(
|
|
1126 |
+ |
// An exit code answers "are there credentials"; an Err answers
|
|
1127 |
+ |
// "the question could not be asked". Collapsing the second into
|
|
1128 |
+ |
// the first reported a capability denial as "no crates.io
|
|
1129 |
+ |
// credentials", which sent a real diagnosis three rounds the
|
|
1130 |
+ |
// wrong way. A check that cannot run is not a failed check.
|
|
1131 |
+ |
let creds =
|
|
1132 |
+ |
ctx.run(
|
| 1107 |
1133 |
|
&ctx.build_host.clone(),
|
| 1108 |
1134 |
|
"cargo login --help >/dev/null 2>&1 && \
|
| 1109 |
1135 |
|
test -s \"${CARGO_HOME:-$HOME/.cargo}/credentials.toml\" \
|
| 1110 |
1136 |
|
|| test -s \"${CARGO_HOME:-$HOME/.cargo}/credentials\"",
|
| 1111 |
1137 |
|
)
|
| 1112 |
|
- |
.is_ok_and(|(code, _)| code == 0);
|
|
1138 |
+ |
.map_err(|e| {
|
|
1139 |
+ |
format!(
|
|
1140 |
+ |
"could not check crates.io credentials on `{}`: {e}",
|
|
1141 |
+ |
ctx.build_host
|
|
1142 |
+ |
)
|
|
1143 |
+ |
})?
|
|
1144 |
+ |
.0 == 0;
|
| 1113 |
1145 |
|
|
| 1114 |
1146 |
|
let published = RecipeCtx::published_versions(&meta.name);
|
| 1115 |
1147 |
|
let problems = crate_publish_problems(&meta, clonable, &published, creds);
|
| 1734 |
1766 |
|
"fw13".into(),
|
| 1735 |
1767 |
|
"/tmp".into(),
|
| 1736 |
1768 |
|
vec![],
|
|
1769 |
+ |
Kind::App,
|
| 1737 |
1770 |
|
1,
|
| 1738 |
1771 |
|
Arc::new(std::collections::HashMap::new()),
|
| 1739 |
1772 |
|
Arc::new(std::collections::HashMap::new()),
|
| 1768 |
1801 |
|
"fw13".into(),
|
| 1769 |
1802 |
|
"/tmp".into(),
|
| 1770 |
1803 |
|
features,
|
|
1804 |
+ |
Kind::App,
|
| 1771 |
1805 |
|
1,
|
| 1772 |
1806 |
|
Arc::new(std::collections::HashMap::new()),
|
| 1773 |
1807 |
|
Arc::new(std::collections::HashMap::new()),
|
| 1833 |
1867 |
|
assert!(problems.iter().any(|p| p.contains("license")));
|
| 1834 |
1868 |
|
}
|
| 1835 |
1869 |
|
|
|
1870 |
+ |
// A library's verify is a crate preflight, not a Gatekeeper check on a
|
|
1871 |
+ |
// signed bundle. Gating it on `gatekeeper` asked a Linux host for a macOS
|
|
1872 |
+ |
// code-signing capability it can never hold, so the step was denied before
|
|
1873 |
+ |
// it ran a command; the denial then surfaced as "no crates.io credentials",
|
|
1874 |
+ |
// which is not what went wrong. The only way to satisfy the old gate was to
|
|
1875 |
+ |
// declare the capability falsely in the topology.
|
|
1876 |
+ |
#[test]
|
|
1877 |
+ |
fn a_library_verify_is_not_gated_on_gatekeeper() {
|
|
1878 |
+ |
assert_eq!(
|
|
1879 |
+ |
action_for(Step::Verify, Kind::Library),
|
|
1880 |
+ |
Action::Build,
|
|
1881 |
+ |
"a crate preflight runs the build toolchain; that is what it needs",
|
|
1882 |
+ |
);
|
|
1883 |
+ |
assert_eq!(
|
|
1884 |
+ |
action_for(Step::Verify, Kind::App),
|
|
1885 |
+ |
Action::Observe(ObserveKind::Custom("gatekeeper".into())),
|
|
1886 |
+ |
"an app's verify still proves the bundle is signed and notarized",
|
|
1887 |
+ |
);
|
|
1888 |
+ |
}
|
|
1889 |
+ |
|
|
1890 |
+ |
// The capability the default host grant actually carries. Without this the
|
|
1891 |
+ |
// fix above is only true by inspection.
|
|
1892 |
+ |
#[test]
|
|
1893 |
+ |
fn a_default_host_can_run_a_library_verify_and_not_an_app_one() {
|
|
1894 |
+ |
let caps =
|
|
1895 |
+ |
ops_exec::CapabilitySet::from_tokens(["build", "package"], ["build-log", "artifact"]);
|
|
1896 |
+ |
assert!(caps.permits(&action_for(Step::Verify, Kind::Library)));
|
|
1897 |
+ |
assert!(!caps.permits(&action_for(Step::Verify, Kind::App)));
|
|
1898 |
+ |
}
|
|
1899 |
+ |
|
|
1900 |
+ |
// Every other step is a property of the step alone; verify is the one that
|
|
1901 |
+ |
// depends on what is being released.
|
|
1902 |
+ |
#[test]
|
|
1903 |
+ |
fn no_other_step_changes_with_the_kind() {
|
|
1904 |
+ |
for step in [
|
|
1905 |
+ |
Step::Checkout,
|
|
1906 |
+ |
Step::Prebuild,
|
|
1907 |
+ |
Step::Build,
|
|
1908 |
+ |
Step::Sign,
|
|
1909 |
+ |
Step::Notarize,
|
|
1910 |
+ |
Step::Staple,
|
|
1911 |
+ |
Step::Package,
|
|
1912 |
+ |
Step::Publish,
|
|
1913 |
+ |
Step::Collect,
|
|
1914 |
+ |
] {
|
|
1915 |
+ |
assert_eq!(
|
|
1916 |
+ |
action_for(step, Kind::App),
|
|
1917 |
+ |
action_for(step, Kind::Library),
|
|
1918 |
+ |
"{step:?} should not depend on the kind",
|
|
1919 |
+ |
);
|
|
1920 |
+ |
}
|
|
1921 |
+ |
}
|
|
1922 |
+ |
|
| 1836 |
1923 |
|
#[test]
|
| 1837 |
1924 |
|
fn preflight_rejects_republishing_the_same_version() {
|
| 1838 |
1925 |
|
let meta = CrateMeta {
|