Skip to content

Implement GutenbergKitResources binary dependency build#318

Closed
mokagio wants to merge 57 commits intotrunkfrom
ainfra-1967-implement-gutenbergkitresources-binary-dependency-build
Closed

Implement GutenbergKitResources binary dependency build#318
mokagio wants to merge 57 commits intotrunkfrom
ainfra-1967-implement-gutenbergkitresources-binary-dependency-build

Conversation

@mokagio
Copy link
Contributor

@mokagio mokagio commented Feb 11, 2026

Note

Closed in favor of #376

What?

Adds infrastructure to build and distribute GutenbergKitResources as a pre-built XCFramework, decoupling the JS build from Swift consumers.

Based on the research work done on #315 .

Why?

Currently, every Swift consumer must run the full JS build to get Gutenberg editor assets.
With a binary XCFramework for resources, consumers can pull a pre-built artifact from CDN instead, significantly reducing build times and simplifying integration.

How?

  • New GutenbergKitResources SPM target wrapping bundled editor assets (HTML, CSS, JS)
  • Package.swift uses Context.environment to switch between local source and CDN binary target
  • build_xcframework.sh assembles a proper .framework from SPM archive DerivedData (binary, swiftmodule, modulemap, headers, resource bundles) since SPM archives don't produce installable frameworks
  • Fastlane release lane handles versioning, GitHub release, and S3 upload
  • CI pipeline updated with XCFramework build step and artifact publishing
  • GutenbergKit target depends on GutenbergKitResources instead of bundling assets directly
  • All package access modifiers migrated to internal (required by packageAccess: false)
  • Tracked JS build artifacts removed from Git

Testing Instructions

  1. make build — builds web assets and copies to all platform directories
  2. make build-swift-package — builds the Swift package with local resources
  3. make build-resources-xcframework — produces GutenbergKitResources-<sha>.xcframework.zip
  4. Verify the XCFramework contains both ios-arm64 and ios-arm64_x86_64-simulator slices with the binary, Modules, Headers, and resource bundle

Generated with the help of Claude Opus 4.6

@mokagio mokagio added the [Type] Task Issues or PRs that have been broken down into an individual action to take label Feb 11, 2026
@mokagio mokagio force-pushed the ainfra-1967-implement-gutenbergkitresources-binary-dependency-build branch from d39e34e to e53dfea Compare February 12, 2026 01:35
/// - ``always``: Always returns `true` - cached responses are always used.
///
package func allowsResponseWith(date: Date, currentDate: Date = .now) -> Bool {
func allowsResponseWith(date: Date, currentDate: Date = .now) -> Bool {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SwiftPM required packageAccess: false on the GutenbergKit target for binary target compatibility.

With that flag off, the compiler no longer passes -package-name, so the package access modifier silently degrades to fileprivate breaking visibility across files within the module.

Comment on lines -55 to 57
let gutenbergCSS = Self.loadGutenbergCSS() ?? ""
let gutenbergCSS = GutenbergKitResources.loadGutenbergCSS() ?? ""
assert(!gutenbergCSS.isEmpty, "Failed to load Gutenberg CSS from bundle. Previews will not render correctly.")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might be more robust ways to handle a path error internally, but I consider them out of scope for the moment.

.gitignore Outdated
Comment on lines +192 to +193
/ios/Sources/GutenbergKit/Gutenberg/assets
/ios/Sources/GutenbergKit/Gutenberg/index.html
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should not actually be generated anymore. We can keep them here for a while to avoid issues while local copies update, or get rid of them already to avoid carrying dead weight. What do you think?

Suggested change
/ios/Sources/GutenbergKit/Gutenberg/assets
/ios/Sources/GutenbergKit/Gutenberg/index.html

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we remove them. Developers can clean the files as needed.

@@ -0,0 +1,152 @@
#!/usr/bin/env bash
Copy link
Contributor Author

@mokagio mokagio Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It took me a while to get this to run in #315 and it's based on a setup that had it in the root and I wasn't game to move it just yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mokagio mokagio force-pushed the ainfra-1967-implement-gutenbergkitresources-binary-dependency-build branch from 9e7a477 to 76639c8 Compare February 19, 2026 07:19
@mokagio mokagio force-pushed the ainfra-1967-implement-gutenbergkitresources-binary-dependency-build branch 2 times, most recently from d1deddd to 60a14ac Compare March 4, 2026 01:25
@mokagio mokagio requested a review from dcalhoun March 4, 2026 10:51
@mokagio
Copy link
Contributor Author

mokagio commented Mar 4, 2026

@dcalhoun I can't tell if the E2E tests are due to my changes or not. I noticed you recently worked on an E2E fix, which I rebased this on top of. Maybe with your extra context you can let me know? Thanks!

@dcalhoun
Copy link
Member

dcalhoun commented Mar 4, 2026

@dcalhoun I can't tell if the E2E tests are due to my changes or not. I noticed you recently worked on an E2E fix, which I rebased this on top of. Maybe with your extra context you can let me know? Thanks!

Thanks for the continued efforts on this, @mokagio! 🙇🏻‍♂️

It seems the CI failures are likely related to the changes in this pull request. When running the iOS UI tests locally in Xcode, I see errors related to the GutenbergKitResources module. The line of the error changes based on whether one is running the tests against a production build (make build) or development server (make dev-server with a GUTENBERG_EDITOR_URL environment variable set in Xcode). See the details below.

Dev server error

GutenbergKitResources/resource_bundle_accessor.swift:44: Fatal error: unable to find bundle named GutenbergKit_GutenbergKitResources

I was able to circumvent the error with the following diff.

diff --git a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift
index 1993d818..828d2519 100644
--- a/ios/Sources/GutenbergKit/Sources/EditorViewController.swift
+++ b/ios/Sources/GutenbergKit/Sources/EditorViewController.swift
@@ -465,7 +465,7 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro
                 onSelection: { [weak self] in self?.didSelectBlockInserterItem($0) },
                 onClose: { [weak self] in self?.notifyInserterClosed() }
             )
-            .environmentObject(htmlPreviewManager)
+//            .environmentObject(htmlPreviewManager)
         })
 
         context.viewController = host

