Files
iccery-v2-mac/AGENTS.md
gronod ecbf46723b
macOS CI / build-and-test (push) Skipped
feat(print): #218 Stage 2 paper-source selection + orientation dropdown
- Detect vendor tray keys in CupsParsers with InputSlot/MediaSource/EPIJ_FdSo/EPIJ_PSrc/CNIJMediaSupply roster; EPIJ_FdSo wins over EPIJ_PSrc.
- PrinterTray now carries raw token + display name; PrinterCapabilities exposes trayKey, trayDefault and tray-specific quality maps.
- CupsService enumerates trays from the detected key, enriches labels from PPD, seeds selection from CUPS default, and precomputes per-tray media→quality constraints.
- MediaQualityConstraints resolves quality availability from both selected media and selected tray, falling back to the media-only map.
- TicketWriteResolver emits the selected tray key=token in the locked write order between PageSize and media key.
- PrintPanelService applies the Stage 2 tray token to PMPrintSettings on open and captures the tray back from the full roster plus AP_D_InputSlot.
- PrintSessionViewModel keeps selectedTrayToken, applies capture-back, preserves unknown captured tokens as synthetic trays, and passes tray into spool overrides.
- Stage2View replaces portrait/landscape buttons with an orientation dropdown (printerOrientationSelect) while preserving portrait/landscape semantics and orientation-requested=3|4.
- Add/update core, UI and fixture tests for tray detection, enumeration, PPD labels, defaults, unknown-token synthesis, panel capture, ticket write order and tray-aware quality clamping.
- Update docs/10-print-system.md, docs/11-print-macos.md, docs/21-ui-reference.md and docs/24-issues-invariants.md.

Verified on the macOS 12.7.6 x86_64 CI runner: xcodebuild build-for-testing and ICCeryCoreTests pass (557 tests, 0 failures).
2026-09-22 19:19:33 +01:00

8.9 KiB
Raw Permalink Blame History

AGENTS.md — ICCery v2 Mac

Product

Native macOS printer ICC/ICM profiling frontend. Drives the Gronod ArgyllCMS 3.5.0 fork as AGPL-isolated subprocesses. Spec snapshot lives in docs/ (chapters 01–25). Ticket plan: BUILD-PLAN.md.

Stack

  • SwiftUI (@Observable, @MainActor view models) + AppKit for printing / panels / file dialogs.
  • Minimum macOS 14.0. Universal arm64 + x86_64.
  • Bundle id com.gronod.iccery2. Product name ICCery.
  • App Sandbox OFF. Hardened Runtime ON. Entitlements in ICCery.entitlements.
  • No Tauri, no Rust host, no WKWebView, no Three.js.

