Add MSIX packaging support for Windows applications

This commit is contained in:
Lea Anthony 2025-06-01 17:47:03 +10:00
commit 4cc8d1cc2f
No known key found for this signature in database
GPG key ID: 33DAF7BB90A58405
14 changed files with 1297 additions and 8 deletions

View file

@ -0,0 +1,59 @@
---
export interface Props {
title?: string;
}
const { title = "" } = Astro.props;
---
<script>
import mermaid from "mermaid";
// Postpone mermaid initialization
mermaid.initialize({ startOnLoad: false });
function extractMermaidCode() {
// Find all mermaid components
const mermaidElements = document.querySelectorAll("figure.expandable-diagram");
mermaidElements.forEach((element) => {
// Find the code content in the details section
const codeElement = element.querySelector("details pre code");
if (!codeElement) return;
// Extract the text content
let code = codeElement.textContent || "";
// Clean up the code
code = code.trim();
// Construct the `pre` element for the diagram code
const preElement = document.createElement("pre");
preElement.className = "mermaid not-prose";
preElement.innerHTML = code;
// Find the diagram content container and override its content
const diagramContainer = element.querySelector(".diagram-content");
if (diagramContainer) {
diagramContainer.innerHTML = "";
diagramContainer.appendChild(preElement);
}
});
}
// Wait for the DOM to be fully loaded
document.addEventListener("DOMContentLoaded", async () => {
extractMermaidCode();
mermaid.initialize({ startOnLoad: true });
});
</script>
<figure class="expandable-diagram">
<figcaption>{title}</figcaption>
<div class="diagram-content">Loading diagram...</div>
<details>
<summary>Source</summary>
<pre><code><slot /></code></pre>
</details>
</figure>

View file

@ -0,0 +1,165 @@
---
title: Wails v3 Architecture
description: Deep-dive diagrams and explanations of every moving part inside Wails v3
sidebar:
order: 1
---
import Mermaid from "../../components/Mermaid.astro";
Wails v3 is a **full-stack desktop framework** consisting of a Go runtime,
a JavaScript bridge, a task-driven tool-chain and a collection of templates that
let you ship native applications powered by modern web tech.
This page presents the *big picture* in four diagrams:
1. **Overall Architecture** how every subsystem connects
2. **Runtime Flow** what happens when JS calls Go and vice-versa
3. **Development vs Production** two modes of the asset server
4. **Platform Implementations** where OS-specific code lives
---
## 1 · Overall Architecture
<Mermaid title="Wails v3 High-Level Stack">
flowchart TD
subgraph Developer
CLI[wails3 CLI]
end
subgraph Build["Build-time Tool-chain"]
GEN["Binding Generator\n(static analysis)"]
TMP["Template Engine"]
ASSETDEV["Asset Server (dev)"]
PKG["Cross-compilation & Packaging"]
end
subgraph Runtime["Native Runtime"]
RT["Desktop Runtime\n(window, dialogs, tray, …)"]
BRIDGE["Message Bridge\n(JSON channel)"]
end
subgraph App["Your Application"]
BACKEND["Go Backend"]
FRONTEND["Web Frontend\n(React/Vue/…)"]
end
CLI -->|init| TMP
CLI -->|generate| GEN
CLI -->|dev| ASSETDEV
CLI -->|build| PKG
GEN -->|Go & TS stubs| BACKEND
GEN -->|bindings.json| FRONTEND
ASSETDEV <-->|HTTP| FRONTEND
BACKEND <--> BRIDGE <--> FRONTEND
BRIDGE <--> RT
RT <-->|serve assets| ASSETDEV
</Mermaid>
---
## 2 · Runtime Call Flow
<Mermaid title="Runtime JavaScript ⇄ Go Calling Path">
sequenceDiagram
participant JS as JavaScript (frontend)
participant Bridge as Bridge (WebView callback)
participant MP as Message Processor (Go)
participant Go as Bound Go Function
JS->>Bridge: invoke("Greet","Alice")
Bridge->>MP: JSON {t:c,id:42,...}
MP->>Go: call Greet("Alice")
Go-->>MP: "Hello Alice"
MP-->>Bridge: JSON {t:r,id:42,result:"Hello Alice"}
Bridge-->>JS: Promise.resolve("Hello Alice")
</Mermaid>
Key points:
* **No HTTP / IPC** the bridge uses the native WebViews in-memory channel
* **Method IDs** deterministic FNV-hash enables O(1) lookup in Go
* **Promises** errors propagate as rejections with stack & code
---
## 3 · Development vs Production Asset Flow
<Mermaid title="Dev ↔ Prod Asset Server">
flowchart LR
subgraph Dev["`wails3 dev`"]
VITE["Framework Dev Server\n(port 5173)"]
ASDEV["Asset Server (dev)\n(proxy + disk)"]
FRONTENDDEV[Browser]
end
subgraph Prod["`wails3 build`"]
EMBED["Embedded FS\n(go:embed)"]
ASPROD["Asset Server (prod)\n(read-only)"]
FRONTENDPROD[WebView Window]
end
VITE <-->|proxy / HMR| ASDEV
ASDEV <-->|http| FRONTENDDEV
EMBED --> ASPROD
ASPROD <-->|in-memory| FRONTENDPROD
</Mermaid>
* In **dev** the server proxies unknown paths to the frameworks live-reload
server and serves static assets from disk.
* In **prod** the same API is backed by `go:embed`, producing a zero-dependency
binary.
---
## 4 · Platform-Specific Runtime Split
<Mermaid title="Per-OS Runtime Files">
classDiagram
class runtime::Window {
+Show()
+Hide()
+Center()
}
runtime::Window <|-- Window_darwin
runtime::Window <|-- Window_linux
runtime::Window <|-- Window_windows
class Window_darwin {
//go:build darwin
+NSWindow* ptr
}
class Window_linux {
//go:build linux
+GtkWindow* ptr
}
class Window_windows {
//go:build windows
+HWND ptr
}
note for runtime::Window "Shared interface\nin pkg/application"
note for Window_darwin "Objective-C (Cgo)"
note for Window_linux "Pure Go GTK calls"
note for Window_windows "Win32 API via syscall"
</Mermaid>
Every feature follows this pattern:
1. **Common interface** in `pkg/application`
2. **Message processor** entry in `pkg/application/messageprocessor_*.go`
3. **Implementation** per OS under `internal/runtime/*.go` guarded by build tags
Missing functionality on an OS should return `ErrCapability` and register
availability via `internal/capabilities`.
---
## Summary
These diagrams outline **where the code lives**, **how data moves**, and
**which layers own which responsibilities**.
Keep them handy while exploring the detailed pages that follow they are your
map to the Wails v3 source tree.

