From 947a69ba9126124c5591566e0859ad0e229f4322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Tue, 6 Oct 2026 08:48:14 -0400 Subject: [PATCH] Pin a local-source install's locally built packages so updates keep them --- README.md | 2 + builder/build-iso.sh | 3 + .../share/omarchy-iso/orchestrator/main.py | 2 + .../omarchy-iso/orchestrator/phases_impl.py | 36 +++++++++ test/unit/test_pin_local_packages.py | 78 +++++++++++++++++++ 5 files changed, 121 insertions(+) create mode 100644 test/unit/test_pin_local_packages.py diff --git a/README.md b/README.md index 8dad9b2..b2ba49a 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ For local development, build the ISO from sibling checkouts: Despite the local folder name, the first argument is the Omarchy source checkout (runtime commands, configs, setup scripts, themes, shell, migrations). The installer itself lives in this ISO repo. +A local-source install pins `omarchy-dev`, `omarchy-settings-dev` and `rime-ice-installer` with `IgnorePkg` in `/etc/pacman.conf`, since the published builds lack the local changes. A `pre-refresh-pacman` hook in the user's `~/.config/omarchy/hooks` puts the line back after `omarchy refresh pacman`. Remove both to take the published packages again. + Use `--dev` or `--rc` to build against those package channels. Both `--dev` and `--edge` select the dev packages from the edge mirror. ## Autoinstall diff --git a/builder/build-iso.sh b/builder/build-iso.sh index 22aeb75..1b271ce 100755 --- a/builder/build-iso.sh +++ b/builder/build-iso.sh @@ -102,6 +102,9 @@ fi if [[ -d /omarchy-source && -d /omarchy-pkgs ]]; then bash /builder/build-omarchy-packages.sh "$offline_mirror_dir" LOCAL_OMARCHY_BUILD=1 + # Published builds of these lack the local changes, so the target pins them. + printf '%s\n' "$OMARCHY_RUNTIME_PACKAGE" "$OMARCHY_SETTINGS_PACKAGE" rime-ice-installer \ + > "$build_cache_dir/airootfs/usr/share/omarchy-iso/pinned-packages" fi # Node.js binary for offline mise install. diff --git a/configs/airootfs/usr/share/omarchy-iso/orchestrator/main.py b/configs/airootfs/usr/share/omarchy-iso/orchestrator/main.py index 9b6ebf5..f6cd136 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/main.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/main.py @@ -37,6 +37,7 @@ def build_phases(ctx: InstallContext): stage_provisioning_state, finalize_limine_boot, run_chroot_finalizer, + pin_local_packages, configure_dns_resolver, configure_login, configure_ssh_access, @@ -56,6 +57,7 @@ def build_phases(ctx: InstallContext): ("Staging provisioning", stage_provisioning_state), ("Finalizing Limine boot", finalize_limine_boot), ("Finalizing user", run_chroot_finalizer), + ("Pinning local builds", pin_local_packages), ("Configuring login", configure_login), ("Configuring SSH access", configure_ssh_access), ("Configuring Tailscale", configure_tailscale), diff --git a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py index 70744f5..9bcef37 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py @@ -1375,6 +1375,42 @@ def run_chroot_finalizer(ctx: InstallContext) -> None: ) + +PINNED_PACKAGES = Path("/usr/share/omarchy-iso/pinned-packages") +PIN_HOOK_SCRIPT = """#!/bin/bash + +# omarchy refresh pacman rewrites pacman.conf; keep this ISO's local builds pinned. +line="{line}" +grep -qxF "$line" /etc/pacman.conf || sudo sed -i "/^\\[options\\]/a $line" /etc/pacman.conf +""" + + +def pin_local_packages(ctx: InstallContext) -> None: + if not PINNED_PACKAGES.exists(): + return + + line = "IgnorePkg = " + " ".join(PINNED_PACKAGES.read_text().split()) + info(f"› {line}") + pacman_conf = ctx.target / "etc" / "pacman.conf" + conf = pacman_conf.read_text() + if line not in conf.splitlines(): + if "[options]\n" not in conf: + raise RuntimeError(f"{pacman_conf} has no [options] section to pin local builds in") + pacman_conf.write_text(conf.replace("[options]\n", f"[options]\n{line}\n", 1)) + + if ctx.defer_provisioning: + return + + hooks = f"/home/{ctx.username}/.config/omarchy/hooks" + hook = ctx.target / hooks.lstrip("/") / "pre-refresh-pacman.d" / "pin-local-packages" + hook.parent.mkdir(parents=True, exist_ok=True) + hook.write_text(PIN_HOOK_SCRIPT.format(line=line)) + hook.chmod(0o755) + subprocess.run( + ["arch-chroot", str(ctx.target), "chown", "-R", f"{ctx.username}:{ctx.username}", hooks], + check=True, + ) + def configure_dns_resolver(ctx: InstallContext) -> None: """Put the installed system in systemd-resolved stub mode. diff --git a/test/unit/test_pin_local_packages.py b/test/unit/test_pin_local_packages.py new file mode 100644 index 0000000..dabcb1a --- /dev/null +++ b/test/unit/test_pin_local_packages.py @@ -0,0 +1,78 @@ +"""Local-source installs pin the packages the ISO built from the checkouts.""" + +import subprocess +import sys +import tempfile +import types +import unittest +from pathlib import Path +from unittest import mock + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / "configs/airootfs/usr/share/omarchy-iso")) +sys.modules.setdefault( + "orchestrator.archinstall_adapter", + types.ModuleType("orchestrator.archinstall_adapter"), +) +from orchestrator import phases_impl # noqa: E402 + +LINE = "IgnorePkg = omarchy-dev omarchy-settings-dev rime-ice-installer" +CONF = "[options]\nHoldPkg = pacman glibc\n\n[core]\nInclude = /etc/pacman.d/mirrorlist\n" + + +class PinLocalPackagesTest(unittest.TestCase): + def run_pin(self, root: Path, *, pinned: bool, defer: bool = False): + (root / "etc").mkdir() + (root / "etc/pacman.conf").write_text(CONF) + pinned_file = root / "pinned-packages" + if pinned: + pinned_file.write_text("omarchy-dev\nomarchy-settings-dev\nrime-ice-installer\n") + ctx = types.SimpleNamespace(target=root, username="test", defer_provisioning=defer) + with mock.patch.object(phases_impl, "PINNED_PACKAGES", pinned_file), \ + mock.patch.object(phases_impl.subprocess, "run") as run: + phases_impl.pin_local_packages(ctx) + phases_impl.pin_local_packages(ctx) + return run + + def test_pins_once_and_installs_refresh_hook(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + run = self.run_pin(root, pinned=True) + self.assertEqual((root / "etc/pacman.conf").read_text(), CONF.replace("[options]\n", f"[options]\n{LINE}\n")) + hook = root / "home/test/.config/omarchy/hooks/pre-refresh-pacman.d/pin-local-packages" + self.assertIn(f'line="{LINE}"', hook.read_text()) + self.assertTrue(hook.stat().st_mode & 0o111) + run.assert_called_with( + ["arch-chroot", str(root), "chown", "-R", "test:test", "/home/test/.config/omarchy/hooks"], + check=True, + ) + + def test_hook_restores_the_pin_after_a_refresh(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + self.run_pin(root, pinned=True) + hook = root / "home/test/.config/omarchy/hooks/pre-refresh-pacman.d/pin-local-packages" + conf = root / "pacman.conf" + conf.write_text(CONF) + script = hook.read_text().replace("/etc/pacman.conf", str(conf)).replace("sudo ", "") + for _ in range(2): + subprocess.run(["bash", "-c", script], check=True) + self.assertEqual(conf.read_text(), CONF.replace("[options]\n", f"[options]\n{LINE}\n")) + + def test_deferred_install_pins_without_a_user_hook(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + run = self.run_pin(root, pinned=True, defer=True) + self.assertIn(LINE, (root / "etc/pacman.conf").read_text()) + self.assertFalse((root / "home").exists()) + run.assert_not_called() + + def test_published_builds_stay_unpinned(self): + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + self.run_pin(root, pinned=False) + self.assertEqual((root / "etc/pacman.conf").read_text(), CONF) + + +if __name__ == "__main__": + unittest.main()