From 812913b2de0c3cadc7a10754d697f0521e6c2d54 Mon Sep 17 00:00:00 2001 From: Evan Culver Date: Wed, 30 Sep 2026 13:52:31 -0700 Subject: [PATCH 1/5] Resolve the base image at build time instead of pinning a version The template hard-coded a release tag, so the repo carried a version somebody had to remember to bump, and every day after a release it pointed at something stale. Packer has a built-in http data source, and sfc_base_image already publishes a release index, so the template can just ask: latest.json for the current release, /manifest.json for a specific one. No version in the repo. The index lists each image's sha256 beside its URL, which turns out to be worth more than dropping the pin. iso_checksum now comes from the same document as iso_url, so the two cannot disagree, and the file:SHA256SUMS indirection is gone along with the whole base_image_checksum variable and its "=none" escape hatch -- which existed only because a pinned tag could predate the checksum file. Pinning stays available and is the documented answer for anyone who needs two builds to produce the same bytes: -var base_image_tag=. Tracking latest means next month's rebuild starts from a different base, so the README says that plainly rather than leaving it implied. One rough edge, documented in the troubleshooting table: plain `packer validate` does not execute data sources, so it now fails with `invalid checksum: encoding/hex: invalid byte: U+003C '<'`. The flag is `packer validate -evaluate-datasources`. The other two new failure modes are in the table too -- a tag with no index 404s from the data source, and an unknown base_image_name fails on an empty collection rather than downloading the wrong thing. Verified on Linux with KVM. No tag passed, resolved the current release and enforced its digest: Trying .../v20260930.063326/ubuntu-24.04-cuda-13.2.qcow2 ?checksum=sha256%3Aac30efbe2de11060802b12fa0f27e81a316f49d814ba... Build 'qemu.sfc_base' finished after 3 minutes 55 seconds. That build is also the first to inherit the base image's sshd hardening, which only became possible with the release cut today: ubuntu hardening inherited: PasswordAuthentication no build override: removed root: root:!:... random-seed: GONE Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 37 ++++++++++++++++++------------- build.pkr.hcl | 60 ++++++++++++++++++++------------------------------- 2 files changed, 45 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index aa1a325..d73d6e6 100644 --- a/README.md +++ b/README.md @@ -100,30 +100,35 @@ driver. ## Choosing a base image -`build.pkr.hcl` pins a specific release by default, so repeated builds -reproduce the same bytes. To see what is current: +By default the build resolves the current release at build time — there is no +version to keep up to date in this repo. Packer prints the one it picked: + +``` +Trying https://tiny-llama.sfcc.xyz/v20260930.063326/ubuntu-24.04-cuda-13.2.qcow2 +``` + +To see what is available: ```bash curl -fsSL https://tiny-llama.sfcc.xyz/latest.json | jq -r '.tag, (.images[].name)' ``` -`latest.json` is the published index of the newest release: the tag, and -every image in it with its URL, SHA256 and size. Point the build at one -with `-var`: +Pick a different image from the release with `base_image_name`: ```bash -packer build -only='qemu.sfc_base' \ - -var 'base_image_tag=' \ - -var 'base_image_name=ubuntu-22.04-cuda-13.1' . +packer build -only='qemu.sfc_base' -var 'base_image_name=ubuntu-22.04-cuda-13.1' . ``` -Then pin that tag in `build.pkr.hcl` so your builds stay reproducible. +**If you need two builds to produce the same bytes, pin the release.** Tracking +the latest means a rebuild next month starts from a different base: + +```bash +packer build -only='qemu.sfc_base' -var 'base_image_tag=' . +``` -Integrity is checked against the `SHA256SUMS` published alongside each -release; Packer fetches it and picks the line matching the image it is -downloading. Releases published before `SHA256SUMS` existed don't have -one, and Packer will fail with a 404 — for those, pass -`-var 'base_image_checksum=none'`. +Integrity needs nothing from you. The release index lists each image's sha256 +next to its URL, and the build verifies the download against it — same +document, so the two cannot disagree. ## What the boilerplate is doing @@ -166,7 +171,9 @@ for the full picture. | `E: Unable to locate package packer` | Packer is not in the Ubuntu archive. See [Requirements](#requirements). | | `packer init` reports an unknown command | Your Packer predates 1.7. Ubuntu 22.04 ships 1.6.6; install a current one per Requirements. | | `Output directory 'output/...' already exists` | `packer build -force ...`, or `rm -rf output/`. | -| A 404 fetching `SHA256SUMS` | The tag you pinned predates checksums. Add `-var 'base_image_checksum=none'`. | +| `Datasource.Execute failed: HTTP request error. Response code: 404` | The tag you pinned has no release index. Drop `-var base_image_tag` to use the current release, or pick a tag from `latest.json`. | +| `invalid checksum: encoding/hex: invalid byte` from `packer validate` | Plain `validate` does not run data sources, so the digest is still unresolved. Use `packer validate -evaluate-datasources .`. | +| `The given key does not identify an element in this collection value` | `base_image_name` is not in that release. List the names with the `jq` command under [Choosing a base image](#choosing-a-base-image). | | `Could not access KVM kernel module` | No `/dev/kvm`, or you are not in the `kvm` group. Drop `-var 'accelerator=kvm'` to build under emulation. | | QEMU exits immediately on macOS | Drop `-var 'accelerator=kvm'`; macOS has no KVM. | | `Could not open '/usr/share/OVMF/...'` | Firmware auto-detection missed yours. Pass `-var 'efi_firmware_code=...' -var 'efi_firmware_vars=...'`; both must be the same variant, so a 4M code file needs a 4M vars file. | diff --git a/build.pkr.hcl b/build.pkr.hcl index e29246e..2025dd3 100644 --- a/build.pkr.hcl +++ b/build.pkr.hcl @@ -32,18 +32,19 @@ packer { variable "base_image_tag" { type = string - default = "v20260924.213848" + default = "" description = <<-EOT - Which SF Compute base image release to build on (qemu.sfc_base only). - - This is a pin, on purpose: the same tag gives you the same bytes - every time. To move to the current release: + Pin the build to one SF Compute base image release (qemu.sfc_base only). - curl -fsSL https://tiny-llama.sfcc.xyz/latest.json | jq -r .tag + Empty, the default, resolves whatever is current at build time. The + build prints the release it picked, so check the log if you need to + know which one you got. - That endpoint also lists every image in the release with its URL, - SHA256 and size -- see the README. + Set this to a tag to pin it, which is what you want if you need two + builds to produce the same bytes. Tags look like v20260930.063326 and + are listed by the release index; releases published before that index + existed cannot be pinned this way. EOT } @@ -82,24 +83,6 @@ variable "nvidia_driver_branch" { EOT } -variable "base_image_checksum" { - type = string - default = "" - - description = <<-EOT - Override the integrity check on the base image (qemu.sfc_base only). - - Empty (the default) derives it from the tag, as - "file://SHA256SUMS" -- Packer fetches that file and picks - the line matching the image it is downloading, exactly as it does - for Ubuntu's cloud images. - - Releases published before SHA256SUMS existed have no such file and - Packer fails with a 404. For those, and only those, pass - -var 'base_image_checksum=none' to skip the check. - EOT -} - variable "efi_firmware_code" { type = string default = "" @@ -142,15 +125,21 @@ variable "disk_size" { EOT } +# The release index lists each image's sha256 beside its URL, so the digest +# below comes from the same document as the download and the two cannot +# disagree. +data "http" "sfc_release" { + url = var.base_image_tag == "" ? "https://tiny-llama.sfcc.xyz/latest.json" : "https://tiny-llama.sfcc.xyz/${var.base_image_tag}/manifest.json" +} + locals { - # Public, unauthenticated CDN serving the SF Compute base images. - sfc_base_url = "https://tiny-llama.sfcc.xyz/${var.base_image_tag}" + sfc_release = jsondecode(data.http.sfc_release.body) - ubuntu_base_url = "https://cloud-images.ubuntu.com/${var.ubuntu_codename}/current" + # Empty if base_image_name is not in the release, which fails the build + # here rather than downloading something else. + sfc_image = [for i in local.sfc_release.images : i if i.name == var.base_image_name][0] - # Variable defaults can't reference locals, so the "empty means derive - # it" fallback lives here rather than on the variable. - base_image_checksum = var.base_image_checksum != "" ? var.base_image_checksum : "file:${local.sfc_base_url}/SHA256SUMS" + ubuntu_base_url = "https://cloud-images.ubuntu.com/${var.ubuntu_codename}/current" # Probe for OVMF, newest layout first. Packer's own default is /usr/share/OVMF/ # OVMF_VARS.fd, which does not exist on Ubuntu 24.04 -- the build gets @@ -201,11 +190,8 @@ source "qemu" "sfc_base" { # The qcow2 rather than the .raw published alongside it: same image, # ~3G instead of ~12G over the wire. format = "raw" below converts on # the way out, so what lands in output/ is still raw. - iso_url = "${local.sfc_base_url}/${var.base_image_name}.qcow2" - # Released tags publish a SHA256SUMS covering every artifact; Packer - # picks the line matching the iso_url basename. Releases built before - # SHA256SUMS existed have no such file -- see base_image_checksum. - iso_checksum = local.base_image_checksum + iso_url = local.sfc_image.qcow2.url + iso_checksum = "sha256:${local.sfc_image.qcow2.sha256}" vm_name = "${var.base_image_name}-custom.raw" output_directory = "output/sfc_base" From 2eb56a6e402ad62abaf2b05d25b58167ea50ab3a Mon Sep 17 00:00:00 2001 From: Evan Culver Date: Fri, 2 Oct 2026 10:13:09 -0700 Subject: [PATCH 2/5] Fix what a first-time reader actually hits An end-user review walked the README literally. Four things it says are wrong, and in one case following it leaves a template that will not load. The -only warning was false. Packer exits 1 on a mistyped -only and lists the valid build names; it does not, as this README claimed, silently succeed having run nothing. That claim came from testing `packer validate`, which does behave that way, and generalizing to `packer build` without checking. Quick start spent a paragraph telling people to audit their terminal for a non-event, and the troubleshooting table taught a diagnosis for a symptom nobody will see. "Making it yours" told readers to delete three things to drop the stock-Ubuntu path, and omitted the fourth: the "source.qemu.ubuntu" entry in the build block's sources list. Doing exactly what it said produced `Error: Unknown source qemu.ubuntu`, with no row in the troubleshooting table to explain it. The README broke the template and then offered no way out. Now four bullets, re-tested by following it to the letter. "Pin the release if you need two builds to produce the same bytes" was false in a way that mattered, because it was the stated reason to keep pinning at all: customize.sh runs apt-get install, so pinning the base image does not pin the Ubuntu archive. Now describes what pinning actually buys -- a repeatable starting point, same driver, CUDA and kernel -- and says plainly that it is not bit-for-bit. The two network failures the new resolver introduces were undocumented and are the ones a laptop hits: DNS/offline, and a captive portal whose HTML lands in a jsondecode error. Both now have rows, as does the case where a 404 means the index is missing rather than a bad pin -- the old row told people to drop a flag they never passed. Also: builds now write output/sfc_base/manifest.json recording the release they resolved, which is what makes pinning usable (build once, read the tag, pin it). The output filename tracks base_image_name, which the upload command silently contradicted. The stock-Ubuntu path now says where its artifact lands. A qemu-img check before pushing 20 GiB. The quoted sample output was not what Packer prints. The CDN host is named as ours once, rather than appearing as a bare domain beside a sha256 the reader is asked to trust. "Image requirements" moved after the troubleshooting table, since it opens by addressing readers who are not following this README. Not taken: a request_timeout on the data source -- not a supported argument in Packer 1.15, verified. -var accelerator=hvf for Intel Macs is probably a large win there but is untested, so it stays unwritten. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 110 ++++++++++++++++++++++++++++++++++---------------- build.pkr.hcl | 24 +++++++++-- 2 files changed, 95 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index d73d6e6..3da4765 100644 --- a/README.md +++ b/README.md @@ -61,14 +61,25 @@ packer build -only='qemu.sfc_base' -var 'accelerator=kvm' . ``` Expect roughly 15 minutes on Linux with KVM, and a few hours on macOS under -emulation. Packer downloads a 3.3 GB base image first. The result is a sparse +emulation. Packer downloads the base image first (~3.5 GB). The result is a sparse 20 GiB raw file — it takes far less room on disk, but `sf images upload` transfers the full 20 GiB. Need more space for your software? Add `-var 'disk_size=40G'` (the platform caps images at 75 GiB). -`-only` matters: without it Packer builds **both** starting points at once. If -you mistype the name, Packer runs nothing and still exits 0, so check that it -actually printed a build. +`-only` matters: without it Packer builds **both** starting points at once. +Mistype the name and Packer lists the valid ones and exits without building. + +Want to check the template before building? `validate` doesn't fetch anything +unless you ask it to, so it can't yet know which base image to download — pass +`-evaluate-datasources`: + +```bash +packer validate -evaluate-datasources . +``` + +Without the flag it fails with `invalid checksum: encoding/hex: invalid byte: +U+003C '<'`, which doesn't sound like the real problem. `packer build` needs no +such flag. Re-running? Packer refuses to overwrite `output/`. Pass `-force`, or `rm -rf output/` first. @@ -80,12 +91,24 @@ Upload it: sf images upload --name my-image --file output/sfc_base/ubuntu-24.04-cuda-13.2-custom.raw ``` +Before you push 20 GiB, confirm it is what the platform expects: + +```bash +qemu-img info output/sfc_base/ubuntu-24.04-cuda-13.2-custom.raw +``` + +You want `file format: raw` and a virtual size at or under 75 GiB. The real +test is booting an instance from it — do that before you rely on the image. + To start from stock Ubuntu instead: ```bash -packer build -only='qemu.ubuntu' -var 'accelerator=kvm' . +packer build -only='qemu.ubuntu' . ``` +That one lands at `output/ubuntu/ubuntu-noble-custom.raw` and uploads the same +way. + ## Making it yours Edit **`customize.sh`**. That is the whole customization surface — it @@ -93,21 +116,27 @@ runs as root inside the build VM on both starting points. Everything else in this repo is boilerplate that makes the resulting image boot correctly on SF Compute. -If you only ever build on our base images, delete `install-nvidia.sh`, its -`provisioner` block in `build.pkr.hcl`, and the `source "qemu" "ubuntu"` -block — otherwise the repo still offers a path that no longer installs a -driver. +If you only ever build on our base images, you can delete the stock-Ubuntu +path — otherwise the repo still offers a route that no longer installs a +driver. Remove all four, or the template will not load: + +- `install-nvidia.sh` +- its `provisioner` block in `build.pkr.hcl` +- the `source "qemu" "ubuntu"` block +- the `"source.qemu.ubuntu"` line from the `build` block's `sources` list + +The `ubuntu_codename` and `nvidia_driver_branch` variables become unused too, +and can go. ## Choosing a base image By default the build resolves the current release at build time — there is no version to keep up to date in this repo. Packer prints the one it picked: -``` -Trying https://tiny-llama.sfcc.xyz/v20260930.063326/ubuntu-24.04-cuda-13.2.qcow2 -``` +Look for the `Trying ...` line in the output — it names the release tag. -To see what is available: +To see what is available in the SF Compute release index (`tiny-llama.sfcc.xyz`, +operated by SF Compute): ```bash curl -fsSL https://tiny-llama.sfcc.xyz/latest.json | jq -r '.tag, (.images[].name)' @@ -119,16 +148,26 @@ Pick a different image from the release with `base_image_name`: packer build -only='qemu.sfc_base' -var 'base_image_name=ubuntu-22.04-cuda-13.1' . ``` -**If you need two builds to produce the same bytes, pin the release.** Tracking -the latest means a rebuild next month starts from a different base: +The output filename follows the image name, so that one lands at +`output/sfc_base/ubuntu-22.04-cuda-13.1-custom.raw`. + +**Pin the release if you need a repeatable starting point.** Tracking the latest +means a rebuild next month begins from a different base image — different driver, +CUDA and kernel. Pinning fixes that. It does not make the build bit-for-bit +reproducible: `customize.sh` still installs whatever the Ubuntu archive has that +day. ```bash packer build -only='qemu.sfc_base' -var 'base_image_tag=' . ``` -Integrity needs nothing from you. The release index lists each image's sha256 -next to its URL, and the build verifies the download against it — same -document, so the two cannot disagree. +You don't supply a checksum. The release index lists each image's sha256 beside +its URL and the build enforces it, so a corrupted or truncated download fails +instead of producing a broken image. + +Each build writes `output/sfc_base/manifest.json` recording the release it +resolved. That is where to find the tag to pass to `base_image_tag` if you want +to rebuild on the same base. ## What the boilerplate is doing @@ -144,6 +183,24 @@ cloud-init datasource when an instance launches. The root password in `cloud-init/user-data` exists only so Packer can log in during the build, and `finalize.sh` removes it. +## When a build fails + +| What you see | What to do | +|---|---| +| `E: Unable to locate package packer` | Packer is not in the Ubuntu archive. See [Requirements](#requirements). | +| `packer init` reports an unknown command | Your Packer predates 1.7. Ubuntu 22.04 ships 1.6.6; install a current one per Requirements. | +| `Output directory 'output/...' already exists` | `packer build -force ...`, or `rm -rf output/`. | +| `Datasource.Execute failed: ... Response code: 404` | The tag you pinned has no release index — pick one from `latest.json`, or drop `-var base_image_tag`. If you did not pin a tag, the index itself is missing: check with `curl -fsSL https://tiny-llama.sfcc.xyz/latest.json`. | +| `Datasource.Execute failed: ... dial tcp ... no such host`, or `connection refused` | The build looks up the base image over the network before it starts, for either starting point. Check you can reach `https://tiny-llama.sfcc.xyz/latest.json`; behind a proxy, set `HTTPS_PROXY`. | +| `Call to function "jsondecode" failed: invalid character '<'` | Something returned HTML instead of the release index — usually a captive portal or a proxy login page. Get past it in a browser, then retry. | +| `invalid checksum: encoding/hex: invalid byte` from `packer validate` | Plain `validate` does not run data sources, so the digest is still unresolved. Use `packer validate -evaluate-datasources .`. | +| `The given key does not identify an element in this collection value` | `base_image_name` is not in that release. List the names with the `jq` command under [Choosing a base image](#choosing-a-base-image). | +| `Could not access KVM kernel module` | No `/dev/kvm`, or you are not in the `kvm` group. Drop `-var 'accelerator=kvm'` to build under emulation. | +| QEMU exits immediately on macOS | Drop `-var 'accelerator=kvm'`; macOS has no KVM. | +| `Could not open '/usr/share/OVMF/...'` | Firmware auto-detection missed yours. Pass `-var 'efi_firmware_code=...' -var 'efi_firmware_vars=...'`; both must be the same variant, so a 4M code file needs a 4M vars file. | +| `Timeout waiting for SSH` | Usually a slow emulated boot. Build on Linux with KVM, or raise `ssh_timeout` in `build.pkr.hcl`. | +| A step you added to `customize.sh` failed | Packer deletes `output/` on failure. Re-run with `-on-error=abort` to keep the VM and disk so you can look. | + ## Image requirements If you build your own template from scratch rather than copying this @@ -164,23 +221,6 @@ The SF Compute base images ship all of it. See the [Images documentation](https://docs.sfcompute.com/preview/images) for the full picture. -## When a build fails - -| What you see | What to do | -|---|---| -| `E: Unable to locate package packer` | Packer is not in the Ubuntu archive. See [Requirements](#requirements). | -| `packer init` reports an unknown command | Your Packer predates 1.7. Ubuntu 22.04 ships 1.6.6; install a current one per Requirements. | -| `Output directory 'output/...' already exists` | `packer build -force ...`, or `rm -rf output/`. | -| `Datasource.Execute failed: HTTP request error. Response code: 404` | The tag you pinned has no release index. Drop `-var base_image_tag` to use the current release, or pick a tag from `latest.json`. | -| `invalid checksum: encoding/hex: invalid byte` from `packer validate` | Plain `validate` does not run data sources, so the digest is still unresolved. Use `packer validate -evaluate-datasources .`. | -| `The given key does not identify an element in this collection value` | `base_image_name` is not in that release. List the names with the `jq` command under [Choosing a base image](#choosing-a-base-image). | -| `Could not access KVM kernel module` | No `/dev/kvm`, or you are not in the `kvm` group. Drop `-var 'accelerator=kvm'` to build under emulation. | -| QEMU exits immediately on macOS | Drop `-var 'accelerator=kvm'`; macOS has no KVM. | -| `Could not open '/usr/share/OVMF/...'` | Firmware auto-detection missed yours. Pass `-var 'efi_firmware_code=...' -var 'efi_firmware_vars=...'`; both must be the same variant, so a 4M code file needs a 4M vars file. | -| `Timeout waiting for SSH` | Usually a slow emulated boot. Build on Linux with KVM, or raise `ssh_timeout` in `build.pkr.hcl`. | -| A step you added to `customize.sh` failed | Packer deletes `output/` on failure. Re-run with `-on-error=abort` to keep the VM and disk so you can look. | -| The build succeeded but `packer` printed no build | You mistyped `-only`. Packer exits 0 having run nothing. | - ## Files | File | Purpose | diff --git a/build.pkr.hcl b/build.pkr.hcl index 2025dd3..8f212d0 100644 --- a/build.pkr.hcl +++ b/build.pkr.hcl @@ -41,10 +41,12 @@ variable "base_image_tag" { build prints the release it picked, so check the log if you need to know which one you got. - Set this to a tag to pin it, which is what you want if you need two - builds to produce the same bytes. Tags look like v20260930.063326 and - are listed by the release index; releases published before that index - existed cannot be pinned this way. + Set this to a tag for a repeatable starting point. It does not make + the build bit-for-bit reproducible -- customize.sh still installs + whatever the Ubuntu archive has that day -- but it fixes the base + image, so a rebuild next month starts from the same driver, CUDA and + kernel. Tags look like v20260930.063326 and are listed by the release + index; releases published before that index existed cannot be pinned. EOT } @@ -290,4 +292,18 @@ build { script = "finalize.sh" environment_vars = ["DEBIAN_FRONTEND=noninteractive"] } + + # Otherwise the resolved tag survives only in terminal output. This is + # what you pass to base_image_tag to rebuild on the same base. + post-processor "manifest" { + only = ["qemu.sfc_base"] + output = "output/sfc_base/manifest.json" + + custom_data = { + base_image_tag = local.sfc_release.tag + base_image_name = var.base_image_name + base_image_url = local.sfc_image.qcow2.url + base_image_sha256 = local.sfc_image.qcow2.sha256 + } + } } From 72436c98e627b4887a09ee313e8548d91f3f335a Mon Sep 17 00:00:00 2001 From: Evan Culver Date: Tue, 20 Oct 2026 22:45:07 -0700 Subject: [PATCH 3/5] Default to the newest CUDA for the LTS instead of naming an image base_image_name defaulted to ubuntu-24.04-cuda-13.2, which was a version in disguise: the release matrix is two newest LTS by two newest CUDA, so the name drops out of latest.json the next time CUDA moves and the default build fails on an empty lookup. A hardcoded tag was removed for the same reason. Empty base_image_name now means the image with the newest CUDA for ubuntu_codename, ranked numerically so 13.10 would outrank 13.9. ubuntu_codename therefore applies to both starting points. An explicit name still wins, and the pinned-tag path is unchanged. The output file is now a fixed sfc-base-custom.raw. With the image chosen at build time, a filename that followed it would make the upload command in the README wrong for anyone who changed a variable; manifest.json already records what was picked. Verified against the live index by starting builds and reading the URL Packer chose: the default resolves ubuntu-24.04-cuda-13.4 from v20261008.012133, ubuntu_codename=resolute resolves ubuntu-26.04-cuda-13.4, an explicit name is honored, and base_image_tag=v20260930.063326 resolves that release's ubuntu-24.04-cuda-13.2. An unknown name or codename fails before any download with the max() error the troubleshooting table now documents. Co-Authored-By: Claude Fable 5.1 --- README.md | 43 +++++++++++++++++++++++----------------- build.pkr.hcl | 54 ++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 68 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 3da4765..6532754 100644 --- a/README.md +++ b/README.md @@ -84,17 +84,16 @@ such flag. Re-running? Packer refuses to overwrite `output/`. Pass `-force`, or `rm -rf output/` first. -The image lands at `output/sfc_base/ubuntu-24.04-cuda-13.2-custom.raw`. -Upload it: +The image lands at `output/sfc_base/sfc-base-custom.raw`. Upload it: ```bash -sf images upload --name my-image --file output/sfc_base/ubuntu-24.04-cuda-13.2-custom.raw +sf images upload --name my-image --file output/sfc_base/sfc-base-custom.raw ``` Before you push 20 GiB, confirm it is what the platform expects: ```bash -qemu-img info output/sfc_base/ubuntu-24.04-cuda-13.2-custom.raw +qemu-img info output/sfc_base/sfc-base-custom.raw ``` You want `file format: raw` and a virtual size at or under 75 GiB. The real @@ -125,15 +124,15 @@ driver. Remove all four, or the template will not load: - the `source "qemu" "ubuntu"` block - the `"source.qemu.ubuntu"` line from the `build` block's `sources` list -The `ubuntu_codename` and `nvidia_driver_branch` variables become unused too, -and can go. +The `nvidia_driver_branch` variable becomes unused too, and can go. +`ubuntu_codename` stays: the base-image path uses it to pick the LTS. ## Choosing a base image -By default the build resolves the current release at build time — there is no -version to keep up to date in this repo. Packer prints the one it picked: - -Look for the `Trying ...` line in the output — it names the release tag. +By default the build resolves the current release at build time and, within +it, the Ubuntu 24.04 image with the newest CUDA — there is no version to keep +up to date in this repo. Look for the `Trying ...` line in Packer's output: it +names the release tag and the image it picked. To see what is available in the SF Compute release index (`tiny-llama.sfcc.xyz`, operated by SF Compute): @@ -142,14 +141,22 @@ operated by SF Compute): curl -fsSL https://tiny-llama.sfcc.xyz/latest.json | jq -r '.tag, (.images[].name)' ``` -Pick a different image from the release with `base_image_name`: +Each release carries one image per CUDA minor for each of the two newest Ubuntu +LTS releases. To build on a different LTS, change `ubuntu_codename`; you still +get its newest CUDA: + +```bash +packer build -only='qemu.sfc_base' -var 'ubuntu_codename=resolute' . +``` + +To build on a specific CUDA, name the image with `base_image_name`: ```bash -packer build -only='qemu.sfc_base' -var 'base_image_name=ubuntu-22.04-cuda-13.1' . +packer build -only='qemu.sfc_base' -var 'base_image_name=ubuntu-24.04-cuda-13.3' . ``` -The output filename follows the image name, so that one lands at -`output/sfc_base/ubuntu-22.04-cuda-13.1-custom.raw`. +The output filename is the same whichever image was picked; +`output/sfc_base/manifest.json` records which one it was. **Pin the release if you need a repeatable starting point.** Tracking the latest means a rebuild next month begins from a different base image — different driver, @@ -165,9 +172,9 @@ You don't supply a checksum. The release index lists each image's sha256 beside its URL and the build enforces it, so a corrupted or truncated download fails instead of producing a broken image. -Each build writes `output/sfc_base/manifest.json` recording the release it -resolved. That is where to find the tag to pass to `base_image_tag` if you want -to rebuild on the same base. +Each build writes `output/sfc_base/manifest.json` recording the release and +image it resolved. That is where to find the values to pass to `base_image_tag` +and `base_image_name` if you want to rebuild on the same base. ## What the boilerplate is doing @@ -194,7 +201,7 @@ build, and `finalize.sh` removes it. | `Datasource.Execute failed: ... dial tcp ... no such host`, or `connection refused` | The build looks up the base image over the network before it starts, for either starting point. Check you can reach `https://tiny-llama.sfcc.xyz/latest.json`; behind a proxy, set `HTTPS_PROXY`. | | `Call to function "jsondecode" failed: invalid character '<'` | Something returned HTML instead of the release index — usually a captive portal or a proxy login page. Get past it in a browser, then retry. | | `invalid checksum: encoding/hex: invalid byte` from `packer validate` | Plain `validate` does not run data sources, so the digest is still unresolved. Use `packer validate -evaluate-datasources .`. | -| `The given key does not identify an element in this collection value` | `base_image_name` is not in that release. List the names with the `jq` command under [Choosing a base image](#choosing-a-base-image). | +| `Call to function "max" failed: must pass at least one number` | No image in that release matches: `base_image_name` is not in it, or `ubuntu_codename` is an LTS it does not carry. List the names with the `jq` command under [Choosing a base image](#choosing-a-base-image). | | `Could not access KVM kernel module` | No `/dev/kvm`, or you are not in the `kvm` group. Drop `-var 'accelerator=kvm'` to build under emulation. | | QEMU exits immediately on macOS | Drop `-var 'accelerator=kvm'`; macOS has no KVM. | | `Could not open '/usr/share/OVMF/...'` | Firmware auto-detection missed yours. Pass `-var 'efi_firmware_code=...' -var 'efi_firmware_vars=...'`; both must be the same variant, so a 4M code file needs a 4M vars file. | diff --git a/build.pkr.hcl b/build.pkr.hcl index 8f212d0..9a648f5 100644 --- a/build.pkr.hcl +++ b/build.pkr.hcl @@ -51,15 +51,28 @@ variable "base_image_tag" { } variable "base_image_name" { - type = string - default = "ubuntu-24.04-cuda-13.2" - description = "Which image within the release (qemu.sfc_base only). Names look like ubuntu--cuda-; `curl -fsSL https://tiny-llama.sfcc.xyz/latest.json | jq -r '.images[].name'` lists them." + type = string + default = "" + + description = <<-EOT + Which image within the release (qemu.sfc_base only). + + Empty, the default, takes the image with the newest CUDA for + ubuntu_codename. A release carries one image per CUDA minor per + Ubuntu LTS and the set moves as NVIDIA and Canonical ship, so a + name written here goes stale; a rule does not. + + Set this to build on a specific CUDA instead. Names look like + ubuntu--cuda-, and + `curl -fsSL https://tiny-llama.sfcc.xyz/latest.json | jq -r '.images[].name'` + lists the ones in the current release. + EOT } variable "ubuntu_codename" { type = string default = "noble" - description = "Ubuntu cloud image to build on (qemu.ubuntu only). noble = 24.04 LTS, jammy = 22.04 LTS." + description = "Which Ubuntu to build on, for either starting point. noble = 24.04 LTS, resolute = 26.04 LTS. For qemu.ubuntu this names the cloud image; for qemu.sfc_base it picks the LTS when base_image_name is empty." } variable "nvidia_driver_branch" { @@ -137,9 +150,24 @@ data "http" "sfc_release" { locals { sfc_release = jsondecode(data.http.sfc_release.body) - # Empty if base_image_name is not in the release, which fails the build - # here rather than downloading something else. - sfc_image = [for i in local.sfc_release.images : i if i.name == var.base_image_name][0] + # The images this build may pick from: the one named, or every image in + # the release for ubuntu_codename. Empty when the name or codename is not + # in the release, which fails the build here rather than downloading + # something else. + sfc_candidates = var.base_image_name != "" ? [ + for i in local.sfc_release.images : i if i.name == var.base_image_name + ] : [ + for i in local.sfc_release.images : i if i.ubuntu_codename == var.ubuntu_codename + ] + + # Newest CUDA wins. Ranked numerically rather than by string, so a + # hypothetical 13.10 outranks 13.9 instead of sorting between 13.1 and + # 13.2. + sfc_cuda_rank = [ + for i in local.sfc_candidates : + parseint(split(".", i.cuda_version)[0], 10) * 1000 + parseint(split(".", i.cuda_version)[1], 10) + ] + sfc_image = local.sfc_candidates[index(local.sfc_cuda_rank, max(local.sfc_cuda_rank...))] ubuntu_base_url = "https://cloud-images.ubuntu.com/${var.ubuntu_codename}/current" @@ -195,7 +223,10 @@ source "qemu" "sfc_base" { iso_url = local.sfc_image.qcow2.url iso_checksum = "sha256:${local.sfc_image.qcow2.sha256}" - vm_name = "${var.base_image_name}-custom.raw" + # A fixed name, since the base image is resolved at build time: the + # upload command in the README stays copy-pasteable whatever the + # release picked. manifest.json beside it records which image that was. + vm_name = "sfc-base-custom.raw" output_directory = "output/sfc_base" disk_image = true @@ -293,15 +324,16 @@ build { environment_vars = ["DEBIAN_FRONTEND=noninteractive"] } - # Otherwise the resolved tag survives only in terminal output. This is - # what you pass to base_image_tag to rebuild on the same base. + # Otherwise the resolved tag and image survive only in terminal output. + # These are what you pass to base_image_tag and base_image_name to + # rebuild on the same base. post-processor "manifest" { only = ["qemu.sfc_base"] output = "output/sfc_base/manifest.json" custom_data = { base_image_tag = local.sfc_release.tag - base_image_name = var.base_image_name + base_image_name = local.sfc_image.name base_image_url = local.sfc_image.qcow2.url base_image_sha256 = local.sfc_image.qcow2.sha256 } From 2cd8a7f372400e343078c8ec6719bbd74c7fe5fc Mon Sep 17 00:00:00 2001 From: Evan Culver Date: Tue, 20 Oct 2026 22:45:07 -0700 Subject: [PATCH 4/5] Add CI: fmt, validate against the live release index, shellcheck Runs on pull requests, pushes to master, and daily. The daily run is the point: the template resolves the release index at build time, so the repo can stop building without a commit, and validate with -evaluate-datasources is exactly the resolution a reader's build does. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 47 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1352bcb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + pull_request: + push: + branches: [master] + # The template resolves the SF Compute release index at build time, so + # the repo can break without a commit: a release that drops the LTS the + # default keys on, or an index that stops answering, fails validate. The + # daily run finds that before a reader does. + schedule: + - cron: "17 6 * * *" + workflow_dispatch: + +jobs: + check: + runs-on: ubuntu-latest + env: + # `packer init` fetches the qemu plugin from GitHub releases; the + # unauthenticated rate limit is low enough to flake a shared runner. + PACKER_GITHUB_API_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v4 + + - uses: hashicorp/setup-packer@v3 + with: + version: "1.15.0" + + # OVMF so firmware auto-detection resolves the way it does on a real + # Ubuntu build host, rather than through its fallback path. + - name: Install OVMF + run: sudo apt-get install -y -qq ovmf + + - name: Install plugins + run: packer init . + + - name: Check formatting + run: packer fmt -check -diff . + + # -evaluate-datasources makes validate fetch the release index and + # resolve the base image, which is the part worth checking: plain + # validate stops at an unresolved checksum. + - name: Validate, resolving the live release index + run: packer validate -evaluate-datasources . + + - name: Lint shell scripts + run: shellcheck ./*.sh From 53e09ce4847b96d41868c757b46d6901932fe9f1 Mon Sep 17 00:00:00 2001 From: Evan Culver Date: Tue, 20 Oct 2026 23:06:10 -0700 Subject: [PATCH 5/5] Address review: tag list claim, variant ties, CI OVMF step and permissions The variable description and the 404 troubleshooting row said older tags are "listed by the release index". They are not: latest.json carries one tag, and the repository that cuts releases is private. The only place a reader can get an earlier tag is a previous build's manifest.json, which both now say. The default candidate filter now keeps only the base variant, so a non-base build published into the same release cannot tie with the base image on CUDA and win on list order, and a cuda_version with no minor ranks as .0 instead of failing on an index error. Both checked against a crafted local index: on a 13.10 tie the base image wins over a -minimal one, 13.10 outranks 13.9, and a bare "14" resolves. CI drops the OVMF install: validate never checks firmware paths, so the step tested nothing and was the one most likely to flake (apt-get install with stale lists). Adds permissions: contents: read, and records GitHub's 60-day inactivity rule for schedules in a public repo, since this repo is meant to go quiet. README: the base_image_name example uses a placeholder rather than a name that will leave the index, and the customization section says what removing the base-image path involves, since data sources are evaluated for every build. Comments that still said jammy now say noble and resolute. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 20 ++++++++++++-------- README.md | 9 +++++++-- build.pkr.hcl | 18 ++++++++++++------ cloud-init/user-data | 2 +- 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1352bcb..46ed9a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,12 +6,20 @@ on: branches: [master] # The template resolves the SF Compute release index at build time, so # the repo can break without a commit: a release that drops the LTS the - # default keys on, or an index that stops answering, fails validate. The - # daily run finds that before a reader does. + # default keys on, or an index that stops answering, fails validate. + # The daily run finds that before a reader does. + # + # GitHub disables scheduled workflows in a public repository after 60 + # days without repository activity, and this repo is meant to go quiet. + # Re-enable it from the Actions tab when that happens; firing + # workflow_dispatch from the release pipeline instead would be immune. schedule: - cron: "17 6 * * *" workflow_dispatch: +permissions: + contents: read + jobs: check: runs-on: ubuntu-latest @@ -26,11 +34,6 @@ jobs: with: version: "1.15.0" - # OVMF so firmware auto-detection resolves the way it does on a real - # Ubuntu build host, rather than through its fallback path. - - name: Install OVMF - run: sudo apt-get install -y -qq ovmf - - name: Install plugins run: packer init . @@ -39,7 +42,8 @@ jobs: # -evaluate-datasources makes validate fetch the release index and # resolve the base image, which is the part worth checking: plain - # validate stops at an unresolved checksum. + # validate stops at an unresolved checksum. Firmware paths are not + # checked by validate, so the runner needs no OVMF. - name: Validate, resolving the live release index run: packer validate -evaluate-datasources . diff --git a/README.md b/README.md index 6532754..f929b40 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,11 @@ driver. Remove all four, or the template will not load: The `nvidia_driver_branch` variable becomes unused too, and can go. `ubuntu_codename` stays: the base-image path uses it to pick the LTS. +Going the other way, keeping only stock Ubuntu, also means removing the +`data "http"` block, the `sfc_*` locals and the `manifest` post-processor. +Packer evaluates data sources for every build, so until they are gone the +stock-Ubuntu path still needs the release index to be reachable. + ## Choosing a base image By default the build resolves the current release at build time and, within @@ -152,7 +157,7 @@ packer build -only='qemu.sfc_base' -var 'ubuntu_codename=resolute' . To build on a specific CUDA, name the image with `base_image_name`: ```bash -packer build -only='qemu.sfc_base' -var 'base_image_name=ubuntu-24.04-cuda-13.3' . +packer build -only='qemu.sfc_base' -var 'base_image_name=' . ``` The output filename is the same whichever image was picked; @@ -197,7 +202,7 @@ build, and `finalize.sh` removes it. | `E: Unable to locate package packer` | Packer is not in the Ubuntu archive. See [Requirements](#requirements). | | `packer init` reports an unknown command | Your Packer predates 1.7. Ubuntu 22.04 ships 1.6.6; install a current one per Requirements. | | `Output directory 'output/...' already exists` | `packer build -force ...`, or `rm -rf output/`. | -| `Datasource.Execute failed: ... Response code: 404` | The tag you pinned has no release index — pick one from `latest.json`, or drop `-var base_image_tag`. If you did not pin a tag, the index itself is missing: check with `curl -fsSL https://tiny-llama.sfcc.xyz/latest.json`. | +| `Datasource.Execute failed: ... Response code: 404` | The tag you pinned has no release index. Use the tag from `latest.json` (the current release) or from an earlier build's `output/sfc_base/manifest.json`, or drop `-var base_image_tag`. If you did not pin a tag, the index itself is missing: check with `curl -fsSL https://tiny-llama.sfcc.xyz/latest.json`. | | `Datasource.Execute failed: ... dial tcp ... no such host`, or `connection refused` | The build looks up the base image over the network before it starts, for either starting point. Check you can reach `https://tiny-llama.sfcc.xyz/latest.json`; behind a proxy, set `HTTPS_PROXY`. | | `Call to function "jsondecode" failed: invalid character '<'` | Something returned HTML instead of the release index — usually a captive portal or a proxy login page. Get past it in a browser, then retry. | | `invalid checksum: encoding/hex: invalid byte` from `packer validate` | Plain `validate` does not run data sources, so the digest is still unresolved. Use `packer validate -evaluate-datasources .`. | diff --git a/build.pkr.hcl b/build.pkr.hcl index 9a648f5..6db869c 100644 --- a/build.pkr.hcl +++ b/build.pkr.hcl @@ -45,8 +45,10 @@ variable "base_image_tag" { the build bit-for-bit reproducible -- customize.sh still installs whatever the Ubuntu archive has that day -- but it fixes the base image, so a rebuild next month starts from the same driver, CUDA and - kernel. Tags look like v20260930.063326 and are listed by the release - index; releases published before that index existed cannot be pinned. + kernel. Tags look like v20260930.063326. latest.json carries the + current one and each build's output/sfc_base/manifest.json records + the one it used; there is no list of older tags, and releases + published before the index existed cannot be pinned. EOT } @@ -85,7 +87,7 @@ variable "nvidia_driver_branch" { Must exist in the Ubuntu archive for your codename: nvidia-headless--server in -updates/restricted and nvidia-fabricmanager- in -updates/multiverse. 590 - is present for both jammy and noble. + is present for both noble and resolute. Ubuntu may resolve the branch you name forward: as of this writing the 590 packages are transitional and depend on their 595 @@ -154,18 +156,22 @@ locals { # the release for ubuntu_codename. Empty when the name or codename is not # in the release, which fails the build here rather than downloading # something else. + # The variant filter keeps a future non-base build published into the + # same release (a "-minimal", say) from tying with the base image on + # CUDA and winning on list order. sfc_candidates = var.base_image_name != "" ? [ for i in local.sfc_release.images : i if i.name == var.base_image_name ] : [ - for i in local.sfc_release.images : i if i.ubuntu_codename == var.ubuntu_codename + for i in local.sfc_release.images : i + if i.ubuntu_codename == var.ubuntu_codename && try(i.variant, "base") == "base" ] # Newest CUDA wins. Ranked numerically rather than by string, so a # hypothetical 13.10 outranks 13.9 instead of sorting between 13.1 and - # 13.2. + # 13.2. A version with no minor ranks as .0. sfc_cuda_rank = [ for i in local.sfc_candidates : - parseint(split(".", i.cuda_version)[0], 10) * 1000 + parseint(split(".", i.cuda_version)[1], 10) + parseint(split(".", i.cuda_version)[0], 10) * 1000 + try(parseint(split(".", i.cuda_version)[1], 10), 0) ] sfc_image = local.sfc_candidates[index(local.sfc_cuda_rank, max(local.sfc_cuda_rank...))] diff --git a/cloud-init/user-data b/cloud-init/user-data index 1f2ee15..32c90e6 100644 --- a/cloud-init/user-data +++ b/cloud-init/user-data @@ -15,6 +15,6 @@ chpasswd: password: packer type: text runcmd: - # The unit is ssh.service on both jammy and noble (sshd is an alias). + # The unit is ssh.service on both noble and resolute (sshd is an alias). - printf 'PasswordAuthentication yes\nPermitRootLogin yes\n' > /etc/ssh/sshd_config.d/10-packer.conf - systemctl restart ssh