// 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::{
env::temp_dir,
ffi::{OsStr, OsString},
num::NonZeroUsize,
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
time::Duration,
};
use ahash::{HashMap, HashSet};
use async_trait::async_trait;
use color_eyre::eyre::{OptionExt, eyre};
use futures::{AsyncWriteExt, stream::FuturesUnordered};
use smol::{
channel::{self, Receiver},
lock::{
RwLock, RwLockReadGuard, RwLockWriteGuard, Semaphore, SemaphoreGuard,
futures::{Acquire, Read, Write},
},
process::{Child, Command, Stdio},
stream::StreamExt,
};
use crate::{
builder::{NixConfig, NowBuilder, remote::RemoteBuilder},
environment::NowEnvironment,
job::JobError,
utils::{wait_for_output, write_output_to_stderr},
workflow::{NowCheckout, NowJob, NowRunMode},
};
pub(crate) struct LocalBuilder {
pub(crate) cancellation: channel::Sender<()>,
pub(crate) receiver: channel::Receiver<()>,
pub(crate) semaphore: Semaphore,
pub(crate) lock: RwLock<()>,
pub(crate) env: HashMap,
pub(crate) hostname: String,
pub(crate) extra_platforms: Vec,
pub(crate) system: String,
pub(crate) system_features: HashSet,
pub(crate) remote_builders: Vec,
pub(crate) run_mode: NowRunMode,
pub(crate) gcroot_dir: PathBuf,
}
pub(crate) struct BuilderGuard<'a> {
pub(crate) guard: SemaphoreGuard<'a>,
pub(crate) lock: RwLockReadGuard<'a, ()>,
pub(crate) receiver: &'a Receiver<()>,
pub(crate) builder: &'a dyn NowBuilder,
}
pub(crate) struct RunnerGuard<'a> {
pub(crate) lock: RwLockWriteGuard<'a, ()>,
pub(crate) receiver: &'a Receiver<()>,
pub(crate) builder: &'a dyn NowBuilder,
}
impl LocalBuilder {
pub(crate) async fn new(
environment: &NowEnvironment,
builders: Option,
run_mode: NowRunMode,
cores: Option,
keep_alive: Option,
) -> color_eyre::Result {
let mut command = Command::new("nix");
command
.args([
"--extra-experimental-features",
"nix-command flakes",
"config",
"show",
"--json",
])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = command.spawn()?;
let output = wait_for_output(&mut child, None).await?;
let config: NixConfig = if output.status.success() {
serde_json::from_slice(&output.stdout)?
} else {
write_output_to_stderr(&output)?;
return Err(eyre!("Failed to fetch Nix config"));
};
let remote_builders = match run_mode {
NowRunMode::All => {
RemoteBuilder::get_remote_builders(
&config,
builders,
environment.nix_project_source.as_ref(),
keep_alive,
)
.await?
}
NowRunMode::LocalOnly => vec![],
NowRunMode::RemoteOnly => {
let remote_builders = RemoteBuilder::get_remote_builders(
&config,
builders,
environment.nix_project_source.as_ref(),
keep_alive,
)
.await?;
if remote_builders.is_empty() {
return Err(eyre!("No remote builders available"));
}
remote_builders
}
};
let (cancellation, receiver) = channel::bounded(1);
let cores = match cores {
Some(cores) => cores.into(),
None => smol::unblock(sysinfo::System::physical_core_count)
.await
.unwrap_or(1),
};
let hostname = smol::unblock(sysinfo::System::host_name)
.await
.unwrap_or_else(|| "localhost".into());
let mut env = environment.local_env.clone();
env.insert(
"NOW_GCROOT_DIR".into(),
environment.gcroot_dir.clone().into(),
);
Ok(Self {
cancellation,
receiver,
semaphore: Semaphore::new(cores),
lock: RwLock::default(),
env,
hostname,
extra_platforms: config.extra_platforms.value,
system: config.system.value,
system_features: config.system_features.value.into_iter().collect(),
remote_builders,
run_mode,
gcroot_dir: environment.gcroot_dir.clone(),
})
}
pub(crate) fn cancel_builders(&self) {
self.cancellation.close();
for remote in &self.remote_builders {
remote.cancellation.close();
}
}
pub(crate) async fn get_builder(
&self,
job: &NowJob,
) -> color_eyre::Result