git.delta.rocks / jrsonnet / refs/commits / 45c49ea21363

difftreelog

feat introduce toplevel-fleet build attribute

xrlvrxmrYaroslav Bolyukin2026-01-22parent: #c653e89.patch.diff
in: trunk

3 files changed

modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
after · cmds/fleet/src/cmds/build_systems.rs
1use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf};23use anyhow::Result;4use clap::Parser;5use fleet_base::{6	deploy::{DeployAction, deploy_task, upload_task},7	host::{Config, DeployKind, GenerationStorage},8	opts::FleetOpts,9};10use nix_eval::nix_go;11use tokio::task::{LocalSet, spawn_blocking};12use tracing::{Instrument, error, field, info, info_span, warn};1314#[derive(Parser)]15pub struct Deploy {16	/// Disable automatic rollback17	#[clap(long)]18	disable_rollback: bool,19	/// Action to execute after system is built20	action: DeployAction,21}2223#[derive(Parser, Clone)]24pub struct BuildSystems {25	/// Attribute to build. Systems are deployed from "toplevel-fleet" attr, well-known used attributes26	/// are "sdImage"/"isoImage", and your configuration may include any other build attributes.27	#[clap(long, default_value = "toplevel-fleet")]28	build_attr: String,29}3031async fn build_task(config: Config, hostname: String, build_attr: &str) -> Result<PathBuf> {32	info!("building");33	let host = config.host(&hostname).await?;34	// let action = Action::from(self.subcommand.clone());35	let nixos = host.nixos_config().await?;36	let drv = nix_go!(nixos.system.build[{ build_attr }]);37	let out_output = spawn_blocking(move || drv.build("out"))38		.await39		.expect("system derivation build should not panic")?;4041	// We already have system profiles for backups.42	if !host.local {43		info!("adding gc root");44		let mut cmd = config.local_host().cmd("nix").await?;45		cmd.arg("build")46			.comparg(47				"--profile",48				format!(49					"/nix/var/nix/profiles/{}-{hostname}",50					config.data().gc_root_prefix51				),52			)53			.arg(&out_output);54		cmd.sudo().run_nix().await?;55	}5657	Ok(out_output)58}5960impl BuildSystems {61	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {62		let hosts = opts.filter_skipped(config.list_hosts().await?).await?;63		let set = LocalSet::new();64		let build_attr = self.build_attr.clone();65		for host in hosts {66			let config = config.clone();67			let span = info_span!("build", host = field::display(&host.name));68			let hostname = host.name;69			let build_attr = build_attr.clone();70			set.spawn_local(71				(async move {72					let built = match build_task(config, hostname.clone(), &build_attr).await {73						Ok(path) => path,74						Err(e) => {75							error!("failed to deploy host: {}", e);76							return;77						}78					};79					// TODO: Handle error80					let mut out = current_dir().expect("cwd exists");81					out.push(format!("built-{hostname}"));8283					info!("linking iso image to {:?}", out);84					if let Err(e) = symlink(built, out) {85						error!("failed to symlink: {e}")86					}87				})88				.instrument(span),89			);90		}91		set.await;92		Ok(())93	}94}9596impl Deploy {97	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {98		let hosts = opts.filter_skipped(config.list_hosts().await?).await?;99		let set = LocalSet::new();100		for host in hosts.into_iter() {101			let config = config.clone();102			let span = info_span!("deploy", host = field::display(&host.name));103			let hostname = host.name.clone();104			let opts = opts.clone();105			if let Some(deploy_kind) = opts.action_attr::<DeployKind>(&host, "deploy_kind").await? {106				host.set_deploy_kind(deploy_kind);107			};108			if let Some(destination) = opts.action_attr::<String>(&host, "dest").await? {109				host.set_session_destination(destination);110			};111			if let Some(legacy) = opts.action_attr::<bool>(&host, "legacy_ssh_store").await? {112				host.set_legacy_ssh_store(legacy);113			};114115			set.spawn_local(116				(async move {117					let built = match build_task(config.clone(), hostname.clone(), "toplevel-fleet").await118					{119						Ok(path) => path,120						Err(e) => {121							error!("failed to build host system closure: {:?}", e);122							return;123						}124					};125126					let deploy_kind = match host.deploy_kind().await {127						Ok(v) => v,128						Err(e) => {129							error!("failed to query target deploy kind: {e}");130							return;131						}132					};133134					// TODO: Make disable_rollback a host attribute instead135					let mut disable_rollback = self.disable_rollback;136					if !disable_rollback && deploy_kind != DeployKind::Fleet {137						warn!("disabling rollback, as not supported by non-fleet deployment kinds");138						disable_rollback = true;139					}140141					let remote_path =142						match upload_task(&config, &host, GenerationStorage::Deployer, built).await143						{144							Ok(v) => v,145							Err(e) => {146								error!("upload failed: {e}");147								return;148							}149						};150151					if let Err(e) = deploy_task(152						self.action,153						&host,154						remote_path,155						match opts.action_attr(&host, "specialisation").await {156							Ok(v) => v,157							_ => {158								error!("unreachable? failed to get specialization");159								return;160							}161						},162						disable_rollback,163					)164					.await165					{166						error!("activation failed: {e}");167					}168				})169				.instrument(span),170			);171		}172		set.await;173		Ok(())174	}175}
modifiedmodules/nixos/module-list.nixdiffbeforeafterboth
--- a/modules/nixos/module-list.nix
+++ b/modules/nixos/module-list.nix
@@ -4,4 +4,5 @@
   ./rollback.nix
   ./nix-sign.nix
   ./online.nix
