From 38d26060f4896e0bbb1ad3751246dcc47d585d89 Mon Sep 17 00:00:00 2001 From: rmichaelthomas Date: Mon, 28 Sep 2026 01:55:03 -0700 Subject: [PATCH] 0.18.3: the macOS binary runs under the hardened runtime 0.18.2's liminate-macos-arm64 can't start. It is signed with our Developer ID and the hardened runtime, but the Python.framework it unpacks at launch kept its original team's signature, and library validation refuses it: "mapping process and mapped file (non-platform) have different Team IDs". codesign --verify passed, so nothing caught it. PyInstaller now signs everything it collects (--codesign-identity), which means the certificate is imported before the build instead of after. build/build_macos_release.sh holds the build, sign and a run of the built binary; the workflow calls it, and so can a local release. A binary built this way runs with library validation on, including after an app bundler re-signs it with the app's own entitlements (checked in a signed CueCue bundle). Actions can't run right now, so the macOS asset for this tag is released by hand with that script (RELEASING.md, "When Actions can't run"). The release job no longer replaces an asset that is already there, so re-running the workflow later adds Linux, Windows and PyPI without changing the macOS binary CueCue pins. No language change. The fixture is renamed for the version, as before. 1753 passed, 2 skipped on Python 3.12; grammar projections up to date. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01PqrbVBAmoTQETg9Yc6aXzz --- .github/workflows/release.yml | 17 +++++----- RELEASING.md | 32 +++++++++++++++++++ build/build_macos_release.sh | 23 +++++++++++++ pyproject.toml | 2 +- ...ce-0.18.2.json => conformance-0.18.3.json} | 2 +- 5 files changed, 66 insertions(+), 10 deletions(-) create mode 100755 build/build_macos_release.sh rename tests/fixtures/{conformance-0.18.2.json => conformance-0.18.3.json} (99%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed6547d..5044891 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,9 +18,8 @@ jobs: - name: Install build dependencies run: pip install ".[build]" - - name: Build binary with PyInstaller - run: pyinstaller --onefile --name liminate --collect-all liminate build/entry.py - + # The certificate comes before the build: PyInstaller signs what it + # collects with it (build/build_macos_release.sh says why). - name: Import signing certificate env: APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }} @@ -51,11 +50,8 @@ jobs: fi echo "SIGNING_IDENTITY=$IDENTITY" >> "$GITHUB_ENV" - - name: Codesign binary - run: | - codesign --force --options runtime --timestamp \ - --sign "$SIGNING_IDENTITY" dist/liminate - codesign --verify --verbose dist/liminate + - name: Build, sign and run the binary + run: build/build_macos_release.sh - name: Notarize binary env: @@ -185,8 +181,13 @@ jobs: cp artifacts/liminate-windows-x64/liminate-windows-x64.exe release/ chmod +x release/liminate-macos-arm64 release/liminate-linux-x64 + # An asset already on the release stays. When Actions can't run, the + # macOS binary is built and attached by hand (RELEASING.md), and + # downstream builds pin its SHA-256; a later run of this workflow fills + # in the rest without replacing it. - uses: softprops/action-gh-release@v3 with: + overwrite_files: false files: | release/liminate-macos-arm64 release/liminate-linux-x64 diff --git a/RELEASING.md b/RELEASING.md index 15ee028..9536357 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -94,6 +94,38 @@ The binary lands at `dist/liminate`. The `--collect-all liminate` flag is required by the src/ layout — without it PyInstaller misses submodules like `packs/timer.py`. +The macOS release binary is built by `build/build_macos_release.sh` +instead, which the workflow also calls. It passes +`--codesign-identity`, so the Python.framework inside the one-file +binary is signed with the same team as the binary itself. Without that, +the hardened runtime's library validation refuses the framework and the +binary exits before running anything, which is what happened to +0.18.2's macOS asset. `codesign --verify` passes either way, so the +script also runs the built binary. + +--- + +## When Actions can't run + +The macOS asset can be released by hand with the same recipe: + +```bash +python3.12 -m venv .venv-release && . .venv-release/bin/activate +pip install ".[build]" +SIGNING_IDENTITY="Developer ID Application: ()" \ + build/build_macos_release.sh +ditto -c -k dist/liminate liminate.zip +xcrun notarytool submit liminate.zip --keychain-profile --wait +mv dist/liminate dist/liminate-macos-arm64 +git tag v0.x.x && git push origin v0.x.x +gh release create v0.x.x dist/liminate-macos-arm64 --title v0.x.x --notes "…" +``` + +When Actions runs again, re-run the tag's `Release` workflow. It builds +the Linux and Windows binaries, publishes to PyPI, and leaves the +macOS asset already on the release alone (`overwrite_files: false`), +so a SHA-256 pinned downstream stays valid. + --- ## Homebrew tap (manual, future) diff --git a/build/build_macos_release.sh b/build/build_macos_release.sh new file mode 100755 index 0000000..eaa502c --- /dev/null +++ b/build/build_macos_release.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# The macOS release binary: build, sign, and run it. release.yml calls this, +# and so does a local release when Actions can't run, so both produce the +# binary the same way. Needs SIGNING_IDENTITY (a Developer ID Application +# identity) and the [build] extra installed. Notarization is a separate step. +set -euo pipefail +: "${SIGNING_IDENTITY:?set SIGNING_IDENTITY to a Developer ID Application identity}" + +# --codesign-identity signs every binary PyInstaller collects with our +# identity, above all the Python.framework a one-file build unpacks at launch. +# Without it that framework keeps its original team's signature, and the +# hardened runtime's library validation refuses to load it into a process +# signed by ours: the binary exits before running a line. 0.18.2's did. +pyinstaller --noconfirm --onefile --name liminate --collect-all liminate \ + --codesign-identity "$SIGNING_IDENTITY" build/entry.py + +codesign --force --options runtime --timestamp --sign "$SIGNING_IDENTITY" dist/liminate +codesign --verify --strict --verbose dist/liminate + +# Run it the way a user does. 0.18.2 passed codesign --verify and still +# could not start. +dist/liminate --version +dist/liminate --quiet --test examples/program1_basics.limn > /dev/null diff --git a/pyproject.toml b/pyproject.toml index d1bbdd6..8fb5178 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ where = ["src"] [project] name = "liminate" -version = "0.18.2" +version = "0.18.3" description = "A prose-as-syntax language designed from the human end." requires-python = ">=3.10" license = "Apache-2.0" diff --git a/tests/fixtures/conformance-0.18.2.json b/tests/fixtures/conformance-0.18.3.json similarity index 99% rename from tests/fixtures/conformance-0.18.2.json rename to tests/fixtures/conformance-0.18.3.json index 68abd4f..7b902c4 100644 --- a/tests/fixtures/conformance-0.18.2.json +++ b/tests/fixtures/conformance-0.18.3.json @@ -1579,6 +1579,6 @@ } ], "generator": "scripts/gen_conformance_corpus.py", - "language_version": "0.18.2", + "language_version": "0.18.3", "surface": "tokenize -> reorder -> parse -> analyze -> render" }