diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d90fd7db..c969d444 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -104,6 +104,16 @@ jobs:
- run: bun run typecheck
- run: bun run check:deps
+ lint-dotnet:
+ name: .NET Lint
+ needs: [changes]
+ if: needs.changes.outputs.code == 'true'
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-dotnet@v4
+ - run: dotnet format src/dotnet/Sentry.Cli.slnx --verify-no-changes
+
test-unit:
name: Unit Tests
needs: [changes]
@@ -198,6 +208,67 @@ jobs:
name: sentry-${{ matrix.target }}
path: dist-bin/sentry-*
+ build-nuget:
+ name: Pack NuGet (${{ matrix.name }})
+ needs: [changes, lint-dotnet, build-binary]
+ if: needs.changes.outputs.code == 'true'
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: linux-x64
+ os: ubuntu-latest
+ pack-target: linux-x64
+ pack-agnostic: true
+ - name: linux-arm64
+ os: ubuntu-24.04-arm
+ pack-target: linux-arm64
+ pack-agnostic: false
+ - name: macos-arm64
+ os: macos-latest
+ pack-target: darwin-arm64
+ pack-agnostic: false
+ - name: macos-x64
+ os: macos-latest
+ pack-target: darwin-x64
+ pack-agnostic: false
+ - name: windows-x64
+ os: windows-latest
+ pack-target: windows-x64
+ pack-agnostic: false
+ steps:
+ - uses: actions/checkout@v4
+ - uses: oven-sh/setup-bun@v2
+ - uses: actions/cache@v4
+ id: cache
+ with:
+ path: node_modules
+ key: node-modules-${{ matrix.os }}-${{ hashFiles('bun.lock', 'patches/**') }}
+ - name: Install dependencies
+ if: steps.cache.outputs.cache-hit != 'true'
+ shell: bash
+ run: bun install --frozen-lockfile
+ - uses: actions/setup-dotnet@v4
+ - name: Download binary
+ uses: actions/download-artifact@v4
+ with:
+ name: sentry-${{ matrix.pack-target }}
+ path: dist-bin
+ - name: Make binary executable
+ if: runner.os != 'Windows'
+ run: chmod +x dist-bin/sentry-*
+ - name: Pack NuGet packages
+ run: bun run script/pack.ts --target ${{ matrix.pack-target }}
+ - name: Pack agnostic NuGet packages
+ if: matrix.pack-agnostic
+ run: bun run script/pack.ts --no-clean --agnostic
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: nuget-${{ matrix.name }}
+ path: dist-pkg/*.nupkg
+
test-e2e:
name: E2E Tests
needs: [build-binary]
@@ -224,6 +295,35 @@ jobs:
SENTRY_CLI_BINARY: ${{ github.workspace }}/dist-bin/sentry-linux-x64
run: bun run test:e2e
+ test-dotnet:
+ name: .NET Tests (${{ matrix.target }})
+ needs: [changes, lint-dotnet, build-binary]
+ if: needs.changes.outputs.code == 'true'
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - target: darwin-arm64
+ os: macos-latest
+ - target: linux-x64
+ os: ubuntu-latest
+ - target: windows-x64
+ os: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-dotnet@v4
+ - name: Download binary
+ uses: actions/download-artifact@v4
+ with:
+ name: sentry-${{ matrix.target }}
+ path: dist-bin
+ - name: Make binary executable
+ if: runner.os != 'Windows'
+ run: chmod +x dist-bin/sentry-${{ matrix.target }}
+ - name: .NET Tests
+ run: dotnet test --project src/dotnet/Sentry.Cli.Tests/Sentry.Cli.Tests.csproj
+
build-npm:
name: Build npm Package (Node ${{ matrix.node }})
needs: [lint, test-unit]
@@ -286,14 +386,14 @@ jobs:
ci-status:
name: CI Status
if: always()
- needs: [changes, check-skill, build-binary, build-npm, build-docs, test-e2e]
+ needs: [changes, check-skill, build-binary, build-npm, build-docs, test-e2e, test-dotnet, build-nuget]
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Check CI status
run: |
# Check for explicit failures or cancellations in all jobs
- results="${{ needs.check-skill.result }} ${{ needs.build-binary.result }} ${{ needs.build-npm.result }} ${{ needs.build-docs.result }} ${{ needs.test-e2e.result }}"
+ results="${{ needs.check-skill.result }} ${{ needs.build-binary.result }} ${{ needs.build-npm.result }} ${{ needs.build-docs.result }} ${{ needs.test-e2e.result }} ${{ needs.test-dotnet.result }} ${{ needs.build-nuget.result }}"
for result in $results; do
if [[ "$result" == "failure" || "$result" == "cancelled" ]]; then
echo "::error::CI failed"
@@ -307,6 +407,14 @@ jobs:
echo "::error::CI failed - upstream job failed causing test-e2e to be skipped"
exit 1
fi
+ if [[ "${{ needs.changes.outputs.code }}" == "true" && "${{ needs.test-dotnet.result }}" == "skipped" ]]; then
+ echo "::error::CI failed - upstream job failed causing test-dotnet to be skipped"
+ exit 1
+ fi
+ if [[ "${{ needs.changes.outputs.code }}" == "true" && "${{ needs.build-nuget.result }}" == "skipped" ]]; then
+ echo "::error::CI failed - upstream job failed causing build-nuget to be skipped"
+ exit 1
+ fi
if [[ "${{ needs.changes.outputs.skill }}" == "true" && "${{ needs.check-skill.result }}" == "skipped" ]]; then
echo "::error::CI failed - upstream job failed causing check-skill to be skipped"
exit 1
diff --git a/.gitignore b/.gitignore
index e9259aae..6e0927d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,3 +45,7 @@ docs/.astro
# Finder (MacOS) folder config
.DS_Store
+
+# .NET & NuGet
+dist-pkg
+dotnet-tools.json
diff --git a/global.json b/global.json
new file mode 100644
index 00000000..1d364c6a
--- /dev/null
+++ b/global.json
@@ -0,0 +1,9 @@
+{
+ "sdk": {
+ "version": "10.0.100",
+ "rollForward": "latestFeature"
+ },
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
+ }
+}
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 00000000..fbcef101
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index 17be1a23..20471e97 100644
--- a/package.json
+++ b/package.json
@@ -14,13 +14,18 @@
"build": "bun run script/build.ts --single",
"build:all": "bun run script/build.ts",
"bundle": "bun run script/bundle.ts",
+ "pack": "bun run script/pack.ts",
+ "pack:agnostic": "bun run script/pack.ts --agnostic",
"typecheck": "tsc --noEmit",
"lint": "bunx ultracite check",
"lint:fix": "bunx ultracite fix",
+ "lint:dotnet": "bun run script/dotnet-lint.ts --check",
+ "lint:dotnet:fix": "bun run script/dotnet-lint.ts",
"test": "bun test",
"test:unit": "bun test test/lib test/commands test/types --coverage --coverage-reporter=lcov",
"test:isolated": "bun test test/isolated",
"test:e2e": "bun test test/e2e",
+ "test:dotnet": "dotnet test --project src/dotnet/Sentry.Cli.Tests/Sentry.Cli.Tests.csproj",
"generate:skill": "bun run script/generate-skill.ts",
"check:skill": "bun run script/check-skill.ts",
"check:deps": "bun run script/check-no-deps.ts"
diff --git a/script/dotnet-lint.ts b/script/dotnet-lint.ts
new file mode 100644
index 00000000..4acdaf3b
--- /dev/null
+++ b/script/dotnet-lint.ts
@@ -0,0 +1,28 @@
+#!/usr/bin/env bun
+
+/**
+ * Lint script for the .NET source code
+ *
+ * Runs `dotnet format` against the solution and optionally verifies that no
+ * changes are needed (useful in CI to enforce formatting without auto-fixing).
+ *
+ * Usage:
+ * bun run script/dotnet-lint.ts # Format in place
+ * bun run script/dotnet-lint.ts --check # Exit non-zero if any changes would be made
+ */
+
+import { $ } from "bun";
+
+const SOLUTION = "src/dotnet/Sentry.Cli.slnx";
+
+const check = process.argv.includes("--check");
+
+try {
+ if (check) {
+ await $`dotnet format ${SOLUTION} --verify-no-changes`;
+ } else {
+ await $`dotnet format ${SOLUTION}`;
+ }
+} catch {
+ process.exit(1);
+}
diff --git a/script/pack.ts b/script/pack.ts
new file mode 100644
index 00000000..9ec58291
--- /dev/null
+++ b/script/pack.ts
@@ -0,0 +1,276 @@
+#!/usr/bin/env bun
+
+/**
+ * Pack script for Sentry CLI NuGet packages
+ *
+ * Creates platform-specific NuGet packages with embedded native binaries,
+ * as well as the required top-level pointer package and the RID-agnostic package as fallback.
+ * The .NET equivalent of build.ts — same target model, same CLI flags, but dependent on its output at "dist-bin/".
+ *
+ * Usage:
+ * bun run script/pack.ts # Pack for current platform + root + any packages
+ * bun run script/pack.ts --agnostic # Pack only root + any packages (no platform-specific)
+ * bun run script/pack.ts --single # Pack for current platform only (no root/any)
+ * bun run script/pack.ts --target darwin-x64 # Pack for a specific target only (no root/any)
+ *
+ * Flags:
+ * --no-clean Skip cleaning the dist-pkg directory before packing
+ *
+ * Output:
+ * dist-pkg/
+ * dotnet-sentry..nupkg # Root package (pointer, no RID)
+ * dotnet-sentry.any..nupkg # Framework-dependent, RID-agnostic package as fallback
+ * dotnet-sentry.osx-arm64..nupkg # RID-specific package for macOS ARM64
+ * dotnet-sentry.osx-x64..nupkg # RID-specific package for macOS x64
+ * dotnet-sentry.linux-arm64..nupkg # RID-specific package for Linux ARM64
+ * dotnet-sentry.linux-x64..nupkg # RID-specific package for Linux x64
+ * dotnet-sentry.win-x64..nupkg # RID-specific package for Windows x64
+ */
+
+import { $ } from "bun";
+import pkg from "../package.json";
+
+const PROJECT_DIR = "src/dotnet/Sentry.Cli";
+const DIST_BIN_DIR = "dist-bin";
+const DIST_PKG_DIR = "dist-pkg";
+const PACKAGE_ID = "dotnet-sentry";
+
+/** Compute the expected .nupkg output path for a given RID (omit for root package) */
+function getNupkgPath(version: string, rid?: string): string {
+ const prefix = rid ? `.${rid}` : "";
+ return `${DIST_PKG_DIR}/${PACKAGE_ID}${prefix}.${version}.nupkg`;
+}
+
+/** Pack targets configuration */
+type PackTarget = {
+ os: "darwin" | "linux" | "win32";
+ arch: "arm64" | "x64";
+};
+
+const ALL_TARGETS: PackTarget[] = [
+ { os: "darwin", arch: "arm64" },
+ { os: "darwin", arch: "x64" },
+ { os: "linux", arch: "arm64" },
+ { os: "linux", arch: "x64" },
+ { os: "win32", arch: "x64" },
+];
+
+/** Get package name for a target (uses "windows" instead of "win32") */
+function getPackageName(target: PackTarget): string {
+ const platformName = target.os === "win32" ? "windows" : target.os;
+ return `sentry-${platformName}-${target.arch}`;
+}
+
+/** Get binary file name for a target */
+function getBinaryName(target: PackTarget): string {
+ const extension = target.os === "win32" ? ".exe" : "";
+ return `${getPackageName(target)}${extension}`;
+}
+
+/** Get .NET Runtime Identifier for a target */
+function getDotnetRid(target: PackTarget): string {
+ if (target.os === "darwin") {
+ return `osx-${target.arch}`;
+ }
+ if (target.os === "win32") {
+ return `win-${target.arch}`;
+ }
+ return `${target.os}-${target.arch}`;
+}
+
+/** Parse target string (e.g., "darwin-x64" or "linux-arm64") into PackTarget */
+function parseTarget(targetStr: string): PackTarget | null {
+ // Handle "windows" alias for "win32"
+ const normalized = targetStr.replace("windows-", "win32-");
+ const [os, arch] = normalized.split("-") as [
+ PackTarget["os"],
+ PackTarget["arch"],
+ ];
+
+ const target = ALL_TARGETS.find((t) => t.os === os && t.arch === arch);
+ return target ?? null;
+}
+
+/** Pack a platform-specific NuGet package with embedded native binary */
+async function packTarget(
+ target: PackTarget,
+ version: string
+): Promise {
+ const rid = getDotnetRid(target);
+ const packageName = getPackageName(target);
+ const outfile = getNupkgPath(version, rid);
+ console.log(` Packing ${packageName} (${rid})...`);
+
+ try {
+ await $`dotnet pack ${PROJECT_DIR} -c Release -r ${rid} -p:PackageVersion=${version} -p:PublishAot=true`.quiet();
+ console.log(` -> ${outfile}`);
+ return true;
+ } catch (error) {
+ console.error(` Failed to pack ${packageName}:`);
+ console.error(error);
+ return false;
+ }
+}
+
+/** Pack the "any" (framework-dependent, CoreCLR) package */
+async function packAny(version: string): Promise {
+ const outfile = getNupkgPath(version, "any");
+ console.log(" Packing any (framework-dependent)...");
+
+ try {
+ await $`dotnet pack ${PROJECT_DIR} -c Release -r any -p:PackageVersion=${version} -p:PublishAot=false`.quiet();
+ console.log(` -> ${outfile}`);
+ return true;
+ } catch (error) {
+ console.error(" Failed to pack any:");
+ console.error(error);
+ return false;
+ }
+}
+
+/** Pack the root package (no RID — pointer/manifest package) */
+async function packRoot(version: string): Promise {
+ const outfile = getNupkgPath(version);
+ console.log(" Packing root (no RID)...");
+
+ try {
+ await $`dotnet pack ${PROJECT_DIR} -c Release -p:PackageVersion=${version} -p:PublishAot=true`.quiet();
+ console.log(` -> ${outfile}`);
+ return true;
+ } catch (error) {
+ console.error(" Failed to pack root:");
+ console.error(error);
+ return false;
+ }
+}
+
+type PackMode = {
+ /** Platform-specific targets to pack */
+ targets: PackTarget[];
+ /** Whether to also pack the root (pointer) and any (agnostic) packages */
+ includeAgnostic: boolean;
+};
+
+/** Resolve pack mode from CLI args, printing a status line and exiting on error */
+function resolveMode(args: string[]): PackMode {
+ if (args.includes("--agnostic")) {
+ console.log("\nPacking agnostic packages (root + any)");
+ return { targets: [], includeAgnostic: true };
+ }
+
+ const targetIndex = args.indexOf("--target");
+ const targetArg = targetIndex !== -1 ? args[targetIndex + 1] : null;
+
+ if (targetArg) {
+ const target = parseTarget(targetArg);
+ if (!target) {
+ console.error(`Invalid target: ${targetArg}`);
+ console.error(
+ `Valid targets: ${ALL_TARGETS.map((t) => `${t.os === "win32" ? "windows" : t.os}-${t.arch}`).join(", ")}`
+ );
+ process.exit(1);
+ }
+ console.log(`\nPacking for target: ${getPackageName(target)}`);
+ return { targets: [target], includeAgnostic: false };
+ }
+
+ const currentTarget = ALL_TARGETS.find(
+ (t) => t.os === process.platform && t.arch === process.arch
+ );
+ if (!currentTarget) {
+ console.error(`Unsupported platform: ${process.platform}-${process.arch}`);
+ process.exit(1);
+ }
+
+ if (args.includes("--single")) {
+ console.log(
+ `\nPacking for current platform: ${getPackageName(currentTarget)}`
+ );
+ return { targets: [currentTarget], includeAgnostic: false };
+ }
+
+ // Default: current platform + agnostic packages
+ console.log(
+ `\nPacking for current platform + agnostic packages: ${getPackageName(currentTarget)}`
+ );
+ return { targets: [currentTarget], includeAgnostic: true };
+}
+
+/** Verify that native binaries exist for all targets, exiting on missing files */
+async function verifyBinaries(targets: PackTarget[]): Promise {
+ console.log("\nVerifying native binaries...");
+ let binaryMissing = false;
+ for (const target of targets) {
+ const binaryPath = `${DIST_BIN_DIR}/${getBinaryName(target)}`;
+ if (await Bun.file(binaryPath).exists()) {
+ console.log(` ✓ ${binaryPath}`);
+ } else {
+ console.error(` ✗ ${binaryPath} not found`);
+ binaryMissing = true;
+ }
+ }
+ if (binaryMissing) {
+ console.error("\nError: Some native binaries are missing.");
+ console.error("Run 'bun run build:all' first to generate all binaries.");
+ process.exit(1);
+ }
+}
+
+/** Main pack function */
+async function pack(): Promise {
+ const args = process.argv.slice(2);
+ const noClean = args.includes("--no-clean");
+
+ console.log(`\nSentry CLI NuGet Pack v${pkg.version}`);
+ console.log("=".repeat(40));
+
+ const mode = resolveMode(args);
+
+ if (mode.targets.length > 0) {
+ await verifyBinaries(mode.targets);
+ }
+
+ // Clean output directory (unless --no-clean is specified)
+ if (!noClean) {
+ await $`rm -rf ${DIST_PKG_DIR}`.quiet();
+ }
+
+ console.log("");
+
+ let successCount = 0;
+ let failCount = 0;
+
+ // Root package (no RID) and any package — only when includeAgnostic is set
+ if (mode.includeAgnostic) {
+ if (await packRoot(pkg.version)) {
+ successCount += 1;
+ } else {
+ failCount += 1;
+ }
+
+ if (await packAny(pkg.version)) {
+ successCount += 1;
+ } else {
+ failCount += 1;
+ }
+ }
+
+ // Platform-specific packages
+ for (const target of mode.targets) {
+ if (await packTarget(target, pkg.version)) {
+ successCount += 1;
+ } else {
+ failCount += 1;
+ }
+ }
+
+ // Summary
+ console.log(`\n${"=".repeat(40)}`);
+ console.log(`Pack complete: ${successCount} succeeded, ${failCount} failed`);
+
+ if (failCount > 0) {
+ process.exit(1);
+ }
+}
+
+await pack();
diff --git a/src/dotnet/.gitignore b/src/dotnet/.gitignore
new file mode 100644
index 00000000..0808c4ad
--- /dev/null
+++ b/src/dotnet/.gitignore
@@ -0,0 +1,482 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from `dotnet new gitignore`
+
+# dotenv files
+.env
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# Tye
+.tye/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+# but not Directory.Build.rsp, as it configures directory-level build defaults
+!Directory.Build.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
+.idea/
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# Vim temporary swap files
+*.swp
diff --git a/src/dotnet/Directory.Build.props b/src/dotnet/Directory.Build.props
new file mode 100644
index 00000000..abcf8e4e
--- /dev/null
+++ b/src/dotnet/Directory.Build.props
@@ -0,0 +1,28 @@
+
+
+
+ enable
+ enable
+
+
+
+ false
+ true
+
+
+
+ true
+ $(MSBuildThisFileDirectory)artifacts
+
+
+
+ true
+ $(DefineConstants);CI_BUILD
+
+
+
+
+
+
+
+
diff --git a/src/dotnet/Directory.Build.targets b/src/dotnet/Directory.Build.targets
new file mode 100644
index 00000000..8c119d54
--- /dev/null
+++ b/src/dotnet/Directory.Build.targets
@@ -0,0 +1,2 @@
+
+
diff --git a/src/dotnet/Directory.Packages.props b/src/dotnet/Directory.Packages.props
new file mode 100644
index 00000000..19be8937
--- /dev/null
+++ b/src/dotnet/Directory.Packages.props
@@ -0,0 +1,13 @@
+
+
+
+ true
+ false
+ false
+
+
+
+
+
+
+
diff --git a/src/dotnet/Sentry.Cli.Tests/DotnetProject.cs b/src/dotnet/Sentry.Cli.Tests/DotnetProject.cs
new file mode 100644
index 00000000..56126021
--- /dev/null
+++ b/src/dotnet/Sentry.Cli.Tests/DotnetProject.cs
@@ -0,0 +1,67 @@
+namespace Sentry.Cli.Tests;
+
+internal sealed class DotnetProject
+{
+ private readonly FileInfo _project;
+ private readonly string _configuration;
+
+ public DotnetProject(string projectPath)
+ : this(new FileInfo(projectPath))
+ {
+ }
+
+ public DotnetProject(FileInfo project)
+ {
+ _project = project;
+
+#if DEBUG
+ _configuration = "Debug";
+#else
+ _configuration = "Release";
+#endif
+ }
+
+ public Task RunAsync()
+ {
+ return ExecAsync("dotnet", ["run",
+ "--project", _project.FullName,
+ "--configuration", _configuration]);
+ }
+
+ public Task PublishAsync(string rid, string outputDirectory)
+ {
+ return ExecAsync("dotnet", ["publish", _project.FullName,
+ "--configuration", _configuration,
+ "--runtime", rid,
+ "--output", outputDirectory,
+ "--property:PublishAot=true"]);
+ }
+
+ public static Task ExecAsync(string fileName)
+ {
+ return ExecAsync(fileName, []);
+ }
+
+ public static async Task ExecAsync(string fileName, ICollection arguments)
+ {
+ ProcessStartInfo startInfo = new(fileName, arguments)
+ {
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ };
+
+ using var process = Process.Start(startInfo);
+
+ await Assert.That(process).IsNotNull();
+
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+ await process.WaitForExitAsync(cts.Token);
+
+ var stdout = await process.StandardOutput.ReadToEndAsync(CancellationToken.None);
+ var stderr = await process.StandardError.ReadToEndAsync(CancellationToken.None);
+
+ return new ProcessResult(process.ExitCode, stdout.Trim(), stderr.Trim());
+ }
+}
diff --git a/src/dotnet/Sentry.Cli.Tests/JsonUtilities.cs b/src/dotnet/Sentry.Cli.Tests/JsonUtilities.cs
new file mode 100644
index 00000000..71204af7
--- /dev/null
+++ b/src/dotnet/Sentry.Cli.Tests/JsonUtilities.cs
@@ -0,0 +1,15 @@
+using System.Text.Json;
+
+namespace Sentry.Cli.Tests;
+
+internal static class JsonUtilities
+{
+ internal static async Task GetVersionAsync(FileInfo packageJson)
+ {
+ await using var stream = File.OpenRead(packageJson.FullName);
+ using var document = await JsonDocument.ParseAsync(stream);
+
+ var version = document.RootElement.GetProperty("version");
+ return version.GetString()!;
+ }
+}
diff --git a/src/dotnet/Sentry.Cli.Tests/LauncherTests.cs b/src/dotnet/Sentry.Cli.Tests/LauncherTests.cs
new file mode 100644
index 00000000..6ee1ac27
--- /dev/null
+++ b/src/dotnet/Sentry.Cli.Tests/LauncherTests.cs
@@ -0,0 +1,43 @@
+namespace Sentry.Cli.Tests;
+
+[NotInParallel]
+public class LauncherTests
+{
+ [Test]
+ public async Task Launch_FrameworkDependent_HasNoSentryCli()
+ {
+ var project = PathUtilities.LauncherProject;
+
+ var result = await project.RunAsync();
+
+ await result.AssertFailureAsync();
+ await result.AssertErrorAsync($"Unsupported platform: {RuntimeInformation.OSDescription} ({RuntimeInformation.OSArchitecture})");
+ }
+
+ [Test]
+ public async Task Launch_PlatformSpecific_HasSentryCli()
+ {
+ var project = PathUtilities.LauncherProject;
+ var artifacts = PathUtilities.ArtifactsDirectory;
+
+ var output = Path.Combine(artifacts.FullName, "test");
+ var result = await project.PublishAsync(RuntimeInformation.RuntimeIdentifier, output);
+ await result.AssertSuccessAsync();
+
+ // copy from dist-bin to test artifacts
+ var sourceFileName = Path.Combine(PathUtilities.BinaryDirectory.FullName, PlatformUtilities.GetNativeExecutableName());
+ var destFileName = Path.Combine(output, PlatformUtilities.GetNativeExecutableName());
+ File.Copy(sourceFileName, destFileName, true);
+ if (OperatingSystem.IsWindows())
+ {
+ File.Copy($"{sourceFileName}.gz", $"{destFileName}.gz", true);
+ }
+
+ var executable = Path.Combine(output, "Sentry.Cli");
+ var exec = await DotnetProject.ExecAsync(executable, ["--version"]);
+
+ var version = await JsonUtilities.GetVersionAsync(PathUtilities.PackageFile);
+ await exec.AssertSuccessAsync();
+ await exec.AssertOutputAsync(version);
+ }
+}
diff --git a/src/dotnet/Sentry.Cli.Tests/PathUtilities.cs b/src/dotnet/Sentry.Cli.Tests/PathUtilities.cs
new file mode 100644
index 00000000..39fd2f0c
--- /dev/null
+++ b/src/dotnet/Sentry.Cli.Tests/PathUtilities.cs
@@ -0,0 +1,116 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace Sentry.Cli.Tests;
+
+internal static class PathUtilities
+{
+ private static readonly Lazy s_testProjectDirectory = new(() => GetTestProjectDirectory());
+ private static readonly Lazy s_launcherProject = new(GetLauncherProject);
+ private static readonly Lazy s_artifactsDirectory = new(GetArtifactsDirectory);
+ private static readonly Lazy s_binaryDirectory = new(GetBinaryDirectory);
+ private static readonly Lazy s_packageFile = new(GetPackageFile);
+
+ internal static DotnetProject LauncherProject => s_launcherProject.Value;
+ internal static DirectoryInfo ArtifactsDirectory => s_artifactsDirectory.Value;
+ internal static DirectoryInfo BinaryDirectory => s_binaryDirectory.Value;
+ internal static FileInfo PackageFile => s_packageFile.Value;
+
+ private static DirectoryInfo GetTestProjectDirectory([CallerFilePath] string? sourceFilePath = null)
+ {
+ var testProjectPath = Path.GetDirectoryName(sourceFilePath);
+ Assert.NotNull(testProjectPath);
+
+ FileInfo testProject = new(Path.Combine(testProjectPath, "Sentry.Cli.Tests.csproj"));
+
+ if (!testProject.Exists)
+ {
+ if (TryFindSolutionDirectory(out var solution))
+ {
+ testProject = new FileInfo(Path.Combine(solution.FullName, "Sentry.Cli.Tests", "Sentry.Cli.Tests.csproj"));
+ if (!testProject.Exists)
+ {
+ Assert.Fail($"Test project not found: {testProject}");
+ }
+ }
+ else
+ {
+ Assert.Fail($"Test project not found: {testProject}");
+ }
+ }
+
+ Assert.NotNull(testProject.Directory);
+ return testProject.Directory;
+ }
+
+ private static bool TryFindSolutionDirectory([NotNullWhen(true)] out DirectoryInfo? solution)
+ {
+ var directory = new DirectoryInfo(Directory.GetCurrentDirectory());
+
+ while (directory.Parent is { } parent)
+ {
+ directory = parent;
+
+ if (Directory.EnumerateFiles(directory.FullName, "Sentry.Cli.slnx", SearchOption.TopDirectoryOnly).Any())
+ {
+ solution = directory;
+ return true;
+ }
+ }
+
+ solution = null;
+ return false;
+ }
+
+ private static DotnetProject GetLauncherProject()
+ {
+ var testProjectDirectory = s_testProjectDirectory.Value;
+ FileInfo project = new(Path.Combine(testProjectDirectory.FullName, "../Sentry.Cli/Sentry.Cli.csproj"));
+
+ if (!project.Exists)
+ {
+ Assert.Fail($"Launcher project not found: {project}");
+ }
+
+ return new DotnetProject(project);
+ }
+
+ private static DirectoryInfo GetArtifactsDirectory()
+ {
+ var testProjectDirectory = s_testProjectDirectory.Value;
+ DirectoryInfo artifacts = new(Path.Combine(testProjectDirectory.FullName, "../artifacts"));
+
+ if (!artifacts.Exists)
+ {
+ Assert.Fail($"Artifacts path not found: {artifacts}");
+ }
+
+ return artifacts;
+ }
+
+ private static DirectoryInfo GetBinaryDirectory()
+ {
+ var testProjectDirectory = s_testProjectDirectory.Value;
+ DirectoryInfo binary = new(Path.Combine(testProjectDirectory.FullName, "../../../dist-bin"));
+
+ if (!binary.Exists)
+ {
+ Assert.Fail($"Binary path not found: {binary}");
+ }
+
+ return binary;
+ }
+
+ private static FileInfo GetPackageFile()
+ {
+ var testProjectDirectory = s_testProjectDirectory.Value;
+ FileInfo package = new(Path.Combine(testProjectDirectory.FullName, "../../../package.json"));
+
+ if (!package.Exists)
+ {
+ Assert.Fail($"Package JSON not found: {package}");
+ }
+
+ return package;
+ }
+}
diff --git a/src/dotnet/Sentry.Cli.Tests/PlatformUtilities.cs b/src/dotnet/Sentry.Cli.Tests/PlatformUtilities.cs
new file mode 100644
index 00000000..9ffcd655
--- /dev/null
+++ b/src/dotnet/Sentry.Cli.Tests/PlatformUtilities.cs
@@ -0,0 +1,23 @@
+namespace Sentry.Cli.Tests;
+
+internal static class PlatformUtilities
+{
+ internal static string GetNativeExecutableName()
+ {
+ var platform = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? "linux" :
+ RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "darwin" :
+ RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "windows" :
+ throw new PlatformNotSupportedException($"Unsupported platform: {RuntimeInformation.OSDescription} ({RuntimeInformation.OSArchitecture})");
+
+ var architecture = RuntimeInformation.OSArchitecture switch
+ {
+ Architecture.Arm64 => "arm64",
+ Architecture.X64 => "x64",
+ _ => throw new PlatformNotSupportedException($"Unsupported platform: {RuntimeInformation.OSDescription} ({RuntimeInformation.OSArchitecture})"),
+ };
+
+ return OperatingSystem.IsWindows()
+ ? $"sentry-{platform}-{architecture}.exe"
+ : $"sentry-{platform}-{architecture}";
+ }
+}
diff --git a/src/dotnet/Sentry.Cli.Tests/ProcessResult.cs b/src/dotnet/Sentry.Cli.Tests/ProcessResult.cs
new file mode 100644
index 00000000..991a993a
--- /dev/null
+++ b/src/dotnet/Sentry.Cli.Tests/ProcessResult.cs
@@ -0,0 +1,46 @@
+namespace Sentry.Cli.Tests;
+
+internal sealed class ProcessResult
+{
+ private readonly int _exitCode;
+ private readonly string _output;
+ private readonly string _error;
+
+ public ProcessResult(int exitCode, string output, string error)
+ {
+ _exitCode = exitCode;
+ _output = output;
+ _error = error;
+ }
+
+ public int ExitCode => _exitCode;
+ public string Output => _output;
+ public string Error => _error;
+
+ public async Task AssertSuccessAsync()
+ {
+ await Assert.That(_exitCode).IsZero();
+ }
+
+ public async Task AssertFailureAsync()
+ {
+ await Assert.That(_exitCode).IsNotZero();
+ }
+
+ public async Task AssertFailureAsync(int exitCode)
+ {
+ await Assert.That(_exitCode).IsEqualTo(exitCode);
+ }
+
+ public async Task AssertOutputAsync(string output)
+ {
+ await Assert.That(_output).IsEqualTo(output);
+ await Assert.That(_error).IsEmpty();
+ }
+
+ public async Task AssertErrorAsync(string error)
+ {
+ await Assert.That(_output).IsEmpty();
+ await Assert.That(_error).IsEqualTo(error);
+ }
+}
diff --git a/src/dotnet/Sentry.Cli.Tests/Sentry.Cli.Tests.csproj b/src/dotnet/Sentry.Cli.Tests/Sentry.Cli.Tests.csproj
new file mode 100644
index 00000000..09c5022e
--- /dev/null
+++ b/src/dotnet/Sentry.Cli.Tests/Sentry.Cli.Tests.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net10.0
+
+
+
+ true
+ true
+
+
+
+
+
+
+
diff --git a/src/dotnet/Sentry.Cli.slnx b/src/dotnet/Sentry.Cli.slnx
new file mode 100644
index 00000000..2a7974d9
--- /dev/null
+++ b/src/dotnet/Sentry.Cli.slnx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/dotnet/Sentry.Cli/Program.cs b/src/dotnet/Sentry.Cli/Program.cs
new file mode 100644
index 00000000..42bba7db
--- /dev/null
+++ b/src/dotnet/Sentry.Cli/Program.cs
@@ -0,0 +1,70 @@
+#if _PLATFORM_SPECIFIC
+return RunNativeExecutable(args);
+#else
+return RunPlatformAgnostic(args);
+#endif
+
+#if _PLATFORM_SPECIFIC
+static int RunNativeExecutable(string[] args)
+{
+ string exeName = GetNativeExecutableName();
+ string exePath = Path.Combine(AppContext.BaseDirectory, exeName);
+ return StartExecutable(exePath, args);
+}
+
+static string GetNativeExecutableName()
+{
+#if _PLATFORM_LINUX_ARM64
+ Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.Linux));
+ Debug.Assert(RuntimeInformation.OSArchitecture == Architecture.Arm64);
+ return "sentry-linux-arm64";
+#elif _PLATFORM_LINUX_X64
+ Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.Linux));
+ Debug.Assert(RuntimeInformation.OSArchitecture == Architecture.X64);
+ return "sentry-linux-x64";
+#elif _PLATFORM_OSX_ARM64
+ Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.OSX));
+ Debug.Assert(RuntimeInformation.OSArchitecture == Architecture.Arm64);
+ return "sentry-darwin-arm64";
+#elif _PLATFORM_OSX_X64
+ Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.OSX));
+ Debug.Assert(RuntimeInformation.OSArchitecture == Architecture.X64);
+ return "sentry-darwin-x64";
+#elif _PLATFORM_WIN_X64
+ Debug.Assert(RuntimeInformation.IsOSPlatform(OSPlatform.Windows));
+ Debug.Assert(RuntimeInformation.OSArchitecture == Architecture.X64);
+ return "sentry-windows-x64.exe";
+#else
+ throw new PlatformNotSupportedException($"Unsupported platform: {RuntimeInformation.OSDescription} ({RuntimeInformation.OSArchitecture})");
+#error Platform not defined.
+#endif
+}
+
+static int StartExecutable(string fileName, string[] args)
+{
+ ProcessStartInfo startInfo = new(fileName, args)
+ {
+#if _PLATFORM_WIN_X64
+ CreateNoWindow = false,
+#else
+ CreateNoWindow = true,
+#endif
+ UseShellExecute = false,
+ };
+
+ var process = Process.Start(startInfo);
+ if (process is null)
+ {
+ throw new InvalidOperationException("Sentry CLI could not be started.");
+ }
+
+ process.WaitForExit();
+ return process.ExitCode;
+}
+#else
+static int RunPlatformAgnostic(string[] args)
+{
+ Console.Error.WriteLine($"Unsupported platform: {RuntimeInformation.OSDescription} ({RuntimeInformation.OSArchitecture})");
+ return 1;
+}
+#endif
diff --git a/src/dotnet/Sentry.Cli/Sentry.Cli.csproj b/src/dotnet/Sentry.Cli/Sentry.Cli.csproj
new file mode 100644
index 00000000..ffc66a37
--- /dev/null
+++ b/src/dotnet/Sentry.Cli/Sentry.Cli.csproj
@@ -0,0 +1,83 @@
+
+
+
+ Exe
+ net10.0
+
+
+
+ true
+
+
+
+ true
+ true
+ sentry
+ any;linux-arm64;linux-x64;osx-arm64;osx-x64;win-x64
+ $(MSBuildThisFileDirectory)../../../dist-pkg
+
+
+
+ dotnet-sentry
+ 0.1.0
+ Sentry Team and Contributors
+ The command-line interface for Sentry. Built for developers and AI agents.
+ Copyright 2025 Functional Software, Inc. dba Sentry
+ LICENSE.md
+ https://cli.sentry.dev/
+ icon.png
+ README.md
+ sentry;cli
+ https://github.com/getsentry/cli/blob/main/CHANGELOG.md
+ https://github.com/getsentry/cli
+ git
+
+
+
+
+ true
+ Speed
+ true
+
+
+
+ $(MSBuildThisFileDirectory)../../../dist-bin/
+
+
+
+ $(DefineConstants);_PLATFORM_SPECIFIC;_PLATFORM_LINUX_ARM64
+ $(SelfContainedPlatformSpecificSentryCliDirectory)sentry-linux-arm64
+
+
+
+ $(DefineConstants);_PLATFORM_SPECIFIC;_PLATFORM_LINUX_X64
+ $(SelfContainedPlatformSpecificSentryCliDirectory)sentry-linux-x64
+
+
+
+ $(DefineConstants);_PLATFORM_SPECIFIC;_PLATFORM_OSX_ARM64
+ $(SelfContainedPlatformSpecificSentryCliDirectory)sentry-darwin-arm64
+
+
+
+ $(DefineConstants);_PLATFORM_SPECIFIC;_PLATFORM_OSX_X64
+ $(SelfContainedPlatformSpecificSentryCliDirectory)sentry-darwin-x64
+
+
+
+ $(DefineConstants);_PLATFORM_SPECIFIC;_PLATFORM_WIN_X64
+ $(SelfContainedPlatformSpecificSentryCliDirectory)sentry-windows-x64.exe
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/dotnet/sentry-nuget.png b/src/dotnet/sentry-nuget.png
new file mode 100644
index 00000000..6a4f4415
Binary files /dev/null and b/src/dotnet/sentry-nuget.png differ