mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-03 04:42:00 +08:00
428 lines
9.5 KiB
Go
428 lines
9.5 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"image"
|
|
"image/png"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/jackmordaunt/icns"
|
|
"golang.org/x/image/draw"
|
|
)
|
|
|
|
// 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 windowsIcoHeader struct {
|
|
_ uint16
|
|
imageType uint16
|
|
imageCount uint16
|
|
}
|
|
|
|
type windowsIcoDescriptor struct {
|
|
width uint8
|
|
height uint8
|
|
colours uint8
|
|
_ uint8
|
|
planes uint16
|
|
bpp uint16
|
|
size uint32
|
|
offset uint32
|
|
}
|
|
|
|
type windowsIcoContainer struct {
|
|
Header windowsIcoDescriptor
|
|
Data []byte
|
|
}
|
|
|
|
func generateWindowsIcon(pngFilename string, iconfile string) error {
|
|
sizes := []int{256, 128, 64, 48, 32, 16}
|
|
|
|
pngfile, err := os.Open(pngFilename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer pngfile.Close()
|
|
|
|
pngdata, err := png.Decode(pngfile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
icons := []windowsIcoContainer{}
|
|
|
|
for _, size := range sizes {
|
|
rect := image.Rect(0, 0, int(size), int(size))
|
|
rawdata := image.NewRGBA(rect)
|
|
scale := draw.CatmullRom
|
|
scale.Scale(rawdata, rect, pngdata, pngdata.Bounds(), draw.Over, nil)
|
|
|
|
icondata := new(bytes.Buffer)
|
|
writer := bufio.NewWriter(icondata)
|
|
err = png.Encode(writer, rawdata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
writer.Flush()
|
|
|
|
imgSize := size
|
|
if imgSize >= 256 {
|
|
imgSize = 0
|
|
}
|
|
|
|
data := icondata.Bytes()
|
|
|
|
icn := windowsIcoContainer{
|
|
Header: windowsIcoDescriptor{
|
|
width: uint8(imgSize),
|
|
height: uint8(imgSize),
|
|
planes: 1,
|
|
bpp: 32,
|
|
size: uint32(len(data)),
|
|
},
|
|
Data: data,
|
|
}
|
|
icons = append(icons, icn)
|
|
}
|
|
|
|
outfile, err := os.Create(iconfile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer outfile.Close()
|
|
|
|
ico := windowsIcoHeader{
|
|
imageType: 1,
|
|
imageCount: uint16(len(sizes)),
|
|
}
|
|
err = binary.Write(outfile, binary.LittleEndian, ico)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
offset := uint32(6 + 16*len(sizes))
|
|
for _, icon := range icons {
|
|
icon.Header.offset = offset
|
|
err = binary.Write(outfile, binary.LittleEndian, icon.Header)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
offset += icon.Header.size
|
|
}
|
|
for _, icon := range icons {
|
|
_, err = outfile.Write(icon.Data)
|
|
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"
|
|
plistFilename := path.Join(build, appname, "Contents", "Info.plist")
|
|
customPlist := path.Join(b.fs.Cwd(), "info.plist")
|
|
|
|
// 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)
|
|
|
|
// Create directories
|
|
exeDir := path.Join(build, appname, "/Contents/MacOS")
|
|
b.fs.MkDirs(exeDir, 0755)
|
|
resourceDir := path.Join(build, appname, "/Contents/Resources")
|
|
b.fs.MkDirs(resourceDir, 0755)
|
|
|
|
// Do we have a custom plist in the project directory?
|
|
if !fs.FileExists(customPlist) {
|
|
|
|
// No - create a new plist from our defaults
|
|
tmpl := template.New("infoPlist")
|
|
plistFile := filepath.Join(b.getPackageFileBaseDir(), "info.plist")
|
|
infoPlist, err := os.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
|
|
}
|
|
|
|
// Save to the package
|
|
err = os.WriteFile(plistFilename, tpl.Bytes(), 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Also write to project directory for customisation
|
|
err = os.WriteFile(customPlist, tpl.Bytes(), 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// Yes - we have a plist. Copy it to the package verbatim
|
|
err = fs.CopyFile(customPlist, plistFilename)
|
|
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 if it doesn't exist
|
|
if !fs.FileExists(basename + ".ico") {
|
|
err = generateWindowsIcon(icon, basename+".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 := os.ReadFile(srcRCfile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rcfiledata := strings.Replace(string(rcfilebytes), "$NAME$", basename, -1)
|
|
err = os.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:1.16.3",
|
|
"-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 := os.ReadFile(iconfile)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
err = os.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)
|
|
}
|