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 a2b30415..00000000 Binary files a/v2/test/kitchensink/icon.png and /dev/null differ