Files

607 lines
15 KiB
Go
Raw Permalink Normal View History

2020-09-15 19:52:54 -05:00
package build
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
2021-12-11 20:06:42 +11:00
"strconv"
2020-09-15 19:52:54 -05:00
"strings"
"github.com/pterm/pterm"
2021-12-11 20:06:42 +11:00
"github.com/wailsapp/wails/v2/internal/system"
2021-10-30 10:33:30 +11:00
"github.com/leaanthony/gosod"
"github.com/wailsapp/wails/v2/internal/frontend/runtime/wrapper"
2021-03-27 20:59:14 +11:00
"github.com/pkg/errors"
2020-09-15 19:52:54 -05:00
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/wailsapp/wails/v2/internal/project"
"github.com/wailsapp/wails/v2/internal/shell"
2020-10-17 13:47:13 +11:00
"github.com/wailsapp/wails/v2/pkg/clilogger"
2020-09-15 19:52:54 -05:00
)
const (
VERBOSE int = 2
)
2020-09-15 19:52:54 -05:00
// BaseBuilder is the common builder struct
type BaseBuilder struct {
filesToDelete slicer.StringSlicer
projectData *project.Project
2021-01-03 05:02:48 +11:00
options *Options
2020-09-15 19:52:54 -05:00
}
// NewBaseBuilder creates a new BaseBuilder
2021-01-03 05:02:48 +11:00
func NewBaseBuilder(options *Options) *BaseBuilder {
result := &BaseBuilder{
options: options,
}
2020-09-15 19:52:54 -05:00
return result
}
// SetProjectData sets the project data for this builder
func (b *BaseBuilder) SetProjectData(projectData *project.Project) {
b.projectData = projectData
}
func (b *BaseBuilder) addFileToDelete(filename string) {
2021-01-03 05:02:48 +11:00
if !b.options.KeepAssets {
b.filesToDelete.Add(filename)
}
2020-09-15 19:52:54 -05:00
}
func (b *BaseBuilder) fileExists(path string) bool {
// if file doesn't exist, ignore
_, err := os.Stat(path)
if err != nil {
return !os.IsNotExist(err)
}
return true
}
func (b *BaseBuilder) convertFileToIntegerString(filename string) (string, error) {
rawData, err := os.ReadFile(filename)
2020-09-15 19:52:54 -05:00
if err != nil {
return "", err
}
return b.convertByteSliceToIntegerString(rawData), nil
}
func (b *BaseBuilder) convertByteSliceToIntegerString(data []byte) string {
// Create string builder
var result strings.Builder
if len(data) > 0 {
// Loop over all but 1 bytes
for i := 0; i < len(data)-1; i++ {
result.WriteString(fmt.Sprintf("%v,", data[i]))
}
2023-11-12 12:30:49 +11:00
result.WriteString(strconv.FormatUint(uint64(data[len(data)-1]), 10))
2020-09-15 19:52:54 -05:00
}
return result.String()
}
// CleanUp does post-build housekeeping
func (b *BaseBuilder) CleanUp() {
// Delete all the files
b.filesToDelete.Each(func(filename string) {
// if file doesn't exist, ignore
if !b.fileExists(filename) {
return
}
// Delete file. We ignore errors because these files will be overwritten
// by the next build anyway.
_ = os.Remove(filename)
2020-09-15 19:52:54 -05:00
})
}
func commandPrettifier(args []string) string {
// If we have a single argument, just return it
if len(args) == 1 {
return args[0]
}
// If an argument contains a space, quote it
for i, arg := range args {
if strings.Contains(arg, " ") {
args[i] = fmt.Sprintf("\"%s\"", arg)
}
}
return strings.Join(args, " ")
}
func (b *BaseBuilder) OutputFilename(options *Options) string {
outputFile := options.OutputFile
if outputFile == "" {
target := strings.TrimSuffix(b.projectData.OutputFilename, ".exe")
if b.projectData.OutputType != "desktop" {
target += "-" + b.projectData.OutputType
}
// If we aren't using the standard compiler, add it to the filename
if options.Compiler != "go" {
// Parse the `go version` output. EG: `go version go1.16 windows/amd64`
stdout, _, err := shell.RunCommand(".", options.Compiler, "version")
if err != nil {
return ""
}
versionSplit := strings.Split(stdout, " ")
if len(versionSplit) == 4 {
target += "-" + versionSplit[2]
}
}
switch b.options.Platform {
case "windows":
outputFile = target + ".exe"
case "darwin", "linux":
if b.options.Arch == "" {
b.options.Arch = runtime.GOARCH
}
outputFile = fmt.Sprintf("%s-%s-%s", target, b.options.Platform, b.options.Arch)
}
}
return outputFile
}
2020-09-15 19:52:54 -05:00
// CompileProject compiles the project
func (b *BaseBuilder) CompileProject(options *Options) error {
// Check if the runtime wrapper exists
err := generateRuntimeWrapper(options)
if err != nil {
return err
}
2021-03-26 18:12:42 +11:00
verbose := options.Verbosity == VERBOSE
2021-02-27 14:03:54 +11:00
// Run go mod tidy first
2021-12-12 15:01:16 +01:00
if !options.SkipModTidy {
2022-09-13 10:05:37 +10:00
cmd := exec.Command(options.Compiler, "mod", "tidy")
2021-12-12 15:01:16 +01:00
cmd.Stderr = os.Stderr
if verbose {
println("")
cmd.Stdout = os.Stdout
}
err = cmd.Run()
if err != nil {
return err
}
2021-02-27 14:03:54 +11:00
}
2022-09-13 10:05:37 +10:00
commands := slicer.String()
compiler := options.Compiler
if options.Obfuscated {
if !shell.CommandExists("garble") {
return fmt.Errorf("the 'garble' command was not found. Please install it with `go install mvdan.cc/garble@latest`")
} else {
compiler = "garble"
if options.GarbleArgs != "" {
commands.AddSlice(strings.Split(options.GarbleArgs, " "))
}
options.UserTags = append(options.UserTags, "obfuscated")
}
}
2020-09-15 19:52:54 -05:00
// Default go build command
2022-09-13 10:05:37 +10:00
commands.Add("build")
2020-09-15 19:52:54 -05:00
2021-01-11 11:21:28 +11:00
// Add better debugging flags
2021-12-29 06:54:42 +11:00
if options.Mode == Dev || options.Mode == Debug {
2021-01-11 11:21:28 +11:00
commands.Add("-gcflags")
commands.Add("all=-N -l")
2021-01-11 11:21:28 +11:00
}
2021-11-27 20:36:25 +11:00
if options.ForceBuild {
commands.Add("-a")
}
2022-04-27 19:10:20 +10:00
if options.TrimPath {
commands.Add("-trimpath")
}
if options.RaceDetector {
commands.Add("-race")
}
2020-09-15 19:52:54 -05:00
var tags slicer.StringSlicer
tags.Add(options.OutputType)
2021-04-04 13:42:48 +10:00
tags.AddSlice(options.UserTags)
// Add webview2 strategy if we have it
if options.WebView2Strategy != "" {
tags.Add(options.WebView2Strategy)
}
2021-12-29 06:54:42 +11:00
if options.Mode == Production || options.Mode == Debug {
tags.Add("production")
}
2022-01-17 21:14:07 +11:00
// This mode allows you to debug a production build (not dev build)
if options.Mode == Debug {
tags.Add("debug")
}
// This options allows you to enable devtools in production build (not dev build as it's always enabled there)
if options.Devtools {
tags.Add("devtools")
}
2022-09-13 10:05:37 +10:00
if options.Obfuscated {
tags.Add("obfuscated")
}
2021-04-04 13:42:48 +10:00
tags.Deduplicate()
2020-09-15 19:52:54 -05:00
// Add the output type build tag
commands.Add("-tags")
commands.Add(tags.Join(","))
2021-06-27 04:18:33 +10:00
// LDFlags
ldflags := slicer.String()
if options.LDFlags != "" {
ldflags.Add(options.LDFlags)
}
2020-09-15 19:52:54 -05:00
if options.Mode == Production {
2021-06-27 04:18:33 +10:00
ldflags.Add("-w", "-s")
2022-07-04 22:52:50 +10:00
if options.Platform == "windows" && !options.WindowsConsole {
2021-06-27 04:18:33 +10:00
ldflags.Add("-H windowsgui")
2020-09-15 19:52:54 -05:00
}
}
2021-06-27 04:18:33 +10:00
ldflags.Deduplicate()
if ldflags.Length() > 0 {
commands.Add("-ldflags")
commands.Add(ldflags.Join(" "))
}
2020-09-15 19:52:54 -05:00
// Get application build directory
2022-11-03 21:21:40 +11:00
appDir := options.BinDirectory
if options.CleanBinDirectory {
err = cleanBinDirectory(options)
2021-03-26 14:10:25 +11:00
if err != nil {
return err
}
}
2020-09-15 19:52:54 -05:00
// Set up output filename
outputFile := b.OutputFilename(options)
2020-11-21 07:09:25 +11:00
compiledBinary := filepath.Join(appDir, outputFile)
2020-09-15 19:52:54 -05:00
commands.Add("-o")
2020-11-21 07:09:25 +11:00
commands.Add(compiledBinary)
2020-09-15 19:52:54 -05:00
2020-11-21 07:09:25 +11:00
options.CompiledBinary = compiledBinary
2020-09-15 19:52:54 -05:00
2022-09-13 10:05:37 +10:00
// Build the application
cmd := exec.Command(compiler, commands.AsSlice()...)
2021-07-25 20:21:21 +10:00
cmd.Stderr = os.Stderr
2021-03-26 18:12:42 +11:00
if verbose {
2022-12-01 18:18:02 +11:00
pterm.Info.Println("Build command:", compiler, commandPrettifier(commands.AsSlice()))
cmd.Stdout = os.Stdout
}
2020-09-15 19:52:54 -05:00
// Set the directory
cmd.Dir = b.projectData.Path
// Add CGO flags
2022-11-03 21:21:40 +11:00
// TODO: Remove this as we don't generate headers any more
// We use the project/build dir as a temporary place for our generated c headers
buildBaseDir, err := fs.RelativeToCwd("build")
if err != nil {
return err
}
2021-03-12 23:41:13 +11:00
cmd.Env = os.Environ() // inherit env
if options.Platform != "windows" {
// Use shell.UpsertEnv so we don't overwrite user's CGO_CFLAGS
cmd.Env = shell.UpsertEnv(cmd.Env, "CGO_CFLAGS", func(v string) string {
2021-11-13 17:06:48 -08:00
if options.Platform == "darwin" {
if v != "" {
v += " "
}
v += "-mmacosx-version-min=10.13"
}
return v
})
// Use shell.UpsertEnv so we don't overwrite user's CGO_CXXFLAGS
cmd.Env = shell.UpsertEnv(cmd.Env, "CGO_CXXFLAGS", func(v string) string {
if v != "" {
v += " "
}
v += "-I" + buildBaseDir
return v
})
cmd.Env = shell.UpsertEnv(cmd.Env, "CGO_ENABLED", func(v string) string {
return "1"
})
2021-11-13 17:06:48 -08:00
if options.Platform == "darwin" {
2021-12-11 20:06:42 +11:00
// Determine version so we can link to newer frameworks
// Why doesn't CGO have this option?!?!
info, err := system.GetInfo()
if err != nil {
return err
}
versionSplit := strings.Split(info.OS.Version, ".")
2021-12-11 20:06:42 +11:00
majorVersion, err := strconv.Atoi(versionSplit[0])
if err != nil {
return err
}
addUTIFramework := majorVersion >= 11
2021-11-04 20:45:22 +11:00
// Set the minimum Mac SDK to 10.13
cmd.Env = shell.UpsertEnv(cmd.Env, "CGO_LDFLAGS", func(v string) string {
2021-11-04 20:45:22 +11:00
if v != "" {
v += " "
}
if addUTIFramework {
v += "-framework UniformTypeIdentifiers "
}
2021-11-04 20:45:22 +11:00
v += "-mmacosx-version-min=10.13"
return v
})
}
}
cmd.Env = shell.UpsertEnv(cmd.Env, "GOOS", func(v string) string {
return options.Platform
})
cmd.Env = shell.UpsertEnv(cmd.Env, "GOARCH", func(v string) string {
return options.Arch
})
2021-03-26 18:12:42 +11:00
if verbose {
2022-12-01 18:18:02 +11:00
printBulletPoint("Environment:", strings.Join(cmd.Env, " "))
2021-03-26 18:12:42 +11:00
}
2020-09-15 19:52:54 -05:00
// Run command
err = cmd.Run()
2021-07-25 20:21:21 +10:00
cmd.Stderr = os.Stderr
2020-09-15 19:52:54 -05:00
// Format error if we have one
if err != nil {
if options.Platform == "darwin" {
output, _ := cmd.CombinedOutput()
stdErr := string(output)
if strings.Contains(err.Error(), "ld: framework not found UniformTypeIdentifiers") ||
strings.Contains(stdErr, "ld: framework not found UniformTypeIdentifiers") {
2022-12-01 18:18:02 +11:00
pterm.Warning.Println(`
NOTE: It would appear that you do not have the latest Xcode cli tools installed.
Please reinstall by doing the following:
1. Remove the current installation located at "xcode-select -p", EG: sudo rm -rf /Library/Developer/CommandLineTools
2022-12-06 03:45:06 +08:00
2. Install latest Xcode tools: xcode-select --install`)
}
}
2021-07-25 20:21:21 +10:00
return err
2020-09-15 19:52:54 -05:00
}
2021-03-27 20:59:14 +11:00
if !options.Compress {
return nil
}
2022-12-01 18:18:02 +11:00
printBulletPoint("Compressing application: ")
2021-03-27 20:59:14 +11:00
// Do we have upx installed?
if !shell.CommandExists("upx") {
2022-12-01 18:18:02 +11:00
pterm.Warning.Println("Warning: Cannot compress binary: upx not found")
2021-03-27 20:59:14 +11:00
return nil
}
2023-11-12 12:30:49 +11:00
args := []string{"--best", "--no-color", "--no-progress", options.CompiledBinary}
2021-05-18 21:25:16 +10:00
if options.CompressFlags != "" {
args = strings.Split(options.CompressFlags, " ")
args = append(args, options.CompiledBinary)
2021-03-27 20:59:14 +11:00
}
2021-05-18 21:25:16 +10:00
if verbose {
2022-12-01 18:18:02 +11:00
pterm.Info.Println("upx", strings.Join(args, " "))
2021-05-18 21:25:16 +10:00
}
output, err := exec.Command("upx", args...).Output()
2021-03-27 20:59:14 +11:00
if err != nil {
return errors.Wrap(err, "Error during compression:")
}
2022-12-01 18:18:02 +11:00
pterm.Println("Done.")
2021-03-27 20:59:14 +11:00
if verbose {
2022-12-01 18:18:02 +11:00
pterm.Info.Println(string(output))
2021-03-27 20:59:14 +11:00
}
2020-09-15 19:52:54 -05:00
return nil
}
func generateRuntimeWrapper(options *Options) error {
2022-02-01 19:47:46 +11:00
if options.WailsJSDir == "" {
2022-04-27 21:29:54 +10:00
cwd, err := os.Getwd()
if err != nil {
return err
}
options.WailsJSDir = filepath.Join(cwd, "frontend")
2022-02-01 19:47:46 +11:00
}
wrapperDir := filepath.Join(options.WailsJSDir, "wailsjs", "runtime")
2021-09-15 23:20:47 +10:00
_ = os.RemoveAll(wrapperDir)
extractor := gosod.New(wrapper.RuntimeWrapper)
2021-10-04 19:58:46 +11:00
err := extractor.Extract(wrapperDir, nil)
if err != nil {
return err
}
return nil
}
2020-09-15 19:52:54 -05:00
// NpmInstall runs "npm install" in the given directory
func (b *BaseBuilder) NpmInstall(sourceDir string, verbose bool) error {
return b.NpmInstallUsingCommand(sourceDir, "npm install", verbose)
2020-09-15 19:52:54 -05:00
}
// NpmInstallUsingCommand runs the given install command in the specified npm project directory
func (b *BaseBuilder) NpmInstallUsingCommand(sourceDir string, installCommand string, verbose bool) error {
2020-09-15 19:52:54 -05:00
packageJSON := filepath.Join(sourceDir, "package.json")
// Check package.json exists
if !fs.FileExists(packageJSON) {
2021-09-07 07:06:18 +10:00
// No package.json, no install
return nil
2020-09-15 19:52:54 -05:00
}
install := false
// Get the MD5 sum of package.json
packageJSONMD5 := fs.MustMD5File(packageJSON)
// Check whether we need to npm install
packageChecksumFile := filepath.Join(sourceDir, "package.json.md5")
if fs.FileExists(packageChecksumFile) {
// Compare checksums
storedChecksum := fs.MustLoadString(packageChecksumFile)
if storedChecksum != packageJSONMD5 {
fs.MustWriteString(packageChecksumFile, packageJSONMD5)
install = true
}
} else {
install = true
fs.MustWriteString(packageChecksumFile, packageJSONMD5)
}
// Install if node_modules doesn't exist
nodeModulesDir := filepath.Join(sourceDir, "node_modules")
if !fs.DirExists(nodeModulesDir) {
install = true
}
// check if forced install
if b.options.ForceBuild {
install = true
}
2020-09-15 19:52:54 -05:00
// Shortcut installation
2023-11-12 12:30:49 +11:00
if !install {
2022-11-03 21:21:40 +11:00
if verbose {
2022-12-01 18:18:02 +11:00
pterm.Println("Skipping npm install")
2022-11-03 21:21:40 +11:00
}
2020-09-15 19:52:54 -05:00
return nil
}
// Split up the InstallCommand and execute it
cmd := strings.Split(installCommand, " ")
stdout, stderr, err := shell.RunCommand(sourceDir, cmd[0], cmd[1:]...)
if verbose || err != nil {
2020-09-15 19:52:54 -05:00
for _, l := range strings.Split(stdout, "\n") {
2022-12-01 18:18:02 +11:00
pterm.Printf(" %s\n", l)
2020-09-15 19:52:54 -05:00
}
for _, l := range strings.Split(stderr, "\n") {
2022-12-01 18:18:02 +11:00
pterm.Printf(" %s\n", l)
2020-09-15 19:52:54 -05:00
}
}
return err
}
// NpmRun executes the npm target in the provided directory
func (b *BaseBuilder) NpmRun(projectDir, buildTarget string, verbose bool) error {
stdout, stderr, err := shell.RunCommand(projectDir, "npm", "run", buildTarget)
if verbose || err != nil {
for _, l := range strings.Split(stdout, "\n") {
2022-12-01 18:18:02 +11:00
pterm.Printf(" %s\n", l)
2020-09-15 19:52:54 -05:00
}
for _, l := range strings.Split(stderr, "\n") {
2022-12-01 18:18:02 +11:00
pterm.Printf(" %s\n", l)
2020-09-15 19:52:54 -05:00
}
}
return err
}
// NpmRunWithEnvironment executes the npm target in the provided directory, with the given environment variables
func (b *BaseBuilder) NpmRunWithEnvironment(projectDir, buildTarget string, verbose bool, envvars []string) error {
cmd := shell.CreateCommand(projectDir, "npm", "run", buildTarget)
cmd.Env = append(os.Environ(), envvars...)
var stdo, stde bytes.Buffer
cmd.Stdout = &stdo
cmd.Stderr = &stde
err := cmd.Run()
if verbose || err != nil {
for _, l := range strings.Split(stdo.String(), "\n") {
2022-12-01 18:18:02 +11:00
pterm.Printf(" %s\n", l)
2020-09-15 19:52:54 -05:00
}
for _, l := range strings.Split(stde.String(), "\n") {
2022-12-01 18:18:02 +11:00
pterm.Printf(" %s\n", l)
2020-09-15 19:52:54 -05:00
}
}
return err
}
// BuildFrontend executes the `npm build` command for the frontend directory
2021-01-01 13:00:38 +11:00
func (b *BaseBuilder) BuildFrontend(outputLogger *clilogger.CLILogger) error {
verbose := b.options.Verbosity == VERBOSE
2020-09-15 19:52:54 -05:00
2022-11-03 21:21:40 +11:00
frontendDir := b.projectData.GetFrontendDir()
if !fs.DirExists(frontendDir) {
return fmt.Errorf("frontend directory '%s' does not exist", frontendDir)
}
2020-09-15 19:52:54 -05:00
// Check there is an 'InstallCommand' provided in wails.json
installCommand := b.projectData.InstallCommand
if b.projectData.OutputType == "dev" {
installCommand = b.projectData.GetDevInstallerCommand()
}
if installCommand == "" {
2020-09-15 19:52:54 -05:00
// No - don't install
2022-12-01 18:18:02 +11:00
printBulletPoint("No Install command. Skipping.")
pterm.Println("")
2020-09-15 19:52:54 -05:00
} else {
// Do install if needed
2022-12-01 18:18:02 +11:00
printBulletPoint("Installing frontend dependencies: ")
if verbose {
2022-12-01 18:18:02 +11:00
pterm.Println("")
pterm.Info.Println("Install command: '" + installCommand + "'")
}
if err := b.NpmInstallUsingCommand(frontendDir, installCommand, verbose); err != nil {
2020-09-15 19:52:54 -05:00
return err
}
outputLogger.Println("Done.")
2020-09-15 19:52:54 -05:00
}
// Check if there is a build command
buildCommand := b.projectData.BuildCommand
if b.projectData.OutputType == "dev" {
buildCommand = b.projectData.GetDevBuildCommand()
}
if buildCommand == "" {
2022-12-01 18:18:02 +11:00
printBulletPoint("No Build command. Skipping.")
pterm.Println("")
2020-09-15 19:52:54 -05:00
// No - ignore
return nil
}
2022-12-01 18:18:02 +11:00
printBulletPoint("Compiling frontend: ")
2021-10-28 19:24:05 +11:00
cmd := strings.Split(buildCommand, " ")
if verbose {
2022-12-01 18:18:02 +11:00
pterm.Println("")
pterm.Info.Println("Build command: '" + buildCommand + "'")
}
2020-09-15 19:52:54 -05:00
stdout, stderr, err := shell.RunCommand(frontendDir, cmd[0], cmd[1:]...)
if verbose || err != nil {
for _, l := range strings.Split(stdout, "\n") {
2022-12-01 18:18:02 +11:00
pterm.Printf(" %s\n", l)
2020-09-15 19:52:54 -05:00
}
for _, l := range strings.Split(stderr, "\n") {
2022-12-01 18:18:02 +11:00
pterm.Printf(" %s\n", l)
2020-09-15 19:52:54 -05:00
}
}
if err != nil {
return err
}
2022-12-01 18:18:02 +11:00
pterm.Println("Done.")
return nil
2020-09-15 19:52:54 -05:00
}