github.com/AlpineAIO/wails/v2@v2.0.0-beta.32.0.20240505041856-1047a8fa5fef/pkg/commands/build/build.go (about) 1 package build 2 3 import ( 4 "fmt" 5 "os" 6 "path/filepath" 7 "runtime" 8 "strings" 9 10 "github.com/google/shlex" 11 "github.com/pterm/pterm" 12 "github.com/samber/lo" 13 14 "github.com/AlpineAIO/wails/v2/internal/staticanalysis" 15 "github.com/AlpineAIO/wails/v2/pkg/commands/bindings" 16 17 "github.com/AlpineAIO/wails/v2/internal/fs" 18 19 "github.com/AlpineAIO/wails/v2/internal/shell" 20 21 "github.com/AlpineAIO/wails/v2/internal/project" 22 "github.com/AlpineAIO/wails/v2/pkg/clilogger" 23 ) 24 25 // Mode is the type used to indicate the build modes 26 type Mode int 27 28 const ( 29 // Dev mode 30 Dev Mode = iota 31 // Production mode 32 Production 33 // Debug build 34 Debug 35 ) 36 37 // Options contains all the build options as well as the project data 38 type Options struct { 39 LDFlags string // Optional flags to pass to linker 40 UserTags []string // Tags to pass to the Go compiler 41 Logger *clilogger.CLILogger // All output to the logger 42 OutputType string // EG: desktop, server.... 43 Mode Mode // release or dev 44 Devtools bool // Enable devtools in production 45 ProjectData *project.Project // The project data 46 Pack bool // Create a package for the app after building 47 Platform string // The platform to build for 48 Arch string // The architecture to build for 49 Compiler string // The compiler command to use 50 SkipModTidy bool // Skip mod tidy before compile 51 IgnoreFrontend bool // Indicates if the frontend does not need building 52 IgnoreApplication bool // Indicates if the application does not need building 53 OutputFile string // Override the output filename 54 BinDirectory string // Directory to use to write the built applications 55 CleanBinDirectory bool // Indicates if the bin output directory should be cleaned before building 56 CompiledBinary string // Fully qualified path to the compiled binary 57 KeepAssets bool // Keep the generated assets/files 58 Verbosity int // Verbosity level (0 - silent, 1 - default, 2 - verbose) 59 Compress bool // Compress the final binary 60 CompressFlags string // Flags to pass to UPX 61 WebView2Strategy string // WebView2 installer strategy 62 RunDelve bool // Indicates if we should run delve after the build 63 WailsJSDir string // Directory to generate the wailsjs module 64 ForceBuild bool // Force 65 BundleName string // Bundlename for Mac 66 TrimPath bool // Use Go's trimpath compiler flag 67 RaceDetector bool // Build with Go's race detector 68 WindowsConsole bool // Indicates that the windows console should be kept 69 Obfuscated bool // Indicates that bound methods should be obfuscated 70 GarbleArgs string // The arguments for Garble 71 SkipBindings bool // Skip binding generation 72 } 73 74 func (o *Options) IsWindowsTargetPlatform() bool { 75 return strings.Contains(strings.ToLower(o.Platform), "windows") 76 } 77 78 // Build the project! 79 func Build(options *Options) (string, error) { 80 // Extract logger 81 outputLogger := options.Logger 82 83 // Get working directory 84 cwd, err := os.Getwd() 85 if err != nil { 86 return "", err 87 } 88 89 // wails js dir 90 options.WailsJSDir = options.ProjectData.GetWailsJSDir() 91 92 // Set build directory 93 options.BinDirectory = filepath.Join(options.ProjectData.GetBuildDir(), "bin") 94 95 // Save the project type 96 options.ProjectData.OutputType = options.OutputType 97 98 // Create builder 99 var builder Builder 100 101 switch options.OutputType { 102 case "desktop": 103 builder = newDesktopBuilder(options) 104 case "dev": 105 builder = newDesktopBuilder(options) 106 default: 107 return "", fmt.Errorf("cannot build assets for output type %s", options.ProjectData.OutputType) 108 } 109 110 // Set up our clean up method 111 defer builder.CleanUp() 112 113 // Initialise Builder 114 builder.SetProjectData(options.ProjectData) 115 116 hookArgs := map[string]string{ 117 "${platform}": options.Platform + "/" + options.Arch, 118 } 119 120 for _, hook := range []string{options.Platform + "/" + options.Arch, options.Platform + "/*", "*/*"} { 121 if err := execPreBuildHook(outputLogger, options, hook, hookArgs); err != nil { 122 return "", err 123 } 124 } 125 126 // Create embed directories if they don't exist 127 if err := CreateEmbedDirectories(cwd, options); err != nil { 128 return "", err 129 } 130 131 // Generate bindings 132 if !options.SkipBindings { 133 err = GenerateBindings(options) 134 if err != nil { 135 return "", err 136 } 137 } 138 139 if !options.IgnoreFrontend { 140 err = builder.BuildFrontend(outputLogger) 141 if err != nil { 142 return "", err 143 } 144 } 145 146 compileBinary := "" 147 if !options.IgnoreApplication { 148 compileBinary, err = execBuildApplication(builder, options) 149 if err != nil { 150 return "", err 151 } 152 153 hookArgs["${bin}"] = compileBinary 154 for _, hook := range []string{options.Platform + "/" + options.Arch, options.Platform + "/*", "*/*"} { 155 if err := execPostBuildHook(outputLogger, options, hook, hookArgs); err != nil { 156 return "", err 157 } 158 } 159 160 } 161 return compileBinary, nil 162 } 163 164 func CreateEmbedDirectories(cwd string, buildOptions *Options) error { 165 path := cwd 166 if buildOptions.ProjectData != nil { 167 path = buildOptions.ProjectData.Path 168 } 169 embedDetails, err := staticanalysis.GetEmbedDetails(path) 170 if err != nil { 171 return err 172 } 173 174 for _, embedDetail := range embedDetails { 175 fullPath := embedDetail.GetFullPath() 176 if _, err := os.Stat(fullPath); os.IsNotExist(err) { 177 err := os.MkdirAll(fullPath, 0o755) 178 if err != nil { 179 return err 180 } 181 f, err := os.Create(filepath.Join(fullPath, "gitkeep")) 182 if err != nil { 183 return err 184 } 185 _ = f.Close() 186 } 187 } 188 189 return nil 190 } 191 192 func fatal(message string) { 193 printer := pterm.PrefixPrinter{ 194 MessageStyle: &pterm.ThemeDefault.FatalMessageStyle, 195 Prefix: pterm.Prefix{ 196 Style: &pterm.ThemeDefault.FatalPrefixStyle, 197 Text: " FATAL ", 198 }, 199 } 200 printer.Println(message) 201 os.Exit(1) 202 } 203 204 func printBulletPoint(text string, args ...any) { 205 item := pterm.BulletListItem{ 206 Level: 2, 207 Text: text, 208 } 209 t, err := pterm.DefaultBulletList.WithItems([]pterm.BulletListItem{item}).Srender() 210 if err != nil { 211 fatal(err.Error()) 212 } 213 t = strings.Trim(t, "\n\r") 214 pterm.Printf(t, args...) 215 } 216 217 func GenerateBindings(buildOptions *Options) error { 218 obfuscated := buildOptions.Obfuscated 219 if obfuscated { 220 printBulletPoint("Generating obfuscated bindings: ") 221 buildOptions.UserTags = append(buildOptions.UserTags, "obfuscated") 222 } else { 223 printBulletPoint("Generating bindings: ") 224 } 225 226 if buildOptions.ProjectData.Bindings.TsGeneration.OutputType == "" { 227 buildOptions.ProjectData.Bindings.TsGeneration.OutputType = "classes" 228 } 229 230 // Generate Bindings 231 output, err := bindings.GenerateBindings(bindings.Options{ 232 Compiler: buildOptions.Compiler, 233 Tags: buildOptions.UserTags, 234 GoModTidy: !buildOptions.SkipModTidy, 235 TsPrefix: buildOptions.ProjectData.Bindings.TsGeneration.Prefix, 236 TsSuffix: buildOptions.ProjectData.Bindings.TsGeneration.Suffix, 237 TsOutputType: buildOptions.ProjectData.Bindings.TsGeneration.OutputType, 238 }) 239 if err != nil { 240 return err 241 } 242 243 if buildOptions.Verbosity == VERBOSE { 244 pterm.Info.Println(output) 245 } 246 247 pterm.Println("Done.") 248 249 return nil 250 } 251 252 func execBuildApplication(builder Builder, options *Options) (string, error) { 253 // If we are building for windows, we will need to generate the asset bundle before 254 // compilation. This will be a .syso file in the project root 255 if options.Pack && options.Platform == "windows" { 256 printBulletPoint("Generating application assets: ") 257 err := packageApplicationForWindows(options) 258 if err != nil { 259 return "", err 260 } 261 pterm.Println("Done.") 262 263 // When we finish, we will want to remove the syso file 264 defer func() { 265 err := os.Remove(filepath.Join(options.ProjectData.Path, options.ProjectData.Name+"-res.syso")) 266 if err != nil { 267 fatal(err.Error()) 268 } 269 }() 270 } 271 272 // Compile the application 273 printBulletPoint("Compiling application: ") 274 275 if options.Platform == "darwin" && options.Arch == "universal" { 276 outputFile := builder.OutputFilename(options) 277 amd64Filename := outputFile + "-amd64" 278 arm64Filename := outputFile + "-arm64" 279 280 // Build amd64 first 281 options.Arch = "amd64" 282 options.OutputFile = amd64Filename 283 options.CleanBinDirectory = false 284 if options.Verbosity == VERBOSE { 285 pterm.Println("Building AMD64 Target: " + filepath.Join(options.BinDirectory, options.OutputFile)) 286 } 287 err := builder.CompileProject(options) 288 if err != nil { 289 return "", err 290 } 291 // Build arm64 292 options.Arch = "arm64" 293 options.OutputFile = arm64Filename 294 options.CleanBinDirectory = false 295 if options.Verbosity == VERBOSE { 296 pterm.Println("Building ARM64 Target: " + filepath.Join(options.BinDirectory, options.OutputFile)) 297 } 298 err = builder.CompileProject(options) 299 300 if err != nil { 301 return "", err 302 } 303 // Run lipo 304 if options.Verbosity == VERBOSE { 305 pterm.Println(fmt.Sprintf("Running lipo: lipo -create -output %s %s %s", outputFile, amd64Filename, arm64Filename)) 306 } 307 _, stderr, err := shell.RunCommand(options.BinDirectory, "lipo", "-create", "-output", outputFile, amd64Filename, arm64Filename) 308 if err != nil { 309 return "", fmt.Errorf("%s - %s", err.Error(), stderr) 310 } 311 // Remove temp binaries 312 err = fs.DeleteFile(filepath.Join(options.BinDirectory, amd64Filename)) 313 if err != nil { 314 return "", err 315 } 316 err = fs.DeleteFile(filepath.Join(options.BinDirectory, arm64Filename)) 317 if err != nil { 318 return "", err 319 } 320 options.ProjectData.OutputFilename = outputFile 321 options.CompiledBinary = filepath.Join(options.BinDirectory, outputFile) 322 } else { 323 err := builder.CompileProject(options) 324 if err != nil { 325 return "", err 326 } 327 } 328 329 if runtime.GOOS == "darwin" { 330 // Remove quarantine attribute 331 if _, err := os.Stat(options.CompiledBinary); os.IsNotExist(err) { 332 return "", fmt.Errorf("compiled binary does not exist at path: %s", options.CompiledBinary) 333 } 334 stdout, stderr, err := shell.RunCommand(options.BinDirectory, "xattr", "-rc", options.CompiledBinary) 335 if err != nil { 336 return "", fmt.Errorf("%s - %s", err.Error(), stderr) 337 } 338 if options.Verbosity == VERBOSE && stdout != "" { 339 pterm.Info.Println(stdout) 340 } 341 } 342 343 pterm.Println("Done.") 344 345 // Do we need to pack the app for non-windows? 346 if options.Pack && options.Platform != "windows" { 347 348 printBulletPoint("Packaging application: ") 349 350 // TODO: Allow cross platform build 351 err := packageProject(options, runtime.GOOS) 352 if err != nil { 353 return "", err 354 } 355 pterm.Println("Done.") 356 } 357 358 if options.Platform == "windows" { 359 const nativeWebView2Loader = "native_webview2loader" 360 361 tags := options.UserTags 362 if lo.Contains(tags, nativeWebView2Loader) { 363 message := "You are using the legacy native WebView2Loader. This loader will be deprecated in the near future. Please report any bugs related to the new loader: https://github.com/AlpineAIO/wails/issues/2004" 364 pterm.Warning.Println(message) 365 } else { 366 tags = append(tags, nativeWebView2Loader) 367 message := fmt.Sprintf("Wails is now using the new Go WebView2Loader. If you encounter any issues with it, please report them to https://github.com/AlpineAIO/wails/issues/2004. You could also use the old legacy loader with `-tags %s`, but keep in mind this will be deprecated in the near future.", strings.Join(tags, ",")) 368 pterm.Info.Println(message) 369 } 370 } 371 372 if options.Platform == "darwin" && (options.Mode == Debug || options.Devtools) { 373 pterm.Warning.Println("This darwin build contains the use of private APIs. This will not pass Apple's AppStore approval process. Please use it only as a test build for testing and debug purposes.") 374 } 375 376 return options.CompiledBinary, nil 377 } 378 379 func execPreBuildHook(outputLogger *clilogger.CLILogger, options *Options, hookIdentifier string, argReplacements map[string]string) error { 380 preBuildHook := options.ProjectData.PreBuildHooks[hookIdentifier] 381 if preBuildHook == "" { 382 return nil 383 } 384 385 return executeBuildHook(outputLogger, options, hookIdentifier, argReplacements, preBuildHook, "pre") 386 } 387 388 func execPostBuildHook(outputLogger *clilogger.CLILogger, options *Options, hookIdentifier string, argReplacements map[string]string) error { 389 postBuildHook := options.ProjectData.PostBuildHooks[hookIdentifier] 390 if postBuildHook == "" { 391 return nil 392 } 393 394 return executeBuildHook(outputLogger, options, hookIdentifier, argReplacements, postBuildHook, "post") 395 } 396 397 func executeBuildHook(_ *clilogger.CLILogger, options *Options, hookIdentifier string, argReplacements map[string]string, buildHook string, hookName string) error { 398 if !options.ProjectData.RunNonNativeBuildHooks { 399 if hookIdentifier == "" { 400 // That's the global hook 401 } else { 402 platformOfHook := strings.Split(hookIdentifier, "/")[0] 403 if platformOfHook == "*" { 404 // That's OK, we don't have a specific platform of the hook 405 } else if platformOfHook == runtime.GOOS { 406 // The hook is for host platform 407 } else { 408 // Skip a hook which is not native 409 printBulletPoint(fmt.Sprintf("Non native build hook '%s': Skipping.", hookIdentifier)) 410 return nil 411 } 412 } 413 } 414 415 printBulletPoint("Executing %s build hook '%s': ", hookName, hookIdentifier) 416 args, err := shlex.Split(buildHook) 417 if err != nil { 418 return fmt.Errorf("could not parse %s build hook command: %w", hookName, err) 419 } 420 for i, arg := range args { 421 newArg := argReplacements[arg] 422 if newArg == "" { 423 continue 424 } 425 args[i] = newArg 426 } 427 428 if options.Verbosity == VERBOSE { 429 pterm.Info.Println(strings.Join(args, " ")) 430 } 431 432 if !fs.DirExists(options.BinDirectory) { 433 if err := fs.MkDirs(options.BinDirectory); err != nil { 434 return fmt.Errorf("could not create target directory: %s", err.Error()) 435 } 436 } 437 438 stdout, stderr, err := shell.RunCommand(options.BinDirectory, args[0], args[1:]...) 439 if options.Verbosity == VERBOSE { 440 pterm.Info.Println(stdout) 441 } 442 if err != nil { 443 return fmt.Errorf("%s - %s", err.Error(), stderr) 444 } 445 pterm.Println("Done.") 446 447 return nil 448 }