error-dev-server

Production build error

GutenbergKitResources/resource_bundle_accessor.swift:44: Fatal error: unable to find bundle named GutenbergKit_GutenbergKitResources

I was unable to identify a quick way to circumvent the error.

error-production-build

@mokagio
Copy link
Contributor Author

mokagio commented Mar 4, 2026

Thanks for the hints @dcalhoun . I'll see where they lead.

@mokagio mokagio force-pushed the ainfra-1967-implement-gutenbergkitresources-binary-dependency-build branch from 39a064c to 74af0b9 Compare March 5, 2026 04:14
Copy link
Member

@dcalhoun dcalhoun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great, @mokagio. Very excited to see this progress. Thank you!

I left a few inline comments that are mostly to help me better understand the functionality and underlying assumptions.

@curl -sX POST "https://api.buildkite.com/v2/organizations/automattic/pipelines/gutenbergkit/builds" \
-H "Authorization: Bearer $(BUILDKITE_API_ACCESS_TOKEN)" \
-H "Content-Type: application/json" \
-d '{"commit":"HEAD","branch":"ainfra-1967-implement-gutenbergkitresources-binary-dependency-build","message":"Release $(NEW_VERSION)","env":{"NEW_VERSION":"$(NEW_VERSION)"}}' \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting that we should update the branch before merging.


- label: ':swift: Test Swift Package'
command: swift test
command: make test-swift-package REFRESH_L10N=1 REFRESH_JS_BUILD=1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After #320, we now cache the JavaScript build as an artifact to improve speed and mitigate 429 errors for translation downloads. Can we reuse that artifact for this job?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

install_gems

echo "--- :xcode: Building XCFramework"
make build-resources-xcframework REFRESH_L10N=1 REFRESH_JS_BUILD=1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to my comment on the Buildkite configuration, should/can we reuse the JavaScript build artifact?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Struggling to understand how these XCFramework releases coincide/integrate with GutenbergKit releases.

The existing release script for the latter bumps the version for inclusion in the JavaScript, so that accurate version numbers are reported in exceptions. Should the XCFramework and GutenbergKit versions align? Maybe that is only required for tagged GutenbergKit releases, and XCFramework releases for Git commits can simply use the last tagged version string?

