// now: Nix-based distributed command runner
// Copyright (C) 2026 Eric Rodrigues Pires
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU Affero General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
// more details.
//
// You should have received a copy of the GNU Affero General Public License along
// with this program. If not, see .
use std::{
collections::{BTreeMap, BTreeSet},
ffi::OsString,
io::Write,
os::unix::ffi::OsStringExt,
path::{Path, PathBuf},
process::Stdio,
sync::{Mutex, OnceLock},
time::Duration,
};
use ahash::{HashMap, HashMapExt, HashSet, HashSetExt};
use color_eyre::eyre::{Context, eyre};
use sha2::{Digest, Sha256};
use smol::{channel::Receiver, process::Command};
use tracing::instrument;
use crate::{
command::TracingMode,
project::ProjectSource,
secret::SecretString,
utils::{parse_bool_from_bytes, wait_for_output, write_output_to_stderr},
workflow::{NowJob, NowJobContainer, NowStepEnvVar, NowWorkflow, WorkflowSource},
};
pub(crate) static EVAL_ID: OnceLock = OnceLock::new();
pub(crate) fn eval_id() -> &'static str {
EVAL_ID
.get()
.expect("EVAL_ID initialized before evaluation")
}
async fn eval_id_from_workflow_source(workflow: &WorkflowSource) -> color_eyre::Result {
let mut command = smol::process::Command::new("nix");
command
.args([
"--extra-experimental-features",
"nix-command flakes",
"eval",
"--impure",
"--raw",
"--expr",
])
.arg(workflow.nix_source_expression()?)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = command.spawn()?;
let output = wait_for_output(&mut child, None).await?;
let mut data = if output.status.success() {
output.stdout
} else {
write_output_to_stderr(&output)?;
return Err(eyre!("Failed to get workflow source from {}", workflow));
};
// Two attributes of the same flake share a source tree; keep their IDs apart.
if let WorkflowSource::Flake { attribute, .. } = workflow {
data.push(b'#');
data.extend_from_slice(attribute.as_bytes());
}
let digest = Sha256::digest(data);
Ok(hex::encode(&digest[..16]))
}
pub(crate) struct NowEnvironment {
pub(crate) nix_project_source: ProjectSource,
pub(crate) tracing: Option,
pub(crate) secrets: HashMap,
pub(crate) vars: HashMap,
pub(crate) termination_timeout: Duration,
pub(crate) jobs: BTreeMap,
pub(crate) local_env: HashMap,
pub(crate) gcroot_dir: PathBuf,
pub(crate) uploads: Mutex>,
}
struct ParsedWorkflow {
vars: HashSet,
secrets: HashSet,
jobs: BTreeMap,
}
#[bon::bon]
impl NowEnvironment {
#[builder]
pub(crate) fn new(
workflow: &WorkflowSource,
ctrl_c: Receiver<()>,
tracing: Option,
env_file: Option,
gcroot_dir: Option,
termination_timeout: Option,
) -> impl Future