View file

@ -6,6 +6,7 @@ sidebar:
---
import { Card, CardGrid } from "@astrojs/starlight/components";
import Mermaid from "../../components/Mermaid.astro";
## Welcome to the Wails v3 Technical Documentation
@ -47,6 +48,59 @@ context you need.
---
## Architectural Overview
<Mermaid title="Wails v3 End-to-End Flow">
```mermaid
flowchart TD
subgraph Developer Environment
CLI[wails3 CLI<br/>Init · Dev · Build · Package]
end
subgraph Build-Time
GEN[Binding System<br/>(Static Analysis & Codegen)]
ASSET[Asset Server<br/>(Dev Proxy · Embed FS)]
PKG[Build & Packaging<br/>Pipeline]
end
subgraph Runtime
RUNTIME[Desktop Runtime<br/>(Window · Events · Dialogs)]
BIND[Bridge<br/>(Message Processor)]
end
subgraph Application
GO[Go Backend<br/>(App Logic)]
WEB[Web Frontend<br/>(React/Vue/...)]
end
%% Relationships
CLI --> |"generate"| GEN
CLI --> |"dev / build"| ASSET
CLI --> |"compile & package"| PKG
GEN --> |"Code Stubs + TS"| GO
GEN --> |"Bindings JSON"| WEB
PKG --> |"Final Binary + Installer"| GO
GO --> |"Function Calls"| BIND
WEB --> |"Invoke / Events"| BIND
RUNTIME <-->|native messages| BIND
RUNTIME --> |"Display Assets"| ASSET
WEB <-->|HTTP / In-Memory| ASSET
```
</Mermaid>
The diagram shows the **end-to-end flow**:
1. **CLI** drives generation, dev server, compilation, and packaging.
2. **Binding System** produces glue code that lets the **Web Frontend** call into the **Go Backend**.
3. During development the **Asset Server** proxies to the framework dev server; in production it serves embedded files.
4. At runtime the **Desktop Runtime** manages windows and OS APIs, while the **Bridge** shuttles messages between Go and JavaScript.
---
## What This Documentation Covers
| Topic | Why It Matters |

View file

