Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<short-sha>@<branch>" 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:
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 23 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
# "<short-sha>@<branch>" 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
Expand Down Expand Up @@ -119,7 +141,7 @@ iso_comp_field = $(if $(ISO_COMPRESSION),isoImage.squashfsCompression = "$(ISO_C
# $(1) = host (nixosConfigurations.<host>)
# $(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
Expand Down
16 changes: 8 additions & 8 deletions nixos/_images/appliance/iso.nix
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
# appliance's image identity.

{
config,
lib,
pkgs,
...
Expand All @@ -38,16 +37,17 @@
# Boot-menu label (both the BIOS/isolinux and EFI/grub entries). The label is
# "<distroName> <version><appendToMenuLabel>"; 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
# "<baseName>.iso", and defaults baseName to "nixos-<version>-<arch>". 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}";
}
61 changes: 61 additions & 0 deletions nixos/_images/base/iso.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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: <title> (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
Expand Down
141 changes: 87 additions & 54 deletions nixos/_images/box-turnkey.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 = {
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading