diff --git a/cmd/fs.go b/cmd/fs.go index 36109e8f..54a0dc2c 100644 --- a/cmd/fs.go +++ b/cmd/fs.go @@ -12,6 +12,7 @@ import ( "path" "path/filepath" "runtime" + "strings" "github.com/leaanthony/slicer" ) @@ -47,6 +48,22 @@ func (fs *FSHelper) FileExists(path string) bool { return fi.Mode().IsRegular() } +// FindFile returns the first occurrence of match inside path. +func (fs *FSHelper) FindFile(path, match string) (string, error) { + files, err := ioutil.ReadDir(path) + if err != nil { + return "", err + } + + for _, f := range files { + if !f.IsDir() && strings.Contains(f.Name(), match) { + return f.Name(), nil + } + } + + return "", fmt.Errorf("file not found") +} + // CreateFile creates a file at the given filename location with the contents // set to the given data. It will create intermediary directories if needed. func (fs *FSHelper) CreateFile(filename string, data []byte) error { diff --git a/cmd/helpers.go b/cmd/helpers.go index bfb033aa..eb2366cc 100644 --- a/cmd/helpers.go +++ b/cmd/helpers.go @@ -11,6 +11,7 @@ import ( "time" "github.com/leaanthony/mewn" + "github.com/leaanthony/mewn/lib" "github.com/leaanthony/slicer" "github.com/leaanthony/spinner" ) @@ -56,22 +57,68 @@ func InstallGoDependencies(verbose bool) error { return nil } +// EmbedAssets will embed the built frontend assets via mewn. +func EmbedAssets() ([]string, error) { + mewnFiles := lib.GetMewnFiles([]string{}, false) + + referencedAssets, err := lib.GetReferencedAssets(mewnFiles) + if err != nil { + return []string{}, err + } + + targetFiles := []string{} + + for _, referencedAsset := range referencedAssets { + packfileData, err := lib.GeneratePackFileString(referencedAsset, false) + if err != nil { + return []string{}, err + } + targetFile := filepath.Join(referencedAsset.BaseDir, referencedAsset.PackageName+"-mewn.go") + targetFiles = append(targetFiles, targetFile) + ioutil.WriteFile(targetFile, []byte(packfileData), 0644) + } + + return targetFiles, nil +} + // BuildApplication will attempt to build the project based on the given inputs func BuildApplication(binaryName string, forceRebuild bool, buildMode string, packageApp bool, projectOptions *ProjectOptions) error { + if buildMode == BuildModeBridge && projectOptions.CrossCompile { + return fmt.Errorf("you cant serve the application in cross-compilation") + } + // Generate Windows assets if needed - if runtime.GOOS == "windows" { + if projectOptions.Platform == "windows" { cleanUp := !packageApp - err := NewPackageHelper().PackageWindows(projectOptions, cleanUp) + err := NewPackageHelper(projectOptions.Platform).PackageWindows(projectOptions, cleanUp) if err != nil { return err } } - // Check Mewn is installed - err := CheckMewn(projectOptions.Verbose) - if err != nil { - return err + if projectOptions.CrossCompile { + // Check build directory + buildDirectory := filepath.Join(fs.Cwd(), "build") + if !fs.DirExists(buildDirectory) { + fs.MkDir(buildDirectory) + } + + // Check Docker + if err := CheckIfInstalled("docker"); err != nil { + return err + } + + // Check xgo + if err := CheckIfInstalled("xgo"); err != nil { + return err + } + } else { + // Check Mewn is installed + err := CheckMewn(projectOptions.Verbose) + if err != nil { + return err + } } compileMessage := "Packing + Compiling project" @@ -89,17 +136,38 @@ func BuildApplication(binaryName string, forceRebuild bool, buildMode string, pa println(compileMessage) } + // embed resources + targetFiles, err := EmbedAssets() + if err != nil { + return err + } + + // cleanup temporary embedded assets + defer func() { + for _, filename := range targetFiles { + if err := os.Remove(filename); err != nil { + fmt.Println(err) + } + } + }() + buildCommand := slicer.String() - buildCommand.Add("mewn") + if projectOptions.CrossCompile { + buildCommand.Add("xgo") + } else { + buildCommand.Add("mewn") + } if buildMode == BuildModeBridge { // Ignore errors buildCommand.Add("-i") } - buildCommand.Add("build") + if !projectOptions.CrossCompile { + buildCommand.Add("build") + } - if binaryName != "" { + if binaryName != "" && !projectOptions.CrossCompile { // Alter binary name based on OS switch runtime.GOOS { case "windows": @@ -115,7 +183,7 @@ func BuildApplication(binaryName string, forceRebuild bool, buildMode string, pa } // If we are forcing a rebuild - if forceRebuild { + if forceRebuild && !projectOptions.CrossCompile { buildCommand.Add("-a") } @@ -126,7 +194,7 @@ func BuildApplication(binaryName string, forceRebuild bool, buildMode string, pa } // Add windows flags - if runtime.GOOS == "windows" && buildMode == BuildModeProd { + if projectOptions.Platform == "windows" && buildMode == BuildModeProd { ldflags += "-H windowsgui " } @@ -143,6 +211,13 @@ func BuildApplication(binaryName string, forceRebuild bool, buildMode string, pa } buildCommand.AddSlice([]string{"-ldflags", ldflags}) + + if projectOptions.CrossCompile { + buildCommand.Add("-targets", projectOptions.Platform+"/"+projectOptions.Architecture) + buildCommand.Add("-out", "build/"+binaryName) + buildCommand.Add("./") + } + err = NewProgramHelper(projectOptions.Verbose).RunCommandArray(buildCommand.AsSlice()) if err != nil { if packSpinner != nil { @@ -169,7 +244,7 @@ func BuildApplication(binaryName string, forceRebuild bool, buildMode string, pa func PackageApplication(projectOptions *ProjectOptions) error { // Package app message := "Generating .app" - if runtime.GOOS == "windows" { + if projectOptions.Platform == "windows" { err := CheckWindres() if err != nil { return err @@ -177,12 +252,15 @@ func PackageApplication(projectOptions *ProjectOptions) error { message = "Generating resource bundle" } var packageSpinner *spinner.Spinner - if projectOptions.Verbose { + if !projectOptions.Verbose { packageSpinner = spinner.New(message) packageSpinner.SetSpinSpeed(50) packageSpinner.Start() + } else { + println(message) } - err := NewPackageHelper().Package(projectOptions) + + err := NewPackageHelper(projectOptions.Platform).Package(projectOptions) if err != nil { if packageSpinner != nil { packageSpinner.Error() @@ -254,6 +332,15 @@ func CheckWindres() (err error) { return nil } +// CheckIfInstalled returns if application is installed +func CheckIfInstalled(application string) (err error) { + programHelper := NewProgramHelper() + if !programHelper.IsInstalled(application) { + return fmt.Errorf("%s not installed. Ensure you have installed %s correctly", application, application) + } + return nil +} + // InstallFrontendDeps attempts to install the frontend dependencies based on the given options func InstallFrontendDeps(projectDir string, projectOptions *ProjectOptions, forceRebuild bool, caller string) error { diff --git a/cmd/package.go b/cmd/package.go index c9b0ef83..2787b54e 100644 --- a/cmd/package.go +++ b/cmd/package.go @@ -18,17 +18,19 @@ import ( // PackageHelper helps with the 'wails package' command type PackageHelper struct { - fs *FSHelper - log *Logger - system *SystemHelper + platform string + fs *FSHelper + log *Logger + system *SystemHelper } // NewPackageHelper creates a new PackageHelper! -func NewPackageHelper() *PackageHelper { +func NewPackageHelper(platform string) *PackageHelper { return &PackageHelper{ - fs: NewFSHelper(), - log: NewLogger(), - system: NewSystemHelper(), + platform: platform, + fs: NewFSHelper(), + log: NewLogger(), + system: NewSystemHelper(), } } @@ -63,16 +65,23 @@ func defaultString(val string, defaultVal string) string { func (b *PackageHelper) getPackageFileBaseDir() string { // Calculate template base dir _, filename, _, _ := runtime.Caller(1) - return filepath.Join(path.Dir(filename), "packages", runtime.GOOS) + 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 runtime.GOOS { + switch b.platform { case "darwin": // Check we have the exe if !b.fs.FileExists(po.BinaryName) { - return fmt.Errorf("cannot bundle non-existent binary file '%s'. Please build with 'wails build' first", po.BinaryName) + // Check cross-compiled application + if b.platform == runtime.GOOS { + return fmt.Errorf("cannot bundle non-existent binary file '%s'. Please build with 'wails build' first", po.BinaryName) + } + + if _, err := b.fs.FindFile(path.Join(b.fs.Cwd(), "build"), "darwin"); err != nil { + return fmt.Errorf("cannot bundle non-existent cross-compiled binary file '%s'. Please build with 'wails build -x darwin' first", po.BinaryName) + } } return b.packageOSX(po) case "windows": @@ -80,7 +89,7 @@ func (b *PackageHelper) Package(po *ProjectOptions) error { case "linux": return fmt.Errorf("linux is not supported at this time. Please see https://github.com/wailsapp/wails/issues/2") default: - return fmt.Errorf("platform '%s' not supported for bundling yet", runtime.GOOS) + return fmt.Errorf("platform '%s' not supported for bundling yet", b.platform) } } @@ -103,6 +112,22 @@ func (b *PackageHelper) packageOSX(po *ProjectOptions) error { // Check binary exists source := path.Join(b.fs.Cwd(), exe) + + if b.platform != runtime.GOOS { + + file, err := b.fs.FindFile(path.Join(b.fs.Cwd(), "build"), "darwin") + if err != nil { + return err + } + + // rename to exe + if err := os.Rename(path.Join(b.fs.Cwd(), "build", file), path.Join(b.fs.Cwd(), "build", exe)); err != nil { + return err + } + + source = path.Join(b.fs.Cwd(), "build", exe) + } + if !b.fs.FileExists(source) { // We need to build! return fmt.Errorf("Target '%s' not available. Has it been compiled yet?", exe) @@ -192,16 +217,36 @@ func (b *PackageHelper) PackageWindows(po *ProjectOptions, cleanUp bool) error { // Build syso sysofile := filepath.Join(b.fs.Cwd(), basename+"-res.syso") - batfile, err := fs.LocalDir(".") - if err != nil { - return err - } + // cross-compile + if b.platform != runtime.GOOS { + folder, err := os.Getwd() + 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 + args := []string{ + "docker", "run", "--rm", + "-v", folder + ":/build", + "--entrypoint", "/bin/sh", + "techknowlogick/xgo", + "-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 + } } // clean up diff --git a/cmd/project.go b/cmd/project.go index d4e878a0..bedc9012 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -158,7 +158,10 @@ type ProjectOptions struct { selectedTemplate *TemplateDetails WailsVersion string typescriptDefsFilename string - Verbose bool `json:"-"` + Verbose bool `json:"-"` + CrossCompile bool `json:"-"` + Platform string `json:"-"` + Architecture string `json:"-"` } // Defaults sets the default project template diff --git a/cmd/wails/4_build.go b/cmd/wails/4_build.go index 40d7415c..8a362316 100644 --- a/cmd/wails/4_build.go +++ b/cmd/wails/4_build.go @@ -3,6 +3,8 @@ package main import ( "fmt" "os" + "runtime" + "strings" "github.com/leaanthony/spinner" "github.com/wailsapp/wails/cmd" @@ -15,6 +17,7 @@ func init() { var debugMode = false var typescriptFilename = "" var verbose = false + var platform = "" buildSpinner := spinner.NewSpinner() buildSpinner.SetSpinSpeed(50) @@ -26,7 +29,8 @@ func init() { BoolFlag("f", "Force rebuild of application components", &forceRebuild). BoolFlag("d", "Build in Debug mode", &debugMode). BoolFlag("verbose", "Verbose output", &verbose). - StringFlag("t", "Generate Typescript definitions to given file (at runtime)", &typescriptFilename) + StringFlag("t", "Generate Typescript definitions to given file (at runtime)", &typescriptFilename). + StringFlag("x", "Cross-compile application to specified platform via xgo", &platform) initCmd.Action(func() error { @@ -130,6 +134,21 @@ func init() { buildSpinner.Success() } + // Set cross-compile + projectOptions.Platform = runtime.GOOS + if len(platform) > 0 { + projectOptions.CrossCompile = true + projectOptions.Platform = platform + projectOptions.Architecture = "amd64" + + // check build architecture + if strings.Contains(platform, "/") { + p := strings.Split(platform, "/") + projectOptions.Platform = p[0] + projectOptions.Architecture = p[1] + } + } + err = cmd.BuildApplication(projectOptions.BinaryName, forceRebuild, buildMode, packageApp, projectOptions) if err != nil { return err