Integrating Kotlin/Native into Swift Build

As part of the Kotlin Build Tools team at JetBrains, we’ve been exploring how to integrate Kotlin/Native directly into Swift Build. Swift Build is the build execution system used by Xcode and Swift Package Manager (SwiftPM) across Swift, Objective-C, C/C++, and other components of the Apple toolchain.

Our prototype extends Swift Build with initial support for the Kotlin/Native compiler, kotlinc-native, allowing Swift Build to compile Kotlin targets and expose the resulting artifacts to Swift targets as dependencies. This enables Swift targets to import and use Kotlin code, much like how Swift can import Objective-C code in Xcode or SwiftPM.

The Kotlin/Native compiler takes Kotlin sources and produces Kotlin/Native libraries, .klib intermediate artifacts. When Kotlin targets depend on one another, their corresponding .klib files are passed through the build graph, so that each Kotlin target can see and use its dependencies during compilation.

When a Swift target depends on one or more Kotlin targets, Swift Build collects all relevant .klib files and invokes the Kotlin/Native compiler again to translate them into a single .framework file. It contains headers and binaries that Swift code can import, enabling cross-language integration.

The prototype supports:

  • Detecting Kotlin sources in a target.
  • Compiling them into .klib libraries.
  • Passing .klib outputs through Kotlin-to-Kotlin dependencies.
  • Generating a single .framework file that Swift can import and link against.

Kotlin Multiplatform (KMP) libraries are most commonly built with Gradle. To support SwiftPM build and distribution, we convert such KMP libraries into SwiftPM-compatible packages with a custom Gradle task that generates a Sources layout and a Package.swift manifest containing the required Kotlin/Native compiler options.

Quickstart

We have already implemented some SwiftPM packages and an Xcode project. Some of the SwiftPM packages are even converted from KMP projects. Please see the following examples and run the commands given to open the projects in Xcode using Swift Build with Kotlin integration:

  1. Git clone the repo : kotlin-swift-build.
  2. Setup the Kotlin/Native Toolchain, check the Kotlin/Native toolchain section.
  3. Change directory to clone repo root.

Here are some samples; you can run the commands from repository root:

  1. SwiftPackage
// to open in Xcode
swift package --disable-sandbox launch-xcode --project-path KotlinSamples/SwiftPackage
// to build directly from terminal
swift package --disable-sandbox run-xcodebuild -- \
-workspace KotlinSamples/SwiftPackage/.swiftpm/xcode/package.xcworkspace \
-scheme SwiftPackage \
-configuration Debug \
-destination 'platform=macOS' \
KOTLINC_NATIVE_PATH="<YourAbsolutePathToKotlinCNativeExecutable>" \
build
  1. ConversionSample
// to open in Xcode
swift package --disable-sandbox launch-xcode --project-path KotlinSamples/ConversionSample
// to build directly from terminal
swift package --disable-sandbox run-xcodebuild -- \
-workspace KotlinSamples/ConversionSample/.swiftpm/xcode/package.xcworkspace \
-scheme ConversionSample \
-configuration Debug \
-destination 'platform=macOS' \
KOTLINC_NATIVE_PATH="<YourAbsolutePathToKotlinCNativeExecutable>" \
build
  1. KotlinSwiftXCodeProject
// to open in Xcode
swift package --disable-sandbox launch-xcode --project-path KotlinSamples/KotlinSwiftXCodeProject
// to build directly from terminal
swift package --disable-sandbox run-xcodebuild -- \
-project KotlinSamples/KotlinSwiftXCodeProject/KotlinSwiftXCodeProject.xcodeproj \
-scheme swiftConsumer \
KOTLINC_NATIVE_PATH="<YourAbsolutePathToKotlinCNativeExecutable>" \
build
  1. kotlin-nsurl-session
