mirror of
https://github.com/wavetermdev/wails.git
synced 2026-08-05 13:53:43 -07:00
[v2] NSIS installer support for Windows (#1184)
* [v2] Add support for post build hooks Currently only supports build-level hooks * [v2] Improve build assets handling and use single source for manifest generation The manifest asset files are now a go template and data will be resolved before they are included into the build output. Breaking Change: Windows manifest file must be named “wails.exe.manifest” and doesn’t depend on the project name anymore. * [v2, windows] NSIS installer generation
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/fs"
|
||||
|
||||
@@ -213,8 +214,63 @@ func Build(options *Options) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result := options.CompiledBinary
|
||||
compileBinary := options.CompiledBinary
|
||||
hookArgs := map[string]string{
|
||||
"${platform}": options.Platform + "/" + options.Arch,
|
||||
"${bin}": compileBinary,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
for _, hook := range []string{options.Platform + "/" + options.Arch, options.Platform + "/*", "*/*"} {
|
||||
if err := execPostBuildHook(outputLogger, options, hook, hookArgs); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return compileBinary, nil
|
||||
}
|
||||
|
||||
func execPostBuildHook(outputLogger *clilogger.CLILogger, options *Options, hookIdentifier string, argReplacements map[string]string) error {
|
||||
postBuildHook := options.ProjectData.PostBuildHooks[hookIdentifier]
|
||||
if postBuildHook == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !options.ProjectData.RunNonNativeBuildHooks {
|
||||
if hookIdentifier == "" {
|
||||
// That's the global hook
|
||||
} else {
|
||||
platformOfHook := strings.Split(hookIdentifier, "/")[0]
|
||||
if platformOfHook == "*" {
|
||||
// Thats OK, we don't have a specific platform of the hook
|
||||
} else if platformOfHook == runtime.GOOS {
|
||||
// The hook is for host platform
|
||||
} else {
|
||||
// Skip a hook which is not native
|
||||
outputLogger.Println(" - Non native build hook '%s': Skipping.", hookIdentifier)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outputLogger.Print(" - Executing post build hook '%s': ", hookIdentifier)
|
||||
args := strings.Split(postBuildHook, " ")
|
||||
for i, arg := range args {
|
||||
newArg := argReplacements[arg]
|
||||
if newArg == "" {
|
||||
continue
|
||||
}
|
||||
args[i] = newArg
|
||||
}
|
||||
|
||||
if options.Verbosity == VERBOSE {
|
||||
outputLogger.Println("%s", strings.Join(args, " "))
|
||||
}
|
||||
|
||||
_, stderr, err := shell.RunCommand(options.BuildDirectory, args[0], args[1:]...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s - %s", err.Error(), stderr)
|
||||
}
|
||||
outputLogger.Println("Done.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package build
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/fs"
|
||||
"github.com/wailsapp/wails/v2/internal/html"
|
||||
@@ -27,7 +26,7 @@ func (d *DesktopBuilder) BuildAssets(options *Options) error {
|
||||
// Check assets directory exists
|
||||
if !fs.DirExists(options.ProjectData.BuildDir) {
|
||||
// Path to default assets
|
||||
err := buildassets.Install(options.ProjectData.Path, options.ProjectData.Name)
|
||||
err := buildassets.Install(options.ProjectData.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -81,7 +80,7 @@ func (d *DesktopBuilder) BuildBaseAssets(assets *html.AssetBundle, options *Opti
|
||||
d.addFileToDelete(assetsFile)
|
||||
|
||||
// Process Icon
|
||||
err = d.processApplicationIcon(assetDir)
|
||||
err = d.processApplicationIcon(assetDir, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -105,15 +104,10 @@ func (d *DesktopBuilder) BuildBaseAssets(assets *html.AssetBundle, options *Opti
|
||||
|
||||
// processApplicationIcon will copy a default icon if one doesn't exist, then, if
|
||||
// needed, will compile the icon
|
||||
func (d *DesktopBuilder) processApplicationIcon(assetDir string) error {
|
||||
|
||||
// Copy default icon if one doesn't exist
|
||||
iconFile := filepath.Join(d.projectData.BuildDir, "appicon.png")
|
||||
if !fs.FileExists(iconFile) {
|
||||
err := buildassets.RegenerateAppIcon(iconFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func (d *DesktopBuilder) processApplicationIcon(assetDir string, options *Options) error {
|
||||
iconFile, err := buildassets.ReadFile(options.ProjectData, "appicon.png")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Compile Icon
|
||||
|
||||
@@ -24,7 +24,7 @@ func (d *DesktopBuilder) convertToHexLiteral(bytes []byte) string {
|
||||
}
|
||||
|
||||
// compileIcon will compile the icon found at <projectdir>/icon.png into the application
|
||||
func (d *DesktopBuilder) compileIcon(assetDir string, iconFile string) error {
|
||||
func (d *DesktopBuilder) compileIcon(assetDir string, iconFile []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package build
|
||||
|
||||
// compileIcon will compile the icon found at <projectdir>/icon.png into the application
|
||||
func (d *DesktopBuilder) compileIcon(assetDir string, iconFile string) error {
|
||||
func (d *DesktopBuilder) compileIcon(assetDir string, iconFile []byte) error {
|
||||
//
|
||||
//// Load icon into a databuffer
|
||||
//targetFilename := "icon"
|
||||
|
||||
@@ -106,7 +106,7 @@ func (d *DesktopBuilder) processTrayIcons(assetDir string, options *Options) err
|
||||
}
|
||||
|
||||
// compileIcon will compile the icon found at <projectdir>/icon.png into the application
|
||||
func (d *DesktopBuilder) compileIcon(assetDir string, iconFile string) error {
|
||||
func (d *DesktopBuilder) compileIcon(assetDir string, iconFile []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/fs"
|
||||
"github.com/wailsapp/wails/v2/internal/shell"
|
||||
"github.com/wailsapp/wails/v2/internal/webview2runtime"
|
||||
"github.com/wailsapp/wails/v2/pkg/buildassets"
|
||||
)
|
||||
|
||||
const (
|
||||
nsisTypeSingle = "single"
|
||||
nsisTypeMultiple = "multiple"
|
||||
|
||||
nsisFolder = "windows/installer"
|
||||
nsisProjectFile = "project.nsi"
|
||||
nsisToolsFile = "wails_tools.nsh"
|
||||
nsisWebView2SetupFile = "tmp/MicrosoftEdgeWebview2Setup.exe"
|
||||
)
|
||||
|
||||
func GenerateNSISInstaller(options *Options, amd64Binary string, arm64Binary string) error {
|
||||
outputLogger := options.Logger
|
||||
outputLogger.Println("Creating NSIS installer\n------------------------------")
|
||||
|
||||
// Ensure the file exists, if not the template will be written.
|
||||
projectFile := path.Join(nsisFolder, nsisProjectFile)
|
||||
if _, err := buildassets.ReadFile(options.ProjectData, projectFile); err != nil {
|
||||
return fmt.Errorf("Unable to generate NSIS installer project template: %w", err)
|
||||
}
|
||||
|
||||
// Write the resolved nsis tools
|
||||
toolsFile := path.Join(nsisFolder, nsisToolsFile)
|
||||
if _, err := buildassets.ReadOriginalFileWithProjectDataAndSave(options.ProjectData, toolsFile); err != nil {
|
||||
return fmt.Errorf("Unable to generate NSIS tools file: %w", err)
|
||||
}
|
||||
|
||||
// Write the WebView2 SetupFile
|
||||
webviewSetup := buildassets.GetLocalPath(options.ProjectData, path.Join(nsisFolder, nsisWebView2SetupFile))
|
||||
if dir := filepath.Dir(webviewSetup); !fs.DirExists(dir) {
|
||||
if err := fs.MkDirs(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := webview2runtime.WriteInstallerToFile(webviewSetup); err != nil {
|
||||
return fmt.Errorf("Unable to write Webview2 Bootstrapper Setup: %w", err)
|
||||
}
|
||||
|
||||
if !shell.CommandExists("makensis") {
|
||||
outputLogger.Println("Warning: Cannot create installer: makensis not found")
|
||||
return nil
|
||||
}
|
||||
|
||||
nsisType := options.ProjectData.NSISType
|
||||
if nsisType == nsisTypeSingle && (amd64Binary == "" || arm64Binary == "") {
|
||||
nsisType = ""
|
||||
}
|
||||
|
||||
switch nsisType {
|
||||
case "":
|
||||
fallthrough
|
||||
case nsisTypeMultiple:
|
||||
if amd64Binary != "" {
|
||||
if err := makeNSIS(options, "amd64", amd64Binary, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if arm64Binary != "" {
|
||||
if err := makeNSIS(options, "arm64", "", arm64Binary); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case nsisTypeSingle:
|
||||
if err := makeNSIS(options, "single", amd64Binary, arm64Binary); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("Unsupported nsisType: %s", nsisType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeNSIS(options *Options, installerKind string, amd64Binary string, arm64Binary string) error {
|
||||
verbose := options.Verbosity == VERBOSE
|
||||
outputLogger := options.Logger
|
||||
|
||||
outputLogger.Print(" - Building '%s' installer: ", installerKind)
|
||||
var args = []string{}
|
||||
if amd64Binary != "" {
|
||||
args = append(args, "-DARG_WAILS_AMD64_BINARY="+amd64Binary)
|
||||
}
|
||||
if arm64Binary != "" {
|
||||
args = append(args, "-DARG_WAILS_ARM64_BINARY="+arm64Binary)
|
||||
}
|
||||
args = append(args, nsisProjectFile)
|
||||
|
||||
if verbose {
|
||||
outputLogger.Println("makensis %s", strings.Join(args, " "))
|
||||
}
|
||||
|
||||
installerDir := buildassets.GetLocalPath(options.ProjectData, nsisFolder)
|
||||
stdOut, stdErr, err := shell.RunCommand(installerDir, "makensis", args...)
|
||||
if err != nil || verbose {
|
||||
outputLogger.Println(stdOut)
|
||||
outputLogger.Println(stdErr)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error during creation of the installer: %w", err)
|
||||
}
|
||||
outputLogger.Println("Done.")
|
||||
return nil
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
@@ -63,18 +63,6 @@ func cleanBuildDirectory(options *Options) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Gets (and creates) the build base directory
|
||||
func getBuildBaseDirectory(options *Options) (string, error) {
|
||||
buildDirectory := filepath.Join(options.ProjectData.Path, "build")
|
||||
if !fs.DirExists(buildDirectory) {
|
||||
err := os.MkdirAll(buildDirectory, 0700)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return buildDirectory, nil
|
||||
}
|
||||
|
||||
// Gets the platform dependent package assets directory
|
||||
func getPackageAssetsDirectory() string {
|
||||
return fs.RelativePath("internal/packager", runtime.GOOS)
|
||||
@@ -115,11 +103,7 @@ func packageApplicationForDarwin(options *Options) error {
|
||||
}
|
||||
|
||||
// Generate Icons
|
||||
buildDir, err := getBuildBaseDirectory(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = processApplicationIcon(resourceDir, buildDir)
|
||||
err = processApplicationIcon(options, resourceDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -130,53 +114,28 @@ func packageApplicationForDarwin(options *Options) error {
|
||||
}
|
||||
|
||||
func processPList(options *Options, contentsDirectory string) error {
|
||||
|
||||
// Check if plist already exists in project dir
|
||||
plistFileDir := filepath.Join(options.ProjectData.Path, "build", "darwin")
|
||||
plistFile := filepath.Join(plistFileDir, "Info.plist")
|
||||
// If the file doesn't exist, generate it
|
||||
if !fs.FileExists(plistFile) {
|
||||
err := buildassets.RegeneratePlist(plistFileDir, options.ProjectData.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Read the resolved BuildAssets file and copy it to the destination
|
||||
content, err := buildassets.ReadFileWithProjectData(options.ProjectData, "darwin/Info.plist")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy it to the contents directory
|
||||
targetFile := filepath.Join(contentsDirectory, "Info.plist")
|
||||
return fs.CopyFile(plistFile, targetFile)
|
||||
return os.WriteFile(targetFile, content, 0644)
|
||||
}
|
||||
|
||||
func processApplicationIcon(resourceDir string, iconsDir string) (err error) {
|
||||
|
||||
appIcon := filepath.Join(iconsDir, "appicon.png")
|
||||
|
||||
// Install default icon if one doesn't exist
|
||||
if !fs.FileExists(appIcon) {
|
||||
// No - Install default icon
|
||||
err = buildassets.RegenerateAppIcon(appIcon)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tgtBundle := path.Join(resourceDir, "iconfile.icns")
|
||||
imageFile, err := os.Open(appIcon)
|
||||
func processApplicationIcon(options *Options, resourceDir string) (err error) {
|
||||
appIcon, err := buildassets.ReadFile(options.ProjectData, "appicon.png")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = imageFile.Close()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
srcImg, _, err := image.Decode(imageFile)
|
||||
srcImg, _, err := image.Decode(bytes.NewBuffer(appIcon))
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
tgtBundle := filepath.Join(resourceDir, "iconfile.icns")
|
||||
dest, err := os.Create(tgtBundle)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -199,12 +158,6 @@ func packageApplicationForWindows(options *Options) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure Manifest is present
|
||||
err = generateManifest(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create syso file
|
||||
err = compileResources(options)
|
||||
if err != nil {
|
||||
@@ -238,33 +191,31 @@ func packageApplicationForLinux(options *Options) error {
|
||||
}
|
||||
|
||||
func generateManifest(options *Options) error {
|
||||
filename := options.ProjectData.Name + ".exe.manifest"
|
||||
manifestFile := filepath.Join(options.ProjectData.Path, "build", "windows", filename)
|
||||
if !fs.FileExists(manifestFile) {
|
||||
return buildassets.RegenerateManifest(manifestFile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateIcoFile(options *Options) error {
|
||||
// Check ico file exists already
|
||||
icoFile := filepath.Join(options.ProjectData.Path, "build", "windows", "icon.ico")
|
||||
icoFile := buildassets.GetLocalPath(options.ProjectData, "windows/icon.ico")
|
||||
if !fs.FileExists(icoFile) {
|
||||
// Check icon exists
|
||||
appicon := filepath.Join(options.ProjectData.Path, "build", "appicon.png")
|
||||
if !fs.FileExists(appicon) {
|
||||
return fmt.Errorf("application icon missing: %s", appicon)
|
||||
}
|
||||
// Load icon
|
||||
input, err := os.Open(appicon)
|
||||
content, err := buildassets.ReadFile(options.ProjectData, "appicon.png")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dir := filepath.Dir(icoFile); !fs.DirExists(dir) {
|
||||
if err := fs.MkDirs(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
output, err := os.OpenFile(icoFile, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = winicon.GenerateIcon(input, output, []int{256, 128, 64, 48, 32, 16})
|
||||
defer output.Close()
|
||||
|
||||
err = winicon.GenerateIcon(bytes.NewBuffer(content), output, []int{256, 128, 64, 48, 32, 16})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -302,15 +253,23 @@ func compileResources(options *Options) error {
|
||||
return err
|
||||
}
|
||||
|
||||
ManifestFilename := options.ProjectData.Name + ".exe.manifest"
|
||||
manifestData, err := os.ReadFile(ManifestFilename)
|
||||
manifestData, err := buildassets.ReadFileWithProjectData(options.ProjectData, "windows/wails.exe.manifest")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
xmlData, err := winres.AppManifestFromXML(manifestData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rs.SetManifest(xmlData)
|
||||
|
||||
if versionInfo, _ := os.ReadFile("info.json"); len(versionInfo) != 0 {
|
||||
versionInfo, err := buildassets.ReadFileWithProjectData(options.ProjectData, "windows/info.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(versionInfo) != 0 {
|
||||
var v version.Info
|
||||
if err := v.UnmarshalJSON(versionInfo); err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user