github.com/AlpineAIO/wails/v2@v2.0.0-beta.32.0.20240505041856-1047a8fa5fef/pkg/commands/build/base.go (about) 1 package build 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "os/exec" 8 "path/filepath" 9 "runtime" 10 "strconv" 11 "strings" 12 13 "github.com/pterm/pterm" 14 15 "github.com/AlpineAIO/wails/v2/internal/system" 16 17 "github.com/AlpineAIO/wails/v2/internal/frontend/runtime/wrapper" 18 "github.com/leaanthony/gosod" 19 20 "github.com/pkg/errors" 21 22 "github.com/AlpineAIO/wails/v2/internal/fs" 23 "github.com/AlpineAIO/wails/v2/internal/project" 24 "github.com/AlpineAIO/wails/v2/internal/shell" 25 "github.com/AlpineAIO/wails/v2/pkg/clilogger" 26 "github.com/leaanthony/slicer" 27 ) 28 29 const ( 30 VERBOSE int = 2 31 ) 32 33 // BaseBuilder is the common builder struct 34 type BaseBuilder struct { 35 filesToDelete slicer.StringSlicer 36 projectData *project.Project 37 options *Options 38 } 39 40 // NewBaseBuilder creates a new BaseBuilder 41 func NewBaseBuilder(options *Options) *BaseBuilder { 42 result := &BaseBuilder{ 43 options: options, 44 } 45 return result 46 } 47 48 // SetProjectData sets the project data for this builder 49 func (b *BaseBuilder) SetProjectData(projectData *project.Project) { 50 b.projectData = projectData 51 } 52 53 func (b *BaseBuilder) addFileToDelete(filename string) { 54 if !b.options.KeepAssets { 55 b.filesToDelete.Add(filename) 56 } 57 } 58 59 func (b *BaseBuilder) fileExists(path string) bool { 60 // if file doesn't exist, ignore 61 _, err := os.Stat(path) 62 if err != nil { 63 return !os.IsNotExist(err) 64 } 65 return true 66 } 67 68 func (b *BaseBuilder) convertFileToIntegerString(filename string) (string, error) { 69 rawData, err := os.ReadFile(filename) 70 if err != nil { 71 return "", err 72 } 73 return b.convertByteSliceToIntegerString(rawData), nil 74 } 75 76 func (b *BaseBuilder) convertByteSliceToIntegerString(data []byte) string { 77 // Create string builder 78 var result strings.Builder 79 80 if len(data) > 0 { 81 82 // Loop over all but 1 bytes 83 for i := 0; i < len(data)-1; i++ { 84 result.WriteString(fmt.Sprintf("%v,", data[i])) 85 } 86 87 result.WriteString(strconv.FormatUint(uint64(data[len(data)-1]), 10)) 88 } 89 90 return result.String() 91 } 92 93 // CleanUp does post-build housekeeping 94 func (b *BaseBuilder) CleanUp() { 95 // Delete all the files 96 b.filesToDelete.Each(func(filename string) { 97 // if file doesn't exist, ignore 98 if !b.fileExists(filename) { 99 return 100 } 101 102 // Delete file. We ignore errors because these files will be overwritten 103 // by the next build anyway. 104 _ = os.Remove(filename) 105 }) 106 } 107 108 func commandPrettifier(args []string) string { 109 // If we have a single argument, just return it 110 if len(args) == 1 { 111 return args[0] 112 } 113 // If an argument contains a space, quote it 114 for i, arg := range args { 115 if strings.Contains(arg, " ") { 116 args[i] = fmt.Sprintf("\"%s\"", arg) 117 } 118 } 119 return strings.Join(args, " ") 120 } 121 122 func (b *BaseBuilder) OutputFilename(options *Options) string { 123 outputFile := options.OutputFile 124 if outputFile == "" { 125 target := strings.TrimSuffix(b.projectData.OutputFilename, ".exe") 126 if b.projectData.OutputType != "desktop" { 127 target += "-" + b.projectData.OutputType 128 } 129 // If we aren't using the standard compiler, add it to the filename 130 if options.Compiler != "go" { 131 // Parse the `go version` output. EG: `go version go1.16 windows/amd64` 132 stdout, _, err := shell.RunCommand(".", options.Compiler, "version") 133 if err != nil { 134 return "" 135 } 136 versionSplit := strings.Split(stdout, " ") 137 if len(versionSplit) == 4 { 138 target += "-" + versionSplit[2] 139 } 140 } 141 switch b.options.Platform { 142 case "windows": 143 outputFile = target + ".exe" 144 case "darwin", "linux": 145 if b.options.Arch == "" { 146 b.options.Arch = runtime.GOARCH 147 } 148 outputFile = fmt.Sprintf("%s-%s-%s", target, b.options.Platform, b.options.Arch) 149 } 150 151 } 152 return outputFile 153 } 154 155 // CompileProject compiles the project 156 func (b *BaseBuilder) CompileProject(options *Options) error { 157 // Check if the runtime wrapper exists 158 err := generateRuntimeWrapper(options) 159 if err != nil { 160 return err 161 } 162 163 verbose := options.Verbosity == VERBOSE 164 // Run go mod tidy first 165 if !options.SkipModTidy { 166 cmd := exec.Command(options.Compiler, "mod", "tidy") 167 cmd.Stderr = os.Stderr 168 if verbose { 169 println("") 170 cmd.Stdout = os.Stdout 171 } 172 err = cmd.Run() 173 if err != nil { 174 return err 175 } 176 } 177 178 commands := slicer.String() 179 180 compiler := options.Compiler 181 if options.Obfuscated { 182 if !shell.CommandExists("garble") { 183 return fmt.Errorf("the 'garble' command was not found. Please install it with `go install mvdan.cc/garble@latest`") 184 } else { 185 compiler = "garble" 186 if options.GarbleArgs != "" { 187 commands.AddSlice(strings.Split(options.GarbleArgs, " ")) 188 } 189 options.UserTags = append(options.UserTags, "obfuscated") 190 } 191 } 192 193 // Default go build command 194 commands.Add("build") 195 196 // Add better debugging flags 197 if options.Mode == Dev || options.Mode == Debug { 198 commands.Add("-gcflags") 199 commands.Add("all=-N -l") 200 } 201 202 if options.ForceBuild { 203 commands.Add("-a") 204 } 205 206 if options.TrimPath { 207 commands.Add("-trimpath") 208 } 209 210 if options.RaceDetector { 211 commands.Add("-race") 212 } 213 214 var tags slicer.StringSlicer 215 tags.Add(options.OutputType) 216 tags.AddSlice(options.UserTags) 217 218 // Add webview2 strategy if we have it 219 if options.WebView2Strategy != "" { 220 tags.Add(options.WebView2Strategy) 221 } 222 223 if options.Mode == Production || options.Mode == Debug { 224 tags.Add("production") 225 } 226 // This mode allows you to debug a production build (not dev build) 227 if options.Mode == Debug { 228 tags.Add("debug") 229 } 230 231 // This options allows you to enable devtools in production build (not dev build as it's always enabled there) 232 if options.Devtools { 233 tags.Add("devtools") 234 } 235 236 if options.Obfuscated { 237 tags.Add("obfuscated") 238 } 239 240 tags.Deduplicate() 241 242 // Add the output type build tag 243 commands.Add("-tags") 244 commands.Add(tags.Join(",")) 245 246 // LDFlags 247 ldflags := slicer.String() 248 if options.LDFlags != "" { 249 ldflags.Add(options.LDFlags) 250 } 251 252 if options.Mode == Production { 253 ldflags.Add("-w", "-s") 254 if options.Platform == "windows" && !options.WindowsConsole { 255 ldflags.Add("-H windowsgui") 256 } 257 } 258 259 ldflags.Deduplicate() 260 261 if ldflags.Length() > 0 { 262 commands.Add("-ldflags") 263 commands.Add(ldflags.Join(" ")) 264 } 265 266 // Get application build directory 267 appDir := options.BinDirectory 268 if options.CleanBinDirectory { 269 err = cleanBinDirectory(options) 270 if err != nil { 271 return err 272 } 273 } 274 275 // Set up output filename 276 outputFile := b.OutputFilename(options) 277 compiledBinary := filepath.Join(appDir, outputFile) 278 commands.Add("-o") 279 commands.Add(compiledBinary) 280 281 options.CompiledBinary = compiledBinary 282 283 // Build the application 284 cmd := exec.Command(compiler, commands.AsSlice()...) 285 cmd.Stderr = os.Stderr 286 if verbose { 287 pterm.Info.Println("Build command:", compiler, commandPrettifier(commands.AsSlice())) 288 cmd.Stdout = os.Stdout 289 } 290 // Set the directory 291 cmd.Dir = b.projectData.Path 292 293 // Add CGO flags 294 // TODO: Remove this as we don't generate headers any more 295 // We use the project/build dir as a temporary place for our generated c headers 296 buildBaseDir, err := fs.RelativeToCwd("build") 297 if err != nil { 298 return err 299 } 300 301 cmd.Env = os.Environ() // inherit env 302 303 if options.Platform != "windows" { 304 // Use shell.UpsertEnv so we don't overwrite user's CGO_CFLAGS 305 cmd.Env = shell.UpsertEnv(cmd.Env, "CGO_CFLAGS", func(v string) string { 306 if options.Platform == "darwin" { 307 if v != "" { 308 v += " " 309 } 310 v += "-mmacosx-version-min=10.13" 311 } 312 return v 313 }) 314 // Use shell.UpsertEnv so we don't overwrite user's CGO_CXXFLAGS 315 cmd.Env = shell.UpsertEnv(cmd.Env, "CGO_CXXFLAGS", func(v string) string { 316 if v != "" { 317 v += " " 318 } 319 v += "-I" + buildBaseDir 320 return v 321 }) 322 323 cmd.Env = shell.UpsertEnv(cmd.Env, "CGO_ENABLED", func(v string) string { 324 return "1" 325 }) 326 if options.Platform == "darwin" { 327 // Determine version so we can link to newer frameworks 328 // Why doesn't CGO have this option?!?! 329 info, err := system.GetInfo() 330 if err != nil { 331 return err 332 } 333 versionSplit := strings.Split(info.OS.Version, ".") 334 majorVersion, err := strconv.Atoi(versionSplit[0]) 335 if err != nil { 336 return err 337 } 338 addUTIFramework := majorVersion >= 11 339 // Set the minimum Mac SDK to 10.13 340 cmd.Env = shell.UpsertEnv(cmd.Env, "CGO_LDFLAGS", func(v string) string { 341 if v != "" { 342 v += " " 343 } 344 if addUTIFramework { 345 v += "-framework UniformTypeIdentifiers " 346 } 347 v += "-mmacosx-version-min=10.13" 348 349 return v 350 }) 351 } 352 } 353 354 cmd.Env = shell.UpsertEnv(cmd.Env, "GOOS", func(v string) string { 355 return options.Platform 356 }) 357 358 cmd.Env = shell.UpsertEnv(cmd.Env, "GOARCH", func(v string) string { 359 return options.Arch 360 }) 361 362 if verbose { 363 printBulletPoint("Environment:", strings.Join(cmd.Env, " ")) 364 } 365 366 // Run command 367 err = cmd.Run() 368 cmd.Stderr = os.Stderr 369 370 // Format error if we have one 371 if err != nil { 372 if options.Platform == "darwin" { 373 output, _ := cmd.CombinedOutput() 374 stdErr := string(output) 375 if strings.Contains(err.Error(), "ld: framework not found UniformTypeIdentifiers") || 376 strings.Contains(stdErr, "ld: framework not found UniformTypeIdentifiers") { 377 pterm.Warning.Println(` 378 NOTE: It would appear that you do not have the latest Xcode cli tools installed. 379 Please reinstall by doing the following: 380 1. Remove the current installation located at "xcode-select -p", EG: sudo rm -rf /Library/Developer/CommandLineTools 381 2. Install latest Xcode tools: xcode-select --install`) 382 } 383 } 384 return err 385 } 386 387 if !options.Compress { 388 return nil 389 } 390 391 printBulletPoint("Compressing application: ") 392 393 // Do we have upx installed? 394 if !shell.CommandExists("upx") { 395 pterm.Warning.Println("Warning: Cannot compress binary: upx not found") 396 return nil 397 } 398 399 args := []string{"--best", "--no-color", "--no-progress", options.CompiledBinary} 400 401 if options.CompressFlags != "" { 402 args = strings.Split(options.CompressFlags, " ") 403 args = append(args, options.CompiledBinary) 404 } 405 406 if verbose { 407 pterm.Info.Println("upx", strings.Join(args, " ")) 408 } 409 410 output, err := exec.Command("upx", args...).Output() 411 if err != nil { 412 return errors.Wrap(err, "Error during compression:") 413 } 414 pterm.Println("Done.") 415 if verbose { 416 pterm.Info.Println(string(output)) 417 } 418 419 return nil 420 } 421 422 func generateRuntimeWrapper(options *Options) error { 423 if options.WailsJSDir == "" { 424 cwd, err := os.Getwd() 425 if err != nil { 426 return err 427 } 428 options.WailsJSDir = filepath.Join(cwd, "frontend") 429 } 430 wrapperDir := filepath.Join(options.WailsJSDir, "wailsjs", "runtime") 431 _ = os.RemoveAll(wrapperDir) 432 extractor := gosod.New(wrapper.RuntimeWrapper) 433 err := extractor.Extract(wrapperDir, nil) 434 if err != nil { 435 return err 436 } 437 438 return nil 439 } 440 441 // NpmInstall runs "npm install" in the given directory 442 func (b *BaseBuilder) NpmInstall(sourceDir string, verbose bool) error { 443 return b.NpmInstallUsingCommand(sourceDir, "npm install", verbose) 444 } 445 446 // NpmInstallUsingCommand runs the given install command in the specified npm project directory 447 func (b *BaseBuilder) NpmInstallUsingCommand(sourceDir string, installCommand string, verbose bool) error { 448 packageJSON := filepath.Join(sourceDir, "package.json") 449 450 // Check package.json exists 451 if !fs.FileExists(packageJSON) { 452 // No package.json, no install 453 return nil 454 } 455 456 install := false 457 458 // Get the MD5 sum of package.json 459 packageJSONMD5 := fs.MustMD5File(packageJSON) 460 461 // Check whether we need to npm install 462 packageChecksumFile := filepath.Join(sourceDir, "package.json.md5") 463 if fs.FileExists(packageChecksumFile) { 464 // Compare checksums 465 storedChecksum := fs.MustLoadString(packageChecksumFile) 466 if storedChecksum != packageJSONMD5 { 467 fs.MustWriteString(packageChecksumFile, packageJSONMD5) 468 install = true 469 } 470 } else { 471 install = true 472 fs.MustWriteString(packageChecksumFile, packageJSONMD5) 473 } 474 475 // Install if node_modules doesn't exist 476 nodeModulesDir := filepath.Join(sourceDir, "node_modules") 477 if !fs.DirExists(nodeModulesDir) { 478 install = true 479 } 480 481 // check if forced install 482 if b.options.ForceBuild { 483 install = true 484 } 485 486 // Shortcut installation 487 if !install { 488 if verbose { 489 pterm.Println("Skipping npm install") 490 } 491 return nil 492 } 493 494 // Split up the InstallCommand and execute it 495 cmd := strings.Split(installCommand, " ") 496 stdout, stderr, err := shell.RunCommand(sourceDir, cmd[0], cmd[1:]...) 497 if verbose || err != nil { 498 for _, l := range strings.Split(stdout, "\n") { 499 pterm.Printf(" %s\n", l) 500 } 501 for _, l := range strings.Split(stderr, "\n") { 502 pterm.Printf(" %s\n", l) 503 } 504 } 505 506 return err 507 } 508 509 // NpmRun executes the npm target in the provided directory 510 func (b *BaseBuilder) NpmRun(projectDir, buildTarget string, verbose bool) error { 511 stdout, stderr, err := shell.RunCommand(projectDir, "npm", "run", buildTarget) 512 if verbose || err != nil { 513 for _, l := range strings.Split(stdout, "\n") { 514 pterm.Printf(" %s\n", l) 515 } 516 for _, l := range strings.Split(stderr, "\n") { 517 pterm.Printf(" %s\n", l) 518 } 519 } 520 return err 521 } 522 523 // NpmRunWithEnvironment executes the npm target in the provided directory, with the given environment variables 524 func (b *BaseBuilder) NpmRunWithEnvironment(projectDir, buildTarget string, verbose bool, envvars []string) error { 525 cmd := shell.CreateCommand(projectDir, "npm", "run", buildTarget) 526 cmd.Env = append(os.Environ(), envvars...) 527 var stdo, stde bytes.Buffer 528 cmd.Stdout = &stdo 529 cmd.Stderr = &stde 530 err := cmd.Run() 531 if verbose || err != nil { 532 for _, l := range strings.Split(stdo.String(), "\n") { 533 pterm.Printf(" %s\n", l) 534 } 535 for _, l := range strings.Split(stde.String(), "\n") { 536 pterm.Printf(" %s\n", l) 537 } 538 } 539 return err 540 } 541 542 // BuildFrontend executes the `npm build` command for the frontend directory 543 func (b *BaseBuilder) BuildFrontend(outputLogger *clilogger.CLILogger) error { 544 verbose := b.options.Verbosity == VERBOSE 545 546 frontendDir := b.projectData.GetFrontendDir() 547 if !fs.DirExists(frontendDir) { 548 return fmt.Errorf("frontend directory '%s' does not exist", frontendDir) 549 } 550 551 // Check there is an 'InstallCommand' provided in wails.json 552 installCommand := b.projectData.InstallCommand 553 if b.projectData.OutputType == "dev" { 554 installCommand = b.projectData.GetDevInstallerCommand() 555 } 556 if installCommand == "" { 557 // No - don't install 558 printBulletPoint("No Install command. Skipping.") 559 pterm.Println("") 560 } else { 561 // Do install if needed 562 printBulletPoint("Installing frontend dependencies: ") 563 if verbose { 564 pterm.Println("") 565 pterm.Info.Println("Install command: '" + installCommand + "'") 566 } 567 if err := b.NpmInstallUsingCommand(frontendDir, installCommand, verbose); err != nil { 568 return err 569 } 570 outputLogger.Println("Done.") 571 } 572 573 // Check if there is a build command 574 buildCommand := b.projectData.BuildCommand 575 if b.projectData.OutputType == "dev" { 576 buildCommand = b.projectData.GetDevBuildCommand() 577 } 578 if buildCommand == "" { 579 printBulletPoint("No Build command. Skipping.") 580 pterm.Println("") 581 // No - ignore 582 return nil 583 } 584 585 printBulletPoint("Compiling frontend: ") 586 cmd := strings.Split(buildCommand, " ") 587 if verbose { 588 pterm.Println("") 589 pterm.Info.Println("Build command: '" + buildCommand + "'") 590 } 591 stdout, stderr, err := shell.RunCommand(frontendDir, cmd[0], cmd[1:]...) 592 if verbose || err != nil { 593 for _, l := range strings.Split(stdout, "\n") { 594 pterm.Printf(" %s\n", l) 595 } 596 for _, l := range strings.Split(stderr, "\n") { 597 pterm.Printf(" %s\n", l) 598 } 599 } 600 if err != nil { 601 return err 602 } 603 604 pterm.Println("Done.") 605 return nil 606 }