// to open in Xcode
swift package --disable-sandbox launch-xcode --project-path KotlinSamples/kotlin-nsurl-session
// to build directly from terminal
swift package --disable-sandbox run-xcodebuild -- \
-workspace KotlinSamples/kotlin-nsurl-session/.swiftpm/xcode/package.xcworkspace \
-scheme kotlin_nsurl_session \
-configuration Debug \
-destination 'platform=macOS' \
KOTLINC_NATIVE_PATH="<YourAbsolutePathToKotlinCNativeExecutable>" \
build

Prototype implementation

NOTE: The current implementation supports only single-target-triple builds. Building for multiple architectures will result in a “Duplicate Task” exception.

Let’s review the structure of the prototype and highlight open integration points.

You can also check out our fork of Swift Build, including all the changes, at kotlin-swift-build.
To learn more about the added tools and changes, you can also check the open merge request.

Configuring the Kotlin/Native Toolchain

A built-in macro is introduced:

  • KOTLINC_NATIVE_PATH

KOTLINC_NATIVE_PATH is evaluated via:

MacroEvaluationScope.evaluateAsString(BuiltinMacros.KOTLINC_NATIVE_PATH)

This value is currently hard-coded and should eventually be detected dynamically.

Compiling Kotlin targets

A new KlibCompilerSpec task compiles all .kt files in a target into a .klib artifact. For Xcode-defined targets, this works out of the box. You can add .kt files to the Kotlin target and write Kotlin code as you would in any IDE. This distinction is important because the same workflow does not work out of the box in a SwiftPM setup.

Kotlin-to-Kotlin Dependency Resolution and .klib Propagation

When Kotlin targets depend on one another, Swift Build must ensure that the compiled Kotlin outputs (.klib files) are passed through the build graph, allowing each Kotlin target to see and use APIs of its dependencies during compilation.

In our prototype, each Kotlin target declares its generated .klib as the output of the KlibCompilerSpec task. During the compilation of another Kotlin target, Swift Build examines the target’s dependencies, identifies which of them are Kotlin targets, reconstructs the expected output locations of their .klib files, and supplies these .klib files as inputs to the dependent target’s Kotlin/Native compilation.

This mechanism ensures that Kotlin dependencies are available to the Kotlin/Native compiler exactly when needed. The resulting .klib files are also used during framework generation, where Swift Build invokes the Kotlin/Native compiler again to produce the final .framework file imported by Swift code.

Support for Kotlin compilation in SwiftPM

SwiftPM does not pass source files directly to the build system. Instead, it converts them into a Package Intermediate Format (PIF), which Xcode and Swift Build use to construct the build graph. During this conversion, SwiftPM classifies files strictly by known language extensions, such as .swift or .c/.cpp. However, it has no concept of .kt files, as a result:

  • .kt files never appear in the target’s source list.
  • Swift Build never receives them as inputs.
  • Kotlin-specific tool specs cannot see or compile them.

Since PIF is the only source description that Swift Build receives from SwiftPM, Kotlin sources must appear under a recognized extension to be propagated through the pipeline.

To make SwiftPM include Kotlin sources in its PIF, we represent Kotlin source files in the package as .kt.swift files:

<file>.kt → <file>.kt.swift

This causes SwiftPM to treat these files as Swift sources and include them in the PIF. Once they reach Swift Build:

  1. They are routed to SwiftCompiler thanks to the .swift-like extension.
  2. SwiftCompiler filters them out, preventing Swift compilation.
  3. The files are restored to their original .kt form inside TARGET_TEMP_DIR.
  4. KlibCompilerSpec receives the restored .kt files and compiles them as Kotlin sources.

This workaround enables Kotlin compilation within the existing SwiftPM → PIF → Swift Build pipeline. However, it’s not ideal, as it relies on SwiftPM misclassifying Kotlin sources as Swift files. Proper support would require SwiftPM to:

  • Recognize .kt as a valid source type.
  • Expose Kotlin sources through the PIF model directly.

Suppressing linker tasks for Kotlin targets

Kotlin targets are not linked using the standard Swift/C/C++ linker pipeline. Kotlin-to-Kotlin linkage is performed by the Kotlin/Native compiler itself using .klib inputs, while a generated .framework performs Swift-to-Kotlin linkage.