I had Claude capture the issue and potential solution in its own words below as well. I'm remain uncertain as to the current state, plan, or appropriate solution.

WDYT?

Additionally, the JS assets baked into the XCFramework will carry whatever version is in package.json at the time of the build — currently 0.14.0-alpha.0 on trunk. This version propagates to crash report tags (gutenberg_kit_version in exception-parser.js) and the generated GutenbergKitVersion.swift/.kt files (used in User-Agent strings).

Since package.json is never bumped to $version before the build runs, the XCFramework artifact will report a stale version at runtime even though the git tag and S3 path use the correct release version.

I think this needs an npm --no-git-tag-version version "$version" before the make call, so the version flows through the same package.jsongenerate-version.js → Vite build pipeline that the local release script (bin/release.sh) already uses.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this @dcalhoun . To be honest, I'm still unsure about how to proceed, too. So far, I've just tried to mimic the wordpress-rs behavior to have CI track the checksum and revision in the Package.swift upon "releasing".

You bring up a good point regarding the version bump. It's frustrating that there's no way to do a version bump and Package.swift update atomically.

I'll have to think about this...

File.open(file_path, 'w') { |file| file.print lines.join }
end

lane :publish_release_to_github do |options|
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What, if any, changes should we make the project's existing bin/release.sh script? Does it remain valid for a different purpose? Should we update it to align with App Infra best practices/tooling?

This may be out of scope for this PR, but it felt worthy of discussing here nonetheless.

.gitignore Outdated
Comment on lines +192 to +193
/ios/Sources/GutenbergKit/Gutenberg/assets
/ios/Sources/GutenbergKit/Gutenberg/index.html
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we remove them. Developers can clean the files as needed.


.PHONY: release
release: ## Create and publish a new release
release: ## Create and publish a new release (local)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure of the (local) designation. It's local in the sense that this script was intended to be run a local development machine, but it does publish tags and GitHub Releases—releases intended to be downloaded and consumed by a host app.

Should release and release-on-ci be labeled as GutenbergKit and GutenbergKitXCFramework respectively? My perception is that they "release" two, disparate things. Is that accurate or am I misunderstanding the state of things?

Do we plan to use both targets in tandem when publishing GutenbergKit release? I suppose this relates to my other comment about coordinating/composing release version bumps/strings.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Transparently, I'm less familiar with the inner workings of Swift/Xcode builds. That said, this script makes sense and looks appropriate to me.

mokagio and others added 9 commits March 12, 2026 11:12
Introduces a new `GutenbergKitResources` target to host
the built Gutenberg web assets as a separate module.

`Package.swift` uses `GUTENBERGKIT_SWIFT_USE_LOCAL_RESOURCES`
env var to switch between a local source target (development)
and a pre-built XCFramework binary target (releases).

Sets `packageAccess: false` on `GutenbergKit` — required
for binary target compatibility within the same package.

---

Generate with the help of Claude Code, https://code.claude.com

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Required by `packageAccess: false` on the GutenbergKit
target, which enables binary target compatibility for
GutenbergKitResources within the same package.

---

Generate with the help of Claude Code, https://code.claude.com

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace `Bundle.module` lookups in `EditorViewController`
and `HTMLPreviewManager` with the new module's API.

---

Generate with the help of Claude Code, https://code.claude.com

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Resources are now served by GutenbergKitResources.
The `Gutenberg/` directory is excluded from the target
and both build output directories are gitignored.

---

Generate with the help of Claude Code, https://code.claude.com

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The script archives GutenbergKitResources for device and
simulator, creates an XCFramework, and outputs a zip with
checksum for SPM consumption.

Makefile changes:

- Set GUTENBERGKIT_SWIFT_USE_LOCAL_RESOURCES for all targets
- Copy dist output to GutenbergKitResources/Resources/
- Add build-resources-xcframework target

---

Generate with the help of Claude Code, https://code.claude.com

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Swift tests now build resources locally first
- Add XCFramework build step with artifact upload

---

Generate with the help of Claude Code, https://code.claude.com

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Modeled on wordpress-rs. The `release` lane:

1. Validates the version doesn't already exist
2. Updates Package.swift version and checksum
3. Tags, pushes, and creates a GitHub release
4. Uploads XCFramework zip to S3

