mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-02 23:51:44 +08:00
581 lines
15 KiB
Go
581 lines
15 KiB
Go
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
|
|
}
|