For pure Kotlin targets, the regular Swift/C/C++ linkers are not applicable. On Apple platforms, Swift and C/C++ targets are linked by ld the platform linker, which relies on search paths, system frameworks, and auto-linking metadata. Kotlin/Native instead expects a dedicated invocation of the Kotlin/Native compiler with an explicit list of .klib files and does not participate in this platform linker model. To avoid producing incorrect or redundant link actions for Kotlin targets, SourcesTaskProducer.generateTasks returns early when .kt sources are detected, suppressing the creation of Swift/C/C++ linker tasks for those targets.

For Swift targets that depend on Kotlin targets, we still use the standard Swift linker, but adjust the link file list so that it does not try to link individual Kotlin targets or their outputs directly. Instead of adding per-target Kotlin artifacts to the link file list, we later add a single generated .framework (produced by the Kotlin-specific tooling) and let the Swift linker link against that framework only.

Generating .framework automatically

Let’s see how the unified framework is produced and wired into the Swift target.

To make Kotlin code usable from Swift, Kotlin targets are packaged into a .framework. This is performed by a dedicated tool spec, Klib2FrameworkTool, using the following command:

kotlinc-native -p framework

NOTE: Current implementation supports only a single KotlinWorld.framework per final executable or dynamic product. A single Swift target may aggregate any number of Kotlin targets into one KotlinWorld.

In Xcode projects, if multiple dynamic targets (for example, frameworks or executables) that depend on Kotlin targets are built as part of the same build graph, Swift Build will attempt to generate more than one KotlinWorld framework. This is currently not supported.

The same limitation applies to SwiftPM-based builds. If multiple executable or dynamic library products depending on Kotlin targets are built in the same build, multiple KotlinWorld frameworks would be generated and this can end up:

  • Failing the build, for example, with multiple commands producing the KotlinWorld.framework.
  • Succeeding with inconsistent symbol mappings due to global Kotlin/Native name translation.

The Klib2FrameworkTool spec:

  1. Collects all Kotlin dependencies of the Swift target.
  2. Resolves the .klib outputs of those dependencies.
  3. Invokes the Kotlin/Native compiler with the complete set of .klib inputs, producing a unified KotlinWorld.framework.

Although Klib2FrameworkTool receives both direct and transitive Kotlin dependencies during framework generation, the Swift target is only intended to be exposed to the APIs of its direct Kotlin dependencies. The generated framework internally contains all Kotlin dependencies, but Swift imports only what is made visible through those direct Kotlin targets. This design behaves correctly for Xcode-defined targets.

However, when building SwiftPM packages, we observed that dependency flattening occurs at the PIF level. For the following chain:

S (SwiftTarget) → K1 (KotlinTarget) → K2 (KotlinTarget)

The PIF produced by swift package dump-pif lists both K1 and K2 as dependencies of S, even though S declares only K1 as a direct dependency in Package.swift. In effect, SwiftPM surfaces transitive Kotlin dependencies as if they were direct dependencies of the Swift target.

This behaviour is clearly visible in the PIF. The Swift product dependencies and its frameworks build phase include entries for both K1 and K2. Consequently, Swift Build treats all reachable Kotlin targets in the dependency graph as direct inputs to the Swift target.

Due to this flattening, our framework-generation logic must currently expose the APIs of transitive Kotlin dependencies when running under SwiftPM as well, even though semantically, only the direct Kotlin dependency should have been visible to the Swift target.

Phase 1. Compilation with SwiftCompiler

Swift Build runs Klib2FrameworkTool with:

omitBinary = true

In this mode, the Kotlin/Native compiler generates headers, module maps etc. but no binary.

This is primarily a performance optimization. Machine code generation is the most expensive stage of the Kotlin/Native compilation pipeline and is unnecessary for the consuming Swift code.

The generated framework is added to the Swift compiler’s search paths so the Swift target can import:

import KotlinWorld

Phase 2. Linking with LdLinkerSpec

