kotlin-native-nuget Help

Publish a Kotlin/Native library as NuGet

Configure a Kotlin Multiplatform module, package its native binaries and generated C# API, then add the resulting MyCatLib package to a .NET project.

1. Apply the plugin and build shared libraries

Apply Kotlin Multiplatform and the NuGet plugin, then give every supported native target the same shared-library baseName:

plugins { kotlin("multiplatform") id("io.github.xxfast.kotlin.native.nuget") version "<version>" } kotlin { mingwX64 { binaries { sharedLib { baseName = "mycatlib" } } } macosArm64 { binaries { sharedLib { baseName = "mycatlib" } } } }

Replace <version> with the version shown in Getting started, and keep it pinned. Only configured targets in the supported target table that this host can link are added to the package this way; a RID built on another host can still be added via prebuiltRuntimes, see One package for every platform below.

java.lang.NullPointerException at ...KlibModuleOriginKt.getKlibModuleOrigin(KlibModuleOrigin.kt:32) at ...cexport.CAdapterCodegen.buildCAdapter(CAdapterCodegen.kt:58)

Ordinary compilation succeeds; only C-export at link time fails. This is KT-62984, a Kotlin/Native backend bug triggered by plugin-generated IR that carries no klib origin, not anything specific to this plugin. Marking the annotated declarations internal does not help: the crashing IR is generated by the other plugin, not by your code.

The workaround is to exclude the offending plugin from the target's kotlinCompilerPluginClasspath<Target> configuration:

