Publish siGit Code to NuGet as a .NET tool

Adds a sixth release channel: `dotnet tool install --global SiGit.Code`. A .NET tool is a single package rather than one artifact per platform, so nuget/sigit/ bundles all six native binaries under native/<os>-<arch>/ and a small managed shim execs the right one. This is the pattern smbcloud-cli and onde-cli already use. The shim leaves stdin, stdout, and stderr unredirected so sigit's TTY check still picks TUI or ACP mode correctly. Two things worth knowing about it: The shim sets RollForward=Major. It targets net8.0, and without that a machine carrying only the .NET 10 runtime installs a tool that refuses to start with "You must install or update .NET to run this application". Bundling every target makes the package large, so the pack job fails if the .nupkg crosses nuget.org's 250 MB limit. At v1.5.1 it lands near 160 MB. If that check ever trips, split into RID-specific tool packages instead of dropping targets. Also sets strip = "symbols" on the release profile. That takes the macOS arm64 binary from 102 MB to 88 MB and shrinks every channel, not just this one.

paydii committed Jul 28, 2026 at 08:14 UTC 06851432e1daec2458d1ad36263c0fc1673290b9
8 files changed +553 -3
.agents/AGENTS.md
+19 -3
@@ -249,6 +249,22 @@ mutating tools; the escape hatch for clients without permission-request support)
249
250 ## Releasing
251
252 -Version lives in `Cargo.toml`. The binary is published to five registries via separate workflows
253 -(`release-crates`, `release-github`, `release-homebrew`, `release-npm`, `release-pypi`); the
254 -`npm/` and `pypi/` dirs hold the wrapper-package templates. Update `CHANGELOG.md` for releases.
252 +Version lives in `Cargo.toml`. The binary is published to six registries via separate workflows
253 +(`release-crates`, `release-github`, `release-homebrew`, `release-npm`, `release-nuget`,
254 +`release-pypi`); the `npm/`, `nuget/`, and `pypi/` dirs hold the wrapper-package templates. Update
255 +`CHANGELOG.md` for releases.
256 +
257 +`[profile.release]` sets `strip = "symbols"` because binary size is a distribution constraint, not
258 +just a nicety — see the NuGet note below.
259 +
260 +The NuGet package (`SiGit.Code`, installed with `dotnet tool install --global SiGit.Code`) is the
261 +odd one out: npm and PyPI publish one artifact per platform, but a .NET tool is a single package,
262 +so `nuget/sigit/` bundles all six binaries under `native/<os>-<arch>/` and a small managed shim
263 +(`Program.cs`) execs the right one. That shim leaves stdin/stdout/stderr unredirected on purpose,
264 +since siGit Code chooses TUI or ACP mode by testing whether stdin is a TTY.
265 +
266 +Bundling every target means the package is large, so `release-nuget.yml` fails the pack job if the
267 +`.nupkg` crosses nuget.org's 250 MB limit. At v1.5.1 it lands around 160 MB. If a future release
268 +trips that check, the fix is not to drop targets but to split into RID-specific tool packages
269 +(.NET 10's `DotnetToolRidPackage`), which ship one binary per platform the way npm already does —
270 +that also needs a fallback package for pre-.NET-10 SDKs.
.github/workflows/release-nuget.yml new
+300
@@ -0,0 +1,300 @@
1 +name: NuGet Release
2 +
3 +on:
4 + push:
5 + tags:
6 + - "v*.*.*"
7 + workflow_dispatch:
8 + inputs:
9 + tag:
10 + description: "Release tag (e.g. v1.5.2)"
11 + required: true
12 +
13 +permissions:
14 + contents: read
15 +
16 +env:
17 + CARGO_TERM_COLOR: always
18 + DOTNET_VERSION: 8.0.x
19 + NUGET_PACKAGE_ID: SiGit.Code
20 +
21 +jobs:
22 + build-native-binaries:
23 + name: Build native binary (${{ matrix.build.NAME }})
24 + runs-on: ${{ matrix.build.OS }}
25 + strategy:
26 + fail-fast: false
27 + matrix:
28 + build:
29 + - {
30 + NAME: linux-x64,
31 + OS: ubuntu-latest,
32 + TARGET: x86_64-unknown-linux-gnu,
33 + }
34 + - {
35 + NAME: linux-arm64,
36 + OS: ubuntu-24.04-arm,
37 + TARGET: aarch64-unknown-linux-gnu,
38 + }
39 + - {
40 + NAME: windows-x64,
41 + OS: windows-2022,
42 + TARGET: x86_64-pc-windows-msvc,
43 + }
44 + - {
45 + NAME: windows-arm64,
46 + OS: windows-2022,
47 + TARGET: aarch64-pc-windows-msvc,
48 + }
49 + - { NAME: darwin-x64, OS: macos-26, TARGET: x86_64-apple-darwin }
50 + - { NAME: darwin-arm64, OS: macos-26, TARGET: aarch64-apple-darwin }
51 + steps:
52 + - name: Checkout
53 + uses: actions/checkout@v6
54 + with:
55 + ref: ${{ github.event.inputs.tag || github.ref }}
56 +
57 + - name: Read Rust toolchain
58 + shell: bash
59 + run: |
60 + rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
61 + if [ -z "$rust_toolchain" ]; then
62 + echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
63 + exit 1
64 + fi
65 + echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
66 +
67 + - name: Install Rust toolchain
68 + uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
69 + with:
70 + toolchain: ${{ env.RUST_TOOLCHAIN }}
71 +
72 + - name: Install Rust target
73 + shell: bash
74 + run: |
75 + rustup target add ${{ matrix.build.TARGET }} --toolchain ${{ env.RUST_TOOLCHAIN }}
76 + rustup target list --installed
77 +
78 + - name: Setup Rust cache
79 + uses: Swatinem/rust-cache@v2
80 + with:
81 + key: nuget-${{ matrix.build.TARGET }}
82 +
83 + - name: Build binary
84 + shell: bash
85 + run: cargo build --locked --release --target ${{ matrix.build.TARGET }}
86 +
87 + - name: Prepare artifact
88 + shell: bash
89 + run: |
90 + executable_name="sigit"
91 + if [[ "${{ matrix.build.OS }}" == "windows-2022" ]]; then
92 + executable_name="sigit.exe"
93 + fi
94 +
95 + mkdir -p "nuget-artifacts/native/${{ matrix.build.NAME }}"
96 + cp "target/${{ matrix.build.TARGET }}/release/${executable_name}" \
97 + "nuget-artifacts/native/${{ matrix.build.NAME }}/${executable_name}"
98 +
99 + - name: Upload native artifact
100 + uses: actions/upload-artifact@v4
101 + with:
102 + name: nuget-native-${{ matrix.build.NAME }}
103 + path: nuget-artifacts/native
104 +
105 + pack-nuget:
106 + name: Pack NuGet .NET tool
107 + runs-on: ubuntu-latest
108 + needs: build-native-binaries
109 + steps:
110 + - name: Checkout
111 + uses: actions/checkout@v6
112 + with:
113 + ref: ${{ github.event.inputs.tag || github.ref }}
114 +
115 + - name: Set the release version
116 + shell: bash
117 + run: |
118 + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
119 + release_version="${GITHUB_REF_NAME#v}"
120 + else
121 + release_version="${{ github.event.inputs.tag }}"
122 + release_version="${release_version#v}"
123 + fi
124 + echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
125 +
126 + - name: Download native artifacts
127 + uses: actions/download-artifact@v4
128 + with:
129 + pattern: nuget-native-*
130 + path: nuget/sigit/native
131 + merge-multiple: true
132 +
133 + - name: Setup .NET SDK
134 + uses: actions/setup-dotnet@v4
135 + with:
136 + dotnet-version: ${{ env.DOTNET_VERSION }}
137 +
138 + - name: Pack .NET tool
139 + shell: bash
140 + run: |
141 + dotnet pack nuget/sigit/SiGit.Code.csproj \
142 + --configuration Release \
143 + --output nuget/dist \
144 + -p:PackageVersion=${RELEASE_VERSION}
145 +
146 + # All six binaries ship in one package, so this sits closer to nuget.org's
147 + # 250 MB ceiling than the single-platform npm and PyPI artifacts do. Fail
148 + # here with a clear message rather than at `dotnet nuget push`, where the
149 + # error is just "the package file exceeds the size limit".
150 + - name: Check package size against the NuGet limit
151 + shell: bash
152 + run: |
153 + package="$(ls nuget/dist/*.nupkg | head -n 1)"
154 + size_bytes="$(wc -c < "${package}")"
155 + limit_bytes=$((250 * 1024 * 1024))
156 +
157 + echo "Package: ${package}"
158 + echo "Size: $((size_bytes / 1024 / 1024)) MiB"
159 +
160 + if [ "${size_bytes}" -gt "${limit_bytes}" ]; then
161 + echo "::error::${package} is over nuget.org's 250 MB package limit. Split the tool into RID-specific packages (.NET 10 DotnetToolRidPackage) instead of bundling every target." >&2
162 + exit 1
163 + fi
164 +
165 + - name: Upload package artifact
166 + uses: actions/upload-artifact@v4
167 + with:
168 + name: nuget-package
169 + path: nuget/dist/*.nupkg
170 +
171 + smoke-test:
172 + name: Smoke test (${{ matrix.os }})
173 + runs-on: ${{ matrix.os }}
174 + needs: pack-nuget
175 + strategy:
176 + fail-fast: false
177 + matrix:
178 + os:
179 + - ubuntu-latest
180 + - windows-2022
181 + - macos-26
182 + steps:
183 + - name: Checkout
184 + uses: actions/checkout@v6
185 + with:
186 + ref: ${{ github.event.inputs.tag || github.ref }}
187 +
188 + - name: Set the release version
189 + shell: bash
190 + run: |
191 + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
192 + release_version="${GITHUB_REF_NAME#v}"
193 + else
194 + release_version="${{ github.event.inputs.tag }}"
195 + release_version="${release_version#v}"
196 + fi
197 + echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
198 +
199 + - name: Download package artifact
200 + uses: actions/download-artifact@v4
201 + with:
202 + name: nuget-package
203 + path: nuget/dist
204 +
205 + - name: Setup .NET SDK
206 + uses: actions/setup-dotnet@v4
207 + with:
208 + dotnet-version: ${{ env.DOTNET_VERSION }}
209 +
210 + - name: Install tool from local package
211 + shell: pwsh
212 + run: |
213 + $packageSource = (Resolve-Path "nuget/dist").Path
214 + @"
215 + <?xml version="1.0" encoding="utf-8"?>
216 + <configuration>
217 + <packageSources>
218 + <clear />
219 + <add key="local" value="$packageSource" />
220 + </packageSources>
221 + </configuration>
222 + "@ | Set-Content -Path "nuget/NuGet.Config"
223 +
224 + dotnet tool install `
225 + --tool-path ".tool" `
226 + --configfile "nuget/NuGet.Config" `
227 + --version "$env:RELEASE_VERSION" `
228 + "$env:NUGET_PACKAGE_ID"
229 +
230 + - name: Verify bundled native binary
231 + shell: pwsh
232 + run: |
233 + $runtimeIdentifier = if ($IsWindows) {
234 + if ([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { "windows-arm64" } else { "windows-x64" }
235 + } elseif ($IsMacOS) {
236 + if ([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { "darwin-arm64" } else { "darwin-x64" }
237 + } else {
238 + if ([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { "linux-arm64" } else { "linux-x64" }
239 + }
240 +
241 + $binaryName = if ($IsWindows) { "sigit.exe" } else { "sigit" }
242 + $nativeBinary = Get-ChildItem ".tool/.store" -Recurse -File | Where-Object {
243 + $_.FullName -like "*native*${runtimeIdentifier}*${binaryName}"
244 + } | Select-Object -First 1
245 +
246 + if (-not $nativeBinary) {
247 + Write-Error "The sigit native binary for ${runtimeIdentifier} was not bundled in the installed .NET tool package."
248 + }
249 +
250 + Write-Host "Found bundled native binary at $($nativeBinary.FullName)"
251 +
252 + publish-nuget:
253 + name: Publish to NuGet
254 + runs-on: ubuntu-latest
255 + needs: smoke-test
256 + steps:
257 + - name: Set the release version
258 + shell: bash
259 + run: |
260 + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
261 + release_version="${GITHUB_REF_NAME#v}"
262 + else
263 + release_version="${{ github.event.inputs.tag }}"
264 + release_version="${release_version#v}"
265 + fi
266 + echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
267 +
268 + - name: Download package artifact
269 + uses: actions/download-artifact@v4
270 + with:
271 + name: nuget-package
272 + path: nuget/dist
273 +
274 + - name: Setup .NET SDK
275 + uses: actions/setup-dotnet@v4
276 + with:
277 + dotnet-version: ${{ env.DOTNET_VERSION }}
278 +
279 + - name: Check whether release already exists on NuGet
280 + id: nuget-check
281 + shell: bash
282 + run: |
283 + package_index_url="https://api.nuget.org/v3-flatcontainer/sigit.code/index.json"
284 + if curl -fsS "${package_index_url}" | grep -F "\"${RELEASE_VERSION}\"" >/dev/null; then
285 + echo "${NUGET_PACKAGE_ID} ${RELEASE_VERSION} already exists on NuGet, skipping publish"
286 + echo "exists=true" >> "$GITHUB_OUTPUT"
287 + else
288 + echo "exists=false" >> "$GITHUB_OUTPUT"
289 + fi
290 +
291 + - name: Publish package
292 + if: steps.nuget-check.outputs.exists != 'true'
293 + env:
294 + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
295 + shell: bash
296 + run: |
297 + dotnet nuget push nuget/dist/*.nupkg \
298 + --api-key "${NUGET_API_KEY}" \
299 + --source https://api.nuget.org/v3/index.json \
300 + --skip-duplicate
Cargo.toml
+8
@@ -54,3 +54,11 @@ regex = "1"
54 reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls", "stream"] }
55 uuid = { version = "1", features = ["v4"] }
56 rpassword = "7"
57 +
58 +# Release binaries ship to five registries, and the NuGet package bundles all
59 +# six targets in a single archive, so binary size is a distribution constraint
60 +# rather than a nicety. Stripping symbols takes the macOS arm64 binary from
61 +# ~102 MB to ~88 MB. The Homebrew tarball already stripped by hand in
62 +# release-github.yml; this makes every channel match.
63 +[profile.release]
64 +strip = "symbols"
README.md
+1
@@ -37,6 +37,7 @@ cargo install sigit
37 | pip | `pip install sigit-code` |
38 | uv | `uvx --from sigit-code sigit` |
39 | npm | `npm install -g @smbcloud/sigit` |
40 +| NuGet | `dotnet tool install --global SiGit.Code` |
41
42 ## First run
43
nuget/.gitignore new
+5
@@ -0,0 +1,5 @@
1 +bin/
2 +obj/
3 +dist/
4 +native/
5 +NuGet.Config
nuget/sigit/Program.cs new
+114
@@ -0,0 +1,114 @@
1 +using System.ComponentModel;
2 +using System.Diagnostics;
3 +using System.Runtime.InteropServices;
4 +
5 +internal static class Program
6 +{
7 + private const string CommandName = "sigit";
8 +
9 + public static async Task<int> Main(string[] arguments)
10 + {
11 + try
12 + {
13 + string executablePath = ResolveExecutablePath();
14 + EnsureExecutablePermissions(executablePath);
15 + return await RunAsync(executablePath, arguments);
16 + }
17 + catch (Exception exception) when (
18 + exception is FileNotFoundException or
19 + PlatformNotSupportedException or
20 + Win32Exception)
21 + {
22 + Console.Error.WriteLine($"{CommandName}: {exception.Message}");
23 + return 1;
24 + }
25 + }
26 +
27 + private static string ResolveExecutablePath()
28 + {
29 + string runtimeIdentifier = GetRuntimeIdentifier();
30 + string executableName = OperatingSystem.IsWindows() ? $"{CommandName}.exe" : CommandName;
31 + string executablePath = Path.GetFullPath(
32 + Path.Combine(AppContext.BaseDirectory, "native", runtimeIdentifier, executableName));
33 +
34 + if (!File.Exists(executablePath))
35 + {
36 + throw new FileNotFoundException(
37 + $"The native {CommandName} executable for '{runtimeIdentifier}' is not bundled in this package.",
38 + executablePath);
39 + }
40 +
41 + return executablePath;
42 + }
43 +
44 + private static string GetRuntimeIdentifier()
45 + {
46 + string operatingSystem = OperatingSystem.IsWindows()
47 + ? "windows"
48 + : OperatingSystem.IsMacOS()
49 + ? "darwin"
50 + : OperatingSystem.IsLinux()
51 + ? "linux"
52 + : throw new PlatformNotSupportedException(
53 + $"{CommandName} does not support this operating system through the .NET tool package.");
54 +
55 + string architecture = RuntimeInformation.ProcessArchitecture switch
56 + {
57 + Architecture.X64 => "x64",
58 + Architecture.Arm64 => "arm64",
59 + _ => throw new PlatformNotSupportedException(
60 + $"{CommandName} does not support the '{RuntimeInformation.ProcessArchitecture}' architecture through the .NET tool package."),
61 + };
62 +
63 + return $"{operatingSystem}-{architecture}";
64 + }
65 +
66 + private static void EnsureExecutablePermissions(string executablePath)
67 + {
68 + if (OperatingSystem.IsWindows())
69 + {
70 + return;
71 + }
72 +
73 + UnixFileMode currentMode = File.GetUnixFileMode(executablePath);
74 + UnixFileMode requiredMode = UnixFileMode.UserRead |
75 + UnixFileMode.UserWrite |
76 + UnixFileMode.UserExecute |
77 + UnixFileMode.GroupRead |
78 + UnixFileMode.GroupExecute |
79 + UnixFileMode.OtherRead |
80 + UnixFileMode.OtherExecute;
81 +
82 + if ((currentMode & requiredMode) == requiredMode)
83 + {
84 + return;
85 + }
86 +
87 + File.SetUnixFileMode(executablePath, currentMode | requiredMode);
88 + }
89 +
90 + private static async Task<int> RunAsync(string executablePath, IReadOnlyList<string> arguments)
91 + {
92 + // siGit Code picks its mode from whether stdin is a TTY: a terminal gets
93 + // the ratatui chat UI, a pipe gets the ACP server. Leaving all three
94 + // streams unredirected makes the child inherit this process's handles,
95 + // so that detection sees the real terminal (or the real pipe from an
96 + // editor) rather than something this launcher introduced.
97 + ProcessStartInfo startInfo = new(executablePath)
98 + {
99 + UseShellExecute = false,
100 + WorkingDirectory = Environment.CurrentDirectory,
101 + };
102 +
103 + foreach (string argument in arguments)
104 + {
105 + startInfo.ArgumentList.Add(argument);
106 + }
107 +
108 + using Process process = Process.Start(startInfo)
109 + ?? throw new Win32Exception($"Failed to start '{CommandName}'.");
110 +
111 + await process.WaitForExitAsync();
112 + return process.ExitCode;
113 + }
114 +}
nuget/sigit/README.md new
+65
@@ -0,0 +1,65 @@
1 +# siGit Code for .NET
2 +
3 +`sigit` is the command-line interface for [siGit Code](https://sigit.si), an AI coding agent that
4 +runs on your machine.
5 +
6 +## Install
7 +
8 +```sh
9 +dotnet tool install --global SiGit.Code
10 +```
11 +
12 +## Update
13 +
14 +```sh
15 +dotnet tool update --global SiGit.Code
16 +```
17 +
18 +## Run
19 +
20 +```sh
21 +sigit
22 +```
23 +
24 +In a terminal that opens the chat UI. When stdin is a pipe, the same binary speaks the Agent
25 +Client Protocol (ACP) over stdio instead, which is how editors such as Zed and VS Code drive it.
26 +
27 +First run downloads a GGUF model, so expect a wait of a gigabyte or two before the first reply.
28 +On macOS the model cache is shared with the siGit Code desktop app, so a model either app has
29 +already fetched is reused.
30 +
31 +## Platform support
32 +
33 +This .NET tool bundles native `sigit` binaries for:
34 +
35 +- macOS `arm64`, `x64`
36 +- Linux `arm64`, `x64` (glibc)
37 +- Windows `arm64`, `x64`
38 +
39 +The terminal chat UI is Unix-only. On Windows the binary runs in ACP mode, so use it through an
40 +editor rather than directly.
41 +
42 +Because all six binaries ship in one package, the install is large. If that matters, the Homebrew,
43 +npm, and PyPI packages each download only the binary for your platform.
44 +
45 +## Other installation methods
46 +
47 +- Cargo: `cargo install sigit`
48 +- npm: `npm install -g @smbcloud/sigit`
49 +- pip: `pip install sigit-code`
50 +- Homebrew: `brew tap getsigit/tap && brew trust --tap getsigit/tap && brew install sigit`
51 +- GitHub Releases: <https://github.com/getsigit/sigit/releases>
52 +
53 +## Source
54 +
55 +- Repository: <https://github.com/getsigit/sigit>
56 +- Website: <https://sigit.si>
57 +- Issues: <https://github.com/getsigit/sigit/issues>
58 +
59 +## License
60 +
61 +[Apache 2.0](https://github.com/getsigit/sigit/blob/main/LICENSE)
62 +
63 +## Copyright
64 +
65 +© 2026 PT Sigit Mitra Bangun ([siGit Code & Deploy](https://sigit.si)).
nuget/sigit/SiGit.Code.csproj new
+41
@@ -0,0 +1,41 @@
1 +<Project Sdk="Microsoft.NET.Sdk">
2 + <PropertyGroup>
3 + <OutputType>Exe</OutputType>
4 + <TargetFramework>net8.0</TargetFramework>
5 + <!--
6 + Targeting net8.0 keeps the tool installable on older SDKs, but a
7 + framework-dependent app only rolls forward across patch versions by
8 + default. Without this, `dotnet tool install` on a machine carrying just
9 + the .NET 10 runtime installs a tool that refuses to start with "You must
10 + install or update .NET to run this application". The shim does nothing
11 + version-sensitive, so rolling forward across majors is safe.
12 + -->
13 + <RollForward>Major</RollForward>
14 + <ImplicitUsings>enable</ImplicitUsings>
15 + <Nullable>enable</Nullable>
16 + <PackAsTool>true</PackAsTool>
17 + <ToolCommandName>sigit</ToolCommandName>
18 + <PackageId>SiGit.Code</PackageId>
19 + <PackageVersion>0.0.0-local</PackageVersion>
20 + <Version>$(PackageVersion)</Version>
21 + <Authors>Seto Elkahfi</Authors>
22 + <Description>siGit Code — ACP-compatible AI coding agent. Sí, git.</Description>
23 + <PackageReadmeFile>README.md</PackageReadmeFile>
24 + <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
25 + <PackageProjectUrl>https://sigit.si</PackageProjectUrl>
26 + <RepositoryUrl>https://github.com/getsigit/sigit</RepositoryUrl>
27 + <PackageTags>cli;sigit;ai;coding-agent;llm;acp;developer-tools;dotnet-tool</PackageTags>
28 + <PublishRepositoryUrl>true</PublishRepositoryUrl>
29 + </PropertyGroup>
30 +
31 + <ItemGroup>
32 + <None Include="README.md" Pack="true" PackagePath="" />
33 + <!--
34 + `native/` is populated by release-nuget.yml from the six per-target build
35 + jobs; it is gitignored and empty in a source checkout. Pack="false" keeps
36 + MSBuild from adding the binaries twice: they reach the package through the
37 + tool's publish output, which is what `dotnet tool install` unpacks.
38 + -->
39 + <Content Include="native/**/*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" Pack="false" Visible="false" />
40 + </ItemGroup>
41 +</Project>