1. Project overview
What PolyInstall does
PolyInstall turns a YAML manifest into one or more self-contained installer binaries. It is designed for
product teams, open-source maintainers, and internal platform engineers who need a repeatable packaging
flow without adopting a different installer authoring system for every operating system.
Current project state
The repository is at v2.0.1, targets net10.0, uses
Avalonia 12.0.4 for the installer UI, and runs CI restore/build/test with coverage on GitHub
Actions. The release pipeline publishes self-contained CLI zips for win-x64,
linux-x64, and osx-arm64; each CLI zip carries its host stub, while separate
stubs-<rid> archives are available when you need to build installers for another target.
Single source of truth
Define metadata, files, build targets, UI steps, platform settings, and lifecycle tasks in one manifest.
Self-contained output
The CLI appends the compressed payload and embedded JSON manifest to a pre-published runtime stub.
Friendly install flow
Ship an Avalonia wizard with welcome, licence, destination, progress, and finish steps.
Value proposition
PolyInstall removes the overhead of maintaining separate installer definitions by giving you a clear YAML
contract, deterministic build commands, schema validation, bundled payload compression, and install-state
tracking for reliable uninstall behaviour.
| Project |
Role |
PolyInstall.Core |
Shared manifest models, packaging logic, validation, and task/runtime primitives. |
PolyInstall.Cli |
Build tool that validates manifests, gathers files, compresses payloads, and emits installers. |
PolyInstall.Runtime |
The generated installer host that reads its appended bundle and launches the install flow. |
PolyInstall.UI |
Avalonia wizard UI used by the runtime. |
PolyInstall.Uninstall |
Dedicated Windows uninstall host used for Add/Remove Programs and command-line uninstall. |
PolyInstall.SchemaGen |
Generates schema/v1.json from the C# manifest model. |
2. Prerequisites
Before you install
The recommended path is to download a release build of the CLI. Release archives are self-contained, so
users do not need the .NET SDK just to build installers from manifests.
For normal use
- A 64-bit host OS that matches the downloaded CLI archive.
- A YAML manifest describing your application.
- A payload directory containing the files you want to install.
- The included host RID stub, a downloaded
stubs-<rid> archive, or a custom stubs/ directory.
For source builds
- .NET SDK 10.x, aligned with the repository target framework.
- Git for cloning and contributing.
mksquashfs from squashfs-tools for Linux AppImage output.
hdiutil on macOS for DMG output.
Target compatibility
The finished installer must run on a machine that matches its target runtime identifier. For example,
a windows-x64 installer is intended for 64-bit Windows, not Linux or macOS.
3. Installation guide
Install and configure PolyInstall
Use the release build when you want to package an application. Build from source only when contributing,
testing unreleased changes, or publishing custom runtime stubs.
Download a release archive
Open the PolyInstall v2.0.1 release
and download the archive that matches your build machine, such as
polyinstall-win-x64-<tag>.zip, polyinstall-linux-x64-<tag>.zip, or
polyinstall-osx-arm64-<tag>.zip. Each CLI archive includes the runtime stub for that
same RID; download a separate stubs-<rid>-<tag>.zip archive when you need to build a
different target from the same host.
# Example layout after extraction
polyinstall/
polyinstall # or polyinstall.exe on Windows
schema/v1.json
stubs/
win-x64/ # host RID stub included with a win-x64 CLI archive
# Optional: extract separate stubs-linux-x64-<tag>.zip or stubs-osx-arm64-<tag>.zip
# under the same stubs/ root when building those targets from this host.
Add the CLI to your shell
Either run polyinstall from the extracted folder or add that folder to PATH.
# PowerShell example for the current session
$env:PATH = "C:\Tools\polyinstall;$env:PATH"
# Confirm the command is visible
polyinstall validate .\manifest.yaml
Connect your editor to the schema
Add the schema comment to the top of each manifest for completions and validation in YAML-aware editors.
# yaml-language-server: $schema=https://bolorundurowb.github.io/PolyInstall/schema/v1.json
Validate before building
Validation catches JSON Schema issues before packaging. The full build command also runs
semantic checks such as relative file paths, wizard ordering, task compatibility, and signing settings.
polyinstall validate manifest.yaml --base ./release-input
polyinstall build manifest.yaml --base ./release-input
Build from source
Contributors can build the CLI and publish runtime stubs directly from the repository.
git clone https://github.com/bolorundurowb/PolyInstall.git
cd PolyInstall
dotnet restore src/PolyInstall.slnx
dotnet build src/PolyInstall.slnx -c Release --no-restore
# Publish a Windows runtime stub and uninstaller
dotnet publish src/PolyInstall.Runtime/PolyInstall.Runtime.csproj -c Release -r win-x64 -o stubs/win-x64
dotnet publish src/PolyInstall.Uninstall/PolyInstall.Uninstall.csproj -c Release -r win-x64 -o stubs/win-x64
# Build the sample installer
dotnet run --project src/PolyInstall.Cli/PolyInstall.Cli.csproj -- build examples/polyinstall.sample.yaml --base examples --stubs stubs
4. Quick start
Build your first installer
This example packages a tiny application directory and creates a Windows x64 installer. It is intentionally
small, so you can validate the full build loop in minutes.
Create a payload directory
Put the files you want to install under a stable source directory. This example uses a single text file.
mkdir -p hello-payload
printf "Hello from PolyInstall\n" > hello-payload/hello.txt
Create manifest.yaml
The manifest declares metadata, output settings, UI steps, and which files to include.
# yaml-language-server: $schema=https://bolorundurowb.github.io/PolyInstall/schema/v1.json
metadata:
name: HelloPoly
version: 0.1.0
publisher: Example Ltd
build:
output_dir: dist
compression: brotli
targets:
- windows-x64
windows:
install_scope: user
register_arp: true
ui:
theme: system
wizard_steps:
- type: welcome
title: Welcome to HelloPoly
- type: destination
title: Choose an install location
default_path: "{UserHome}/HelloPoly"
- type: progress
title: Installing HelloPoly
- type: finish
title: Installation complete
files:
- source_dir: hello-payload
include:
- "**/*"
Validate and build
Run validation first, then build the installer. Output files are written under
build.output_dir, relative to --base.
polyinstall validate manifest.yaml --base .
polyinstall build manifest.yaml --base .
Run the installer
Launch the generated binary on a matching target machine. The wizard lets the user choose the
destination, copies the payload, records install state, and finishes with a summary.
# Windows output
dist/HelloPoly-windows-x64.exe
5. Advanced usage and features
Use the full manifest surface
PolyInstall supports multi-target builds, platform-specific packaging, wizard customisation, lifecycle
tasks, glob filtering, custom output names, optional signing, environment variable substitution, and
install-state tracking.
Multi-platform targets
A single manifest can build installers for Windows, Linux, and macOS. Platform options let you enable
Add/Remove Programs registration, AppImage packaging, or DMG creation where the host supports it.
Wizard UI and branding
Configure the wizard sequence, select light, dark, or system
theme mode, and provide a logo path resolved from the extracted payload at install time.
Lifecycle tasks
Run pre-install, post-install, pre-uninstall, and post-uninstall actions such as shortcuts, registry
writes, desktop entries, permission changes, and PATH updates.
Reliable uninstall
Install state is written under .polyinstall/, allowing the Windows uninstaller to remove
registered entries, installed files, and configured resources consistently.
Complete multi-platform manifest
This manifest demonstrates advanced build options, glob filtering, environment substitution, branding, and
OS-gated tasks.
# yaml-language-server: $schema=https://bolorundurowb.github.io/PolyInstall/schema/v1.json
metadata:
name: Contoso Studio
version: "${APP_VERSION:-1.0.0}"
id: com.contoso.studio
publisher: Contoso Ltd
build:
output_dir: dist
output_name: "{name}-{version}-{target}"
compression: brotli
targets:
- windows-x64
- linux-x64
- osx-arm64
windows:
install_scope: user
register_arp: true
linux:
package: appimage
macos:
package: dmg
signing:
windows:
certificate_path: "${WINDOWS_CERT_PATH}"
certificate_password_env: WINDOWS_CERT_PASSWORD
timestamp_url: "http://timestamp.digicert.com"
macos:
identity: "Developer ID Application: Contoso Ltd"
keychain: "${MACOS_KEYCHAIN_PATH}"
notarization_profile: "polyinstall-notary"
ui:
theme: system
logo_path: "branding/contoso-logo.svg"
wizard_steps:
- type: welcome
title: Welcome to Contoso Studio
- type: eula
title: Licence agreement
source: "legal/LICENCE.txt"
- type: destination
title: Choose where to install
default_path: "{ProgramFiles}/Contoso Studio"
- type: progress
title: Installing
- type: finish
title: Ready to launch
files:
- source_dir: app
include:
- "**/*"
exclude:
- "**/*.pdb"
- "**/*.map"
- "**/tmp/**"
- source_dir: packaging
include:
- "branding/**"
- "legal/**"
file_associations:
- extension: ".cstudio"
description: "Contoso Studio project"
prog_id: "ContosoStudio.Project.1"
icon: "{AppDir}\\branding\\contoso.ico"
command: '"{AppDir}\\ContosoStudio.exe" "%1"'
mime_type: "application/x-contoso-studio"
bundle_path: "{AppDir}/Contoso Studio.app"
tasks:
pre_install:
- require: os.isWindows
action: write_registry
parameters:
key_path: "HKCU\\Software\\Contoso\\Studio"
value_name: InstallStarted
value: "true"
value_kind: string
post_install:
- require: os.isWindows
action: create_shortcut
parameters:
target_path: "{AppDir}\\ContosoStudio.exe"
name: "Contoso Studio"
location: desktop
description: "Launch Contoso Studio"
icon_path: "{AppDir}\\branding\\contoso.ico"
- require: os.isWindows
action: add_to_path
parameters:
path: "{AppDir}"
scope: user
- require: os.isLinux
action: create_desktop_entry
parameters:
file_name: "contoso-studio.desktop"
name: "Contoso Studio"
exec: "{AppDir}/ContosoStudio"
icon: "{AppDir}/branding/contoso.png"
comment: "Launch Contoso Studio"
- require: os.isUnix
action: set_permissions
parameters:
path: "{AppDir}/ContosoStudio"
mode: 755
pre_uninstall:
- require: os.isWindows
action: write_registry
parameters:
key_path: "HKCU\\Software\\Contoso\\Studio"
value_name: UninstallStarted
value: "true"
value_kind: string
Shortcut and file association behaviour
Shortcut names are display names, not full destination paths. PolyInstall chooses the correct desktop or
Start Menu directory from location and optional subfolder, then combines it with
name. On Windows, .lnk is appended automatically when name does not
already end with it; name: "Contoso Studio" and name: "Contoso Studio.lnk" both
produce a single Contoso Studio.lnk file. Prefer omitting .lnk in manifests so the
same task reads naturally on Linux and macOS.
tasks:
post_install:
- require: os.isWindows
action: create_shortcut
parameters:
target_path: "{AppDir}\\ContosoStudio.exe"
name: "Contoso Studio" # Do not include .lnk unless you need to be explicit.
location: start_menu # start_menu or desktop
subfolder: "Contoso"
description: "Launch Contoso Studio"
icon_path: "{AppDir}\\branding\\contoso.ico"
Do not package generated shortcuts
Do not include .lnk, .desktop, or symlink shortcut files in your
files payload just to place launchers on the user's machine. Include the real executable and
icon assets, then create launchers with create_shortcut or
create_desktop_entry. This keeps uninstall state accurate and lets PolyInstall place shortcuts
in the correct per-user or machine-wide location.
File associations should normally be declared with top-level file_associations entries. Use a
leading dot in extension, choose a stable prog_id that will not change between
releases, include the icon file in your payload, and quote %1 in the open command so paths with
spaces work correctly. If prog_id is omitted, PolyInstall derives one from the app name and
extension, but explicit values are clearer for long-lived products.
file_associations:
- extension: ".cstudio"
description: "Contoso Studio project"
prog_id: "ContosoStudio.Project.1"
icon: "{AppDir}\\branding\\contoso.ico"
command: '"{AppDir}\\ContosoStudio.exe" "%1"'
mime_type: "application/x-contoso-studio" # Linux; optional if the derived type is fine.
bundle_path: "{AppDir}/Contoso Studio.app" # macOS; required for macOS associations.
Use the file_association task action only when an association must be created conditionally in a
lifecycle phase. Top-level associations are easier to read, participate in feature selection, and are
registered after files are copied.
Custom stub RID note
Most users only need the manifest target tokens. The .NET runtime identifiers matter when you publish
your own stubs, extract separate release stub archives, or set build.stub_path.
| Manifest token |
Stub folder |
windows-x64 | win-x64 |
windows-arm64 | win-arm64 |
linux-x64 | linux-x64 |
linux-arm64 | linux-arm64 |
osx-x64 | osx-x64 |
osx-arm64 | osx-arm64 |
Path placeholders
| Placeholder |
Meaning |
{AppDir} | The selected install directory or extracted app directory. |
{ProgramFiles} | The OS-appropriate programme files location. |
{UserHome} | The current user's home directory. |
{Desktop} | The current user's desktop folder. |
Platform packaging notes
Windows ARP
With register_arp: true, PolyInstall records install state, copies
Uninstall.exe, and registers the app in Add/Remove Programs under HKCU or HKLM.
Linux AppImage
Set build.linux.package: appimage. AppImage creation must run on Linux with
mksquashfs available on PATH.
macOS DMG
Set build.macos.package: dmg. DMG creation requires macOS because PolyInstall invokes
hdiutil.
Optional installer signing
Signing is opt-in. If build.signing is omitted, PolyInstall produces unsigned installers.
If you configure a platform signing block, the build validates that the required identity reference is present
and signs only the matching platform artifacts. Do not put plaintext private keys or account passwords in YAML.
Windows signing
Windows uses signtool from PATH or tool_path. Configure exactly one
identity source: certificate_path, certificate_thumbprint, or
certificate_subject. Certificate passwords must come from
certificate_password_env.
macOS signing and notarisation
macOS uses codesign with an identity. DMG builds can also use an Apple
notarytool keychain profile through notarization_profile; PolyInstall submits and staples
when that profile is configured. Linux signing is not built in.
build:
signing:
windows:
certificate_thumbprint: "0123456789abcdef..."
store_name: My
store_location: current_user
timestamp_url: "http://timestamp.digicert.com"
macos:
identity: "Developer ID Application: Example Corp"
keychain: "${MACOS_KEYCHAIN_PATH}"
notarization_profile: "polyinstall-notary"
What can be used together?
| Feature |
Allowed with |
Not allowed or ignored |
Validation note |
build.signing.windows |
windows-* targets, ARP registration, PFX files, or Windows certificate store references. |
Linux/macOS-only target lists. |
Requires exactly one identity source and rejects plaintext certificate_password. |
build.signing.macos |
osx-* targets and optional build.macos.package: dmg. |
Windows/Linux-only target lists. |
identity is required. notarization_profile requires package: dmg. |
build.signing.linux |
External signing workflows after PolyInstall finishes. |
Manifest-based Linux executable signing. |
Rejected by semantic validation because Linux has no single platform signing equivalent here. |
build.linux.package: appimage |
linux-x64 or linux-arm64 targets on a Linux build host. |
Windows/macOS hosts for AppImage creation. |
Requires mksquashfs; raw Linux installer output is still created first. |
build.macos.package: dmg |
osx-x64 or osx-arm64 targets on macOS. |
Windows/Linux hosts for DMG creation. |
Requires hdiutil. DMG signing/notarisation happens after packaging. |
add_to_path task |
scope: user with any install scope; scope: machine with machine installs. |
scope: machine in a user-scope Windows install. |
Machine PATH changes require build.windows.install_scope: machine. |
Environment variable substitution
During build and validate, string values support ${VAR} and
${VAR:-default}. This is useful in CI, but avoid embedding secrets because the generated
manifest is stored inside the installer binary.
metadata:
version: "${GITHUB_REF_NAME:-0.0.0-local}"
build:
output_dir: "${DIST_DIR:-dist}"
Command reference
polyinstall build <manifest.yaml> [--base <dir>] [--stubs <dir>]
polyinstall validate <manifest.yaml> [--base <dir>]
| Option |
Purpose |
--base |
Working directory used to resolve file source directories and the default output directory. |
--stubs |
Root folder containing per-RID runtime stubs. Omit it when your target matches the included host stub; pass it when using separate stub archives or custom stubs. |
6. GitHub Actions
Build installers in CI
Use GitHub Actions to validate manifests on pull requests and build release installers from the same YAML
file you use locally. The project includes a reusable setup action that downloads a PolyInstall release,
verifies its checksum, extracts it, and adds polyinstall to PATH.
Choose a runner that matches the output you need
The release CLI archive includes the stub for its own RID. A Linux runner is the easiest path for
linux-x64 output and AppImage packaging, a Windows runner for windows-x64, and a
macOS ARM64 runner for osx-arm64 and DMG packaging. Cross-target raw installer builds are
possible when you also provide the matching stubs-<rid> archive or custom stubs directory.
Public release downloads
Keep github-token: "" when downloading the public PolyInstall release from another repository.
Set a token only when you point the action at a private release repository or mirror.
Validate manifests on pull requests
A lightweight validation workflow catches schema mistakes, missing payload paths, invalid shortcut parameters,
and incompatible task/platform settings before a release job tries to package artifacts.
name: Validate installer manifest
on:
pull_request:
paths:
- "installer/**"
- ".github/workflows/installer.yml"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bolorundurowb/PolyInstall/.github/actions/setup-polyinstall@v2.0.1
with:
version: v2.0.1
github-token: ""
- name: Validate manifest
run: polyinstall validate installer/manifest.yaml --base installer
Build and upload installers
Use a matrix when you want separate artifacts per target. The example below sets
APP_VERSION from the tag name so manifests can use
${APP_VERSION:-0.0.0-local} without hard-coding release versions. For per-target matrix builds,
set build.targets in the manifest to a substituted value such as
"${POLYINSTALL_TARGET:-windows-x64}".
name: Build installers
on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- runner: windows-latest
rid: win-x64
target: windows-x64
artifact: installer-windows-x64
- runner: ubuntu-latest
rid: linux-x64
target: linux-x64
artifact: installer-linux-x64
- runner: macos-14
rid: osx-arm64
target: osx-arm64
artifact: installer-osx-arm64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- uses: bolorundurowb/PolyInstall/.github/actions/setup-polyinstall@v2.0.1
with:
version: v2.0.1
rid: ${{ matrix.rid }}
github-token: ""
- name: Validate manifest
env:
POLYINSTALL_TARGET: ${{ matrix.target }}
run: polyinstall validate installer/manifest.yaml --base installer
- name: Build installer
env:
APP_VERSION: ${{ github.ref_name }}
POLYINSTALL_TARGET: ${{ matrix.target }}
run: polyinstall build installer/manifest.yaml --base installer
- name: Upload installer artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: installer/dist/**
Use secrets carefully
Store signing passwords and keychain material in GitHub secrets, then reference them through environment
variables such as WINDOWS_CERT_PASSWORD. Do not substitute runtime secrets into the manifest
because the resolved manifest is embedded in the installer.
Keep output predictable
Set build.output_name to a template such as
{name}-{version}-{target} so artifact upload patterns and release automation do not depend
on display-name formatting.
7. Schema reference
Manifest options in detail
PolyInstall manifests are YAML documents that validate against the published schema at
https://bolorundurowb.github.io/PolyInstall/schema/v1.json. Think of a manifest as the recipe
for your installer: it names the product, chooses where output files go, describes the installer screens,
selects the files to copy, and optionally runs setup tasks.
Recommended schema header
Add this comment to the top of a manifest to enable YAML language server completion and validation.
# yaml-language-server: $schema=https://bolorundurowb.github.io/PolyInstall/schema/v1.json
Start small
For a first installer, set metadata,
build, ui, and
files. Leave tasks
empty until you need shortcuts, registry values, desktop entries, or permissions.
Use schema keys
Manifest keys use snake_case. Let your editor load the published schema so typos such as
outputDir instead of output_dir are visible while you write the manifest.
Paths are relative
Most file paths are resolved from --base or from the extracted payload. If a file is not
found, check the command's --base value before changing the manifest.
Validation model
The published schema catches field names, basic shapes, and allowed enums in YAML-aware editors. The
validate command checks the manifest after YAML parsing and environment substitution. The
build command also runs semantic validation: target names, compression, relative file source
directories, non-empty includes, destination-before-progress wizard order, task/platform compatibility,
machine-scope PATH rules, and signing combinations.
Root object
The root object groups product metadata, build settings, UI flow, payload selection, optional integrations,
and lifecycle behaviour. Use these links when you see a field in a sample manifest and want to understand
what it controls.
| Option |
Type |
What it holds |
Beginner guidance |
metadata |
object |
Product identity used by output naming, UI labels, registry metadata, and user-facing installer text. |
Always set name and version. Add id once you need stable updates/uninstall identity. |
build |
object |
Controls output location, compression, target platforms, stubs, and platform-specific packaging. |
Start with one target, then add more targets after the first installer works. |
ui |
object |
Configures the wizard theme, branding assets, and ordered installer pages. |
Include welcome, destination, progress, and finish for a normal install wizard. |
files |
array |
Defines one or more payload file groups using source directories and glob patterns. |
This is what actually gets copied to the user's install folder. Start with "**/*". |
file_associations |
array or null |
Registers document extensions with the installed app after files are copied. |
Use for open-with behaviour. Prefer this over ad hoc registry tasks for normal file associations. |
tasks |
object or null |
Optional lifecycle actions that run before or after install and uninstall phases. |
Use only when copying files is not enough, for example creating a desktop shortcut. |
features |
array or null |
Optional user-selectable feature list used by files, tasks, and file_associations. |
Add a features wizard step when users should choose optional components. |
services |
array or null |
Optional background service or daemon registrations. |
Always include an OS require predicate so each service uses the right platform manager. |
metadata describes the product you are packaging. These values appear in generated filenames,
installer text, and platform integration such as Windows Add/Remove Programs.
| Option |
Type |
What to put here |
Example |
metadata.name |
string |
The human-readable product name. PolyInstall also uses it when deriving output filenames. |
Contoso Studio |
metadata.version |
string |
The version you are shipping. Semantic versions are easiest to reason about during updates. |
1.2.0 |
metadata.id |
string or null |
A stable identifier that should not change between releases of the same product. |
com.contoso.studio |
metadata.publisher |
string or null |
The company, person, or project that publishes the app. |
Contoso Ltd |
build
build controls what PolyInstall creates. This is where you choose output folders, compression,
target operating systems, and optional native packaging formats.
| Option |
Type |
What it does |
How to choose |
build.output_dir |
string |
Directory where generated installers are written, resolved relative to --base. |
Use dist unless your release pipeline expects another folder. |
build.output_name |
string or null |
Optional output filename template. Supports {name}, {version}, and {target}. |
Use it when your release pipeline needs predictable names such as {name}-{version}-{target}. |
build.compression |
string |
Compression format for the zip payload appended to the installer stub. |
Use brotli for smaller output. Use gzip only if you need that format for compatibility. |
build.targets |
array of strings |
Installer targets to produce, such as windows-x64, linux-x64, or osx-arm64. |
Start with the OS you can test locally. Add more once the manifest works. |
build.installer_target |
string or null |
Optional target hint used by the runtime when it needs one specific installer target context. |
Most users can leave this empty; the CLI sets target context while building each output. |
build.stub_path |
string or null |
Advanced override for locating runtime stubs. Supports {rid} when you manage custom stub folders. |
Leave empty when using release zips and their stubs/ layout. |
build.windows |
object or null |
Windows-specific install and Add/Remove Programs options. |
Use it when building Windows installers, especially if you want ARP registration. |
build.linux |
object or null |
Linux-specific packaging options. |
Use it when you want an AppImage in addition to the raw Linux installer. |
build.macos |
object or null |
macOS-specific packaging options. |
Use it when you want a DMG in addition to the raw macOS installer. |
build.signing |
object or null |
Optional Windows and macOS signing configuration. |
Leave empty for unsigned installers. Configure only the platform blocks you intend to sign. |
Windows options
install_scope: user installs per user and writes registration under HKCU. Use it for most
desktop apps because it avoids administrator prompts.
install_scope: machine writes under HKLM and requires elevation. Use it only when the app
must be available to every user on the machine.
register_arp: true adds the app to Add/Remove Programs and installs the dedicated
Uninstall.exe.
Linux options
package: none creates the raw Linux installer binary. package: appimage also
wraps it as an AppImage for a more familiar Linux distribution format.
AppImage output must run on Linux with mksquashfs installed.
macOS options
package: none creates the raw macOS installer binary. package: dmg also creates
a compressed DMG beside it.
DMG output must run on macOS because PolyInstall invokes hdiutil.
build.signing
build.signing is optional. It is validated only when present, so unsigned installers require no
signing configuration. Platform signing blocks should reference existing certificate stores, files, keychains,
or CI secrets rather than storing credentials in the manifest.
| Block |
Important fields |
Allowed values and combinations |
build.signing.windows |
tool_path, one of certificate_path, certificate_thumbprint, or certificate_subject, optional store_name, store_location, certificate_password_env, timestamp_url, digest algorithms. |
Use with windows-* targets. store_location is current_user or local_machine. Digest algorithms are sha1, sha256, sha384, or sha512. |
build.signing.macos |
identity, optional codesign_path, xcrun_path, keychain, timestamp, options, notarization_profile, staple. |
Use with osx-* targets. Notarisation requires build.macos.package: dmg because the DMG is what gets submitted and stapled. |
build.signing.linux |
No supported fields. |
Do not configure this block. Use external detached signing or checksums after the build if your Linux release process requires them. |
ui
ui controls the install wizard the user sees. It is about presentation and flow, not which files
are packaged. File selection happens in files.
| Option |
Type |
What it does |
Beginner guidance |
ui.theme |
string |
Controls the visual theme of the installer wizard. |
system follows the user's OS preference. light and dark force one mode. |
ui.logo_path |
string or null |
Optional path to an SVG or raster logo shown in the wizard header. |
The logo file must be included in your payload through files. |
ui.assets |
array or null |
Optional named assets with id and path, resolved from the extracted payload. |
Useful for packaging icons and branding. AppImage packaging can use the first PNG asset as the application icon. |
ui.wizard_steps |
array |
Ordered pages shown by the installer wizard. |
Include a progress step. Without it, the normal file-copy phase is not part of the wizard flow. |
wizard_steps entries
Each wizard step is one screen. The order in the manifest is the order the user sees.
| Step type |
What the user sees |
Important fields |
When to use it |
welcome |
An opening screen that introduces the installer. |
title |
Use as the first step for a friendly start. |
eula |
A licence or terms screen loaded from a text file. |
title, source |
Use when users must read or accept licence text before install. |
destination |
A folder picker for the install location. |
title, default_path |
Use when the user should choose where the app is installed. |
features |
A full/custom install screen with checkboxes for optional features. |
title |
Use when the manifest defines top-level features. It is skipped when no features exist. |
progress |
A progress screen while tasks run and files are copied. |
title |
Use in most installers. This is the normal place where installation work happens. |
finish |
A final confirmation screen. |
title |
Use as the last step to show that installation completed. |
files entries
files tells PolyInstall what to place inside the installer. Each entry starts at one source
directory, includes matching files, and then removes anything matched by exclude.
| Option |
Type |
What it does |
Example |
source_dir |
string |
Root directory to scan, resolved relative to --base. |
app or publish/win-x64 |
include |
array of strings |
Glob patterns to include in the payload. |
"**/*" includes everything under source_dir. |
exclude |
array or null |
Optional glob patterns removed from the included set. |
Use "**/*.pdb", "**/*.map", or "**/tmp/**" to skip files you do not want to ship. |
features |
array or null |
Optional feature ids that gate the matched files. |
Omit for core files that are always installed. Set only when users can choose optional components. |
File path mental model
If you run polyinstall build manifest.yaml --base ./release-input and set
source_dir: app, PolyInstall scans ./release-input/app. Files are stored in the
payload relative to that source directory.
file_associations entries
Top-level file_associations entries register document extensions after installation. They are
easier to maintain than platform-specific registry or desktop-database tasks, and they can be gated by
optional features in the same way as file groups and tasks.
| Option |
Type |
What it does |
Best practice |
extension |
string |
Document extension to register. |
Include the leading dot, for example .cstudio. |
description |
string |
User-facing file type description. |
Use a readable name such as Contoso Studio project. |
prog_id |
string or null |
Stable programmatic identifier for the association. |
Set an explicit value such as ContosoStudio.Project.1 for shipped apps. |
icon |
string or null |
Icon file for the associated document type. |
Include the icon in your payload and reference it with {AppDir} or a relative path. |
command |
string |
Command used when the OS opens a matching file. |
Quote both the executable path and %1, for example "{AppDir}\App.exe" "%1". |
mime_type |
string or null |
Linux MIME type. |
Optional on Linux. If omitted, PolyInstall derives application/x-<extension>. |
bundle_path |
string or null |
macOS app bundle to update for document support. |
Required for macOS associations, for example {AppDir}/Contoso.app. |
features |
array or null |
Optional feature ids that gate the association. |
Use only when the associated executable or plugin is optional. |
file_associations:
- extension: ".cstudio"
description: "Contoso Studio project"
prog_id: "ContosoStudio.Project.1"
icon: "{AppDir}\\branding\\contoso.ico"
command: '"{AppDir}\\ContosoStudio.exe" "%1"'
tasks
Lifecycle task arrays are optional. Install tasks run around file copy. Uninstall tasks run while installed
files are still present, so they can stop services, remove shortcuts, or clean configuration before the
install tree is deleted. A complete novice can ignore tasks until the installer needs to change
the system outside the install folder.
| Option |
When it runs |
Common use |
tasks.pre_install |
After the user confirms the destination and before files are copied. |
Check prerequisites or prepare a folder before files are installed. |
tasks.post_install |
After files have been copied to the install directory. |
Create shortcuts, write registry values, create desktop entries, or set permissions. |
tasks.pre_uninstall |
At the start of uninstall, before registration and files are removed. |
Stop services or close helper processes while installed files still exist. |
tasks.post_uninstall |
After pre_uninstall, but still before files and Add/Remove Programs entries are removed. |
Run cleanup that must happen after earlier uninstall tasks but before the install tree is deleted. |
InstallTask entries
| Option |
Type |
What it does |
Beginner guidance |
require |
string or null |
Optional OS predicate. Supported values include os.isWindows, os.isLinux, os.isOSX, os.isMacOS, and os.isUnix. |
Use it for platform-specific tasks. Omit it when a task should run on every OS. |
action |
string |
Task action to run. Supported actions are create_shortcut, write_registry, create_desktop_entry, set_permissions, add_to_path, and file_association. |
Choose the action first, then fill parameters with the keys that action expects. |
parameters |
object or null |
Action-specific key/value map. String values can use path placeholders such as {AppDir}, {Desktop}, and {ProgramFiles}. |
Parameter names are snake_case. The required keys depend on the selected action. |
features |
array or null |
Optional feature ids that gate this task. |
Omit for tasks that always run. Set only when the task belongs to an optional feature. |
Task action parameters
| Action |
Platforms |
Typical parameters |
What it is for |
create_shortcut |
Windows, Linux, macOS |
target_path, name, location (desktop or start_menu), optional subfolder, description, optional icon_path. |
Create a user-facing launcher, such as a desktop shortcut. |
write_registry |
Windows only |
key_path, value_name, value, and value_kind. |
Write app metadata or settings to the Windows Registry. |
create_desktop_entry |
Linux, macOS |
file_name, name, exec, optional icon, optional comment. |
Create a Freedesktop-style app launcher entry. |
set_permissions |
Unix platforms |
path and mode. |
Make a copied file executable or adjust Unix permissions after install. |
add_to_path |
Windows, Linux, macOS |
Optional path (defaults to install directory), optional scope (user or machine). |
Add an installed CLI or tool directory to PATH. Machine scope requires build.windows.install_scope: machine. |
file_association |
Windows, Linux, macOS |
extension, description, command, optional prog_id, icon, mime_type, and macOS bundle_path. |
Register or unregister an association during a lifecycle phase. Prefer top-level file_associations for normal app associations. |
Placeholders you will use often
{AppDir} means the chosen install directory, {ProgramFiles} means the OS program
files location, {UserHome} means the current user's home folder, and {Desktop}
means the current user's desktop folder.
8. Troubleshooting and FAQ
Common issues
Most build failures come from missing stubs, schema mismatches, host-specific packaging tools, or file globs
that do not match the intended payload directory.
The build says the stub cannot be found.
Confirm that the resolved stubs root contains stubs/<rid>/PolyInstall.Runtime or
PolyInstall.Runtime.exe. Windows targets also need
stubs/<rid>/PolyInstall.Uninstall.exe unless you provide a custom layout.
My manifest fails schema validation.
Use polyinstall validate manifest.yaml and check that keys use snake_case. Add the
yaml-language-server schema comment so your editor catches issues before the CLI runs.
If polyinstall build fails after schema validation passes, read the semantic validation
message: it usually points to incompatible task/platform settings, file path rules, wizard ordering, or
signing configuration.
No files are included in the installer.
Check files[].source_dir relative to --base, then review
include and exclude glob patterns. Start with "**/*" and narrow
the match once the build works.
AppImage or DMG output is missing.
AppImage output requires a Linux host with mksquashfs. DMG output requires macOS with
hdiutil. Cross-building the raw installer binary is separate from host-native packaging.
Windows shortcuts or registry tasks fail.
Check paths after placeholder expansion, confirm the install has sufficient permissions, and remember
that install_scope: machine writes to HKLM and requires elevation. For shortcuts, use the
current parameters target_path, name, and location; older
shortcut_path examples are no longer accepted.
Signing is configured but the build fails.
Signing runs only when build.signing.windows or build.signing.macos is present.
Windows needs signtool and exactly one identity source. macOS needs codesign
and an identity. Notarisation requires a DMG package and a notarytool keychain profile.
Linux signing is not supported in the manifest.
Can I combine machine install scope with PATH changes?
Yes, but only deliberately. add_to_path defaults to scope: user. If you set
scope: machine, also set build.windows.install_scope: machine and expect the
Windows installer to require elevation.
Can I put secrets in environment variables?
Do not use PolyInstall manifests for runtime secrets. Environment substitution happens before the
manifest is embedded in the installer, so substituted values can be inspected from the final binary.
9. Contributing and support
Help improve PolyInstall
PolyInstall welcomes focused issues, documentation improvements, tests, and implementation changes that keep
the manifest contract, generated schema, CLI behaviour, and runtime behaviour aligned.
Report an issue
Open a GitHub issue with the PolyInstall version, host OS, target OS, manifest excerpt, command output,
and the smallest payload structure that reproduces the problem.
Open a pull request
Create a branch from master, keep the change focused, add tests for behavioural changes,
and update docs, examples, or schema files when the manifest surface changes.
Contributor workflow
git clone https://github.com/bolorundurowb/PolyInstall.git
cd PolyInstall
dotnet restore src/PolyInstall.slnx
dotnet build src/PolyInstall.slnx -c Release --no-restore
dotnet test src/PolyInstall.slnx -c Release --no-build --verbosity normal
When changing manifest models
Regenerate schema/v1.json with dotnet run --project src/PolyInstall.SchemaGen/PolyInstall.SchemaGen.csproj
and commit the schema update with the related code.