[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:
stffabi
2022-03-02 19:44:31 +11:00
committed by GitHub
parent c63b1f1981
commit b02dbfaddf
27 changed files with 781 additions and 166 deletions
+58 -2
View File
@@ -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
}
+6 -12
View File
@@ -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
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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
}
+119
View File
@@ -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
}
+35 -76
View File
@@ -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