Skip to content

Initial SPM support - #1040

Merged
ekharkunov merged 3 commits into
devfrom
feature/swift-package-manager
Aug 31, 2026
Merged

Initial SPM support#1040
ekharkunov merged 3 commits into
devfrom
feature/swift-package-manager

Conversation

@ekharkunov

@ekharkunov ekharkunov commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

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.json in its ios/ or osx/ 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, or revision. An optional top-level "wrapperType": "static" | "dynamic" overrides the wrapper linkage (see below). Manifests from multiple extensions are merged: minVersion is 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, AsyncBuilder invokes the new SwiftPackageManagerService (behind extender.spm.enabled, off by default) before the extension build:

  1. Discover & parse — find SwiftPackages.json files in the job, parse and merge them (SpmManifestParser).
  2. Generate — render two templates into the job dir:
    • an aggregator Package.swift with a single static-library target SpmDeps depending on every requested product;
    • an XcodeGen project.yml for a wrapper project with one framework target SpmWrapper that links the SpmDeps package product.
  3. Build — run xcodegen generate, then xcodebuild -project SpmWrapper.xcodeproj -scheme SpmWrapper -configuration Release build with a destination mapped from the target platform/arch. Xcode resolves the package graph, fetches sources/binary artifacts, and compiles everything. Full output is captured to build.log in the job folder.
  4. Harvest — locate the wrapper's final link line in the log (SpmBuildOutputParser; clang -dynamiclib line for the dynamic shape, Libtool -static for the static one) and combine it with the products directory (ResolvedPackages.harvest):
    • built frameworks, system frameworks/libs, framework/library search paths, rpaths;
    • resource bundles and privacy manifests (deduped by canonical path);
    • include paths for the extension's own sources, derived from the generated module maps (so #import <Module/Header.h> and <Module>-Swift.h work for source-built packages);
    • the Swift runtime lib dir for the static shape;
    • Package.resolved as the lock file emitted with the build output.
  5. Consume — the harvested result feeds the normal compile/link flow (see integration below); dynamic frameworks are copied to the build output for embedding.

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 an mh_dylib wrapper that is embedded into the app bundle with the matching rpath flags. Both shapes are first-class; extender.spm.wrapper-mach-o-type sets the default.

Integration: ResolvedNativeDeps

The CocoaPods-specific plumbing in Extender is generalized behind a new ResolvedNativeDeps interface (frameworks, search paths, static libs, include paths, link flags, min OS version, resources/bundles, dynamic frameworks, lock file, privacy manifests). ResolvedPods implements it via delegating aliases — no CocoaPods behavior or test changes — and the new ResolvedPackages is the SPM implementation. Extender iterates a List<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_DIR is resolved per build: the job's XCODE_VERSION is looked up in the extender.spm.xcode-developer-dirs map, falling back to extender.spm.default-developer-dir. Selection is per-process env only — never xcode-select — so concurrent jobs on different Xcodes don't interfere.

DerivedData, the module cache, and -clonedSourcePackagesDirPath are 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

xcodebuild runs with -scmProvider system and GIT_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

extender:
    spm:
        enabled: false
        home-dir-prefix: /tmp/.swiftpm
        default-developer-dir: /Applications/Xcode.app/Contents/Developer
        # xcode-developer-dirs:
        #     "26.2": /Applications/Xcode_26.2.app/Contents/Developer
        xcodegen-path: xcodegen
        wrapper-mach-o-type: staticlib   # staticlib | mh_dylib
        swift-version: "6.0"
        cache-dir-rotate-cron: "0 20 2 * * *"
        old-cache-clean-cron: "0 20 6 * * *"

Enabled in the standalone-dev profile. Builders additionally need xcodegen installed. A new extender.job.spm.resolve metric times the resolution stage.

Fixes #824

@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown

Summary - Extender code coverage report

Summary
Generated on: 08/30/2026 - 10:14:56
Coverage date: 08/30/2026 - 10:14:21
Parser: JaCoCo
Assemblies: 15
Classes: 102
Files: 102
Line coverage: 48% (3904 of 8121)
Covered lines: 3904
Uncovered lines: 4217
Coverable lines: 8121
Total lines: 16598
Branch coverage: 50.3% (1789 of 3551)
Covered branches: 1789
Total branches: 3551
Method coverage: Feature is only available for sponsors
Tag: 246_33305943924

Coverage

