diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 64ec4ee..911ac1d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -156,6 +156,12 @@ jobs: # keep their plain names. CODER_BOX_PR_TITLE: ${{ github.event.pull_request.title }} CODER_BOX_PR_NUMBER: ${{ github.event.pull_request.number }} + # Branch name for the boot-screen label's "@" stamp. A + # PR checkout is a detached HEAD (so the Makefile's `git rev-parse + # --abbrev-ref HEAD` would say "HEAD"); github.head_ref is the real source + # branch on pull_request events and empty otherwise (the Makefile then + # falls back to the local branch name). + CODER_BOX_BRANCH: ${{ github.head_ref }} strategy: fail-fast: false matrix: diff --git a/.gitignore b/.gitignore index 6b99441..0aee9d0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ hosts/*/local.nix .terraform/ *.bak +# Editor / OS cruft and scratch dirs. +.DS_Store +tmp/ + # Appliance image build outputs (Makefile --out-link target dir + artifacts) out/ *.iso diff --git a/Makefile b/Makefile index 9d61a1d..93a1961 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,21 @@ NIX ?= nix FLAKE ?= . +# Every target below drives the flake CLI (`nix build`, `nix flake check`, +# `nix fmt`), so enable the flakes + nix-command interface here instead of +# making each developer turn it on in their global nix.conf. Two deliberate +# choices keep this non-invasive: +# * `--extra-experimental-features` is ADDITIVE — it ORs with whatever the +# user's nix.conf already enables, so it never replaces their settings +# (unlike the non-`extra-` form, which overwrites the whole list). +# * We APPEND to $(NIX) with `+=` rather than redefining it, so an env-set +# `NIX` (e.g. a custom binary/path) is preserved — we add the flag, we +# don't clobber the user's choice. (`+=` honours an environment NIX; a +# command-line `make NIX=…` still wins outright, as expected.) +# Set `NIX_EXTRA_EXPERIMENTAL_FEATURES=` (empty) to opt out without editing this file. +NIX_EXTRA_EXPERIMENTAL_FEATURES ?= nix-command flakes +NIX += $(if $(strip $(NIX_EXTRA_EXPERIMENTAL_FEATURES)),--extra-experimental-features "$(NIX_EXTRA_EXPERIMENTAL_FEATURES)") + # Build parallelism + substituter tuning, applied to every Nix invocation # (build and eval) via NIX_PERF_FLAGS below. max-jobs runs independent # derivations concurrently and cores=0 lets each build use all CPUs; @@ -71,6 +86,13 @@ NIX_OUTPUT_FLAGS ?= # checkout (the module then falls back to self.rev / "unknown"). GIT_REV := $(shell git rev-parse HEAD 2>/dev/null)$(shell git diff-index --quiet HEAD -- 2>/dev/null || echo -dirty) +# Git branch injected into images alongside the rev, for the boot-screen label's +# "@" stamp (coderBox.branch). Prefer an explicit +# CODER_BOX_BRANCH (CI sets it from github.head_ref, since a PR checkout is a +# detached HEAD where `rev-parse --abbrev-ref HEAD` would just say "HEAD"); fall +# back to the local branch name. Empty/"HEAD" is dropped from the label. +GIT_BRANCH := $(or $(CODER_BOX_BRANCH),$(shell git rev-parse --abbrev-ref HEAD 2>/dev/null)) + # Optional PR title + number woven into the image's pretty version name # (boot-menu label + ISO file name) via coderBox.prTitle / coderBox.prNumber. # Not injected through the Nix expression (an arbitrary title would need @@ -119,7 +141,7 @@ iso_comp_field = $(if $(ISO_COMPRESSION),isoImage.squashfsCompression = "$(ISO_C # $(1) = host (nixosConfigurations.) # $(2) = arch token (empty = builder's native arch) # $(3) = extra module fields (nix attrset body, may be empty) -box_cfg = let f = builtins.getFlake (toString ./.); in (f.nixosConfigurations.$(1).extendModules { modules = [ { nixpkgs.hostPlatform = "$(if $(2),$(call norm_arch,$(2)),$${builtins.currentSystem})"; coderBox.rev = "$(GIT_REV)"; $(3) } ]; }).config.system.build +box_cfg = let f = builtins.getFlake (toString ./.); in (f.nixosConfigurations.$(1).extendModules { modules = [ { nixpkgs.hostPlatform = "$(if $(2),$(call norm_arch,$(2)),$${builtins.currentSystem})"; coderBox.rev = "$(GIT_REV)"; coderBox.branch = "$(GIT_BRANCH)"; $(3) } ]; }).config.system.build define box_build @mkdir -p out diff --git a/nixos/_images/appliance/iso.nix b/nixos/_images/appliance/iso.nix index 8f4e473..a700954 100644 --- a/nixos/_images/appliance/iso.nix +++ b/nixos/_images/appliance/iso.nix @@ -21,7 +21,6 @@ # appliance's image identity. { - config, lib, pkgs, ... @@ -38,16 +37,17 @@ # Boot-menu label (both the BIOS/isolinux and EFI/grub entries). The label is # " "; the default append is # " Installer", which is misleading here since this is the appliance, not the - # installer. Leading space is required (it's concatenated directly). The - # PR-title suffix (coderBox.prMenuSuffix, set in box-turnkey.nix) is empty - # unless this is a PR preview build. - isoImage.appendToMenuLabel = " - Coder Box Appliance${config.coderBox.prMenuSuffix}"; + # installer. Leading space is required (it's concatenated directly). The label + # carries no PR identity — a PR preview shows the same " - Coder Box Appliance" + # entry as a release build (PR builds surface their identity off-menu via + # /etc/coder-box-pr; see box-turnkey.nix). + isoImage.appendToMenuLabel = " - Coder Box Appliance"; # ISO file name. iso-image.nix derives isoName from image.baseName as # ".iso", and defaults baseName to "nixos--". We # override baseName (mkForce, to win over that default) but keep the arch # suffix so the file is e.g. coder-box-appliance-aarch64-linux.iso — the arch # is visible in the name and x86_64/aarch64 ISOs don't collide in ./out. The - # PR-slug suffix (coderBox.prFileSuffix) is empty unless this is a PR preview - # build (e.g. coder-box-appliance-x86_64-linux-pr-fix-the-thing.iso). - image.baseName = lib.mkForce "coder-box-appliance-${pkgs.stdenv.hostPlatform.system}${config.coderBox.prFileSuffix}"; + # name is constant for a given flavour/arch and carries no PR identity, so a PR + # preview ISO has the same file name as a release build. + image.baseName = lib.mkForce "coder-box-appliance-${pkgs.stdenv.hostPlatform.system}"; } diff --git a/nixos/_images/base/iso.nix b/nixos/_images/base/iso.nix index 617f8d4..8b7ac1e 100644 --- a/nixos/_images/base/iso.nix +++ b/nixos/_images/base/iso.nix @@ -35,6 +35,67 @@ boot.loader.systemd-boot.enable = lib.mkForce false; boot.loader.efi.canTouchEfiVariables = lib.mkForce false; + # ── Show the build identity on the boot screen as a footer label ───────────── + # The boot-menu ENTRY can't carry the build identity: it's a single, + # non-wrapping line in a fixed-width GRUB/isolinux menu, so a long PR title + # overflows and gets clipped (and widening the menu makes it span the whole + # screen). Instead, render coderBox.bootLabel — e.g. + # "Coder Box - PR #46: (abc123@branch)", or just + # "Coder Box - abc123@branch" with no PR — as a standalone GRUB *label* + # (boot-screen chrome, not a selectable entry) shown as a footer UNDER the menu. + # The label is ALWAYS present (every build has a commit/branch), so this also + # makes the footer visible on local/non-PR builds, not just CI PR previews. + # + # Key gotcha: the stock theme's boot_menu fills almost the whole height with an + # opaque white pixmap (boot_menu_*.png), and GRUB paints that box OVER any + # label that overlaps it — so a footer placed inside the box is invisible. The + # menu only has a handful of entries, so we shrink boot_menu's height to free + # the lower area of the screen, then place the label there on the plain + # background (dark text, good contrast). The menu WIDTH is untouched (still the + # stock 800px), so the menu doesn't blow up horizontally like the earlier + # full-width experiment. EFI/GRUB only; the BIOS/isolinux text menu has no + # theme, so there the identity is available off-menu via /etc/coder-box-pr + + # the installer console banner. + # + # substituteInPlace uses --replace-fail so a future nixpkgs theme reflow fails + # the build loudly instead of silently dropping the footer. + isoImage.grubTheme = + let + inherit (config.coderBox) bootLabel; + # theme.txt wraps the text in double quotes; neutralise quotes/backslashes + # so an arbitrary title can't break the theme parser. + bootLabelThemeSafe = builtins.replaceStrings [ "\"" "\\" ] [ "'" "" ] bootLabel; + in + pkgs.runCommand "coder-box-grub2-theme" { } '' + cp -r ${pkgs.nixos-grub2-theme} "$out" + chmod -R u+w "$out" + + # 1) Shrink the menu box so its opaque pixmap stops at ~46% of the screen + # (~1/3 shorter than the previous 62%), leaving the lower area free for + # the footer (stock fills to the progress bar and would cover it). + substituteInPlace "$out/theme.txt" \ + --replace-fail 'height = 100%-3%-100-3%-3%-32-3%' 'height = 46%-100-6%' + + # 2) Footer label just BELOW the shrunk box (top ~50%), so it sits close to + # the menu rather than far down the screen. Its left/width match the menu + # box (left = 50%-400, width = 800) so the centred text lines up with the + # rectangle instead of spanning the full screen. Quoted heredoc so the + # build shell does no expansion; the text is already substituted by Nix + # (and quote/backslash-sanitised). + cat >> "$out/theme.txt" <<'EOF' + + + label { + top = 50% + left = 50%-400 + width = 800 + align = "center" + color = "#232627" + font = "DejaVu Regular" + text = "${bootLabelThemeSafe}" + } + EOF + ''; + # Tools install.sh expects at runtime, shipped in the base layer so every ISO # flavour (installer + appliance) has them: # - dmidecode: read SMBIOS/DMI (board model, BIOS, serial) for the diff --git a/nixos/_images/box-turnkey.nix b/nixos/_images/box-turnkey.nix index cce4d64..14eed13 100644 --- a/nixos/_images/box-turnkey.nix +++ b/nixos/_images/box-turnkey.nix @@ -22,49 +22,58 @@ }: let - # ── Pretty version name helpers ────────────────────────────────────────────── - # The "pretty version name" is the human-facing identity woven into the boot - # menu label and the ISO file name. For a PR preview build CI injects the PR - # title (coderBox.prTitle); when present it is appended so reviewers can tell - # at a glance which PR an image came from. Empty for tag/main/local builds. - inherit (config.coderBox) prTitle; - - # Boot-menu form: keep the raw title but neutralise the few characters that - # would break the generated isolinux/grub menu (quotes, backslashes, newlines). - menuTitle = builtins.replaceStrings [ "\n" "\"" "\\" ] [ " " "'" "" ] prTitle; - - # File-name form: lowercase the title and reduce it to a [a-z0-9-] slug so the - # ISO name stays portable. Collapse dash runs, trim edges, and cap the length - # so the file name doesn't balloon for a long PR title. - slugify = - title: - let - lower = lib.toLower title; - safe = lib.concatMapStrings (c: if builtins.match "[a-z0-9]" c != null then c else "-") ( - lib.stringToCharacters lower - ); - collapse = - s: - let - r = builtins.replaceStrings [ "--" ] [ "-" ] s; - in - if r == s then s else collapse r; - capped = - let - c = collapse safe; - in - if builtins.stringLength c > 40 then builtins.substring 0 40 c else c; - in - lib.removeSuffix "-" (lib.removePrefix "-" capped); - - prSlug = slugify prTitle; - - # PR number (the GitHub PR "ID"). CI injects it alongside the title; included - # in both the menu label ("#46") and the file name ("-46") when present so an - # image can be traced straight back to its PR. Empty for non-PR builds. - inherit (config.coderBox) prNumber; + # ── PR-preview identity helpers ────────────────────────────────────────────── + # For a PR-preview build CI injects the PR title + number (coderBox.prTitle / + # coderBox.prNumber) so reviewers can tell which PR an image came from. This is + # deliberately kept OFF the boot menu and OUT of the ISO file name: + # * The boot-menu entry is a single, non-wrapping line in a fixed-width + # GRUB/isolinux menu — a long title overflows and gets clipped, and + # widening the menu makes it span the whole screen, which looks broken. + # * The ISO file name stays constant per flavour/arch regardless of build, + # so it's predictable and downloads/links never change. + # The PR identity is surfaced off-menu instead: recorded at /etc/coder-box-pr + # and printed by the installer console banner (see prFull / ../installer/iso.nix). + # Empty for tag/main/local builds. + inherit (config.coderBox) + prTitle + prNumber + rev + branch + ; + + # Flatten newlines so the identity stays a single line in the file / banner. + prTitleClean = builtins.replaceStrings [ "\n" ] [ " " ] prTitle; prNumMenu = lib.optionalString (prNumber != "") " #${prNumber}"; - prNumFile = lib.optionalString (prNumber != "") "-${prNumber}"; + + # Full PR identity for off-menu surfaces (baked image record + installer + # console banner): "PR #46: <full title>". Empty for non-PR builds. + prFull = lib.optionalString (prTitle != "") "PR${prNumMenu}: ${prTitleClean}"; + + # Short build revision for the human-facing version stamp (full 40-char hashes + # are unwieldy on the boot screen). Keep the "-dirty" suffix if present. + revShort = + if rev == "unknown" || rev == "" then + "unknown" + else + let + dirty = lib.hasSuffix "-dirty" rev; + bare = lib.removeSuffix "-dirty" rev; + in + builtins.substring 0 12 bare + lib.optionalString dirty "-dirty"; + + # A detached-HEAD checkout (the default for GitHub Actions' PR builds) reports + # branch "HEAD", which is noise — treat it like an unknown branch and drop it. + branchClean = if branch == "HEAD" || branch == "unknown" then "" else branch; + + # Version stamp woven into the boot-screen label, using the conventional + # "<short-sha>@<branch>" form (branch omitted when unknown/detached). + versionStamp = revShort + lib.optionalString (branchClean != "") "@${branchClean}"; + + # Boot-screen label (the GRUB footer in ../base/iso.nix). ALWAYS present so the + # image's identity is visible on the boot screen regardless of build type: + # with a PR: "Coder Box - PR #46: <title> (<short-sha>@<branch>)" + # without a PR: "Coder Box - <short-sha>@<branch>" + bootLabel = "Coder Box - " + (if prFull != "" then "${prFull} (${versionStamp})" else versionStamp); in { imports = [ @@ -92,6 +101,17 @@ in description = "Git revision this Coder box image was built from."; }; + # Git branch this image was built from, woven into the boot-screen label as + # "<short-sha>@<branch>". Like coderBox.rev, the Makefile injects it (path + # flakerefs carry no git metadata); CI can also set CODER_BOX_BRANCH for PR + # builds where the checkout is a detached HEAD. Empty/"HEAD" is treated as + # unknown and dropped from the label. + options.coderBox.branch = lib.mkOption { + type = lib.types.str; + default = builtins.getEnv "CODER_BOX_BRANCH"; + description = "Git branch this Coder box image was built from (woven into the boot-screen label when known)."; + }; + # PR title for a pull-request preview build. CI sets it via the # CODER_BOX_PR_TITLE environment variable (read here under `--impure`, which # every image build already uses), so an arbitrary title never has to be @@ -101,7 +121,7 @@ in options.coderBox.prTitle = lib.mkOption { type = lib.types.str; default = builtins.getEnv "CODER_BOX_PR_TITLE"; - description = "Pull-request title this image was built for, woven into the pretty version name (boot-menu label + ISO file name) when non-empty."; + description = "Pull-request title this image was built for. Surfaced off the boot menu (recorded at /etc/coder-box-pr and printed by the installer console) when non-empty; never woven into the menu label or ISO file name, which carry only the PR number."; }; # PR number (GitHub PR "ID") for a pull-request preview build. CI sets it via @@ -112,24 +132,29 @@ in description = "Pull-request number (ID) this image was built for, woven into the pretty version name when non-empty."; }; - # Derived, read-only fragments so every image flavour appends the PR identity - # the same way. coderBox.prMenuSuffix goes on the boot-menu label; - # coderBox.prFileSuffix goes on the ISO file name. Both carry the PR number - # ("#46" / "-46") when known, and are empty when there's no PR title, so - # they're safe to interpolate unconditionally. - options.coderBox.prMenuSuffix = lib.mkOption { + # Full, untruncated PR identity ("PR #46: <title>") for surfaces that AREN'T + # the boot menu — recorded at /etc/coder-box-pr (below) and printed by the + # installer console banner (../installer/iso.nix). The boot-menu label and the + # ISO file name deliberately carry NO PR identity (neither title nor number): + # the menu entry stays a clean "Coder Box <flavour> (<rev>)" and the file name + # is constant per flavour/arch. Empty for non-PR builds. + options.coderBox.prFull = lib.mkOption { type = lib.types.str; internal = true; readOnly = true; - default = lib.optionalString (prTitle != "") " - PR${prNumMenu}: ${menuTitle}"; - description = "Boot-menu label fragment naming the PR this image was built for (empty for non-PR builds)."; + default = prFull; + description = "Full PR identity (number + untruncated title) for off-menu surfaces (empty for non-PR builds)."; }; - options.coderBox.prFileSuffix = lib.mkOption { + + # Boot-screen label (the GRUB footer in ../base/iso.nix), e.g. + # "Coder Box - PR #46: <title> (abc123def456@my-branch)" or, without a PR, + # "Coder Box - abc123def456@my-branch". Always non-empty. + options.coderBox.bootLabel = lib.mkOption { type = lib.types.str; internal = true; readOnly = true; - default = lib.optionalString (prSlug != "") "-pr${prNumFile}-${prSlug}"; - description = "ISO file-name fragment naming the PR this image was built for (empty for non-PR builds)."; + default = bootLabel; + description = "Boot-screen footer label (build identity: PR + short-sha@branch); always present."; }; config = { @@ -168,6 +193,14 @@ in && !(lib.hasSuffix ".raw" base); }; + # Record the full PR identity (number + untruncated title) in the image so + # it's available off the boot menu, where the title can't fit. The installer + # console prints it (../installer/iso.nix); on any flavour it can also be + # read with `cat /etc/coder-box-pr`. Only created for PR-preview builds. + environment.etc."coder-box-pr" = lib.mkIf (prFull != "") { + text = prFull + "\n"; + }; + # Make the pinned nixpkgs resolvable on the box so `nix` / flake commands # behave like an installed system, without shipping a channel. nix.registry.nixpkgs.flake = inputs.nixpkgs; diff --git a/nixos/_images/installer/iso.nix b/nixos/_images/installer/iso.nix index d5601e1..70fdd95 100644 --- a/nixos/_images/installer/iso.nix +++ b/nixos/_images/installer/iso.nix @@ -25,8 +25,6 @@ let boxRev = config.coderBox.rev; - # Short form for the boot-menu label (full 40-char hashes are unwieldy there). - boxRevShort = if boxRev == "unknown" then "unknown" else builtins.substring 0 12 boxRev; # Launcher run inside the preopened Konsole: cd into the baked repo, run the # installer as root (passwordless sudo is configured in configuration.nix), @@ -42,6 +40,12 @@ let # wins (install.sh ignores --interactive when --yes is given). set -- --interactive "$@" echo "=== Coder Box installer ===" + # For PR-preview builds, print the full PR identity (number + untruncated + # title) here, off the boot menu, where it can't be clipped. The file only + # exists on PR builds (see ../box-turnkey.nix), so non-PR images print nothing. + if [ -s /etc/coder-box-pr ]; then + echo "Image: $(cat /etc/coder-box-pr)" + fi echo "Running: sudo ./install.sh $*" echo if sudo ./install.sh "$@"; then @@ -72,27 +76,23 @@ in # ── Image identity ───────────────────────────────────────────────────────── isoImage.volumeID = "CODER_BOX_INSTALLER"; # Boot-menu label (BIOS/isolinux + EFI/grub). See ../appliance/iso.nix for - # the format; leading space is required. Include the short build revision so - # the boot menu shows exactly which image you're booting. For a PR preview - # build (coderBox.prMenuSuffix non-empty) the PR reference comes first and - # the commit hash moves to the very end, so the label reads - # " - Coder Box Installer - PR #46: <title> (<rev>)"; otherwise it's just - # " - Coder Box Installer (<rev>)". - isoImage.appendToMenuLabel = - if config.coderBox.prMenuSuffix != "" then - " - Coder Box Installer${config.coderBox.prMenuSuffix} (${boxRevShort})" - else - " - Coder Box Installer (${boxRevShort})"; + # the format; leading space is required. The label is the same for every + # build — " - Coder Box Installer" — with NO build identity: the commit + # short-sha (and, for PR previews, the PR number + title) live in the + # boot-screen footer label instead (coderBox.bootLabel, see ../base/iso.nix), + # so the menu entry stays clean. The full "PR #N: <title>" is also printed by + # the installer console banner and recorded at /etc/coder-box-pr. + isoImage.appendToMenuLabel = " - Coder Box Installer"; # Record the full build revision for install.sh to print (the baked repo # under /etc/nixos-repo has no .git, so the script can't get it from git). environment.etc."coder-box-rev".text = boxRev + "\n"; # ISO file name, with arch suffix (e.g. coder-box-installer-x86_64-linux.iso). - # See ../appliance/iso.nix for why this is mkForce + arch-suffixed. The - # PR-slug suffix (coderBox.prFileSuffix) is empty unless this is a PR preview - # build (e.g. coder-box-installer-x86_64-linux-pr-fix-the-thing.iso). - image.baseName = lib.mkForce "coder-box-installer-${pkgs.stdenv.hostPlatform.system}${config.coderBox.prFileSuffix}"; + # See ../appliance/iso.nix for why this is mkForce + arch-suffixed. The name + # is constant for a given flavour/arch — it carries NO PR identity, so a PR + # preview ISO has the exact same file name as a release build. + image.baseName = lib.mkForce "coder-box-installer-${pkgs.stdenv.hostPlatform.system}"; # ── Auto-launch a full-screen Konsole that runs the installer ────────────── # box-turnkey.nix autologins straight into the Plasma (X11) desktop. For the