Guides
Building and distributing
Packaged apps for macOS, Windows and Linux, signing, installers and disk images.
mygo build turns a project into apps people install: an app bundle and a
disk image on macOS, an executable and an installer on Windows, an
executable, a desktop entry, an archive with its install script and a Debian
package on Linux.
bun run build # mygo buildIt:
- writes the TypeScript client, so the frontend builds against the Go code;
- runs
buildCommandfrom the configuration, which builds the frontend intofrontendDist; - compiles the app with the frontend embedded, for production: without the web inspector, and with the name, identifier and version of the configuration linked in;
- packages it for each platform in
build/<os>-<arch>/(outin the configuration); - signs what it can, and with updates configured, writes the signed update archives, and delta updates from the published versions.
Platforms
-platform lists GOOS/GOARCH targets, by default the machine's own:
mygo build -platform darwin/universal,windows/amd64,windows/arm64,linux/amd64,linux/arm64darwin/universal combines arm64 and amd64 in one app. MyGo needs no cgo,
so any machine compiles for every platform. Some steps need the tools of a
platform, and are skipped with a note elsewhere: macOS apps are signed and
put in disk images on macOS only, and Windows installers are made on other
systems only where NSIS is installed.
| Platform | In build/<os>-<arch>/ |
|---|---|
| macOS | My App.app, signed, and My App 0.1.0.dmg |
| Windows | My App.exe, the files of the app, and the installer My App Setup 0.1.0.exe |
| Linux | my-app, my-app.desktop, my-app.png, my-app.xml for the file types the app defines, the files of the app, their archive my-app-0.1.0-linux-amd64.tar.gz with install.sh, and the Debian package my-app_0.1.0_amd64.deb |
Other flags: -debug keeps development features such as the inspector,
-skip-dmg and -skip-notarize skip those steps, -sign overrides the
signing identity of macOS, and -o the output directory. See
the CLI.
Name, icon and version
The configuration describes the app:
export default defineConfig({
name: "My App",
identifier: "com.example.myapp",
version: "1.2.0",
copyright: "© 2026 Example Inc.",
});nameis what users see: the app bundle, the executable, menus, the installer.identifieris a reverse DNS name unique to the app. Systems key preferences, permissions and registrations on it: keep it once released.versionis the app's version, whichApp.Version()returns and updates compare.
The icon is resources/icon.png, or the icon of the configuration: a square PNG,
ideally 1024×1024. mygo build makes it the .icns of macOS, the icon
resource of the Windows executable and the icon of the Linux desktop entry.
Resources
Files the app reads at run time, such as a database seed, a helper binary
or the images of a tray icon, ship with it. Everything in the resources
directory of the project is copied into the app, as are the files and
directories listed in resources in the configuration, under their base names:
export default defineConfig({
resources: ["third_party/licenses", "bin/helper"],
});At run time App.Path(mygo.PathResources) is where they are:
Contents/Resources in a macOS app, the executable's directory elsewhere,
and the project's resources directory under go run and go test:
dir, _ := mygo.App.Path(mygo.PathResources)
seed := filepath.Join(dir, "seed.db")mygo dev copies them into the development app too, and rebuilds it when
they change. Hidden files are left out.
Platform resources
Some resources belong to one platform, such as a program built for one
system and processor. Directories of resources named after a platform
hold them, laid out like resources itself:
resources/
├── data/seed.db every app
├── darwin/ macOS apps
├── darwin-arm64/bin/server macOS on Apple silicon
├── darwin-amd64/bin/server macOS on Intel
├── linux-amd64/bin/server Linux on x86-64
├── linux-arm64/bin/server Linux on ARM64
└── windows-amd64/bin/server.exe Windows on x86-64They are named like the targets of -platform: a system, darwin,
linux or windows, for all of its apps, or a system and an
architecture, as in darwin-arm64 or windows-amd64, for one. An app
ships the platform directories of its target merged with the other
resources, so the Linux x86-64 app above has data/seed.db and its own
bin/server, and nothing of the other platforms. A path that two
directories would both install fails the build, and so does a directory
named the way other tools name platforms, such as darwin-x64 or macos,
which would ship to every platform. The build also notes a target without
a directory of its own when another architecture of its system has one,
such as linux/arm64 next to linux-amd64.
A universal macOS app (darwin/universal) ships darwin and
darwin-universal, and combines darwin-arm64 with darwin-amd64:
every file of one needs its counterpart in the other, programs and
libraries become universal binaries, as lipo makes them, and other files
must be identical. Put what serves both architectures, such as programs
that already are universal binaries, in darwin.
mygo dev ships the platform directories of the machine it runs on.
Under go run and go test, PathResources is the resources
directory as it is, platform directories included.
Helper executables
Programs the app runs, such as a server written in another language or a
tool like ffmpeg, are resources too: put the build for each platform in
its platform directory and run it from there.
dir, _ := mygo.App.Path(mygo.PathResources)
cmd := exec.Command(filepath.Join(dir, "bin", "server"))The same code runs bin\server.exe on Windows, where exec.Command
finds the program of an absolute path without its extension.
mygo build signs them with the app, so that Gatekeeper and SmartScreen
accept them and notarization passes:
- On macOS, every Mach-O executable and library among the resources, and
every app, framework or plug-in bundle holding some, is signed from the
inside out with
signingIdentity, with the hardened runtime and a timestamp for a Developer ID. Signed ad hoc, the default, only code without a signature is signed: Apple silicon runs no unsigned code. - On Windows, with
certificateorsignCommand, every executable and DLL among the resources that has no signature is signed. Those signed by their publishers keep their signatures.
Under the hardened runtime, some programs need entitlements, such as
com.apple.security.cs.allow-jit for a JavaScript runtime that compiles
code as it runs. Code keeps the entitlements it was signed with, so the
official builds of such runtimes work as they are, and
macos.helperEntitlements gives code entitlements of its own, by its path
in the app's resources, such as bin/server for
resources/darwin-arm64/bin/server:
export default defineConfig({
macos: {
signingIdentity: "Developer ID Application: Jane Doe (TEAMID)",
helperEntitlements: { "bin/server": "server.entitlements.plist" },
},
});<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>macOS
mygo build makes an app bundle, signs it, and puts it in a disk image
whose window invites users to drag the app to Applications.
Signing and notarization
Apps signed ad hoc, the default, run on the Mac that built them, but Gatekeeper blocks them on others. To ship an app, sign it with a Developer ID of the Apple Developer Program and have Apple notarize it:
export default defineConfig({
macos: {
signingIdentity: "Developer ID Application: Jane Doe (TEAMID)",
notarize: { keychainProfile: "notary" },
},
});Store the credentials of the notary service in the keychain once:
xcrun notarytool store-credentials notarymygo build then signs the app with the hardened runtime, submits the disk
image to the notary service, and staples the ticket to the disk image and
to the app, which updates ship. security find-identity -v -p codesigning lists your identities, and mygo doctor
counts the Developer IDs.
Info.plist and entitlements
macos.infoPlist adds keys to the app's Info.plist, or replaces MyGo's,
for example the usage descriptions that pages using the camera or the
microphone need:
export default defineConfig({
macos: {
minimumSystemVersion: "13.0",
infoPlist: {
NSCameraUsageDescription: "Scan documents with the camera.",
LSApplicationCategoryType: "public.app-category.productivity",
},
entitlements: "entitlements.plist",
dmgTitle: "My App Installer",
},
});entitlements signs the app with a property list of entitlements, and
helperEntitlements helper executables.
minimumSystemVersion is the oldest macOS the app runs on, 12.0 by
default.
Windows
The executable carries the icon, the version information and a manifest
(per-monitor DPI awareness, modern controls), and runs without a console
window. A .syso file of your own in the main package replaces them.
The installer
mygo build also makes My App Setup 1.2.0.exe. It installs the app for
the current user in %LOCALAPPDATA%\Programs\My App, which needs no
administrator rights and lets the app update itself, adds a
Start menu shortcut and an uninstaller listed in Settings, and registers
the app's URL schemes and file associations.
The installer is made with NSIS. On
Windows, mygo build uses NSIS when it is installed, and otherwise
downloads it the first time it needs it: the official NSIS 3.13 zip,
checked against its SHA-256 and kept in %LOCALAPPDATA%\mygo. On macOS and
Linux, install NSIS to make Windows installers (brew install makensis,
apt install nsis); without it, mygo build skips the installer.
Code signing
Unsigned apps make SmartScreen warn users. Sign the executable, the helper executables, the installer and its uninstaller with a certificate:
export default defineConfig({
windows: { certificate: "certs/code-signing.pfx" },
});The password comes from the MYGO_WINDOWS_CERTIFICATE_PASSWORD
environment variable. On Windows signtool signs, from the Windows SDK;
elsewhere osslsigncode. timestampUrl changes the time stamping server,
by default http://timestamp.digicert.com.
Certificates on hardware tokens or in cloud services, such as Azure Trusted
Signing, sign with a command of their own, in which %1 is the file:
export default defineConfig({
windows: {
signCommand: "signtool sign /fd sha256 /tr http://timestamp.digicert.com /td sha256 /a %1",
},
});Linux
mygo build writes the executable, named after the app in lower case, a
desktop entry and the icon, and a Debian package, which installs the app
in /opt/my-app with its desktop entry, icons, URL schemes and file
types:
export default defineConfig({
linux: {
maintainer: "Jane Doe <jane@example.com>",
comment: "Take notes",
categories: ["Office"],
depends: ["libayatana-appindicator3-1"],
command: "my-app",
},
});The package depends on GTK 3 and WebKitGTK; depends adds more packages.
maintainer defaults to the author of package.json, else the name of
the app.
comment describes the app in its desktop entry and package, and
categories places it in application menus (Utility by default).
The app opens from the applications menu. command also gives it a
command, /usr/bin/my-app from the package and ~/.local/bin/my-app from
install.sh; without it, neither adds a command,
which leaves the name to a command-line tool of the same name, such as
the app's own.
The install script
Every Linux build also gets the app as an archive,
my-app-1.2.0-linux-amd64.tar.gz, and install.sh, which installs it for
the user without root: the app in ~/.local/my-app.app, where it can
update itself, its desktop entry, icon, URL schemes and file
types, and with linux.command that command in ~/.local/bin, unless
another program is there. With
updates configured, it downloads the latest version, which
the update manifest of the machine names, so one line installs the app:
curl -fsSL https://github.com/me/my-app/releases/latest/download/install.sh | shWith a tagPrefix other than v, the script of any
release, such as …/releases/download/desktop-v1.2.0/install.sh, installs
the newest release tagged with the prefix.
-upload publishes it with the archives; with updates.url, publish it
next to the manifests. Next to the archive of its version, as in
build/linux-amd64/, it installs that archive instead, and it installs
the one it is given:
sh install.sh my-app-1.2.0-linux-amd64.tar.gz
sh install.sh --uninstall # keeps the settings and data of the appRunning it again installs the version it finds over the installed one.
The app needs WebKitGTK, which install.sh does not install: when the
system's library cache has none, it says so after installing, with the
command that installs it on Debian, Ubuntu, Fedora, Arch and openSUSE.
When the app updates itself, it registers the desktop entry and file types
of the new version, so a version that adds URL schemes or file types gets
them. The Debian package suits people who want the system's package manager to
update the app; install.sh suits the others, and is the Linux install
that updates itself.
URL schemes and file types
urlSchemes and fileAssociations in the configuration (see
deep links and file associations)
are registered by each package: in the macOS app's Info.plist, by the
Windows installer, and by the Linux desktop entry, install script and
Debian package.
Publishing
With updates.github set in the configuration, mygo build -upload uploads the
disk images, installers, packages, Linux archives and install script, and
update files to the GitHub release of the version, tagged v1.2.0,
creating it as a draft. Review the draft and publish it. It needs the
GitHub CLI (gh), signed in. With updates.s3,
it uploads them to a bucket of Amazon S3 or of a compatible service, such
as Cloudflare R2, with the credentials in AWS_ACCESS_KEY_ID and
AWS_SECRET_ACCESS_KEY (see publishing to S3).
Build on each platform in CI and upload to the same release: macOS runners
sign, notarize and make the disk images; any runner compiles and packages
Windows and Linux apps. Keep the signing secrets in CI secrets:
MYGO_WINDOWS_CERTIFICATE_PASSWORD, MYGO_UPDATER_PRIVATE_KEY, and the
notary credentials in a keychain of the macOS runner.