macOS and Windows installers
This guide shows how to connect an existing DMG, MSI or Inno Setup script to native-packages. You supply a built app and a packaging script. The build command checks the app, runs your script, and records the finished installer’s checksum.
Build DMGs on macOS and MSI or Inno installers on Windows. These targets use your native tools and do not need an nFPM executable.
Prepare your app
Build the complete application first, including any libraries and supporting
files. A macOS input can be dist/Hello.app. A Windows input can be a directory
such as dist/windows containing hello.exe and its dependencies.
If your inputs come from another job, download and unpack them before packaging.
Native targets require a local directory and use build --version; they do
not accept build --release downloads directly.
Configure the targets
Here is a complete configuration for an ARM64 Mac app and an x86-64 Windows app:
schema: 1
tool:
version: '0.11.0'
nfpm: '2.47.0'
nfpm:
name: hello
description: A small greeting application
maintainer: Your Name <you@example.com>
license: MIT
release:
repository: your-name/hello
targets:
macos-arm64:
platform: macos
arch: arm64
formats: [dmg]
input:
kind: directory
local: dist/Hello.app
native:
command: [ruby, packaging/dmg.rb, '@PAYLOAD@', '@PACKAGE@']
output: 'hello-@TAG@-macos-arm64.dmg'
windows-amd64:
platform: windows
arch: amd64
formats: [msi]
input:
kind: directory
local: dist/windows
native:
command: [ruby, packaging/msi.rb, '@PAYLOAD@', '@PACKAGE@', '@VERSION@']
output: 'hello-@TAG@-windows-amd64.msi'
For an Inno Setup installer instead, use formats: [inno] and an .exe
output. A Windows target builds one installer format.
The nfpm section still holds shared package metadata, and tool.nfpm keeps
mixed-platform configurations consistent. Native targets use native.command
to create their output.
For Intel Macs, use arch: amd64. Use arch: universal for a build containing
both Intel and ARM64 code. Architecture checks use Apple’s lipo.
Write the packaging command
native.command is a list of arguments. It must include @PACKAGE@:
| Value | What your script receives |
|---|---|
@PAYLOAD@ |
A temporary copy of the input to package. |
@PACKAGE@ |
The exact, absolute path where the output must be created. |
@VERSION@ |
App version, such as 1.2.3. |
@TAG@ |
Release tag, such as v1.2.3. |
Commands run in the configuration directory. They also receive
NATIVE_PACKAGES_TARGET, NATIVE_PACKAGES_VERSION, and SOURCE_DATE_EPOCH.
A minimal DMG script
For the Hello.app input above, create packaging/dmg.rb:
require "fileutils"
require "tmpdir"
payload, output = ARGV
abort "expected payload and output paths" unless payload && output
abort "output already exists" if File.exist?(output)
Dir.mktmpdir("hello-dmg-") do |staging|
FileUtils.cp_r(payload, File.join(staging, "Hello.app"), preserve: true)
File.symlink("/Applications", File.join(staging, "Applications"))
success = system("hdiutil", "create", "-volname", "Hello",
"-srcfolder", staging, "-format", "UDZO", output)
abort "DMG creation failed" unless success
end
abort "DMG verification failed" unless system("hdiutil", "verify", output)
This places the app and an Applications shortcut inside a compressed disk image. Adapt the volume name and app name to your project.
A Windows Installer (MSI) project
Keep your installer’s WiX source in the repository. This minimal
packaging/hello.wxs (WiX Toolset 5) installs everything in the payload
for all users and replaces earlier versions on upgrade:
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Package Name="Hello" Manufacturer="Your Name" Version="$(Version)"
UpgradeCode="{00000000-0000-0000-0000-000000000000}" Scope="perMachine" Compressed="yes">
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
<MediaTemplate EmbedCab="yes" />
<StandardDirectory Id="ProgramFiles64Folder">
<Directory Id="INSTALLFOLDER" Name="Hello" />
</StandardDirectory>
<Files Include="$(SourceDir)\**" Directory="INSTALLFOLDER" />
</Package>
</Wix>
Generate your own UpgradeCode once and never change it: it is how Windows
recognizes an upgrade. Then have packaging/msi.rb build it:
payload, output, version = ARGV
# Windows Installer versions are numeric: 1.2.3-beta.1 installs as 1.2.3.
numeric = version.split("-").first
success = system("wix", "build", File.join(__dir__, "hello.wxs"), "-arch", "x64",
"-d", "Version=#{numeric}", "-d", "SourceDir=#{payload}", "-pdbtype", "none", "-o", output)
abort "MSI build failed" unless success
-pdbtype none stops WiX writing a .wixpdb debug file next to the MSI;
the recipe may create only the declared output.
Install WiX with dotnet tool install --global wix --version 5.0.2. Windows
compares only the first three version fields, each at most 255.255.65535, so
release candidates of one version upgrade each other in place. Per-user
installs, shortcuts, file associations, and closing a running app are part of
your WiX project; see the WiX documentation.
An Inno Setup script
Keep your app’s .iss recipe in the repository. Have packaging/setup.rb
invoke your installed Inno compiler and pass it the three arguments above.
The wrapper should:
- Use
@PAYLOAD@as the source directory for the recipe’s files. - Map
@VERSION@to the app and numeric installer version fields. - Set the compiler’s output directory and filename from
@PACKAGE@. - Fail if compilation fails.
An Inno recipe also owns your app’s identity, installation location, shortcuts, and uninstall behavior. See the Inno command-line compiler documentation for invoking it from your wrapper.
Build on each host
On the Mac:
native-packages doctor --target macos-arm64
native-packages build --version 1.2.3 --target macos-arm64 --output dist/macos-packages
On Windows:
native-packages doctor --target windows-amd64
native-packages build --version 1.2.3 --target windows-amd64 --output dist/windows-packages
doctor checks the host and the command’s executable. Your script checks its
additional tools, such as WiX or the Inno compiler. validate and build --dry-run
do not execute it.
The build checks Mach-O or PE architecture, runs the script, and checks that it produced exactly the declared DMG, MSI or EXE. Test installation, startup, upgrades, and removal separately for your app.
Preserve the prepared input
Package the copy at @PAYLOAD@. A top-level .app keeps its basename; other
input directories are copied into a directory named payload.
The copy preserves file modes and safe internal relative symlinks, including framework links. Dangling links, links escaping the input, and special files are rejected. Your recipe and output hooks must leave the copied input unchanged. Use your own temporary staging directory when arranging files.
The command must create only its declared output in the destination directory. Keep logs and temporary files elsewhere.
Sign before recording checksums
For DMG targets, providing complete Apple credentials enables automatic input signing and DMG notarization. Follow Apple signing and notarization. The DMG script must preserve the signed app when copying it into the image.
For MSI and Inno targets, a windows.signing section and Azure Artifact
Signing settings enable automatic signing of the input’s code and of the
installer. Follow Windows code signing.
To sign with another tool, use an after_package hook:
after_package: [pwsh, -File, packaging/sign.ps1, '@PACKAGE@']
This runs before the output hash is recorded. It must preserve the output’s filename and file set. Do not sign or otherwise change the installer after building; publication would reject the changed hash.
MSIX is a separate Windows format. It uses package metadata and file mappings
instead of native.command; see Supported platforms.
Combine and publish
Use Building across platforms to combine these directories with Linux builds. If your configuration also has AUR or Homebrew templates, that guide explains how to generate recipes after all native packages exist.