Later in the build graph, Swift Build invokes Klib2FrameworkTool again, this time with the full binary generation enabled. The Kotlin/Native compiler performs hermetic linkage over all the collected .klib files and produces a final fully linked KotlinWorld.framework file containing the compiled Kotlin binaries.

This framework is then passed to the ld platform linker as a standard framework dependency, and the Swift target links against it.

Converting Kotlin to SwiftPM with a Gradle task

A Gradle task transforms a KMP module into a SwiftPM package by:

  • Generating Sources/<module>/<sourceSet>/ with kt.swift symlinks.
  • Computing the Kotlin/Native compiler configuration derived from:
    • Source sets
    • Fragment definitions and refinements relationships (-Xfragments, -Xfragment-refines, and so on)

In Gradle, Kotlin/Native compiler arguments and the source sets are target-specific and depend on the active Apple target, for example, ios_simulator_arm64 or macos_arm64. To reproduce the same compilation model under SwiftPM, these arguments are serialized into Package.swift file, so that Swift Build can reconstruct the correct Kotlin/Native invocation per architecture.

Instead of embedding a single flat compiler string, the conversion task emits a structured representation via cSettings:

cSettings: [
  .define("KOTLINC_NATIVE_ARCH_ARGS", to: "{ ... }"), // JSON: kotlinc-native Target -> fragment flags
  .define("KOTLINC_NATIVE_ARCH_SOURCES", to: "{ ... }"), // JSON: source -> allowed kotlinc-native targets
  .define("KOTLINC_NATIVE_COMMON_ARGS", to: "[ ... ]")  // JSON: shared compiler flags
]

In kotlin-swift-build, KlibCompilerSpec reads these values from GCC_PREPROCESSOR_DEFINITIONS and:

  • Determines the active Kotlin/Native target from the current build environment.
  • Selects the corresponding entry from KOTLINC_NATIVE_ARCH_ARGS.
  • Appends KOTLINC_NATIVE_COMMON_ARGS.
  • Filters source inputs using KOTLINC_NATIVE_ARCH_SOURCES when necessary.
  • Forwards the reconstructed argument set to kotlinc-native.

This approach preserves Kotlin Multiplatform’s target-specific compilation semantics while maintaining deterministic Package.swift.

Here are some examples that we converted:

How to use

Set up the environment

1. Clone our Swift Build prototype

Clone our modified implementation of Swift Build:
git clone https://github.com/Kotlin/kotlin-swift-build

2. Kotlin/Native toolchain

2.1 Provision Kotlin/Native (kotlinc-native) with Gradle (How to get Kotlin/Native toolchain)

The recommended way to provision the Kotlin/Native compiler for Swift Build is to use the Gradle task provided by the Kotlin samples in this repository. This ensures that a compatible Kotlin/Native distribution is downloaded and installed locally.

// Navigate to the kotlin-swift-build repository root
cd <path-to-kotlin-swift-build>

// Navigate to a Kotlin sample that provides Gradle setup
cd KotlinSamples/ConversionSample

// Download and prepare the Kotlin/Native distribution
./gradlew commonizeNativeDistribution

After the task completes, return to the repository root and verify that the Kotlin/Native distribution was installed:

// Navigate back to repository fork
cd <path-to-kotlin-swift-build>

// List all installed Kotlin/Native distributions
ls -ld "$HOME/.konan/kotlin-native-prebuilt-"*

This command will print one or more directories such as:

~/.konan/kotlin-native-prebuilt-macos-aarch64-2.2.21
~/.konan/kotlin-native-prebuilt-macos-x86_64-2.2.21

Note the paths aside for the next step where you override KOTLINC_NATIVE_PATH.

When overriding KOTLINC_NATIVE_PATH, point it to the kotlinc-native executable, eg. it should look like following:

KOTLINC_NATIVE_PATH = ~/.konan/kotlin-native-prebuilt-macos-aarch64-2.2.21/bin/kotlinc-native

2.2 Providing Kotlin/Native toolchain into Swift Build (How to make Swift Build consume Kotlin/Native toolchain)

There are two alternative ways to provide a Kotlin/Native toolchain to Swift Build.