@ -0,0 +1,133 @@
# MSIX Packaging (Windows)
Wails v3 can generate modern **MSIX** installers for Windows applications, providing a cleaner, safer and Store-ready alternative to traditional **NSIS** or plain `.exe` bundles.
This guide walks through:
* Prerequisites & tool installation
* Building your app as an MSIX package
* Signing the package
* Command-line reference
* Troubleshooting
---
## 1. Prerequisites
| Requirement | Notes |
|-------------|-------|
| **Windows 10 1809+ / Windows 11** | MSIX is only supported on Windows. |
| **Windows SDK** (for `MakeAppx.exe` & `signtool.exe`) | Install from the [Windows SDK download page](https://developer.microsoft.com/windows/downloads/windows-sdk/). |
| **Microsoft MSIX Packaging Tool** (optional) | Available from the Microsoft Store provides a GUI & CLI. |
| **Code-signing certificate** (recommended) | A `.pfx` file generated by your CA or `New-SelfSignedCertificate`. |
> **Tip:** Wails ships a Task that opens the download pages for you:
```bash
# installs MakeAppx / signtool (via Windows SDK) and the MSIX Packaging Tool
wails3 task windows:install:msix:tools
```
---
## 2. Building an MSIX package
### 2.1 Quick CLI
```bash
# Production build + MSIX
wails3 tool msix \
--name MyApp \ # executable name
--executable build/bin/MyApp.exe \
--arch x64 \ # x64, x86 or arm64
--out build/bin/MyApp-x64.msix
```
The command will:
1. Create a temporary layout (`AppxManifest.xml`, `Assets/`).
2. Call **MakeAppx.exe** (default) or **MsixPackagingTool.exe** if `--use-msix-tool` is passed.
3. Optionally sign the package (see §3).
### 2.2 Using the generated Taskfile
When you ran `wails init`, Wails created `build/windows/Taskfile.yml`.
Packaging with MSIX is a one-liner:
```bash
# default=nsis, override FORMAT
wails3 task windows:package FORMAT=msix
```
Output goes to `build/bin/MyApp-<arch>.msix`.
---
## 3. Signing the package
Windows will refuse unsigned MSIX packages unless you enable developer-mode, so signing is strongly recommended.
```bash
wails3 tool msix \
--cert build/cert/CodeSign.pfx \
--cert-password "pfx-password" \
--publisher "CN=MyCompany" \
--out build/bin/MyApp.msix
```
* If you pass `--cert`, Wails automatically runs `signtool sign …`.
* `--publisher` sets the `Publisher` field inside `AppxManifest.xml`.
It **must** match the subject of your certificate.
---
## 4. Command-line reference
| Flag | Default | Description |
|------|---------|-------------|
| `--config` | `wails.json` | Project config with **Info** & `fileAssociations`. |
| `--name` | — | Executable name inside the package (no spaces). |
| `--executable` | — | Path to the built `.exe`. |
| `--arch` | `x64` | `x64`, `x86`, or `arm64`. |
| `--out` | `<name>.msix` | Output path / filename. |
| `--publisher` | `CN=<CompanyName>` | Publisher string in the manifest. |
| `--cert` | ― | Path to `.pfx` certificate for signing. |
| `--cert-password` | ― | Password for the `.pfx`. |
| `--use-msix-tool` | `false` | Use **MsixPackagingTool.exe** instead of **MakeAppx.exe**. |
| `--use-makeappx` | `true` | Force MakeAppx even if the MSIX Tool is installed. |
---
## 5. File associations
Wails automatically injects file associations declared in `wails.json` into the package manifest:
```json
"fileAssociations": [
{ "ext": "wails", "name": "Wails Project", "description": "Wails file", "role": "Editor" }
]
```
After installation, Windows will offer your app as a handler for these extensions.
---
## 6. Troubleshooting
| Problem | Solution |
|---------|----------|
| `MakeAppx.exe not found` | Install the Windows SDK and restart the terminal. |
| `signtool.exe not found` | Same as above both live in the SDKs *bin* folder. |
| *Package cannot be installed because publisher mismatch* | The certificate subject (CN) must match `--publisher`. |
| *The certificate is not trusted* | Import the certificate into **Trusted Root Certification Authorities** or use a publicly trusted code-signing cert. |
| Need GUI | Install **MSIX Packaging Tool** from the store and run `MsixPackagingTool.exe`. The template generated by Wails is fully compatible. |
---
## 7. Next steps
* [Windows Installer (NSIS) guide](./windows-installer.mdx) legacy format.
* [Cross-platform update mechanism](../updates.mdx) coming soon.
* Join the community on Discord to share feedback!
Happy packaging!

View file

@ -1,4 +1,6 @@
@import './mermaid.css';
html {
scrollbar-gutter: stable;
overflow-y: scroll; /* Show vertical scrollbar */
}
}

View file

