dotnet-cli-packaging

SKILL.md

dotnet-cli-packaging

Multi-platform packaging for .NET CLI tools: Homebrew formula authoring (binary tap and cask), apt/deb packaging with dpkg-deb, winget manifest YAML schema and PR submission to winget-pkgs, Scoop manifest JSON, Chocolatey package creation, dotnet tool global/local packaging, and NuGet distribution.

Version assumptions: .NET 8.0+ baseline. Package manager formats are stable across .NET versions.

Scope

  • Homebrew formula authoring (binary tap and cask)
  • apt/deb packaging with dpkg-deb
  • winget manifest YAML schema and PR submission
  • Scoop manifest JSON for Windows
  • Chocolatey package creation
  • dotnet tool global/local packaging and NuGet distribution

Out of scope

  • CLI distribution strategy (AOT vs framework-dependent decision) -- see [skill:dotnet-cli-distribution]
  • Release CI/CD pipeline that automates packaging -- see [skill:dotnet-cli-release-pipeline]
  • Native AOT compilation -- see [skill:dotnet-native-aot]
  • Container-based distribution -- see [skill:dotnet-containers]
  • General CI/CD patterns -- see [skill:dotnet-gha-patterns] and [skill:dotnet-ado-patterns]

Cross-references: [skill:dotnet-cli-distribution] for distribution strategy and RID matrix, [skill:dotnet-cli-release-pipeline] for automated package publishing, [skill:dotnet-native-aot] for AOT binary production, [skill:dotnet-containers] for container-based distribution, [skill:dotnet-tool-management] for consumer-side tool installation and manifest management.


Homebrew (macOS / Linux)

Homebrew is the primary package manager for macOS and widely used on Linux. Two distribution formats exist for CLI tools.

Binary Tap (Formula)

A formula downloads pre-built binaries per platform. This is the recommended approach for Native AOT CLI tools.


# Formula/mytool.rb
class Mytool < Formula
  desc "A CLI tool for managing widgets"
  homepage "https://github.com/myorg/mytool"
  version "1.2.3"
  license "MIT"

  on_macos do
    on_arm do
      url "https://github.com/myorg/mytool/releases/download/v1.2.3/mytool-1.2.3-osx-arm64.tar.gz"
      sha256 "abc123..."
    end
    # Optional: remove on_intel block if not targeting Intel Macs
    on_intel do
      url "https://github.com/myorg/mytool/releases/download/v1.2.3/mytool-1.2.3-osx-x64.tar.gz"
      sha256 "def456..."
    end
  end

  on_linux do
    on_arm do
      url "https://github.com/myorg/mytool/releases/download/v1.2.3/mytool-1.2.3-linux-arm64.tar.gz"
      sha256 "ghi789..."
    end
    on_intel do
      url "https://github.com/myorg/mytool/releases/download/v1.2.3/mytool-1.2.3-linux-x64.tar.gz"
      sha256 "jkl012..."
    end
  end

  def install
    bin.install "mytool"
  end

  test do
    assert_match version.to_s, shell_output("#{bin}/mytool --version")
  end
end

```bash

### Hosting a Tap

A tap is a Git repository containing formulae. Create a repo named `homebrew-tap`:

```text

myorg/homebrew-tap/
  Formula/
    mytool.rb

```text

Users install with:

```bash

brew tap myorg/tap
brew install mytool

```bash

### Homebrew Cask

Casks are for GUI applications or tools with an installer. For pure CLI tools, prefer formulae over casks.

```ruby

# Casks/mytool.rb -- only if the tool has a GUI component
cask "mytool" do
  version "1.2.3"
  sha256 "abc123..."

  url "https://github.com/myorg/mytool/releases/download/v#{version}/mytool-#{version}-osx-arm64.tar.gz"
  name "MyTool"
  homepage "https://github.com/myorg/mytool"

  binary "mytool"
end

```text

---

## apt/deb (Debian/Ubuntu)

### Building a .deb Package with dpkg-deb

Create the package directory structure:

```text

mytool_1.2.3_amd64/
  DEBIAN/
    control
  usr/
    bin/
      mytool

```text

**Control file:**

```text

Package: mytool
Version: 1.2.3
Section: utils
Priority: optional
Architecture: amd64
Maintainer: My Org <dev@myorg.com>
Description: A CLI tool for managing widgets
 MyTool provides fast widget management from the command line.
 Built with .NET Native AOT for zero-dependency execution.
