Skip to content

[Feature] Add Swift Package Manager support - #376

Open
OhKanghoon wants to merge 6 commits into
lynx-family:developfrom
OhKanghoon:feature/swift-package-manager
Open

[Feature] Add Swift Package Manager support#376
OhKanghoon wants to merge 6 commits into
lynx-family:developfrom
OhKanghoon:feature/swift-package-manager

Conversation

@OhKanghoon

@OhKanghoon OhKanghoon commented Aug 20, 2026

Copy link
Copy Markdown

This adds a Package.swift so PrimJS can be consumed with Swift Package Manager in addition to CocoaPods.

What's in it

The manifest is a 1:1 mapping of PrimJS.podspec. Sources, preprocessor definitions and header search paths are taken from the podspec and point at the existing src/ tree; nothing is copied or wrapped. Sources are declared per directory wherever the podspec uses a recursive glob; the few files listed individually (napi.cc, napi_module.cc, the two adapter files) are the ones the podspec also lists individually, with the reason next to each.

CocoaPods subspec SwiftPM
PrimJS/quickjs + PrimJS/log PrimJS
PrimJS/quickjs_debugger Debugger package trait (internal PrimJSInspector target)
PrimJS/napi/{core,env,quickjs} PrimJSNAPI
PrimJS/napi/jsc PrimJSNAPIJSC
PrimJS/napi/adapter PrimJSNAPIAdapter

Public headers. SwiftPM allows a single public header directory per target and cannot rename or re-root headers, while CocoaPods assembles Headers/Public/PrimJS/<header_dir>/<name> from public_header_files, header_dir and header_mappings_dir. tools/ios_tools/generate_spm_headers.py writes that same layout as relative symlinks under swiftpm/headers/<target>/, so the include paths are identical for both package managers (quickjs/include/quickjs.h, quickjs/include/trace-gc.h, napi_env_quickjs.h, ...). I checked the generated tree against what pod install produces for the same subspecs; all 65 entries match. The script is plain Python with a declarative table of what each target publishes and has no dependency on CocoaPods, so the SwiftPM setup keeps working after the podspec is gone. Its --check mode runs in CI and fails with the list of stale/missing entries when a public header is added or moved without regenerating.

Debugger. quickjs_debugger is a package trait instead of a product. Adding the subspec to a pod changes ENABLE_QUICKJS_DEBUGGER for the whole pod target, which changes struct layouts in quickjs-inner.h and switches the snapshot to embedded-inspector.S. A separate SwiftPM target layered on top of the plain engine would therefore link the wrong snapshot and see the wrong ABI; the trait reproduces the pod behaviour. This is the reason for swift-tools-version: 6.1 (Xcode 16.3+).

Swift. Without a module map SwiftPM treats the whole public header directory as the module's umbrella, so import PrimJS would fail on the first C++ header (the same happens with CocoaPods use_frameworks!, whose umbrella imports every public header). A hand-written umbrella header in swiftpm/headers/PrimJS/ lists the C API (quickjs.h, preceded by <stdbool.h> since the header is written for C++) and a three-line module map points at it, so Swift can call the engine directly. Objective-C++ consumers are unaffected; they never go through the module. The header generator only manages the symlinks and leaves these two files alone.

Tests. swift build only archives the library targets, so a small XCTest target (one .mm file) links the engine, evaluates a script on the template interpreter and runs one through the Node-API layer; a second one does import PrimJS from Swift. Both run with and without the trait.

CI. A swift-package job runs the header check, swift build/swift test on macOS with and without Debugger, xcodebuild test on the iOS Simulator and a generic/platform=iOS build. It uses macos-latest; happy to move it to the self-hosted darwin runners if that's preferred.

Limitations

  • arm64 only, same as the podspec: the template interpreter ships as pre-generated arm64 assembly, so x86_64 simulator builds don't link.
  • As with CocoaPods, consumers of the Node-API headers have to define ENABLE_CODECACHE themselves; SwiftPM can't propagate defines to dependents. Noted in Package.swift next to the test target, which does exactly that.