@ -0,0 +1,108 @@
/* Mermaid diagram styling for Starlight */
/* Container for the whole diagram component */
figure.expandable-diagram {
margin: 2rem 0;
padding: 1rem;
border-radius: 0.5rem;
background-color: var(--sl-color-gray-6);
box-shadow: var(--sl-shadow-sm);
}
/* Dark mode adjustments */
:root[data-theme="dark"] figure.expandable-diagram {
background-color: var(--sl-color-gray-1);
}
/* Title for the diagram */
figure.expandable-diagram figcaption {
font-weight: bold;
font-size: 1.1rem;
margin-bottom: 1rem;
color: var(--sl-color-text);
}
/* Container for the actual diagram */
.diagram-content {
display: flex;
justify-content: center;
margin: 1rem 0;
min-height: 100px;
overflow-x: auto;
}
/* The diagram itself */
.mermaid {
background-color: var(--sl-color-white);
padding: 1rem;
border-radius: 0.375rem;
max-width: 100%;
}
:root[data-theme="dark"] .mermaid {
background-color: var(--sl-color-black);
}
/* Source code details element */
figure.expandable-diagram details {
margin-top: 1rem;
border-top: 1px solid var(--sl-color-gray-5);
padding-top: 0.5rem;
}
:root[data-theme="dark"] figure.expandable-diagram details {
border-top-color: var(--sl-color-gray-3);
}
/* Source button */
figure.expandable-diagram summary {
cursor: pointer;
color: var(--sl-color-text-accent);
font-weight: 500;
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
figure.expandable-diagram summary:hover {
background-color: var(--sl-color-gray-5);
}
:root[data-theme="dark"] figure.expandable-diagram summary:hover {
background-color: var(--sl-color-gray-2);
}
/* Source code */
figure.expandable-diagram details pre {
margin-top: 0.5rem;
padding: 0.75rem;
border-radius: 0.375rem;
background-color: var(--sl-color-gray-7);
overflow-x: auto;
}
:root[data-theme="dark"] figure.expandable-diagram details pre {
background-color: var(--sl-color-gray-0);
}
/* Mermaid diagram specific adjustments */
.mermaid .label {
font-family: var(--sl-font);
font-size: 0.9rem;
}
/* Fix for diagram text in dark mode */
:root[data-theme="dark"] .mermaid text {
fill: var(--sl-color-white);
}
/* Ensure diagrams are responsive */
@media (max-width: 768px) {
.diagram-content {
overflow-x: auto;
}
.mermaid {
min-width: 100%;
}
}

View file

@ -31,9 +31,16 @@ tasks:
PRODUCTION: '{{.PRODUCTION | default "false"}}'
package:
summary: Packages a production build of the application into a `.exe` bundle
summary: Packages a production build of the application
cmds:
- task: create:nsis:installer
- |-
if [ "{{.FORMAT | default "nsis"}}" = "msix" ]; then
task: create:msix:package
else
task: create:nsis:installer
fi
vars:
FORMAT: '{{.FORMAT | default "nsis"}}'
generate:syso:
summary: Generates Windows `.syso` file
@ -58,6 +65,34 @@ tasks:
ARCH: '{{.ARCH | default ARCH}}'
ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
create:msix:package:
summary: Creates an MSIX package
deps:
- task: build
vars:
PRODUCTION: "true"
cmds:
- |-
wails3 tool msix \
--config "{{.ROOT_DIR}}/wails.json" \
--name "{{.APP_NAME}}" \
--executable "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" \
--arch "{{.ARCH}}" \
--out "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}-{{.ARCH}}.msix" \
{{if .CERT_PATH}}--cert "{{.CERT_PATH}}"{{end}} \
{{if .PUBLISHER}}--publisher "{{.PUBLISHER}}"{{end}} \
{{if .USE_MSIX_TOOL}}--use-msix-tool{{else}}--use-makeappx{{end}}
vars:
ARCH: '{{.ARCH | default ARCH}}'
CERT_PATH: '{{.CERT_PATH | default ""}}'
PUBLISHER: '{{.PUBLISHER | default ""}}'
USE_MSIX_TOOL: '{{.USE_MSIX_TOOL | default "false"}}'
install:msix:tools:
summary: Installs tools required for MSIX packaging
cmds:
- wails3 tool msix-install-tools
run:
cmds:
- '{{.BIN_DIR}}/{{.APP_NAME}}.exe'

View file

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10">
<Identity
Name="{{.Info.ProductIdentifier}}"
Publisher="{{.Publisher}}"
Version="{{.Info.Version}}.0"
ProcessorArchitecture="{{.ProcessorArchitecture}}" />
<Properties>
<DisplayName>{{.Info.ProductName}}</DisplayName>
<PublisherDisplayName>{{.Info.CompanyName}}</PublisherDisplayName>
<Description>{{.Info.Description}}</Description>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="en-us" />
</Resources>
<Applications>
<Application Id="{{.Info.ProductIdentifier}}" Executable="{{.ExecutableName}}" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="{{.Info.ProductName}}"
Description="{{.Info.Description}}"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<desktop:Extension Category="windows.fullTrustProcess" Executable="{{.ExecutableName}}" />
{{if .FileAssociations}}
<uap:Extension Category="windows.fileTypeAssociation">
<uap:FileTypeAssociation Name="{{.Info.ProductIdentifier}}">
<uap:DisplayName>{{.Info.ProductName}}</uap:DisplayName>
<uap:Logo>Assets\FileIcon.png</uap:Logo>
<uap:InfoTip>{{.Info.ProductName}} File</uap:InfoTip>
<uap:SupportedFileTypes>
{{range .FileAssociations}}
<uap:FileType>.{{.Ext}}</uap:FileType>
{{end}}
</uap:SupportedFileTypes>
</uap:FileTypeAssociation>
</uap:Extension>
{{end}}
</Extensions>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
{{if .FileAssociations}}
<uap:Capability Name="documentsLibrary" />
{{end}}
</Capabilities>
</Package>

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<MsixPackagingToolTemplate
xmlns="http://schemas.microsoft.com/msix/packaging/msixpackagingtool/template/2022">
<Settings
AllowTelemetry="false"
ApplyACLsToPackageFiles="true"
GenerateCommandLineFile="true"
AllowPromptForPassword="false">
</Settings>
<Installer
Path="{{.ExecutablePath}}"
Arguments=""
InstallLocation="C:\Program Files\{{.Info.CompanyName}}\{{.Info.ProductName}}">
</Installer>
<PackageInformation
PackageName="{{.Info.ProductName}}"
PackageDisplayName="{{.Info.ProductName}}"
PublisherName="CN={{.Info.CompanyName}}"
PublisherDisplayName="{{.Info.CompanyName}}"
Version="{{.Info.Version}}.0"
PackageDescription="{{.Info.Description}}">
<Capabilities>
<Capability Name="runFullTrust" />
{{if .FileAssociations}}
<Capability Name="documentsLibrary" />
{{end}}
</Capabilities>
<Applications>
<Application
Id="{{.Info.ProductIdentifier}}"
Description="{{.Info.Description}}"
DisplayName="{{.Info.ProductName}}"
ExecutableName="{{.ExecutableName}}"
EntryPoint="Windows.FullTrustApplication">
{{if .FileAssociations}}
<Extensions>
<Extension Category="windows.fileTypeAssociation">
<FileTypeAssociation Name="{{.Info.ProductIdentifier}}">
{{range .FileAssociations}}
<SupportedFileTypes>
<FileType>.{{.Ext}}</FileType>
</SupportedFileTypes>
{{end}}
</FileTypeAssociation>
</Extension>
</Extensions>
{{end}}
</Application>
</Applications>
<Resources>
<Resource Language="en-us" />
</Resources>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Properties>
<Framework>false</Framework>
<DisplayName>{{.Info.ProductName}}</DisplayName>
<PublisherDisplayName>{{.Info.CompanyName}}</PublisherDisplayName>
<Description>{{.Info.Description}}</Description>
<Logo>Assets\AppIcon.png</Logo>
</Properties>
</PackageInformation>
<SaveLocation PackagePath="{{.OutputPath}}" />
<PackageIntegrity>
<CertificatePath>{{.CertificatePath}}</CertificatePath>
</PackageIntegrity>
</MsixPackagingToolTemplate>

View file

@ -0,0 +1,488 @@
package commands
import (
"embed"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"text/template"
"github.com/leaanthony/gosod"
"github.com/wailsapp/wails/v3/internal/flags"
"github.com/wailsapp/wails/v3/internal/s"
)
//go:embed build_assets/windows/msix/*
var msixAssets embed.FS
// MSIXOptions represents the configuration for MSIX packaging
type MSIXOptions struct {
// Info from project config
Info struct {
CompanyName string `json:"companyName"`
ProductName string `json:"productName"`
ProductVersion string `json:"version"`
ProductIdentifier string `json:"productIdentifier"`
Description string `json:"description"`
Copyright string `json:"copyright"`
Comments string `json:"comments"`
}
// File associations
FileAssociations []struct {
Ext string `json:"ext"`
Name string `json:"name"`
Description string `json:"description"`
IconName string `json:"iconName"`
Role string `json:"role"`
MimeType string `json:"mimeType,omitempty"`
} `json:"fileAssociations"`
// MSIX specific options
Publisher string `json:"publisher"`
CertificatePath string `json:"certificatePath"`
CertificatePassword string `json:"certificatePassword,omitempty"`
ProcessorArchitecture string `json:"processorArchitecture"`
ExecutableName string `json:"executableName"`
ExecutablePath string `json:"executablePath"`
OutputPath string `json:"outputPath"`
UseMsixPackagingTool bool `json:"useMsixPackagingTool"`
UseMakeAppx bool `json:"useMakeAppx"`
}
// ToolMSIX creates an MSIX package for Windows applications
func ToolMSIX(options *flags.ToolMSIX) error {
DisableFooter = true
if runtime.GOOS != "windows" {
return fmt.Errorf("MSIX packaging is only supported on Windows")
}
// Check if required tools are installed
if err := checkMSIXTools(options); err != nil {
return err
}
// Load project configuration
configPath := options.ConfigPath
if configPath == "" {
configPath = "wails.json"
}
// Read the config file
configData, err := os.ReadFile(configPath)
if err != nil {
return fmt.Errorf("error reading config file: %w", err)
}
// Parse the config
var config struct {
Info map[string]interface{} `json:"info"`
FileAssociations []map[string]interface{} `json:"fileAssociations"`
}
if err := json.Unmarshal(configData, &config); err != nil {
return fmt.Errorf("error parsing config file: %w", err)
}
// Create MSIX options
msixOptions := MSIXOptions{
Publisher: options.Publisher,
CertificatePath: options.CertificatePath,
CertificatePassword: options.CertificatePassword,
ProcessorArchitecture: options.Arch,
ExecutableName: options.ExecutableName,
ExecutablePath: options.ExecutablePath,
OutputPath: options.OutputPath,
UseMsixPackagingTool: options.UseMsixPackagingTool,
UseMakeAppx: options.UseMakeAppx,
}
// Copy info from config
infoBytes, err := json.Marshal(config.Info)
if err != nil {
return fmt.Errorf("error marshaling info: %w", err)
}
if err := json.Unmarshal(infoBytes, &msixOptions.Info); err != nil {
return fmt.Errorf("error unmarshaling info: %w", err)
}
// Copy file associations from config
if len(config.FileAssociations) > 0 {
faBytes, err := json.Marshal(config.FileAssociations)
if err != nil {
return fmt.Errorf("error marshaling file associations: %w", err)
}
if err := json.Unmarshal(faBytes, &msixOptions.FileAssociations); err != nil {
return fmt.Errorf("error unmarshaling file associations: %w", err)
}
}
// Validate options
if err := validateMSIXOptions(&msixOptions); err != nil {
return err
}
// Create MSIX package
if msixOptions.UseMsixPackagingTool {
return createMSIXWithPackagingTool(&msixOptions)
} else if msixOptions.UseMakeAppx {
return createMSIXWithMakeAppx(&msixOptions)
}
// Default to MakeAppx if neither is specified
return createMSIXWithMakeAppx(&msixOptions)
}
// checkMSIXTools checks if the required tools for MSIX packaging are installed
func checkMSIXTools(options *flags.ToolMSIX) error {
// Check if MsixPackagingTool is installed if requested
if options.UseMsixPackagingTool {
cmd := exec.Command("powershell", "-Command", "Get-AppxPackage -Name Microsoft.MsixPackagingTool")
if err := cmd.Run(); err != nil {
return fmt.Errorf("Microsoft MSIX Packaging Tool is not installed. Please install it from the Microsoft Store")
}
}
// Check if MakeAppx is available if requested
if options.UseMakeAppx {
cmd := exec.Command("where", "MakeAppx.exe")
if err := cmd.Run(); err != nil {
return fmt.Errorf("MakeAppx.exe is not found in PATH. Please install the Windows SDK")
}
}
// If neither is specified, check for MakeAppx as the default
if !options.UseMsixPackagingTool && !options.UseMakeAppx {
cmd := exec.Command("where", "MakeAppx.exe")
if err := cmd.Run(); err != nil {
return fmt.Errorf("MakeAppx.exe is not found in PATH. Please install the Windows SDK")
}
}
// Check if signtool is available for signing
if options.CertificatePath != "" {
cmd := exec.Command("where", "signtool.exe")
if err := cmd.Run(); err != nil {
return fmt.Errorf("signtool.exe is not found in PATH. Please install the Windows SDK")
}
}
return nil
}
// validateMSIXOptions validates the MSIX options
func validateMSIXOptions(options *MSIXOptions) error {
// Check required fields
if options.Info.ProductName == "" {
return fmt.Errorf("product name is required")
}
if options.Info.ProductIdentifier == "" {
return fmt.Errorf("product identifier is required")
}
if options.Info.CompanyName == "" {
return fmt.Errorf("company name is required")
}
if options.ExecutableName == "" {
return fmt.Errorf("executable name is required")
}
if options.ExecutablePath == "" {
return fmt.Errorf("executable path is required")
}
// Validate executable path
if _, err := os.Stat(options.ExecutablePath); os.IsNotExist(err) {
return fmt.Errorf("executable file not found: %s", options.ExecutablePath)
}
// Validate certificate path if provided
if options.CertificatePath != "" {
if _, err := os.Stat(options.CertificatePath); os.IsNotExist(err) {
return fmt.Errorf("certificate file not found: %s", options.CertificatePath)
}
}
// Set default processor architecture if not provided
if options.ProcessorArchitecture == "" {
options.ProcessorArchitecture = "x64"
}
// Set default publisher if not provided
if options.Publisher == "" {
options.Publisher = fmt.Sprintf("CN=%s", options.Info.CompanyName)
}
// Set default output path if not provided
if options.OutputPath == "" {
options.OutputPath = filepath.Join(".", fmt.Sprintf("%s.msix", options.Info.ProductName))
}
return nil
}
// createMSIXWithPackagingTool creates an MSIX package using the Microsoft MSIX Packaging Tool
func createMSIXWithPackagingTool(options *MSIXOptions) error {
// Create a temporary directory for the template
tempDir, err := os.MkdirTemp("", "wails-msix-")
if err != nil {
return fmt.Errorf("error creating temporary directory: %w", err)
}
defer os.RemoveAll(tempDir)
// Generate the template file
templatePath := filepath.Join(tempDir, "template.xml")
if err := generateMSIXTemplate(options, templatePath); err != nil {
return fmt.Errorf("error generating MSIX template: %w", err)
}
// Create the MSIX package
fmt.Println("Creating MSIX package using Microsoft MSIX Packaging Tool...")
args := []string{"create-package", "--template", templatePath}
// Add certificate password if provided
if options.CertificatePassword != "" {
args = append(args, "--certPassword", options.CertificatePassword)
}
cmd := exec.Command("MsixPackagingTool.exe", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("error creating MSIX package: %w", err)
}
fmt.Printf("MSIX package created successfully: %s\n", options.OutputPath)
return nil
}
// createMSIXWithMakeAppx creates an MSIX package using MakeAppx.exe
func createMSIXWithMakeAppx(options *MSIXOptions) error {
// Create a temporary directory for the package structure
tempDir, err := os.MkdirTemp("", "wails-msix-")
if err != nil {
return fmt.Errorf("error creating temporary directory: %w", err)
}
defer os.RemoveAll(tempDir)
// Create the package structure
if err := createMSIXPackageStructure(options, tempDir); err != nil {
return fmt.Errorf("error creating MSIX package structure: %w", err)
}
// Create the MSIX package
fmt.Println("Creating MSIX package using MakeAppx.exe...")
cmd := exec.Command("MakeAppx.exe", "pack", "/d", tempDir, "/p", options.OutputPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("error creating MSIX package: %w", err)
}
// Sign the package if certificate is provided
if options.CertificatePath != "" {
fmt.Println("Signing MSIX package...")
signArgs := []string{"sign", "/fd", "SHA256", "/a", "/f", options.CertificatePath}
// Add certificate password if provided
if options.CertificatePassword != "" {
signArgs = append(signArgs, "/p", options.CertificatePassword)
}
signArgs = append(signArgs, options.OutputPath)
cmd = exec.Command("signtool.exe", signArgs...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("error signing MSIX package: %w", err)
}
}
fmt.Printf("MSIX package created successfully: %s\n", options.OutputPath)
return nil
}
// generateMSIXTemplate generates the MSIX template file for the Microsoft MSIX Packaging Tool
func generateMSIXTemplate(options *MSIXOptions, outputPath string) error {
// Read the template file
templateData, err := msixAssets.ReadFile("build_assets/windows/msix/template.xml.tmpl")
if err != nil {
return fmt.Errorf("error reading template file: %w", err)
}
// Parse the template
tmpl, err := template.New("msix-template").Parse(string(templateData))
if err != nil {
return fmt.Errorf("error parsing template: %w", err)
}
// Create the output file
file, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("error creating output file: %w", err)
}
defer file.Close()
// Execute the template
if err := tmpl.Execute(file, options); err != nil {
return fmt.Errorf("error executing template: %w", err)
}
return nil
}
// createMSIXPackageStructure creates the MSIX package structure for MakeAppx.exe
func createMSIXPackageStructure(options *MSIXOptions, outputDir string) error {
// Create the Assets directory
assetsDir := filepath.Join(outputDir, "Assets")
if err := os.MkdirAll(assetsDir, 0755); err != nil {
return fmt.Errorf("error creating Assets directory: %w", err)
}
// Generate the AppxManifest.xml file
manifestPath := filepath.Join(outputDir, "AppxManifest.xml")
if err := generateAppxManifest(options, manifestPath); err != nil {
return fmt.Errorf("error generating AppxManifest.xml: %w", err)
}
// Copy the executable
executableDest := filepath.Join(outputDir, filepath.Base(options.ExecutablePath))
if err := copyFile(options.ExecutablePath, executableDest); err != nil {
return fmt.Errorf("error copying executable: %w", err)
}
// Copy any additional files needed for the application
// This would include DLLs, resources, etc.
// For now, we'll just copy the executable
// Generate placeholder assets
assets := []string{
"Square150x150Logo.png",
"Square44x44Logo.png",
"Wide310x150Logo.png",
"SplashScreen.png",
"StoreLogo.png",
}
// Add FileIcon.png if there are file associations
if len(options.FileAssociations) > 0 {
assets = append(assets, "FileIcon.png")
}
// Generate placeholder assets
for _, asset := range assets {
assetPath := filepath.Join(assetsDir, asset)
if err := generatePlaceholderImage(assetPath); err != nil {
return fmt.Errorf("error generating placeholder image %s: %w", asset, err)
}
}
return nil
}
// generateAppxManifest generates the AppxManifest.xml file
func generateAppxManifest(options *MSIXOptions, outputPath string) error {
// Read the template file
templateData, err := msixAssets.ReadFile("build_assets/windows/msix/app_manifest.xml.tmpl")
if err != nil {
return fmt.Errorf("error reading template file: %w", err)
}
// Parse the template
tmpl, err := template.New("appx-manifest").Parse(string(templateData))
if err != nil {
return fmt.Errorf("error parsing template: %w", err)
}
// Create the output file
file, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("error creating output file: %w", err)
}
defer file.Close()
// Execute the template
if err := tmpl.Execute(file, options); err != nil {
return fmt.Errorf("error executing template: %w", err)
}
return nil
}
// generatePlaceholderImage generates a placeholder image file
func generatePlaceholderImage(outputPath string) error {
// For now, we'll create a simple 1x1 transparent PNG
// In a real implementation, we would generate proper icons based on the application icon
// Create a minimal valid PNG file (1x1 transparent pixel)
pngData := []byte{
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00,
0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00,
0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49,
0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
}
return os.WriteFile(outputPath, pngData, 0644)
}
// copyFile copies a file from src to dst
func copyFile(src, dst string) error {
// Read the source file
data, err := os.ReadFile(src)
if err != nil {
return err
}
// Write the destination file
return os.WriteFile(dst, data, 0644)
}
// InstallMSIXTools installs the required tools for MSIX packaging
func InstallMSIXTools() error {
// Check if running on Windows
if runtime.GOOS != "windows" {
return fmt.Errorf("MSIX packaging is only supported on Windows")
}
fmt.Println("Installing MSIX packaging tools...")
// Install MSIX Packaging Tool from Microsoft Store
fmt.Println("Installing Microsoft MSIX Packaging Tool from Microsoft Store...")
cmd := exec.Command("powershell", "-Command", "Start-Process ms-windows-store://pdp/?ProductId=9N5R1TQPJVBP")
if err := cmd.Run(); err != nil {
return fmt.Errorf("error launching Microsoft Store: %w", err)
}
// Check if Windows SDK is installed
fmt.Println("Checking for Windows SDK...")
sdkInstalled := false
cmd = exec.Command("where", "MakeAppx.exe")
if err := cmd.Run(); err == nil {
sdkInstalled = true
fmt.Println("Windows SDK is already installed.")
}
// Install Windows SDK if not installed
if !sdkInstalled {
fmt.Println("Windows SDK is not installed. Please download and install from:")
fmt.Println("https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/")
// Open the download page
cmd = exec.Command("powershell", "-Command", "Start-Process https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/")
if err := cmd.Run(); err != nil {
return fmt.Errorf("error opening Windows SDK download page: %w", err)
}
}
fmt.Println("MSIX packaging tools installation initiated. Please complete the installation process in the opened windows.")
return nil
}
// init registers the MSIX command
func init() {
// Register the MSIX command in the CLI
// This will be called by the CLI framework
}

