package cmd import ( "bufio" "bytes" "encoding/binary" "fmt" "image" "image/png" "io/ioutil" "os" "path" "path/filepath" "runtime" "strings" "text/template" "time" "golang.org/x/image/draw" "github.com/jackmordaunt/icns" ) // PackageHelper helps with the 'wails package' command type PackageHelper struct { platform string fs *FSHelper log *Logger system *SystemHelper } // NewPackageHelper creates a new PackageHelper! func NewPackageHelper(platform string) *PackageHelper { return &PackageHelper{ platform: platform, fs: NewFSHelper(), log: NewLogger(), system: NewSystemHelper(), } } type plistData struct { Title string Exe string PackageID string Version string Author string Date string } func newPlistData(title, exe, packageID, version, author string) *plistData { now := time.Now().Format(time.RFC822) return &plistData{ Title: title, Exe: exe, Version: version, PackageID: packageID, Author: author, Date: now, } } type windowsIconHeader struct { _ uint16 imageType uint16 imageCount uint16 width uint8 height uint8 colours uint8 _ uint8 planes uint16 bpp uint16 size uint32 offset uint32 } func generateWindowsIcon(pngFilename string, iconfile string) error { header := &windowsIconHeader{ imageType: 1, imageCount: 1, bpp: 32, planes: 1, offset: 22, width: 255, height: 255, } // Load png pngfile, err := os.Open(pngFilename) if err != nil { return err } defer pngfile.Close() // Decode to internal image pngdata, err := png.Decode(pngfile) if err != nil { return err } // Scale to 256*256 rect := image.Rect(0, 0, 255, 255) rawdata := image.NewRGBA(rect) scale := draw.ApproxBiLinear scale.Scale(rawdata, rect, pngdata, pngdata.Bounds(), draw.Over, nil) // Convert back to PNG icondata := new(bytes.Buffer) writer := bufio.NewWriter(icondata) err = png.Encode(writer, rawdata) if err != nil { return err } err = writer.Flush() if err != nil { return err } // Save size of PNG data header.size = uint32(len(icondata.Bytes())) // Open icon file for writing outfilename := filepath.Join(filepath.Dir(pngFilename), iconfile) outfile, err := os.Create(outfilename) if err != nil { return err } defer outfile.Close() // Write out the header err = binary.Write(outfile, binary.LittleEndian, header) if err != nil { return err } // Write out the image data _, err = outfile.Write(icondata.Bytes()) if err != nil { return err } return nil } func defaultString(val string, defaultVal string) string { if val != "" { return val } return defaultVal } func (b *PackageHelper) getPackageFileBaseDir() string { // Calculate template base dir _, filename, _, _ := runtime.Caller(1) return filepath.Join(path.Dir(filename), "packages", b.platform) } // Package the application into a platform specific package func (b *PackageHelper) Package(po *ProjectOptions) error { switch b.platform { case "darwin": return b.packageOSX(po) case "windows": return b.PackageWindows(po, true) case "linux": return b.packageLinux(po) default: return fmt.Errorf("platform '%s' not supported for bundling yet", b.platform) } } func (b *PackageHelper) packageLinux(po *ProjectOptions) error { return nil } // Package the application for OSX func (b *PackageHelper) packageOSX(po *ProjectOptions) error { build := path.Join(b.fs.Cwd(), "build") system := NewSystemHelper() config, err := system.LoadConfig() if err != nil { return err } name := defaultString(po.Name, "WailsTest") exe := defaultString(po.BinaryName, name) version := defaultString(po.Version, "0.1.0") author := defaultString(config.Name, "Anonymous") packageID := strings.Join([]string{"wails", name, version}, ".") plistData := newPlistData(name, exe, packageID, version, author) appname := po.Name + ".app" // Check binary exists source := path.Join(build, exe) if po.CrossCompile == true { file, err := b.fs.FindFile(build, "darwin") if err != nil { return err } source = path.Join(build, file) } if !b.fs.FileExists(source) { // We need to build! return fmt.Errorf("Target '%s' not available. Has it been compiled yet?", source) } // Remove the existing package os.RemoveAll(appname) exeDir := path.Join(build, appname, "/Contents/MacOS") b.fs.MkDirs(exeDir, 0755) resourceDir := path.Join(build, appname, "/Contents/Resources") b.fs.MkDirs(resourceDir, 0755) tmpl := template.New("infoPlist") plistFile := filepath.Join(b.getPackageFileBaseDir(), "info.plist") infoPlist, err := ioutil.ReadFile(plistFile) if err != nil { return err } tmpl.Parse(string(infoPlist)) // Write the template to a buffer var tpl bytes.Buffer err = tmpl.Execute(&tpl, plistData) if err != nil { return err } filename := path.Join(build, appname, "Contents", "Info.plist") err = ioutil.WriteFile(filename, tpl.Bytes(), 0644) if err != nil { return err } // Copy executable target := path.Join(exeDir, exe) err = b.fs.CopyFile(source, target) if err != nil { return err } err = os.Chmod(target, 0755) if err != nil { return err } err = b.packageIconOSX(resourceDir) return err } // CleanWindows removes any windows related files found in the directory func (b *PackageHelper) CleanWindows(po *ProjectOptions) { pdir := b.fs.Cwd() basename := strings.TrimSuffix(po.BinaryName, ".exe") exts := []string{".ico", ".exe.manifest", ".rc", "-res.syso"} rsrcs := []string{} for _, ext := range exts { rsrcs = append(rsrcs, filepath.Join(pdir, basename+ext)) } b.fs.RemoveFiles(rsrcs, true) } // PackageWindows packages the application for windows platforms func (b *PackageHelper) PackageWindows(po *ProjectOptions, cleanUp bool) error { outputDir := b.fs.Cwd() basename := strings.TrimSuffix(po.BinaryName, ".exe") // Copy default icon if needed icon, err := b.copyIcon() if err != nil { return err } // Generate icon from PNG err = generateWindowsIcon(icon, po.BinaryName+".ico") if err != nil { return err } // Copy manifest tgtManifestFile := filepath.Join(outputDir, basename+".exe.manifest") if !b.fs.FileExists(tgtManifestFile) { srcManifestfile := filepath.Join(b.getPackageFileBaseDir(), "wails.exe.manifest") err := b.fs.CopyFile(srcManifestfile, tgtManifestFile) if err != nil { return err } } // Copy rc file tgtRCFile := filepath.Join(outputDir, basename+".rc") if !b.fs.FileExists(tgtRCFile) { srcRCfile := filepath.Join(b.getPackageFileBaseDir(), "wails.rc") rcfilebytes, err := ioutil.ReadFile(srcRCfile) if err != nil { return err } rcfiledata := strings.Replace(string(rcfilebytes), "$NAME$", basename, -1) err = ioutil.WriteFile(tgtRCFile, []byte(rcfiledata), 0755) if err != nil { return err } } // Build syso sysofile := filepath.Join(outputDir, basename+"-res.syso") // cross-compile if b.platform != runtime.GOOS { args := []string{ "docker", "run", "--rm", "-v", outputDir + ":/build", "--entrypoint", "/bin/sh", "wailsapp/xgo:latest", "-c", "/usr/bin/x86_64-w64-mingw32-windres -o /build/" + basename + "-res.syso /build/" + basename + ".rc", } if err := NewProgramHelper().RunCommandArray(args); err != nil { return err } } else { batfile, err := fs.LocalDir(".") if err != nil { return err } windresBatFile := filepath.Join(batfile.fullPath, "windres.bat") windresCommand := []string{windresBatFile, sysofile, tgtRCFile} err = NewProgramHelper().RunCommandArray(windresCommand) if err != nil { return err } } return nil } func (b *PackageHelper) copyIcon() (string, error) { // TODO: Read this from project.json const appIconFilename = "appicon.png" srcIcon := path.Join(b.fs.Cwd(), appIconFilename) // Check if appicon.png exists if !b.fs.FileExists(srcIcon) { // Install default icon iconfile := filepath.Join(b.getPackageFileBaseDir(), "icon.png") iconData, err := ioutil.ReadFile(iconfile) if err != nil { return "", err } err = ioutil.WriteFile(srcIcon, iconData, 0644) if err != nil { return "", err } } return srcIcon, nil } func (b *PackageHelper) packageIconOSX(resourceDir string) error { srcIcon, err := b.copyIcon() if err != nil { return err } tgtBundle := path.Join(resourceDir, "iconfile.icns") imageFile, err := os.Open(srcIcon) if err != nil { return err } defer imageFile.Close() srcImg, _, err := image.Decode(imageFile) if err != nil { return err } dest, err := os.Create(tgtBundle) if err != nil { return err } defer dest.Close() return icns.Encode(dest, srcImg) }