Homepage: https://github.com/myorg/mytool

```bash

**Build the package:**

```bash

#!/bin/bash
set -euo pipefail

VERSION="${1:?Usage: build-deb.sh <version>}"
ARCH="amd64"  # or arm64
PKG_DIR="mytool_${VERSION}_${ARCH}"

mkdir -p "$PKG_DIR/DEBIAN"
mkdir -p "$PKG_DIR/usr/bin"

# Copy the published binary
cp "artifacts/linux-x64/mytool" "$PKG_DIR/usr/bin/mytool"
chmod 755 "$PKG_DIR/usr/bin/mytool"

# Write control file
cat > "$PKG_DIR/DEBIAN/control" << EOF
Package: mytool
Version: ${VERSION}
Section: utils
Priority: optional
Architecture: ${ARCH}
Maintainer: My Org <dev@myorg.com>
Description: A CLI tool for managing widgets
Homepage: https://github.com/myorg/mytool
EOF

# Build the .deb
dpkg-deb --build --root-owner-group "$PKG_DIR"
echo "Built: ${PKG_DIR}.deb"

```text

**RID to Debian architecture mapping:**

| .NET RID      | Debian Architecture |
| ------------- | ------------------- |
| `linux-x64`   | `amd64`             |
| `linux-arm64` | `arm64`             |

### Installing the .deb

```bash

sudo dpkg -i mytool_1.2.3_amd64.deb

```bash

---

## winget (Windows Package Manager)

### Manifest YAML Schema

winget manifests consist of multiple YAML files in a versioned directory structure within the `microsoft/winget-pkgs`
repository.

**Directory structure:**

```text

manifests/
  m/
    MyOrg/
      MyTool/
        1.2.3/
          MyOrg.MyTool.yaml              # Version manifest
          MyOrg.MyTool.installer.yaml    # Installer manifest
          MyOrg.MyTool.locale.en-US.yaml # Locale manifest

```yaml

**Version manifest (MyOrg.MyTool.yaml):**

```yaml

PackageIdentifier: MyOrg.MyTool
PackageVersion: 1.2.3
DefaultLocale: en-US
ManifestType: version
ManifestVersion: 1.9.0

```text

**Installer manifest (MyOrg.MyTool.installer.yaml):**

```yaml

PackageIdentifier: MyOrg.MyTool
PackageVersion: 1.2.3
InstallerType: zip
NestedInstallerType: portable
NestedInstallerFiles:
  - RelativeFilePath: mytool.exe
    PortableCommandAlias: mytool
Installers:
  - Architecture: x64
    InstallerUrl: https://github.com/myorg/mytool/releases/download/v1.2.3/mytool-1.2.3-win-x64.zip
    InstallerSha256: ABC123...
  - Architecture: arm64
    InstallerUrl: https://github.com/myorg/mytool/releases/download/v1.2.3/mytool-1.2.3-win-arm64.zip
    InstallerSha256: DEF456...
ManifestType: installer
ManifestVersion: 1.9.0

```text

**Locale manifest (MyOrg.MyTool.locale.en-US.yaml):**

```yaml

PackageIdentifier: MyOrg.MyTool
PackageVersion: 1.2.3
PackageLocale: en-US
PackageName: MyTool
Publisher: My Org
ShortDescription: A CLI tool for managing widgets
License: MIT
PackageUrl: https://github.com/myorg/mytool
ManifestType: defaultLocale
ManifestVersion: 1.9.0

```text

### Submitting to winget-pkgs

1. Fork `microsoft/winget-pkgs` on GitHub
2. Create the manifest files in the correct directory structure
3. Validate locally: `winget validate --manifest <path>`
4. Submit a PR -- automated checks run against the manifest
5. Microsoft team reviews and merges

See [skill:dotnet-cli-release-pipeline] for automating winget PR creation.

---

## Scoop (Windows)

Scoop is popular among Windows power users. Manifests are JSON files in a bucket repository.

### Scoop Manifest

```json