---

Generate with the help of Claude Code, https://code.claude.com

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
These ~58 files are now generated during the build and
distributed via the GutenbergKitResources XCFramework.

---

Generate with the help of Claude Code, https://code.claude.com

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
mokagio and others added 24 commits March 12, 2026 11:12
A directory named `Resources` inside a flat `.bundle`
confuses `codesign`: it can't distinguish the iOS flat
layout from the macOS deep layout, failing with
"bundle format unrecognized, invalid, or unsuitable".

Renaming to `Gutenberg` avoids the reserved name.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
The build script names the zip with the git SHA
(`GutenbergKitResources-{sha}.xcframework.zip`), but
`Package.swift` expects the stable name
`GutenbergKitResources.xcframework.zip`.
Use the stable name as the S3 key so SPM can resolve it.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
Ruby's `||` doesn't catch empty strings, so
`version:` with no value silently passes through,
producing S3 keys without a version segment.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
Inline `$$` escaping in pipeline YAML failed to
pass `BUILDKITE_TAG` through to the shell.
A standalone script with `set -euo pipefail` avoids
the double-interpolation issue entirely.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
Without it, iOS refuses to install the app:
"missing or invalid CFBundleExecutable in its
Info.plist".

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
SPM produces a `.o` object file for library targets.
Copying it directly as the framework binary caused Xcode
to statically link the code into the consuming app.
This put the auto-generated `BundleFinder` class in the
app binary, so `Bundle(for: BundleFinder.self)` resolved
to the main bundle instead of the framework bundle—missing
the resource bundle nested inside the framework directory.

Linking the `.o` into a proper dylib keeps `BundleFinder`
in the framework at runtime, fixing the crash:
`unable to find bundle named GutenbergKit_GutenbergKitResources`

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
The `test-ios-e2e` target copied build output into
`GutenbergKit/Gutenberg/` but not into the new
`GutenbergKitResources/Gutenberg/` directory, which is
where the editor now loads resources from.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
The `GutenbergKit` target now excludes `Gutenberg/` in
`Package.swift`—all web assets are served from the
`GutenbergKitResources` target instead.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
Separate the iOS and Android dist copy steps into
dedicated Makefile targets so they can be run or
debugged independently.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
Replace the env var toggle and separate version/checksum constants
with a single `DependencyMode` enum, matching the pattern used in
wordpress-rs.
The enum is generic and reusable across projects.

The Fastlane `update_swift_package` lane now rewrites one line
instead of two.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
Trunk always builds from source now.
CI rewrites this line to `.release` when cutting a tagged release.
The env var toggle is no longer needed.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
Replace the tag-triggered publish step with one guarded by
`NEW_VERSION` env var.
The new `.buildkite/release.sh` orchestrates the full flow:
build xcframework, sign, then hand off to `fastlane release`
which rewrites `Package.swift`, tags, and uploads.

The tag is an output of the release process, not a trigger.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
For end-to-end testing of the release flow.
Revert before merging.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
Match the Buildkite docs naming convention.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
GutenbergKit xcframeworks were never signed.
The signing step was copied from wordpress-rs but the
CI environment doesn't have the ASC env vars configured.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
The release step needs write access to push tags and commits.
The bot credentials are managed by buildkite-ci and require
the pipeline to be in the `use-bot-for-git` allow-list.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
The old `ios/Sources/GutenbergKit/Gutenberg/` build output path is no longer
used now that dist goes into `GutenbergKitResources`.
PR review feedback from @dcalhoun.

---

Generated with the help of Claude Code, https://claude.ai/code

Co-Authored-By: Claude Code Opus 4.6 <noreply@anthropic.com>
Must have come in with a rebase...
@mokagio mokagio force-pushed the ainfra-1967-implement-gutenbergkitresources-binary-dependency-build branch from 9e01fbb to 0184d1b Compare March 12, 2026 00:13
@mokagio
Copy link
Contributor Author

mokagio commented Mar 17, 2026

Closing in favor of #376 because I need to do more thinking around the XCFramework and release automation.

@mokagio mokagio closed this Mar 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Task Issues or PRs that have been broken down into an individual action to take

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants