Task — engineering-spec@1

"MAS updater exclusion — `mas` cargo feature compiles the self-updater out of the Mac App Store build"

doneTASK-IMP-075
module improvement · class product · priority p0 · created 2026-07-13 · shipped 2026-07-13
depends on none · blocks none

§1 — Description

  1. apps/desktop/src-tauri/Cargo.toml MUST declare a mas feature (marker, no deps). Default builds (no feature) MUST be byte-for-byte behavior-identical to today: updater registered, launch check runs.
  2. All three updater code sites in lib.rs (spawn_update_check definition, the tauri_plugin_updater registration block, the setup-time launch call) MUST change #[cfg(desktop)] to #[cfg(all(desktop, not(feature = "mas")))] — with --features mas the plugin is never registered and no update check is compiled in, so the configured plugins.updater block in tauri.conf.json becomes inert for the MAS target without touching that shared config (TASK-APP-003 §1 #1 forbids mutating it).
  3. release-mas.yml's build step MUST pass the feature (--features mas). If the Tauri CLI's cargo-feature passthrough flag differs on the pinned CLI major, the job fails loudly at build - an acceptable first-gated-run discovery, noted in-file (no macOS/cargo toolchain exists in this authoring environment to pre-verify).
  4. docs/deploy/mac-app-store-submission.md hard blocker #1 MUST flip to resolved-pending-first-build, with one honest residual noted: the target-scoped dependency still compiles into the binary as dead code (registration excluded); shrinking it to an optional dependency is possible later on a real toolchain but is not submission-blocking the way active self-update was.

Length note: sanctioned lean profile — a two-attribute Rust change plus one CI flag; §5's checks are the complete machine surface available pre-toolchain.

§2 — Why this design

Compile-time exclusion is the only path TASK-APP-003's answer sheet accepts: runtime checks and config-overlay null-outs both leave the self-update code reachable and were explicitly rejected there as unverifiable submission-safety. A marker feature + not(feature) widening is the minimal diff that keeps the default build provably unchanged (the attribute is strictly narrower only when the feature is on).

§3 — API contract

[features]
custom-protocol = ["tauri/custom-protocol"]
mas = []   # Mac App Store target: compiles the self-updater OUT (TASK-IMP-075)
#[cfg(all(desktop, not(feature = "mas")))]   // x3, replacing #[cfg(desktop)]
run: npx --yes @tauri-apps/cli@2 build --config src-tauri/tauri.mas.conf.json --bundles app --target universal-apple-darwin --features mas

§4 — Acceptance criteria

  1. Default-build neutrality: grep -c '#\[cfg(all(desktop, not(feature = "mas")))\]' src/lib.rs == 3 and grep -c '#\[cfg(desktop)\]' src/lib.rs == 0 — no site left half-gated.
  2. mas = [] present in Cargo.toml [features].
  3. release-mas.yml build step carries --features mas; YAML parses.
  4. Answer sheet blocker #1 updated; residual dead-code note present.
  5. First real toolchain run (cargo check then cargo check --features mas, later the gated CI build) compiles both ways — expected-pending here, executed on Stephen's machine or the first MAS_RELEASE run.

§5 — Verification

grep -c 'not(feature = "mas")' apps/desktop/src-tauri/src/lib.rs        # 3
grep -c '#\[cfg(desktop)\]' apps/desktop/src-tauri/src/lib.rs           # 0
grep -n '^mas = \[\]' apps/desktop/src-tauri/Cargo.toml
grep -n 'features mas' .github/workflows/release-mas.yml
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/release-mas.yml'))"
# pending real toolchain: cargo check && cargo check --features mas

§6 — Implementation skeleton

§3 is exhaustive.

§7 — Dependencies

Upstream: TASK-APP-003 (done) discovered and scoped this. Downstream: unblocks MAS_RELEASE=true (remaining blockers are Stephen's account-side items). Batched with TASK-IMP-074 (cone-independent).

§8 — Example payloads

Post-change gate output: mas-entitlement-lint: OK unchanged; MAS build log shows no cyberos updater: lines.

§9 — Open questions

§10 — Failure modes inventory

FailureDetectionOutcomeRecovery
cfg typo breaks the DEFAULT buildnext desktop build/tag fails compile loudlyno silent shiptrivial attribute fix
CLI drops/renames --featuresMAS job fails at build steploud, pre-submissionadjust flag per CLI --help
feature on but a 4th updater site appears latergrep AC #1 in this spec + review normgate catches count driftwiden the new site
Apple flags dead plugin codereview feedback (external)resubmit cycleoptional-dep follow-up (§9)
default build accidentally passes --features masonly possible in release-mas.yml, which never builds the Developer ID channelnonestructural separation of workflows

§11 — Implementation notes

The three-site count (not two, not four) comes from reading lib.rs, not convention — AC #1's grep pins it. Batched with TASK-IMP-074 under v2.5.0 §11a.

End of TASK-IMP-075.

Audit

§1 — Verdict summary

Lean profile, justified (two-attribute Rust change + one CI flag). 4 clauses, 5 ACs, 5 failure rows; toolchain-dependent checks honestly marked expected-pending.

§2 — Findings (resolved in-pass)

  • ISS-001: draft asserted "3 cfg sites" without a pinning mechanism → AC #1 dual-grep (count==3 AND zero bare #[cfg(desktop)] remain) makes the count machine-checked, not prose.
  • ISS-002: dead-code residual (dependency still compiled) was unstated → clause 4 + §9/§10 rows disclose it and scope the optional-dep shrink as a later, non-blocking follow-up.

§3 — Resolution

All resolved same pass. Score = 10/10. End of TASK-IMP-075 audit.