{
  "version": "1.2.3",
  "description": "A CLI tool for managing widgets",
  "homepage": "https://github.com/myorg/mytool",
  "license": "MIT",
  "architecture": {
    "64bit": {
      "url": "https://github.com/myorg/mytool/releases/download/v1.2.3/mytool-1.2.3-win-x64.zip",
      "hash": "abc123..."
    },
    "arm64": {
      "url": "https://github.com/myorg/mytool/releases/download/v1.2.3/mytool-1.2.3-win-arm64.zip",
      "hash": "def456..."
    }
  },
  "bin": "mytool.exe",
  "checkver": {
    "github": "https://github.com/myorg/mytool"
  },
  "autoupdate": {
    "architecture": {
      "64bit": {
        "url": "https://github.com/myorg/mytool/releases/download/v$version/mytool-$version-win-x64.zip"
      },
      "arm64": {
        "url": "https://github.com/myorg/mytool/releases/download/v$version/mytool-$version-win-arm64.zip"
      }
    }
  }
}

```text

### Hosting a Scoop Bucket

Create a GitHub repo named `scoop-mytool` (or `scoop-bucket`):

```text

myorg/scoop-mytool/
  bucket/
    mytool.json

```json

Users install with:

```powershell

scoop bucket add myorg https://github.com/myorg/scoop-mytool
scoop install mytool

```bash

---

## Chocolatey

Chocolatey is Windows' most established package manager for binary distribution.

### Package Structure

```text

mytool/
  mytool.nuspec
  tools/
    chocolateyInstall.ps1
    LICENSE.txt

```powershell

**mytool.nuspec:**

```xml

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.xmldata.org/2004/07/nuspec">
  <metadata>
    <id>mytool</id>
    <version>1.2.3</version>
    <title>MyTool</title>
    <authors>My Org</authors>
    <projectUrl>https://github.com/myorg/mytool</projectUrl>
    <license type="expression">MIT</license>
    <description>A CLI tool for managing widgets.</description>
    <tags>cli dotnet tools</tags>
  </metadata>
</package>

```text

**tools/chocolateyInstall.ps1:**

```powershell

$ErrorActionPreference = 'Stop'

$packageArgs = @{
  packageName    = 'mytool'
  url64bit       = 'https://github.com/myorg/mytool/releases/download/v1.2.3/mytool-1.2.3-win-x64.zip'
  checksum64     = 'ABC123...'
  checksumType64 = 'sha256'
  unzipLocation  = "$(Split-Path -Parent $MyInvocation.MyCommand.Definition)"
}

Install-ChocolateyZipPackage @packageArgs

```bash

### Building and Publishing

```powershell

# Pack the .nupkg
choco pack mytool.nuspec

# Test locally
choco install mytool --source="." --force

# Push to Chocolatey Community Repository
choco push mytool.1.2.3.nupkg --source https://push.chocolatey.org/ --api-key $env:CHOCO_API_KEY

```text

---

## dotnet tool (Global and Local)

`dotnet tool` is the simplest distribution for .NET developers. Tools are distributed as NuGet packages.

### Project Configuration for Tool Packaging

```xml

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>

    <!-- Tool packaging properties -->
    <PackAsTool>true</PackAsTool>
    <ToolCommandName>mytool</ToolCommandName>
    <PackageId>MyOrg.MyTool</PackageId>
    <Version>1.2.3</Version>
    <Description>A CLI tool for managing widgets</Description>
    <Authors>My Org</Authors>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <PackageProjectUrl>https://github.com/myorg/mytool</PackageProjectUrl>
    <PackageReadmeFile>README.md</PackageReadmeFile>
  </PropertyGroup>

  <ItemGroup>
    <None Include="../../README.md" Pack="true" PackagePath="/" />
  </ItemGroup>
</Project>

```markdown

### Building and Publishing

```bash

# Pack the tool
dotnet pack -c Release

# Publish to NuGet.org
dotnet nuget push bin/Release/MyOrg.MyTool.1.2.3.nupkg \
  --source https://api.nuget.org/v3/index.json \
  --api-key "$NUGET_API_KEY"

```json

### Installing dotnet Tools

```bash

# Global tool (available system-wide)
dotnet tool install -g MyOrg.MyTool

# Local tool (per-project, tracked in .config/dotnet-tools.json)
dotnet new tool-manifest  # first time only
dotnet tool install MyOrg.MyTool

# Update
dotnet tool update -g MyOrg.MyTool

# Run local tool
dotnet tool run mytool
# or just:
dotnet mytool

```text

### Global vs Local Tools

| Aspect             | Global Tool                 | Local Tool                         |
| ------------------ | --------------------------- | ---------------------------------- |
| Scope              | System-wide (per user)      | Per-project directory              |
| Install location   | `~/.dotnet/tools`           | `.config/dotnet-tools.json`        |
| Version management | Manual update               | Tracked in source control          |
| CI/CD              | Must install before use     | `dotnet tool restore` restores all |
| Best for           | Personal productivity tools | Project-specific build tools       |

---

## NuGet Distribution

For tools distributed as NuGet packages (either as `dotnet tool` or standalone):

### Package Metadata

```xml