Verification

  • swift build, swift test, both with --traits Debugger as well (macOS arm64, Xcode 26.4)
  • xcodebuild test -scheme PrimJS-Package on an iOS 27 simulator, xcodebuild build -destination generic/platform=iOS
  • python3 tools/ios_tools/generate_spm_headers.py --check
  • Fresh clone of the branch builds and tests from the committed files alone
  • pod spec lint is unaffected: no existing file is modified except .gitignore and ci.yml

@cla-assistant

cla-assistant Bot commented Aug 20, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@OhKanghoon
OhKanghoon force-pushed the feature/swift-package-manager branch 3 times, most recently from 43ea51d to 246649d Compare August 20, 2026 15:21
Ignore .build/, .swiftpm/, Package.resolved and DerivedData/ ahead of adding Package.swift.
Swift Package Manager allows a single public header directory per target
and cannot rename or re-root headers. tools/ios_tools/generate_spm_headers.py
therefore writes, for each SwiftPM target, a directory of relative symlinks
under swiftpm/headers/<target>/.

The layout follows the CocoaPods rules for `public_header_files`,
`header_dir` and `header_mappings_dir` (Headers/Public/PrimJS/...), so the
include paths are the ones CocoaPods users have always used:
`quickjs/include/quickjs.h`, `quickjs/include/trace-gc.h`,
`napi_env_quickjs.h`, `basic/log/logging.h`. The generated tree is
identical to what `pod install` produces for the same subspecs.

The script is plain Python with a declarative table of what each target
publishes; it does not depend on CocoaPods so the SwiftPM setup keeps
working after the podspec is gone. `--check` verifies the committed tree
is up to date and is run in CI.
Add Package.swift mapping PrimJS.podspec 1:1 onto SwiftPM targets, using
the existing sources, preprocessor definitions and header search paths
from the original src/ tree:

  PrimJS/quickjs + log            -> PrimJS
  PrimJS/quickjs_debugger         -> `Debugger` trait (PrimJSInspector)
  PrimJS/napi/{core,env,quickjs}  -> PrimJSNAPI
  PrimJS/napi/jsc                 -> PrimJSNAPIJSC
  PrimJS/napi/adapter             -> PrimJSNAPIAdapter

Sources are declared per directory wherever the podspec uses a recursive
glob; the few files listed individually are the ones the podspec also
lists individually. Public headers come from the generated
swiftpm/headers/ directories, so include paths are the same as with
CocoaPods.

The debugger is a package trait rather than a separate product because,
exactly like adding the subspec to a pod target, ENABLE_QUICKJS_DEBUGGER
changes the engine ABI (quickjs-inner.h) and selects the inspector
snapshot (embedded-inspector.S) for every target. Both snapshots live in
the PrimJS target and guard themselves on that define.

As with the podspec, only arm64 is supported: the template interpreter
ships as pre-generated arm64 assembly.
`swift build` only archives the library targets. The test links the
engine, including the pre-generated snapshot (embedded.S), evaluates a
script on the template interpreter and runs one through the Node-API
layer, using the CocoaPods include paths reproduced by swiftpm/headers.
It also runs with `--traits Debugger`, which exercises the inspector
snapshot and the engine <-> inspector link.
Verify that swiftpm/headers is up to date, build and test on macOS with
and without the Debugger trait, test on the iOS Simulator and build for
iOS devices.
Without a module map SwiftPM treats the whole public header directory as
the module's umbrella, so `import PrimJS` fails on the first C++ header
(logging.h, quickjs-inner.h, ...). CocoaPods has the same problem with
`use_frameworks!`, whose umbrella imports every public header.

Add a hand-written umbrella header, swiftpm/headers/PrimJS/PrimJS.h, that
lists the C API (quickjs.h) and pulls in <stdbool.h> first, since quickjs.h
is written for C++ where `bool` is built in, plus a module map that points
at it. Objective-C++ consumers are unaffected: they never go through the
module and keep including headers textually.

The header generator now owns only the symlinks in swiftpm/headers and
leaves regular files alone, so regenerating keeps the two files. A Swift
test target imports the module and evaluates a script.
@OhKanghoon
OhKanghoon force-pushed the feature/swift-package-manager branch from 246649d to 7a5632f Compare August 20, 2026 15:37
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.

1 participant