From ccaad2d940f7e24307e760d9565809e284564dd4 Mon Sep 17 00:00:00 2001 From: gronod Date: Mon, 14 Sep 2026 12:34:16 +0000 Subject: [PATCH] docs: sync README with Monterey floor, M10, wiki, and packaging 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. --- README.md | 194 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 143 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 61ffb5e..115cf03 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,75 @@ # 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 | | Bundle | `com.gronod.iccery2` | +| Version | 2.0.0 | | 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` | -| M6 | Stage 0 calibration, CGATS import, SceneKit gamut viewer, packaging — shipped on `develop` | -| M7 | Pre-UAT hardening & baseline consolidation — shipped on `develop` | -| M8 | Deduplication/consolidation contracts & UAT-ready hardening (#79–#86) — shipped on `develop` | -| M9 | macOS 12 / Xcode 14.2 retarget — shipped on `develop` (PR #145) | -| M10 | Studio workflow (#146–#149) — in flight on `milestone/m10-studio` | +| M6 | Stage 0 calibration, CGATS import, SceneKit gamut viewer, packaging — shipped | +| M7 | Pre-UAT hardening — shipped | +| M8 | Deduplication contracts & UAT-ready hardening (#79–#86) — shipped | +| M9 | macOS 12 / Xcode 14.2 retarget (PR #145) — shipped | +| 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 | ## What it does The wizard is artefact-gated: -1. **Stage 0** — printer calibration: `printcal` / `applycal` session, `CAL_` basename restore -2. **Stage 1** — `targen` → `.ti1` -3. **Stage 2** — `printtarg` → `.ti2` + TIFF, unmanaged `lp` spool, bound `NSPrintPanel` -4. **Stage 3** — `instlist` + streaming `chartread` (strip / XY / handheld) → `.ti3`, multi-pass average, CIEDE2000 -5. **Stage 4** — `colprof` → `.icc` / `.icm`; optional `applycal`; `iccgamut` next to the profile -6. **Stage 5** — `profcheck`, verification history, ColorSync user/system install +1. **Stage 1** — `targen` → `.ti1` +2. **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 4** — `colprof` → `.icc` / `.icm`; optional `applycal`; `iccgamut` next to the profile +5. **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 -- macOS 14+ -- Xcode 15.4+ with the macOS 14 SDK and Swift 6.0 -- [XcodeGen](https://github.com/yonaskolb/XcodeGen) -- Network once, to fetch Argyll sidecars +To **run** a packaged build: -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** (Homebrew’s 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 @@ -49,7 +79,7 @@ cd iccery-v2-mac git checkout develop 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 ``` @@ -62,9 +92,14 @@ xcodebuild test -scheme ICCery \ 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 # optional @@ -74,9 +109,11 @@ export ARGYLL_RELEASE_TAG=… # default: latest 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 @@ -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 ``` -The script builds with a fixed derived data path (`build/DerivedData`), -locates `Release/ICCery.app` from it, signs the bundle, recursively verifies -every bundled Mach-O sidecar (`scripts/verify-sidecar-signatures.sh`), and -writes `ICCery-${VERSION}-${BUILD_NUM}.dmg` via `dmgbuild`. Sidecars stay -ad-hoc signed inside the bundle — the app is never `codesign --deep`ed. +The script builds with a fixed derived data path (`build/DerivedData`), locates +`Release/ICCery.app` from it, signs the bundle, recursively verifies every +bundled Mach-O sidecar (`scripts/verify-sidecar-signatures.sh`), builds a +HiDPI TIFF from `Resources/dmg-background.png` (+ `@2x`) via `tiffutil`, and +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: @@ -110,30 +156,47 @@ Resources/ assets; Argyll reference files (not the tools) Vendor/Argyll/ fetched sidecars (gitignored) Tests/ICCeryCoreTests/ argv goldens, parsers, stores Tests/ICCeryUITests/ fixture / mock-binary UI tests +scripts/ensure-host-tools.sh scripts/fetch-argyll.sh +scripts/package-release.sh 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 -- **Spawn, never link.** Tools resolve through `BinaryResolver` inside the bundle / `Vendor` tree. `$PATH` is not searched. `ARGYLL_NOT_INTERACTIVE=1` is always set. -- **`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. -- **Argv builders** in ICCeryCore (`TargenArgs`, `PrinttargArgs`, `ChartreadArgs`, `ColprofArgs`, `ApplycalArgs`, `IccgamutArgs`, `ProfcheckArgs`, `LpArgs`, …). 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. SwiftUI `@Observable` for new state. -- **Print.** Unmanaged `lp` with ColorSync suppression (`AP_ColorMatchingMode` / `AP.ColorMatchingMode`). Captured `NSPrintPanel` options win over derived CUPS keys. Never `lp -o raw`. +- **Spawn, never link.** Tools resolve through `BinaryResolver` inside the + bundle / `Vendor` tree. `$PATH` is not searched. `ARGYLL_NOT_INTERACTIVE=1` + is always set. +- **`ProcessManager` actor** owns child lifetime. Streaming tools + (`chartread`, `printtarg`, `colprof`, …) use the event bus; one-shot tools + (`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 ```bash -# full suite (host arch) +# full suite (host arch) — same as CI xcodebuild test -scheme ICCery \ -destination 'platform=macOS' \ 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 # examples @@ -143,9 +206,20 @@ xcodebuild test -scheme ICCery -destination 'platform=macOS' \ -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 @@ -156,11 +230,18 @@ Detected via bundled `instlist`: - SpyderPrint (`p3`) - SpectroScan (`SS`) - 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 -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 | |---|---| @@ -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/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/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/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 | -Agent / branch rules: [`AGENTS.md`](AGENTS.md), [`BUILD-PLAN.md`](BUILD-PLAN.md). - ## Git ``` -develop - └── milestone/m10-studio # M10 integration branch - └── feat/- # one issue per branch +develop # integration; PRs land here unless a milestone branch is announced +main # protected release line (PR from develop) +feat/- +fix/- ``` -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 -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 +- [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/ICCery](https://git.i3omb.com/gronod/ICCery) — v1 Tauri application (spec source, not this tree) -- 2.39.5