diff --git a/v2/pkg/commands/build/build.go b/v2/pkg/commands/build/build.go index 2b05bda0..a64858ae 100644 --- a/v2/pkg/commands/build/build.go +++ b/v2/pkg/commands/build/build.go @@ -115,7 +115,7 @@ func Build(options *Options) (string, error) { // compilation. This will be a .syso file in the project root if options.Pack && options.Platform == "windows" { outputLogger.Print("Generating bundle assets: ") - err := packageApplication(options) + err := packageApplicationForWindows(options) if err != nil { return "", err } diff --git a/v2/pkg/commands/build/packager.go b/v2/pkg/commands/build/packager.go index 9c1f2bd3..59816650 100644 --- a/v2/pkg/commands/build/packager.go +++ b/v2/pkg/commands/build/packager.go @@ -1,10 +1,23 @@ package build import ( + "bytes" "fmt" + "image" + "io/ioutil" "os" + "path" "path/filepath" "runtime" + "strings" + "text/template" + + "github.com/leaanthony/winicon" + "github.com/tc-hib/winres" + + "github.com/jackmordaunt/icns" + "github.com/pkg/errors" + "github.com/wailsapp/wails/v2/pkg/buildassets" "github.com/wailsapp/wails/v2/internal/fs" ) @@ -14,8 +27,10 @@ func packageProject(options *Options, platform string) error { var err error switch platform { - case "darwin", "windows": - err = packageApplication(options) + case "darwin": + err = packageApplicationForDarwin(options) + case "windows": + err = packageApplicationForWindows(options) default: err = fmt.Errorf("packing not supported for %s yet", platform) } @@ -65,3 +80,284 @@ func getBuildBaseDirectory(options *Options) (string, error) { func getPackageAssetsDirectory() string { return fs.RelativePath("internal/packager", runtime.GOOS) } + +func packageApplicationForDarwin(options *Options) error { + + var err error + + // Create directory structure + bundlename := options.ProjectData.Name + ".app" + + contentsDirectory := filepath.Join(options.BuildDirectory, bundlename, "/Contents") + exeDir := filepath.Join(contentsDirectory, "/MacOS") + err = fs.MkDirs(exeDir, 0755) + if err != nil { + return err + } + resourceDir := filepath.Join(contentsDirectory, "/Resources") + err = fs.MkDirs(resourceDir, 0755) + if err != nil { + return err + } + // Copy binary + packedBinaryPath := filepath.Join(exeDir, options.ProjectData.Name) + err = fs.MoveFile(options.CompiledBinary, packedBinaryPath) + if err != nil { + return errors.Wrap(err, "Cannot move file: "+options.ProjectData.OutputFilename) + } + + // Generate Info.plist + err = processPList(options, contentsDirectory) + if err != nil { + return err + } + + // Generate Icons + err = processApplicationIcon(resourceDir, options.ProjectData.BuildDir) + if err != nil { + return err + } + + options.CompiledBinary = packedBinaryPath + + return nil +} + +func processPList(options *Options, contentsDirectory string) error { + + // Check if plist already exists in project dir + plistFile := filepath.Join(options.ProjectData.BuildDir, "darwin", "Info.plist") + + // If the file doesn't exist, generate it + if !fs.FileExists(plistFile) { + err := generateDefaultPlist(options, plistFile) + if err != nil { + return err + } + } + + // Copy it to the contents directory + targetFile := filepath.Join(contentsDirectory, "Info.plist") + return fs.CopyFile(plistFile, targetFile) +} + +func generateDefaultPlist(options *Options, targetPlistFile string) error { + name := defaultString(options.ProjectData.Name, "WailsTest") + exe := defaultString(options.OutputFile, name) + version := "1.0.0" + author := defaultString(options.ProjectData.Author.Name, "Anonymous") + packageID := strings.Join([]string{"wails", name}, ".") + plistData := newPlistData(name, exe, packageID, version, author) + + tmpl := template.New("infoPlist") + plistTemplate := fs.RelativePath("./internal/packager/darwin/Info.plist") + infoPlist, err := ioutil.ReadFile(plistTemplate) + if err != nil { + return errors.Wrap(err, "Cannot open plist template") + } + _, err = tmpl.Parse(string(infoPlist)) + if err != nil { + return err + } + // Write the template to a buffer + var tpl bytes.Buffer + err = tmpl.Execute(&tpl, plistData) + if err != nil { + return err + } + + // Create the directory if it doesn't exist + err = fs.MkDirs(filepath.Dir(targetPlistFile)) + if err != nil { + return err + } + // Save the file + return ioutil.WriteFile(targetPlistFile, tpl.Bytes(), 0644) +} + +func defaultString(val string, defaultVal string) string { + if val != "" { + return val + } + return defaultVal +} + +type plistData struct { + Title string + Exe string + PackageID string + Version string + Author string +} + +func newPlistData(title, exe, packageID, version, author string) *plistData { + return &plistData{ + Title: title, + Exe: exe, + Version: version, + PackageID: packageID, + Author: author, + } +} + +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) + if err != nil { + return err + } + + defer func() { + err = imageFile.Close() + if err == nil { + return + } + }() + srcImg, _, err := image.Decode(imageFile) + if err != nil { + return err + + } + dest, err := os.Create(tgtBundle) + if err != nil { + return err + + } + defer func() { + err = dest.Close() + if err == nil { + return + } + }() + return icns.Encode(dest, srcImg) +} + +func packageApplicationForWindows(options *Options) error { + // Generate icon + var err error + err = generateIcoFile(options) + if err != nil { + return err + } + + // Ensure Manifest is present + err = generateManifest(options) + if err != nil { + return err + } + + // Create syso file + err = compileResources(options) + if err != nil { + return err + } + + return nil +} + +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") + 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) + if err != nil { + return err + } + output, err := os.OpenFile(icoFile, os.O_CREATE, 0644) + if err != nil { + return err + } + err = winicon.GenerateIcon(input, output, []int{256, 128, 64, 48, 32, 16}) + if err != nil { + return err + } + } + return nil +} + +func compileResources(options *Options) error { + + currentDir, err := os.Getwd() + if err != nil { + return err + } + defer func() { + os.Chdir(currentDir) + }() + windowsDir := filepath.Join(options.ProjectData.Path, "build", "windows") + err = os.Chdir(windowsDir) + if err != nil { + return err + } + rs := winres.ResourceSet{} + icon := filepath.Join(windowsDir, "icon.ico") + iconFile, err := os.Open(icon) + if err != nil { + return err + } + defer iconFile.Close() + ico, err := winres.LoadICO(iconFile) + if err != nil { + return err + } + err = rs.SetIcon(winres.RT_ICON, ico) + if err != nil { + return err + } + + ManifestFilename := options.ProjectData.Name + ".exe.manifest" + manifestData, err := os.ReadFile(ManifestFilename) + xmlData, err := winres.AppManifestFromXML(manifestData) + if err != nil { + return err + } + rs.SetManifest(xmlData) + + targetFile := filepath.Join(options.ProjectData.Path, options.ProjectData.Name+"-res.syso") + fout, err := os.Create(targetFile) + if err != nil { + return err + } + defer fout.Close() + + archs := map[string]winres.Arch{ + "amd64": winres.ArchAMD64, + } + targetArch, supported := archs[options.Arch] + if !supported { + return fmt.Errorf("arch '%s' not supported", options.Arch) + } + + err = rs.WriteObject(fout, targetArch) + if err != nil { + return err + } + return nil +} diff --git a/v2/pkg/commands/build/packager_darwin.go b/v2/pkg/commands/build/packager_darwin.go deleted file mode 100644 index 76aab000..00000000 --- a/v2/pkg/commands/build/packager_darwin.go +++ /dev/null @@ -1,181 +0,0 @@ -package build - -import ( - "bytes" - "image" - "io/ioutil" - "os" - "path" - "path/filepath" - "strings" - "text/template" - - "github.com/wailsapp/wails/v2/pkg/buildassets" - - "github.com/jackmordaunt/icns" - "github.com/pkg/errors" - "github.com/wailsapp/wails/v2/internal/fs" -) - -func packageApplication(options *Options) error { - - var err error - - // Create directory structure - bundlename := options.ProjectData.Name + ".app" - - contentsDirectory := filepath.Join(options.BuildDirectory, bundlename, "/Contents") - exeDir := filepath.Join(contentsDirectory, "/MacOS") - err = fs.MkDirs(exeDir, 0755) - if err != nil { - return err - } - resourceDir := filepath.Join(contentsDirectory, "/Resources") - err = fs.MkDirs(resourceDir, 0755) - if err != nil { - return err - } - // Copy binary - packedBinaryPath := filepath.Join(exeDir, options.ProjectData.Name) - err = fs.MoveFile(options.CompiledBinary, packedBinaryPath) - if err != nil { - return errors.Wrap(err, "Cannot move file: "+options.ProjectData.OutputFilename) - } - - // Generate Info.plist - err = processPList(options, contentsDirectory) - if err != nil { - return err - } - - // Generate Icons - err = processApplicationIcon(resourceDir, options.ProjectData.BuildDir) - if err != nil { - return err - } - - options.CompiledBinary = packedBinaryPath - - return nil -} - -func processPList(options *Options, contentsDirectory string) error { - - // Check if plist already exists in project dir - plistFile := filepath.Join(options.ProjectData.BuildDir, "darwin", "Info.plist") - - // If the file doesn't exist, generate it - if !fs.FileExists(plistFile) { - err := generateDefaultPlist(options, plistFile) - if err != nil { - return err - } - } - - // Copy it to the contents directory - targetFile := filepath.Join(contentsDirectory, "Info.plist") - return fs.CopyFile(plistFile, targetFile) -} - -func generateDefaultPlist(options *Options, targetPlistFile string) error { - name := defaultString(options.ProjectData.Name, "WailsTest") - exe := defaultString(options.OutputFile, name) - version := "1.0.0" - author := defaultString(options.ProjectData.Author.Name, "Anonymous") - packageID := strings.Join([]string{"wails", name}, ".") - plistData := newPlistData(name, exe, packageID, version, author) - - tmpl := template.New("infoPlist") - plistTemplate := fs.RelativePath("./internal/packager/darwin/Info.plist") - infoPlist, err := ioutil.ReadFile(plistTemplate) - if err != nil { - return errors.Wrap(err, "Cannot open plist template") - } - _, err = tmpl.Parse(string(infoPlist)) - if err != nil { - return err - } - // Write the template to a buffer - var tpl bytes.Buffer - err = tmpl.Execute(&tpl, plistData) - if err != nil { - return err - } - - // Create the directory if it doesn't exist - err = fs.MkDirs(filepath.Dir(targetPlistFile)) - if err != nil { - return err - } - // Save the file - return ioutil.WriteFile(targetPlistFile, tpl.Bytes(), 0644) -} - -func defaultString(val string, defaultVal string) string { - if val != "" { - return val - } - return defaultVal -} - -type plistData struct { - Title string - Exe string - PackageID string - Version string - Author string -} - -func newPlistData(title, exe, packageID, version, author string) *plistData { - return &plistData{ - Title: title, - Exe: exe, - Version: version, - PackageID: packageID, - Author: author, - } -} - -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) - if err != nil { - return err - } - - defer func() { - err = imageFile.Close() - if err == nil { - return - } - }() - srcImg, _, err := image.Decode(imageFile) - if err != nil { - return err - - } - dest, err := os.Create(tgtBundle) - if err != nil { - return err - - } - defer func() { - err = dest.Close() - if err == nil { - return - } - }() - return icns.Encode(dest, srcImg) -} diff --git a/v2/pkg/commands/build/packager_windows.go b/v2/pkg/commands/build/packager_windows.go deleted file mode 100644 index 85b39556..00000000 --- a/v2/pkg/commands/build/packager_windows.go +++ /dev/null @@ -1,129 +0,0 @@ -package build - -import ( - "fmt" - "github.com/leaanthony/winicon" - "github.com/tc-hib/winres" - "github.com/wailsapp/wails/v2/internal/fs" - "github.com/wailsapp/wails/v2/pkg/buildassets" - "os" - "path/filepath" -) - -func packageApplication(options *Options) error { - // Generate icon - var err error - err = generateIcoFile(options) - if err != nil { - return err - } - - // Ensure Manifest is present - err = generateManifest(options) - if err != nil { - return err - } - - // Create syso file - err = compileResources(options) - if err != nil { - return err - } - - return nil -} - -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") - 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) - if err != nil { - return err - } - output, err := os.OpenFile(icoFile, os.O_CREATE, 0644) - if err != nil { - return err - } - err = winicon.GenerateIcon(input, output, []int{256, 128, 64, 48, 32, 16}) - if err != nil { - return err - } - } - return nil -} - -func compileResources(options *Options) error { - - currentDir, err := os.Getwd() - if err != nil { - return err - } - defer func() { - os.Chdir(currentDir) - }() - windowsDir := filepath.Join(options.ProjectData.Path, "build", "windows") - err = os.Chdir(windowsDir) - if err != nil { - return err - } - rs := winres.ResourceSet{} - icon := filepath.Join(windowsDir, "icon.ico") - iconFile, err := os.Open(icon) - if err != nil { - return err - } - defer iconFile.Close() - ico, err := winres.LoadICO(iconFile) - if err != nil { - return err - } - err = rs.SetIcon(winres.RT_ICON, ico) - if err != nil { - return err - } - - ManifestFilename := options.ProjectData.Name + ".exe.manifest" - manifestData, err := os.ReadFile(ManifestFilename) - xmlData, err := winres.AppManifestFromXML(manifestData) - if err != nil { - return err - } - rs.SetManifest(xmlData) - - targetFile := filepath.Join(options.ProjectData.Path, options.ProjectData.Name+"-res.syso") - fout, err := os.Create(targetFile) - if err != nil { - return err - } - defer fout.Close() - - archs := map[string]winres.Arch{ - "amd64": winres.ArchAMD64, - } - targetArch, supported := archs[options.Arch] - if !supported { - return fmt.Errorf("arch '%s' not supported", options.Arch) - } - - err = rs.WriteObject(fout, targetArch) - if err != nil { - return err - } - return nil -}