use crate::types::{QedStep, StepKind};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PortabilityGap {
SourceDoesNotTravel {
camp_reference: String,
},
BuildToolNeedsTheTree {
tool: String,
},
ProducesOffDurableDir {
path: String,
},
DefaultForgeImage,
}
const WHAT_TRAVELS_WITH_SOURCE_CONTEXT: &str =
"FIX: `source_context = [\"<subtree>\", …]`, camp-root-relative, naming every subtree \
the argv needs — mesofact-musl names four, because mesofact's path deps escape its own \
workspace into oss/yah-base and oss/cheers. THREE THINGS TRAVEL WITH THAT KEY and none \
are optional: (a) only GIT-TRACKED files are packed, so `git add` a new file or the \
worker gets a `mod` line without its file; (b) the argv fetches and unpacks the tarball \
itself from `$YAH_SOURCE_CONTEXT_URL` — use `tar --no-same-owner`, or the extract dies \
with \"Cannot change ownership\" on every file before the build starts; (c) if the image \
ENTRYPOINT is [\"bash\",\"-c\"] the whole invocation is ONE argv string, because a \
multi-element argv drops everything past [0]. Worked example, comments and all: \
.yah/qed/mesofact-musl.toml.";
impl PortabilityGap {
pub fn blocking(&self) -> bool {
!matches!(self, PortabilityGap::DefaultForgeImage)
}
pub fn kind(&self) -> &'static str {
match self {
PortabilityGap::SourceDoesNotTravel { .. } => "source-does-not-travel",
PortabilityGap::BuildToolNeedsTheTree { .. } => "build-tool-needs-the-tree",
PortabilityGap::ProducesOffDurableDir { .. } => "produces-off-durable-dir",
PortabilityGap::DefaultForgeImage => "default-forge-image",
}
}
pub fn remedy(&self) -> String {
match self {
PortabilityGap::SourceDoesNotTravel { camp_reference } => format!(
"declares no `source_context`, but its argv reads the camp tree \
(`{camp_reference}`). A remote subprocess gets image + argv + the \
/yah/produced mount and nothing else, so that path does not exist on the \
worker and the step dies minutes in, inside a container. \
{WHAT_TRAVELS_WITH_SOURCE_CONTEXT} \
If the step genuinely needs no camp source (rusty-v8-musl clones V8 \
inside the container), the other fix is to stop referencing \
`{camp_reference}` and bake it into the image instead."
),
PortabilityGap::BuildToolNeedsTheTree { tool } => format!(
"declares no `source_context`, but runs `{tool}`, which reads its manifest \
out of the working directory — and a remote subprocess has no camp tree \
to read one from (image + argv + the /yah/produced mount, nothing else). \
The step starts, and dies on a missing manifest. \
{WHAT_TRAVELS_WITH_SOURCE_CONTEXT}"
),
PortabilityGap::ProducesOffDurableDir { path } => format!(
"declares produced artifact `{path}`, which is not under \
`/yah/produced`. That dir is the durable host-backed bind mount; \
anything written elsewhere lands in the container's writable layer and \
is gone the moment the build-worker reaps the container (R603-T5). \
FIX: point the build's output at `/yah/produced/…` and set \
`produces.path` to that CONTAINER-side path — mesofact-musl writes \
`/yah/produced/mesofact-x86_64-unknown-linux-musl.tar.gz` and lets \
retrieve_remote_artifacts land it content-addressed on the camp."
),
PortabilityGap::DefaultForgeImage => String::from(
"names no `image`, so it runs in the default forge image \
(`yah-rust-bun`: rust + bun). The worker has no host userland to \
borrow, so that image is the whole environment. Fine for a plain cargo \
build; if the argv needs anything else — a musl sysroot, a prebuilt \
librusty_v8.a, python — name a digest-pinned image the way \
mesofact-musl does (a bare catalog name resolves to \
ghcr.io/yah-ai/<name>:latest and can silently pull the wrong \
registry, R590-B5). NOT a refusal: the default image is real, and \
whether this argv fits inside it is not answerable from the recipe.",
),
}
}
}
impl std::fmt::Display for PortabilityGap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PortabilityGap::SourceDoesNotTravel { camp_reference } => {
write!(f, "no source_context, argv reads `{camp_reference}`")
}
PortabilityGap::BuildToolNeedsTheTree { tool } => {
write!(f, "no source_context, runs `{tool}` against the tree")
}
PortabilityGap::ProducesOffDurableDir { path } => {
write!(f, "produces `{path}` outside /yah/produced")
}
PortabilityGap::DefaultForgeImage => write!(f, "no image (default yah-rust-bun)"),
}
}
}
const CAMP_TREE_TOOLS: &[&str] = &[
"cargo", "xtask", "bun", "npm", "pnpm", "yarn", "make", "just",
];
pub fn camp_tree_reference(argv: &[String], camp_root: Option<&std::path::Path>) -> Option<String> {
for element in argv {
for word in element.split_whitespace() {
let word = word.trim_matches(|c| c == '\'' || c == '"' || c == '`' || c == ',');
if word.starts_with("./") || word.starts_with("../") {
return Some(word.to_string());
}
let Some(root) = camp_root else { continue };
if !looks_like_a_relative_path(word) {
continue;
}
if root.join(word).exists() {
return Some(word.to_string());
}
}
}
None
}
pub fn build_tool_head(argv: &[String]) -> Option<String> {
let head = argv.first()?.split_whitespace().next()?;
CAMP_TREE_TOOLS.contains(&head).then(|| head.to_string())
}
fn looks_like_a_relative_path(word: &str) -> bool {
!word.is_empty()
&& word.len() < 256
&& word.contains('/')
&& !word.starts_with('/')
&& !word.starts_with('-')
&& !word.starts_with('$')
&& !word.contains("://")
&& !word.contains('$')
&& !word.contains('*')
}
pub fn gaps(step: &QedStep, camp_root: Option<&std::path::Path>) -> Vec<PortabilityGap> {
let mut out = Vec::new();
if step.source_context.is_empty() {
if let Some(camp_reference) = camp_tree_reference(&step.argv, camp_root) {
out.push(PortabilityGap::SourceDoesNotTravel { camp_reference });
} else if let Some(tool) = build_tool_head(&step.argv) {
out.push(PortabilityGap::BuildToolNeedsTheTree { tool });
}
}
for artifact in &step.produces {
let path = std::path::Path::new(&artifact.path);
if !workload_spec::forge_produced::is_durable_path(path) {
out.push(PortabilityGap::ProducesOffDurableDir {
path: artifact.path.clone(),
});
}
}
if step.image.is_none() {
out.push(PortabilityGap::DefaultForgeImage);
}
out
}
pub fn fleet_portable(
step: &QedStep,
camp_root: Option<&std::path::Path>,
) -> Result<(), Vec<PortabilityGap>> {
let blocking: Vec<_> = gaps(step, camp_root)
.into_iter()
.filter(|g| g.blocking())
.collect();
if blocking.is_empty() {
Ok(())
} else {
Err(blocking)
}
}
pub fn is_dispatchable_kind(step: &QedStep) -> bool {
matches!(step.kind, StepKind::Subprocess)
}
pub fn fleet_clause(step: &QedStep, camp_root: Option<&std::path::Path>) -> String {
if !is_dispatchable_kind(step) {
return format!("fleet = n/a (kind = {:?} runs on the camp)", step.kind);
}
let gaps = gaps(step, camp_root);
if gaps.is_empty() {
return "fleet = portable".to_string();
}
let blocking = gaps.iter().any(|g| g.blocking());
let labels = gaps
.iter()
.map(|g| g.to_string())
.collect::<Vec<_>>()
.join("; ");
if blocking {
format!("fleet = NOT portable ({labels})")
} else {
format!("fleet = portable ({labels})")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{OnFail, ProducedArtifact, StepActivation};
fn step(name: &str, argv: &[&str]) -> QedStep {
QedStep {
expect_slow: false,
name: name.to_string(),
argv: argv.iter().map(|s| s.to_string()).collect(),
cwd: None,
env: std::collections::HashMap::new(),
timeout: None,
on_fail: OnFail::Abort,
produces: Vec::new(),
runtime: None,
kind: StepKind::Subprocess,
image: None,
tag: None,
push: false,
platforms: Vec::new(),
binary_path: None,
triple: None,
package: None,
context: None,
source_context: Vec::new(),
cache: false,
load: false,
sub_pipeline: None,
outputs: Vec::new(),
inputs: Vec::new(),
secret: false,
gha_workflow: None,
import: None,
matrix: None,
enabled: true,
activation: StepActivation::default(),
if_cond: None,
background: false,
background_until: None,
wait_for: None,
manual: None,
manifest_stitch: None,
platform: None,
toolchain: None,
needs: None,
resource: None,
participant: None,
}
}
fn produced(path: &str) -> ProducedArtifact {
ProducedArtifact {
binary: "out".to_string(),
path: path.to_string(),
triple: None,
}
}
#[test]
fn a_repo_relative_script_is_not_portable() {
let s = step("check", &["./scripts/check-workspace-members.sh"]);
let err = fleet_portable(&s, None).unwrap_err();
assert_eq!(
err,
vec![PortabilityGap::SourceDoesNotTravel {
camp_reference: "./scripts/check-workspace-members.sh".into()
}],
);
let remedy = err[0].remedy();
assert!(remedy.contains("source_context"), "{remedy}");
assert!(remedy.contains("GIT-TRACKED"), "{remedy}");
assert!(remedy.contains("--no-same-owner"), "{remedy}");
assert!(remedy.contains("mesofact-musl"), "{remedy}");
}
#[test]
fn a_bare_build_tool_head_is_not_portable() {
let s = step("check", &["cargo", "check", "-p", "cloud"]);
assert!(matches!(
fleet_portable(&s, None).unwrap_err().as_slice(),
[PortabilityGap::BuildToolNeedsTheTree { tool }] if tool == "cargo"
));
let remedy = fleet_portable(&s, None).unwrap_err()[0].remedy();
assert!(remedy.contains("reads its manifest"), "{remedy}");
assert!(remedy.contains("GIT-TRACKED"), "{remedy}");
assert!(
!remedy.contains("stop referencing"),
"the path-shaped advice must not leak into the tool case: {remedy}"
);
}
#[test]
fn a_self_sufficient_image_step_is_portable_without_source_context() {
let mut s = step(
"build-v8-musl",
&["build-v8.sh 'x86_64-unknown-linux-musl' '/yah/produced/rusty-v8-x86_64-unknown-linux-musl.tar.gz'"],
);
s.image = Some("cr.yah.dev/rusty-v8-musl-builder:v149.4.0-amd64@sha256:7e9f".into());
s.produces = vec![produced("/yah/produced/rusty-v8-x86_64-unknown-linux-musl.tar.gz")];
assert_eq!(fleet_portable(&s, None), Ok(()));
assert_eq!(gaps(&s, None), Vec::new());
assert_eq!(fleet_clause(&s, None), "fleet = portable");
}
#[test]
fn the_mesofact_musl_shape_passes_the_gate() {
let mut s = step(
"build-mesofact-x86_64-musl",
&["set -eu\ncurl -fsSL \"$YAH_SOURCE_CONTEXT_URL\" -o /tmp/src.tar.gz\ntar --no-same-owner -xzf /tmp/src.tar.gz -C /work\ncd /work/oss/mesofact\ncargo build --release"],
);
s.source_context = vec![
std::path::PathBuf::from("oss/mesofact"),
std::path::PathBuf::from("oss/yah-base"),
];
s.image = Some("cr.yah.dev/mesofact-musl-builder:v149.4.0-rust1.97-amd64@sha256:0f87".into());
s.produces = vec![produced("/yah/produced/mesofact-x86_64-unknown-linux-musl.tar.gz")];
assert_eq!(fleet_portable(&s, None), Ok(()));
}
#[test]
fn a_relative_path_that_exists_in_the_camp_tree_is_a_camp_reference() {
let tmp = tempfile::TempDir::new().expect("tempdir");
std::fs::create_dir_all(tmp.path().join("scripts")).expect("scripts dir");
std::fs::write(tmp.path().join("scripts/smoke.sh"), "#!/bin/sh\n").expect("script");
let argv = vec!["bash".to_string(), "scripts/smoke.sh".to_string()];
assert_eq!(
camp_tree_reference(&argv, Some(tmp.path())),
Some("scripts/smoke.sh".to_string()),
);
assert_eq!(camp_tree_reference(&argv, None), None);
}
#[test]
fn slashes_that_are_not_paths_are_not_camp_references() {
let tmp = tempfile::TempDir::new().expect("tempdir");
for argv in [
vec!["bash".into(), "-c".into(), "echo x | sed 's/^/pre: /'".to_string()],
vec!["ip".into(), "route".into(), "add".into(), "0.0.0.0/0".to_string()],
vec!["curl".into(), "-fsSL".into(), "https://cdn.yah.dev/x.tar.gz".to_string()],
] {
assert_eq!(
camp_tree_reference(&argv, Some(tmp.path())),
None,
"argv: {argv:?}",
);
}
}
#[test]
fn an_inner_cargo_after_an_unpack_is_not_a_camp_reference() {
let argv = vec![
"set -eu\ntar -xzf /tmp/src.tar.gz -C /work\ncd /work/oss/mesofact\ncargo build --release"
.to_string(),
];
assert_eq!(camp_tree_reference(&argv, None), None);
}
#[test]
fn a_produced_path_off_the_durable_dir_is_blocking() {
let mut s = step("build", &["build.sh"]);
s.image = Some("cr.yah.dev/whatever@sha256:0f87".into());
s.produces = vec![produced("/tmp/out.tar.gz")];
let err = fleet_portable(&s, None).unwrap_err();
assert_eq!(
err,
vec![PortabilityGap::ProducesOffDurableDir {
path: "/tmp/out.tar.gz".into()
}],
);
assert!(err[0].remedy().contains("/yah/produced"));
}
#[test]
fn a_missing_image_is_advisory_not_blocking() {
let s = step("build", &["build-v8.sh /yah/produced/out.tar.gz"]);
assert_eq!(fleet_portable(&s, None), Ok(()));
assert_eq!(gaps(&s, None), vec![PortabilityGap::DefaultForgeImage]);
assert_eq!(
fleet_clause(&s, None),
"fleet = portable (no image (default yah-rust-bun))"
);
}
#[test]
fn a_non_subprocess_kind_is_not_asked() {
let mut s = step("child", &[]);
s.kind = StepKind::SubPipeline;
assert!(!is_dispatchable_kind(&s));
assert!(fleet_clause(&s, None).contains("n/a"));
}
}