diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 328c14e..7196360 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -14,8 +14,14 @@ on: push: branches: [main] # Manual re-run, e.g. after a token or permission failure. Idempotent: it re-opens or refreshes - # the same release PR rather than creating a second one. + # the same release PR rather than creating a second one. `package_tag` re-runs + # ONLY the packaging job for an existing tag, for a build that failed. workflow_dispatch: + inputs: + package_tag: + description: "Re-build and re-attach the package for this tag (e.g. v0.4.1). Leave empty for a normal run." + required: false + default: "" permissions: contents: write @@ -31,9 +37,65 @@ jobs: release-please: runs-on: ubuntu-latest timeout-minutes: 15 + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} steps: - uses: googleapis/release-please-action@v5 + id: release with: target-branch: main config-file: release-please-config.json manifest-file: .release-please-manifest.json + + # Attaches an installable package to the release the job above just cut, so a + # release is something you can `pacman -U` instead of a source zip. + # + # IT LIVES HERE, not in a workflow keyed on `release: [published]`, and that is + # the whole point: release-please creates the Release with the default + # GITHUB_TOKEN, and GitHub does not start workflow runs from events raised by + # GITHUB_TOKEN. A `release: published` trigger would never fire on the real + # path -- and would leave no failed run in the Actions tab to notice. This repo + # has already paid for that lesson once. + package: + needs: release-please + if: needs.release-please.outputs.release_created == 'true' || inputs.package_tag != '' + runs-on: ubuntu-latest + # arch=('any'), so the x86_64 image is honest here: nothing is compiled, and + # the python package is co-located with its data rather than dropped into the + # BUILDING interpreter's site-packages. A repo that really compiles needs the + # other treatment -- macarchy-install#18. + container: archlinux:base-devel + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + with: + # The tag, never the default branch: re-running for an older tag must + # not build main's PKGBUILD and clobber that package onto the old release. + ref: ${{ needs.release-please.outputs.tag_name || inputs.package_tag }} + + - name: Build the package + # -Syu, not -Sy: a partial upgrade can link the fresh python against a + # glibc this image does not have, and it reads as a build bug. + # github-cli because `gh` lives on the RUNNER, not inside the container. + # makepkg refuses to run as root, so it gets a throwaway user. + # --nodeps because `depends` is a runtime contract for the target machine. + run: | + pacman -Syu --noconfirm --needed git python github-cli + useradd -m build && chown -R build:build . + su build -c 'makepkg -f --nodeps --noconfirm' + ls -l ./*.pkg.tar.* + + - name: Attach it to the release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ needs.release-please.outputs.tag_name || inputs.package_tag }} + # A GLOB, not a name: PKGEXT is .zst here and .xz on the maintainer's + # machine, and hardcoding either uploads nothing on the other. The version + # check is what stops a mis-targeted manual re-run from clobbering an old + # release with a package built from a different tag. + run: | + test -n "$(echo ./*.pkg.tar.* )" + ls ./*.pkg.tar.* | grep -q "${TAG#v}" \ + || { echo "built package does not carry $TAG — refusing to upload"; exit 1; } + gh release upload "$TAG" ./*.pkg.tar.* --clobber --repo "$GITHUB_REPOSITORY" diff --git a/PKGBUILD b/PKGBUILD new file mode 100644 index 0000000..84728b7 --- /dev/null +++ b/PKGBUILD @@ -0,0 +1,66 @@ +# Maintainer: Philippe Matray +# +# Installs system-wide what ./install.sh installs into $HOME. The two channels +# are kept honest by tests/test_pkgbuild.py, which fails if either grows a file +# the other does not carry. +pkgname=macarchy-touchbar +pkgver=0.4.0 # x-release-please-version +pkgrel=1 +pkgdesc="A Touch Bar daemon for MacBooks on Linux — draws every pixel over DRM, follows the focused app, takes modules" +arch=('any') +url="https://github.com/macarchy/macarchy-touchbar" +license=('MIT') +install=macarchy-touchbar.install +depends=('python' 'python-cairo' 'python-gobject' 'brightnessctl') +optdepends=('papirus-icon-theme: application icons on the bar' + 'tiny-dfr: what install.sh --uninstall hands the bar back to') +# install.sh:27 curls this from master, unpinned -- the file changed between +# 2 Sep and 5 Sep 2026. A package has to be reproducible, so the commit is +# pinned and the checksum is real. +source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz" + "MaterialSymbolsRounded.ttf::https://raw.githubusercontent.com/google/material-design-icons/0cbb08816df07faaae3dca060d4ebb10b66c214f/variablefont/MaterialSymbolsRounded%5BFILL%2CGRAD%2Copsz%2Cwght%5D.ttf" + "MaterialSymbolsRounded.codepoints::https://raw.githubusercontent.com/google/material-design-icons/0cbb08816df07faaae3dca060d4ebb10b66c214f/variablefont/MaterialSymbolsRounded%5BFILL%2CGRAD%2Copsz%2Cwght%5D.codepoints") +sha256sums=('SKIP' + '24f9f678388abc5a0e2c5bf722eeab7aea08a0a058459920d5eb117bf0f8557b' + 'cbea7bfbd34d1d4f8dd2628c34587e447f935cf4f2219b264988da48736eca75') + +package() { + cd "$srcdir/$pkgname-$pkgver" + + install -Dm755 bin/macarchy-touchbar "$pkgdir/usr/bin/macarchy-touchbar" + + # Code and data together under /usr/share, NOT the python package in + # site-packages. site-packages would bake the BUILDING interpreter's version + # into an arch=('any') artifact: this is built in a container whose python is + # routinely ahead of Asahi's, and the target would then get ImportError while + # depends=('python') claims to be satisfied. Co-located, "one directory above + # the package" resolves in both layouts and there is one rule, not two. + install -d "$pkgdir/usr/share/$pkgname" + cp -r macarchy_touchbar modules config "$pkgdir/usr/share/$pkgname/" + + install -Dm644 "$srcdir/MaterialSymbolsRounded.ttf" \ + "$pkgdir/usr/share/fonts/TTF/MaterialSymbolsRounded.ttf" + # draw.py:57 opens this. It is gitignored, so it is NOT in the release tarball + # and has to come from the same pinned commit as the font. No `|| true`: a + # missing codepoints file means a bar with no icons, and the build should say + # so rather than ship one. + install -Dm644 "$srcdir/MaterialSymbolsRounded.codepoints" \ + "$pkgdir/usr/share/$pkgname/fonts/MaterialSymbolsRounded.codepoints" + + install -Dm644 udev/70-macarchy-touchbar.rules \ + "$pkgdir/usr/lib/udev/rules.d/70-macarchy-touchbar.rules" + install -Dm644 modules-load.d/macarchy-touchbar.conf \ + "$pkgdir/usr/lib/modules-load.d/macarchy-touchbar.conf" + # The shipped unit says ExecStart=%h/.local/bin/… because install.sh symlinks + # the CLI there. A package install never writes into $HOME, so shipping it + # verbatim would give 203/EXEC, ten restarts to StartLimitBurst, and an + # OnFailure toast -- from a package that installed perfectly. + sed 's|%h/\.local/bin/|/usr/bin/|' systemd/macarchy-touchbar.service \ + > "$srcdir/macarchy-touchbar.service.pkg" + grep -q '^ExecStart=/usr/bin/' "$srcdir/macarchy-touchbar.service.pkg" # or fail the build + install -Dm644 "$srcdir/macarchy-touchbar.service.pkg" \ + "$pkgdir/usr/lib/systemd/user/macarchy-touchbar.service" + + install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" + install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md" +} diff --git a/bin/macarchy-touchbar b/bin/macarchy-touchbar index 225c686..d80a4d9 100755 --- a/bin/macarchy-touchbar +++ b/bin/macarchy-touchbar @@ -3,8 +3,28 @@ import os import sys -ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) -sys.path.insert(0, ROOT) +# Find the tree that holds macarchy_touchbar/ next to modules/ and config/, and +# put it on sys.path. Code and data are co-located ON PURPOSE: everything below +# then resolves relative to the package, exactly as it did when the only layout +# was a git checkout. +# +# The alternative -- python package in site-packages, data in /usr/share -- was +# tried and rejected: it bakes the CI container's interpreter version into an +# arch=('any') package, so a target whose python differs by one minor version +# gets ImportError while depends=('python') claims to be satisfied. +_HERE = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) +_CANDIDATES = (os.environ.get("MACARCHY_TOUCHBAR_DATA"), _HERE, "/usr/share/macarchy-touchbar") +for _root in _CANDIDATES: + if _root and os.path.isdir(os.path.join(_root, "macarchy_touchbar")): + sys.path.insert(0, _root) + break +else: + # Say which places were tried. Falling through to the import would raise a + # bare ImportError naming a module, which tells the reader nothing about the + # real problem: the tree is somewhere this binary did not look. + sys.exit("macarchy-touchbar: cannot find macarchy_touchbar/ — looked in " + + ", ".join(repr(c) for c in _CANDIDATES if c) + + "\nSet MACARCHY_TOUCHBAR_DATA to the tree that holds it.") if len(sys.argv) > 1 and sys.argv[1] == "daemon": from macarchy_touchbar.daemon import main # noqa: E402 diff --git a/macarchy-touchbar.install b/macarchy-touchbar.install new file mode 100644 index 0000000..873b35e --- /dev/null +++ b/macarchy-touchbar.install @@ -0,0 +1,32 @@ +post_install() { + cat <<'NOTE' + + macarchy-touchbar is installed, but pacman cannot do the half of install.sh + that is not file copying. Until these are done the bar will not come up: + + sudo usermod -aG video "$USER" # open the Touch Bar's DRM card (needs a relogin) + sudo modprobe uinput # the rule is shipped; the module must be loaded once + sudo systemctl disable --now tiny-dfr && sudo systemctl mask tiny-dfr + # tiny-dfr drives the same panel; two owners means neither works + + systemctl --user enable --now macarchy-touchbar.service + + A dead bar looks exactly like a bar with nothing on it, so if it stays blank: + journalctl --user -u macarchy-touchbar -b + +NOTE +} + +post_upgrade() { + systemctl --user try-restart macarchy-touchbar.service 2>/dev/null || true +} + +pre_remove() { + systemctl --user disable --now macarchy-touchbar.service 2>/dev/null || true + cat <<'NOTE' + + The Touch Bar is unowned now. To hand it back to tiny-dfr: + sudo systemctl unmask tiny-dfr && sudo systemctl enable --now tiny-dfr + +NOTE +} diff --git a/macarchy_touchbar/daemon.py b/macarchy_touchbar/daemon.py index 6e613a8..f52f375 100644 --- a/macarchy_touchbar/daemon.py +++ b/macarchy_touchbar/daemon.py @@ -20,7 +20,7 @@ from .uinput import VirtualKeyboard from .widgets import Sprite -ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +from macarchy_touchbar.paths import data_root # noqa: E402 HOME = os.path.expanduser("~") CFG = os.path.join(os.environ.get("XDG_CONFIG_HOME") or f"{HOME}/.config", "macarchy-touchbar", "layouts.toml") PLUGINS = os.path.join(os.environ.get("XDG_CONFIG_HOME") or f"{HOME}/.config", "omarchy", "plugins") @@ -40,7 +40,7 @@ def build(loop, output, config, plugins_dir=None, shell_json=None): host = ModuleHost(loop, None, registry) bar = Bar(output, loop, Painter(output.surface), config, registry, host) host.hooks = bar - specs = discover(os.path.join(ROOT, "modules"), plugins_dir or PLUGINS, + specs = discover(os.path.join(data_root(), "modules"), plugins_dir or PLUGINS, _shell_json() if shell_json is None else shell_json) for spec in specs: host.load(spec) @@ -68,7 +68,7 @@ def _load_config(path): return Config.load(path) except (OSError, ValueError) as e: log(f"{path}: {e}; using the shipped layouts") - return Config.load(os.path.join(ROOT, "config", "layouts.toml")) + return Config.load(os.path.join(data_root(), "config", "layouts.toml")) def run_daemon(headless=False, config_path=CFG): @@ -155,7 +155,7 @@ def deliver(gs): def reload(): nonlocal config config = _load_config(config_path) - rediscover(host, os.path.join(ROOT, "modules"), PLUGINS, _shell_json()) + rediscover(host, os.path.join(data_root(), "modules"), PLUGINS, _shell_json()) bar.reload_config(config) return "reloaded" diff --git a/macarchy_touchbar/draw.py b/macarchy_touchbar/draw.py index ef29c6a..70eac44 100644 --- a/macarchy_touchbar/draw.py +++ b/macarchy_touchbar/draw.py @@ -21,8 +21,14 @@ except (ValueError, ImportError): Gdk = None -ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -CODEPOINTS = os.path.join(ROOT, "fonts", "MaterialSymbolsRounded.codepoints") +from macarchy_touchbar.paths import data_root + +def codepoints_path(): + # A call, not a module constant: data_root() reads the environment, and a + # constant frozen at import would move modules/ and config/ without moving + # the icon font -- the OSError below is swallowed and every icon silently + # resolves to None. (_codepoints, below, is the parsed cache; different thing.) + return os.path.join(data_root(), "fonts", "MaterialSymbolsRounded.codepoints") class Theme: @@ -53,7 +59,7 @@ def icon_codepoint(name): if _codepoints is None: _codepoints = {} try: - with open(CODEPOINTS) as f: + with open(codepoints_path()) as f: for line in f: n, _, hexcode = line.strip().partition(" ") if hexcode: diff --git a/macarchy_touchbar/paths.py b/macarchy_touchbar/paths.py new file mode 100644 index 0000000..466e010 --- /dev/null +++ b/macarchy_touchbar/paths.py @@ -0,0 +1,23 @@ +"""Where the daemon's data lives. + +modules/, config/layouts.toml and fonts/*.codepoints sit one directory above the +python package. That was already true in a git checkout, and the packaging work +kept it true rather than inventing a second layout: the package installs +macarchy_touchbar/ NEXT TO modules/ and config/ under /usr/share/macarchy-touchbar, +so "one level up from the code" resolves correctly in both. + +Co-locating them is what makes arch=('any') honest. Putting the python package in +site-packages instead would bake the building interpreter's version into the +artifact, and a target whose python differs by a minor version gets ImportError +while depends=('python') claims to be satisfied. macarchy-install#16. + +$MACARCHY_TOUCHBAR_DATA overrides, and is honoured even when it points nowhere: +someone who sets it meant it, and a silent fallback would hide the typo until the +bar came up with no modules on it. +""" +import os + + +def data_root(): + return os.environ.get("MACARCHY_TOUCHBAR_DATA") or \ + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) diff --git a/release-please-config.json b/release-please-config.json index 0a52ab3..ae0c7cc 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -7,7 +7,10 @@ "release-type": "simple", "package-name": "macarchy-touchbar", "include-component-in-tag": false, - "changelog-path": "CHANGELOG.md" + "changelog-path": "CHANGELOG.md", + "extra-files": [ + "PKGBUILD" + ] } } } diff --git a/tests/test_data_root.py b/tests/test_data_root.py new file mode 100644 index 0000000..d79cfc1 --- /dev/null +++ b/tests/test_data_root.py @@ -0,0 +1,68 @@ +"""Where the daemon looks for modules/, config/ and fonts/. + +The data sits one directory above the python package. That was already true in a +git checkout; the packaging work kept it true rather than adding a second layout, +by installing macarchy_touchbar/ NEXT TO modules/ and config/ under +/usr/share/macarchy-touchbar. macarchy-install#16. + +The checkout case is the one that must never break: ./install.sh symlinks +bin/macarchy-touchbar out of the repo and everything still has to resolve. +""" +import os +import subprocess +import sys + +from macarchy_touchbar import paths + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def test_the_env_var_wins(tmp_path, monkeypatch): + monkeypatch.setenv("MACARCHY_TOUCHBAR_DATA", str(tmp_path)) + assert paths.data_root() == str(tmp_path) + + +def test_an_env_var_pointing_nowhere_is_still_honoured(tmp_path, monkeypatch): + # Explicit beats clever: a silent fallback would hide the typo until the bar + # came up with no modules on it. + missing = tmp_path / "gone" + monkeypatch.setenv("MACARCHY_TOUCHBAR_DATA", str(missing)) + assert paths.data_root() == str(missing) + + +def test_the_data_sits_beside_the_code(monkeypatch): + monkeypatch.delenv("MACARCHY_TOUCHBAR_DATA", raising=False) + root = paths.data_root() + assert os.path.isdir(os.path.join(root, "modules")) + assert os.path.isfile(os.path.join(root, "config", "layouts.toml")) + assert os.path.isdir(os.path.join(root, "macarchy_touchbar")) + + +def test_a_package_layout_resolves_the_same_way(tmp_path, monkeypatch): + # Simulate /usr/share/macarchy-touchbar: the python package beside the data. + # No special case in data_root() is what makes the two layouts one rule. + monkeypatch.delenv("MACARCHY_TOUCHBAR_DATA", raising=False) + share = tmp_path / "share" / "macarchy-touchbar" + (share / "macarchy_touchbar").mkdir(parents=True) + (share / "modules").mkdir() + (share / "macarchy_touchbar" / "paths.py").write_text( + (ROOT / "macarchy_touchbar" / "paths.py").read_text() + if hasattr(ROOT, "__truediv__") else + open(os.path.join(ROOT, "macarchy_touchbar", "paths.py")).read()) + (share / "macarchy_touchbar" / "__init__.py").write_text("") + out = subprocess.run( + [sys.executable, "-c", + "import sys; sys.path.insert(0, %r);" + "from macarchy_touchbar.paths import data_root; print(data_root())" % str(share)], + capture_output=True, text=True, env={k: v for k, v in os.environ.items() + if k != "MACARCHY_TOUCHBAR_DATA"}) + assert out.stdout.strip() == str(share), out.stderr + + +def test_the_launcher_finds_the_tree_in_a_packaged_layout(tmp_path): + # bin/macarchy-touchbar sits in /usr/bin once packaged, so "one level up" is + # /usr and useless — it has to search. This is the bootstrap that makes the + # single rule above work from an installed binary. + launcher = open(os.path.join(ROOT, "bin", "macarchy-touchbar")).read() + assert "/usr/share/macarchy-touchbar" in launcher + assert "MACARCHY_TOUCHBAR_DATA" in launcher diff --git a/tests/test_draw.py b/tests/test_draw.py index affb18e..eb7ea70 100644 --- a/tests/test_draw.py +++ b/tests/test_draw.py @@ -19,7 +19,7 @@ def pixel(s, x, y): return (r, g, b) -@pytest.mark.skipif(not os.path.exists(draw.CODEPOINTS), +@pytest.mark.skipif(not os.path.exists(draw.codepoints_path()), reason="fonts/MaterialSymbolsRounded.codepoints not downloaded (install.sh)") def test_codepoint_lookup(): assert icon_codepoint("brightness_high") == "" @@ -61,7 +61,7 @@ def test_missing_icon_falls_back_to_warning_and_never_raises(): def test_icon_never_raises_when_codepoints_file_is_missing(monkeypatch): # Fresh checkout: fonts/*.codepoints is git-ignored and may not exist. - monkeypatch.setattr(draw, "CODEPOINTS", "/nonexistent/MaterialSymbolsRounded.codepoints") + monkeypatch.setattr(draw, "codepoints_path", lambda: "/nonexistent/MaterialSymbolsRounded.codepoints") monkeypatch.setattr(draw, "_codepoints", None) s = surface() Painter(s).icon(cairo.Context(s), "brightness_high", 50, 30) diff --git a/tests/test_pkgbuild.py b/tests/test_pkgbuild.py new file mode 100644 index 0000000..e0fe799 --- /dev/null +++ b/tests/test_pkgbuild.py @@ -0,0 +1,137 @@ +"""The PKGBUILD must land everything install.sh lands. + +Two install channels that drift apart are worse than one: the package would +install cleanly and be missing a file nobody notices until the daemon needs it. +So this reads both and asserts they agree on WHAT is installed, not on where — +install.sh works in $HOME, the package works in /usr. macarchy-install#16. +""" +import json +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +PKGBUILD = (ROOT / "PKGBUILD").read_text() +# Comments mention every artefact by name, so a substring match over the whole +# file passes even when the install line is gone. Match the code. +PKG_CODE = "\n".join(l for l in PKGBUILD.splitlines() if not l.lstrip().startswith("#")) +INSTALL = (ROOT / "install.sh").read_text() + +# What install.sh actually COPIES onto the machine. The package must carry each +# one too; only the destination differs ($HOME versus /usr). +INSTALLED = [ + "bin/macarchy-touchbar", + "udev/70-macarchy-touchbar.rules", + "modules-load.d/macarchy-touchbar.conf", + "systemd/macarchy-touchbar.service", + "MaterialSymbolsRounded", +] + +# What install.sh does NOT copy, because the daemon reads it in place from the +# checkout: the python package itself, modules/ and config/. That asymmetry IS +# the packaging bug — a package has nowhere to read "in place" from, which is +# why paths.data_root() exists. So the PKGBUILD must carry them and install.sh +# never will. +USED_IN_PLACE = ["macarchy_touchbar", "modules", "config"] +ARTEFACTS = INSTALLED + USED_IN_PLACE + + +def test_the_package_carries_everything_install_sh_does(): + missing = [a for a in ARTEFACTS if a not in PKG_CODE] + assert not missing, f"install.sh installs {missing}; PKGBUILD does not mention them" + + +def test_install_sh_still_installs_what_this_test_claims(): + # The other half of the drift guard: if install.sh stops shipping one of + # these, the list above is stale and the first assertion checks a fiction. + for a in INSTALLED: + stem = a.split("/")[-1] + assert stem in INSTALL, f"{a} is in INSTALLED but install.sh no longer mentions it" + + +def test_the_in_place_data_really_is_read_through_data_root(): + # USED_IN_PLACE is only correct while the daemon resolves those directories + # rather than assuming the checkout. If that regressed, the package would + # ship files nothing reads. + daemon = (ROOT / "macarchy_touchbar" / "daemon.py").read_text() + assert "data_root()" in daemon + assert 'os.path.dirname(os.path.dirname(os.path.abspath(__file__)))' not in daemon + + +def test_the_font_is_pinned_with_a_checksum(): + # install.sh curls it from master, unpinned: the file changed between 2 Sep + # and 5 Sep. A package must be reproducible, so the URL carries a commit and + # the source carries a real sha256 rather than SKIP. + assert re.search(r"raw\.githubusercontent\.com/google/material-design-icons/[0-9a-f]{40}/", PKG_CODE) + sums = re.search(r"sha256sums=\((.*?)\)", PKG_CODE, re.S).group(1).split() + assert len(sums) == 3, "expected three sources: the tarball, the font and its codepoints" + assert any(re.fullmatch(r"'[0-9a-f]{64}'", s) for s in sums), "the font must carry a real checksum" + + +def test_site_packages_is_derived_not_hardcoded(): + # It carries the interpreter version (python3.14 today). + # No site-packages at all now: the python package is co-located with its + # data under /usr/share, which is what keeps arch=('any') honest. + assert "sysconfig" not in PKG_CODE + assert "site-packages" not in PKG_CODE + assert not re.search(r"python3\.\d", PKG_CODE) + + +def test_the_package_is_arch_independent(): + assert "arch=('any')" in PKG_CODE + + +def test_pkgver_is_maintained_by_release_please(): + # Without the marker, release-please stops bumping pkgver and a release + # ships a package whose version is the previous tag's — silently. + assert "x-release-please-version" in PKG_CODE + cfg = json.loads((ROOT / "release-please-config.json").read_text()) + assert "PKGBUILD" in cfg["packages"]["."]["extra-files"] + + +def test_the_codepoints_are_shipped_and_not_skipped(): + # draw.py:57 opens them; they are gitignored so they are absent from the + # release tarball. A `|| true` here would ship a bar with no icons. + assert "codepoints" in PKG_CODE + assert "|| true" not in PKG_CODE, "a silent skip in package()" + + +def test_the_package_job_lives_where_it_will_actually_fire(): + # A `release: [published]` trigger never fires on the real path: release-please + # creates the Release with GITHUB_TOKEN, and GitHub raises no workflow run from + # a GITHUB_TOKEN event. The job has to hang off release-please's own output. + assert not (ROOT / ".github" / "workflows" / "package.yml").exists() + wf = (ROOT / ".github" / "workflows" / "release-please.yml").read_text() + assert "release_created" in wf + assert "types: [published]" not in wf + + +def test_the_upload_globs_and_clobbers(): + wf = (ROOT / ".github" / "workflows" / "release-please.yml").read_text() + assert "*.pkg.tar.*" in wf, "hardcoding an extension uploads nothing when PKGEXT differs" + assert "--clobber" in wf, "a re-run must replace the asset, not fail" + + +def test_the_package_job_builds_the_tag_not_the_branch(): + # Without an explicit ref a manual re-run checks out the default branch and + # clobbers an old release with a package built from a different tag. + wf = (ROOT / ".github" / "workflows" / "release-please.yml").read_text() + assert "ref: ${{ needs.release-please.outputs.tag_name" in wf + assert "github-cli" in wf, "gh lives on the runner, not inside the container" + assert "pacman -Syu" in wf, "a partial upgrade reads as a build bug" + + +def test_the_unit_is_repointed_away_from_HOME(): + # The shipped unit says %h/.local/bin/… because install.sh symlinks there; a + # package install writes nothing into $HOME, so shipping it verbatim gives + # 203/EXEC and ten restarts from a package that installed perfectly. + assert "%h/" in (ROOT / "systemd" / "macarchy-touchbar.service").read_text() + assert "sed 's|%h/" in PKG_CODE and "/usr/bin/" in PKG_CODE + + +def test_there_is_a_scriptlet_for_what_pacman_cannot_do(): + # usermod -aG video, modprobe uinput and masking tiny-dfr are install.sh's + # non-file half. pacman does none of it; silence would leave a blank bar. + assert "install=macarchy-touchbar.install" in PKG_CODE + s = (ROOT / "macarchy-touchbar.install").read_text() + for step in ("video", "uinput", "tiny-dfr"): + assert step in s