Skip to content

Commit 395fc05

Browse files
montycltclaude
andcommitted
feat(installer): leave the installed system able to update the tools it was given
Every tool this project ships except the installer now goes into the target, together with the keyring the rest are checked against, and the bootstrap writes this project's repository into the target's own pacman.conf. It is the published address and never the medium's, which is a copy of one moment on a filesystem that will not be there once the machine is running; and it goes above the official ones, where Package Repository has always said it belongs, which means in front of the first repository in the file rather than appended below every one of them. The same correction applies to the medium, where appending had been putting it under core and extra all along. And an installation no longer fetches what it is standing on. The medium's repository is named as a package cache as well as served as a repository — one directory, so the image does not grow — and a package already there at the version being installed is taken rather than downloaded. What makes that reach pacman is -c, which leaves the cache to the configuration instead of pacstrap's command line, and it is passed only where the medium says where its cache is: the official Arch image says nothing, and there the downloads would land in the live system's own directories, which are memory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01484ofT7TdcBt7bgNyhUn5D
1 parent 0c7ea65 commit 395fc05

8 files changed

Lines changed: 248 additions & 26 deletions

File tree

archiso/build.sh

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,30 @@ readonly MEDIUM_REPO_DIR=/usr/share/oparch/repo
3131
# other, so a run given it synchronises nothing and reaches for nothing.
3232
readonly MEDIUM_CONF=/usr/share/oparch/medium.conf
3333

34+
# Where an installation keeps what it had to fetch. It is the installer's mount
35+
# point, which is fixed, and the cache has to be on the target rather than on
36+
# the medium: the live system's own directories are memory, and a bootstrap is
37+
# more of it than a small machine has.
38+
readonly TARGET_CACHE=/mnt/var/cache/pacman/pkg
39+
40+
# Everything from the top of a pacman configuration down to the first
41+
# repository, which is what `[options]` is; and the repositories after it, in
42+
# the order pacman reads them. Commented-out repositories are not headers and
43+
# stay where they were written.
44+
options_section() { awk '/^\[/ && $0 != "[options]" { exit } { print }' "$1"; }
45+
repositories_section() {
46+
awk 'found { print } !found && /^\[/ && $0 != "[options]" { found = 1; print }' "$1"
47+
}
48+
49+
# Where the packages an installation installs are looked for before any of them
50+
# is fetched. The first is on the target, and is where whatever did have to be
51+
# downloaded is kept. The second is the medium's own repository — the same
52+
# directory that serves it, so the files are on the image once and named twice
53+
# — and a package whose exact version is already there is not downloaded again.
54+
package_cache() {
55+
printf 'CacheDir = %s/\nCacheDir = %s/\n' "$TARGET_CACHE" "$MEDIUM_REPO_DIR"
56+
}
57+
3458
# The stanza that names the repository the medium carries. Both files that
3559
# mention it are written from here, so they cannot come to disagree about where
3660
# it is or what is asked of what it holds.
@@ -103,13 +127,19 @@ pacman-key --add "$HERE/../packages/oparch-keyring/oparch.gpg"
103127
pacman-key --lsign-key "$fingerprint"
104128

105129
# The published repository, above the official ones, so that the medium is
106-
# built out of the same packages an installed system would update to.
107-
cat >> "$profile/pacman.conf" <<EOF
108-
109-
[$REPOSITORY_NAME]
110-
SigLevel = Required TrustedOnly
111-
Server = $REPOSITORY
112-
EOF
130+
# built out of the same packages an installed system would update to, and so
131+
# that a name this project publishes resolves to this project's package
132+
# wherever else that name turns up. Above means in front of the first
133+
# repository in the file and not at the end of it, which is where appending
134+
# would put it: below every official one.
135+
say "Adding the published repository, above the official ones"
136+
{
137+
options_section "$profile/pacman.conf"
138+
printf '\n[%s]\nSigLevel = Required TrustedOnly\nServer = %s\n\n' \
139+
"$REPOSITORY_NAME" "$REPOSITORY"
140+
repositories_section "$profile/pacman.conf"
141+
} > "$work/pacman.conf"
142+
mv "$work/pacman.conf" "$profile/pacman.conf"
113143

114144
# ------------------------------------------- the repository the medium carries
115145

