From 9e6ae4d76279db517d572d7317dd0c983da4e471 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sat, 21 Nov 2020 07:09:25 +1100 Subject: [PATCH] Support building Mac package --- v2/cmd/wails/internal/commands/build/build.go | 3 +- v2/go.mod | 2 + v2/go.sum | 4 + v2/internal/fs/fs.go | 18 ++ v2/internal/project/project.go | 9 + v2/pkg/commands/build/base.go | 10 +- v2/pkg/commands/build/build.go | 6 + .../build/internal/packager/darwin/info.plist | 12 ++ v2/pkg/commands/build/packager.go | 15 +- v2/pkg/commands/build/packager_darwin.go | 156 +++++++++++++++++- v2/test/kitchensink/.gitignore | 2 + v2/test/kitchensink/icon.png | Bin 6765 -> 0 bytes 12 files changed, 224 insertions(+), 13 deletions(-) create mode 100644 v2/pkg/commands/build/internal/packager/darwin/info.plist create mode 100644 v2/test/kitchensink/.gitignore delete mode 100644 v2/test/kitchensink/icon.png diff --git a/v2/cmd/wails/internal/commands/build/build.go b/v2/cmd/wails/internal/commands/build/build.go index c57d50dd..ae8ef299 100644 --- a/v2/cmd/wails/internal/commands/build/build.go +++ b/v2/cmd/wails/internal/commands/build/build.go @@ -32,7 +32,7 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) { // Setup pack flag pack := false - command.BoolFlag("pack", "Create a platform specific package", &pack) + command.BoolFlag("package", "Create a platform specific package", &pack) compilerCommand := "go" command.StringFlag("compiler", "Use a different go compiler to build, eg go1.15beta1", &compilerCommand) @@ -103,6 +103,7 @@ func doBuild(buildOptions *build.Options) error { if err != nil { return err } + // Output stats elapsed := time.Since(start) buildOptions.Logger.Println("") diff --git a/v2/go.mod b/v2/go.mod index a30b7f70..4604933f 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -7,10 +7,12 @@ require ( github.com/fatih/structtag v1.2.0 github.com/fsnotify/fsnotify v1.4.9 github.com/imdario/mergo v0.3.11 + github.com/jackmordaunt/icns v1.0.0 github.com/leaanthony/clir v1.0.4 github.com/leaanthony/gosod v0.0.4 github.com/leaanthony/slicer v1.5.0 github.com/matryer/is v1.4.0 + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/olekukonko/tablewriter v0.0.4 github.com/pkg/errors v0.9.1 diff --git a/v2/go.sum b/v2/go.sum index e73c4ab5..08f80d04 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -33,6 +33,8 @@ github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvK github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/jackmordaunt/icns v1.0.0 h1:RYSxplerf/l/DUd09AHtITwckkv/mqjVv4DjYdPmAMQ= +github.com/jackmordaunt/icns v1.0.0/go.mod h1:7TTQVEuGzVVfOPPlLNHJIkzA6CoV7aH1Dv9dW351oOo= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= @@ -58,6 +60,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= diff --git a/v2/internal/fs/fs.go b/v2/internal/fs/fs.go index 0867dfa0..1915bc22 100644 --- a/v2/internal/fs/fs.go +++ b/v2/internal/fs/fs.go @@ -36,6 +36,24 @@ func Mkdir(dirname string) error { return os.Mkdir(dirname, 0755) } +// MkDirs creates the given nested directories. +// Returns error on failure +func MkDirs(fullPath string, mode ...os.FileMode) error { + var perms os.FileMode + perms = 0700 + if len(mode) == 1 { + perms = mode[0] + } + return os.MkdirAll(fullPath, perms) +} + +// MoveFile attempts to move the source file to the target +// Target is a fully qualified path to a file *name*, not a +// directory +func MoveFile(source string, target string) error { + return os.Rename(source, target) +} + // DeleteFile will delete the given file func DeleteFile(filename string) error { return os.Remove(filename) diff --git a/v2/internal/project/project.go b/v2/internal/project/project.go index 10e707b8..a196d3ac 100644 --- a/v2/internal/project/project.go +++ b/v2/internal/project/project.go @@ -33,6 +33,15 @@ type Project struct { // The platform to target Platform string + + // The application author + Author Author +} + +// Author stores details about the application author +type Author struct { + Name string `json:"name"` + Email string `json:"email"` } // Load the project from the current working directory diff --git a/v2/pkg/commands/build/base.go b/v2/pkg/commands/build/base.go index a6e09f54..684c8caa 100644 --- a/v2/pkg/commands/build/base.go +++ b/v2/pkg/commands/build/base.go @@ -165,7 +165,8 @@ func (b *BaseBuilder) CompileProject(options *Options) error { } // Get application build directory - appDir, err := getApplicationBuildDirectory(options, options.Platform) + appDir := options.BuildDirectory + err := cleanBuildDirectory(options, options.Platform) if err != nil { return err } @@ -180,11 +181,12 @@ func (b *BaseBuilder) CompileProject(options *Options) error { if outputFile == "" { outputFile = b.projectData.OutputFilename } - outputFilePath := filepath.Join(appDir, outputFile) + compiledBinary := filepath.Join(appDir, outputFile) commands.Add("-o") - commands.Add(outputFilePath) + commands.Add(compiledBinary) - b.projectData.OutputFilename = strings.TrimPrefix(outputFilePath, options.ProjectData.Path) + b.projectData.OutputFilename = strings.TrimPrefix(compiledBinary, options.ProjectData.Path) + options.CompiledBinary = compiledBinary // Create the command cmd := exec.Command(options.Compiler, commands.AsSlice()...) diff --git a/v2/pkg/commands/build/build.go b/v2/pkg/commands/build/build.go index 05173620..94fd9c2f 100644 --- a/v2/pkg/commands/build/build.go +++ b/v2/pkg/commands/build/build.go @@ -3,6 +3,7 @@ package build import ( "fmt" "os" + "path/filepath" "runtime" "github.com/leaanthony/slicer" @@ -34,6 +35,8 @@ type Options struct { Compiler string // The compiler command to use IgnoreFrontend bool // Indicates if the frontend does not need building OutputFile string // Override the output filename + BuildDirectory string // Directory to use for building the application + CompiledBinary string // Fully qualified path to the compiled binary } // GetModeAsString returns the current mode as a string @@ -66,6 +69,9 @@ func Build(options *Options) (string, error) { } options.ProjectData = projectData + // Calculate build dir + options.BuildDirectory = filepath.Join(options.ProjectData.Path, "build", options.Platform, options.OutputType) + // Save the project type projectData.OutputType = options.OutputType diff --git a/v2/pkg/commands/build/internal/packager/darwin/info.plist b/v2/pkg/commands/build/internal/packager/darwin/info.plist new file mode 100644 index 00000000..30ded0ef --- /dev/null +++ b/v2/pkg/commands/build/internal/packager/darwin/info.plist @@ -0,0 +1,12 @@ + + + CFBundlePackageTypeAPPL + CFBundleName{{.Title}} + CFBundleExecutable{{.Exe}} + CFBundleIdentifier{{.PackageID}} + CFBundleVersion{{.Version}} + CFBundleGetInfoStringBuilt by {{.Author}} using Wails (https://wails.app) + CFBundleShortVersionString{{.Version}} + CFBundleIconFileiconfile + NSHighResolutionCapabletrue + \ No newline at end of file diff --git a/v2/pkg/commands/build/packager.go b/v2/pkg/commands/build/packager.go index ba6efd41..f48a5e55 100644 --- a/v2/pkg/commands/build/packager.go +++ b/v2/pkg/commands/build/packager.go @@ -14,7 +14,7 @@ func packageProject(options *Options, platform string) error { var err error switch platform { - case "linux": + case "linux", "darwin": err = packageApplication(options) default: err = fmt.Errorf("packing not supported for %s yet", platform) @@ -27,25 +27,26 @@ func packageProject(options *Options, platform string) error { return nil } -// Gets (and creates) the platform/target build directory -func getApplicationBuildDirectory(options *Options, platform string) (string, error) { - buildDirectory := filepath.Join(options.ProjectData.Path, "build", platform, options.OutputType) +// cleanBuildDirectory will remove an existing build directory and recreate it +func cleanBuildDirectory(options *Options, platform string) error { + + buildDirectory := options.BuildDirectory // Clear out old builds if fs.DirExists(buildDirectory) { err := os.RemoveAll(buildDirectory) if err != nil { - return "", err + return err } } // Create clean directory err := os.MkdirAll(buildDirectory, 0700) if err != nil { - return "", err + return err } - return buildDirectory, nil + return nil } func copyFileToBuildDirectory() {} diff --git a/v2/pkg/commands/build/packager_darwin.go b/v2/pkg/commands/build/packager_darwin.go index a64ba85c..0e21dcde 100644 --- a/v2/pkg/commands/build/packager_darwin.go +++ b/v2/pkg/commands/build/packager_darwin.go @@ -1,6 +1,160 @@ package build +import ( + "bytes" + "image" + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + "text/template" + + "github.com/jackmordaunt/icns" + "github.com/pkg/errors" + "github.com/wailsapp/wails/v2/internal/fs" +) + func packageApplication(options *Options) error { - // TBD + + var err error + + // Create directory structure + bundlename := options.ProjectData.Name + ".app" + + contentsDirectory := filepath.Join(options.BuildDirectory, bundlename, "/Contents") + exeDir := filepath.Join(contentsDirectory, "/MacOS") + fs.MkDirs(exeDir, 0755) + resourceDir := filepath.Join(contentsDirectory, "/Resources") + fs.MkDirs(resourceDir, 0755) + + // 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 = processIcon(resourceDir) + if err != nil { + return err + } + return nil } + +func processPList(options *Options, contentsDirectory string) error { + // Check if plist already exists in project dir + plistFile, err := fs.RelativeToCwd("info.plist") + if err != nil { + return err + } + + // 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, version}, ".") + 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") + } + 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 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 processIcon(resourceDir string) error { + + appIcon, err := fs.RelativeToCwd("appicon.png") + if err != nil { + return err + } + + // Install default icon if one doesn't exist + if !fs.FileExists(appIcon) { + // No - Install default icon + defaultIcon := fs.RelativePath("./internal/packager/icon1024.png") + err := fs.CopyFile(defaultIcon, appIcon) + if err != nil { + return err + } + } + + tgtBundle := path.Join(resourceDir, "iconfile.icns") + imageFile, err := os.Open(appIcon) + 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) +} diff --git a/v2/test/kitchensink/.gitignore b/v2/test/kitchensink/.gitignore new file mode 100644 index 00000000..2f16e856 --- /dev/null +++ b/v2/test/kitchensink/.gitignore @@ -0,0 +1,2 @@ +appicon.png +info.plist \ No newline at end of file diff --git a/v2/test/kitchensink/icon.png b/v2/test/kitchensink/icon.png deleted file mode 100644 index a2b304154044f39db7f59fe497728f64bf4d551f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6765 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 KT-G@yGywn=%3+TH