diff --git a/docs/src/components/Mermaid.astro b/docs/src/components/Mermaid.astro new file mode 100644 index 000000000..65b7e3ca9 --- /dev/null +++ b/docs/src/components/Mermaid.astro @@ -0,0 +1,59 @@ +--- +export interface Props { + title?: string; +} + +const { title = "" } = Astro.props; +--- + + + +
+
{title}
+
Loading diagram...
+
+ Source +
+
+
diff --git a/docs/src/content/docs/contributing/architecture.mdx b/docs/src/content/docs/contributing/architecture.mdx new file mode 100644 index 000000000..6490a35bc --- /dev/null +++ b/docs/src/content/docs/contributing/architecture.mdx @@ -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 + + +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 + + +--- + +## 2 · Runtime Call Flow + + +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") + + +Key points: + +* **No HTTP / IPC** – the bridge uses the native WebView’s 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 + + +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 + + +* In **dev** the server proxies unknown paths to the framework’s 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 + + +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" + + +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. diff --git a/docs/src/content/docs/contributing/index.mdx b/docs/src/content/docs/contributing/index.mdx index b36e905b7..a6dc96497 100644 --- a/docs/src/content/docs/contributing/index.mdx +++ b/docs/src/content/docs/contributing/index.mdx @@ -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 +flowchart TD + subgraph Developer Environment + CLI[wails3 CLI
Init · Dev · Build · Package] + end + + subgraph Build-Time + GEN[Binding System
(Static Analysis & Codegen)] + ASSET[Asset Server
(Dev Proxy · Embed FS)] + PKG[Build & Packaging
Pipeline] + end + + subgraph Runtime + RUNTIME[Desktop Runtime
(Window · Events · Dialogs)] + BIND[Bridge
(Message Processor)] + end + + subgraph Application + GO[Go Backend
(App Logic)] + WEB[Web Frontend
(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 +``` +
+ +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 | diff --git a/docs/src/content/docs/guides/msix-packaging.mdx b/docs/src/content/docs/guides/msix-packaging.mdx new file mode 100644 index 000000000..971bd271e --- /dev/null +++ b/docs/src/content/docs/guides/msix-packaging.mdx @@ -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-.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` | `.msix` | Output path / filename. | +| `--publisher` | `CN=` | 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 SDK’s *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! diff --git a/docs/src/stylesheets/extra.css b/docs/src/stylesheets/extra.css index 690c38cab..47e0c5386 100644 --- a/docs/src/stylesheets/extra.css +++ b/docs/src/stylesheets/extra.css @@ -1,4 +1,6 @@ +@import './mermaid.css'; + html { scrollbar-gutter: stable; overflow-y: scroll; /* Show vertical scrollbar */ -} \ No newline at end of file +} diff --git a/docs/src/stylesheets/mermaid.css b/docs/src/stylesheets/mermaid.css new file mode 100644 index 000000000..4818131fa --- /dev/null +++ b/docs/src/stylesheets/mermaid.css @@ -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%; + } +} diff --git a/v3/internal/commands/build_assets/windows/Taskfile.yml b/v3/internal/commands/build_assets/windows/Taskfile.yml index 534f4fb31..19f137616 100644 --- a/v3/internal/commands/build_assets/windows/Taskfile.yml +++ b/v3/internal/commands/build_assets/windows/Taskfile.yml @@ -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' diff --git a/v3/internal/commands/build_assets/windows/msix/app_manifest.xml.tmpl b/v3/internal/commands/build_assets/windows/msix/app_manifest.xml.tmpl new file mode 100644 index 000000000..7697b318f --- /dev/null +++ b/v3/internal/commands/build_assets/windows/msix/app_manifest.xml.tmpl @@ -0,0 +1,67 @@ + + + + + + + {{.Info.ProductName}} + {{.Info.CompanyName}} + {{.Info.Description}} + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + {{if .FileAssociations}} + + + {{.Info.ProductName}} + Assets\FileIcon.png + {{.Info.ProductName}} File + + {{range .FileAssociations}} + .{{.Ext}} + {{end}} + + + + {{end}} + + + + + + + {{if .FileAssociations}} + + {{end}} + + diff --git a/v3/internal/commands/build_assets/windows/msix/template.xml.tmpl b/v3/internal/commands/build_assets/windows/msix/template.xml.tmpl new file mode 100644 index 000000000..69b57bdf0 --- /dev/null +++ b/v3/internal/commands/build_assets/windows/msix/template.xml.tmpl @@ -0,0 +1,68 @@ + + + + + + + + + + {{if .FileAssociations}} + + {{end}} + + + + {{if .FileAssociations}} + + + + {{range .FileAssociations}} + + .{{.Ext}} + + {{end}} + + + + {{end}} + + + + + + + + + + false + {{.Info.ProductName}} + {{.Info.CompanyName}} + {{.Info.Description}} + Assets\AppIcon.png + + + + + {{.CertificatePath}} + + diff --git a/v3/internal/commands/msix.go b/v3/internal/commands/msix.go new file mode 100644 index 000000000..457165fc7 --- /dev/null +++ b/v3/internal/commands/msix.go @@ -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 +} diff --git a/v3/internal/commands/tool_package.go b/v3/internal/commands/tool_package.go index 2bf2721a6..208f5e000 100644 --- a/v3/internal/commands/tool_package.go +++ b/v3/internal/commands/tool_package.go @@ -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 diff --git a/v3/internal/doctor/doctor_windows.go b/v3/internal/doctor/doctor_windows.go index 541041e0a..607b689fa 100644 --- a/v3/internal/doctor/doctor_windows.go +++ b/v3/internal/doctor/doctor_windows.go @@ -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() } diff --git a/v3/internal/flags/msix.go b/v3/internal/flags/msix.go new file mode 100644 index 000000000..17bbbd446 --- /dev/null +++ b/v3/internal/flags/msix.go @@ -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"` +} diff --git a/v3/internal/flags/package.go b/v3/internal/flags/package.go index f9160192b..bd56107c5 100644 --- a/v3/internal/flags/package.go +++ b/v3/internal/flags/package.go @@ -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"` }