kotlin-native-nuget Help

The nuget {} DSL

Reference for the nuget {} extension.

nuget { }

Member

Configures

Required

publish { }

NugetPublishConfig

optional, omit for a consume-only project

dependencies { }

NugetDependencyScope

optional, omit for a publish-only project

targetFramework

Property<String>

no, default net10.0

sources(...)

ListProperty<String>

no, empty; feeds every dependency restores from, see Declaring dependencies

The targetFramework is the .NET target framework the generated package targets. It must be exactly netX.0 with X of 10 or higher (the generated C# needs C# 14), and anything else fails the build. The package ships its C# under contentFiles/cs/<tfm>/, so a consumer project on a lower framework fails restore with NU1202 instead of building without the generated code. The same value is used for the restore of bound dependencies and the pre-pack compile check. Platform frameworks such as net10.0-android, netstandard and .NET Framework are not supported.

nuget { targetFramework = "net10.0" }

A project can declare either block, both, or neither meaningfully. publish {} alone publishes a package with no reverse bindings; dependencies {} alone binds C# packages into Kotlin without publishing anything. See Gradle tasks for exactly which tasks each combination registers.

Every setting is a Gradle Property, ListProperty, RegularFileProperty or DirectoryProperty, so you can feed one from a Provider and the build stays configuration-cache friendly. In a Kotlin build script you still write plain = assignment (Gradle 8.2 or later, verified on 9.1.0). A nested block cannot call a function from an enclosing block: include("x") inside repositories { nuget("feed") { } } is a compile error instead of silently widening publish.

Repeating a block merges instead of replacing. These two publish {} blocks produce one configuration with both include entries, and the same holds for bind {}, dependency("X") and nuget("x"):

nuget { publish { include("com.example.a") } publish { include("com.example.b") } }

Breaking in 0.9.0

Scripts that only assign values and call the block functions need no change. These differences matter if you read the DSL back or depend on task shape:

  • Plugin internals are no longer public: build scripts that imported the rir model, the generator functions (generateKotlinStubs, generateCSharpShims, generateCsproj) or NugetPusher no longer compile. The DSL and the task classes stay public.

  • The package targets net10.0 by default, up from a hardcoded net8.0, and targetFramework now sets it. A consumer project below net10.0 fails restore with NU1202.

  • Reading a value returns a Provider, so nuget.publish?.packageId becomes nuget.publish.packageId (a Property<String>). nuget.publish is never null; test whether a value was set with packageId.isPresent.

  • The checks that used to fail while the build was configured now fail when the task runs, with the same message: a repository with no url, snapshot = true without a version or packageId, and a project with no supported targets.

  • packNuget exists on every project with a publish {} block and fails at execution when there is nothing to pack. A project without publish {} still has no packNuget.

  • nugetSnapshotVersion and nugetSnapshotVersionProps exist on every publish {} project. packNuget depends on them only when snapshot is true.

  • Repositories and dependencies are processed in name order, not declaration order.

  • publishNuget depends on every push task in the project.

  • A second publish {}, bind {}, dependency("X") or nuget("x") used to replace the first or register a duplicate (a duplicate nuget("x") failed the build). It now merges.

Renamed so a name says what it selects or does. There are no aliases: a script or CI step using an old task name fails with "task not found".

Before

Now

bind { include(...) }, bind { exclude(...) }

includeNamespaces(...), excludeNamespaces(...). The old functions were deprecated at ERROR level in 0.9.0 and removed in 0.10.0. publish { include/exclude } is unchanged: it selects Kotlin packages.

nugetGen

nugetGenerateRestoreProject

NugetGenTask

NugetGenerateRestoreProjectTask

Tasks that pack or publish are verb-first (packNuget, publishNuget) and steps that drive the .NET tool start with nuget, as in Kotlin's Gradle plugin; see Gradle tasks.

Groovy build scripts can use the same blocks, but are not tested.

Annotations dependency

Applying the plugin adds nuget-annotations, which carries @CSharpName, to your commonMainApi (published as io.github.xxfast:nuget-annotations). It's a common module, so JVM, JS and native targets all resolve it; you add nothing yourself. See Choosing the C# name.

Only the module that applies the plugin gets the dependency. To annotate a declaration in a dependency module that doesn't apply it, which the cross-module export closure can still bridge, apply the annotations-only plugin there. It adds nuget-annotations to commonMainApi at the plugin's own version and does nothing else: no nuget {} block, no code generation.

// build.gradle.kts of the dependency module plugins { kotlin("multiplatform") id("io.github.xxfast.kotlin.native.nuget.annotations") version "<version>" }

Use the same <version> as the main plugin, as in Getting started. Declaring api("io.github.xxfast:nuget-annotations:<version>") yourself works too, but you then keep the version in step by hand.

publish { }

Configures NugetPublishConfig. The five string settings have no default. Nothing in the DSL itself enforces they're set, but packNuget fails once it reads an unset one.

Property

Type

Required

Maps to

packageId

Property<String>

yes

.nuspec <id>, and the .nupkg file name. When null or blank (or with no publish block) the generated C# is rooted at namespace Interop, and packNuget fails with "needs a non-blank package id"

version

Property<String>

yes

.nuspec <version>

authors

Property<String>

yes

.nuspec <authors>

description

Property<String>

yes

.nuspec <description>

rootPackage

Property<String>

yes

the Kotlin package the generated C# namespaces are rooted at; sub-packages map relative to it. Also the default export scope: see below

include(vararg packages: String)

function, adds to a ListProperty

no

empty; when set, only these package prefixes (and their sub-packages) are bridged

exclude(vararg packages: String)

function, adds to a ListProperty

no

empty; a package prefix or a qualified declaration name (a class, object, sealed base, or top-level function, plus everything nested under it); applied after include, and always wins over it

admit(vararg types: String)

function, adds to a ListProperty

no

empty; additively admits a dependency-module type into the cross-module export closure by qualified name or package prefix, without touching include/rootPackage; see below

strictDependencyTypes

Property<Boolean>

no

false; when true, an un-admitted dependency type in a public signature fails the build (ERROR_UNEXPORTED_DEPENDENCY_TYPE) instead of warning; see below

strictCompileCheck

Property<Boolean>

no

false; when true, a missing or unusable dotnet fails check and packNuget instead of skipping the pre-pack compile check with a warning; the Gradle property nuget.strictCompileCheck is the default, and this value wins; see Gradle tasks

snapshot

Property<Boolean>

no

false; when true, packNuget mints <version>-snapshot.<epochMillis> at execution time instead of using version literally, and always writes an MSBuild props file. Requires packageId and a non-blank version

versionPropsFile

RegularFileProperty

no

only consulted when snapshot is true. Default <rootProject>/build/<packageId>Versions.props

prebuiltRuntimes

DirectoryProperty

no

unset; the complete runtimes/ tree staged by another host, including each RID's native/ library, ForwardAbi.json, and Interop.cs; merged with this host's linked RIDs

repositories { }

function

no

empty; declares named feeds publishNuget pushes the packed .nupkg to, see below

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

Snapshot versioning

With snapshot = true, every packNuget run mints a fresh, immutable version instead of reusing version as-is, so a .NET consumer's next restore always sees a new version and never serves NuGet's cached copy of the previous build:

nuget { publish { // packageId, version, authors, description, rootPackage as above snapshot = true } }

The minted version (1.0.0-snapshot.<epochMillis>) is pinned in an MSBuild props file under a property name derived from packageId: every character outside [A-Za-z0-9_] is dropped, a leading digit gets a _ prefix, and Version is appended (MyCatLib becomes MyCatLibVersion). See Publish a Kotlin/Native library as NuGet for the full local-iteration flow and the consumer-side props import.

Publishing repositories

repositories { } declares named feeds inside publish { }; each nuget(name) { } block gets its own publishNugetTo<Name>Repository task:

nuget { publish { // packageId, version, authors, description, rootPackage as above repositories { nuget("nugetOrg") { url = "https://api.nuget.org/v3/index.json" // apiKey unset: falls back to the `nugetOrgApiKey` Gradle property } } } }

See Publish a Kotlin/Native library as NuGet for credential resolution, --dryRun, and --skipDuplicate.

Multi-RID packages

packNuget only links the native targets this host's Kotlin/Native toolchain can build, but a package worth publishing needs every RID your CI matrix produces. prebuiltRuntimes points at a directory built by another host and merges it into this host's own pack, so one packNuget run still produces one package covering both:

nuget { publish { packageId = "MyCatLib" version = "1.0.0" prebuiltRuntimes = file("build/prebuilt-runtimes") // <rid>/native/*.dll|*.dylib|*.so } }

A typical two-host CI flow: a Windows leg runs packNuget and uploads its complete staged runtimes/ folder as an artifact, then a macOS leg downloads it, points prebuiltRuntimes at it, and runs packNuget itself, producing one package with both RIDs. Keep the sidecars alongside each RID's native/ folder when transferring the artifact; they let the packing host compare each producer's API and native contract with the package's single generated C# API and contract.

packNuget validates the merge rather than silently dropping anything:

  • A locally linked RID with no .dll/.dylib/.so to copy fails the build naming the RID and the directory scanned.

  • A prebuiltRuntimes directory with no RID subdirectories, or a RID subdirectory whose native/ folder is missing or empty, fails naming the expected layout (<prebuiltRuntimes>/<rid>/native/*.dll|*.dylib|*.so).

  • Every RID must also include the producer's ForwardAbi.json and Interop.cs beside native/. Missing or invalid sidecars fail packing. Binary-only artifacts from older plugin versions must be rebuilt by their original producer and transferred as a complete runtimes/ tree.

  • packNuget compares each RID's native contract and generated C# source with one baseline before writing the package. A mismatch fails with the differing ABI symbols or C# token context; align the exported declarations or publish the incompatible targets in separate packages.

  • The same RID arriving both locally linked and prebuilt fails naming both sources: pick one producer per RID.

  • A prebuilt RID name this plugin version does not know how to build is a warning only, since the RID set NuGet accepts is open and the tree may come from a newer plugin.

Export scoping

By default NugetProcessor bridges every public declaration in the module. include/exclude scope that down: a package prefix match (pkg == p || pkg.startsWith("$p.")), with exclude always winning over include. exclude additionally matches a qualified declaration name and everything nested under it: exclude("com.contoso.api.Shape") drops the Shape class (and, for a sealed base, its subclasses) from the export set, so a member that references it is skipped with a named diagnostic and the rest of the package still bridges. include stays package-level.

nuget { publish { packageId = "Contoso.Api" rootPackage = "com.contoso.api" include("com.contoso.api") // whitelist of package prefixes exclude("com.contoso.api.internal") // exclude wins exclude("com.contoso.api.Legacy") // one type, by qualified name } }

When include is empty, the effective include set defaults to rootPackage if set, otherwise to everything. This makes rootPackage a scoping knob, not just a renaming one: a public declaration outside rootPackage is not bridged unless named explicitly in include. A scope that admits none of the module's public declarations (for example a stray include("kotlin")) warns once with SKIPPED_ALL_DECLARATIONS instead of silently shipping a package with no Interop.cs.

A package that is only reached via dependencies { dependency(...) { bind { } } } (the reverse stub packages generated for a consumed C# dependency) is always exported regardless of include/exclude, since a module that both publishes forward and consumes a NuGet dependency needs those bound types reachable from its own forward return types to keep compiling.

Cross-module export closure

include/exclude/rootPackage/admit decide what crosses a Gradle module boundary. The export set is a reachability closure from the module's own admitted declarations: the processor walks return types, parameter types, property types, type arguments of an admitted carrier (Flow<T>, List<T>/Set<T>/Map<K,V>), sealed subclasses, and primary-constructor parameter types, and admits every discovered declaration through the same predicate: include/rootPackage for a whole package, or an additive admit entry for one type or package, minus anything exclude names, whether it lives in this module or in a dependency module pulled in with implementation(project(":models")). A :models module under the same rootPackage is admitted automatically, with no extra config.

nuget { publish { packageId = "TestLibrary" rootPackage = "io.github.xxfast.kotlin.native.nuget.test" // io.github.xxfast.kotlin.native.nuget.test.models is under rootPackage: admitted with no // extra config. dev.other.core is not: reachable types from it are skipped unless named here. include("dev.other.core") } }

include(...) widens admission by whole package, which is right when your own API genuinely reaches most of a dependency's models. For a single third-party type (a ktor Url, a kermit Severity) that would mean owning and versioning a whole package you don't control just to reach one class or enum. admit(vararg types: String) is the finer-grained, additive alternative: it takes a qualified type name or a package prefix, the same matcher exclude uses, adds entries to the closure's admission predicate only, and never picks roots or replaces the rootPackage default the way an explicit include does.

nuget { publish { packageId = "TestLibrary" rootPackage = "io.github.xxfast.kotlin.native.nuget.test" // admit ONE type by name: only Url itself is admitted, not io.ktor.http. admit("io.ktor.http.Url") // admit a whole package by prefix: the other arm of the same matcher. admit("co.touchlab.kermit") } }

The types can come straight from published Maven klibs. With the block above and a Kotlin class that exposes them (fun parse(address: String): Url, fun nameOf(severity: Severity): String), C# gets Severity as a plain enum and Url as a disposable handle class:

using var postbox = new Postbox("Oreo"); using Url url = postbox.Parse("https://example.com:8443/cats/oreo"); Console.WriteLine(url.Host); // example.com Console.WriteLine(postbox.NameOf(Severity.Warn)); // Warn

Severity lives under TestLibrary.Co.Touchlab.Kermit and Url under TestLibrary.Io.Ktor.Http, the dependency's package PascalCased below your packageId. Url has no public constructor, so create it in Kotlin and hand it over. Its String, Int and List<string> members (Host, Port, EncodedPath) bind; members typed with ktor types you didn't admit, such as Protocol and Parameters, are skipped with a hint to admit them.

Admitting a type admits only that declaration, not its package: a sibling type or a member typed with something you didn't also admit still skips named, with a hint that says add admit("<qualified type>"), never include(...) (an additive verb has no replacement trap to word around). exclude still wins over admit the same way it wins over include. Use include to widen a whole dependency package your own API is built around; use admit to pull in one or a few specific types from a package you otherwise leave alone; use exclude to record either omission as deliberate.

Admitting a nested type (admit("dep.edge.Ledger.Entry")) doesn't work: the closure climbs from a nested declaration to its owner before admitting anything, so that entry refuses Ledger and the refusal propagates onto Entry. Admit the outermost type instead (admit("dep.edge.Ledger")), which also declares Entry as its nested type once Ledger is admitted. exclude, by contrast, is tested on the declaration itself before that climb, so it still takes the full nested name (exclude("dep.edge.Ledger.Entry")).

By default an un-admitted dependency type is a warning plus a named skip (below). Set strictDependencyTypes = true to fail the build instead, once every dependency type reached by a public signature is either admitted or excluded by name:

nuget { publish { rootPackage = "com.example.sdk" admit("io.ktor.http.Url") strictDependencyTypes = true } }

Under strictDependencyTypes, a type the closure refused because it was never admitted, or because admit/include/rootPackage are all unset so the closure never crosses the module boundary at all, becomes ERROR_UNEXPORTED_DEPENDENCY_TYPE and stops the build. A type refused because you excluded it stays a warning: that omission is already deliberate, so escalating it would make strict mode unsatisfiable for any dependency the build genuinely amputates.

A dependency-module type the closure refuses to admit is skipped, and the diagnostic names the actual reason (not admitted, excluded, cross-module admission off entirely, or an expect declaration whose actualization lives in the dependency and can't be brought into scope at all) with an actionable fix for each. See Publishing Kotlin to C# for the full set of messages.

A dependency type nested inside another declaration (Broadcast.Schedule) is declared nested under its owner in the generated C#, exactly like a module-local nested type, once the owner (Broadcast) is admitted. A member naming only the nested type still admits the owner (climbing the chain first), and a declared nested type's own member types are walked too, so Broadcast.Schedule.timetable(): Timetable admits the top-level dependency type Timetable on the strength of a member declared two levels down. A dependency type nested under a still-deferred owner shape (an enum class or a generic interface) is refused admission outright, the same as a module-local one under the same deferred shape; see Classes and objects: Nested types.

Every cross-namespace type reference in the generated Interop.cs is global::Namespace.Name qualified, so the file compiles with no using needed regardless of how many packages a namespace-crossing member touches:

public Issue41Bundle(IReadOnlyList<global::TestLibrary.Issue41.Issue41Thing> things, global::TestLibrary.Issue41.Issue41Thing one)

dependencies { }

Configures a NugetDependencyScope, whose only member is dependency(...).

dependency(id, version = null) { }

Parameter

Type

Required

id

String

yes, the NuGet package id

version

String?

no, omit for an unpinned <PackageReference Include="$id" />

Inside the trailing block, NugetDependency exposes:

Property / function

Type

Required

Notes

version

Property<String>

no

same as the version parameter; settable inside the block instead of passing it positionally

source

Property<String>

no

where to restore from: a feed URL (added to <RestoreSources> alongside api.nuget.org) (an extra feed, not a replacement for the shared list), a directory of .nupkg files (needs version), or one .nupkg file (id and version read from the package; a differing version fails). A same-version rebuild of a local source is picked up, and the build fails if the restored package is not the local one. See Declaring dependencies

bind { }

function, configures NugetBindConfig

no

omit to resolve the dependency without generating any Kotlin bindings for it

nuget { dependencies { dependency("TestDependency", version = "1.0.0") { source = "https://my.private.feed/v3/index.json" bind { /* ... */ } } } }

bind { }

Configures NugetBindConfig. Declaring bind {} at all is what triggers nugetExtractApi, nugetGenerateBindings, and nugetGenerateShims for that dependency. See Gradle tasks.

Property / function

Type

Required

Default

packageName

Property<String>

no

the dependency id, lowercased with - replaced by _ (e.g. TestDependency becomes testdependency)

includeNamespaces(vararg namespace: String)

function

no

empty, with no includeNamespaces at all, every namespace in the package is considered, subject to excludeNamespaces

excludeNamespaces(vararg namespace: String)

function

no

empty

alias(csharpNamespace, kotlinPackage)

function

no

none

includeNamespaces/excludeNamespaces match a C# namespace exactly, or any of its sub-namespaces (ns == filter or ns.startsWith("$filter.")); when both match the same namespace, excludeNamespaces wins. alias maps one C# namespace to a Kotlin package, overriding both packageName and the id-derived default for that namespace only. Unlike the filters, alias is exact: an included sub-namespace such as Test.Text.Internal needs its own alias line or it lands in the packageName package. Several namespaces may alias to the same Kotlin package.

dependency("TestDependency", version = "1.0.0") { bind { includeNamespaces("Test.Text") excludeNamespaces("Test.Text.Internal") alias("Test.Text", "sample.text") } }

A second bind { } block for the same dependency merges into the first: namespace lists and aliases accumulate, and packageName is last-wins. To put namespace groups in different Kotlin packages, use one alias per namespace rather than one block per group. For what actually gets bound once a namespace is included, see Consuming C# in Kotlin.

Last modified: 03 November 2026