package cmd import ( "fmt" "io/ioutil" "os" "os/exec" "os/user" "path/filepath" "runtime" "strconv" "strings" "time" "github.com/leaanthony/mewn" "github.com/leaanthony/mewn/lib" "github.com/leaanthony/slicer" "github.com/leaanthony/spinner" ) var fs = NewFSHelper() // ValidateFrontendConfig checks if the frontend config is valid func ValidateFrontendConfig(projectOptions *ProjectOptions) error { if projectOptions.FrontEnd.Dir == "" { return fmt.Errorf("Frontend directory not set in project.json") } if projectOptions.FrontEnd.Build == "" { return fmt.Errorf("Frontend build command not set in project.json") } if projectOptions.FrontEnd.Install == "" { return fmt.Errorf("Frontend install command not set in project.json") } if projectOptions.FrontEnd.Bridge == "" { return fmt.Errorf("Frontend bridge config not set in project.json") } return nil } // InstallGoDependencies will run go get in the current directory func InstallGoDependencies(verbose bool) error { var depSpinner *spinner.Spinner if !verbose { depSpinner = spinner.New("Ensuring Dependencies are up to date...") depSpinner.SetSpinSpeed(50) depSpinner.Start() } err := NewProgramHelper(verbose).RunCommand("go get") if err != nil { if !verbose { depSpinner.Error() } return err } if !verbose { depSpinner.Success() } 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 } func InitializeCrossCompilation(verbose bool) error { // Check Docker if err := CheckIfInstalled("docker"); err != nil { return err } var packSpinner *spinner.Spinner if !verbose { packSpinner = spinner.New("Pulling wailsapp/xgo:latest docker image... (may take a while)") packSpinner.SetSpinSpeed(50) packSpinner.Start() } else { println("Pulling wailsapp/xgo:latest docker image... (may take a while)") } err := NewProgramHelper(verbose).RunCommandArray([]string{"docker", "pull", "wailsapp/xgo:latest"}) if err != nil { if packSpinner != nil { packSpinner.Error() } return err } if packSpinner != nil { packSpinner.Success() } return nil } // BuildDocker builds the project using the cross compiling wailsapp/xgo:latest container func BuildDocker(binaryName string, buildMode string, projectOptions *ProjectOptions) error { var packSpinner *spinner.Spinner if buildMode == BuildModeBridge { return fmt.Errorf("you cant serve the application in cross-compilation") } // Check build directory buildDirectory := filepath.Join(fs.Cwd(), "build") if !fs.DirExists(buildDirectory) { fs.MkDir(buildDirectory) } buildCommand := slicer.String() userid := 1000 user, _ := user.Current() if i, err := strconv.Atoi(user.Uid); err == nil { userid = i } for _, arg := range []string{ "docker", "run", "--rm", "-v", fmt.Sprintf("%s:/build", filepath.Join(fs.Cwd(), "build")), "-v", fmt.Sprintf("%s:/source", fs.Cwd()), "-e", fmt.Sprintf("LOCAL_USER_ID=%v", userid), "-e", fmt.Sprintf("FLAG_LDFLAGS=%s", ldFlags(projectOptions, buildMode)), "-e", "FLAG_V=false", "-e", "FLAG_X=false", "-e", "FLAG_RACE=false", "-e", "FLAG_BUILDMODE=default", "-e", "FLAG_TRIMPATH=false", "-e", fmt.Sprintf("TARGETS=%s", projectOptions.Platform+"/"+projectOptions.Architecture), "-e", "GOPROXY=", "-e", "GO111MODULE=on", "wailsapp/xgo:latest", ".", } { buildCommand.Add(arg) } compileMessage := fmt.Sprintf( "Packing + Compiling project for %s/%s using docker image wailsapp/xgo:latest", projectOptions.Platform, projectOptions.Architecture) if buildMode == BuildModeDebug { compileMessage += " (Debug Mode)" } if !projectOptions.Verbose { packSpinner = spinner.New(compileMessage + "...") packSpinner.SetSpinSpeed(50) packSpinner.Start() } else { println(compileMessage) } err := NewProgramHelper(projectOptions.Verbose).RunCommandArray(buildCommand.AsSlice()) if err != nil { if packSpinner != nil { packSpinner.Error() } return err } if packSpinner != nil { packSpinner.Success() } return nil } // BuildNative builds on the target platform itself. func BuildNative(binaryName string, forceRebuild bool, buildMode string, projectOptions *ProjectOptions) error { // Check Mewn is installed if err := CheckMewn(projectOptions.Verbose); err != nil { return err } if err := CheckWindres(); err != nil { return err } compileMessage := "Packing + Compiling project" if buildMode == BuildModeDebug { compileMessage += " (Debug Mode)" } var packSpinner *spinner.Spinner if !projectOptions.Verbose { packSpinner = spinner.New(compileMessage + "...") packSpinner.SetSpinSpeed(50) packSpinner.Start() } else { println(compileMessage) } buildCommand := slicer.String() buildCommand.Add("go") buildCommand.Add("build") if buildMode == BuildModeBridge { // Ignore errors buildCommand.Add("-i") } if binaryName != "" { // Alter binary name based on OS switch projectOptions.Platform { case "windows": if !strings.HasSuffix(binaryName, ".exe") { binaryName += ".exe" } default: if strings.HasSuffix(binaryName, ".exe") { binaryName = strings.TrimSuffix(binaryName, ".exe") } } buildCommand.Add("-o", filepath.Join("build", binaryName)) } // If we are forcing a rebuild if forceRebuild { buildCommand.Add("-a") } buildCommand.AddSlice([]string{"-ldflags", ldFlags(projectOptions, buildMode)}) if projectOptions.Verbose { fmt.Printf("Command: %v\n", buildCommand.AsSlice()) } err := NewProgramHelper(projectOptions.Verbose).RunCommandArray(buildCommand.AsSlice()) if err != nil { if packSpinner != nil { packSpinner.Error() } return err } if packSpinner != nil { packSpinner.Success() } return 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 { var err error // embed resources targetFiles, err := EmbedAssets() if err != nil { return err } if projectOptions.CrossCompile { if err := InitializeCrossCompilation(projectOptions.Verbose); err != nil { return err } } helper := NewPackageHelper(projectOptions.Platform) // Generate windows resources if projectOptions.Platform == "windows" { if err := helper.PackageWindows(projectOptions, false); err != nil { return err } } // cleanup temporary embedded assets defer func() { for _, filename := range targetFiles { if err := os.Remove(filename); err != nil { fmt.Println(err) } } // Removed by popular demand // TODO: Potentially add a flag to cleanup // if projectOptions.Platform == "windows" { // helper.CleanWindows(projectOptions) // } }() if projectOptions.CrossCompile { err = BuildDocker(binaryName, buildMode, projectOptions) } else { err = BuildNative(binaryName, forceRebuild, buildMode, projectOptions) } if err != nil { return err } if packageApp { err = PackageApplication(projectOptions) if err != nil { return err } } return nil } // PackageApplication will attempt to package the application in a platform dependent way func PackageApplication(projectOptions *ProjectOptions) error { var packageSpinner *spinner.Spinner if projectOptions.Verbose { packageSpinner = spinner.New("Packaging application...") packageSpinner.SetSpinSpeed(50) packageSpinner.Start() } err := NewPackageHelper(projectOptions.Platform).Package(projectOptions) if err != nil { if packageSpinner != nil { packageSpinner.Error() } return err } if packageSpinner != nil { packageSpinner.Success() } return nil } // BuildFrontend runs the given build command func BuildFrontend(projectOptions *ProjectOptions) error { var buildFESpinner *spinner.Spinner if !projectOptions.Verbose { buildFESpinner = spinner.New("Building frontend...") buildFESpinner.SetSpinSpeed(50) buildFESpinner.Start() } else { println("Building frontend...") } err := NewProgramHelper(projectOptions.Verbose).RunCommand(projectOptions.FrontEnd.Build) if err != nil { if buildFESpinner != nil { buildFESpinner.Error() } return err } if buildFESpinner != nil { buildFESpinner.Success() } return nil } // CheckMewn checks if mewn is installed and if not, attempts to fetch it func CheckMewn(verbose bool) (err error) { programHelper := NewProgramHelper(verbose) if !programHelper.IsInstalled("mewn") { var buildSpinner *spinner.Spinner if !verbose { buildSpinner = spinner.New() buildSpinner.SetSpinSpeed(50) buildSpinner.Start("Installing Mewn asset packer...") } err := programHelper.InstallGoPackage("github.com/leaanthony/mewn/cmd/mewn") if err != nil { if buildSpinner != nil { buildSpinner.Error() } return err } if buildSpinner != nil { buildSpinner.Success() } } return nil } // CheckWindres checks if Windres is installed and if not, aborts func CheckWindres() (err error) { if runtime.GOOS != "windows" { // FIXME: Handle windows cross-compile for windows! return nil } programHelper := NewProgramHelper() if !programHelper.IsInstalled("windres") { return fmt.Errorf("windres not installed. It comes by default with mingw. Ensure you have installed mingw correctly") } 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 { // Install frontend deps err := os.Chdir(projectOptions.FrontEnd.Dir) if err != nil { return err } // Check if frontend deps have been updated var feSpinner *spinner.Spinner if !projectOptions.Verbose { feSpinner = spinner.New("Ensuring frontend dependencies are up to date (This may take a while)") feSpinner.SetSpinSpeed(50) feSpinner.Start() } else { println("Ensuring frontend dependencies are up to date (This may take a while)") } requiresNPMInstall := true // Read in package.json MD5 fs := NewFSHelper() packageJSONMD5, err := fs.FileMD5("package.json") if err != nil { return err } const md5sumFile = "package.json.md5" // If node_modules does not exist, force a rebuild. nodeModulesPath, err := filepath.Abs(filepath.Join(".", "node_modules")) if err != nil { return err } if !fs.DirExists(nodeModulesPath) { forceRebuild = true } // If we aren't forcing the install and the md5sum file exists if !forceRebuild && fs.FileExists(md5sumFile) { // Yes - read contents savedMD5sum, err := fs.LoadAsString(md5sumFile) // File exists if err == nil { // Compare md5 if savedMD5sum == packageJSONMD5 { // Same - no need for reinstall requiresNPMInstall = false if feSpinner != nil { feSpinner.Success("Skipped frontend dependencies (-f to force rebuild)") } else { println("Skipped frontend dependencies (-f to force rebuild)") } } } } // Md5 sum package.json // Different? Build if requiresNPMInstall || forceRebuild { // Install dependencies err = NewProgramHelper(projectOptions.Verbose).RunCommand(projectOptions.FrontEnd.Install) if err != nil { if feSpinner != nil { feSpinner.Error() } return err } if feSpinner != nil { feSpinner.Success() } // Update md5sum file ioutil.WriteFile(md5sumFile, []byte(packageJSONMD5), 0644) } // Install the runtime err = InstallRuntime(caller, projectDir, projectOptions) if err != nil { return err } // Build frontend err = BuildFrontend(projectOptions) if err != nil { return err } return nil } // InstallRuntime installs the correct runtime for the type of build func InstallRuntime(caller string, projectDir string, projectOptions *ProjectOptions) error { if caller == "build" { return InstallProdRuntime(projectDir, projectOptions) } return InstallBridge(projectDir, projectOptions) } // InstallBridge installs the relevant bridge javascript library func InstallBridge(projectDir string, projectOptions *ProjectOptions) error { bridgeFileData := mewn.String("../runtime/assets/bridge.js") bridgeFileTarget := filepath.Join(projectDir, projectOptions.FrontEnd.Dir, "node_modules", "@wailsapp", "runtime", "init.js") err := fs.CreateFile(bridgeFileTarget, []byte(bridgeFileData)) return err } // InstallProdRuntime installs the production runtime func InstallProdRuntime(projectDir string, projectOptions *ProjectOptions) error { prodInit := mewn.String("../runtime/js/runtime/init.js") bridgeFileTarget := filepath.Join(projectDir, projectOptions.FrontEnd.Dir, "node_modules", "@wailsapp", "runtime", "init.js") err := fs.CreateFile(bridgeFileTarget, []byte(prodInit)) return err } // ServeProject attempts to serve up the current project so that it may be connected to // via the Wails bridge func ServeProject(projectOptions *ProjectOptions, logger *Logger) error { go func() { time.Sleep(2 * time.Second) logger.Green(">>>>> To connect, you will need to run '" + projectOptions.FrontEnd.Serve + "' in the '" + projectOptions.FrontEnd.Dir + "' directory <<<<<") }() location, err := filepath.Abs(filepath.Join("build", projectOptions.BinaryName)) if err != nil { return err } logger.Yellow("Serving Application: " + location) cmd := exec.Command(location) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Run() if err != nil { return err } return nil } func ldFlags(po *ProjectOptions, buildMode string) string { // Setup ld flags ldflags := "-w -s " if buildMode == BuildModeDebug { ldflags = "" } // Add windows flags if po.Platform == "windows" && buildMode == BuildModeProd { ldflags += "-H windowsgui " } ldflags += "-X github.com/wailsapp/wails.BuildMode=" + buildMode // Add additional ldflags passed in via the `ldflags` cli flag if len(po.LdFlags) > 0 { ldflags += " " + po.LdFlags } // If we wish to generate typescript if po.typescriptDefsFilename != "" { cwd, err := os.Getwd() if err == nil { filename := filepath.Join(cwd, po.FrontEnd.Dir, po.typescriptDefsFilename) ldflags += " -X github.com/wailsapp/wails/lib/binding.typescriptDefinitionFilename=" + filename } } return ldflags }