7.7 KiB
AGENTS.md: zip-builder
zip-builder turns a Swift package into a release zip of .xcframeworks; Firebase runs it on the
repo's Package.swift with zip_builder.jsonc. Read README.md for usage and
docs/how-it-works.md for the rules it implements.
Map
One executable target, Sources/ZipBuilder, in folders that follow the pipeline:
| Folder | Stage | Main entry point |
|---|---|---|
Commands/ |
CLI, one file per subcommand | ZipBuilderCommand; shared stage steps in SharedOptions.swift |
Config/ |
the config file | Config.swift → Config.load(from:); defaults in Default.swift; UnknownKeys.swift → warnAboutUnknownKeys |
Model/ |
the plan | Plan.swift → Plan; XcodeSettings.swift, Platform.swift |
Planning/ |
1–2: load and plan | LoadPackage.swift → loadPackage; MakePlan.swift → makePlan; DynamicFrameworks.swift → dynamicTargets; HeaderSearchPaths.swift → dependencyIncludeDirectories |
Customizing/ |
3: apply the config | Customize.swift → customize; Merge.swift → aggregate, merge, rename; Folders.swift → arrangingFolders |
Building/ |
4: project and build | MakeProject.swift → makeProject; BuildXCFrameworks.swift → buildXCFrameworks; Linking.swift → linkage check (checkDynamicFrameworksLinkNothingStatic), reachedDependencies; FrameworkCache.swift → fingerprints, FrameworkCache |
Packaging/ |
5: the zip | MakeZip.swift → makeZip; Metadata.swift → metadata |
Inspecting/ |
verify and diff | Verify.swift → verify; Diff.swift → diffDistributions; RenderDiff.swift → renderText, renderMarkdown, renderGitHubComment; Distribution.swift → readDistribution, readXCFrameworks |
Support/ |
shell, logging, files | Shell.swift → shell, runTool; Output.swift → warn |
Tests in Tests/ZipBuilderTests mirror these folders. The fixture package is
Fixtures/MiniPackage; keep it free of .build and Package.resolved.
ci/ holds the steps of .github/workflows/infra.zip_diff.yml in zip_diff.sh, one subcommand
per step. Its diff step writes the pull request comment with diff --format github-comment, and
.github/actions/pr_comment posts it. tool_hash.sh names CI's framework cache directories, for
zip_diff.sh and infra.zip_quickstart.yml. Keep logic there rather than inline in the workflow.
Invariants
- Firebase specifics live in
zip_builder.jsonc, never in code. No Firebase product, target or path names inSources/. If Firebase needs a behavior, add a general config key. - The plan is the contract. Only
makePlanandcustomizemake decisions.makeProjectandbuildXCFrameworksread no config; they carry out the plan.makeZipreads onlyrelease,zipandproducts.base;verifyonlyproducts.baseandverify. - Fingerprint text is byte-stable.
FingerprintTests.goldenFingerprintsmust pass unchanged. Changing the text invalidates every cache entry, CI's included; do it only on purpose, and bumpfingerprintFormatwith it. plan.jsonstays deterministic. Same input, same bytes: sort collections, encode sets sorted, keep paths relative to the package root.- A behavior change needs a test that pins it. Pin the current behavior first, then change it.
- Don't casually change dependencies. Direct ones are pinned to commits in
Package.swift(bump the revision and its version comment, thenswift package resolve);Package.resolvedpins the rest. CI andscripts/zip_builder.shbuild with--force-resolved-versions.
Conventions
- One file per stage or concern, named after its main function. Keep files under ~350 lines; split along a real seam, not by concatenating types.
- Plain functions over the plan. No managers, factories, protocols with one implementation, or forwarding wrappers.
- Natural-language names:
restrictingPlatforms,shippingPlatforms(of:), not abbreviations. - Put why-comments next to the code they explain. Don't narrate what the code does.
- No dense regex: normalize the input, then match it simply.
- Use the dependencies' own models and constants (SwiftPM's
ResolvedModule, XcodeProj'sXcode.Default.*, …) instead of copies or literals. Read the dependency's source when unsure. - Log with
loggerand warn withwarn(), neverprint: standard output is for results. - A config name that doesn't match the plan is a warning (
warnSkipped), not an error, so--productkeeps working. An unknown config key is a warning too (warnAboutUnknownKeys). - Tests use Swift Testing (
@Test,#expect,#require). Check warnings withWarnings.$collected.withValue(…). - New files get the Apache license header. 2-space indentation, lines of at most 100 characters.
- Delete obsolete code and update its callers; don't keep aliases or compatibility shims. The tool has no external API to preserve.
Commands
Run from the repository root.
swift build --package-path scripts/zip_builder --build-tests --force-resolved-versions
swift test --package-path scripts/zip_builder --force-resolved-versions
ZIP_BUILDER_E2E=1 swift test --package-path scripts/zip_builder --force-resolved-versions \
--filter BuildEndToEnd
mint run swiftformat scripts/zip_builder/Sources scripts/zip_builder/Tests
.agents/skills/style_and_commit/scripts/pre_commit.sh
Real-release regression check, for changes to Planning/, Customizing/, Building/ or
Packaging/. Fingerprints don't cover the tool's code, so the second run needs a fresh cache:
VERSION="$(sed -nE 's/^let firebaseVersion = "([^"]+)".*/\1/p' Package.swift)"
git stash # or check out the commit before your change
./scripts/zip_builder.sh release --version "$VERSION" --work-dir /tmp/zb-before
git stash pop
./scripts/zip_builder.sh release --version "$VERSION" --work-dir /tmp/zb-after \
--cache-dir "$(mktemp -d)"
./scripts/zip_builder.sh diff /tmp/zb-before/dist/Firebase /tmp/zb-after/dist/Firebase
An empty diff means the zip didn't change. Explain every difference you didn't intend to make.
Before you finish
- Build with
--build-tests: no new warnings. - All tests pass, and the E2E suite too if you touched
Planning/,Customizing/orBuilding/. - New behavior and new helpers have tests; changed behavior has a test that pins it.
goldenFingerprintspasses unchanged, orfingerprintFormatis bumped on purpose.- Code that can change a built framework without changing its fingerprint is covered by
ci/tool_hash.sh(todayBuilding/, exceptFrameworkCache.swiftandLayout.swift, andModel/), so CI doesn't reuse stale cache entries. - Changed files are formatted with swiftformat.
- Config changes (keys, defaults, meaning) are in
docs/configuration.md. - Rule changes (what the tool derives, or how) are in
docs/how-it-works.md. - CLI changes (commands, flags, options) are in
README.md, including any affected common tasks. - Added, moved or renamed files are in the Map above.
Fixtures/MiniPackagehas no.buildorPackage.resolved.
Don'ts
- Don't add Firebase names or Firebase-only behavior to
Sources/. - Don't make a decision outside planning and
customize, or read config inBuilding/. - Don't change fingerprint text by accident, e.g. by reordering or renaming fields it hashes.
- Don't add heuristics: public headers come from SwiftPM's include directory, wrappers from the code-free rule, and everything else from explicit config.
- Don't
printdiagnostics, and don't make an unknown config name fatal. - Don't update dependencies or
Package.resolvedas a side effect of another change. - Don't commit build output (
build/,.build/).