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 configure a shared library for each target. The plugin derives the native library name from packageId:
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.
The plugin derives a unique native library name from packageId, so separate Kotlin-built packages can be loaded in one .NET project. MobileEvidence.Kotlin ships kn_mobileevidence_kotlin.dll: kn_ plus the lowercased id, with ., - and _ written as _. Leave baseName unset; a baseName that differs from the derived name fails the build. Prebuilt files must use the derived name, and each prebuilt RID input must contain exactly that package’s expected primary native library. Package ids that differ only in their separators share a native name and cannot be loaded together.
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:
2. Configure the NuGet package
Publish every public declaration below rootPackage into the corresponding C# namespace:
For example, add this API under src/nativeMain/kotlin/com/example/cats/Cat.kt:
The root package maps to the MyCatLib C# namespace. After adding the NuGet package to a .NET project, use the generated API like this:
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:
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:
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:
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:
The resulting package carries both native libraries and producer contract sidecars for each RID. packNuget checks that all targets agree on the native contract and generated C# API before it writes the package. If a target differs, packing reports the affected RIDs and ABI or C# token differences; align the declarations or publish those targets separately. Comparison is conservative: comments and whitespace outside literals are ignored, but generated declaration order and implementation tokens must match, so equivalent APIs with reordered generated code can be rejected.
Upload and download the complete staged runtimes/ tree, including the two sidecars beside each RID's native/ directory. Binary-only prebuilt runtimes from older plugin versions must be rebuilt by their original producer. See The nuget {} DSL for the full validation rules 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:
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):
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:
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, push the generated package to NuGet.org, GitHub Packages, or a private feed with the publishNuget task. It pushes over plain HTTP, so no .NET SDK is required for this step either.
Declare a named repository inside publish { }:
Each repository gets its own publishNugetTo<Name>Repository task (publishNugetToNugetOrgRepository, publishNugetToGithubRepository, both depending on packNuget), plus an aggregate publishNuget that runs all of them:
An unset apiKey, username, or password on a repository named "nugetOrg" resolves from the Gradle properties nugetOrgApiKey, nugetOrgUsername, nugetOrgPassword when the task runs, so a credential never has to sit in the build file:
username/password send HTTP basic auth alongside the key header, which is what GitHub Packages and Azure Artifacts PATs expect; leave them unset for a plain API-key feed like nuget.org.
Two task options:
--dryRunresolves the feed and validates the package and credential without pushing anything.--skipDuplicateturns an already-published version (HTTP 409) into a warning instead of a build failure; it is also settable per repository withskipDuplicate = true.
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.
The shared contract package
Every package the plugin builds depends on Xxfast.Kotlin.Native.Interop, a small assembly holding KotlinException, the mapped exception types and KotlinOptional<T> (see Exceptions). It is released with the plugin, at the plugin's version: a package built by plugin 0.9.0 declares it as [0.9.0,1.0.0).
Consumers do not reference it. NuGet restores it from nuget.org alongside your package, so a consumer whose NuGet.Config lists nuget.org needs nothing extra.
A consumer that restores only from a private or offline feed must find it there too. Mirror Xxfast.Kotlin.Native.Interop into that feed, at the plugin version your package was built with or a newer one below the next major. It is published to nuget.org and mirrored to GitHub Packages at https://nuget.pkg.github.com/xxfast/index.json, which needs a token even to read.
The pre-pack compile check in packNuget restores it as well, from nuget.org and any feed declared with sources(...).