com/defold/extender - 42.3%
Name Line Branch
com/defold/extender 42.3% 48.2%
com/defold/extender/AppManifestConfiguration 100%
com/defold/extender/AppManifestPlatformConfig 100%
com/defold/extender/AsyncBuilder 0% 0%
com/defold/extender/BuilderConstants 80%
com/defold/extender/BuildProgressController 93.1% 91.6%
com/defold/extender/Configuration 100%
com/defold/extender/Extender 14.4% 14.3%
com/defold/extender/ExtenderApplication 0%
com/defold/extender/ExtenderBuildState 62.5% 57.1%
com/defold/extender/ExtenderConst 0%
com/defold/extender/ExtenderController 16.9% 21.4%
com/defold/extender/ExtenderException 100%
com/defold/extender/ExtenderUtil 70.5% 61.6%
com/defold/extender/ExtenderYamlSafeConstructor 100%
com/defold/extender/ExtensionManifestValidator 94.6% 86.3%
com/defold/extender/ManifestConfiguration 100%
com/defold/extender/ManifestPlatformConfig 100%
com/defold/extender/PlatformConfig 100%
com/defold/extender/PlatformNotSupportedException 0%
com/defold/extender/R8Builder 87.5% 76.8%
com/defold/extender/R8Configuration 93.5% 50%
com/defold/extender/R8RulePolicy 76% 70.4%
com/defold/extender/SandboxedPath 72.5% 71.4%
com/defold/extender/TemplateExecutor 39.1% 50%
com/defold/extender/Timer 100%
com/defold/extender/TreePrinter 0% 0%
com/defold/extender/Version 0%
com/defold/extender/VersionNotSupportedException 0%
com/defold/extender/WebSecurityConfig 0% 0%
com/defold/extender/WhitelistConfig 100%
com/defold/extender/ZipUtils 80.3% 69.2%
com/defold/extender/builders - 0%
Name Line Branch
com/defold/extender/builders 0% 0%
com/defold/extender/builders/CSharpBuilder 0% 0%
com/defold/extender/cache - 33.3%
Name Line Branch
com/defold/extender/cache 33.3% 16.6%
com/defold/extender/cache/CacheEntry 64.7% 75%
com/defold/extender/cache/CacheKeyGenerator 81.8% 100%
com/defold/extender/cache/DataCache
com/defold/extender/cache/DataCacheFactory 5% 0%
com/defold/extender/cache/DummyDataCache 80%
com/defold/extender/cache/GCPDataCache 0% 0%
com/defold/extender/cache/LocalDiskDataCache 64.7% 16.6%
com/defold/extender/cache/info - 100%
Name Line Branch
com/defold/extender/cache/info 100% ****
com/defold/extender/cache/info/CacheInfoFileParser 100%
com/defold/extender/cache/info/CacheInfoFileWriter 100%
com/defold/extender/cache/info/CacheInfoWrapper 100%
com/defold/extender/log - 14.2%
Name Line Branch
com/defold/extender/log 14.2% 0%
com/defold/extender/log/ExtenderLogEnhancer 0% 0%
com/defold/extender/log/ExtenderLogEnhancerConfiguration 0% 0%
com/defold/extender/log/Markers 80%
com/defold/extender/metrics - 37.5%
Name Line Branch
com/defold/extender/metrics 37.5% ****
com/defold/extender/metrics/MetricsWriter 37.5%
com/defold/extender/process - 73.4%
Name Line Branch
com/defold/extender/process 73.4% 75.4%
com/defold/extender/process/CommandLineTokenizer 97.7% 89.7%
com/defold/extender/process/ProcessExecutor 64.3% 61.1%
com/defold/extender/process/ProcessUtils 0% 0%
com/defold/extender/progress - 90.6%
Name Line Branch
com/defold/extender/progress 90.6% 81.6%
com/defold/extender/progress/BuildProgressEvent 100%
com/defold/extender/progress/BuildProgressService 90.2% 80.5%
com/defold/extender/progress/BuildStage 100% 100%
com/defold/extender/progress/ProgressReporter 40%
com/defold/extender/remote - 89%
Name Line Branch
com/defold/extender/remote 89% 67.2%
com/defold/extender/remote/RemoteBuildException 100%
com/defold/extender/remote/RemoteEngineBuilder 93.3% 71.8%
com/defold/extender/remote/RemoteHostConfiguration 100%
com/defold/extender/remote/RemoteInstanceConfig 100%
com/defold/extender/remote/RemoteProgressRelay 76.7% 60.8%
com/defold/extender/services - 58%
Name Line Branch
com/defold/extender/services 58% 47.2%
com/defold/extender/services/DataCacheService 79.8% 61.9%
com/defold/extender/services/DefoldSdkService 75.5% 61%
com/defold/extender/services/DefoldSdkServiceConfiguration
com/defold/extender/services/GCPInstanceService 1.1% 0%
com/defold/extender/services/GradleArtifact 100%
com/defold/extender/services/GradleService 0%
com/defold/extender/services/GradleServiceInterface
com/defold/extender/services/HealthReporterService 82.4% 55.2%
com/defold/extender/services/MockGradleService 0%
com/defold/extender/services/RealGradleService 55.9% 40%
com/defold/extender/services/ResolvedNativeDeps 0%
com/defold/extender/services/UserUpdateService 0% 0%
com/defold/extender/services/cocoapods - 51.7%
Name Line Branch
com/defold/extender/services/cocoapods 51.7% 56.6%
com/defold/extender/services/cocoapods/CocoaPodsService 9.4% 20.6%
com/defold/extender/services/cocoapods/CocoaPodsServiceBuildState 16.6% 0%
com/defold/extender/services/cocoapods/CreateBuildSpecArgs 60.7%
com/defold/extender/services/cocoapods/IConfigParser
com/defold/extender/services/cocoapods/LanguageSet 66.6% 50%
com/defold/extender/services/cocoapods/MainPodfile 20%
com/defold/extender/services/cocoapods/PlatformAndLanguageSet 0% 0%
com/defold/extender/services/cocoapods/PlatformSet 0% 0%
com/defold/extender/services/cocoapods/PlistBuddyWrapper 0% 0%
com/defold/extender/services/cocoapods/PodBuildSpec 81.4% 65.4%
com/defold/extender/services/cocoapods/PodfileParser 94.9% 82.2%
com/defold/extender/services/cocoapods/PodfileParsingException 100%
com/defold/extender/services/cocoapods/PodSpec 60.4% 40%
com/defold/extender/services/cocoapods/PodSpecParser 89.5% 79.1%
com/defold/extender/services/cocoapods/PodUtils 63% 51.4%
com/defold/extender/services/cocoapods/ResolvedPods 51.5% 45.6%
com/defold/extender/services/cocoapods/XCConfigParser 94.5% 85.2%
com/defold/extender/services/data - 80.7%
Name Line Branch
com/defold/extender/services/data 80.7% 75%
com/defold/extender/services/data/DefoldSdk 87.5% 75%
com/defold/extender/services/data/GCPInstanceState 0%
com/defold/extender/services/spm - 40.6%
Name Line Branch
com/defold/extender/services/spm 40.6% 47.7%
com/defold/extender/services/spm/ResolvedPackages 0% 0%
com/defold/extender/services/spm/SpmBuildOutputParser 91.3% 81.1%
com/defold/extender/services/spm/SpmManifestParser 87.3% 72.1%
com/defold/extender/services/spm/SpmManifestParsingException 100%
com/defold/extender/services/spm/SpmServiceBuildState 0% 0%
com/defold/extender/services/spm/SpmServiceConfiguration 0%
com/defold/extender/services/spm/SwiftPackageManagerService 0% 0%
com/defold/extender/tracing - 21.8%
Name Line Branch
com/defold/extender/tracing 21.8% 25%
com/defold/extender/tracing/ExtenderExecutor 0% 0%
com/defold/extender/tracing/ExtenderTracerInterceptor 100% 100%
com/defold/extender/tracing/TraceIdInResponseServletFilter 0% 0%
com/defold/extender/utils - 18.1%
Name Line Branch
com/defold/extender/utils 18.1% 32.5%
com/defold/extender/utils/FrameworkUtil 0% 0%
com/defold/extender/utils/PodBuildUtil 0% 0%
com/defold/extender/utils/VersionUtil 69.2% 65%

Comment thread server/src/main/java/com/defold/extender/Extender.java Fixed
@ekharkunov
ekharkunov marked this pull request as ready for review August 30, 2026 10:01

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

@ekharkunov

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +109 to +112
/** SPM package identity: the repository basename without the ".git" suffix. */
public String label() {
String basename = url.substring(url.lastIndexOf('/') + 1);
if (basename.endsWith(".git")) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +1148 to +1150
} else {
resourceDest.mkdirs();
FileUtils.copyDirectory(resourceFile, resourceDest);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

@ekharkunov
ekharkunov requested a review from britzl August 30, 2026 10:25

@britzl britzl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good as far as I can tell!

@ekharkunov
ekharkunov merged commit a4d253f into dev Aug 31, 2026
4 of 5 checks passed
@ekharkunov
ekharkunov deleted the feature/swift-package-manager branch August 31, 2026 16:58
@AGulev

AGulev commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

we should update extensions manuals as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Swift Package Manager support

4 participants