Compare commits

..
Author SHA1 Message Date
gronod ccaad2d940 docs: sync README with Monterey floor, M10, wiki, and packaging
macOS CI / package (pull_request) Canceled after 0s
macOS CI / build-and-test (pull_request) Canceled after 51s
macOS CI / build-and-test (push) Failing after 43m55s
macOS CI / package (push) Skipped
Requirements were still Xcode 15.4 / Swift 6. Point the product
at the user wiki, record host-arch tests, the dmgbuild venv, and
the current develop-first git workflow.
2026-09-14 12:34:16 +00:00
gronod 7e5e20cb69 Merge origin/main into develop (keep README)
macOS CI / package (push) Canceled after 0s
macOS CI / build-and-test (push) Canceled after 16m10s
2026-09-14 13:23:21 +01:00
gronod e72039c123 Delete README.md 2026-09-14 13:11:43 +01:00
gronod fa02e774ca Merge pull request 'fix(packaging): DMG background visible on Sonoma+ (#95)' (#167) from fix/95-dmg-background-sonoma into develop
macOS CI / build-and-test (pull_request) Skipped
macOS CI / build-and-test (push) Successful in 38m21s
macOS CI / package (push) Successful in 4m14s
Reviewed-on: #167
2026-09-14 11:55:39 +01:00
gronod 2a8ad4d5d4 fix(ci): cap pip below 26.1 in the Python 3.9 dmgbuild venv
macOS CI / build-and-test (push) Successful in 11s
macOS CI / package (push) Successful in 4m5s
macOS CI / package (pull_request) Canceled after 0s
macOS CI / build-and-test (pull_request) Canceled after 3m29s
Unbounded pip upgrade installed 26.2.1, which uses
dataclass(slots=True) and crashes on Monterey 3.9. Recreate
the venv if pip is already broken, then pin pip>=24.3,<26.1
before force-installing dmgbuild 1.6.7.
2026-09-14 10:34:16 +00:00
gronod c2ac9341c8 fix(ci): force dmgbuild 1.6.7 into the package venv
macOS CI / package (pull_request) Canceled after 0s
macOS CI / build-and-test (pull_request) Canceled after 1m6s
macOS CI / build-and-test (push) Successful in 11s
macOS CI / package (push) Failing after 35s
Python 3.9 hides 1.6.6+ (Requires-Python >=3.10), so
dmgbuild>=1.6.5 left a cached 1.6.5 install. The wheels are
py3-none-any; install 1.6.7 with PIP_IGNORE_REQUIRES_PYTHON,
force-reinstall ds_store>=1.3.3 and mac_alias>=2.2.3, and fail
if the venv is still older than 1.6.7.
2026-09-14 10:29:33 +00:00
gronod 1c310706e4 fix(ci): isolate dmgbuild in a venv, skip it on the test job
macOS CI / package (pull_request) Canceled after 0s
macOS CI / build-and-test (pull_request) Canceled after 1m50s
macOS CI / build-and-test (push) Successful in 13s
macOS CI / package (push) Successful in 4m12s
ensure-host-tools.sh always bootstraps xcodegen. dmgbuild now
lives in build/.venv-dmgbuild and is only installed when
INSTALL_DMGBUILD=1 (package job). The test job no longer
pip-installs dmgbuild, which is what failed on Python 3.9.

package-release.sh reuses that venv (or bootstraps it locally)
instead of creating a second copy.
2026-09-14 10:22:09 +00:00
gronod 7f6c47d85c fix(ci): do not require dmgbuild 1.6.7 on Monterey Python 3.9
macOS CI / package (pull_request) Canceled after 0s
macOS CI / build-and-test (pull_request) Canceled after 6m31s
The tools step runs on the test job. System Python is 3.9;
dmgbuild 1.6.6+ declares Requires-Python >=3.10, so pip only
lists 1.6.5 and the 1.6.7 floor failed the gate.

Install the newest wheel this interpreter accepts. Keep the
HiDPI TIFF background path and the hard-fail if art is missing.
2026-09-14 10:14:29 +00:00
gronod 3b6323378e fix(packaging): DMG background visible on Sonoma+ (#95)
macOS CI / build-and-test (pull_request) Failing after 20s
macOS CI / package (pull_request) Skipped
Pin dmgbuild >= 1.6.7 so Finder gets a bookmark instead of a
Monterey-era Alias Manager blob. Feed a HiDPI TIFF from
tiffutil rather than the 1x RGBA PNG, and fail the package job
if the art is missing instead of shipping a grey window.

Exclude installer chrome from the app bundle; it is not an
in-app asset.
2026-09-14 09:57:37 +00:00
gronod d37dcedfd9 Update README.md 2026-09-09 19:18:43 +01:00
7 changed files with 262 additions and 76 deletions
+5 -2
View File
@@ -35,7 +35,8 @@ jobs:
# The job still succeeds quickly so `package`'s `needs:` stays satisfied. # The job still succeeds quickly so `package`'s `needs:` stays satisfied.
# Homebrew's xcodegen formula requires Xcode 15.3, which cannot be # Homebrew's xcodegen formula requires Xcode 15.3, which cannot be
# installed on macOS 12 (#109). The script installs a pinned # installed on macOS 12 (#109). The script installs a pinned
# prebuilt release instead. # prebuilt release instead. dmgbuild is not installed here — the
# test job does not package (#95).
- name: Ensure host tools - name: Ensure host tools
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))" if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
run: scripts/ensure-host-tools.sh run: scripts/ensure-host-tools.sh
@@ -193,8 +194,10 @@ jobs:
# scripts/package-release.sh runs `xcodegen generate` and dmgbuild; # scripts/package-release.sh runs `xcodegen generate` and dmgbuild;
# see build-and-test for why brew is not used on macOS 12 (#109). # see build-and-test for why brew is not used on macOS 12 (#109).
# INSTALL_DMGBUILD isolates dmgbuild in build/.venv-dmgbuild so
# the test job never pip-installs it (#95).
- name: Ensure host tools - name: Ensure host tools
run: scripts/ensure-host-tools.sh run: INSTALL_DMGBUILD=1 scripts/ensure-host-tools.sh
- name: Package release - name: Package release
run: scripts/package-release.sh run: scripts/package-release.sh
+143 -51
View File
@@ -1,45 +1,75 @@
# ICCery # ICCery
Native macOS frontend for printer ICC/ICM profiling. ICCery walks a user from chart generation through measurement, `colprof`, verification, and ColorSync install. It is **not** a colour engine. Native macOS frontend for printer ICC/ICM profiling. ICCery walks a user from
chart generation through measurement, `colprof`, verification, and ColorSync
install. It is **not** a colour engine.
All measurement, chart generation, and profile mathematics live in the [Gronod ArgyllCMS 3.5.0 fork](https://git.i3omb.com/gronod/argyllcms), spawned as AGPLv3 child processes. The GUI never `dlopen`s or links Argyll. **End-user guide:** the [repository wiki](https://git.i3omb.com/gronod/iccery-v2-mac/wiki)
covers every screen (Getting Started through Troubleshooting). This README is
for building, packaging, and contributing.
All measurement, chart generation, and profile mathematics live in the
[Gronod ArgyllCMS 3.5.0 fork](https://git.i3omb.com/gronod/argyllcms), spawned
as AGPLv3 child processes. The GUI never `dlopen`s or links Argyll.
| | | | | |
|---|---| |---|---|
| Product | ICCery v2 for macOS | | Product | ICCery v2 for macOS |
| Bundle | `com.gronod.iccery2` | | Bundle | `com.gronod.iccery2` |
| Version | 2.0.0 |
| Floor | macOS 12.0 Monterey, universal `arm64` + `x86_64` | | Floor | macOS 12.0 Monterey, universal `arm64` + `x86_64` |
| Toolchain | Xcode 14.2 / Swift 5.7 (project `SWIFT_VERSION` is 5.0) |
| CI | Gitea Actions `macos-12` runner |
| Default branch | `develop` | | Default branch | `develop` |
| M6 | Stage 0 calibration, CGATS import, SceneKit gamut viewer, packaging — shipped on `develop` | | M6 | Stage 0 calibration, CGATS import, SceneKit gamut viewer, packaging — shipped |
| M7 | Pre-UAT hardening & baseline consolidation — shipped on `develop` | | M7 | Pre-UAT hardening — shipped |
| M8 | Deduplication/consolidation contracts & UAT-ready hardening (#79#86) — shipped on `develop` | | M8 | Deduplication contracts & UAT-ready hardening (#79#86) — shipped |
| M9 | macOS 12 / Xcode 14.2 retarget — shipped on `develop` (PR #145) | | M9 | macOS 12 / Xcode 14.2 retarget (PR #145) — shipped |
| M10 | Studio workflow (#146#149) — in flight on `milestone/m10-studio` | | M10 | Studio workflow: gamut compare (#147), Spot Read (#148), project files (#149) shipped on `develop`; media library (#146) is in the tree, issue still open |
| Licence | Proprietary source in [`LICENCE.md`](LICENCE.md); bundled Argyll sidecars remain AGPLv3 | | Licence | Proprietary source in [`LICENCE.md`](LICENCE.md); bundled Argyll sidecars remain AGPLv3 |
## What it does ## What it does
The wizard is artefact-gated: The wizard is artefact-gated:
1. **Stage 0**printer calibration: `printcal` / `applycal` session, `CAL_` basename restore 1. **Stage 1**`targen` `.ti1`
2. **Stage 1**`targen``.ti1` 2. **Stage 2**`printtarg``.ti2` + TIFF, unmanaged `lp` spool, bound `NSPrintPanel`
3. **Stage 2**`printtarg``.ti2` + TIFF, unmanaged `lp` spool, bound `NSPrintPanel` 3. **Stage 3**`instlist` + streaming `chartread` (strip / XY / handheld) → `.ti3`, multi-pass average, CIEDE2000
4. **Stage 3**`instlist` + streaming `chartread` (strip / XY / handheld) → `.ti3`, multi-pass average, CIEDE2000 4. **Stage 4**`colprof``.icc` / `.icm`; optional `applycal`; `iccgamut` next to the profile
5. **Stage 4**`colprof``.icc` / `.icm`; optional `applycal`; `iccgamut` next to the profile 5. **Stage 5**`profcheck`, verification history, ColorSync user/system install
6. **Stage 5**`profcheck`, verification history, ColorSync user/system install
Plus CGATS dataset import (`.ti3` / `.txt` / `.cgats` / `.csv`), SceneKit gamut preview with sRGB overlay, and signed `.dmg` packaging. Plus:
**Not this product:** display calibration (`dispwin` / `dispread`), i18n, Windows/Linux print trees, in-process Argyll, App Sandbox. - **Calibrate Printer** — optional `printcal` / `applycal` session under a `CAL_` basename
- **CGATS import** — `.ti3` / `.txt` / `.cgats` / `.csv`
- **Media recipes and presets** — printer + paper + ink bound to a preset and optional `.cal`
- **Spot Read** — live one-patch Lab/XYZ from the instrument
- **Project files** — `.icceryproj` bookmark over folder, basename, recipe, last ΔE
- **Gamut viewer** — SceneKit Lab hull, sRGB overlay, second-profile compare, click-inspect
- **Settings** — default instrument, ΔE good/warning cutoffs, install location, logging
- Signed `.dmg` packaging with a HiDPI Finder background (Monterey through Sonoma)
**Not this product:** display calibration (`dispwin` / `dispread`), i18n,
Windows/Linux print trees, in-process Argyll, App Sandbox.
## Requirements ## Requirements
- macOS 14+ To **run** a packaged build:
- Xcode 15.4+ with the macOS 14 SDK and Swift 6.0
- [XcodeGen](https://github.com/yonaskolb/XcodeGen)
- Network once, to fetch Argyll sidecars
App Sandbox is **off**. Hardened Runtime is **on**. Entitlements live in `ICCery.entitlements`. - macOS 12.0 Monterey or later (Intel or Apple silicon)
To **build** on the supported CI/host floor:
- macOS 12 with **Xcode 14.2** (macOS 12 SDK, Swift 5.7)
- [XcodeGen](https://github.com/yonaskolb/XcodeGen) **2.38.0** (Homebrews current
formula needs Xcode 15.3; CI installs the pinned zip via
`scripts/ensure-host-tools.sh`)
- Network once, to fetch Argyll sidecars
- For DMGs: Python 3.9+ and `dmgbuild==1.6.7` in `build/.venv-dmgbuild`
(`INSTALL_DMGBUILD=1 scripts/ensure-host-tools.sh`)
App Sandbox is **off**. Hardened Runtime is **on**. Entitlements live in
`ICCery.entitlements`.
## Build ## Build
@@ -49,7 +79,7 @@ cd iccery-v2-mac
git checkout develop git checkout develop
make fetch-argyll # Vendor/Argyll/macos-universal/, ad-hoc signed make fetch-argyll # Vendor/Argyll/macos-universal/, ad-hoc signed
make test # xcodegen + xcodebuild build test make test # xcodegen + xcodebuild build test (host arch)
make universal # ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO make universal # ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO
``` ```
@@ -62,9 +92,14 @@ xcodebuild test -scheme ICCery \
ARCHS="$(uname -m)" ARCHS="$(uname -m)"
``` ```
`project.yml` sets `ARCHS: "$(ARCHS_STANDARD)"`, so a plain `xcodebuild test` (and `make test`) builds universal; the `ARCHS="$(uname -m)"` override narrows it to the host slice. `project.yml` sets `ARCHS: "$(ARCHS_STANDARD)"`. CI and `make test` override
that with `ARCHS="$(uname -m)"` so unit/UI tests build the host slice only.
Fat binaries are `make universal` / `scripts/package-release.sh`.
Sidecars are **not** in git. `scripts/fetch-argyll.sh` pulls the latest (or `ARGYLL_RELEASE_TAG`) macOS-universal release from `gronod/argyllcms`, extracts to `Vendor/Argyll/macos-universal/`, ad-hoc signs every Mach-O, and fails if `codesign -dvv` or the `instlist` marker is missing. Sidecars are **not** in git. `scripts/fetch-argyll.sh` pulls the latest (or
`ARGYLL_RELEASE_TAG`) macOS-universal release from `gronod/argyllcms`, extracts
to `Vendor/Argyll/macos-universal/`, ad-hoc signs every Mach-O, and fails if
`codesign -dvv` or the `instlist` marker is missing.
```bash ```bash
# optional # optional
@@ -74,9 +109,11 @@ export ARGYLL_RELEASE_TAG=… # default: latest
export GITEA_TOKEN=# private releases export GITEA_TOKEN=# private releases
``` ```
`make clean` drops `ICCery.xcodeproj`, `DerivedData`, and `Packages/ICCeryCore/.build`. `make clean` drops `ICCery.xcodeproj`, `DerivedData`, and
`Packages/ICCeryCore/.build`.
Do not open the generated xcodeproj as the source of truth. Edit `project.yml` and regenerate. Do not open the generated xcodeproj as the source of truth. Edit `project.yml`
and regenerate.
## Release packaging ## Release packaging
@@ -84,11 +121,20 @@ Do not open the generated xcodeproj as the source of truth. Edit `project.yml` a
scripts/package-release.sh # fetch → sign → universal build → verify → DMG scripts/package-release.sh # fetch → sign → universal build → verify → DMG
``` ```
The script builds with a fixed derived data path (`build/DerivedData`), The script builds with a fixed derived data path (`build/DerivedData`), locates
locates `Release/ICCery.app` from it, signs the bundle, recursively verifies `Release/ICCery.app` from it, signs the bundle, recursively verifies every
every bundled Mach-O sidecar (`scripts/verify-sidecar-signatures.sh`), and bundled Mach-O sidecar (`scripts/verify-sidecar-signatures.sh`), builds a
writes `ICCery-${VERSION}-${BUILD_NUM}.dmg` via `dmgbuild`. Sidecars stay HiDPI TIFF from `Resources/dmg-background.png` (+ `@2x`) via `tiffutil`, and
ad-hoc signed inside the bundle — the app is never `codesign --deep`ed. writes `ICCery-${VERSION}-${BUILD_NUM}.dmg` with `dmgbuild==1.6.7`.
Sidecars stay ad-hoc signed inside the bundle — the app is never
`codesign --deep`ed.
`dmgbuild` is **not** a test-job dependency. The package job sets
`INSTALL_DMGBUILD=1` so `scripts/ensure-host-tools.sh` creates
`build/.venv-dmgbuild`. On the Monterey runner (Python 3.9) that install uses
`PIP_IGNORE_REQUIRES_PYTHON=1` and pins `pip>=24.3,<26.1` (pip 26.1+ needs
3.10). Missing background art is a hard fail (#95).
Environment variables read by the pipeline: Environment variables read by the pipeline:
@@ -110,30 +156,47 @@ Resources/ assets; Argyll reference files (not the tools)
Vendor/Argyll/ fetched sidecars (gitignored) Vendor/Argyll/ fetched sidecars (gitignored)
Tests/ICCeryCoreTests/ argv goldens, parsers, stores Tests/ICCeryCoreTests/ argv goldens, parsers, stores
Tests/ICCeryUITests/ fixture / mock-binary UI tests Tests/ICCeryUITests/ fixture / mock-binary UI tests
scripts/ensure-host-tools.sh
scripts/fetch-argyll.sh scripts/fetch-argyll.sh
scripts/package-release.sh
docs/ functional spec + v2 ticket plan docs/ functional spec + v2 ticket plan
``` ```
`ICCeryPrintKit` (issue #16, Quartz / AirPrint / TargetPrint) is v2.1 and is not in this tree. `ICCeryPrintKit` (issue #16, Quartz / AirPrint / TargetPrint) is v2.1 and is
not in this tree.
## Architecture ## Architecture
- **Spawn, never link.** Tools resolve through `BinaryResolver` inside the bundle / `Vendor` tree. `$PATH` is not searched. `ARGYLL_NOT_INTERACTIVE=1` is always set. - **Spawn, never link.** Tools resolve through `BinaryResolver` inside the
- **`ProcessManager` actor** owns child lifetime. Streaming tools (`chartread`, `printcal`, etc.) use the event bus; one-shot tools use `runCaptured`. Exclusive `ProcessID` leases. Quit path: `q\n`, ~500 ms, kill; `killAll` on terminate. bundle / `Vendor` tree. `$PATH` is not searched. `ARGYLL_NOT_INTERACTIVE=1`
- **Argv builders** in ICCeryCore (`TargenArgs`, `PrinttargArgs`, `ChartreadArgs`, `ColprofArgs`, `ApplycalArgs`, `IccgamutArgs`, `ProfcheckArgs`, `LpArgs`, …). UI must not concatenate flags. is always set.
- **Atomic artefacts.** Writes go to `*.tmp` then `replaceItemAt`. `applycal` must not replace the input profile on cancel or non-zero exit. - **`ProcessManager` actor** owns child lifetime. Streaming tools
- **Concurrency.** View models are `@MainActor`. No blocking I/O on the main actor. SwiftUI `@Observable` for new state. (`chartread`, `printtarg`, `colprof`, …) use the event bus; one-shot tools
- **Print.** Unmanaged `lp` with ColorSync suppression (`AP_ColorMatchingMode` / `AP.ColorMatchingMode`). Captured `NSPrintPanel` options win over derived CUPS keys. Never `lp -o raw`. (`printcal`, `applycal`, CUPS) use `runCaptured`. Exclusive `ProcessID`
leases. Quit path: `q\n`, ~500 ms, kill; `killAll` on terminate.
- **Argv builders** in ICCeryCore (`TargenArgs`, `PrinttargArgs`,
`ChartreadArgs`, `ColprofArgs`, `ApplycalArgs`, `IccgamutArgs`,
`ProfcheckArgs`, `LpArgs`, `SpotReadArgs`, …). UI must not concatenate flags.
- **Atomic artefacts.** Writes go to `*.tmp` then `replaceItemAt`. `applycal`
must not replace the input profile on cancel or non-zero exit.
- **Concurrency.** View models are `@MainActor`. No blocking I/O on the main
actor. Swift 5.7 / macOS 12: `ObservableObject`, not Observation
`@Observable`.
- **Print.** Unmanaged `lp` with ColorSync suppression
(`AP_ColorMatchingMode` / `AP.ColorMatchingMode`). Captured `NSPrintPanel`
options win over derived CUPS keys. Never `lp -o raw`.
- **SwiftUI ViewBuilder.** Xcode 14.2 / Swift 5.7 still has the ten-child
limit. Split large `VStack`/`Group` trees (#146).
## Tests ## Tests
```bash ```bash
# full suite (host arch) # full suite (host arch) — same as CI
xcodebuild test -scheme ICCery \ xcodebuild test -scheme ICCery \
-destination 'platform=macOS' \ -destination 'platform=macOS' \
ARCHS="$(uname -m)" ARCHS="$(uname -m)"
# to compile-check both slices instead: # fat compile-check (not the default test path):
# ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO # ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO
# examples # examples
@@ -143,9 +206,20 @@ xcodebuild test -scheme ICCery -destination 'platform=macOS' \
-only-testing:ICCeryUITests/Milestone5UITests -only-testing:ICCeryUITests/Milestone5UITests
``` ```
UI tests need an unlocked console (`IOConsoleLocked=false`). Mock Argyll / CUPS fixtures live under the test bundles; they must not be treated as proof that a real `.gam` / `.icc` was extracted. CI (`.gitea/workflows/macos.yml`) runs `build-and-test` then `package` on
`develop` and on `v*` tags. Tags whose name contains `prerelease` skip the
test job and still package. `pull_request` is wired for **`develop` only**.
Hardware gates (real instrument, real printer, Gatekeeper-open `.dmg`) are manual and block release, not compile. UI tests need an unlocked console (`IOConsoleLocked=false`). Mock Argyll /
CUPS fixtures live under the test bundles; they must not be treated as proof
that a real `.gam` / `.icc` was extracted.
Hardware gates (real instrument, real printer, Gatekeeper-open `.dmg`) are
manual and block release, not compile.
`ArgyllRunnerPrinttargTests.testSuccess` can flake if streaming stdout is
dropped on a fast mock exit; that is a `ProcessManager` drain race, not a
missing fixture.
## Instruments ## Instruments
@@ -156,11 +230,18 @@ Detected via bundled `instlist`:
- SpyderPrint (`p3`) - SpyderPrint (`p3`)
- SpectroScan (`SS`) - SpectroScan (`SS`)
- DTP20 / 22 / 41 / 51 - DTP20 / 22 / 41 / 51
- XY tables (SpectroScan, i1iO) when the `instlist` name matches `/spectro\s?scan|i1io/i` - XY tables (SpectroScan, i1iO) when the `instlist` name matches
`/spectro\s?scan|i1io/i`
## Docs ## Docs
Normative spec is [`docs/`](docs/README.md). Implementation order: | Where | Audience |
|---|---|
| [Wiki](https://git.i3omb.com/gronod/iccery-v2-mac/wiki) | End users — screens, workflow, troubleshooting |
| [`docs/`](docs/README.md) | Functional spec (normative for implementers) |
| [`AGENTS.md`](AGENTS.md), [`BUILD-PLAN.md`](BUILD-PLAN.md) | Agent / branch rules |
Implementation order in `docs/`:
| Doc | Topic | | Doc | Topic |
|---|---| |---|---|
@@ -168,29 +249,40 @@ Normative spec is [`docs/`](docs/README.md). Implementation order:
| [`docs/03-ipc-and-process-manager.md`](docs/03-ipc-and-process-manager.md) | Spawn / stdin / kill | | [`docs/03-ipc-and-process-manager.md`](docs/03-ipc-and-process-manager.md) | Spawn / stdin / kill |
| [`docs/04-argyll-binaries.md`](docs/04-argyll-binaries.md) | CLI argv | | [`docs/04-argyll-binaries.md`](docs/04-argyll-binaries.md) | CLI argv |
| [`docs/06-wizard-and-artefacts.md`](docs/06-wizard-and-artefacts.md) | Gating | | [`docs/06-wizard-and-artefacts.md`](docs/06-wizard-and-artefacts.md) | Gating |
| [`docs/23-assets.md`](docs/23-assets.md) | Icons, DMG chrome |
| [`docs/24-issues-invariants.md`](docs/24-issues-invariants.md) | Bugs that must not return | | [`docs/24-issues-invariants.md`](docs/24-issues-invariants.md) | Bugs that must not return |
| [`docs/26-v2-mac-ticket-plan.md`](docs/26-v2-mac-ticket-plan.md) | Gitea tickets | | [`docs/26-v2-mac-ticket-plan.md`](docs/26-v2-mac-ticket-plan.md) | Gitea tickets |
| [`docs/PREUAT.md`](docs/PREUAT.md) | Pre-UAT tester kit | | [`docs/PREUAT.md`](docs/PREUAT.md) | Pre-UAT tester kit |
Agent / branch rules: [`AGENTS.md`](AGENTS.md), [`BUILD-PLAN.md`](BUILD-PLAN.md).
## Git ## Git
``` ```
develop develop # integration; PRs land here unless a milestone branch is announced
└── milestone/m10-studio # M10 integration branch main # protected release line (PR from develop)
└── feat/<issue>-<slug> # one issue per branch feat/<issue>-<slug>
fix/<issue>-<slug>
``` ```
Feature PRs target the current milestone branch, not `develop`. The milestone branch merges to `develop` when its issues are green. Completion PRs for issues #146#149 target `milestone/m10-studio`; `milestone/m10-studio` merges into `develop` once all milestone gates pass. Do not open umbrella "bugfix" branches that mix tickets. Open feature/fix PRs against **`develop`**. A `milestone/m…` integration
branch is used only while that milestone is assembling; `milestone/m10-studio`
has been merged and deleted. Do not open umbrella “bugfix” branches that mix
tickets.
`main` is push-protected and requires status check
`macOS CI / build-and-test (push)`. Protected **file** patterns on `main`
block PR merges that touch matching paths — do not set that field to `*`.
## Licence ## Licence
GUI source: © 2026 Gordon Bolton — see [`LICENCE.md`](LICENCE.md). Viewing and personal evaluation only unless a separate grant says otherwise. GUI source: © 2026 Gordon Bolton — see [`LICENCE.md`](LICENCE.md). Viewing
and personal evaluation only unless a separate grant says otherwise.
ArgyllCMS binaries fetched into `Vendor/Argyll/` are **AGPLv3**. They stay subprocess-isolated (stdin / stdout / stderr only). Linking them, or spawning via `$PATH`, is a licence break. ArgyllCMS binaries fetched into `Vendor/Argyll/` are **AGPLv3**. They stay
subprocess-isolated (stdin / stdout / stderr only). Linking them, or spawning
via `$PATH`, is a licence break.
## Related ## Related
- [User wiki](https://git.i3omb.com/gronod/iccery-v2-mac/wiki)
- [gronod/argyllcms](https://git.i3omb.com/gronod/argyllcms) — Argyll 3.5.0 fork (`-u` JSON, `instlist`) - [gronod/argyllcms](https://git.i3omb.com/gronod/argyllcms) — Argyll 3.5.0 fork (`-u` JSON, `instlist`)
- [gronod/ICCery](https://git.i3omb.com/gronod/ICCery) — v1 Tauri application (spec source, not this tree) - [gronod/ICCery](https://git.i3omb.com/gronod/ICCery) — v1 Tauri application (spec source, not this tree)
+1 -1
View File
@@ -25,7 +25,7 @@ Cone-only mark for window/taskbar. Raster set:
| File | Use | | File | Use |
|------|-----| |------|-----|
| `icons/dmg-background.png` (+ `@2x`, `.svg`) | macOS DMG window (ice cream / wordmark scene). Headless `dmgbuild` after #189 | | `Resources/dmg-background.png` (+ `@2x`; source `brand/dmg-background.svg`) | macOS DMG window. `scripts/package-release.sh` builds a HiDPI TIFF and passes it to `dmgbuild==1.6.7` from `build/.venv-dmgbuild` (created by `INSTALL_DMGBUILD=1 scripts/ensure-host-tools.sh`). Monterey Python 3.9 needs `PIP_IGNORE_REQUIRES_PYTHON=1` or pip will keep 1.6.5. Missing art is a hard fail (#95, #189). |
| `icons/wix-banner.bmp`, `wix-dialog.bmp` | MSI | | `icons/wix-banner.bmp`, `wix-dialog.bmp` | MSI |
| `icons/nsis-header.bmp`, `nsis-sidebar.bmp` | NSIS | | `icons/nsis-header.bmp`, `nsis-sidebar.bmp` | NSIS |
+2
View File
@@ -21,6 +21,8 @@ targets:
- ICCery.entitlements - ICCery.entitlements
- ICCery.Debug.entitlements - ICCery.Debug.entitlements
- Argyll - Argyll
- dmg-background.png
- dmg-background@2x.png
- path: Resources/Argyll - path: Resources/Argyll
type: folder type: folder
dependencies: dependencies:
+17 -9
View File
@@ -1,10 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# scripts/dmgbuild-settings.py # scripts/dmgbuild-settings.py
# #
# dmgbuild settings for ICCery. Set DMG_APP, DMG_FILENAME and DMG_VOLUME_NAME # dmgbuild settings for ICCery. scripts/package-release.sh exports
# in the environment, or accept the defaults. Background art can be supplied # DMG_APP, DMG_FILENAME, DMG_VOLUME_NAME, and DMG_BACKGROUND (a
# later by placing a PNG at Resources/dmg-background.png and setting # HiDPI TIFF). A missing background is a hard error — a grey
# DMG_BACKGROUND. # Finder window is not an acceptable release artefact (#95).
import os import os
import sys import sys
@@ -23,15 +23,23 @@ if not app_path or not app_path.endswith('.app') or not os.path.isdir(app_path):
files = [app_path] files = [app_path]
# Background art is optional. If the referenced PNG does not exist, fall back # Finder on Sonoma+ is picky about PNG-with-alpha window pictures and
# to a plain window. See docs/23-assets.md for the DMG background spec. # about classic Alias Manager blobs. package-release.sh always passes
background = os.environ.get('DMG_BACKGROUND', 'Resources/dmg-background.png') # a flattened HiDPI TIFF as DMG_BACKGROUND. dmgbuild 1.6.7 (bookmark
if background and not os.path.exists(background): # .DS_Store) needs Python >= 3.10, which the Monterey runner does not
background = None # have; 1.6.5 + TIFF is what CI can ship (#95).
background = os.environ.get('DMG_BACKGROUND', '')
if not background or not os.path.isfile(background):
sys.stderr.write(
'error: DMG_BACKGROUND must point at an existing image '
'(got %r)\n' % background)
sys.exit(1)
icon = None icon = None
# Window size is enough for the app icon and the Applications alias. # Window size is enough for the app icon and the Applications alias.
# Bitmap is slightly larger than this rect so title-bar chrome on
# 14+ does not crop the wordmark.
window_rect = ((100, 100), (660, 400)) window_rect = ((100, 100), (660, 400))
# Use icon view without extra chrome. # Use icon view without extra chrome.
+66 -6
View File
@@ -2,19 +2,79 @@
# scripts/ensure-host-tools.sh # scripts/ensure-host-tools.sh
# #
# Bootstrap host tools needed by CI on the macOS 12 runner: # Bootstrap host tools needed by CI on the macOS 12 runner:
# - xcodegen: pinned prebuilt release from GitHub (Homebrew's current # - xcodegen: always. Pinned prebuilt release from GitHub (Homebrew's
# formula requires Xcode 15.3, which cannot be installed on macOS 12). # current formula requires Xcode 15.3, which cannot be installed on
# - dmgbuild: via pip3 (used by scripts/package-release.sh). # macOS 12).
# - dmgbuild: only when INSTALL_DMGBUILD=1 or --dmgbuild. Isolated in
# build/.venv-dmgbuild so the test job never pip-installs it.
# #
# Safe to run repeatedly: existing tools are left alone. # dmgbuild 1.6.6+, ds_store 1.3.2+ and mac_alias 2.2.3 declare
# Requires-Python >= 3.10. The wheels are py3-none-any and run on the
# runner's 3.9; PIP_IGNORE_REQUIRES_PYTHON is required or pip will only
# offer 1.6.5 and keep a cached venv on that version (#95).
# pip itself is capped at <26.1: 26.1+ needs Python 3.10.
#
# Safe to run repeatedly: existing tools are left alone unless the
# dmgbuild pin is not met.
set -eu set -eu
ROOT="$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)"
XCODEGEN_VERSION="2.38.0" XCODEGEN_VERSION="2.38.0"
INSTALL_ROOT="${XCODEGEN_HOME:-$HOME/.local/xcodegen/$XCODEGEN_VERSION}" INSTALL_ROOT="${XCODEGEN_HOME:-$HOME/.local/xcodegen/$XCODEGEN_VERSION}"
VENV="$ROOT/build/.venv-dmgbuild"
DMGBUILD_PIN="1.6.7"
echo "==> Ensuring dmgbuild" INSTALL_DMGBUILD="${INSTALL_DMGBUILD:-0}"
python3 -c "import dmgbuild" 2>/dev/null || pip3 install dmgbuild for arg in "$@"; do
case "$arg" in
--dmgbuild) INSTALL_DMGBUILD=1 ;;
esac
done
if [ "$INSTALL_DMGBUILD" = "1" ]; then
echo "==> Ensuring dmgbuild==$DMGBUILD_PIN in $VENV"
mkdir -p "$ROOT/build"
# pip 26.1+ requires Python 3.10 (dataclass slots). A leftover
# `pip install --upgrade pip` on this 3.9 venv installed 26.2.1 and
# the next pip invocation crashed. Recreate if pip is already dead.
if [ -x "$VENV/bin/python" ] \
&& ! "$VENV/bin/python" -m pip --version >/dev/null 2>&1; then
echo "==> venv pip is broken; recreating $VENV"
rm -rf "$VENV"
fi
if [ ! -x "$VENV/bin/python" ]; then
python3 -m venv "$VENV"
fi
# Without this, pip on Python 3.9 hides 1.6.6+ and leaves 1.6.5.
PIP_IGNORE_REQUIRES_PYTHON=1
export PIP_IGNORE_REQUIRES_PYTHON
"$VENV/bin/python" -m pip install --upgrade 'pip>=24.3,<26.1'
"$VENV/bin/python" -m pip install --upgrade --force-reinstall \
"dmgbuild==$DMGBUILD_PIN" \
'ds_store>=1.3.3' \
'mac_alias>=2.2.3'
"$VENV/bin/python" -c 'from importlib.metadata import version
print("dmgbuild", version("dmgbuild"))
print("ds_store", version("ds_store"))
print("mac_alias", version("mac_alias"))
parts=[]
for p in version("dmgbuild").split("."):
try:
parts.append(int("".join(c for c in p if c.isdigit()) or "0"))
except ValueError:
parts.append(0)
parts += [0, 0, 0]
raise SystemExit(0 if tuple(parts[:3]) >= (1, 6, 7) else 1)
'
if [ -n "${GITHUB_PATH:-}" ]; then
echo "$VENV/bin" >> "$GITHUB_PATH"
fi
PATH="$VENV/bin:$PATH"
export PATH
else
echo "==> Skipping dmgbuild (set INSTALL_DMGBUILD=1 for the package job)"
fi
if command -v xcodegen >/dev/null 2>&1; then if command -v xcodegen >/dev/null 2>&1; then
echo "==> xcodegen already on PATH: $(xcodegen --version)" echo "==> xcodegen already on PATH: $(xcodegen --version)"
+28 -7
View File
@@ -103,19 +103,39 @@ EOF
scripts/verify-sidecar-signatures.sh "$APP" scripts/verify-sidecar-signatures.sh "$APP"
fi fi
echo "==> Installing / locating dmgbuild" echo "==> Locating dmgbuild"
if ! command -v dmgbuild >/dev/null 2>&1; then # The package CI job already ran INSTALL_DMGBUILD=1 ensure-host-tools.sh,
# which created build/.venv-dmgbuild and prepended it to PATH. Local
# runs bootstrap the same venv if dmgbuild is missing.
VENV="$ROOT/build/.venv-dmgbuild" VENV="$ROOT/build/.venv-dmgbuild"
if [ ! -d "$VENV/bin" ]; then if ! command -v dmgbuild >/dev/null 2>&1; then
python3 -m venv "$VENV" if [ ! -x "$VENV/bin/dmgbuild" ]; then
"$VENV/bin/pip" install --upgrade pip INSTALL_DMGBUILD=1 "$ROOT/scripts/ensure-host-tools.sh" --dmgbuild
"$VENV/bin/pip" install dmgbuild
fi fi
PATH="$VENV/bin:$PATH" PATH="$VENV/bin:$PATH"
export PATH export PATH
fi fi
if ! command -v dmgbuild >/dev/null 2>&1; then if ! command -v dmgbuild >/dev/null 2>&1; then
echo "error: dmgbuild not available. Try 'python3 -m venv .venv && pip install dmgbuild'" >&2 echo "error: dmgbuild not on PATH; run INSTALL_DMGBUILD=1 scripts/ensure-host-tools.sh" >&2
exit 1
fi
echo "dmgbuild $(command -v dmgbuild)"
if [ -x "$VENV/bin/python" ]; then
"$VENV/bin/python" -c 'from importlib.metadata import version; print("dmgbuild", version("dmgbuild"))'
fi
PNG1X="$ROOT/Resources/dmg-background.png"
PNG2X="$ROOT/Resources/dmg-background@2x.png"
if [ ! -f "$PNG1X" ] || [ ! -f "$PNG2X" ]; then
echo "error: missing $PNG1X or $PNG2X" >&2
exit 1
fi
mkdir -p "$ROOT/build"
DMG_BACKGROUND="$ROOT/build/dmg-background.tiff"
echo "==> Building HiDPI DMG background TIFF"
tiffutil -cathidpicheck "$PNG1X" "$PNG2X" -out "$DMG_BACKGROUND"
if [ ! -f "$DMG_BACKGROUND" ]; then
echo "error: tiffutil did not write $DMG_BACKGROUND" >&2
exit 1 exit 1
fi fi
@@ -128,6 +148,7 @@ VOLUME_NAME="ICCery ${VERSION}"
DMG_APP="$APP" \ DMG_APP="$APP" \
DMG_FILENAME="$DMG" \ DMG_FILENAME="$DMG" \
DMG_VOLUME_NAME="$VOLUME_NAME" \ DMG_VOLUME_NAME="$VOLUME_NAME" \
DMG_BACKGROUND="$DMG_BACKGROUND" \
dmgbuild -s scripts/dmgbuild-settings.py "$VOLUME_NAME" "$DMG" dmgbuild -s scripts/dmgbuild-settings.py "$VOLUME_NAME" "$DMG"
echo "DMG: $PWD/$DMG" echo "DMG: $PWD/$DMG"