View file

@ -4,17 +4,23 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/wailsapp/wails/v3/internal/commands/dmg"
"github.com/wailsapp/wails/v3/internal/flags"
"github.com/wailsapp/wails/v3/internal/packager"
)
// ToolPackage generates a Linux package in the specified format using nfpm
// ToolPackage generates a package in the specified format
func ToolPackage(options *flags.ToolPackage) error {
DisableFooter = true
if options.ConfigPath == "" {
// Check if we're creating a DMG
isDMG := strings.ToLower(options.Format) == "dmg" || options.CreateDMG
// Config file is required for Linux packages but optional for DMG
if options.ConfigPath == "" && !isDMG {
return fmt.Errorf("please provide a config file using the -config flag")
}
@ -22,7 +28,48 @@ func ToolPackage(options *flags.ToolPackage) error {
return fmt.Errorf("please provide an executable name using the -name flag")
}
// Validate format
// Handle DMG creation for macOS
if isDMG {
if runtime.GOOS != "darwin" {
return fmt.Errorf("DMG creation is only supported on macOS")
}
// For DMG, we expect the .app bundle to already exist
appPath := filepath.Join(options.Out, fmt.Sprintf("%s.app", options.ExecutableName))
if _, err := os.Stat(appPath); os.IsNotExist(err) {
return fmt.Errorf("application bundle not found: %s", appPath)
}
// Create output path for DMG
dmgPath := filepath.Join(options.Out, fmt.Sprintf("%s.dmg", options.ExecutableName))
// Create DMG creator
dmgCreator, err := dmg.New(appPath, dmgPath, options.ExecutableName)
if err != nil {
return fmt.Errorf("error creating DMG: %w", err)
}
// Set background image if provided
if options.BackgroundImage != "" {
if err := dmgCreator.SetBackgroundImage(options.BackgroundImage); err != nil {
return fmt.Errorf("error setting background image: %w", err)
}
}
// Set default icon positions
dmgCreator.AddIconPosition(filepath.Base(appPath), 150, 175)
dmgCreator.AddIconPosition("Applications", 450, 175)
// Create the DMG
if err := dmgCreator.Create(); err != nil {
return fmt.Errorf("error creating DMG: %w", err)
}
fmt.Printf("DMG created successfully: %s\n", dmgPath)
return nil
}
// For Linux packages, continue with existing logic
var pkgType packager.PackageType
switch strings.ToLower(options.Format) {
case "deb":
@ -32,7 +79,7 @@ func ToolPackage(options *flags.ToolPackage) error {
case "archlinux":
pkgType = packager.ARCH
default:
return fmt.Errorf("unsupported package format '%s'. Supported formats: deb, rpm, archlinux", options.Format)
return fmt.Errorf("unsupported package format '%s'. Supported formats: deb, rpm, archlinux, dmg", options.Format)
}
// Get absolute path of config file

View file

@ -6,6 +6,7 @@ import (
"github.com/samber/lo"
"github.com/wailsapp/go-webview2/webviewloader"
"os/exec"
"strings"
)
func getInfo() (map[string]string, bool) {
@ -30,8 +31,42 @@ func getNSISVersion() string {
return string(output)
}
func getMakeAppxVersion() string {
// Check if MakeAppx.exe is available (part of Windows SDK)
_, err := exec.LookPath("MakeAppx.exe")
if err != nil {
return "Not Installed"
}
return "Installed"
}
func getMSIXPackagingToolVersion() string {
// Check if MSIX Packaging Tool is installed
// Use PowerShell to check if the app is installed from Microsoft Store
cmd := exec.Command("powershell", "-Command", "Get-AppxPackage -Name Microsoft.MsixPackagingTool")
output, err := cmd.Output()
if err != nil || len(output) == 0 || !strings.Contains(string(output), "Microsoft.MsixPackagingTool") {
return "Not Installed"
}
return "Installed"
}
func getSignToolVersion() string {
// Check if signtool.exe is available (part of Windows SDK)
_, err := exec.LookPath("signtool.exe")
if err != nil {
return "Not Installed"
}
return "Installed"
}
func checkPlatformDependencies(result map[string]string, ok *bool) {
checkCommonDependencies(result, ok)
// add nsis
result["NSIS"] = getNSISVersion()
// Add MSIX tooling checks
result["MakeAppx.exe (Windows SDK)"] = getMakeAppxVersion()
result["MSIX Packaging Tool"] = getMSIXPackagingToolVersion()
result["SignTool.exe (Windows SDK)"] = getSignToolVersion()
}

26
v3/internal/flags/msix.go Normal file
View file

@ -0,0 +1,26 @@
package flags
// ToolMSIX represents the options for the MSIX packaging command
type ToolMSIX struct {
Common
// Project configuration
ConfigPath string `name:"config" description:"Path to the project configuration file" default:"wails.json"`
// MSIX package information
Publisher string `name:"publisher" description:"Publisher name for the MSIX package (e.g., CN=CompanyName)" default:""`
// Certificate for signing
CertificatePath string `name:"cert" description:"Path to the certificate file for signing the MSIX package" default:""`
CertificatePassword string `name:"cert-password" description:"Password for the certificate file" default:""`
// Build options
Arch string `name:"arch" description:"Architecture of the package (x64, x86, arm64)" default:"x64"`
ExecutableName string `name:"name" description:"Name of the executable in the package" default:""`
ExecutablePath string `name:"executable" description:"Path to the executable file to package" default:""`
OutputPath string `name:"out" description:"Path where the MSIX package will be saved" default:""`
// Tool selection
UseMsixPackagingTool bool `name:"use-msix-tool" description:"Use the Microsoft MSIX Packaging Tool for packaging" default:"false"`
UseMakeAppx bool `name:"use-makeappx" description:"Use MakeAppx.exe for packaging" default:"true"`
}

View file

@ -4,8 +4,10 @@ package flags
type ToolPackage struct {
Common
Format string `name:"format" description:"Package format to generate (deb, rpm, archlinux)" default:"deb"`
Format string `name:"format" description:"Package format to generate (deb, rpm, archlinux, dmg)" default:"deb"`
ExecutableName string `name:"name" description:"Name of the executable to package" default:"myapp"`
ConfigPath string `name:"config" description:"Path to the package configuration file" default:""`
Out string `name:"out" description:"Path to the output dir" default:"."`
BackgroundImage string `name:"background" description:"Path to an optional background image for the DMG" default:""`
CreateDMG bool `name:"create-dmg" description:"Create a DMG file (macOS only)" default:"false"`
}