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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