+  ./top-level.nix
 ]
addedmodules/nixos/top-level.nixdiffbeforeafterboth
--- /dev/null
+++ b/modules/nixos/top-level.nix
@@ -0,0 +1,71 @@
+{
+  pkgs,
+  config,
+  lib,
+}:
+let
+  inherit (lib.strings) optionalString;
+  # FIXME: Should not be copy-pasted, instead nixpkgs should export systemBuilder directly
+  systemBuilder = ''
+    mkdir $out
+
+    ${
+      if config.boot.initrd.enable && config.boot.initrd.systemd.enable then
+        ''
+          # This must not be a symlink or the abs_path of the grub builder for the tests
+          # will resolve the symlink and we end up with a path that doesn't point to a
+          # system closure.
+          cp "$systemd/lib/systemd/systemd" $out/init
+
+          ${lib.optionalString (!config.system.nixos-init.enable) ''
+            cp ${config.system.build.bootStage2} $out/prepare-root
+            substituteInPlace $out/prepare-root --subst-var-by systemConfig $out
+          ''}
+        ''
+      else
+        ''
+          cp ${config.system.build.bootStage2} $out/init
+          substituteInPlace $out/init --subst-var-by systemConfig $out
+        ''
+    }
+
+    ln -s ${config.system.build.etc}/etc $out/etc
+
+    ${lib.optionalString config.system.etc.overlay.enable ''
+      ln -s ${config.system.build.etcMetadataImage} $out/etc-metadata-image
+      ln -s ${config.system.build.etcBasedir} $out/etc-basedir
+    ''}
+
+    ln -s ${config.system.path} $out/sw
+    ln -s "$systemd" $out/systemd
+
+    echo -n "systemd ${toString config.systemd.package.interfaceVersion}" > $out/init-interface-version
+    echo -n "$nixosLabel" > $out/nixos-version
+    echo -n "${config.boot.kernelPackages.stdenv.hostPlatform.system}" > $out/system
+
+    ${config.system.systemBuilderCommands}
+
+    cp "$extraDependenciesPath" "$out/extra-dependencies"
+
+      ${config.boot.bootspec.writer}
+      ${optionalString config.boot.bootspec.enableValidation ''${config.boot.bootspec.validator} "$out/${config.boot.bootspec.filename}"''}
+  '';
+in
+{
+  system.build.toplevel-fleet = pkgs.stdenvNoCC.mkDerivation (
+    {
+      name = "nixos-system-${config.system.name}-${config.system.nixos.label}";
+      preferLocalBuild = true;
+      allowSubstitutes = false;
+      passAsFile = [ "extraDependencies" ];
+      buildCommand = systemBuilder;
+
+      systemd = config.systemd.package;
+
+      nixosLabel = config.system.nixos.label;
+
+      inherit (config.system) extraDependencies;
+    }
+    // config.system.systemBuilderArgs
+  );
+}