DTC.Installer is a cross‑platform packaging helper for .NET applications. It is designed to live inside your repo (often as a Git submodule) and give you a single, repeatable command to produce installable artifacts for Windows and macOS.
The goal is not to replace platform‑native tooling, but to orchestrate it consistently with minimal configuration and sensible defaults.
At a high level, the tool:
- Locates your
.csprojautomatically (or uses an explicit path if provided) - Runs
dotnet publishwith the correct runtime(s) - Generates installers using platform‑native tools
- Produces clean, versioned output in a predictable
dist/layout - Persists all configuration in a single
packaging.jsonfile
Once configured, packaging your app is a one‑command operation.
- Uses Inno Setup 6 (
ISCC.exe) to build a.exeinstaller - Handles versioning, icons, metadata, and install layout
- Automatically appends
.exewhere required
- Builds both Apple Silicon and Intel runtimes by default
- Produces drag‑and‑drop
.dmginstallers - Generates a proper
.appbundle withInfo.plist - Automatically creates a bundle identifier if missing
- Includes an
Applicationsshortcut inside the DMG
(Linux support is intentionally deferred but the structure allows it to be added cleanly.)
- Python 3.9+
- .NET SDK matching your target project
- Inno Setup 6 (Windows packaging only)
- macOS: built‑in
hdiutil(no extra installs required)
-
Add the installer to your repo (recommended as a submodule):
git submodule add https://github.com/deanthecoder/DTC.Installer.git Installer
(Or copy it directly into an
Installer/folder.) -
From your repo root, run:
python Installer/pack.py
-
On first run, the script:
- Scans for a
.csproj - Infers sensible defaults (product name, company, bundle ID, etc.)
- Writes a
packaging.json - Exits without building
- Scans for a
-
Review and tweak
packaging.jsonas needed. -
Run the same command again to build installers:
python Installer/pack.py
To build with a one-off version without editing
packaging.json, pass it on the command line:python Installer/pack.py --version 1.2
All behaviour is driven by a single config file.
Product– Display name of the applicationCompany– Company / publisher nameProject– Relative path to the.csproj(optional if auto‑detected)Executable– Base executable name (without.exe)Version– Default package version; can be overridden for a single run with--version
"Win": {
"Icon": "Assets/app.ico",
"Publisher": "My Company Ltd"
}.exeis added automatically if omitted- Missing icons generate warnings, not hard failures
InstallOpenAL(defaultfalse) includesexternal/oalinst.exeas3rdParty/oalinst.exeand runs it silently during install when enabledShowRunOnStartupTask(defaultfalse) shows an unchecked installer task that can add the app to the current user's Windows startup registry key
"Mac": {
"Icon": "Assets/app.icns",
"BundleIdentifier": "com.mycompany.myapp"
}Defaults applied automatically if fields are missing:
BundleIdentifier→ derived from Company + Product- Runtime targets →
osx-arm64andosx-x64 Info.plist→ generated from template
Generated files are written to:
dist/
├─ win/
│ └─ MyApp‑Setup‑1.2.0.exe
└─ mac/
├─ MyApp‑1.2.0‑arm64.dmg
└─ MyApp‑1.2.0‑x64.dmg
- Idempotent: Re‑running the script reuses existing configuration
- Submodule‑friendly: No global installs, no repo pollution
- Fail‑soft: Missing optional assets emit warnings, not crashes
- Explicit over magic: Defaults are written into
packaging.json, not hidden
# once
python Installer/pack.py
# edit packaging.json
# every release
python Installer/pack.py
# one-off release version
python Installer/pack.py --version 1.2That’s it.
The installer repo also provides a reusable workflow at:
deanthecoder/DTC.Installer/.github/workflows/installers.yml@mainGitHub only discovers workflows from the parent repository's own
.github/workflows folder, so a submodule workflow will not run automatically
just because the submodule is checked out. Add a small wrapper workflow to each
product repo instead:
name: Installers
on:
workflow_dispatch:
inputs:
version:
description: "Installer version, e.g. 0.1"
required: true
default: "0.1"
type: string
build_windows:
description: "Build the Windows installer."
required: true
default: true
type: boolean
build_macos:
description: "Build the macOS DMGs."
required: true
default: true
type: boolean
create_tag:
description: "Create v-prefixed tag after successful builds."
required: true
default: true
type: boolean
jobs:
installers:
permissions:
contents: write
uses: deanthecoder/DTC.Installer/.github/workflows/installers.yml@main
with:
version: ${{ inputs.version }}
build_windows: ${{ inputs.build_windows }}
build_macos: ${{ inputs.build_macos }}
create_tag: ${{ inputs.create_tag }}The reusable workflow checks out the caller repository with submodules enabled,
runs Installer/pack.py --version ..., and uploads:
dist/win/*.exefrom a Windows runner.dist/mac/*.dmgfrom a macOS runner.
When create_tag is enabled, the workflow also creates an annotated
v<version> tag after the selected installer builds pass. Existing tags are not
overwritten.
See LICENSE for details.