kotlin { macosArm64 { // Excludes the Koin compiler plugin's generated-code dependency from this target's // compiler-plugin classpath only; other targets are unaffected. project.configurations.named("kotlinCompilerPluginClasspathMacosArm64Main") { exclude(group = "io.insert-koin", module = "koin-compiler-plugin") } } }

2. Configure the NuGet package

Publish every public declaration below rootPackage into the corresponding C# namespace:

nuget { publish { packageId = "MyCatLib" version = "1.0.0" authors = "yourname" description = "A Kotlin/Native library for cat lovers" rootPackage = "com.example.cats" } }

For example, add this API under src/nativeMain/kotlin/com/example/cats/Cat.kt:

package com.example.cats interface Pet { val name: String fun speak(): String } enum class Mood { HAPPY, SLEEPY, GRUMPY } abstract class Animal(override val name: String) : Pet class Cat( name: String, val lives: Int = 9, ) : Animal(name) { var brother: Cat? = null var mood: Mood = Mood.SLEEPY val toys: List<Toy> = listOf(Toy("Mouse", "Gray")) val onMeow: () -> String = { "Meow! My name is $name" } override fun speak(): String = "Meow!" } data class Toy(val name: String, val color: String) class Box<T>(val item: T) fun owner(name: String): String? = if (name == "Oreo") "Isuru" else null

The root package maps to the MyCatLib C# namespace. After adding the NuGet package to a .NET project, use the generated API like this:

using MyCatLib; using var oreo = new Cat("Oreo", 9); Console.WriteLine(oreo.Name); // Oreo Console.WriteLine(oreo.Speak()); // Meow! using var mylo = new Cat("Mylo", 9); oreo.Brother = mylo; oreo.Mood = Mood.Happy;

See Publishing Kotlin to C# for the supported Kotlin constructs and their generated C# shapes. The complete package metadata options are in the nuget {} DSL reference.

3. Build and consume the package

Build the package from the project root:

./gradlew packNuget

The package is written to build/nuget/MyCatLib.1.0.0.nupkg. See Gradle tasks for the complete output layout.

Add that directory as a NuGet package source, then add a PackageReference from the C# consumer. For example, create a NuGet.Config beside the .NET solution:

<?xml version="1.0" encoding="utf-8"?> <configuration> <packageSources> <clear /> <add key="nuget.org" value="https://api.nuget.org/v3/index.json" /> <add key="mycatlib-local" value="../my-cat-lib/build/nuget" /> </packageSources> </configuration>
<ItemGroup> <PackageReference Include="MyCatLib" Version="1.0.0" /> </ItemGroup>

Adjust the relative feed path for your directory layout. Keep the package reference version in sync with publish {}. NuGet brings in the generated C# source and selects the native binary for the consumer's runtime identifier.

A framework-dependent C# build also needs a runtime identifier so MSBuild copies the matching native asset beside the host. For a cross-platform project, use the SDK host RID:

<RuntimeIdentifier>$(NETCoreSdkRuntimeIdentifier)</RuntimeIdentifier> <SelfContained>false</SelfContained>

Do not hardcode one host RID into a cross-platform test project. For deployment, select the RID of the target environment.

One package for every platform

No single host can link every RID: a macOS host links macosArm64 and cross-compiles mingwX64, but a Windows host can only link mingwX64, and neither can link the other's Apple- or Windows-only targets. prebuiltRuntimes merges another host's already-linked runtimes/ output into this host's own pack, so one host still produces one publishable package instead of each CI leg shipping a package that only covers its own platform.

A typical two-host flow: the Windows leg runs packNuget and uploads its staged runtimes/ folder as a CI artifact, then the macOS leg downloads that artifact into a local directory and points at it before running its own packNuget:

nuget { publish { packageId = "MyCatLib" version = "1.0.0" authors = "yourname" description = "A Kotlin/Native library for cat lovers" rootPackage = "com.example.cats" prebuiltRuntimes = file("build/prebuilt-runtimes") // <rid>/native/*.dll|*.dylib|*.so } }

The resulting package carries a runtimes/<rid>/native/ folder for both the locally linked RID and the prebuilt one:

MyCatLib.1.0.0.nupkg ├── runtimes/osx-arm64/native/libmycatlib.dylib (locally linked) ├── runtimes/win-x64/native/mycatlib.dll (prebuilt) ├── contentFiles/cs/any/*.cs ├── build/MyCatLib.targets └── MyCatLib.nuspec

See The nuget {} DSL for the full validation rules (an empty prebuilt directory, a RID declared both locally and prebuilt, an unknown RID name) and ADR-093 for the design rationale.

4. Iterate locally without bumping the version

Without snapshot versioning, every local Kotlin change needs a version bump, or the consumer's packNuget + delete-from-cache + dotnet restore --force --no-cache dance, because NuGet treats a version as immutable and serves the cached copy of the previous build otherwise. Add snapshot = true to publish {} to skip that entirely:

nuget { publish { packageId = "MyCatLib" version = "1.0.0" authors = "yourname" description = "A Kotlin/Native library for cat lovers" rootPackage = "com.example.cats" snapshot = true } }

Every packNuget now mints a fresh 1.0.0-snapshot.<epochMillis> identity and writes build/MyCatLibVersions.props, pinning it under a sanitized property name (see The nuget {} DSL for the naming rule):

<Project> <PropertyGroup> <MyCatLibVersion>1.0.0-snapshot.1754817000000</MyCatLibVersion> </PropertyGroup> </Project>

Import that props file from the .NET consumer, guarded with Exists() so the build still works before the first pack, then reference the package by the property instead of a literal version:

<Project> <Import Project="$(MSBuildThisFileDirectory)../my-cat-lib/build/MyCatLibVersions.props" Condition="Exists('$(MSBuildThisFileDirectory)../my-cat-lib/build/MyCatLibVersions.props')" /> </Project>
<PackageReference Include="MyCatLib" Version="$(MyCatLibVersion)" />

Adjust the relative path to the props file for your directory layout, and to a shared Directory.Build.props if the consumer has several projects. dotnet restore now picks up the new version on every build, no cache clearing required. Successive snapshot packs accumulate .nupkg files in build/nuget; this is harmless, and clean removes them.

5. Publish the package to a feed

The local package source above is useful while developing and testing the library. To distribute MyCatLib, publish the generated package to NuGet.org or a private NuGet feed instead.

packNuget creates the .nupkg, but does not upload it. With the .NET SDK installed and credentials for the destination feed, push the package with dotnet nuget push:

dotnet nuget push build/nuget/MyCatLib.1.0.0.nupkg \ --source https://api.nuget.org/v3/index.json \ --api-key <NUGET_API_KEY>

For a private feed, replace --source with that feed's endpoint and use credentials accepted by the feed. Consumers then reference MyCatLib with the same normal PackageReference shown above and restore it from the published feed. They do not need the mycatlib-local entry in their NuGet.Config.

Last modified: 19 September 2026