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.
End-user guide: the repository 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, spawned
as AGPLv3 child processes. The GUI never dlopens 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 |
| 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; bundled Argyll sidecars remain AGPLv3 |
What it does
The wizard is artefact-gated:
- Stage 1 —
targen→.ti1 - Stage 2 —
printtarg→.ti2+ TIFF, unmanagedlpspool, boundNSPrintPanel - Stage 3 —
instlist+ streamingchartread(strip / XY / handheld) →.ti3, multi-pass average, CIEDE2000 - Stage 4 —
colprof→.icc/.icm; optionalapplycal;iccgamutnext to the profile - Stage 5 —
profcheck, verification history, ColorSync user/system install
Plus:
- Calibrate Printer — optional
printcal/applycalsession under aCAL_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 —
.icceryprojbookmark 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
.dmgpackaging 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
To run a packaged build:
- 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 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.7inbuild/.venv-dmgbuild(INSTALL_DMGBUILD=1 scripts/ensure-host-tools.sh)
App Sandbox is off. Hardened Runtime is on. Entitlements live in
ICCery.entitlements.
Build
git clone https://git.i3omb.com/gronod/iccery-v2-mac.git
cd iccery-v2-mac
git checkout develop
make fetch-argyll # Vendor/Argyll/macos-universal/, ad-hoc signed
make test # xcodegen + xcodebuild build test (host arch)
make universal # ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO
Equivalent without Make:
xcodegen generate
xcodebuild test -scheme ICCery \
-destination 'platform=macOS' \
ARCHS="$(uname -m)"
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.
# optional
export ARGYLL_SERVER_URL=https://git.i3omb.com
export ARGYLL_REPO=gronod/argyllcms
export ARGYLL_RELEASE_TAG=… # default: latest
export GITEA_TOKEN=… # private releases
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.
Release packaging
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), 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 --deeped.
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:
| Variable | Purpose |
|---|---|
GITEA_TOKEN |
private gronod/argyllcms release downloads |
ARGYLL_SERVER_URL / ARGYLL_REPO / ARGYLL_RELEASE_TAG |
sidecar release override |
CODESIGN_IDENTITY |
Developer ID identity for the outer .app; unset or - = ad-hoc |
DEVELOPMENT_TEAM |
team ID passed to xcodebuild when signing |
NOTARIZE_APPLE_ID / NOTARIZE_PASSWORD / APPLE_TEAM_ID |
notarytool + staple when all three are set |
Layout
Sources/ICCery/ SwiftUI + AppKit shell, stage views, workflow VMs
Packages/ICCeryCore/ wizard state, ProcessManager, argv builders,
settings, CGATS, ΔE₀₀ — no NSPrintPanel
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.
Architecture
- Spawn, never link. Tools resolve through
BinaryResolverinside the bundle /Vendortree.$PATHis not searched.ARGYLL_NOT_INTERACTIVE=1is always set. ProcessManageractor owns child lifetime. Streaming tools (chartread,printtarg,colprof, …) use the event bus; one-shot tools (printcal,applycal, CUPS) userunCaptured. ExclusiveProcessIDleases. Quit path:q\n, ~500 ms, kill;killAllon terminate.- Argv builders in ICCeryCore (
TargenArgs,PrinttargArgs,ChartreadArgs,ColprofArgs,ApplycalArgs,IccgamutArgs,ProfcheckArgs,LpArgs,SpotReadArgs, …). UI must not concatenate flags. - Atomic artefacts. Writes go to
*.tmpthenreplaceItemAt.applycalmust 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
lpwith ColorSync suppression (AP_ColorMatchingMode/AP.ColorMatchingMode). CapturedNSPrintPaneloptions win over derived CUPS keys. Neverlp -o raw. - SwiftUI ViewBuilder. Xcode 14.2 / Swift 5.7 still has the ten-child
limit. Split large
VStack/Grouptrees (#146).
Tests
# full suite (host arch) — same as CI
xcodebuild test -scheme ICCery \
-destination 'platform=macOS' \
ARCHS="$(uname -m)"
# fat compile-check (not the default test path):
# ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO
# examples
xcodebuild test -scheme ICCery -destination 'platform=macOS' \
-only-testing:ICCeryCoreTests/ChartreadClassifierTests
xcodebuild test -scheme ICCery -destination 'platform=macOS' \
-only-testing:ICCeryUITests/Milestone5UITests
CI (.gitea/workflows/macos.yml on Gitea, .github/workflows/macos.yml on
GitHub) 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.
The GitHub file is the same pipeline on macos-14 (github.com retired
macos-12), actions/upload-artifact@v4, and gh release upload for tag
DMGs.
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
Detected via bundled instlist:
- i1 Pro / i1 Pro 2 (
i1) - ColorMunki (
CM) - SpyderPrint (
p3) - SpectroScan (
SS) - DTP20 / 22 / 41 / 51
- XY tables (SpectroScan, i1iO) when the
instlistname matches/spectro\s?scan|i1io/i
Docs
| Where | Audience |
|---|---|
| Wiki | End users — screens, workflow, troubleshooting |
docs/ |
Functional spec (normative for implementers) |
AGENTS.md, BUILD-PLAN.md |
Agent / branch rules |
Implementation order in docs/:
| Doc | Topic |
|---|---|
docs/01-overview.md |
Product and wizard |
docs/03-ipc-and-process-manager.md |
Spawn / stdin / kill |
docs/04-argyll-binaries.md |
CLI argv |
docs/06-wizard-and-artefacts.md |
Gating |
docs/23-assets.md |
Icons, DMG chrome |
docs/24-issues-invariants.md |
Bugs that must not return |
docs/26-v2-mac-ticket-plan.md |
Gitea tickets |
docs/PREUAT.md |
Pre-UAT tester kit |
Git
develop # integration; PRs land here unless a milestone branch is announced
main # protected release line (PR from develop)
feat/<issue>-<slug>
fix/<issue>-<slug>
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. 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.
Related
- User wiki
- gronod/argyllcms — Argyll 3.5.0 fork (
-uJSON,instlist) - gronod/ICCery — v1 Tauri application (spec source, not this tree)