Docs

derive-exec

Source: crates/lvcs/src/runner/derive.rs.

/// One derivation run (row 180) of runner job `job`.
///
/// It regenerates the derived files from the documents `inputs` names at
/// `frontier`, running the generator `action`'s steps in the run directory
/// `<dir>/derive/`, where `dir` is the job's directory under
/// `.lvcs/tmp/runs/`, so `lvcs sandbox` gives it a check's profile.
/// `frontier` is the job's frontier less the documents deleted at their
/// heads there, as a check's [`super::Materialize`] is: an input it doesn't
/// hold is absent from the run.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DeriveJob {
    /// The runner job the run belongs to.
    pub(crate) job: JobId,
    /// The frontier it runs on.
    pub(crate) frontier: Frontier,
    /// The job's directory.
    pub(crate) dir: PathBuf,
    /// The generator.
    pub(crate) action: Action,
    /// The input documents' addresses.
    pub(crate) inputs: BTreeSet<String>,
}

/// How a derivation run ended.
///
/// It ends with the outputs it wrote, each derived file it created or
/// changed mapped to its bytes and their SHA-256 (sorted, at most
/// `GENERATION_OUTPUTS_MAX`), or why it failed. The runner machine's
/// own outcome type is lvcs-action's; the daemon maps this to it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum DeriveResult {
    /// It exited 0 and wrote only derived files.
    Outputs(BTreeMap<String, Output>),
    /// It exited non-zero, was killed, or timed out (`Exit`), or it
    /// created, changed, or removed a file `is_derived` doesn't call
    /// derived (`NonDerivedWrite`).
    Failed(DeriveFailure),
}

/// One output of a derivation: its bytes and their SHA-256.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Output {
    /// The SHA-256 of its bytes.
    pub(crate) digest: [u8; 32],
    /// Its bytes, which the runner writes into its document.
    pub(crate) bytes: Vec<u8>,
}

/// Run derivation `job` on `files`, classifying what it touched.
///
/// `files` holds the bytes and executable bit the shell read for its
/// inputs at its frontier, and each file the run touched is classified with `is_derived`, which
/// takes the file's path and its first
/// `DERIVED_HEADER_BYTES` (a removed file's from before the run). A path
/// that isn't an input and that `syncs` says the project never syncs (a
/// `.gitignore`d or `.lvcsignore`d one, or never-sync) is left out of the
/// walk whatever it is, so it is never an output (decision 299).
///
/// The run directory is made fresh (0700, empty `work/`, `home/`,
/// `cargo/`, and `tmp/`), and `work/` holds each input `frontier` holds,
/// with its bytes from `files`, mode 0755 when its document's executable
/// bit is set and 0644 otherwise (row 307). The generator's steps run there through
/// the `run` machine, journaled as `journal`, each through `lvcs sandbox`
/// with a change action's profile: no network, no secrets, within its
/// `timeout_secs`. A run that doesn't pass (a step's non-zero exit, a
/// kill, a timeout, or bad step files) is `Failed(Exit)`. Otherwise every regular file under `work/` that is new
/// or whose bytes changed, and every input that is gone, is classified: a
/// derived one that is new or changed is an output, and any other is
/// `Failed(NonDerivedWrite)`, as is anything under `work/` that is neither
/// a regular file nor a directory (a symlink, a device, a FIFO, a socket)
/// or whose path isn't UTF-8. The run directory ends with its `log` alone:
/// `work/`, `home/`, `cargo/`, and `tmp/` are removed whatever happened.
/// The run records a `derive` span whose `outcome` is `ok` for outputs and
/// `error` otherwise. Dropping `kill`'s switch cancels the run (decision
/// 249, revised): its step under way is killed, and it is `Failed(Exit)`.
///
/// # Errors
///
/// With no result, which the runner must not record: an input
/// `frontier` holds is missing from `files` (a refused read) or isn't a
/// plain relative path; the run directory can't be made, written, walked,
/// or cleared; the sandbox can't be started; or the run's result can't be
/// recorded as a generation (a derived input it removed, which has no
/// bytes to name; more than `GENERATION_OUTPUTS_MAX` outputs; or a file
/// over `CHECK_FILE_BYTES`, which the sandbox's file size limit forbids).
pub(crate) fn derive(
    job: &DeriveJob,
    files: &BTreeMap<String, TreeFile>,
    is_derived: &dyn Fn(&str, &[u8]) -> bool,
    syncs: &dyn Fn(&str) -> bool,
    journal: RunJournal,
    kill: KillWatch,
) -> io::Result<DeriveResult> {
    let span = tracing::info_span!("derive", outcome = Empty);
    let _entered = span.enter();
    let result = run_derivation(job, files, is_derived, syncs, journal, kill);
    let outcome = match &result {
        Ok(DeriveResult::Outputs(_)) => Outcome::Ok,
        Ok(DeriveResult::Failed(_)) | Err(_) => Outcome::Error,
    };
    span.record("outcome", outcome.as_str());
    result
}
/// The input documents of a derivation at `frontier` (row 180).
///
/// Its default inputs are every file document `frontier` holds that isn't
/// derived (decision 171): `derived` names those whose `/meta/derived` is
/// true at their heads there, as the shell read them. `[derive] inputs`
/// patterns, when set, narrow the default, so a derived document is never
/// an input. `None` when a pattern doesn't parse, which the config's parse
/// already refuses.
pub(crate) fn derive_inputs(
    frontier: &Frontier,
    derived: &BTreeSet<String>,
    patterns: Option<&[String]>,
) -> Option<BTreeSet<String>> {
    let narrow = match patterns {
        Some(given) => Some(PathSet::new(given, false).ok()?),
        None => None,
    };
    Some(
        inputs::file_docs(frontier)
            .filter(|(address, _)| !derived.contains(*address))
            .filter(|(address, _)| narrow.as_ref().is_none_or(|set| set.matches(address)))
            .map(|(address, _)| address.clone())
            .collect(),
    )
}

/// What a derivation's `InputsHash` hashes: `[derive id, inputs,
/// toolchain_hash]`, `inputs` mapping each input document to its sorted
/// heads.
#[derive(Serialize)]
struct DeriveHashed<'a>(
    &'a ActionId,
    BTreeMap<&'a str, inputs::Heads>,
    #[serde(with = "lvcs_doc::cbor::fixed")] [u8; 32],
);

/// Derivation `derive`'s `InputsHash` at `frontier` (row 180).
///
/// It is `HASH([derive id, inputs, toolchain_hash])`, the derive id and the
/// toolchain hash as 32-byte byte strings, and `inputs` the map from each
/// address of `inputs` that `frontier` holds to its sorted heads, an array
/// of 32-byte byte strings, as a check's `InputsHash` maps a document that
/// isn't derived (decision 161).
///
/// # Errors
///
/// When the hashed array can't be encoded.
pub(crate) fn derive_inputs_hash(
    derive: &ActionId,
    frontier: &Frontier,
    inputs: &BTreeSet<String>,
    toolchain_hash: &[u8; 32],
) -> io::Result<InputsHash> {
    let heads: BTreeMap<&str, inputs::Heads> = frontier
        .docs
        .iter()
        .filter(|(address, _)| inputs.contains(*address))
        .map(|(address, heads)| (address.as_str(), inputs::Heads::of(heads)))
        .collect();
    cbor::digest(&DeriveHashed(derive, heads, *toolchain_hash))
        .map(InputsHash)
        .map_err(|err| io::Error::other(err.to_string()))
}