Option 1. Use the expected default path (no code changes required)

Install Kotlin/Native 2.2.21 under the path assumed by the built-in macros:

$HOME/.konan/kotlin-native-prebuilt-macos-aarch64-2.2.21/bin/kotlinc-native
Option 2. Override KOTLINC_NATIVE_PATH
Static Override in macro table

You may provide custom toolchain locations by defining the macros manually in Swift Build.

To do this, navigate to kotlin-swift-build/Sources/SWBCore/Settings/Settings.swift and update the getKotlinCNativeSettingsTable() function:

func getKotlinCNativeSettingsTable() -> MacroValueAssignmentTable{
  var kotlinCNativeTable = MacroValueAssignmentTable(namespace: core.specRegistry.internalMacroNamespace)
  kotlinCNativeTable.push(
    BuiltinMacros.KOTLINC_NATIVE_PATH,
    Static{BuiltinMacros.namespace.parseString("$(HOME)/.konan/kotlin-native-prebuilt-macos-aarch64-2.2.21/bin/kotlinc-native")
    })
  return kotlinCNativeTable
}

Swift Build will resolve KOTLINC_NATIVE_PATH from this macro table for all Kotlin-related specs.

Dynamic Override

When building Xcode projects through Swift Build, the macros can also be overridden via build settings passed to the run-xcodebuild command plugin.

// For Xcode projects
swift package --disable-sandbox run-xcodebuild -- \
-project <YourProject>.xcodeproj \
-scheme <YourScheme> \
KOTLINC_NATIVE_PATH="<YourAbsolutePathToKotlinCNativeExecutable>" \
build
// For SwiftPM packages
swift package --disable-sandbox run-xcodebuild -- \
-workspace <YourPackage>/.swiftpm/xcode/package.xcworkspace \
-scheme SwiftPackage \
-configuration Debug \
-destination 'platform=macOS' \
KOTLINC_NATIVE_PATH="<YourAbsolutePathToKotlinCNativeExecutable>" \
build

Run Xcode with the modified Build System

You can test the modified Swift Build directly in Xcode. In the root directory of the kotlin-swift-build, run:

swift package --disable-sandbox launch-xcode

This launches the currently selected Xcode.app with the modified Swift Build instance instead of the Build System Service.

Building a Xcode Project

  1. Open an existing Xcode project.
  2. Add a new Framework target for your Kotlin code.
  3. Add .kt files directly (do not use the .kt.swift workaround used for SwiftPM).
  4. Navigate to Project Navigator → Your Kotlin Framework Target → Build Phases → Compile Sources and add your .kt files.
  5. Select the Swift target that should depend on Kotlin. Go to Build Phases → Target Dependencies and add your Kotlin framework target.
  6. In your Swift target, import the generated framework:
    import KotlinWorld
    
  7. Call Kotlin APIs as needed and build the Swift target. You will see the Kotlin-related compiler and framework logs in the build output.

Build a SwiftPM Project

  1. Create a new SwiftPM package or open an existing one.
  2. Under Sources, create directories for the Kotlin targets, for example:
    Sources/
    ├── KotlinTarget1/ //import KotlinTarget2 code in KotlinTarget1 
    ├── KotlinTarget2/ 
    
  3. Inside these directories, create Kotlin files using the .kt.swift extension, for example, MyFile.kt.swift.
  4. Define the Kotlin target in Package.swift file:
    // Package.swift
    targets: [
        .target(
            name: "KotlinTarget2",
            path: "Sources/KotlinTarget2"
        ),
        .target(
            name: "KotlinTarget1",
            path: "Sources/KotlinTarget1",
     dependencies: [
                .target(name: "KotlinTarget2")  
            ],
        ),
    ]
    
  5. Add the Kotlin target as a dependency of a Swift target:
    // Package.swift
    targets: [
        .target(
            name: "KotlinTarget2",
            path: "Sources/KotlinTarget2"
        ),
        .target(
            name: "KotlinTarget1",
            path: "Sources/KotlinTarget1",
            dependencies: [
                .target(name: "KotlinTarget2")  
            ],
        ),
        .target(
            name: "SwiftTarget",
            dependencies: [
                .target(name: "KotlinTarget") //transitively depends on KotlinTarget2 
            ],
            path: "Sources/SwiftTarget"
        )
    ]
    