Package layout

  • ICCery — app target (SwiftUI shell). Also owns the native print stack in Sources/ICCery/Print/: PMTicketBridge, PrintTicket, TicketWriteResolver, NativeTargetSpooler (+ RecordingTargetSpooler), TargetRaster, TargetPageCanvasView (#201 D1 — AppKit/NSPrintOperation lives here, never in ICCeryCore).
  • ICCeryCore — wizard state, ProcessManager, argv builders, settings, CGATS, ΔE₀₀ (no AppKit print panel; CUPS enumeration/parsers only).
  • ICCeryPrintKit — v2.1 only (issue 16). Zero deps on wizard types.

AGPL boundary

Never link Argyll. Spawn only.

  • Streaming: ProcessManager actor (targen, printtarg, chartread, average, colprof, profcheck, iccgamut, instlist).
  • Captured: runCaptured (printcal, applycal only). Both paths set ARGYLL_NOT_INTERACTIVE=1. Never search $PATH for binaries.

Concurrency

No blocking subprocess I/O on @MainActor. Do not hop to main per stdout line (colprof emits thousands of .). Stdin handle is independent of wait (#84). Process ids are exclusive leases (#116). killAll on NSApplication.willTerminate and last-window close (#147, #149). XY cancel: send q\n, wait ~500 ms, then kill.

Argyll flag discipline

See docs/25-rewrite-notes.md and docs/04-argyll-binaries.md §15. -d / -r / -R / -u / -Y / -c mean different things per tool. v2.0 -u policy: printtarg + chartread + profcheck only.

Files

Artefact gating on disk. No placeholder basenames (#60). Empty cwd illegal (#59). Atomic writes = .tmp + rename (#213). User-supplied strings via SwiftUI Text only (#114). TIFF never rendered directly — host-side PNG preview (#58).

Print spool — native since v2.0 (#201); v1 lp path eradicated

Target printing is a headless NSPrintOperation via NativeTargetSpooler (#201): restore the captured PrintTicket, apply TicketWriteResolver (Stage 2 always wins, D6), draw 1:1 with interpolation off. lp is eradicated from the target-print path (historical v1: LpArgs, CupsService.printTarget, ICCERY_TEST_LP_ARGV all deleted). CupsParsers/CupsOptionsFilter stay (D4): enumeration, capabilities, media/quality/bypass key detection and the Stage 2 mirror. UI-test seam: ICCERY_TEST_SPOOL_LOG — DEBUG RecordingTargetSpooler appends one resolved-ticket line per page (D8).

Versioning

scripts/version.sh is the single source: tag/describe → ICCERY_RELEASE_TAG (About shows tag (marketing)), MARKETING_VERSION = strict X.Y.Z, CURRENT_PROJECT_VERSION = git rev-list --count HEAD (#189). v* tag builds hard-fail if tag's X.Y.Z ≠ project.yml MARKETING_VERSION — bump project.yml on develop before tagging. CI needs fetch-depth: 0.

Branching

develop ← milestone/mN-<name> ← feat/<issue#>-<slug>. PRs via Gitea MCP. Every issue/PR: Project/ICCery-v2 + Feature/* or Bug/* + Priority/*.

Issue ticket style

  • Title: [Kind/Priority] short description — e.g. [Bug/Critical] …, [Feature/Medium] ….
  • Labels: Kind/Bug or Kind/Feature (also Kind/Testing for test work), one Bug/<area> or Feature/<area> (Architecture/Backend/UI/DevOps), one Priority/*, plus Project/ICCery-v2. Set the milestone when the work belongs to an active mN milestone.
  • Bug bodies: ## Summary → ## Root Cause Analysis (file:line evidence; note checked-and-dismissed hypotheses) → ## Proposed Fix (options or deterministic plan) → ## Acceptance Criteria (checkbox list) → ## Dependencies → ## References.
  • Feature bodies: same skeleton minus Root Cause; lead with Summary and a concrete implementation plan.
  • Dependencies/blockers must always be recorded via the gitea MCP issue_write methods (add_dependency, block_issue; reads via issue_read list_dependencies / list_blocks — see "Gitea issue dependencies"), not just mentioned in the body. This is mandatory when issues share a milestone with an implementation order: wire up add_dependency (blocked-by) and block_issue (blocks) links so the order is machine-readable. The ## Dependencies body section may still summarise them for readability, but the MCP links are authoritative.

Verify

xcodebuild test -scheme ICCery -destination 'platform=macOS' ARCHS="$(uname -m)"
codesign -dvv <sidecar>

Universal (ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO) is still required for release verification / packaging.

Remote test builds (runner host)

Policy: all builds and test runs that can execute on the remote host should execute there. Use localadmin@192.168.0.172 whenever the Gitea macos_12 runner is not actively executing a workflow job.

Test builds done as part of change implementations run on the CI runner host, not the dev machine — the runner is a macOS 12.7.6 x86_64 VM with Xcode 14.2, and CI failures there are often environmental and do not reproduce locally.

  • Host: localadmin@192.168.0.172 (SSH, LAN). Runner daemon: gitea-runner daemon -c /opt/gitea-runner/config.yaml.
  • Credentials: ~/Projects/build-host.env defines BUILD_HOST_USER / BUILD_HOST_PASSWORD. Never read or print this file — load it with set -a; source ~/Projects/build-host.env; set +a so values stay in the environment.
  • sshpass is installed (/usr/local/bin/sshpass). Feed the password via the SSHPASS env var — never on the command line:
    SSHPASS="$BUILD_HOST_PASSWORD" sshpass -e ssh localadmin@192.168.0.172 '<cmd>'
    SSHPASS="$BUILD_HOST_PASSWORD" sshpass -e scp <local> localadmin@192.168.0.172:<remote>
    
    First connection needs -o StrictHostKeyChecking=accept-new.
  • Remote checkout: ~/Projects/iccery-v2-mac (anonymous HTTPS clone works; full history needed by scripts/version.sh). Before testing, sync it to the same base commit as the local work: git fetch origin && git checkout <local base sha>, then scp each locally-changed file over its remote counterpart (uncommitted work transfers this way — there is no push).
  • xcodegen is not on PATH: prepend $HOME/.local/xcodegen/2.38.0/xcodegen/bin (prebuilt 2.38.0 installed by scripts/ensure-host-tools.sh; brew's formula needs Xcode 15.3, impossible on macOS 12 — #109).
  • Mirror .gitea/workflows/macos.yml exactly, from the repo root with DERIVED=build/DerivedData-test: scripts/ensure-host-tools.sh → xcodegen generate --spec project.yml → eval "$(scripts/version.sh)" → xcodebuild build-for-testing -scheme ICCery -destination 'platform=macOS' -derivedDataPath "$DERIVED" -configuration Debug ARCHS="$(uname -m)" ONLY_ACTIVE_ARCH=NO CODE_SIGNING_ALLOWED=YES CODE_SIGN_IDENTITY='-' MARKETING_VERSION=… CURRENT_PROJECT_VERSION=… ICCERY_RELEASE_TAG=… → codesign every *_PackageProduct.framework under $DERIVED/Build/Products/Debug (#119) → xcodebuild test-without-building -xctestrun <ICCery*.xctestrun> -only-testing:ICCeryCoreTests -destination 'platform=macOS' -derivedDataPath "$DERIVED".
  • Run unit tests only (ICCeryCoreTests). UI tests need a logged-in GUI session and fail environmentally on the VM (runs 29700, 29804, 42183) — do not use them for verification unless asked.
  • xcodebuild takes minutes — run the ssh command in a background shell and poll rather than blocking.

Private ColorSync SPI

2-arg (PMPrintSession, CFStringRef) -> OSStatus. Never pass integer 1. Modes: AP_ApplicationColorMatching then ApplicationColorMatching. One spool path remains (#201 D2): write both vocabularies on the native path — locked AP_* (AP_ColorMatchingMode + AP.ColorMatchingMode = AP_ApplicationColorMatching) and the Quartz dictionary (PMColorMatchingMode=APCustomColorMatching, PMCustomColorMatchingProfile="", legacy com.apple.print.PrintSettings.PMColorMatchingMode, nested com.apple.print.printSettings mirror).

Gitea issue dependencies

Use the gitea MCP (custom build with blocking support — verified working):

  • issue_write methods:
    • add_dependency — blocking_issue blocks issue_number.
    • remove_dependency — removes blocking_issue from issue_number's blockers.
    • block_issue / unblock_issue — issue_number blocks/unblocks blocked_issue.
  • issue_read methods: list_dependencies (issues blocking N), list_blocks (issues N blocks).
  • All issue numbers are display numbers, not db ids.