use crate::tasks::workflows::{ runners::{Arch, Platform}, steps::{CommonJobConditions, CommonPermissionSets, DEFAULT_REPOSITORY_OWNER_GUARD, NamedJob},}; use super::{runners, steps, steps::named, vars};use gh_workflow::*; /// Generates the nix_build.yml workflow, which builds the Nix package on PRs/// that carry the `run-nix` or `run-bundling` label. The Nix jobs live only/// here (not in run_bundling.yml) so that setting both labels doesn't build/// them twice.pub fn nix_build() -> Workflow { let [nix_linux_x86_64, nix_mac_aarch64] = nix_pr_jobs(&["run-nix", "run-bundling"]); named::workflow() .with_minimal_permissions() .on(Event::default().pull_request( PullRequest::default().types([PullRequestType::Labeled, PullRequestType::Synchronize]), )) .concurrency( Concurrency::new(Expression::new( "${{ github.workflow }}-${{ github.head_ref || github.ref }}", )) .cancel_in_progress(true), ) .add_env(("CARGO_TERM_COLOR", "always")) .add_env(("RUST_BACKTRACE", "1")) .add_job(nix_linux_x86_64.name, nix_linux_x86_64.job) .add_job(nix_mac_aarch64.name, nix_mac_aarch64.job)} /// Builds the pair of PR Nix jobs (Linux x86_64 + macOS aarch64), each gated so/// they run when any of the given PR `labels` is present (on/// `labeled`/`synchronize` events).fn nix_pr_jobs(labels: &[&str]) -> [NamedJob; 2] { let labeled = labels .iter() .map(|label| format!("github.event.label.name == '{label}'")) .collect::<Vec<_>>() .join(" || "); let synchronized = labels .iter() .map(|label| format!("contains(github.event.pull_request.labels.*.name, '{label}')")) .collect::<Vec<_>>() .join(" || "); [ (Platform::Linux, Arch::X86_64), (Platform::Mac, Arch::AARCH64), ] .map(|(platform, arch)| { let mut job = build_nix( platform, arch, "default", // don't push PR builds to the cache Some("-zed-editor-[0-9.]*"), &[], ); job.job = job.job.cond(Expression::new(format!( "{DEFAULT_REPOSITORY_OWNER_GUARD} && \ ((github.event.action == 'labeled' && ({labeled})) || \ (github.event.action == 'synchronize' && ({synchronized})))" ))); job })} pub(crate) fn build_nix( platform: Platform, arch: Arch, flake_output: &str, cachix_filter: Option<&str>, deps: &[&NamedJob],) -> NamedJob { pub fn install_nix() -> Step<Use> { named::uses( "cachix", "install-nix-action", "02a151ada4993995686f9ed4f1be7cfbb229e56f", // v31 ) .add_with(("github_access_token", vars::GITHUB_TOKEN)) } pub fn cachix_action(cachix_filter: Option<&str>) -> Step<Use> { let mut step = named::uses( "cachix", "cachix-action", "0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad", // v16 ) .add_with(("name", "zed")) .add_with(("authToken", vars::CACHIX_AUTH_TOKEN)) .add_with(("cachixArgs", "-v")); if let Some(cachix_filter) = cachix_filter { step = step.add_with(("pushFilter", cachix_filter)); } step } pub fn build(flake_output: &str) -> Step<Run> { named::bash(&format!( "nix build .#{} -L --accept-flake-config", flake_output )) } // After install-nix, register ~/nix-cache as a local binary cache // substituter so nix pulls from it on demand during builds (no bulk // import). Also restart the daemon so it picks up the new config. pub fn configure_local_nix_cache() -> Step<Run> { named::bash(indoc::indoc! {r#" mkdir -p ~/nix-cache echo "extra-substituters = file://$HOME/nix-cache?priority=10" | sudo tee -a /etc/nix/nix.conf echo "require-sigs = false" | sudo tee -a /etc/nix/nix.conf sudo launchctl kickstart -k system/org.nixos.nix-daemon "#}) } // Incrementally copy only new store paths from the build result's // closure into the local binary cache for the next run. pub fn export_to_local_nix_cache() -> Step<Run> { named::bash(indoc::indoc! {r#" if [ -L result ]; then echo "Copying build closure to local binary cache..." nix copy --to "file://$HOME/nix-cache" ./result || echo "Warning: nix copy to local cache failed" else echo "No build result found, skipping cache export." fi "#}) .if_condition(Expression::new("always()")) } let runner = match platform { Platform::Windows => unimplemented!(), Platform::Linux => runners::LINUX_X86_BUNDLER, Platform::Mac => runners::MAC_DEFAULT, }; let mut job = Job::default() .timeout_minutes(60u32) .continue_on_error(true) .with_repository_owner_guard() .runs_on(runner) .add_env(("ZED_CLIENT_CHECKSUM_SEED", vars::ZED_CLIENT_CHECKSUM_SEED)) .add_env(("ZED_MINIDUMP_ENDPOINT", vars::ZED_SENTRY_MINIDUMP_ENDPOINT)) .add_env(( "ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON", vars::ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON, )) .add_env(("GIT_LFS_SKIP_SMUDGE", "1")) // breaks the livekit rust sdk examples which we don't actually depend on .add_step(steps::checkout_repo()); if deps.len() > 0 { job = job.needs(deps.iter().map(|d| d.name.clone()).collect::<Vec<String>>()); } // On Linux, `cache: nix` uses bind-mounts so the /nix store is available // before install-nix-action runs — no extra steps needed. // // On macOS, `/nix` lives on a read-only root filesystem and the nscloud // cache action cannot mount or symlink there. Instead we cache a // user-writable directory (~/nix-cache) as a local binary cache and // register it as a nix substituter. Nix then pulls paths from it on // demand during builds (zero-copy at startup), and after building we // incrementally copy new paths into the cache for the next run. job = match platform { Platform::Linux => job .add_step(steps::cache_nix_dependencies_namespace()) .add_step(install_nix()) .add_step(cachix_action(cachix_filter)) .add_step(build(&flake_output)), Platform::Mac => job .add_step(steps::cache_nix_store_macos()) .add_step(install_nix()) .add_step(configure_local_nix_cache()) .add_step(cachix_action(cachix_filter)) .add_step(build(&flake_output)) .add_step(export_to_local_nix_cache()), Platform::Windows => unimplemented!(), }; NamedJob { name: format!("build_nix_{platform}_{arch}"), job, }}
use crate::tasks::workflows::{
runners::{Arch, Platform},
steps::{CommonJobConditions, CommonPermissionSets, DEFAULT_REPOSITORY_OWNER_GUARD, NamedJob},
};
use super::{runners, steps, steps::named, vars};
use gh_workflow::*;
/// Generates the nix_build.yml workflow, which builds the Nix package on PRs
/// that carry the `run-nix` or `run-bundling` label. The Nix jobs live only
/// here (not in run_bundling.yml) so that setting both labels doesn't build
/// them twice.
pub fn nix_build() -> Workflow {
let [nix_linux_x86_64, nix_mac_aarch64] = nix_pr_jobs(&["run-nix", "run-bundling"]);
named::workflow()
.with_minimal_permissions()
.on(Event::default().pull_request(
PullRequest::default().types([PullRequestType::Labeled, PullRequestType::Synchronize]),
))
.concurrency(
Concurrency::new(Expression::new(
"${{ github.workflow }}-${{ github.head_ref || github.ref }}",
))
.cancel_in_progress(true),
)
.add_env(("CARGO_TERM_COLOR", "always"))
.add_env(("RUST_BACKTRACE", "1"))
.add_job(nix_linux_x86_64.name, nix_linux_x86_64.job)
.add_job(nix_mac_aarch64.name, nix_mac_aarch64.job)
}
/// Builds the pair of PR Nix jobs (Linux x86_64 + macOS aarch64), each gated so
/// they run when any of the given PR `labels` is present (on
/// `labeled`/`synchronize` events).
fn nix_pr_jobs(labels: &[&str]) -> [NamedJob; 2] {
let labeled = labels
.iter()
.map(|label| format!("github.event.label.name == '{label}'"))
.collect::<Vec<_>>()
.join(" || ");
let synchronized = labels
.iter()
.map(|label| format!("contains(github.event.pull_request.labels.*.name, '{label}')"))
.collect::<Vec<_>>()
.join(" || ");
[
(Platform::Linux, Arch::X86_64),
(Platform::Mac, Arch::AARCH64),
]
.map(|(platform, arch)| {
let mut job = build_nix(
platform,
arch,
"default",
// don't push PR builds to the cache
Some("-zed-editor-[0-9.]*"),
&[],
);
job.job = job.job.cond(Expression::new(format!(
"{DEFAULT_REPOSITORY_OWNER_GUARD} && \
((github.event.action == 'labeled' && ({labeled})) || \
(github.event.action == 'synchronize' && ({synchronized})))"
)));
job
})
}
pub(crate) fn build_nix(
platform: Platform,
arch: Arch,
flake_output: &str,
cachix_filter: Option<&str>,
deps: &[&NamedJob],
) -> NamedJob {
pub fn install_nix() -> Step<Use> {
named::uses(
"cachix",
"install-nix-action",
"02a151ada4993995686f9ed4f1be7cfbb229e56f", // v31
)
.add_with(("github_access_token", vars::GITHUB_TOKEN))
}
pub fn cachix_action(cachix_filter: Option<&str>) -> Step<Use> {
let mut step = named::uses(
"cachix",
"cachix-action",
"0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad", // v16
)
.add_with(("name", "zed"))
.add_with(("authToken", vars::CACHIX_AUTH_TOKEN))
.add_with(("cachixArgs", "-v"));
if let Some(cachix_filter) = cachix_filter {
step = step.add_with(("pushFilter", cachix_filter));
}
step
}
pub fn build(flake_output: &str) -> Step<Run> {
named::bash(&format!(
"nix build .#{} -L --accept-flake-config",
flake_output
))
}
// After install-nix, register ~/nix-cache as a local binary cache
// substituter so nix pulls from it on demand during builds (no bulk
// import). Also restart the daemon so it picks up the new config.
pub fn configure_local_nix_cache() -> Step<Run> {
named::bash(indoc::indoc! {r#"
mkdir -p ~/nix-cache
echo "extra-substituters = file://$HOME/nix-cache?priority=10" | sudo tee -a /etc/nix/nix.conf
echo "require-sigs = false" | sudo tee -a /etc/nix/nix.conf
sudo launchctl kickstart -k system/org.nixos.nix-daemon
"#})
}
// Incrementally copy only new store paths from the build result's
// closure into the local binary cache for the next run.
pub fn export_to_local_nix_cache() -> Step<Run> {
named::bash(indoc::indoc! {r#"
if [ -L result ]; then
echo "Copying build closure to local binary cache..."
nix copy --to "file://$HOME/nix-cache" ./result || echo "Warning: nix copy to local cache failed"
else
echo "No build result found, skipping cache export."
fi
"#})
.if_condition(Expression::new("always()"))
}
let runner = match platform {
Platform::Windows => unimplemented!(),
Platform::Linux => runners::LINUX_X86_BUNDLER,
Platform::Mac => runners::MAC_DEFAULT,
};
let mut job = Job::default()
.timeout_minutes(60u32)
.continue_on_error(true)
.with_repository_owner_guard()
.runs_on(runner)
.add_env(("ZED_CLIENT_CHECKSUM_SEED", vars::ZED_CLIENT_CHECKSUM_SEED))
.add_env(("ZED_MINIDUMP_ENDPOINT", vars::ZED_SENTRY_MINIDUMP_ENDPOINT))
.add_env((
"ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON",
vars::ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON,
))
.add_env(("GIT_LFS_SKIP_SMUDGE", "1")) // breaks the livekit rust sdk examples which we don't actually depend on
.add_step(steps::checkout_repo());
if deps.len() > 0 {
job = job.needs(deps.iter().map(|d| d.name.clone()).collect::<Vec<String>>());
}
// On Linux, `cache: nix` uses bind-mounts so the /nix store is available
// before install-nix-action runs — no extra steps needed.
//
// On macOS, `/nix` lives on a read-only root filesystem and the nscloud
// cache action cannot mount or symlink there. Instead we cache a
// user-writable directory (~/nix-cache) as a local binary cache and
// register it as a nix substituter. Nix then pulls paths from it on
// demand during builds (zero-copy at startup), and after building we
// incrementally copy new paths into the cache for the next run.
job = match platform {
Platform::Linux => job
.add_step(steps::cache_nix_dependencies_namespace())
.add_step(install_nix())
.add_step(cachix_action(cachix_filter))
.add_step(build(&flake_output)),
Platform::Mac => job
.add_step(steps::cache_nix_store_macos())
.add_step(install_nix())
.add_step(configure_local_nix_cache())
.add_step(cachix_action(cachix_filter))
.add_step(build(&flake_output))
.add_step(export_to_local_nix_cache()),
Platform::Windows => unimplemented!(),
};
NamedJob {
name: format!("build_nix_{platform}_{arch}"),
job,
}
}