NOTE: SwiftPM builds flatten dependencies within a package. For example, in the SwiftTarget → KotlinTarget1 → KotlinTarget2 chain, SwiftPM surfaces both KotlinTarget1 and KotlinTarget2 as direct dependencies of SwiftTarget.

  1. In the Swift target, import the Kotlin framework KotlinWorld.framework:
    import KotlinWorld
    
  2. Call Kotlin-exposed APIs from Swift.
  3. (Optional) You can also create a macOS Swift executable, depend on the Kotlin target, and run it to validate end-to-end behavior.
  4. (Optional) Use a remote SwiftPM Kotlin dependency:
    1. Add the remote dependency in your Package.swift:
      // Package.swift
      
      dependencies: [
         .package(
             url: "https://github.com/abdulowork/kotlinx-io-fork-for-swift-build",
            exact: "0.9.0-swiftpm-SNAPSHOT.3"
         )
      ]
      
      1. Import and use KotlinWorld:
    // main.swift 
    
    import KotlinWorld
    
    let buf = Buffer() // from kotlinx-io-fork-for-swift-build
    buf.writeInt(int: 64)
    ...
    

Open Questions and Suggested Improvements

The current implementation of our Swift Build prototype leaves room for improvement and raises some integration questions.

Kotlin/Native Code Generation Model vs Target-Based Build

SwiftPM and Xcode target models, as used for Swift and Objective-C, assume that each target or package (the producer) compiles and emits its own machine code artifact, which is later consumed and linked by downstream targets. The producer does not need to know who will consume its output.

Kotlin/Native differs from this model. In the current integration, machine code can only be generated once all consumers of Kotlin code are known, because Kotlin/Native performs final code generation and linkage across the full dependency set. As a result, machine code is effectively produced at the consumer level rather than by individual Kotlin targets.

This raises an open design question for Swift Build:

  • What is the right way to model Kotlin/Native compilation in Swift Build?

Toolchain Resolution

The current implementation relies on hard-coded KOTLINC_NATIVE_PATH.

In contrast to Swift, Kotlin/Native toolchains are not typically discovered from local installations. In the Kotlin ecosystem, the Kotlin version determines the compiler distribution, which is then downloaded and cached automatically (for example, by Gradle).

Open questions here are:

  • How does Swift Build discover the Swift compiler toolchain?
  • Is there an established mechanism for registering or discovering third-party compilers?
  • Does Swift Build support resolving, downloading, and caching external toolchains based on a declared version rather than discovering preinstalled ones?
  • Are there existing examples of version-driven toolchain resolution for non-Swift compilers (for example Android NDK support)?

Overriding KOTLINC_NATIVE_PATH in SwiftPM Contexts

Within Xcode projects, the developer can override toolchain-related build settings, such as providing a custom KOTLINC_NATIVE_PATH through Xcode Build Settings.

For SwiftPM packages, such overrides are also possible when building via xcodebuild, by passing build settings on the command line, for example:

xcodebuild -scheme SwiftPMProduct KOTLINC_NATIVE_PATH=/custom/path/to/kotlinc-native

However, there is currently no way to express these overrides in Package.swift itself.

This raises the question of whether it’s feasible for SwiftPM to allow passing such overrides through the Package manifest, for example:

//Package.swift
.kotlinCNativeToolchain(
    version: "2.2.21",
    path: "/custom/path/to/kotlinc-native"
)

Inclusion of .kt files into Kotlin to SwiftPM conversion

The workaround of defining Kotlin sources as .kt.swift files, so that SwiftPM emits them into the PIF, is suboptimal. Open questions here are:

  • Can SwiftPM be extended to pass .kt files directly to the build system without the .kt.swift workaround?
  • Is there an extension point in SwiftPM for defining new source types?

