Initial SPM support - #1040
Conversation
Summary - Extender code coverage reportSummary
Coveragecom/defold/extender - 42.3%
com/defold/extender/builders - 0%
com/defold/extender/cache - 33.3%
com/defold/extender/cache/info - 100%
com/defold/extender/log - 14.2%
com/defold/extender/metrics - 37.5%
com/defold/extender/process - 73.4%
com/defold/extender/progress - 90.6%
com/defold/extender/remote - 89%
com/defold/extender/services - 58%
com/defold/extender/services/cocoapods - 51.7%
com/defold/extender/services/data - 80.7%
com/defold/extender/services/spm - 40.6%
com/defold/extender/tracing - 21.8%
com/defold/extender/utils - 18.1%
|
…n, createDirectories
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1c2573fe72
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 27b845989d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| /** SPM package identity: the repository basename without the ".git" suffix. */ | ||
| public String label() { | ||
| String basename = url.substring(url.lastIndexOf('/') + 1); | ||
| if (basename.endsWith(".git")) { |
There was a problem hiding this comment.
Normalize trailing slashes before deriving package identity
When a valid repository URL ends in / (for example, https://github.com/apple/swift-log.git/), lastIndexOf('/') + 1 points past the end and produces an empty package label. The generated manifest consequently contains .product(..., package: ""), so SwiftPM cannot associate the requested product with its dependency and the build fails. Strip trailing slashes during sanitization or before deriving the label.
Useful? React with 👍 / 👎.
| } else { | ||
| resourceDest.mkdirs(); | ||
| FileUtils.copyDirectory(resourceFile, resourceDest); |
There was a problem hiding this comment.
Clear colliding resource directories before copying
When CocoaPods and SPM provide resource directories with the same basename, this path logs that the later resource will overwrite the earlier one but FileUtils.copyDirectory actually merges it into the existing directory. Files present only in the first bundle remain, producing a hybrid resource bundle rather than deterministic last-writer-wins output; delete the destination directory on same-type directory collisions before copying.
Useful? React with 👍 / 👎.
britzl
left a comment
There was a problem hiding this comment.
Looks good as far as I can tell!
|
we should update extensions manuals as well |
This PR adds Swift Package Manager support for iOS/macOS extension builds. An extension can now declare SPM dependencies next to (or instead of) CocoaPods, and the resolved products are compiled against and linked into the engine exactly like pod artifacts. CocoaPods and SPM coexist in one build.
Declaring dependencies
An extension ships a
SwiftPackages.jsonin itsios/orosx/folder:{ "platform": "ios", "minVersion": "15.0", "packages": [ { "url": "https://github.com/getsentry/sentry-cocoa.git", "from": "9.0.0", "products": ["Sentry"] } ] }Each package takes exactly one requirement:
version(exact),from,branch, orrevision. An optional top-level"wrapperType": "static" | "dynamic"overrides the wrapper linkage (see below). Manifests from multiple extensions are merged:minVersionis the maximum, packages are keyed by canonical URL, and conflicting requirements for the same package fail the build with a clear error.The manifest is plain JSON — user-authored Swift is never executed. The parser validates and reconstructs every value from whitelisted parts (https-only URLs without userinfo/port/query, regex-constrained versions/branches/revisions/product names, size caps), so no raw manifest input ever reaches a generated file or a command line.
Build flow
For Apple platforms,
AsyncBuilderinvokes the newSwiftPackageManagerService(behindextender.spm.enabled, off by default) before the extension build:SwiftPackages.jsonfiles in the job, parse and merge them (SpmManifestParser).Package.swiftwith a single static-library targetSpmDepsdepending on every requested product;project.ymlfor a wrapper project with one framework targetSpmWrapperthat links theSpmDepspackage product.xcodegen generate, thenxcodebuild -project SpmWrapper.xcodeproj -scheme SpmWrapper -configuration Release buildwith a destination mapped from the target platform/arch. Xcode resolves the package graph, fetches sources/binary artifacts, and compiles everything. Full output is captured tobuild.login the job folder.SpmBuildOutputParser; clang-dynamiclibline for the dynamic shape,Libtool -staticfor the static one) and combine it with the products directory (ResolvedPackages.harvest):#import <Module/Header.h>and<Module>-Swift.hwork for source-built packages);Package.resolvedas the lock file emitted with the build output.Static vs dynamic wrapper
By default the wrapper builds as
MACH_O_TYPE=staticlib: libtool folds all package objects into one archive that links straight into the engine — no embedding, no load-time cost. Some SDKs don't survive static merging (e.g. binary xcframeworks whose duplicate inputs libtool folds, producing duplicate symbols under the engine's-ObjC); a manifest can opt into"wrapperType": "dynamic", which builds anmh_dylibwrapper that is embedded into the app bundle with the matching rpath flags. Both shapes are first-class;extender.spm.wrapper-mach-o-typesets the default.Integration:
ResolvedNativeDepsThe CocoaPods-specific plumbing in
Extenderis generalized behind a newResolvedNativeDepsinterface (frameworks, search paths, static libs, include paths, link flags, min OS version, resources/bundles, dynamic frameworks, lock file, privacy manifests).ResolvedPodsimplements it via delegating aliases — no CocoaPods behavior or test changes — and the newResolvedPackagesis the SPM implementation.Extenderiterates aList<ResolvedNativeDeps>, so compile/link argument collection, resource copying, framework embedding, and lock-file emission are shared between both dependency managers. The platform min version used for the engine link is the max across all resolvers.Multi-Xcode & caching
One Extender instance can serve jobs pinned to different Xcode versions, so
DEVELOPER_DIRis resolved per build: the job'sXCODE_VERSIONis looked up in theextender.spm.xcode-developer-dirsmap, falling back toextender.spm.default-developer-dir. Selection is per-process env only — neverxcode-select— so concurrent jobs on different Xcodes don't interfere.DerivedData, the module cache, and
-clonedSourcePackagesDirPathare per-job (SwiftPM prunes the cloned-packages dir to the current graph, so it must not be shared). Cross-job warmth comes from a shared-packageCachePath(git mirrors + binary artifacts), keyed by Xcode version and rotated/cleaned by crons mirroring the CocoaPods cache machinery.Hardening
xcodebuildruns with-scmProvider systemandGIT_CONFIG_NOSYSTEM=1,GIT_CONFIG_GLOBAL=/dev/null,GIT_TERMINAL_PROMPT=0,GIT_ASKPASS=/usr/bin/true: package fetching uses system git with no user/system config, no credential prompts, and no keychain-backed git credential lookups. Code signing is disabled (CODE_SIGNING_ALLOWED=NO) and package plugins are not validated interactively (-skipPackagePluginValidation). Note: packages shipping binary artifacts hit SwiftPM's internal downloader, whose keychain-backed auth provider has no off-switch — on a fresh builder the keychain must either contain no items for the artifact hosts or have access pre-granted once.Configuration
Enabled in the
standalone-devprofile. Builders additionally needxcodegeninstalled. A newextender.job.spm.resolvemetric times the resolution stage.Fixes #824