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  }