Custom Kotlin-Specific Settings in Package.swift

A natural extension would be allowing SwiftPM to define new configuration domains, similar to cSettings.

An open question here is whether SwiftPM can define keys for custom settings.

For example, for kmpSettings they could be:

//Package.swift
.target(
    name: "KMPTarget",
    kmpSettings: [
        .define("-Xfragments", to: "..."),
        .define("-Xfragment-refines", to: "..."),
        .define("-Xfragment-sources", to: "...")
    ]
)
  • If yes, what might be the best way to do it? And how could these settings be fetched from Swift Build?
  • If not, what would be your suggestion?

A higher-level KMP DSL in Package.swift

In Gradle, Kotlin Multiplatform avoids raw compiler options by exposing structured concepts such as commonMain, iosMain, macosMain through dependsOn refinement rules.

From these, Gradle automatically derives:

  • Fragment relationships
  • Compilation order
  • All required -Xfragment* options
  • Library wiring

This reduces developer overhead and eliminates a major source of configuration errors.

A similar higher-level DSL in SwiftPM would remove the need for long, fragile compiler options and make KMP usage significantly easier.

For example, a basic SwiftPM DSL could look like:

//Package.swift 
.kmpTarget(
    name: "KMPTarget",
    sourceSets: [
        .common("Sources/KMPTarget/commonMain"),
        .ios("Sources/KMPTarget/iosMain"),
        .macOS("Sources/KMPTarget/macosMain")
    ]
)

Such a DSL would allow SwiftPM to:

  1. Model KMP source sets and refinements directly.
  2. Derive all fragment options automatically.
  3. Pass structured metadata to Swift Build rather than opaque strings.
  4. Simplify the KMP → SwiftPM conversion.

Open questions here are:

  • Can SwiftPM support a new DSL domain (for example, kmpTarget) for modeling source sets and refinements?
  • What mechanism would allow Package.swift to define these settings, manifest extension?

Using a Custom SwiftPM and Swift Build Together

Both .kt inclusion and fragment flag propagation require enhancements to SwiftPM.

This raises a broader question of whether it’s feasible to integrate a custom SwiftPM with a custom Swift Build inside Xcode.

Swift Build Plugins

We are also evaluating whether parts of this integration could be re-implemented as a Swift Build plugin. Open questions here are:

  • What is the recommended approach for implementing third-party compiler integrations using Swift Build plugins?
  • Are there known limitations that would prevent Kotlin/Native from being integrated solely through plugins?
12 Likes

Update (dependency version):
In the “Build a SwiftPM Project” → “(Optional) Use a remote SwiftPM Kotlin dependency” section, the dependency version should be 0.9.0-swiftpm-SNAPSHOT.4 (not .3).

I’m leaving this as a comment since I can’t edit the original post anymore. I believe editing requires additional permissions on Swift Forums, which I don’t have yet.

If you tried the steps earlier and ran into issues, please update the dependency version to …SNAPSHOT.4 and re-resolve packages.

1 Like

Thanks for the detailed writeup- I'll do my best to answer some of the questions inline.

Is this monolithic native code generation motivated by optimizing across the entire set of dependencies, or something else? Broadly speaking, this is somewhat similar to the embedded Swift compilation model, where swiftmodules serve a similar role. Many embedded projects today are emitting an empty object file alongside their swiftmodule so there's an artifact for CMake/SwiftPM/etc. to link, which is usually workable but obviously suboptimal. Some kind of generalized support for interface-only libraries might help solve both problems.

Much of this is implemented in ToolchainRegistry.swift. At a high level, the client of the build system (e.g. SwiftPM) sets up a list of toolchain search paths (e.g. the parent directory of the swift-build executable, plus other known toolchain locations) to load metadata from.

Generally the toolset.json in a Swift SDK is the user-level entry point for customizing a compiler, which is translated to settings overrides in the PIF for settings like CC.

No, we delegate toolchain installation/management to the build system's clients instead of having the build system manage these itself.