<PropertyGroup>
  <PackageId>MyOrg.MyTool</PackageId>
  <Version>1.2.3</Version>
  <Description>A CLI tool for managing widgets</Description>
  <Authors>My Org</Authors>
  <PackageLicenseExpression>MIT</PackageLicenseExpression>
  <PackageProjectUrl>https://github.com/myorg/mytool</PackageProjectUrl>
  <PackageReadmeFile>README.md</PackageReadmeFile>
  <PackageTags>cli;tools;widgets</PackageTags>
  <RepositoryUrl>https://github.com/myorg/mytool</RepositoryUrl>
  <RepositoryType>git</RepositoryType>
</PropertyGroup>

```text

### Publishing to NuGet.org

```bash

# Pack
dotnet pack -c Release -o ./nupkgs

# Push (use env var for API key -- never hardcode)
dotnet nuget push ./nupkgs/MyOrg.MyTool.1.2.3.nupkg \
  --source https://api.nuget.org/v3/index.json \
  --api-key "$NUGET_API_KEY"

```json

### Private Feed Distribution

```bash

# Push to a private feed (Azure Artifacts, GitHub Packages, etc.)
dotnet nuget push ./nupkgs/MyOrg.MyTool.1.2.3.nupkg \
  --source https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json \
  --api-key "$AZURE_ARTIFACTS_PAT"

```json

---

## Package Format Comparison

| Format           | Platform       | Requires .NET   | Auto-Update          | Difficulty |
| ---------------- | -------------- | --------------- | -------------------- | ---------- |
| Homebrew formula | macOS, Linux   | No (binary tap) | `brew upgrade`       | Medium     |
| apt/deb          | Debian/Ubuntu  | No (AOT binary) | Via apt repo         | Medium     |
| winget           | Windows 10+    | No (portable)   | `winget upgrade`     | Medium     |
| Scoop            | Windows        | No (portable)   | `scoop update`       | Low        |
| Chocolatey       | Windows        | No              | `choco upgrade`      | Medium     |
| dotnet tool      | Cross-platform | Yes (SDK)       | `dotnet tool update` | Low        |
| NuGet (library)  | Cross-platform | Yes (SDK)       | NuGet restore        | Low        |

---

## Agent Gotchas

1. **Do not hardcode SHA-256 hashes in package manifests.** Generate checksums from actual release artifacts, not
   placeholder values. All package managers validate checksums against downloaded files.
2. **Do not use `InstallerType: exe` for portable CLI tools in winget.** Use `InstallerType: zip` with
   `NestedInstallerType: portable` for standalone executables. The `exe` type implies an installer with silent flags.
3. **Do not forget `PackAsTool` for dotnet tool projects.** Without `<PackAsTool>true</PackAsTool>`, `dotnet pack`
   produces a library package, not an installable tool.
4. **Do not hardcode API keys in packaging scripts.** Use environment variable references (`$NUGET_API_KEY`,
   `$env:CHOCO_API_KEY`) with a comment noting CI secret configuration.
5. **Do not mix Homebrew formula and cask for the same CLI tool.** Pure CLI tools should use formulae. Casks are for GUI
   applications with macOS app bundles.
6. **Do not skip the `test` block in Homebrew formulae.** Homebrew CI runs formula tests. A missing test block causes
   review rejection. At minimum, test `--version` output.

---

## References

- [Homebrew Formula Cookbook](https://docs.brew.sh/Formula-Cookbook)
- [Homebrew Taps](https://docs.brew.sh/Taps)
- [dpkg-deb manual](https://man7.org/linux/man-pages/man1/dpkg-deb.1.html)
- [winget manifest schema](https://learn.microsoft.com/en-us/windows/package-manager/package/manifest)
- [winget-pkgs repository](https://github.com/microsoft/winget-pkgs)
- [Scoop Wiki](https://github.com/ScoopInstaller/Scoop/wiki)
- [Chocolatey package creation](https://docs.chocolatey.org/en-us/create/create-packages)
- [.NET tool packaging](https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools-how-to-create)
- [NuGet publishing](https://learn.microsoft.com/en-us/nuget/nuget-org/publish-a-package)
Weekly Installs
1
First Seen
11 days ago
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1