@@ -138,18 +168,21 @@ repo-add "$carried/$MEDIUM_REPO.db.tar.gz" "$carried"/*.pkg.tar.zst
138168
say "Writing the live system's pacman.conf"
139169
mkdir -p "$profile/airootfs/etc"
140170
{
141-
cat "$profile/pacman.conf"
171+
options_section "$profile/pacman.conf"
172+
package_cache
173+
repositories_section "$profile/pacman.conf"
142174
medium_repository
143175
} > "$profile/airootfs/etc/pacman.conf"
144176

145177
# And the one an installation without a network is run with: the same options
146-
# the medium was built under, and the medium's repository as the only one there
147-
# is. Everything from the top of the file down to the first repository is what
148-
# `[options]` is, so the section is taken rather than written out again and the
149-
# two configurations cannot disagree about how packages are checked.
178+
# the medium was built under, the same cache, and the medium's repository as
179+
# the only one there is. The options are taken from the profile's file rather
180+
# than written out again, so the two configurations cannot disagree about how
181+
# packages are checked or where they are looked for.
150182
say "Writing the configuration an installation without a network uses"
151183
{
152-
awk '/^\[/ && $0 != "[options]" { exit } { print }' "$profile/pacman.conf"
184+
options_section "$profile/pacman.conf"
185+
package_cache
153186
medium_repository
154187
} > "$profile/airootfs$MEDIUM_CONF"
155188

docs/development/008-building-and-publishing.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ The profile is not kept here. `releng` is archiso's, it moves with archiso, and
5454

5555
The medium's repository is filled by downloading rather than installing: what is wanted is package files, so that an installation with nothing to fetch from has something to install. Every package an answer might ask for is fetched, and not the ones a particular answer would, because an installation without a network cannot go and get the microcode of the processor it turns out to be running on.
5656

57-
Two repositories end up in the live system's `pacman.conf`. Their order separates them: the published one above the official ones, because packages of this project's are this project's wherever else a name appears; and the medium's own below everything, because it is what answers when nothing else can and never what wins while something else can. What is asked of them separates them too — the published one has to be signed and the medium's own is not checked at all, for the reason [Installation ISO](../decisions/018-installation-iso.md) gives. That file is generated from the profile's own rather than written out a second time, so the repositories an image was built from and the ones it installs from cannot come to disagree.
57+
Two repositories end up in the live system's `pacman.conf`. Their order separates them: the published one above the official ones, because packages of this project's are this project's wherever else a name appears; and the medium's own below everything, because it is what answers when nothing else can and never what wins while something else can. Above means in front of the first repository already in the file, not appended to the end of it, which is where every official one would then be ahead of it. What is asked of them separates them too — the published one has to be signed and the medium's own is not checked at all, for the reason [Installation ISO](../decisions/018-installation-iso.md) gives.
58+
59+
Both configurations also name where packages are looked for before any of them is fetched: the target's own cache, and the medium's repository. That is the same directory that serves the repository, so the files are on the image once and named twice, and an installation with a network does not download a package the medium already holds at the version being installed. That file is generated from the profile's own rather than written out a second time, so the repositories an image was built from and the ones it installs from cannot come to disagree.
5860

5961
A second configuration is written beside the medium's repository, holding that repository and no other. It is what an installation told it has no network is run with, and the installer names it by its path: with it there is nothing to synchronise and nothing to reach for, where the live system's own would try the official repositories first and fail on all of them. It carries the same options the medium was built under, taken from the profile's file rather than written again, and the stanza naming the repository is written once for both.
6062

docs/state/000-what-is-built.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ This document is descriptive. What the tools are for is defined in `../tools/`,
1919

2020
The layout and the reason for it are [Repository Layout](../development/002-repository-layout.md); how the last two are used is [Building and Publishing](../development/008-building-and-publishing.md).
2121

22-
Tests, counted on 2026-09-11: 299 in `src/installer`, 133 in `src/return-message-render`, 85 in `src/dotfiles-sync`, 42 in `src/utils`. Counts move with the work, so treat them as of that date rather than as a fact about the suite. Every suite runs with `baml test` and needs no host, no bridge, no ImageMagick and no privileges. The counts overlap: a suite also runs the tests of every namespace linked into it.
22+
Tests, counted on 2026-09-11: 304 in `src/installer`, 133 in `src/return-message-render`, 85 in `src/dotfiles-sync`, 42 in `src/utils`. Counts move with the work, so treat them as of that date rather than as a fact about the suite. Every suite runs with `baml test` and needs no host, no bridge, no ImageMagick and no privileges. The counts overlap: a suite also runs the tests of every namespace linked into it.
2323

2424
## The installer
2525

@@ -31,6 +31,8 @@ Tests, counted on 2026-09-11: 299 in `src/installer`, 133 in `src/return-message
3131

3232
**Eleven phases**, orchestrated in order and stopping at the first failure: `prepare_layout`, `bootstrap_base_system`, `configure_localization`, `configure_identity`, `configure_users`, `configure_network`, `configure_swap`, `configure_return_message`, `configure_initramfs`, `configure_bootloader`, `configure_dotfiles`. Nine of them always run; the return message and the dotfiles are present only when the installation was given what they act on, so the list never shows a step that will not happen. Every command's exit status is checked, and a failed file operation is reported by the host and noticed by the orchestrator.
3333

34+
**What the installed machine is left with.** The bootstrap puts on it every tool this project ships except the installer, and the keyring they are checked against; and it writes this project's repository into the target's own `pacman.conf`, above the official ones and as the published address rather than the medium's, so that `pacman -Syu` there updates those tools along with everything else. The medium's copies are also named as a package cache, so an installation with a network fetches only what the medium does not already hold at the version being installed.
35+
3436
**The template package and the theme.** Manifest parsing with format-version checking, message bodies with `{{field}}` references and `[[optional region]]` removal, and loading either from a local directory or from a URL as a `tar` whose entries are listed and refused before extraction if any would land outside the destination — the same path for both, since a theme is delivered like a package. The return-message screen asks for a package, asks for a theme, then asks the fields the package declares and offers the numbers of languages the theme lays out. Both are read and checked, with the values, before the disk is touched. Formats: [Return Message Template Package Format](../tools/oparch-return-message-render/001-template-package-format.md), [Return Message Values Format](../tools/oparch-return-message-render/002-values-format.md) and [Return Message Theme Format](../tools/oparch-return-message-render/003-theme-format.md); the decision behind the theme is [Return Message Themes](../tools/oparch-return-message-render/004-themes.md).
3537

3638
**The pre-boot message phase.** It downloads the logo, asking the operator what to do when the download fails; writes the values into the target as `/etc/opinionatedarch/return-message.yaml`, composed by the same namespace that parses that format so the two cannot disagree; installs the project's Plymouth theme; runs `oparch-return-message-render` against the file it just wrote, with the theme in the target as its output; and makes that theme the default. It runs before the initramfs is built, and does nothing at all when no return message was asked for.

docs/tools/oparch-installer/002-inputs-and-bootstrap-baseline.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,21 @@ Installed with `pacstrap`:
8484
- `sudo`
8585
- `networkmanager`
8686
- `ipxe`
87+
- `oparch-assets`
88+
- `oparch-return-message-render`
89+
- `oparch-dotfiles-sync`
90+
- `oparch-keyring`
8791
- `zram-generator` (if the zram swap size is greater than zero)
8892
- `intel-ucode` (if selected as the ucode package)
8993
- `amd-ucode` (if selected as the ucode package)
9094
- `nvidia-open` (if GPU driver is `nvidia-open`)
9195
- `plymouth` (if pre-boot return message is enabled)
9296

93-
They come from the repositories the live environment is configured with, the official ones first and the medium's own last; or, when the installation was told it has no network, from the medium's own configuration, which names the repository the medium carries and nothing else. Which of the two an installation draws from is settled in [Installation ISO](../../decisions/018-installation-iso.md), and the answer that settles it is prompt 2.
97+
The four this project publishes are every tool it ships except `oparch-installer`, which runs from a live environment and has no use on a machine that is installed, together with the keyring that lets the rest be updated from where they came from. What that is, and why the repository sits above the official ones on the installed system, is [Package Repository](../../decisions/016-package-repository.md); the installation writes it into the target as the published repository and never as the one the medium carries, which will not be there once the machine is running.
98+
99+
They come from the repositories the live environment is configured with, this project's first, the official ones after it and the medium's own last; or, when the installation was told it has no network, from the medium's own configuration, which names the repository the medium carries and nothing else. Which of the two an installation draws from is settled in [Installation ISO](../../decisions/018-installation-iso.md), and the answer that settles it is prompt 2.
100+
101+
Where they are looked for is not where they come from. The medium's own repository is also named as a package cache, so a package whose exact version is already on the medium is taken from there instead of being fetched, and only what the machine does not already have is downloaded. What is fetched is kept in the target's cache; the live environment's own directories are memory, and a bootstrap is more of it than a small machine has.
94102

95103
### Netboot Recovery Binary
96104

packages/oparch-installer/PKGBUILD

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
pkgname=oparch-installer
2-
pkgver=0.1.0dev4
2+
pkgver=0.1.0dev5
33
pkgrel=1
44
pkgdesc="Installs OpinionatedArch onto a machine, from a live environment"
55
arch=('x86_64')

src/installer/baml_src/config_file.baml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,9 @@ function packaged_files() -> root.common.RecordingFiles {
458458
"[Trigger]\nTarget = boot/amd-ucode.img\n");
459459
files.write("/assets/pacman-hooks/90-oparch-microcode-intel.hook",
460460
"[Trigger]\nTarget = boot/intel-ucode.img\n");
461+
//# what the `pacman` package leaves in the target, which is there by the
462+
//# time the bootstrap writes this project's repository into it
463+
files.write("/mnt/etc/pacman.conf", packaged_pacman_conf());
461464
files
462465
}
463466

src/installer/baml_src/network.baml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,11 +216,18 @@ function reaches_repository(shell: root.common.Shell) -> bool {
216216
repository_probe()]).exit_code == 0
217217
}
218218

219+
/// Where this project publishes its packages. It is asked about here and
220+
/// written into the installed system by the bootstrap, and both say it from
221+
/// here so that they cannot come to name different servers.
222+
function package_repository() -> string {
223+
"https://packages.oparch.iokode.dev"
224+
}
225+
219226
/// What is asked. The database rather than the bare host, because a name that
220227
/// resolves and a server that answers are different things, and the second is
221228
/// the one that matters.
222229
function repository_probe() -> string {
223-
"https://packages.oparch.iokode.dev/oparch.db"
230+
`${package_repository()}/oparch.db`
224231
}
225232

226233
/// Where the medium keeps the pacman configuration that resolves against the

0 commit comments

Comments
 (0)