There's no technical constraint preventing SwiftPM from implementing these kinds of overrides. Currently SwiftPM doesn't expose every setting supported by the low level build system primarily because certain kinds of customization would make it easier to accidentally break hermeticity, cacheability, and other desirable properties of package builds, or would allow packages to introduce settings which conflict in a package graph containing code from multiple different sources. So additions are generally very deliberate and guided by swift evolution.

I'm not familiar with "source sets", are there any docs available to learn more?

Yeah, making the package manifest API more extensible is IMO very desirable in general. I think ideas for improvements in this area would be very welcome.

There are two plugin models which are relevant here, Swift Build plugins (an implementation detail of the build system primarily used as an organizational tool to divide up the core build system's platform support) and Swift Package plugins (SwiftPM's user-facing extensibility model). The general goal over time has been to decouple more tools from the core build system implementation and move them into package plugins which extend the behavior of the shared core. That said, package plugins as they exist today aren't flexible enough to support your use case - they're primarily used for source code and resource generation. I think it's worth considering what new, general features would be required to support this well. A few off the top of my head:

  • Support for producing a static archive or relocatable object to be included in a target's binary output
  • Support for producing headers
  • Support for aggregating intermediate outputs of dependency targets (to compile Kotlin libs produced by dependencies)

A lot of these are very broadly applicable features which would benefit other ecosystems as well.

3 Likes

Thanks a lot for the response and the detailed answers.

Monolithic Kotlin/Native code generation is driven by Kotlin/Native’s linkage and artifact model and is intentionally leveraged for whole-program optimization and dead-code elimination. Individual Kotlin targets compile independently into .klib artifacts for Kotlin-to-Kotlin dependency propagation. Final linkage is performed by the Kotlin/Native compiler over the full set of reachable .klibs. As a result, native code is produced at the consumer aggregation point when a Swift target depends on Kotlin.

In our integration, when Swift Build constructs tasks for a Swift target with Kotlin dependencies, we collect the relevant .klibs and generate a framework in two phases: an interface-only framework (headers/module maps, no binary) for Swift compilation, followed by a binary framework for the link step. We suppress the standard Swift/C/C++ linker tasks for pure Kotlin targets, since Kotlin-to-Kotlin linkage is handled entirely via .klibs.

From what you described (ToolchainRegistry.swift), Swift Build largely treats toolchains as client-provided: SwiftPM/Xcode supplies toolchain search paths, and Swift Build loads tool metadata from there, with third-party customization flowing through SDK/toolset metadata. In our prototype we currently use a KOTLINC_NATIVE_PATH macro build setting with a hardcoded default (which can be overridden), and we could also pass the Kotlin/Native toolchain path via cSettings during PIF generation in a similar way. Introducing a Kotlin-specific settings block (for example, a kotlincNativeSettings that defines KOTLINC_NATIVE_PATH) would be flow-wise very similar to using cSettings, and conceptually aligned with how SwiftPM already injects toolchain-related configuration into Swift Build via the PIF, rather than Swift Build owning toolchain provisioning itself.

On “source sets” / refinements: in Kotlin Multiplatform, a source set is a named group of sources (for example commonMain, iosMain, macosMain), and “refinement” is the directed relationship that defines sharing and visibility between them (for example, iosMain refining/depending on commonMain). Gradle uses this refinement graph to derive the compilation model (which sources participate in which targets) and to synthesize the required compiler wiring automatically, so users don’t have to spell out low-level flags.

The Kotlin Multiplatform docs give an overview of source sets here:

and in more detail for the hierarchical/refinement model here:

More broadly, some form of manifest extensibility (or a constrained new DSL surface) seems like the right mechanism if SwiftPM were to model KMP concepts directly, since the goal would be to pass structured metadata rather than long, opaque compiler strings.

Thank you for the detailed explanation of the current state of plugins. We would greatly benefit from additional capabilities in Swift Package plugins (for example, producing linkable artifacts, headers, or dependency-aware outputs). If there is interest in evolving package plugins in this direction, we’d be very happy to collaborate and provide concrete Kotlin/Native use cases and requirements to help guide that work.