diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 60492d7b8..c07d63ee9 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -29,6 +29,7 @@ jobs: go-version: ${{ matrix.go-version }} - name: Run tests + working-directory: ./v2 run: go test -v ./... test_templates: diff --git a/v2/cmd/wails/build.go b/v2/cmd/wails/build.go new file mode 100644 index 000000000..da9cf7130 --- /dev/null +++ b/v2/cmd/wails/build.go @@ -0,0 +1,254 @@ +package main + +import ( + "fmt" + "github.com/leaanthony/slicer" + "github.com/pterm/pterm" + "github.com/wailsapp/wails/v2/cmd/wails/flags" + "github.com/wailsapp/wails/v2/cmd/wails/internal/gomod" + "github.com/wailsapp/wails/v2/internal/colour" + "github.com/wailsapp/wails/v2/internal/project" + "github.com/wailsapp/wails/v2/pkg/clilogger" + "github.com/wailsapp/wails/v2/pkg/commands/build" + "os" + "runtime" + "strings" + "time" +) + +func buildApplication(f *flags.Build) error { + + if f.NoColour { + pterm.DisableColor() + colour.ColourEnabled = false + } + + quiet := f.Verbosity == flags.Quiet + + // Create logger + logger := clilogger.New(os.Stdout) + logger.Mute(quiet) + + if quiet { + pterm.DisableOutput() + } else { + app.PrintBanner() + } + + err := f.Process() + if err != nil { + return err + } + + cwd, err := os.Getwd() + if err != nil { + return err + } + projectOptions, err := project.Load(cwd) + if err != nil { + return err + } + + // Create BuildOptions + buildOptions := &build.Options{ + Logger: logger, + OutputType: "desktop", + OutputFile: f.OutputFilename, + CleanBinDirectory: f.Clean, + Mode: f.GetBuildMode(), + Pack: !f.NoPackage, + LDFlags: f.LdFlags, + Compiler: f.Compiler, + SkipModTidy: f.SkipModTidy, + Verbosity: f.Verbosity, + ForceBuild: f.ForceBuild, + IgnoreFrontend: f.SkipFrontend, + Compress: f.Upx, + CompressFlags: f.UpxFlags, + UserTags: f.GetTags(), + WebView2Strategy: f.GetWebView2Strategy(), + TrimPath: f.TrimPath, + RaceDetector: f.RaceDetector, + WindowsConsole: f.WindowsConsole, + Obfuscated: f.Obfuscated, + GarbleArgs: f.GarbleArgs, + SkipBindings: f.SkipBindings, + ProjectData: projectOptions, + } + + tableData := pterm.TableData{ + {"Platform(s)", f.Platform}, + {"Compiler", f.GetCompilerPath()}, + {"Skip Bindings", bool2Str(f.SkipBindings)}, + {"Build Mode", f.GetBuildModeAsString()}, + {"Frontend Directory", projectOptions.GetFrontendDir()}, + {"Obfuscated", bool2Str(f.Obfuscated)}, + } + if f.Obfuscated { + tableData = append(tableData, []string{"Garble Args", f.GarbleArgs}) + } + tableData = append(tableData, pterm.TableData{ + {"Skip Frontend", bool2Str(f.SkipFrontend)}, + {"Compress", bool2Str(f.Upx)}, + {"Package", bool2Str(!f.NoPackage)}, + {"Clean Bin Dir", bool2Str(f.Clean)}, + {"LDFlags", f.LdFlags}, + {"Tags", "[" + strings.Join(f.GetTags(), ",") + "]"}, + {"Race Detector", bool2Str(f.RaceDetector)}, + }...) + if len(buildOptions.OutputFile) > 0 && f.GetTargets().Length() == 1 { + tableData = append(tableData, []string{"Output File", f.OutputFilename}) + } + pterm.DefaultSection.Println("Build Options") + + err = pterm.DefaultTable.WithData(tableData).Render() + if err != nil { + return err + } + + err = gomod.SyncGoMod(logger, f.UpdateWailsVersionGoMod) + if err != nil { + return err + } + + // Check platform + validPlatformArch := slicer.String([]string{ + "darwin", + "darwin/amd64", + "darwin/arm64", + "darwin/universal", + "linux", + "linux/amd64", + "linux/arm64", + "linux/arm", + "windows", + "windows/amd64", + "windows/arm64", + "windows/386", + }) + + outputBinaries := map[string]string{} + + // Allows cancelling the build after the first error. It would be nice if targets.Each would support funcs + // returning an error. + var targetErr error + targets := f.GetTargets() + targets.Each(func(platform string) { + if targetErr != nil { + return + } + + if !validPlatformArch.Contains(platform) { + buildOptions.Logger.Println("platform '%s' is not supported - skipping. Supported platforms: %s", platform, validPlatformArch.Join(",")) + return + } + + desiredFilename := projectOptions.OutputFilename + if desiredFilename == "" { + desiredFilename = projectOptions.Name + } + desiredFilename = strings.TrimSuffix(desiredFilename, ".exe") + + // Calculate platform and arch + platformSplit := strings.Split(platform, "/") + buildOptions.Platform = platformSplit[0] + buildOptions.Arch = f.GetDefaultArch() + if len(platformSplit) > 1 { + buildOptions.Arch = platformSplit[1] + } + banner := "Building target: " + buildOptions.Platform + "/" + buildOptions.Arch + pterm.DefaultSection.Println(banner) + + if f.Upx && platform == "darwin/universal" { + pterm.Warning.Println("Warning: compress flag unsupported for universal binaries. Ignoring.") + f.Upx = false + } + + switch buildOptions.Platform { + case "linux": + if runtime.GOOS != "linux" { + pterm.Warning.Println("Crosscompiling to Linux not currently supported.\n") + return + } + case "darwin": + if runtime.GOOS != "darwin" { + pterm.Warning.Println("Crosscompiling to Mac not currently supported.\n") + return + } + macTargets := targets.Filter(func(platform string) bool { + return strings.HasPrefix(platform, "darwin") + }) + if macTargets.Length() == 2 { + buildOptions.BundleName = fmt.Sprintf("%s-%s.app", desiredFilename, buildOptions.Arch) + } + } + + if targets.Length() > 1 { + // target filename + switch buildOptions.Platform { + case "windows": + desiredFilename = fmt.Sprintf("%s-%s", desiredFilename, buildOptions.Arch) + case "linux", "darwin": + desiredFilename = fmt.Sprintf("%s-%s-%s", desiredFilename, buildOptions.Platform, buildOptions.Arch) + } + } + if buildOptions.Platform == "windows" { + desiredFilename += ".exe" + } + buildOptions.OutputFile = desiredFilename + + if f.OutputFilename != "" { + buildOptions.OutputFile = f.OutputFilename + } + + if f.Obfuscated && f.SkipBindings { + pterm.Warning.Println("obfuscated flag overrides skipbindings flag.") + buildOptions.SkipBindings = false + } + + if !f.DryRun { + // Start Time + start := time.Now() + + compiledBinary, err := build.Build(buildOptions) + if err != nil { + pterm.Error.Println(err.Error()) + targetErr = err + return + } + + buildOptions.IgnoreFrontend = true + buildOptions.CleanBinDirectory = false + + // Output stats + buildOptions.Logger.Println(fmt.Sprintf("Built '%s' in %s.\n", compiledBinary, time.Since(start).Round(time.Millisecond).String())) + + outputBinaries[buildOptions.Platform+"/"+buildOptions.Arch] = compiledBinary + } else { + pterm.Info.Println("Dry run: skipped build.") + } + }) + + if targetErr != nil { + return targetErr + } + + if f.DryRun { + return nil + } + + if f.NSIS { + amd64Binary := outputBinaries["windows/amd64"] + arm64Binary := outputBinaries["windows/arm64"] + if amd64Binary == "" && arm64Binary == "" { + return fmt.Errorf("cannot build nsis installer - no windows targets") + } + + if err := build.GenerateNSISInstaller(buildOptions, amd64Binary, arm64Binary); err != nil { + return err + } + } + + return nil + +} diff --git a/v2/cmd/wails/dev.go b/v2/cmd/wails/dev.go new file mode 100644 index 000000000..dbb2cf5d8 --- /dev/null +++ b/v2/cmd/wails/dev.go @@ -0,0 +1,38 @@ +package main + +import ( + "github.com/pterm/pterm" + "github.com/wailsapp/wails/v2/cmd/wails/flags" + "github.com/wailsapp/wails/v2/cmd/wails/internal/dev" + "github.com/wailsapp/wails/v2/internal/colour" + "github.com/wailsapp/wails/v2/pkg/clilogger" + "os" +) + +func devApplication(f *flags.Dev) error { + + if f.NoColour { + pterm.DisableColor() + colour.ColourEnabled = false + } + + quiet := f.Verbosity == flags.Quiet + + // Create logger + logger := clilogger.New(os.Stdout) + logger.Mute(quiet) + + if quiet { + pterm.DisableOutput() + } else { + app.PrintBanner() + } + + err := f.Process() + if err != nil { + return err + } + + return dev.Application(f, logger) + +} diff --git a/v2/cmd/wails/doctor.go b/v2/cmd/wails/doctor.go new file mode 100644 index 000000000..836b963e2 --- /dev/null +++ b/v2/cmd/wails/doctor.go @@ -0,0 +1,165 @@ +package main + +import ( + "github.com/pterm/pterm" + "github.com/wailsapp/wails/v2/cmd/wails/flags" + "github.com/wailsapp/wails/v2/internal/colour" + "github.com/wailsapp/wails/v2/internal/system" + "github.com/wailsapp/wails/v2/internal/system/packagemanager" + "runtime" + "runtime/debug" + "strings" +) + +func diagnoseEnvironment(f *flags.Doctor) error { + + if f.NoColour { + pterm.DisableColor() + colour.ColourEnabled = false + } + + app.PrintBanner() + + pterm.Print("Scanning system - Please wait (this may take a long time)...") + + // Get system info + info, err := system.GetInfo() + if err != nil { + pterm.Println("Failed.") + return err + } + pterm.Println("Done.") + + pterm.DefaultSection.Println("System") + + systemTabledata := [][]string{ + {"OS", info.OS.Name}, + {"Version", info.OS.Version}, + {"ID", info.OS.ID}, + {"Go Version", runtime.Version()}, + {"Platform", runtime.GOOS}, + {"Architecture", runtime.GOARCH}, + } + + err = pterm.DefaultTable.WithData(systemTabledata).Render() + if err != nil { + return err + } + + pterm.DefaultSection.Println("Wails") + + wailsTableData := [][]string{ + {"Version", app.Version()}, + } + + if buildInfo, _ := debug.ReadBuildInfo(); buildInfo != nil { + buildSettingToName := map[string]string{ + "vcs.revision": "Revision", + "vcs.modified": "Modified", + } + for _, buildSetting := range buildInfo.Settings { + name := buildSettingToName[buildSetting.Key] + if name == "" { + continue + } + wailsTableData = append(wailsTableData, []string{name, buildSetting.Value}) + } + } + + // Exit early if PM not found + if info.PM != nil { + wailsTableData = append(wailsTableData, []string{"Package Manager", info.PM.Name()}) + } + + err = pterm.DefaultTable.WithData(wailsTableData).Render() + if err != nil { + return err + } + + pterm.DefaultSection.Println("Dependencies") + + // Output Dependencies Status + var dependenciesMissing = []string{} + var externalPackages = []*packagemanager.Dependency{} + var dependenciesAvailableRequired = 0 + var dependenciesAvailableOptional = 0 + + dependenciesTableData := [][]string{ + {"Dependency", "Package Name", "Status", "Version"}, + } + + hasOptionalDependencies := false + // Loop over dependencies + for _, dependency := range info.Dependencies { + + name := dependency.Name + if dependency.Optional { + name = "*" + name + hasOptionalDependencies = true + } + packageName := "Unknown" + status := "Not Found" + + // If we found the package + if dependency.PackageName != "" { + + packageName = dependency.PackageName + + // If it's installed, update the status + if dependency.Installed { + status = "Installed" + } else { + // Generate meaningful status text + status = "Available" + + if dependency.Optional { + dependenciesAvailableOptional++ + } else { + dependenciesAvailableRequired++ + } + } + } else { + if !dependency.Optional { + dependenciesMissing = append(dependenciesMissing, dependency.Name) + } + + if dependency.External { + externalPackages = append(externalPackages, dependency) + } + } + + dependenciesTableData = append(dependenciesTableData, []string{name, packageName, status, dependency.Version}) + } + + err = pterm.DefaultTable.WithHasHeader(true).WithData(dependenciesTableData).Render() + + if hasOptionalDependencies { + pterm.Println("* - Optional Dependency") + } + + pterm.DefaultSection.Println("Diagnosis") + + // Generate an appropriate diagnosis + + if len(dependenciesMissing) == 0 && dependenciesAvailableRequired == 0 { + pterm.Println("Your system is ready for Wails development!") + } else { + pterm.Println("Your system has missing dependencies!\n") + } + + if dependenciesAvailableRequired != 0 { + pterm.Println("Required package(s) installation details: \n" + info.Dependencies.InstallAllRequiredCommand()) + } + + if dependenciesAvailableOptional != 0 { + pterm.Println("Optional package(s) installation details: \n" + info.Dependencies.InstallAllOptionalCommand()) + } + + if len(dependenciesMissing) != 0 { + pterm.Println("Fatal:") + pterm.Println("Required dependencies missing: " + strings.Join(dependenciesMissing, " ")) + pterm.Println("Please read this article on how to resolve this: https://wails.io/guides/resolving-missing-packages") + } + + return nil +} diff --git a/v2/cmd/wails/flags/build.go b/v2/cmd/wails/flags/build.go new file mode 100644 index 000000000..618a2d4e8 --- /dev/null +++ b/v2/cmd/wails/flags/build.go @@ -0,0 +1,166 @@ +package flags + +import ( + "fmt" + "github.com/leaanthony/slicer" + "github.com/wailsapp/wails/v2/internal/system" + "github.com/wailsapp/wails/v2/pkg/commands/build" + "github.com/wailsapp/wails/v2/pkg/commands/buildtags" + "os" + "os/exec" + "runtime" + "strings" +) + +const ( + Quiet int = 0 + Normal int = 1 + Verbose int = 2 +) + +// TODO: unify this and `build.Options` +type Build struct { + Common + BuildCommon + + NoPackage bool `name:"noPackage" description:"Skips platform specific packaging"` + SkipModTidy bool `name:"m" description:"Skip mod tidy before compile"` + Upx bool `description:"Compress final binary with UPX (if installed)"` + UpxFlags string `description:"Flags to pass to upx"` + Platform string `description:"Platform to target. Comma separate multiple platforms"` + OutputFilename string `name:"o" description:"Output filename"` + Clean bool `description:"Clean the bin directory before building"` + WebView2 string `description:"WebView2 installer strategy: download,embed,browser,error"` + ForceBuild bool `name:"f" description:"Force build of application"` + UpdateWailsVersionGoMod bool `name:"u" description:"Updates go.mod to use the same Wails version as the CLI"` + Debug bool `description:"Builds the application in debug mode"` + NSIS bool `description:"Generate NSIS installer for Windows"` + TrimPath bool `description:"Remove all file system paths from the resulting executable"` + WindowsConsole bool `description:"Keep the console when building for Windows"` + Obfuscated bool `description:"Code obfuscation of bound Wails methods"` + GarbleArgs string `description:"Arguments to pass to garble"` + DryRun bool `description:"Prints the build command without executing it"` + + // Build Specific + + // Internal state + compilerPath string + userTags []string + wv2rtstrategy string // WebView2 runtime strategy + defaultArch string // Default architecture +} + +func (b *Build) Default() *Build { + + defaultPlatform := os.Getenv("GOOS") + if defaultPlatform == "" { + defaultPlatform = runtime.GOOS + } + defaultArch := os.Getenv("GOARCH") + if defaultArch == "" { + if system.IsAppleSilicon { + defaultArch = "arm64" + } else { + defaultArch = runtime.GOARCH + } + } + b.defaultArch = defaultArch + platform := defaultPlatform + "/" + defaultArch + + result := &Build{ + Platform: platform, + WebView2: "download", + GarbleArgs: "-literals -tiny -seed=random", + } + result.BuildCommon = result.BuildCommon.Default() + return result +} + +func (b *Build) GetBuildMode() build.Mode { + if b.Debug { + return build.Debug + } + return build.Production +} + +func (b *Build) GetWebView2Strategy() string { + return b.wv2rtstrategy +} + +func (b *Build) GetTargets() *slicer.StringSlicer { + var targets slicer.StringSlicer + targets.AddSlice(strings.Split(b.Platform, ",")) + targets.Deduplicate() + return &targets +} + +func (b *Build) GetCompilerPath() string { + return b.compilerPath +} + +func (b *Build) GetTags() []string { + return b.userTags +} + +func (b *Build) Process() error { + // Lookup compiler path + var err error + b.compilerPath, err = exec.LookPath(b.Compiler) + if err != nil { + return fmt.Errorf("unable to find compiler: %s", b.Compiler) + } + + // Process User Tags + b.userTags, err = buildtags.Parse(b.Tags) + if err != nil { + return err + } + + // WebView2 installer strategy (download by default) + b.WebView2 = strings.ToLower(b.WebView2) + if b.WebView2 != "" { + validWV2Runtime := slicer.String([]string{"download", "embed", "browser", "error"}) + if !validWV2Runtime.Contains(b.WebView2) { + return fmt.Errorf("invalid option for flag 'webview2': %s", b.WebView2) + } + b.wv2rtstrategy = "wv2runtime." + b.WebView2 + } + + return nil +} + +func bool2Str(b bool) string { + if b { + return "true" + } + return "false" +} + +func (b *Build) GetBuildModeAsString() string { + if b.Debug { + return "debug" + } + return "production" +} + +func (b *Build) GetDefaultArch() string { + return b.defaultArch +} + +/* + _, _ = fmt.Fprintf(w, "Frontend Directory: \t%s\n", projectOptions.GetFrontendDir()) + _, _ = fmt.Fprintf(w, "Obfuscated: \t%t\n", buildOptions.Obfuscated) + if buildOptions.Obfuscated { + _, _ = fmt.Fprintf(w, "Garble Args: \t%s\n", buildOptions.GarbleArgs) + } + _, _ = fmt.Fprintf(w, "Skip Frontend: \t%t\n", skipFrontend) + _, _ = fmt.Fprintf(w, "Compress: \t%t\n", buildOptions.Compress) + _, _ = fmt.Fprintf(w, "Package: \t%t\n", buildOptions.Pack) + _, _ = fmt.Fprintf(w, "Clean Bin Dir: \t%t\n", buildOptions.CleanBinDirectory) + _, _ = fmt.Fprintf(w, "LDFlags: \t\"%s\"\n", buildOptions.LDFlags) + _, _ = fmt.Fprintf(w, "Tags: \t[%s]\n", strings.Join(buildOptions.UserTags, ",")) + _, _ = fmt.Fprintf(w, "Race Detector: \t%t\n", buildOptions.RaceDetector) + if len(buildOptions.OutputFile) > 0 && targets.Length() == 1 { + _, _ = fmt.Fprintf(w, "Output File: \t%s\n", buildOptions.OutputFile) + } +*/ diff --git a/v2/cmd/wails/flags/buildcommon.go b/v2/cmd/wails/flags/buildcommon.go new file mode 100644 index 000000000..b4014c6c1 --- /dev/null +++ b/v2/cmd/wails/flags/buildcommon.go @@ -0,0 +1,18 @@ +package flags + +type BuildCommon struct { + LdFlags string `description:"Additional ldflags to pass to the compiler"` + Compiler string `description:"Use a different go compiler to build, eg go1.15beta1"` + SkipBindings bool `description:"Skips generation of bindings"` + RaceDetector bool `name:"race" description:"Build with Go's race detector"` + SkipFrontend bool `name:"s" description:"Skips building the frontend"` + Verbosity int `name:"v" description:"Verbosity level (0 = quiet, 1 = normal, 2 = verbose)"` + Tags string `description:"Build tags to pass to Go compiler. Must be quoted. Space or comma (but not both) separated"` +} + +func (c BuildCommon) Default() BuildCommon { + return BuildCommon{ + Compiler: "go", + Verbosity: 1, + } +} diff --git a/v2/cmd/wails/flags/common.go b/v2/cmd/wails/flags/common.go new file mode 100644 index 000000000..e58eff411 --- /dev/null +++ b/v2/cmd/wails/flags/common.go @@ -0,0 +1,5 @@ +package flags + +type Common struct { + NoColour bool `description:"Disable colour in output"` +} diff --git a/v2/cmd/wails/flags/dev.go b/v2/cmd/wails/flags/dev.go new file mode 100644 index 000000000..d6e5d361e --- /dev/null +++ b/v2/cmd/wails/flags/dev.go @@ -0,0 +1,144 @@ +package flags + +import ( + "fmt" + "github.com/samber/lo" + "github.com/wailsapp/wails/v2/internal/project" + "github.com/wailsapp/wails/v2/pkg/commands/build" + "net" + "net/url" + "os" + "path/filepath" + "runtime" +) + +type Dev struct { + BuildCommon + + AssetDir string `flag:"assetdir" description:"Serve assets from the given directory instead of using the provided asset FS"` + Extensions string `flag:"e" description:"Extensions to trigger rebuilds (comma separated) eg go"` + ReloadDirs string `flag:"reloaddirs" description:"Additional directories to trigger reloads (comma separated)"` + Browser bool `flag:"browser" description:"Open the application in a browser"` + NoReload bool `flag:"noreload" description:"Disable reload on asset change"` + NoColour bool `flag:"nocolor" description:"Disable colour in output"` + WailsJSDir string `flag:"wailsjsdir" description:"Directory to generate the Wails JS modules"` + LogLevel string `flag:"loglevel" description:"LogLevel to use - Trace, Debug, Info, Warning, Error)"` + ForceBuild bool `flag:"f" description:"Force build of application"` + Debounce int `flag:"debounce" description:"The amount of time to wait to trigger a reload on change"` + DevServer string `flag:"devserver" description:"The address of the wails dev server"` + AppArgs string `flag:"appargs" description:"arguments to pass to the underlying app (quoted and space separated)"` + Save bool `flag:"save" description:"Save the given flags as defaults"` + FrontendDevServerURL string `flag:"frontenddevserverurl" description:"The url of the external frontend dev server to use"` + + // Internal state + devServerURL *url.URL + projectConfig *project.Project +} + +func (*Dev) Default() *Dev { + result := &Dev{ + Extensions: "go", + Debounce: 100, + } + result.BuildCommon = result.BuildCommon.Default() + return result +} + +func (d *Dev) Process() error { + + var err error + err = d.loadAndMergeProjectConfig() + if err != nil { + return err + } + + if _, _, err := net.SplitHostPort(d.DevServer); err != nil { + return fmt.Errorf("DevServer is not of the form 'host:port', please check your wails.json") + } + + d.devServerURL, err = url.Parse("http://" + d.DevServer) + if err != nil { + return err + } + + return nil +} + +func (d *Dev) loadAndMergeProjectConfig() error { + var err error + cwd, err := os.Getwd() + if err != nil { + return err + } + d.projectConfig, err = project.Load(cwd) + if err != nil { + return err + } + + d.AssetDir, _ = lo.Coalesce(d.AssetDir, d.projectConfig.AssetDirectory) + d.projectConfig.AssetDirectory = filepath.ToSlash(d.AssetDir) + if d.AssetDir != "" { + d.AssetDir, err = filepath.Abs(d.AssetDir) + if err != nil { + return err + } + } + + d.ReloadDirs, _ = lo.Coalesce(d.ReloadDirs, d.projectConfig.ReloadDirectories) + d.projectConfig.ReloadDirectories = filepath.ToSlash(d.ReloadDirs) + d.DevServer, _ = lo.Coalesce(d.DevServer, d.projectConfig.DevServer) + d.projectConfig.DevServer = d.DevServer + d.FrontendDevServerURL, _ = lo.Coalesce(d.FrontendDevServerURL, d.projectConfig.FrontendDevServerURL) + d.projectConfig.FrontendDevServerURL = d.FrontendDevServerURL + d.WailsJSDir, _ = lo.Coalesce(d.WailsJSDir, d.projectConfig.GetWailsJSDir(), d.projectConfig.GetFrontendDir()) + d.projectConfig.WailsJSDir = filepath.ToSlash(d.WailsJSDir) + + if d.Debounce == 100 && d.projectConfig.DebounceMS != 100 { + if d.projectConfig.DebounceMS == 0 { + d.projectConfig.DebounceMS = 100 + } + d.Debounce = d.projectConfig.DebounceMS + } + d.projectConfig.DebounceMS = d.Debounce + + d.AppArgs, _ = lo.Coalesce(d.AppArgs, d.projectConfig.AppArgs) + + if d.Save { + err = d.projectConfig.Save() + if err != nil { + return err + } + } + + return nil + +} + +// GenerateBuildOptions creates a build.Options using the flags +func (d *Dev) GenerateBuildOptions() *build.Options { + result := &build.Options{ + OutputType: "dev", + Mode: build.Dev, + Arch: runtime.GOARCH, + Pack: true, + Platform: runtime.GOOS, + LDFlags: d.LdFlags, + Compiler: d.Compiler, + ForceBuild: d.ForceBuild, + IgnoreFrontend: d.SkipFrontend, + Verbosity: d.Verbosity, + WailsJSDir: d.WailsJSDir, + RaceDetector: d.RaceDetector, + ProjectData: d.projectConfig, + } + + return result +} + +func (d *Dev) ProjectConfig() *project.Project { + return d.projectConfig +} + +func (d *Dev) DevServerURL() *url.URL { + return d.devServerURL +} diff --git a/v2/cmd/wails/flags/doctor.go b/v2/cmd/wails/flags/doctor.go new file mode 100644 index 000000000..e4816b969 --- /dev/null +++ b/v2/cmd/wails/flags/doctor.go @@ -0,0 +1,9 @@ +package flags + +type Doctor struct { + Common +} + +func (b *Doctor) Default() *Doctor { + return &Doctor{} +} diff --git a/v2/cmd/wails/flags/generate.go b/v2/cmd/wails/flags/generate.go new file mode 100644 index 000000000..9adf78aa4 --- /dev/null +++ b/v2/cmd/wails/flags/generate.go @@ -0,0 +1,14 @@ +package flags + +type GenerateModule struct { + Common + Tags string `description:"Build tags to pass to Go compiler. Must be quoted. Space or comma (but not both) separated"` + Verbosity int `name:"v" description:"Verbosity level (0 = quiet, 1 = normal, 2 = verbose)"` +} + +type GenerateTemplate struct { + Common + Name string `description:"Name of the template to generate"` + Frontend string `description:"Frontend to use for the template"` + Quiet bool `description:"Suppress output"` +} diff --git a/v2/cmd/wails/flags/init.go b/v2/cmd/wails/flags/init.go new file mode 100644 index 000000000..6e642ec9a --- /dev/null +++ b/v2/cmd/wails/flags/init.go @@ -0,0 +1,22 @@ +package flags + +type Init struct { + Common + + TemplateName string `name:"t" description:"Name of built-in template to use, path to template or template url"` + ProjectName string `name:"n" description:"Name of project"` + CIMode bool `name:"ci" description:"CI Mode"` + ProjectDir string `name:"d" description:"Project directory"` + Quiet bool `name:"q" description:"Suppress output to console"` + InitGit bool `name:"g" description:"Initialise git repository"` + IDE string `name:"ide" description:"Generate IDE project files"` + List bool `name:"l" description:"List templates"` +} + +func (i *Init) Default() *Init { + + result := &Init{ + TemplateName: "vanilla", + } + return result +} diff --git a/v2/cmd/wails/flags/show.go b/v2/cmd/wails/flags/show.go new file mode 100644 index 000000000..a8220f3cc --- /dev/null +++ b/v2/cmd/wails/flags/show.go @@ -0,0 +1,6 @@ +package flags + +type ShowReleaseNotes struct { + Common + Version string `description:"The version to show the release notes for"` +} diff --git a/v2/cmd/wails/flags/update.go b/v2/cmd/wails/flags/update.go new file mode 100644 index 000000000..ffd143a9f --- /dev/null +++ b/v2/cmd/wails/flags/update.go @@ -0,0 +1,7 @@ +package flags + +type Update struct { + Common + Version string `description:"The version to update to"` + PreRelease bool `name:"pre" description:"Update to latest pre-release"` +} diff --git a/v2/cmd/wails/generate.go b/v2/cmd/wails/generate.go new file mode 100644 index 000000000..a7b059ecf --- /dev/null +++ b/v2/cmd/wails/generate.go @@ -0,0 +1,245 @@ +package main + +import ( + "fmt" + "github.com/leaanthony/debme" + "github.com/leaanthony/gosod" + "github.com/pterm/pterm" + "github.com/tidwall/sjson" + "github.com/wailsapp/wails/v2/cmd/wails/flags" + "github.com/wailsapp/wails/v2/cmd/wails/internal/template" + "github.com/wailsapp/wails/v2/internal/colour" + "github.com/wailsapp/wails/v2/internal/fs" + "github.com/wailsapp/wails/v2/internal/project" + "github.com/wailsapp/wails/v2/pkg/clilogger" + "github.com/wailsapp/wails/v2/pkg/commands/bindings" + "github.com/wailsapp/wails/v2/pkg/commands/buildtags" + "os" + "path/filepath" +) + +func generateModule(f *flags.GenerateModule) error { + + if f.NoColour { + pterm.DisableColor() + colour.ColourEnabled = false + } + + quiet := f.Verbosity == flags.Quiet + logger := clilogger.New(os.Stdout) + logger.Mute(quiet) + + buildTags, err := buildtags.Parse(f.Tags) + if err != nil { + return err + } + + cwd, err := os.Getwd() + if err != nil { + return err + } + projectConfig, err := project.Load(cwd) + if err != nil { + return err + } + + _, err = bindings.GenerateBindings(bindings.Options{ + Tags: buildTags, + TsPrefix: projectConfig.Bindings.TsGeneration.Prefix, + TsSuffix: projectConfig.Bindings.TsGeneration.Suffix, + }) + if err != nil { + return err + } + return nil +} + +func generateTemplate(f *flags.GenerateTemplate) error { + + if f.NoColour { + pterm.DisableColor() + colour.ColourEnabled = false + } + + quiet := f.Quiet + logger := clilogger.New(os.Stdout) + logger.Mute(quiet) + + // name is mandatory + if f.Name == "" { + return fmt.Errorf("please provide a template name using the -name flag") + } + + // If the current directory is not empty, we create a new directory + cwd, err := os.Getwd() + if err != nil { + return err + } + templateDir := filepath.Join(cwd, f.Name) + if !fs.DirExists(templateDir) { + err := os.MkdirAll(templateDir, 0755) + if err != nil { + return err + } + } + empty, err := fs.DirIsEmpty(templateDir) + if err != nil { + return err + } + + pterm.DefaultSection.Println("Generating template") + + if !empty { + templateDir = filepath.Join(cwd, f.Name) + printBulletPoint("Creating new template directory:", f.Name) + err = fs.Mkdir(templateDir) + if err != nil { + return err + } + } + + // Create base template + baseTemplate, err := debme.FS(template.Base, "base") + if err != nil { + return err + } + g := gosod.New(baseTemplate) + g.SetTemplateFilters([]string{".template"}) + + err = os.Chdir(templateDir) + if err != nil { + return err + } + + type templateData struct { + Name string + Description string + TemplateDir string + WailsVersion string + } + + printBulletPoint("Extracting base template files...") + + err = g.Extract(templateDir, &templateData{ + Name: f.Name, + TemplateDir: templateDir, + WailsVersion: app.Version(), + }) + if err != nil { + return err + } + + err = os.Chdir(cwd) + if err != nil { + return err + } + + // If we aren't migrating the files, just exit + if f.Frontend == "" { + pterm.Println() + pterm.Println() + pterm.Info.Println("No frontend specified to migrate. Template created.") + pterm.Println() + return nil + } + + // Remove frontend directory + frontendDir := filepath.Join(templateDir, "frontend") + err = os.RemoveAll(frontendDir) + if err != nil { + return err + } + + // Copy the files into a new frontend directory + printBulletPoint("Migrating existing project files to frontend directory...") + + sourceDir, err := filepath.Abs(f.Frontend) + if err != nil { + return err + } + + newFrontendDir := filepath.Join(templateDir, "frontend") + err = fs.CopyDirExtended(sourceDir, newFrontendDir, []string{f.Name, "node_modules"}) + if err != nil { + return err + } + + // Process package.json + err = processPackageJSON(frontendDir) + if err != nil { + return err + } + + // Process package-lock.json + err = processPackageLockJSON(frontendDir) + if err != nil { + return err + } + + // Remove node_modules - ignore error, eg it doesn't exist + _ = os.RemoveAll(filepath.Join(frontendDir, "node_modules")) + + return nil +} + +func processPackageJSON(frontendDir string) error { + var err error + + packageJSON := filepath.Join(frontendDir, "package.json") + if !fs.FileExists(packageJSON) { + return fmt.Errorf("no package.json found - cannot process") + } + + json, err := os.ReadFile(packageJSON) + if err != nil { + return err + } + + // We will ignore these errors - it's not critical + printBulletPoint("Updating package.json data...") + json, _ = sjson.SetBytes(json, "name", "{{.ProjectName}}") + json, _ = sjson.SetBytes(json, "author", "{{.AuthorName}}") + + err = os.WriteFile(packageJSON, json, 0644) + if err != nil { + return err + } + baseDir := filepath.Dir(packageJSON) + printBulletPoint("Renaming package.json -> package.tmpl.json...") + err = os.Rename(packageJSON, filepath.Join(baseDir, "package.tmpl.json")) + if err != nil { + return err + } + return nil +} + +func processPackageLockJSON(frontendDir string) error { + var err error + + filename := filepath.Join(frontendDir, "package-lock.json") + if !fs.FileExists(filename) { + return fmt.Errorf("no package-lock.json found - cannot process") + } + + data, err := os.ReadFile(filename) + if err != nil { + return err + } + json := string(data) + + // We will ignore these errors - it's not critical + printBulletPoint("Updating package-lock.json data...") + json, _ = sjson.Set(json, "name", "{{.ProjectName}}") + + err = os.WriteFile(filename, []byte(json), 0644) + if err != nil { + return err + } + baseDir := filepath.Dir(filename) + printBulletPoint("Renaming package-lock.json -> package-lock.tmpl.json...") + err = os.Rename(filename, filepath.Join(baseDir, "package-lock.tmpl.json")) + if err != nil { + return err + } + return nil +} diff --git a/v2/cmd/wails/init.go b/v2/cmd/wails/init.go new file mode 100644 index 000000000..f9a9c6b3f --- /dev/null +++ b/v2/cmd/wails/init.go @@ -0,0 +1,278 @@ +package main + +import ( + "bufio" + "fmt" + "github.com/flytam/filenamify" + "github.com/leaanthony/slicer" + "github.com/pkg/errors" + "github.com/pterm/pterm" + "github.com/wailsapp/wails/v2/cmd/wails/flags" + "github.com/wailsapp/wails/v2/internal/colour" + "github.com/wailsapp/wails/v2/pkg/buildassets" + "github.com/wailsapp/wails/v2/pkg/clilogger" + "github.com/wailsapp/wails/v2/pkg/git" + "github.com/wailsapp/wails/v2/pkg/templates" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +func initProject(f *flags.Init) error { + + if f.NoColour { + pterm.DisableColor() + colour.ColourEnabled = false + } + + quiet := f.Quiet + + // Create logger + logger := clilogger.New(os.Stdout) + logger.Mute(quiet) + + // Are we listing templates? + if f.List { + app.PrintBanner() + templateList, err := templates.List() + if err != nil { + return err + } + + pterm.DefaultSection.Println("Available templates") + + table := pterm.TableData{{"Template", "Short Name", "Description"}} + for _, template := range templateList { + table = append(table, []string{template.Name, template.ShortName, template.Description}) + } + err = pterm.DefaultTable.WithHasHeader(true).WithBoxed(true).WithData(table).Render() + pterm.Println() + return err + } + + // Validate name + if len(f.ProjectName) == 0 { + return fmt.Errorf("please provide a project name using the -n flag") + } + + // Validate IDE option + supportedIDEs := slicer.String([]string{"vscode", "goland"}) + ide := strings.ToLower(f.IDE) + if ide != "" { + if !supportedIDEs.Contains(ide) { + return fmt.Errorf("ide '%s' not supported. Valid values: %s", ide, supportedIDEs.Join(" ")) + } + } + + if !quiet { + app.PrintBanner() + } + + pterm.DefaultSection.Printf("Initialising Project '%s'", f.ProjectName) + + projectFilename, err := filenamify.Filenamify(f.ProjectName, filenamify.Options{ + Replacement: "_", + MaxLength: 255, + }) + if err != nil { + return err + } + goBinary, err := exec.LookPath("go") + if err != nil { + return fmt.Errorf("unable to find Go compiler. Please download and install Go: https://golang.org/dl/") + } + + // Get base path and convert to forward slashes + goPath := filepath.ToSlash(filepath.Dir(goBinary)) + // Trim bin directory + goSDKPath := strings.TrimSuffix(goPath, "/bin") + + // Create Template Options + options := &templates.Options{ + ProjectName: f.ProjectName, + TargetDir: f.ProjectDir, + TemplateName: f.TemplateName, + Logger: logger, + IDE: ide, + InitGit: f.InitGit, + ProjectNameFilename: projectFilename, + WailsVersion: app.Version(), + GoSDKPath: goSDKPath, + } + + // Try to discover author details from git config + findAuthorDetails(options) + + // Start Time + start := time.Now() + + // Install the template + remote, template, err := templates.Install(options) + if err != nil { + return err + } + + // Install the default assets + err = buildassets.Install(options.TargetDir) + if err != nil { + return err + } + + err = os.Chdir(options.TargetDir) + if err != nil { + return err + } + + if !f.CIMode { + // Run `go mod tidy` to ensure `go.sum` is up to date + cmd := exec.Command("go", "mod", "tidy") + cmd.Dir = options.TargetDir + cmd.Stderr = os.Stderr + if !quiet { + cmd.Stdout = os.Stdout + } + err = cmd.Run() + if err != nil { + return err + } + } else { + // Update go mod + workspace := os.Getenv("GITHUB_WORKSPACE") + pterm.Println("GitHub workspace:", workspace) + if workspace == "" { + os.Exit(1) + } + updateReplaceLine(workspace) + } + + // Remove the `.git`` directory in the template project + err = os.RemoveAll(".git") + if err != nil { + return err + } + + if options.InitGit { + err = initGit(options) + if err != nil { + return err + } + } + + if quiet { + return nil + } + + // Output stats + elapsed := time.Since(start) + + // Create pterm table + table := pterm.TableData{ + {"Project Name", options.ProjectName}, + {"Project Directory", options.TargetDir}, + {"Template", template.Name}, + {"Template Source", template.HelpURL}, + } + err = pterm.DefaultTable.WithData(table).Render() + if err != nil { + return err + } + + // IDE message + switch options.IDE { + case "vscode": + pterm.Println() + pterm.Info.Println("VSCode config files generated.") + case "goland": + pterm.Println() + pterm.Info.Println("Goland config files generated.") + } + + if options.InitGit { + pterm.Info.Println("Git repository initialised.") + } + + if remote { + pterm.Warning.Println("NOTE: You have created a project using a remote template. The Wails project takes no responsibility for 3rd party templates. Only use remote templates that you trust.") + } + + pterm.Println("") + pterm.Printf("Initialised project '%s' in %s.\n", options.ProjectName, elapsed.Round(time.Millisecond).String()) + pterm.Println("") + + return nil +} + +func initGit(options *templates.Options) error { + err := git.InitRepo(options.TargetDir) + if err != nil { + return errors.Wrap(err, "Unable to initialise git repository:") + } + + ignore := []string{ + "build/bin", + "frontend/dist", + "frontend/node_modules", + } + err = os.WriteFile(filepath.Join(options.TargetDir, ".gitignore"), []byte(strings.Join(ignore, "\n")), 0644) + if err != nil { + return errors.Wrap(err, "Unable to create gitignore") + } + + return nil +} + +// findAuthorDetails tries to find the user's name and email +// from gitconfig. If it finds them, it stores them in the project options +func findAuthorDetails(options *templates.Options) { + if git.IsInstalled() { + name, err := git.Name() + if err == nil { + options.AuthorName = strings.TrimSpace(name) + } + + email, err := git.Email() + if err == nil { + options.AuthorEmail = strings.TrimSpace(email) + } + } +} + +func updateReplaceLine(targetPath string) { + file, err := os.Open("go.mod") + if err != nil { + fatal(err.Error()) + } + + var lines []string + scanner := bufio.NewScanner(file) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + + err = file.Close() + if err != nil { + fatal(err.Error()) + } + + if err := scanner.Err(); err != nil { + fatal(err.Error()) + } + + for i, line := range lines { + println(line) + if strings.HasPrefix(line, "// replace") { + pterm.Println("Found replace line") + splitLine := strings.Split(line, " ") + splitLine[5] = targetPath + "/v2" + lines[i] = strings.Join(splitLine[1:], " ") + continue + } + } + + err = os.WriteFile("go.mod", []byte(strings.Join(lines, "\n")), 0644) + if err != nil { + fatal(err.Error()) + } +} diff --git a/v2/cmd/wails/internal/commands/build/README.md b/v2/cmd/wails/internal/commands/build/README.md deleted file mode 100644 index b4bcc21d6..000000000 --- a/v2/cmd/wails/internal/commands/build/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Build - -The build command processes the Wails project and generates an application binary. - -## Usage - -`wails build ` - -### Flags - -| Flag | Details | Default | -| :------------- | :----------- | :------ | -| -clean | Clean the bin directory before building | | -| -compiler path/to/compiler | Use a different go compiler, eg go1.15beta1 | go | -| -ldflags "custom ld flags" | Use given ldflags | | -| -o path/to/binary | Compile to given path/filename | | -| -k | Keep generated assets | | -| -tags | Build tags to pass to Go compiler (quoted and space separated) | | -| -upx | Compress final binary with UPX (if installed) | | -| -upxflags "custom flags" | Flags to pass to upx | | -| -v int | Verbosity level (0 - silent, 1 - default, 2 - verbose) | 1 | -| -delve | If true, runs delve on the compiled binary | false | - -## The Build Process - -The build process is as follows: - - - The flags are processed, and an Options struct built containing the build context. - - The type of target is determined, and a custom build process is followed for target. - -### Desktop Target - - - The frontend dependencies are installed. The command is read from the project file `wails.json` under the key `frontend:install` and executed in the `frontend` directory. If this is not defined, it is ignored. - - The frontend is then built. This command is read from the project file `wails.json` under the key `frontend:install` and executed in the `frontend` directory. If this is not defined, it is ignored. - - The project directory is checked to see if the `build` directory exists. If not, it is created and default project assets are copied to it. - - An asset bundle is then created by reading the `html` key from `wails.json` and loading the referenced file. This is then parsed, looking for local Javascript and CSS references. Those files are in turn loaded into memory, converted to C data and saved into the asset bundle located at `build/assets.h`, which also includes the original HTML. - - The application icon is then processed: if there is no `build/appicon.png`, a default icon is copied. On Windows, - an `app.ico` file is generated from this png. On Mac, `icons.icns` is generated. - - The platform assets in the `build/` directory are processed: manifest + icons compiled to a `.syso` file ( - deleted after compilation), `info.plist` copied to `.app` on Mac. - - If we are building a universal binary for Mac, the application is compiled for both `arm64` and `amd64`. The `lipo` - tool is then executed to create the universal binary. - - If we are not building a universal binary for Mac, the application is built using `go build`, using build tags to indicate type of application and build mode (debug/production). - - If the `-upx` flag was provided, `upx` is invoked to compress the binary. Custom flags may be provided using the `-upxflags` flag. - - diff --git a/v2/cmd/wails/internal/commands/build/build.go b/v2/cmd/wails/internal/commands/build/build.go deleted file mode 100644 index 4df9aaa8d..000000000 --- a/v2/cmd/wails/internal/commands/build/build.go +++ /dev/null @@ -1,401 +0,0 @@ -package build - -import ( - "fmt" - "io" - "os" - "os/exec" - "runtime" - "strings" - "text/tabwriter" - "time" - - "github.com/wailsapp/wails/v2/pkg/commands/buildtags" - - "github.com/wailsapp/wails/v2/internal/colour" - "github.com/wailsapp/wails/v2/internal/project" - "github.com/wailsapp/wails/v2/internal/system" - - "github.com/leaanthony/clir" - "github.com/leaanthony/slicer" - "github.com/wailsapp/wails/v2/pkg/clilogger" - "github.com/wailsapp/wails/v2/pkg/commands/build" -) - -// AddBuildSubcommand adds the `build` command for the Wails application -func AddBuildSubcommand(app *clir.Cli, w io.Writer) { - - outputType := "desktop" - - validTargetTypes := slicer.String([]string{"desktop", "hybrid", "server"}) - - command := app.NewSubCommand("build", "Builds the application") - - // Setup noPackage flag - noPackage := false - command.BoolFlag("noPackage", "Skips platform specific packaging", &noPackage) - - compilerCommand := "go" - command.StringFlag("compiler", "Use a different go compiler to build, eg go1.15beta1", &compilerCommand) - - skipModTidy := false - command.BoolFlag("m", "Skip mod tidy before compile", &skipModTidy) - - compress := false - command.BoolFlag("upx", "Compress final binary with UPX (if installed)", &compress) - - compressFlags := "" - command.StringFlag("upxflags", "Flags to pass to upx", &compressFlags) - - defaultPlatform := os.Getenv("GOOS") - if defaultPlatform == "" { - defaultPlatform = runtime.GOOS - } - defaultArch := os.Getenv("GOARCH") - if defaultArch == "" { - if system.IsAppleSilicon { - defaultArch = "arm64" - } else { - defaultArch = runtime.GOARCH - } - } - platform := defaultPlatform + "/" + defaultArch - - command.StringFlag("platform", "Platform to target. Comma separate multiple platforms", &platform) - - // Verbosity - verbosity := 1 - command.IntFlag("v", "Verbosity level (0 - silent, 1 - default, 2 - verbose)", &verbosity) - - // ldflags to pass to `go` - ldflags := "" - command.StringFlag("ldflags", "optional ldflags", &ldflags) - - // tags to pass to `go` - tags := "" - command.StringFlag("tags", "Build tags to pass to Go compiler. Must be quoted. Space or comma (but not both) separated", &tags) - - outputFilename := "" - command.StringFlag("o", "Output filename", &outputFilename) - - // Clean bin directory - cleanBinDirectory := false - command.BoolFlag("clean", "Clean the bin directory before building", &cleanBinDirectory) - - webview2 := "download" - command.StringFlag("webview2", "WebView2 installer strategy: download,embed,browser,error.", &webview2) - - skipFrontend := false - command.BoolFlag("s", "Skips building the frontend", &skipFrontend) - - forceBuild := false - command.BoolFlag("f", "Force build application", &forceBuild) - - updateGoModWailsVersion := false - command.BoolFlag("u", "Updates go.mod to use the same Wails version as the CLI", &updateGoModWailsVersion) - - debug := false - command.BoolFlag("debug", "Retains debug data in the compiled application", &debug) - - nsis := false - command.BoolFlag("nsis", "Generate NSIS installer for Windows", &nsis) - - trimpath := false - command.BoolFlag("trimpath", "Remove all file system paths from the resulting executable", &trimpath) - - raceDetector := false - command.BoolFlag("race", "Build with Go's race detector", &raceDetector) - - windowsConsole := false - command.BoolFlag("windowsconsole", "Keep the console when building for Windows", &windowsConsole) - - obfuscated := false - command.BoolFlag("obfuscated", "Code obfuscation of bound Wails methods", &obfuscated) - - garbleargs := "-literals -tiny -seed=random" - command.StringFlag("garbleargs", "Arguments to pass to garble", &garbleargs) - - dryRun := false - command.BoolFlag("dryrun", "Dry run, prints the config for the command that would be executed", &dryRun) - - skipBindings := false - command.BoolFlag("skipbindings", "Skips generation of bindings", &skipBindings) - - command.Action(func() error { - - quiet := verbosity == 0 - - // Create logger - logger := clilogger.New(w) - logger.Mute(quiet) - - // Validate output type - if !validTargetTypes.Contains(outputType) { - return fmt.Errorf("output type '%s' is not valid", outputType) - } - - if !quiet { - app.PrintBanner() - } - - // Lookup compiler path - compilerPath, err := exec.LookPath(compilerCommand) - if err != nil { - return fmt.Errorf("unable to find compiler: %s", compilerCommand) - } - - // Process User Tags - userTags, err := buildtags.Parse(tags) - if err != nil { - return err - } - - // Webview2 installer strategy (download by default) - wv2rtstrategy := "" - webview2 = strings.ToLower(webview2) - if webview2 != "" { - validWV2Runtime := slicer.String([]string{"download", "embed", "browser", "error"}) - if !validWV2Runtime.Contains(webview2) { - return fmt.Errorf("invalid option for flag 'webview2': %s", webview2) - } - // These are the build tags associated with the strategies - switch webview2 { - case "embed": - wv2rtstrategy = "wv2runtime.embed" - case "error": - wv2rtstrategy = "wv2runtime.error" - case "browser": - wv2rtstrategy = "wv2runtime.browser" - } - } - - mode := build.Production - modeString := "Production" - if debug { - mode = build.Debug - modeString = "Debug" - } - - var targets slicer.StringSlicer - targets.AddSlice(strings.Split(platform, ",")) - targets.Deduplicate() - - cwd, err := os.Getwd() - if err != nil { - return err - } - projectOptions, err := project.Load(cwd) - if err != nil { - return err - } - - // Create BuildOptions - buildOptions := &build.Options{ - Logger: logger, - OutputType: outputType, - OutputFile: outputFilename, - CleanBinDirectory: cleanBinDirectory, - Mode: mode, - Pack: !noPackage, - LDFlags: ldflags, - Compiler: compilerCommand, - SkipModTidy: skipModTidy, - Verbosity: verbosity, - ForceBuild: forceBuild, - IgnoreFrontend: skipFrontend, - Compress: compress, - CompressFlags: compressFlags, - UserTags: userTags, - WebView2Strategy: wv2rtstrategy, - TrimPath: trimpath, - RaceDetector: raceDetector, - WindowsConsole: windowsConsole, - Obfuscated: obfuscated, - GarbleArgs: garbleargs, - SkipBindings: skipBindings, - ProjectData: projectOptions, - } - - // Start a new tabwriter - if !quiet { - w := new(tabwriter.Writer) - w.Init(os.Stdout, 8, 8, 0, '\t', 0) - - // Write out the system information - _, _ = fmt.Fprintf(w, "App Type: \t%s\n", buildOptions.OutputType) - _, _ = fmt.Fprintf(w, "Platforms: \t%s\n", platform) - _, _ = fmt.Fprintf(w, "Compiler: \t%s\n", compilerPath) - _, _ = fmt.Fprintf(w, "Skip Bindings: \t%t\n", skipBindings) - _, _ = fmt.Fprintf(w, "Build Mode: \t%s\n", modeString) - _, _ = fmt.Fprintf(w, "Frontend Directory: \t%s\n", projectOptions.GetFrontendDir()) - _, _ = fmt.Fprintf(w, "Obfuscated: \t%t\n", buildOptions.Obfuscated) - if buildOptions.Obfuscated { - _, _ = fmt.Fprintf(w, "Garble Args: \t%s\n", buildOptions.GarbleArgs) - } - _, _ = fmt.Fprintf(w, "Skip Frontend: \t%t\n", skipFrontend) - _, _ = fmt.Fprintf(w, "Compress: \t%t\n", buildOptions.Compress) - _, _ = fmt.Fprintf(w, "Package: \t%t\n", buildOptions.Pack) - _, _ = fmt.Fprintf(w, "Clean Bin Dir: \t%t\n", buildOptions.CleanBinDirectory) - _, _ = fmt.Fprintf(w, "LDFlags: \t\"%s\"\n", buildOptions.LDFlags) - _, _ = fmt.Fprintf(w, "Tags: \t[%s]\n", strings.Join(buildOptions.UserTags, ",")) - _, _ = fmt.Fprintf(w, "Race Detector: \t%t\n", buildOptions.RaceDetector) - if len(buildOptions.OutputFile) > 0 && targets.Length() == 1 { - _, _ = fmt.Fprintf(w, "Output File: \t%s\n", buildOptions.OutputFile) - } - _, _ = fmt.Fprintf(w, "\n") - err = w.Flush() - if err != nil { - return err - } - } - - err = SyncGoMod(logger, updateGoModWailsVersion) - if err != nil { - return err - } - - // Check platform - validPlatformArch := slicer.String([]string{ - "darwin", - "darwin/amd64", - "darwin/arm64", - "darwin/universal", - "linux", - "linux/amd64", - "linux/arm64", - "linux/arm", - "windows", - "windows/amd64", - "windows/arm64", - "windows/386", - }) - - outputBinaries := map[string]string{} - - // Allows cancelling the build after the first error. It would be nice if targets.Each would support funcs - // returning an error. - var targetErr error - targets.Each(func(platform string) { - if targetErr != nil { - return - } - - if !validPlatformArch.Contains(platform) { - buildOptions.Logger.Println("platform '%s' is not supported - skipping. Supported platforms: %s", platform, validPlatformArch.Join(",")) - return - } - - desiredFilename := projectOptions.OutputFilename - if desiredFilename == "" { - desiredFilename = projectOptions.Name - } - desiredFilename = strings.TrimSuffix(desiredFilename, ".exe") - - // Calculate platform and arch - platformSplit := strings.Split(platform, "/") - buildOptions.Platform = platformSplit[0] - buildOptions.Arch = defaultArch - if len(platformSplit) > 1 { - buildOptions.Arch = platformSplit[1] - } - banner := "Building target: " + buildOptions.Platform + "/" + buildOptions.Arch - logger.Println(banner) - logger.Println(strings.Repeat("-", len(banner))) - - if compress && platform == "darwin/universal" { - logger.Println("Warning: compress flag unsupported for universal binaries. Ignoring.") - compress = false - } - - switch buildOptions.Platform { - case "linux": - if runtime.GOOS != "linux" { - logger.Println("Crosscompiling to Linux not currently supported.\n") - return - } - case "darwin": - if runtime.GOOS != "darwin" { - logger.Println("Crosscompiling to Mac not currently supported.\n") - return - } - macTargets := targets.Filter(func(platform string) bool { - return strings.HasPrefix(platform, "darwin") - }) - if macTargets.Length() == 2 { - buildOptions.BundleName = fmt.Sprintf("%s-%s.app", desiredFilename, buildOptions.Arch) - } - } - - if targets.Length() > 1 { - // target filename - switch buildOptions.Platform { - case "windows": - desiredFilename = fmt.Sprintf("%s-%s", desiredFilename, buildOptions.Arch) - case "linux", "darwin": - desiredFilename = fmt.Sprintf("%s-%s-%s", desiredFilename, buildOptions.Platform, buildOptions.Arch) - } - } - if buildOptions.Platform == "windows" { - desiredFilename += ".exe" - } - buildOptions.OutputFile = desiredFilename - - if outputFilename != "" { - buildOptions.OutputFile = outputFilename - } - - if obfuscated && skipBindings { - logger.Println("Warning: obfuscated flag overrides skipbindings flag.") - buildOptions.SkipBindings = false - } - - if !dryRun { - // Start Time - start := time.Now() - - compiledBinary, err := build.Build(buildOptions) - if err != nil { - logger.Println("Error: %s", err.Error()) - targetErr = err - return - } - - buildOptions.IgnoreFrontend = true - buildOptions.CleanBinDirectory = false - - // Output stats - buildOptions.Logger.Println(fmt.Sprintf("Built '%s' in %s.\n", compiledBinary, time.Since(start).Round(time.Millisecond).String())) - - outputBinaries[buildOptions.Platform+"/"+buildOptions.Arch] = compiledBinary - } else { - logger.Println("Dry run: skipped build.") - } - }) - - if targetErr != nil { - return targetErr - } - - if dryRun { - return nil - } - - if nsis { - amd64Binary := outputBinaries["windows/amd64"] - arm64Binary := outputBinaries["windows/arm64"] - if amd64Binary == "" && arm64Binary == "" { - return fmt.Errorf("cannot build nsis installer - no windows targets") - } - - if err := build.GenerateNSISInstaller(buildOptions, amd64Binary, arm64Binary); err != nil { - return err - } - } - return nil - }) -} - -func LogGreen(message string, args ...interface{}) { - text := fmt.Sprintf(message, args...) - println(colour.Green(text)) -} diff --git a/v2/cmd/wails/internal/commands/dev/README.md b/v2/cmd/wails/internal/commands/dev/README.md deleted file mode 100644 index 96eb3d2a2..000000000 --- a/v2/cmd/wails/internal/commands/dev/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Dev - -The dev command allows you to develop your application through a standard browser. - -## Usage - -`wails dev ` - -### Flags - -| Flag | Details | Default | -| :------------- | :----------- | :------ | -| -compiler path/to/compiler | Use a different go compiler, eg go1.15beta1 | go | -| -ldflags "custom ld flags" | Use given ldflags | | -| -e list,of,extensions | File extensions to trigger rebuilds | go | -| -w | Show warnings | false | -| -v int | Verbosity level (0 - silent, 1 - default, 2 - verbose) | 1 | -| -loglevel | Loglevel to pass to the application - Trace, Debug, Info, Warning, Error | Debug | - -## How it works - -The project is built using a special mode that starts a webserver and starts listening to port 34115. When the frontend project is run independently, so long as the JS is wrapped with the runtime method `ready`, then the frontend will connect to the backend code via websockets. The interface should be present in your browser, and you should be able to interact with the backend as you would in a desktop app. \ No newline at end of file diff --git a/v2/cmd/wails/internal/commands/dev/dev.go b/v2/cmd/wails/internal/commands/dev/dev.go deleted file mode 100644 index 907e996b4..000000000 --- a/v2/cmd/wails/internal/commands/dev/dev.go +++ /dev/null @@ -1,694 +0,0 @@ -package dev - -import ( - "context" - "errors" - "fmt" - "io" - "net" - "net/http" - "net/url" - "os" - "os/exec" - "os/signal" - "path" - "path/filepath" - "runtime" - "strings" - "sync" - "sync/atomic" - "syscall" - "time" - - "github.com/wailsapp/wails/v2/pkg/commands/bindings" - "github.com/wailsapp/wails/v2/pkg/commands/buildtags" - - "github.com/google/shlex" - buildcmd "github.com/wailsapp/wails/v2/cmd/wails/internal/commands/build" - - "github.com/wailsapp/wails/v2/internal/project" - - "github.com/pkg/browser" - "github.com/wailsapp/wails/v2/internal/colour" - - "github.com/fsnotify/fsnotify" - "github.com/leaanthony/clir" - "github.com/wailsapp/wails/v2/cmd/wails/internal/logutils" - "github.com/wailsapp/wails/v2/internal/fs" - "github.com/wailsapp/wails/v2/internal/process" - "github.com/wailsapp/wails/v2/pkg/clilogger" - "github.com/wailsapp/wails/v2/pkg/commands/build" -) - -func sliceToMap(input []string) map[string]struct{} { - result := map[string]struct{}{} - for _, value := range input { - result[value] = struct{}{} - } - return result -} - -type devFlags struct { - ldflags string - compilerCommand string - assetDir string - extensions string - reloadDirs string - openBrowser bool - noReload bool - skipBindings bool - wailsjsdir string - tags string - verbosity int - loglevel string - forceBuild bool - debounceMS int - devServer string - appargs string - saveConfig bool - raceDetector bool - - frontendDevServerURL string - skipFrontend bool - noColour bool -} - -// AddSubcommand adds the `dev` command for the Wails application -func AddSubcommand(app *clir.Cli, w io.Writer) error { - - command := app.NewSubCommand("dev", "Development mode") - - flags := defaultDevFlags() - command.StringFlag("ldflags", "optional ldflags", &flags.ldflags) - command.StringFlag("compiler", "Use a different go compiler to build, eg go1.15beta1", &flags.compilerCommand) - command.StringFlag("assetdir", "Serve assets from the given directory instead of using the provided asset FS", &flags.assetDir) - command.StringFlag("e", "Extensions to trigger rebuilds (comma separated) eg go", &flags.extensions) - command.StringFlag("reloaddirs", "Additional directories to trigger reloads (comma separated)", &flags.reloadDirs) - command.BoolFlag("browser", "Open application in browser", &flags.openBrowser) - command.BoolFlag("noreload", "Disable reload on asset change", &flags.noReload) - command.BoolFlag("nocolour", "Turn off colour cli output", &flags.noColour) - command.BoolFlag("skipbindings", "Skip bindings generation", &flags.skipBindings) - command.StringFlag("wailsjsdir", "Directory to generate the Wails JS modules", &flags.wailsjsdir) - command.StringFlag("tags", "Build tags to pass to Go compiler. Must be quoted. Space or comma (but not both) separated", &flags.tags) - command.IntFlag("v", "Verbosity level (0 - silent, 1 - standard, 2 - verbose)", &flags.verbosity) - command.StringFlag("loglevel", "Loglevel to use - Trace, Debug, Info, Warning, Error", &flags.loglevel) - command.BoolFlag("f", "Force build application", &flags.forceBuild) - command.IntFlag("debounce", "The amount of time to wait to trigger a reload on change", &flags.debounceMS) - command.StringFlag("devserver", "The address of the wails dev server", &flags.devServer) - command.StringFlag("frontenddevserverurl", "The url of the external frontend dev server to use", &flags.frontendDevServerURL) - command.StringFlag("appargs", "arguments to pass to the underlying app (quoted and space separated)", &flags.appargs) - command.BoolFlag("save", "Save given flags as defaults", &flags.saveConfig) - command.BoolFlag("race", "Build with Go's race detector", &flags.raceDetector) - command.BoolFlag("s", "Skips building the frontend", &flags.skipFrontend) - - command.Action(func() error { - if flags.noColour { - colour.ColourEnabled = false - } - - // Create logger - logger := clilogger.New(w) - app.PrintBanner() - - cwd, err := os.Getwd() - if err != nil { - return err - } - - projectConfig, err := loadAndMergeProjectConfig(cwd, &flags) - if err != nil { - return err - } - - devServer := flags.devServer - if _, _, err := net.SplitHostPort(devServer); err != nil { - return fmt.Errorf("DevServer is not of the form 'host:port', please check your wails.json") - } - - devServerURL, err := url.Parse("http://" + devServer) - if err != nil { - return err - } - - // Update go.mod to use current wails version - err = buildcmd.SyncGoMod(logger, true) - if err != nil { - return err - } - - // Run go mod tidy to ensure we're up-to-date - err = runCommand(cwd, false, "go", "mod", "tidy") - if err != nil { - return err - } - - buildOptions := generateBuildOptions(flags) - buildOptions.ProjectData = projectConfig - buildOptions.SkipBindings = flags.skipBindings - buildOptions.Logger = logger - - userTags, err := buildtags.Parse(flags.tags) - if err != nil { - return err - } - - buildOptions.UserTags = userTags - - err = build.CreateEmbedDirectories(cwd, buildOptions) - if err != nil { - return err - } - - if !buildOptions.SkipBindings { - if flags.verbosity == build.VERBOSE { - logutils.LogGreen("Generating Bindings...") - } - stdout, err := bindings.GenerateBindings(bindings.Options{ - Tags: buildOptions.UserTags, - TsPrefix: projectConfig.Bindings.TsGeneration.Prefix, - TsSuffix: projectConfig.Bindings.TsGeneration.Suffix, - }) - if err != nil { - return err - } - if flags.verbosity == build.VERBOSE { - logutils.LogGreen(stdout) - } - } - - // Setup signal handler - quitChannel := make(chan os.Signal, 1) - signal.Notify(quitChannel, os.Interrupt, os.Kill, syscall.SIGTERM) - exitCodeChannel := make(chan int, 1) - - // Build the frontend if requested, but ignore building the application itself. - ignoreFrontend := buildOptions.IgnoreFrontend - if !ignoreFrontend { - buildOptions.IgnoreApplication = true - if _, err := build.Build(buildOptions); err != nil { - return err - } - buildOptions.IgnoreApplication = false - } - - // frontend:dev:watcher command. - frontendDevAutoDiscovery := projectConfig.IsFrontendDevServerURLAutoDiscovery() - if command := projectConfig.DevWatcherCommand; command != "" { - closer, devServerURL, err := runFrontendDevWatcherCommand(projectConfig.GetFrontendDir(), command, frontendDevAutoDiscovery) - if err != nil { - return err - } - if devServerURL != "" { - projectConfig.FrontendDevServerURL = devServerURL - flags.frontendDevServerURL = devServerURL - } - defer closer() - } else if frontendDevAutoDiscovery { - return fmt.Errorf("Unable to auto discover frontend:dev:serverUrl without a frontend:dev:watcher command, please either set frontend:dev:watcher or remove the auto discovery from frontend:dev:serverUrl") - } - - // Do initial build but only for the application. - logger.Println("Building application for development...") - buildOptions.IgnoreFrontend = true - debugBinaryProcess, appBinary, err := restartApp(buildOptions, nil, flags, exitCodeChannel) - buildOptions.IgnoreFrontend = ignoreFrontend || flags.frontendDevServerURL != "" - if err != nil { - return err - } - defer func() { - if err := killProcessAndCleanupBinary(debugBinaryProcess, appBinary); err != nil { - logutils.LogDarkYellow("Unable to kill process and cleanup binary: %s", err) - } - }() - - // open browser - if flags.openBrowser { - err = browser.OpenURL(devServerURL.String()) - if err != nil { - return err - } - } - - // create the project files watcher - watcher, err := initialiseWatcher(cwd) - if err != nil { - return err - } - - defer func(watcher *fsnotify.Watcher) { - err := watcher.Close() - if err != nil { - logger.Fatal(err.Error()) - } - }(watcher) - - logutils.LogGreen("Watching (sub)/directory: %s", cwd) - logutils.LogGreen("Using DevServer URL: %s", devServerURL) - if flags.frontendDevServerURL != "" { - logutils.LogGreen("Using Frontend DevServer URL: %s", flags.frontendDevServerURL) - } - logutils.LogGreen("Using reload debounce setting of %d milliseconds", flags.debounceMS) - - // Show dev server URL in terminal after 3 seconds - go func() { - time.Sleep(3 * time.Second) - logutils.LogGreen("\n\nTo develop in the browser and call your bound Go methods from Javascript, navigate to: %s", devServerURL) - }() - - // Watch for changes and trigger restartApp() - debugBinaryProcess = doWatcherLoop(buildOptions, debugBinaryProcess, flags, watcher, exitCodeChannel, quitChannel, devServerURL) - - // Kill the current program if running and remove dev binary - if err := killProcessAndCleanupBinary(debugBinaryProcess, appBinary); err != nil { - return err - } - - // Reset the process and the binary so defer knows about it and is a nop. - debugBinaryProcess = nil - appBinary = "" - - logutils.LogGreen("Development mode exited") - - return nil - }) - return nil -} - -func killProcessAndCleanupBinary(process *process.Process, binary string) error { - if process != nil && process.Running { - if err := process.Kill(); err != nil { - return err - } - } - - if binary != "" { - err := os.Remove(binary) - if err != nil && !errors.Is(err, os.ErrNotExist) { - return err - } - } - return nil -} - -func runCommand(dir string, exitOnError bool, command string, args ...string) error { - logutils.LogGreen("Executing: " + command + " " + strings.Join(args, " ")) - cmd := exec.Command(command, args...) - cmd.Dir = dir - output, err := cmd.CombinedOutput() - if err != nil { - println(string(output)) - println(err.Error()) - if exitOnError { - os.Exit(1) - } - return err - } - return nil -} - -// defaultDevFlags generates devFlags with default options -func defaultDevFlags() devFlags { - return devFlags{ - compilerCommand: "go", - verbosity: 1, - extensions: "go", - debounceMS: 100, - } -} - -// generateBuildOptions creates a build.Options using the flags -func generateBuildOptions(flags devFlags) *build.Options { - result := &build.Options{ - OutputType: "dev", - Mode: build.Dev, - Arch: runtime.GOARCH, - Pack: true, - Platform: runtime.GOOS, - LDFlags: flags.ldflags, - Compiler: flags.compilerCommand, - ForceBuild: flags.forceBuild, - IgnoreFrontend: flags.skipFrontend, - Verbosity: flags.verbosity, - WailsJSDir: flags.wailsjsdir, - RaceDetector: flags.raceDetector, - } - - return result -} - -// loadAndMergeProjectConfig reconciles flags passed to the CLI with project config settings and updates -// the project config if necessary -func loadAndMergeProjectConfig(projectFileDirectory string, flags *devFlags) (*project.Project, error) { - projectConfig, err := project.Load(projectFileDirectory) - if err != nil { - return nil, err - } - - if flags.assetDir == "" && projectConfig.AssetDirectory != "" { - flags.assetDir = projectConfig.AssetDirectory - } - - if flags.assetDir != projectConfig.AssetDirectory { - projectConfig.AssetDirectory = filepath.ToSlash(flags.assetDir) - } - - if flags.assetDir != "" { - flags.assetDir, err = filepath.Abs(flags.assetDir) - if err != nil { - return nil, err - } - } - - if flags.reloadDirs == "" && projectConfig.ReloadDirectories != "" { - flags.reloadDirs = projectConfig.ReloadDirectories - } - - if flags.reloadDirs != projectConfig.ReloadDirectories { - projectConfig.ReloadDirectories = filepath.ToSlash(flags.reloadDirs) - } - - if flags.devServer == "" && projectConfig.DevServer != "" { - flags.devServer = projectConfig.DevServer - } - - if flags.frontendDevServerURL == "" && projectConfig.FrontendDevServerURL != "" { - flags.frontendDevServerURL = projectConfig.FrontendDevServerURL - } - - if flags.wailsjsdir == "" && projectConfig.WailsJSDir != "" { - flags.wailsjsdir = projectConfig.GetWailsJSDir() - } - - if flags.wailsjsdir == "" { - flags.wailsjsdir = projectConfig.GetFrontendDir() - } - - if flags.wailsjsdir != projectConfig.WailsJSDir { - projectConfig.WailsJSDir = filepath.ToSlash(flags.wailsjsdir) - } - - if flags.debounceMS == 100 && projectConfig.DebounceMS != 100 { - if projectConfig.DebounceMS == 0 { - projectConfig.DebounceMS = 100 - } - flags.debounceMS = projectConfig.DebounceMS - } - - if flags.debounceMS != projectConfig.DebounceMS { - projectConfig.DebounceMS = flags.debounceMS - } - - if flags.appargs == "" && projectConfig.AppArgs != "" { - flags.appargs = projectConfig.AppArgs - } - - if flags.saveConfig { - err = projectConfig.Save() - if err != nil { - return nil, err - } - } - - return projectConfig, nil -} - -// runFrontendDevWatcherCommand will run the `frontend:dev:watcher` command if it was given, ex- `npm run dev` -func runFrontendDevWatcherCommand(frontendDirectory string, devCommand string, discoverViteServerURL bool) (func(), string, error) { - ctx, cancel := context.WithCancel(context.Background()) - scanner := NewStdoutScanner() - cmdSlice := strings.Split(devCommand, " ") - cmd := exec.CommandContext(ctx, cmdSlice[0], cmdSlice[1:]...) - cmd.Stderr = os.Stderr - cmd.Stdout = scanner - cmd.Dir = frontendDirectory - setParentGID(cmd) - - if err := cmd.Start(); err != nil { - cancel() - return nil, "", fmt.Errorf("unable to start frontend DevWatcher: %w", err) - } - - var viteServerURL string - if discoverViteServerURL { - select { - case serverURL := <-scanner.ViteServerURLChan: - viteServerURL = serverURL - case <-time.After(time.Second * 10): - cancel() - return nil, "", errors.New("failed to find Vite server URL") - } - } - - logutils.LogGreen("Running frontend DevWatcher command: '%s'", devCommand) - var wg sync.WaitGroup - wg.Add(1) - - const ( - stateRunning int32 = 0 - stateCanceling = 1 - stateStopped = 2 - ) - state := stateRunning - go func() { - if err := cmd.Wait(); err != nil { - wasRunning := atomic.CompareAndSwapInt32(&state, stateRunning, stateStopped) - if err.Error() != "exit status 1" && wasRunning { - logutils.LogRed("Error from DevWatcher '%s': %s", devCommand, err.Error()) - } - } - atomic.StoreInt32(&state, stateStopped) - wg.Done() - }() - - return func() { - if atomic.CompareAndSwapInt32(&state, stateRunning, stateCanceling) { - killProc(cmd, devCommand) - } - cancel() - wg.Wait() - }, viteServerURL, nil -} - -// restartApp does the actual rebuilding of the application when files change -func restartApp(buildOptions *build.Options, debugBinaryProcess *process.Process, flags devFlags, exitCodeChannel chan int) (*process.Process, string, error) { - - appBinary, err := build.Build(buildOptions) - println() - if err != nil { - logutils.LogRed("Build error - " + err.Error()) - - msg := "Continuing to run current version" - if debugBinaryProcess == nil { - msg = "No version running, build will be retriggered as soon as changes have been detected" - } - logutils.LogDarkYellow(msg) - return nil, "", nil - } - - // Kill existing binary if need be - if debugBinaryProcess != nil { - killError := debugBinaryProcess.Kill() - - if killError != nil { - buildOptions.Logger.Fatal("Unable to kill debug binary (PID: %d)!", debugBinaryProcess.PID()) - } - - debugBinaryProcess = nil - } - - // parse appargs if any - args, err := shlex.Split(flags.appargs) - - if err != nil { - buildOptions.Logger.Fatal("Unable to parse appargs: %s", err.Error()) - } - - // Set environment variables accordingly - os.Setenv("loglevel", flags.loglevel) - os.Setenv("assetdir", flags.assetDir) - os.Setenv("devserver", flags.devServer) - os.Setenv("frontenddevserverurl", flags.frontendDevServerURL) - - // Start up new binary with correct args - newProcess := process.NewProcess(appBinary, args...) - err = newProcess.Start(exitCodeChannel) - if err != nil { - // Remove binary - if fs.FileExists(appBinary) { - deleteError := fs.DeleteFile(appBinary) - if deleteError != nil { - buildOptions.Logger.Fatal("Unable to delete app binary: " + appBinary) - } - } - buildOptions.Logger.Fatal("Unable to start application: %s", err.Error()) - } - - return newProcess, appBinary, nil -} - -// doWatcherLoop is the main watch loop that runs while dev is active -func doWatcherLoop(buildOptions *build.Options, debugBinaryProcess *process.Process, flags devFlags, watcher *fsnotify.Watcher, exitCodeChannel chan int, quitChannel chan os.Signal, devServerURL *url.URL) *process.Process { - // Main Loop - var extensionsThatTriggerARebuild = sliceToMap(strings.Split(flags.extensions, ",")) - var dirsThatTriggerAReload []string - for _, dir := range strings.Split(flags.reloadDirs, ",") { - if dir == "" { - continue - } - thePath, err := filepath.Abs(dir) - if err != nil { - logutils.LogRed("Unable to expand reloadDir '%s': %s", dir, err) - continue - } - dirsThatTriggerAReload = append(dirsThatTriggerAReload, thePath) - } - - quit := false - interval := time.Duration(flags.debounceMS) * time.Millisecond - timer := time.NewTimer(interval) - rebuild := false - reload := false - assetDir := "" - changedPaths := map[string]struct{}{} - - // If we are using an external dev server, the reloading of the frontend part can be skipped or if the user requested it - skipAssetsReload := (flags.frontendDevServerURL != "" || flags.noReload) - - assetDirURL := joinPath(devServerURL, "/wails/assetdir") - reloadURL := joinPath(devServerURL, "/wails/reload") - for quit == false { - // reload := false - select { - case exitCode := <-exitCodeChannel: - if exitCode == 0 { - quit = true - } - case err := <-watcher.Errors: - logutils.LogDarkYellow(err.Error()) - case item := <-watcher.Events: - isEligibleFile := func(fileName string) bool { - // Iterate all file patterns - ext := filepath.Ext(fileName) - if ext != "" { - ext = ext[1:] - if _, exists := extensionsThatTriggerARebuild[ext]; exists { - return true - } - } - return false - } - - // Handle write operations - if item.Op&fsnotify.Write == fsnotify.Write { - // Ignore directories - itemName := item.Name - if fs.DirExists(itemName) { - continue - } - - if isEligibleFile(itemName) { - rebuild = true - timer.Reset(interval) - continue - } - - for _, reloadDir := range dirsThatTriggerAReload { - if strings.HasPrefix(itemName, reloadDir) { - reload = true - break - } - } - - if !reload { - changedPaths[filepath.Dir(itemName)] = struct{}{} - } - - timer.Reset(interval) - } - - // Handle new fs entries that are created - if item.Op&fsnotify.Create == fsnotify.Create { - // If this is a folder, add it to our watch list - if fs.DirExists(item.Name) { - // node_modules is BANNED! - if !strings.Contains(item.Name, "node_modules") { - err := watcher.Add(item.Name) - if err != nil { - buildOptions.Logger.Fatal("%s", err.Error()) - } - logutils.LogGreen("Added new directory to watcher: %s", item.Name) - } - } else if isEligibleFile(item.Name) { - // Handle creation of new file. - // Note: On some platforms an update to a file is represented as - // REMOVE -> CREATE instead of WRITE, so this is not only new files - // but also updates to existing files - rebuild = true - timer.Reset(interval) - continue - } - } - case <-timer.C: - if rebuild { - rebuild = false - logutils.LogGreen("[Rebuild triggered] files updated") - // Try and build the app - newBinaryProcess, _, err := restartApp(buildOptions, debugBinaryProcess, flags, exitCodeChannel) - if err != nil { - logutils.LogRed("Error during build: %s", err.Error()) - continue - } - // If we have a new process, saveConfig it - if newBinaryProcess != nil { - debugBinaryProcess = newBinaryProcess - } - } - - if !skipAssetsReload && len(changedPaths) != 0 { - if assetDir == "" { - resp, err := http.Get(assetDirURL) - if err != nil { - logutils.LogRed("Error during retrieving assetdir: %s", err.Error()) - } else { - content, err := io.ReadAll(resp.Body) - if err != nil { - logutils.LogRed("Error reading assetdir from devserver: %s", err.Error()) - } else { - assetDir = string(content) - } - resp.Body.Close() - } - } - - if assetDir != "" { - for thePath := range changedPaths { - if strings.HasPrefix(thePath, assetDir) { - reload = true - break - } - } - } else if len(dirsThatTriggerAReload) == 0 { - logutils.LogRed("Reloading couldn't be triggered: Please specify -assetdir or -reloaddirs") - } - } - if reload { - reload = false - _, err := http.Get(reloadURL) - if err != nil { - logutils.LogRed("Error during refresh: %s", err.Error()) - } - } - changedPaths = map[string]struct{}{} - case <-quitChannel: - logutils.LogGreen("\nCaught quit") - quit = true - } - } - return debugBinaryProcess -} - -func joinPath(url *url.URL, subPath string) string { - u := *url - u.Path = path.Join(u.Path, subPath) - return u.String() -} diff --git a/v2/cmd/wails/internal/commands/doctor/doctor.go b/v2/cmd/wails/internal/commands/doctor/doctor.go deleted file mode 100644 index 1cee9ac30..000000000 --- a/v2/cmd/wails/internal/commands/doctor/doctor.go +++ /dev/null @@ -1,174 +0,0 @@ -package doctor - -import ( - "fmt" - "io" - "os" - "runtime" - "runtime/debug" - "strings" - "text/tabwriter" - - "github.com/leaanthony/clir" - "github.com/wailsapp/wails/v2/internal/system" - "github.com/wailsapp/wails/v2/internal/system/packagemanager" - "github.com/wailsapp/wails/v2/pkg/clilogger" -) - -// AddSubcommand adds the `doctor` command for the Wails application -func AddSubcommand(app *clir.Cli, w io.Writer) error { - - command := app.NewSubCommand("doctor", "Diagnose your environment") - - command.Action(func() error { - - logger := clilogger.New(w) - - app.PrintBanner() - - logger.Print("Scanning system - Please wait (this may take a long time)...") - - // Get system info - info, err := system.GetInfo() - if err != nil { - logger.Println("Failed.") - return err - } - logger.Println("Done.") - - logger.Println("") - - // Start a new tabwriter - w := new(tabwriter.Writer) - w.Init(os.Stdout, 8, 8, 0, '\t', 0) - - // Write out the system information - fmt.Fprintf(w, "System\n") - fmt.Fprintf(w, "------\n") - fmt.Fprintf(w, "%s\t%s\n", "OS:", info.OS.Name) - fmt.Fprintf(w, "%s\t%s\n", "Version: ", info.OS.Version) - fmt.Fprintf(w, "%s\t%s\n", "ID:", info.OS.ID) - - // Output Go Information - fmt.Fprintf(w, "%s\t%s\n", "Go Version:", runtime.Version()) - fmt.Fprintf(w, "%s\t%s\n", "Platform:", runtime.GOOS) - fmt.Fprintf(w, "%s\t%s\n", "Architecture:", runtime.GOARCH) - - // Write out the wails information - fmt.Fprintf(w, "\n") - fmt.Fprintf(w, "Wails\n") - fmt.Fprintf(w, "------\n") - fmt.Fprintf(w, "%s\t%s\n", "Version: ", app.Version()) - - printBuildSettings(w) - - // Exit early if PM not found - if info.PM != nil { - fmt.Fprintf(w, "%s\t%s\n", "Package Manager: ", info.PM.Name()) - } - - // Output Dependencies Status - var dependenciesMissing = []string{} - var externalPackages = []*packagemanager.Dependency{} - var dependenciesAvailableRequired = 0 - var dependenciesAvailableOptional = 0 - fmt.Fprintf(w, "\n") - fmt.Fprintf(w, "Dependency\tPackage Name\tStatus\tVersion\n") - fmt.Fprintf(w, "----------\t------------\t------\t-------\n") - - hasOptionalDependencies := false - // Loop over dependencies - for _, dependency := range info.Dependencies { - - name := dependency.Name - if dependency.Optional { - name = "*" + name - hasOptionalDependencies = true - } - packageName := "Unknown" - status := "Not Found" - - // If we found the package - if dependency.PackageName != "" { - - packageName = dependency.PackageName - - // If it's installed, update the status - if dependency.Installed { - status = "Installed" - } else { - // Generate meaningful status text - status = "Available" - - if dependency.Optional { - dependenciesAvailableOptional++ - } else { - dependenciesAvailableRequired++ - } - } - } else { - if !dependency.Optional { - dependenciesMissing = append(dependenciesMissing, dependency.Name) - } - - if dependency.External { - externalPackages = append(externalPackages, dependency) - } - } - - fmt.Fprintf(w, "%s \t%s \t%s \t%s\n", name, packageName, status, dependency.Version) - } - if hasOptionalDependencies { - fmt.Fprintf(w, "\n") - fmt.Fprintf(w, "* - Optional Dependency\n") - } - w.Flush() - logger.Println("") - logger.Println("Diagnosis") - logger.Println("---------") - - // Generate an appropriate diagnosis - - if len(dependenciesMissing) == 0 && dependenciesAvailableRequired == 0 { - logger.Println("Your system is ready for Wails development!") - } else { - logger.Println("Your system has missing dependencies!\n") - } - - if dependenciesAvailableRequired != 0 { - logger.Println("Required package(s) installation details: \n" + info.Dependencies.InstallAllRequiredCommand()) - } - - if dependenciesAvailableOptional != 0 { - logger.Println("Optional package(s) installation details: \n" + info.Dependencies.InstallAllOptionalCommand()) - } - - if len(dependenciesMissing) != 0 { - logger.Println("Fatal:") - logger.Println("Required dependencies missing: " + strings.Join(dependenciesMissing, " ")) - logger.Println("Please read this article on how to resolve this: https://wails.io/guides/resolving-missing-packages") - } - - logger.Println("") - return nil - }) - - return nil -} - -func printBuildSettings(w *tabwriter.Writer) { - if buildInfo, _ := debug.ReadBuildInfo(); buildInfo != nil { - buildSettingToName := map[string]string{ - "vcs.revision": "Revision", - "vcs.modified": "Modified", - } - for _, buildSetting := range buildInfo.Settings { - name := buildSettingToName[buildSetting.Key] - if name == "" { - continue - } - - _, _ = fmt.Fprintf(w, "%s:\t%s\n", name, buildSetting.Value) - } - } -} diff --git a/v2/cmd/wails/internal/commands/generate/README.md b/v2/cmd/wails/internal/commands/generate/README.md deleted file mode 100644 index 5360ae791..000000000 --- a/v2/cmd/wails/internal/commands/generate/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Generate - -The `generate` command provides the ability to generate various Wails related components. - -## Usage - -`wails generate [subcommand] [options]` - -## Template - -`wails generate template -name [-frontend] [-q]` - -Generate a starter template for you to customise. - -| Flag | Details | -| :------------- | :----------- | -| -frontend | Copies all the files from the current directory into the template's `frontend` directory. Useful for converting frontend projects created by boilerplate generators. | -| -q | Suppress output | - - -## Module - -`wails generate module [-h]` - -Generate TS module for your frontend diff --git a/v2/cmd/wails/internal/commands/generate/generate.go b/v2/cmd/wails/internal/commands/generate/generate.go deleted file mode 100644 index c1b45e113..000000000 --- a/v2/cmd/wails/internal/commands/generate/generate.go +++ /dev/null @@ -1,23 +0,0 @@ -package generate - -import ( - "github.com/wailsapp/wails/v2/cmd/wails/internal/commands/generate/template" - "io" - - "github.com/leaanthony/clir" -) - -// AddSubcommand adds the `generate` command for the Wails application -func AddSubcommand(app *clir.Cli, w io.Writer) error { - - command := app.NewSubCommand("generate", "Code Generation Tools") - - err := AddModuleCommand(app, command, w) - if err != nil { - return err - } - - template.AddSubCommand(app, command, w) - - return nil -} diff --git a/v2/cmd/wails/internal/commands/generate/module.go b/v2/cmd/wails/internal/commands/generate/module.go deleted file mode 100644 index a46d7d0e3..000000000 --- a/v2/cmd/wails/internal/commands/generate/module.go +++ /dev/null @@ -1,51 +0,0 @@ -package generate - -import ( - "io" - "os" - - "github.com/leaanthony/clir" - "github.com/wailsapp/wails/v2/internal/project" - "github.com/wailsapp/wails/v2/pkg/commands/bindings" - "github.com/wailsapp/wails/v2/pkg/commands/buildtags" -) - -type generateFlags struct { - tags string -} - -// AddModuleCommand adds the `module` subcommand for the `generate` command -func AddModuleCommand(app *clir.Cli, parent *clir.Command, w io.Writer) error { - - command := parent.NewSubCommand("module", "Generate wailsjs modules") - genFlags := generateFlags{} - command.StringFlag("tags", "tags to pass to Go compiler (quoted and space separated)", &genFlags.tags) - - command.Action(func() error { - cwd, err := os.Getwd() - if err != nil { - return err - } - projectConfig, err := project.Load(cwd) - if err != nil { - return err - } - - buildTags, err := buildtags.Parse(genFlags.tags) - if err != nil { - return err - } - - _, err = bindings.GenerateBindings(bindings.Options{ - Tags: buildTags, - TsPrefix: projectConfig.Bindings.TsGeneration.Prefix, - TsSuffix: projectConfig.Bindings.TsGeneration.Suffix, - }) - if err != nil { - return err - } - - return nil - }) - return nil -} diff --git a/v2/cmd/wails/internal/commands/generate/template/template.go b/v2/cmd/wails/internal/commands/generate/template/template.go deleted file mode 100644 index 1f7af0e0d..000000000 --- a/v2/cmd/wails/internal/commands/generate/template/template.go +++ /dev/null @@ -1,214 +0,0 @@ -package template - -import ( - "embed" - "fmt" - "io" - "os" - "path/filepath" - - "github.com/leaanthony/debme" - - "github.com/leaanthony/gosod" - "github.com/wailsapp/wails/v2/internal/fs" - - "github.com/leaanthony/clir" - "github.com/tidwall/sjson" -) - -//go:embed base -var base embed.FS - -func AddSubCommand(app *clir.Cli, parent *clir.Command, w io.Writer) { - - // command - command := parent.NewSubCommand("template", "Generates a wails template") - - name := "" - command.StringFlag("name", "The name of the template", &name) - - existingProjectDir := "" - command.StringFlag("frontend", "A path to an existing frontend project to include in the template", &existingProjectDir) - - // Quiet Init - quiet := false - command.BoolFlag("q", "Suppress output to console", &quiet) - - command.Action(func() error { - - // name is mandatory - if name == "" { - command.PrintHelp() - return fmt.Errorf("no template name given") - } - - // If the current directory is not empty, we create a new directory - cwd, err := os.Getwd() - if err != nil { - return err - } - templateDir := filepath.Join(cwd, name) - if !fs.DirExists(templateDir) { - err := os.MkdirAll(templateDir, 0755) - if err != nil { - return err - } - } - empty, err := fs.DirIsEmpty(templateDir) - if err != nil { - return err - } - if !empty { - templateDir = filepath.Join(cwd, name) - println("Creating new template directory:", name) - err = fs.Mkdir(templateDir) - if err != nil { - return err - } - } - - // Create base template - baseTemplate, err := debme.FS(base, "base") - if err != nil { - return err - } - g := gosod.New(baseTemplate) - g.SetTemplateFilters([]string{".template"}) - - err = os.Chdir(templateDir) - if err != nil { - return err - } - - type templateData struct { - Name string - Description string - TemplateDir string - WailsVersion string - } - - println("Extracting base template files...") - - err = g.Extract(templateDir, &templateData{ - Name: name, - TemplateDir: templateDir, - WailsVersion: app.Version(), - }) - if err != nil { - return err - } - - err = os.Chdir(cwd) - if err != nil { - return err - } - - // If we aren't migrating the files, just exit - if existingProjectDir == "" { - return nil - } - - // Remove frontend directory - frontendDir := filepath.Join(templateDir, "frontend") - err = os.RemoveAll(frontendDir) - if err != nil { - return err - } - - // Copy the files into a new frontend directory - println("Migrating existing project files to frontend directory...") - - sourceDir, err := filepath.Abs(existingProjectDir) - if err != nil { - return err - } - - newFrontendDir := filepath.Join(templateDir, "frontend") - err = fs.CopyDirExtended(sourceDir, newFrontendDir, []string{name, "node_modules"}) - if err != nil { - return err - } - - // Process package.json - err = processPackageJSON(frontendDir) - if err != nil { - return err - } - - // Process package-lock.json - err = processPackageLockJSON(frontendDir) - if err != nil { - return err - } - - // Remove node_modules - ignore error, eg it doesn't exist - _ = os.RemoveAll(filepath.Join(frontendDir, "node_modules")) - - return nil - - }) -} - -func processPackageJSON(frontendDir string) error { - var err error - - packageJSON := filepath.Join(frontendDir, "package.json") - if !fs.FileExists(packageJSON) { - println("No package.json found - cannot process.") - return nil - } - - json, err := os.ReadFile(packageJSON) - if err != nil { - return err - } - - // We will ignore these errors - it's not critical - println("Updating package.json data...") - json, _ = sjson.SetBytes(json, "name", "{{.ProjectName}}") - json, _ = sjson.SetBytes(json, "author", "{{.AuthorName}}") - - err = os.WriteFile(packageJSON, json, 0644) - if err != nil { - return err - } - baseDir := filepath.Dir(packageJSON) - println("Renaming package.json -> package.tmpl.json...") - err = os.Rename(packageJSON, filepath.Join(baseDir, "package.tmpl.json")) - if err != nil { - return err - } - return nil -} - -func processPackageLockJSON(frontendDir string) error { - var err error - - filename := filepath.Join(frontendDir, "package-lock.json") - if !fs.FileExists(filename) { - println("No package-lock.json found - cannot process.") - return nil - } - - data, err := os.ReadFile(filename) - if err != nil { - return err - } - json := string(data) - - // We will ignore these errors - it's not critical - println("Updating package-lock.json data...") - json, _ = sjson.Set(json, "name", "{{.ProjectName}}") - - err = os.WriteFile(filename, []byte(json), 0644) - if err != nil { - return err - } - baseDir := filepath.Dir(filename) - println("Renaming package-lock.json -> package-lock.tmpl.json...") - err = os.Rename(filename, filepath.Join(baseDir, "package-lock.tmpl.json")) - if err != nil { - return err - } - return nil -} diff --git a/v2/cmd/wails/internal/commands/initialise/initialise.go b/v2/cmd/wails/internal/commands/initialise/initialise.go deleted file mode 100644 index 212743906..000000000 --- a/v2/cmd/wails/internal/commands/initialise/initialise.go +++ /dev/null @@ -1,310 +0,0 @@ -package initialise - -import ( - "bufio" - "fmt" - "io" - "log" - "os" - "os/exec" - "path/filepath" - "strings" - "time" - - "github.com/flytam/filenamify" - "github.com/leaanthony/slicer" - - "github.com/wailsapp/wails/v2/pkg/buildassets" - - "github.com/wailsapp/wails/v2/pkg/templates" - - "github.com/leaanthony/clir" - "github.com/pkg/errors" - "github.com/wailsapp/wails/v2/pkg/clilogger" - "github.com/wailsapp/wails/v2/pkg/git" -) - -// AddSubcommand adds the `init` command for the Wails application -func AddSubcommand(app *clir.Cli, w io.Writer) error { - - command := app.NewSubCommand("init", "Initialise a new Wails project") - - // Setup template name flag - templateName := "vanilla" - description := "Name of built-in template to use, path to template or template url." - command.StringFlag("t", description, &templateName) - - // Setup project name - projectName := "" - command.StringFlag("n", "Name of project", &projectName) - - // For CI - ciMode := false - command.BoolFlag("ci", "CI Mode", &ciMode) - - // Setup project directory - projectDirectory := "" - command.StringFlag("d", "Project directory", &projectDirectory) - - // Quiet Init - quiet := false - command.BoolFlag("q", "Suppress output to console", &quiet) - - initGit := false - gitInstalled := git.IsInstalled() - if gitInstalled { - // Git Init - command.BoolFlag("g", "Initialise git repository", &initGit) - } - - // VSCode project files - ide := "" - command.StringFlag("ide", "Generate IDE project files", &ide) - - // List templates - list := false - command.BoolFlag("l", "List templates", &list) - - command.Action(func() error { - - // Create logger - logger := clilogger.New(w) - logger.Mute(quiet) - - // Are we listing templates? - if list { - app.PrintBanner() - err := templates.OutputList(logger) - logger.Println("") - return err - } - - // Validate name - if len(projectName) == 0 { - logger.Println("ERROR: Project name required") - logger.Println("") - command.PrintHelp() - return nil - } - - // Validate IDE option - supportedIDEs := slicer.String([]string{"vscode", "goland"}) - ide = strings.ToLower(ide) - if ide != "" { - if !supportedIDEs.Contains(ide) { - return fmt.Errorf("ide '%s' not supported. Valid values: %s", ide, supportedIDEs.Join(" ")) - } - } - - if !quiet { - app.PrintBanner() - } - - task := fmt.Sprintf("Initialising Project '%s'", projectName) - logger.Println(task) - logger.Println(strings.Repeat("-", len(task))) - - projectFilename, err := filenamify.Filenamify(projectName, filenamify.Options{ - Replacement: "_", - MaxLength: 255, - }) - if err != nil { - return err - } - goBinary, err := exec.LookPath("go") - if err != nil { - return fmt.Errorf("unable to find Go compiler. Please download and install Go: https://golang.org/dl/") - } - - // Get base path and convert to forward slashes - goPath := filepath.ToSlash(filepath.Dir(goBinary)) - // Trim bin directory - goSDKPath := strings.TrimSuffix(goPath, "/bin") - - // Create Template Options - options := &templates.Options{ - ProjectName: projectName, - TargetDir: projectDirectory, - TemplateName: templateName, - Logger: logger, - IDE: ide, - InitGit: initGit, - ProjectNameFilename: projectFilename, - WailsVersion: app.Version(), - GoSDKPath: goSDKPath, - } - - // Try to discover author details from git config - findAuthorDetails(options) - - return initProject(options, quiet, ciMode) - }) - - return nil -} - -// initProject is our main init command -func initProject(options *templates.Options, quiet bool, ciMode bool) error { - - // Start Time - start := time.Now() - - // Install the template - remote, template, err := templates.Install(options) - if err != nil { - return err - } - - // Install the default assets - err = buildassets.Install(options.TargetDir) - if err != nil { - return err - } - - err = os.Chdir(options.TargetDir) - if err != nil { - return err - } - - if !ciMode { - // Run `go mod tidy` to ensure `go.sum` is up to date - cmd := exec.Command("go", "mod", "tidy") - cmd.Dir = options.TargetDir - cmd.Stderr = os.Stderr - if !quiet { - println("") - cmd.Stdout = os.Stdout - } - err = cmd.Run() - if err != nil { - return err - } - } else { - // Update go mod - workspace := os.Getenv("GITHUB_WORKSPACE") - println("GitHub workspace:", workspace) - if workspace == "" { - os.Exit(1) - } - updateReplaceLine(workspace) - } - - // Remove the `.git`` directory in the template project - err = os.RemoveAll(".git") - if err != nil { - return err - } - - if options.InitGit { - err = initGit(options) - if err != nil { - return err - } - } - - if quiet { - return nil - } - - // Output stats - elapsed := time.Since(start) - options.Logger.Println("Project Name: " + options.ProjectName) - options.Logger.Println("Project Directory: " + options.TargetDir) - options.Logger.Println("Project Template: " + options.TemplateName) - options.Logger.Println("Template Support: " + template.HelpURL) - - // IDE message - switch options.IDE { - case "vscode": - options.Logger.Println("VSCode config files generated.") - case "goland": - options.Logger.Println("Goland config files generated.") - } - - if options.InitGit { - options.Logger.Println("Git repository initialised.") - } - - if remote { - options.Logger.Println("\nNOTE: You have created a project using a remote template. The Wails project takes no responsibility for 3rd party templates. Only use remote templates that you trust.") - } - - options.Logger.Println("") - options.Logger.Println(fmt.Sprintf("Initialised project '%s' in %s.", options.ProjectName, elapsed.Round(time.Millisecond).String())) - options.Logger.Println("") - - return nil -} - -func initGit(options *templates.Options) error { - err := git.InitRepo(options.TargetDir) - if err != nil { - return errors.Wrap(err, "Unable to initialise git repository:") - } - - ignore := []string{ - "build/bin", - "frontend/dist", - "frontend/node_modules", - } - err = os.WriteFile(filepath.Join(options.TargetDir, ".gitignore"), []byte(strings.Join(ignore, "\n")), 0644) - if err != nil { - return errors.Wrap(err, "Unable to create gitignore") - } - - return nil -} - -// findAuthorDetails tries to find the user's name and email -// from gitconfig. If it finds them, it stores them in the project options -func findAuthorDetails(options *templates.Options) { - if git.IsInstalled() { - name, err := git.Name() - if err == nil { - options.AuthorName = strings.TrimSpace(name) - } - - email, err := git.Email() - if err == nil { - options.AuthorEmail = strings.TrimSpace(email) - } - } -} - -func updateReplaceLine(targetPath string) { - file, err := os.Open("go.mod") - if err != nil { - log.Fatal(err) - } - - var lines []string - scanner := bufio.NewScanner(file) - for scanner.Scan() { - lines = append(lines, scanner.Text()) - } - - err = file.Close() - if err != nil { - log.Fatal(err) - } - - if err := scanner.Err(); err != nil { - log.Fatal(err) - } - - for i, line := range lines { - println(line) - if strings.HasPrefix(line, "// replace") { - println("Found replace line") - splitLine := strings.Split(line, " ") - splitLine[5] = targetPath + "/v2" - lines[i] = strings.Join(splitLine[1:], " ") - continue - } - } - - err = os.WriteFile("go.mod", []byte(strings.Join(lines, "\n")), 0644) - if err != nil { - log.Fatal(err) - } -} diff --git a/v2/cmd/wails/internal/commands/initialise/templates/generate/plain/main.go.tmpl b/v2/cmd/wails/internal/commands/initialise/templates/generate/plain/main.go.tmpl deleted file mode 100644 index 433cfee4e..000000000 --- a/v2/cmd/wails/internal/commands/initialise/templates/generate/plain/main.go.tmpl +++ /dev/null @@ -1,88 +0,0 @@ -package main - -import ( - "embed" - "log" - - "github.com/wailsapp/wails/v2/pkg/options/mac" - - "github.com/wailsapp/wails/v2" - "github.com/wailsapp/wails/v2/pkg/logger" - "github.com/wailsapp/wails/v2/pkg/options" - "github.com/wailsapp/wails/v2/pkg/options/assetserver" - "github.com/wailsapp/wails/v2/pkg/options/windows" -) - -//go:embed all:frontend/src -var assets embed.FS - -//go:embed build/appicon.png -var icon []byte - -func main() { - // Create an instance of the app structure - app := NewApp() - - // Create application with options - err := wails.Run(&options.App{ - Title: "{{.ProjectName}}", - Width: 1024, - Height: 768, - MinWidth: 1024, - MinHeight: 768, - MaxWidth: 1280, - MaxHeight: 800, - DisableResize: false, - Fullscreen: false, - Frameless: false, - StartHidden: false, - HideWindowOnClose: false, - BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, - AssetServer: &assetserver.Options{ - Assets: assets, - }, - Menu: nil, - Logger: nil, - LogLevel: logger.DEBUG, - OnStartup: app.startup, - OnDomReady: app.domReady, - OnBeforeClose: app.beforeClose, - OnShutdown: app.shutdown, - WindowStartState: options.Normal, - Bind: []interface{}{ - app, - }, - // Windows platform specific options - Windows: &windows.Options{ - WebviewIsTransparent: false, - WindowIsTranslucent: false, - DisableWindowIcon: false, - // DisableFramelessWindowDecorations: false, - WebviewUserDataPath: "", - IsZoomControlEnabled: false, - ZoomFactor: float64, - }, - Mac: &mac.Options{ - TitleBar: &mac.TitleBar{ - TitlebarAppearsTransparent: true, - HideTitle: false, - HideTitleBar: false, - FullSizeContent: false, - UseToolbar: false, - HideToolbarSeparator: true, - }, - Appearance: mac.NSAppearanceNameDarkAqua, - WebviewIsTransparent: true, - WindowIsTranslucent: true, - About: &mac.AboutInfo{ - Title: "Plain Template", - Message: "Part of the Wails projects", - Icon: icon, - }, - }, - }) - - if err != nil { - log.Fatal(err) - } -} diff --git a/v2/cmd/wails/internal/commands/initialise/templates/templates/vue/main.tmpl.go b/v2/cmd/wails/internal/commands/initialise/templates/templates/vue/main.tmpl.go deleted file mode 100644 index e24782be3..000000000 --- a/v2/cmd/wails/internal/commands/initialise/templates/templates/vue/main.tmpl.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "embed" - - "github.com/wailsapp/wails/v2" - "github.com/wailsapp/wails/v2/pkg/options" - "github.com/wailsapp/wails/v2/pkg/options/assetserver" -) - -//go:embed all:frontend/dist -var assets embed.FS - -func main() { - // Create an instance of the app structure - app := NewApp() - - // Create application with options - err := wails.Run(&options.App{ - Title: "{{.ProjectName}}", - Width: 1024, - Height: 768, - AssetServer: &assetserver.Options{ - Assets: assets, - }, - BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, - OnStartup: app.startup, - Bind: []interface{}{ - app, - }, - }) - - if err != nil { - println("Error:", err.Error()) - } -} diff --git a/v2/cmd/wails/internal/commands/show/show.go b/v2/cmd/wails/internal/commands/show/show.go deleted file mode 100644 index 54c4180fb..000000000 --- a/v2/cmd/wails/internal/commands/show/show.go +++ /dev/null @@ -1,24 +0,0 @@ -package show - -import ( - "fmt" - "github.com/leaanthony/clir" - "github.com/wailsapp/wails/v2/cmd/wails/internal" - "github.com/wailsapp/wails/v2/internal/github" - "io" -) - -// AddSubcommand adds the `show` command for the Wails application -func AddSubcommand(app *clir.Cli, w io.Writer) { - showCommand := app.NewSubCommand("show", "Shows various information") - - version := internal.Version - releaseNotes := showCommand.NewSubCommand("releasenotes", "Shows the release notes for the current version") - releaseNotes.StringFlag("version", "The version to show the release notes for", &version) - releaseNotes.Action(func() error { - app.PrintBanner() - releaseNotes := github.GetReleaseNotes(version) - _, _ = fmt.Fprintln(w, releaseNotes) - return nil - }) -} diff --git a/v2/cmd/wails/internal/commands/update/update.go b/v2/cmd/wails/internal/commands/update/update.go deleted file mode 100644 index a6a5dd020..000000000 --- a/v2/cmd/wails/internal/commands/update/update.go +++ /dev/null @@ -1,172 +0,0 @@ -package update - -import ( - "fmt" - "github.com/labstack/gommon/color" - "github.com/wailsapp/wails/v2/internal/shell" - "io" - "log" - "os" - - "github.com/wailsapp/wails/v2/internal/github" - - "github.com/leaanthony/clir" - "github.com/wailsapp/wails/v2/pkg/clilogger" -) - -// AddSubcommand adds the `init` command for the Wails application -func AddSubcommand(app *clir.Cli, w io.Writer, currentVersion string) error { - - command := app.NewSubCommand("update", "Update the Wails CLI") - command.LongDescription(`This command allows you to update your version of the Wails CLI.`) - - // Setup flags - var prereleaseRequired bool - command.BoolFlag("pre", "Update CLI to latest Prerelease", &prereleaseRequired) - - var specificVersion string - command.StringFlag("version", "Install a specific version (Overrides other flags) of the CLI", &specificVersion) - - command.Action(func() error { - - // Create logger - logger := clilogger.New(w) - - // Print banner - app.PrintBanner() - logger.Println("Checking for updates...") - - var desiredVersion *github.SemanticVersion - var err error - var valid bool - - if len(specificVersion) > 0 { - // Check if this is a valid version - valid, err = github.IsValidTag(specificVersion) - if err == nil { - if !valid { - err = fmt.Errorf("version '%s' is invalid", specificVersion) - } else { - desiredVersion, err = github.NewSemanticVersion(specificVersion) - } - } - } else { - if prereleaseRequired { - desiredVersion, err = github.GetLatestPreRelease() - } else { - desiredVersion, err = github.GetLatestStableRelease() - if err != nil { - println("") - println("No stable release found for this major version. To update to the latest pre-release (eg beta), run:") - println(" wails update -pre") - return nil - } - } - } - if err != nil { - return err - } - fmt.Println() - - fmt.Println(" Current Version : " + currentVersion) - - if len(specificVersion) > 0 { - fmt.Printf(" Desired Version : v%s\n", desiredVersion) - } else { - if prereleaseRequired { - fmt.Printf(" Latest Prerelease : v%s\n", desiredVersion) - } else { - fmt.Printf(" Latest Release : v%s\n", desiredVersion) - } - } - - return updateToVersion(logger, desiredVersion, len(specificVersion) > 0, currentVersion) - }) - - return nil -} - -func updateToVersion(logger *clilogger.CLILogger, targetVersion *github.SemanticVersion, force bool, currentVersion string) error { - - var targetVersionString = "v" + targetVersion.String() - - // Early exit - if targetVersionString == currentVersion { - logger.Println("\nLooks like you're up to date!") - return nil - } - - var desiredVersion string - - if !force { - - compareVersion := currentVersion - - currentVersion, err := github.NewSemanticVersion(compareVersion) - if err != nil { - return err - } - - var success bool - - // Release -> Pre-Release = Massage current version to prerelease format - if targetVersion.IsPreRelease() && currentVersion.IsRelease() { - testVersion, err := github.NewSemanticVersion(compareVersion + "-0") - if err != nil { - return err - } - success, _ = targetVersion.IsGreaterThan(testVersion) - } - // Pre-Release -> Release = Massage target version to prerelease format - if targetVersion.IsRelease() && currentVersion.IsPreRelease() { - // We are ok with greater than or equal - mainversion := currentVersion.MainVersion() - targetVersion, err = github.NewSemanticVersion(targetVersion.String()) - if err != nil { - return err - } - success, _ = targetVersion.IsGreaterThanOrEqual(mainversion) - } - - // Release -> Release = Standard check - if (targetVersion.IsRelease() && currentVersion.IsRelease()) || - (targetVersion.IsPreRelease() && currentVersion.IsPreRelease()) { - - success, _ = targetVersion.IsGreaterThan(currentVersion) - } - - // Compare - if !success { - logger.Println("Error: The requested version is lower than the current version.") - logger.Println("If this is what you really want to do, use `wails update -version %s`", targetVersionString) - return nil - } - - desiredVersion = "v" + targetVersion.String() - - } else { - desiredVersion = "v" + targetVersion.String() - } - - fmt.Println() - logger.Print("Installing Wails CLI " + desiredVersion + "...") - - // Run command in non module directory - homeDir, err := os.UserHomeDir() - if err != nil { - log.Fatal("Cannot find home directory! Please file a bug report!") - } - - sout, serr, err := shell.RunCommand(homeDir, "go", "install", "github.com/wailsapp/wails/v2/cmd/wails@"+desiredVersion) - if err != nil { - logger.Println("Failed.") - logger.Println(sout + `\n` + serr) - return err - } - logger.Println("Done.") - logger.Println(color.Green("\nMake sure you update your project go.mod file to use " + desiredVersion + ":")) - logger.Println(color.Green(" require github.com/wailsapp/wails/v2 " + desiredVersion)) - logger.Println(color.Red("\nTo view the release notes, please run `wails show releasenotes`")) - - return nil -} diff --git a/v2/cmd/wails/internal/dev/dev.go b/v2/cmd/wails/internal/dev/dev.go new file mode 100644 index 000000000..2cd11510b --- /dev/null +++ b/v2/cmd/wails/internal/dev/dev.go @@ -0,0 +1,530 @@ +package dev + +import ( + "context" + "errors" + "fmt" + "github.com/samber/lo" + "github.com/wailsapp/wails/v2/cmd/wails/flags" + "github.com/wailsapp/wails/v2/cmd/wails/internal/gomod" + "github.com/wailsapp/wails/v2/cmd/wails/internal/logutils" + "io" + "net/http" + "net/url" + "os" + "os/exec" + "os/signal" + "path" + "path/filepath" + "strings" + "sync" + "sync/atomic" + "syscall" + "time" + + "github.com/wailsapp/wails/v2/pkg/commands/bindings" + "github.com/wailsapp/wails/v2/pkg/commands/buildtags" + + "github.com/google/shlex" + + "github.com/pkg/browser" + + "github.com/fsnotify/fsnotify" + "github.com/wailsapp/wails/v2/internal/fs" + "github.com/wailsapp/wails/v2/internal/process" + "github.com/wailsapp/wails/v2/pkg/clilogger" + "github.com/wailsapp/wails/v2/pkg/commands/build" +) + +func sliceToMap(input []string) map[string]struct{} { + result := map[string]struct{}{} + for _, value := range input { + result[value] = struct{}{} + } + return result +} + +type devFlags struct { + ldflags string + compilerCommand string + assetDir string + extensions string + reloadDirs string + openBrowser bool + noReload bool + skipBindings bool + wailsjsdir string + tags string + verbosity int + loglevel string + forceBuild bool + debounceMS int + devServer string + appargs string + saveConfig bool + raceDetector bool + + frontendDevServerURL string + skipFrontend bool + noColour bool +} + +// Application runs the application in dev mode +func Application(f *flags.Dev, logger *clilogger.CLILogger) error { + + cwd := lo.Must(os.Getwd()) + + // Update go.mod to use current wails version + err := gomod.SyncGoMod(logger, true) + if err != nil { + return err + } + + // Run go mod tidy to ensure we're up-to-date + err = runCommand(cwd, false, "go", "mod", "tidy") + if err != nil { + return err + } + + buildOptions := f.GenerateBuildOptions() + buildOptions.Logger = logger + + userTags, err := buildtags.Parse(f.Tags) + if err != nil { + return err + } + + buildOptions.UserTags = userTags + + err = build.CreateEmbedDirectories(cwd, buildOptions) + if err != nil { + return err + } + + projectConfig := f.ProjectConfig() + + if !buildOptions.SkipBindings { + if f.Verbosity == build.VERBOSE { + logutils.LogGreen("Generating Bindings...") + } + stdout, err := bindings.GenerateBindings(bindings.Options{ + Tags: buildOptions.UserTags, + TsPrefix: projectConfig.Bindings.TsGeneration.Prefix, + TsSuffix: projectConfig.Bindings.TsGeneration.Suffix, + }) + if err != nil { + return err + } + if f.Verbosity == build.VERBOSE { + logutils.LogGreen(stdout) + } + } + + // Setup signal handler + quitChannel := make(chan os.Signal, 1) + signal.Notify(quitChannel, os.Interrupt, os.Kill, syscall.SIGTERM) + exitCodeChannel := make(chan int, 1) + + // Build the frontend if requested, but ignore building the application itself. + ignoreFrontend := buildOptions.IgnoreFrontend + if !ignoreFrontend { + buildOptions.IgnoreApplication = true + if _, err := build.Build(buildOptions); err != nil { + return err + } + buildOptions.IgnoreApplication = false + } + + // frontend:dev:watcher command. + frontendDevAutoDiscovery := projectConfig.IsFrontendDevServerURLAutoDiscovery() + if command := projectConfig.DevWatcherCommand; command != "" { + closer, devServerURL, err := runFrontendDevWatcherCommand(projectConfig.GetFrontendDir(), command, frontendDevAutoDiscovery) + if err != nil { + return err + } + if devServerURL != "" { + projectConfig.FrontendDevServerURL = devServerURL + f.FrontendDevServerURL = devServerURL + } + defer closer() + } else if frontendDevAutoDiscovery { + return fmt.Errorf("unable to auto discover frontend:dev:serverUrl without a frontend:dev:watcher command, please either set frontend:dev:watcher or remove the auto discovery from frontend:dev:serverUrl") + } + + // Do initial build but only for the application. + logger.Println("Building application for development...") + buildOptions.IgnoreFrontend = true + debugBinaryProcess, appBinary, err := restartApp(buildOptions, nil, f, exitCodeChannel) + buildOptions.IgnoreFrontend = ignoreFrontend || f.FrontendDevServerURL != "" + if err != nil { + return err + } + defer func() { + if err := killProcessAndCleanupBinary(debugBinaryProcess, appBinary); err != nil { + logutils.LogDarkYellow("Unable to kill process and cleanup binary: %s", err) + } + }() + + // open browser + if f.Browser { + err = browser.OpenURL(f.DevServerURL().String()) + if err != nil { + return err + } + } + + // create the project files watcher + watcher, err := initialiseWatcher(cwd) + if err != nil { + return err + } + + defer func(watcher *fsnotify.Watcher) { + err := watcher.Close() + if err != nil { + logger.Fatal(err.Error()) + } + }(watcher) + + logutils.LogGreen("Watching (sub)/directory: %s", cwd) + logutils.LogGreen("Using DevServer URL: %s", f.DevServerURL()) + if f.FrontendDevServerURL != "" { + logutils.LogGreen("Using Frontend DevServer URL: %s", f.FrontendDevServerURL) + } + logutils.LogGreen("Using reload debounce setting of %d milliseconds", f.Debounce) + + // Show dev server URL in terminal after 3 seconds + go func() { + time.Sleep(3 * time.Second) + logutils.LogGreen("\n\nTo develop in the browser and call your bound Go methods from Javascript, navigate to: %s", f.DevServerURL()) + }() + + // Watch for changes and trigger restartApp() + debugBinaryProcess = doWatcherLoop(buildOptions, debugBinaryProcess, f, watcher, exitCodeChannel, quitChannel, f.DevServerURL()) + + // Kill the current program if running and remove dev binary + if err := killProcessAndCleanupBinary(debugBinaryProcess, appBinary); err != nil { + return err + } + + // Reset the process and the binary so defer knows about it and is a nop. + debugBinaryProcess = nil + appBinary = "" + + logutils.LogGreen("Development mode exited") + + return nil +} + +func killProcessAndCleanupBinary(process *process.Process, binary string) error { + if process != nil && process.Running { + if err := process.Kill(); err != nil { + return err + } + } + + if binary != "" { + err := os.Remove(binary) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + } + return nil +} + +func runCommand(dir string, exitOnError bool, command string, args ...string) error { + logutils.LogGreen("Executing: " + command + " " + strings.Join(args, " ")) + cmd := exec.Command(command, args...) + cmd.Dir = dir + output, err := cmd.CombinedOutput() + if err != nil { + println(string(output)) + println(err.Error()) + if exitOnError { + os.Exit(1) + } + return err + } + return nil +} + +// runFrontendDevWatcherCommand will run the `frontend:dev:watcher` command if it was given, ex- `npm run dev` +func runFrontendDevWatcherCommand(frontendDirectory string, devCommand string, discoverViteServerURL bool) (func(), string, error) { + ctx, cancel := context.WithCancel(context.Background()) + scanner := NewStdoutScanner() + cmdSlice := strings.Split(devCommand, " ") + cmd := exec.CommandContext(ctx, cmdSlice[0], cmdSlice[1:]...) + cmd.Stderr = os.Stderr + cmd.Stdout = scanner + cmd.Dir = frontendDirectory + setParentGID(cmd) + + if err := cmd.Start(); err != nil { + cancel() + return nil, "", fmt.Errorf("unable to start frontend DevWatcher: %w", err) + } + + var viteServerURL string + if discoverViteServerURL { + select { + case serverURL := <-scanner.ViteServerURLChan: + viteServerURL = serverURL + case <-time.After(time.Second * 10): + cancel() + return nil, "", errors.New("failed to find Vite server URL") + } + } + + logutils.LogGreen("Running frontend DevWatcher command: '%s'", devCommand) + var wg sync.WaitGroup + wg.Add(1) + + const ( + stateRunning int32 = 0 + stateCanceling = 1 + stateStopped = 2 + ) + state := stateRunning + go func() { + if err := cmd.Wait(); err != nil { + wasRunning := atomic.CompareAndSwapInt32(&state, stateRunning, stateStopped) + if err.Error() != "exit status 1" && wasRunning { + logutils.LogRed("Error from DevWatcher '%s': %s", devCommand, err.Error()) + } + } + atomic.StoreInt32(&state, stateStopped) + wg.Done() + }() + + return func() { + if atomic.CompareAndSwapInt32(&state, stateRunning, stateCanceling) { + killProc(cmd, devCommand) + } + cancel() + wg.Wait() + }, viteServerURL, nil +} + +// restartApp does the actual rebuilding of the application when files change +func restartApp(buildOptions *build.Options, debugBinaryProcess *process.Process, f *flags.Dev, exitCodeChannel chan int) (*process.Process, string, error) { + + appBinary, err := build.Build(buildOptions) + println() + if err != nil { + logutils.LogRed("Build error - " + err.Error()) + + msg := "Continuing to run current version" + if debugBinaryProcess == nil { + msg = "No version running, build will be retriggered as soon as changes have been detected" + } + logutils.LogDarkYellow(msg) + return nil, "", nil + } + + // Kill existing binary if need be + if debugBinaryProcess != nil { + killError := debugBinaryProcess.Kill() + + if killError != nil { + buildOptions.Logger.Fatal("Unable to kill debug binary (PID: %d)!", debugBinaryProcess.PID()) + } + + debugBinaryProcess = nil + } + + // parse appargs if any + args, err := shlex.Split(f.AppArgs) + + if err != nil { + buildOptions.Logger.Fatal("Unable to parse appargs: %s", err.Error()) + } + + // Set environment variables accordingly + os.Setenv("loglevel", f.LogLevel) + os.Setenv("assetdir", f.AssetDir) + os.Setenv("devserver", f.DevServer) + os.Setenv("frontenddevserverurl", f.FrontendDevServerURL) + + // Start up new binary with correct args + newProcess := process.NewProcess(appBinary, args...) + err = newProcess.Start(exitCodeChannel) + if err != nil { + // Remove binary + if fs.FileExists(appBinary) { + deleteError := fs.DeleteFile(appBinary) + if deleteError != nil { + buildOptions.Logger.Fatal("Unable to delete app binary: " + appBinary) + } + } + buildOptions.Logger.Fatal("Unable to start application: %s", err.Error()) + } + + return newProcess, appBinary, nil +} + +// doWatcherLoop is the main watch loop that runs while dev is active +func doWatcherLoop(buildOptions *build.Options, debugBinaryProcess *process.Process, f *flags.Dev, watcher *fsnotify.Watcher, exitCodeChannel chan int, quitChannel chan os.Signal, devServerURL *url.URL) *process.Process { + // Main Loop + var extensionsThatTriggerARebuild = sliceToMap(strings.Split(f.Extensions, ",")) + var dirsThatTriggerAReload []string + for _, dir := range strings.Split(f.ReloadDirs, ",") { + if dir == "" { + continue + } + thePath, err := filepath.Abs(dir) + if err != nil { + logutils.LogRed("Unable to expand reloadDir '%s': %s", dir, err) + continue + } + dirsThatTriggerAReload = append(dirsThatTriggerAReload, thePath) + } + + quit := false + interval := time.Duration(f.Debounce) * time.Millisecond + timer := time.NewTimer(interval) + rebuild := false + reload := false + assetDir := "" + changedPaths := map[string]struct{}{} + + // If we are using an external dev server, the reloading of the frontend part can be skipped or if the user requested it + skipAssetsReload := f.FrontendDevServerURL != "" || f.NoReload + + assetDirURL := joinPath(devServerURL, "/wails/assetdir") + reloadURL := joinPath(devServerURL, "/wails/reload") + for quit == false { + // reload := false + select { + case exitCode := <-exitCodeChannel: + if exitCode == 0 { + quit = true + } + case err := <-watcher.Errors: + logutils.LogDarkYellow(err.Error()) + case item := <-watcher.Events: + isEligibleFile := func(fileName string) bool { + // Iterate all file patterns + ext := filepath.Ext(fileName) + if ext != "" { + ext = ext[1:] + if _, exists := extensionsThatTriggerARebuild[ext]; exists { + return true + } + } + return false + } + + // Handle write operations + if item.Op&fsnotify.Write == fsnotify.Write { + // Ignore directories + itemName := item.Name + if fs.DirExists(itemName) { + continue + } + + if isEligibleFile(itemName) { + rebuild = true + timer.Reset(interval) + continue + } + + for _, reloadDir := range dirsThatTriggerAReload { + if strings.HasPrefix(itemName, reloadDir) { + reload = true + break + } + } + + if !reload { + changedPaths[filepath.Dir(itemName)] = struct{}{} + } + + timer.Reset(interval) + } + + // Handle new fs entries that are created + if item.Op&fsnotify.Create == fsnotify.Create { + // If this is a folder, add it to our watch list + if fs.DirExists(item.Name) { + // node_modules is BANNED! + if !strings.Contains(item.Name, "node_modules") { + err := watcher.Add(item.Name) + if err != nil { + buildOptions.Logger.Fatal("%s", err.Error()) + } + logutils.LogGreen("Added new directory to watcher: %s", item.Name) + } + } else if isEligibleFile(item.Name) { + // Handle creation of new file. + // Note: On some platforms an update to a file is represented as + // REMOVE -> CREATE instead of WRITE, so this is not only new files + // but also updates to existing files + rebuild = true + timer.Reset(interval) + continue + } + } + case <-timer.C: + if rebuild { + rebuild = false + logutils.LogGreen("[Rebuild triggered] files updated") + // Try and build the app + newBinaryProcess, _, err := restartApp(buildOptions, debugBinaryProcess, f, exitCodeChannel) + if err != nil { + logutils.LogRed("Error during build: %s", err.Error()) + continue + } + // If we have a new process, saveConfig it + if newBinaryProcess != nil { + debugBinaryProcess = newBinaryProcess + } + } + + if !skipAssetsReload && len(changedPaths) != 0 { + if assetDir == "" { + resp, err := http.Get(assetDirURL) + if err != nil { + logutils.LogRed("Error during retrieving assetdir: %s", err.Error()) + } else { + content, err := io.ReadAll(resp.Body) + if err != nil { + logutils.LogRed("Error reading assetdir from devserver: %s", err.Error()) + } else { + assetDir = string(content) + } + resp.Body.Close() + } + } + + if assetDir != "" { + for thePath := range changedPaths { + if strings.HasPrefix(thePath, assetDir) { + reload = true + break + } + } + } else if len(dirsThatTriggerAReload) == 0 { + logutils.LogRed("Reloading couldn't be triggered: Please specify -assetdir or -reloaddirs") + } + } + if reload { + reload = false + _, err := http.Get(reloadURL) + if err != nil { + logutils.LogRed("Error during refresh: %s", err.Error()) + } + } + changedPaths = map[string]struct{}{} + case <-quitChannel: + logutils.LogGreen("\nCaught quit") + quit = true + } + } + return debugBinaryProcess +} + +func joinPath(url *url.URL, subPath string) string { + u := *url + u.Path = path.Join(u.Path, subPath) + return u.String() +} diff --git a/v2/cmd/wails/internal/commands/dev/dev_other.go b/v2/cmd/wails/internal/dev/dev_other.go similarity index 100% rename from v2/cmd/wails/internal/commands/dev/dev_other.go rename to v2/cmd/wails/internal/dev/dev_other.go diff --git a/v2/cmd/wails/internal/commands/dev/dev_windows.go b/v2/cmd/wails/internal/dev/dev_windows.go similarity index 100% rename from v2/cmd/wails/internal/commands/dev/dev_windows.go rename to v2/cmd/wails/internal/dev/dev_windows.go diff --git a/v2/cmd/wails/internal/commands/dev/stdout_scanner.go b/v2/cmd/wails/internal/dev/stdout_scanner.go similarity index 100% rename from v2/cmd/wails/internal/commands/dev/stdout_scanner.go rename to v2/cmd/wails/internal/dev/stdout_scanner.go diff --git a/v2/cmd/wails/internal/commands/dev/watcher.go b/v2/cmd/wails/internal/dev/watcher.go similarity index 100% rename from v2/cmd/wails/internal/commands/dev/watcher.go rename to v2/cmd/wails/internal/dev/watcher.go diff --git a/v2/cmd/wails/internal/commands/dev/watcher_test.go b/v2/cmd/wails/internal/dev/watcher_test.go similarity index 100% rename from v2/cmd/wails/internal/commands/dev/watcher_test.go rename to v2/cmd/wails/internal/dev/watcher_test.go diff --git a/v2/cmd/wails/internal/commands/build/gomod.go b/v2/cmd/wails/internal/gomod/gomod.go similarity index 89% rename from v2/cmd/wails/internal/commands/build/gomod.go rename to v2/cmd/wails/internal/gomod/gomod.go index 29bc9ffb6..e4873a57e 100644 --- a/v2/cmd/wails/internal/commands/build/gomod.go +++ b/v2/cmd/wails/internal/gomod/gomod.go @@ -1,8 +1,9 @@ -package build +package gomod import ( "fmt" "github.com/wailsapp/wails/v2/cmd/wails/internal" + "github.com/wailsapp/wails/v2/internal/colour" "github.com/wailsapp/wails/v2/internal/fs" "github.com/wailsapp/wails/v2/internal/gomod" "github.com/wailsapp/wails/v2/internal/goversion" @@ -57,3 +58,8 @@ func SyncGoMod(logger *clilogger.CLILogger, updateWailsVersion bool) error { return nil } + +func LogGreen(message string, args ...interface{}) { + text := fmt.Sprintf(message, args...) + println(colour.Green(text)) +} diff --git a/v2/cmd/wails/internal/commands/generate/template/base/NEXTSTEPS.md.template b/v2/cmd/wails/internal/template/base/NEXTSTEPS.md.template similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/NEXTSTEPS.md.template rename to v2/cmd/wails/internal/template/base/NEXTSTEPS.md.template diff --git a/v2/cmd/wails/internal/commands/generate/template/base/README.md b/v2/cmd/wails/internal/template/base/README.md similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/README.md rename to v2/cmd/wails/internal/template/base/README.md diff --git a/v2/cmd/wails/internal/commands/generate/template/base/app.tmpl.go b/v2/cmd/wails/internal/template/base/app.tmpl.go similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/app.tmpl.go rename to v2/cmd/wails/internal/template/base/app.tmpl.go diff --git a/v2/cmd/wails/internal/commands/generate/template/base/frontend/dist/assets/fonts/OFL.txt b/v2/cmd/wails/internal/template/base/frontend/dist/assets/fonts/OFL.txt similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/frontend/dist/assets/fonts/OFL.txt rename to v2/cmd/wails/internal/template/base/frontend/dist/assets/fonts/OFL.txt diff --git a/v2/cmd/wails/internal/commands/generate/template/base/frontend/dist/assets/fonts/nunito-v16-latin-regular.woff2 b/v2/cmd/wails/internal/template/base/frontend/dist/assets/fonts/nunito-v16-latin-regular.woff2 similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/frontend/dist/assets/fonts/nunito-v16-latin-regular.woff2 rename to v2/cmd/wails/internal/template/base/frontend/dist/assets/fonts/nunito-v16-latin-regular.woff2 diff --git a/v2/cmd/wails/internal/commands/generate/template/base/frontend/dist/assets/images/logo-universal.png b/v2/cmd/wails/internal/template/base/frontend/dist/assets/images/logo-universal.png similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/frontend/dist/assets/images/logo-universal.png rename to v2/cmd/wails/internal/template/base/frontend/dist/assets/images/logo-universal.png diff --git a/v2/cmd/wails/internal/commands/generate/template/base/frontend/dist/index.html b/v2/cmd/wails/internal/template/base/frontend/dist/index.html similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/frontend/dist/index.html rename to v2/cmd/wails/internal/template/base/frontend/dist/index.html diff --git a/v2/cmd/wails/internal/commands/generate/template/base/frontend/dist/main.css b/v2/cmd/wails/internal/template/base/frontend/dist/main.css similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/frontend/dist/main.css rename to v2/cmd/wails/internal/template/base/frontend/dist/main.css diff --git a/v2/cmd/wails/internal/commands/generate/template/base/frontend/dist/main.js b/v2/cmd/wails/internal/template/base/frontend/dist/main.js similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/frontend/dist/main.js rename to v2/cmd/wails/internal/template/base/frontend/dist/main.js diff --git a/v2/cmd/wails/internal/commands/generate/template/base/frontend/package.tmpl.json b/v2/cmd/wails/internal/template/base/frontend/package.tmpl.json similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/frontend/package.tmpl.json rename to v2/cmd/wails/internal/template/base/frontend/package.tmpl.json diff --git a/v2/cmd/wails/internal/commands/generate/template/base/go.sum b/v2/cmd/wails/internal/template/base/go.sum similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/go.sum rename to v2/cmd/wails/internal/template/base/go.sum diff --git a/v2/cmd/wails/internal/commands/generate/template/base/go.tmpl.mod b/v2/cmd/wails/internal/template/base/go.tmpl.mod similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/go.tmpl.mod rename to v2/cmd/wails/internal/template/base/go.tmpl.mod diff --git a/v2/cmd/wails/internal/commands/generate/template/base/main.go.tmpl b/v2/cmd/wails/internal/template/base/main.go.tmpl similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/main.go.tmpl rename to v2/cmd/wails/internal/template/base/main.go.tmpl diff --git a/v2/cmd/wails/internal/commands/generate/template/base/scripts/build-macos-arm.sh b/v2/cmd/wails/internal/template/base/scripts/build-macos-arm.sh similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/scripts/build-macos-arm.sh rename to v2/cmd/wails/internal/template/base/scripts/build-macos-arm.sh diff --git a/v2/cmd/wails/internal/commands/generate/template/base/scripts/build-macos-intel.sh b/v2/cmd/wails/internal/template/base/scripts/build-macos-intel.sh similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/scripts/build-macos-intel.sh rename to v2/cmd/wails/internal/template/base/scripts/build-macos-intel.sh diff --git a/v2/cmd/wails/internal/commands/generate/template/base/scripts/build-macos.sh b/v2/cmd/wails/internal/template/base/scripts/build-macos.sh similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/scripts/build-macos.sh rename to v2/cmd/wails/internal/template/base/scripts/build-macos.sh diff --git a/v2/cmd/wails/internal/commands/generate/template/base/scripts/build-windows.sh b/v2/cmd/wails/internal/template/base/scripts/build-windows.sh similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/scripts/build-windows.sh rename to v2/cmd/wails/internal/template/base/scripts/build-windows.sh diff --git a/v2/cmd/wails/internal/commands/generate/template/base/scripts/build.sh b/v2/cmd/wails/internal/template/base/scripts/build.sh similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/scripts/build.sh rename to v2/cmd/wails/internal/template/base/scripts/build.sh diff --git a/v2/cmd/wails/internal/commands/generate/template/base/scripts/install-wails-cli.sh b/v2/cmd/wails/internal/template/base/scripts/install-wails-cli.sh similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/scripts/install-wails-cli.sh rename to v2/cmd/wails/internal/template/base/scripts/install-wails-cli.sh diff --git a/v2/cmd/wails/internal/commands/generate/template/base/template.json.template b/v2/cmd/wails/internal/template/base/template.json.template similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/template.json.template rename to v2/cmd/wails/internal/template/base/template.json.template diff --git a/v2/cmd/wails/internal/commands/generate/template/base/wails.tmpl.json b/v2/cmd/wails/internal/template/base/wails.tmpl.json similarity index 100% rename from v2/cmd/wails/internal/commands/generate/template/base/wails.tmpl.json rename to v2/cmd/wails/internal/template/base/wails.tmpl.json diff --git a/v2/cmd/wails/internal/template/template.go b/v2/cmd/wails/internal/template/template.go new file mode 100644 index 000000000..6b4937db6 --- /dev/null +++ b/v2/cmd/wails/internal/template/template.go @@ -0,0 +1,8 @@ +package template + +import ( + "embed" +) + +//go:embed base +var Base embed.FS diff --git a/v2/cmd/wails/main.go b/v2/cmd/wails/main.go index 3e28dbdc3..fbbeb2d05 100644 --- a/v2/cmd/wails/main.go +++ b/v2/cmd/wails/main.go @@ -2,83 +2,100 @@ package main import ( "fmt" + "github.com/pterm/pterm" "github.com/wailsapp/wails/v2/cmd/wails/internal" - "github.com/wailsapp/wails/v2/cmd/wails/internal/commands/show" "os" + "strings" "github.com/wailsapp/wails/v2/internal/colour" - "github.com/wailsapp/wails/v2/cmd/wails/internal/commands/update" - "github.com/leaanthony/clir" - "github.com/wailsapp/wails/v2/cmd/wails/internal/commands/build" - "github.com/wailsapp/wails/v2/cmd/wails/internal/commands/dev" - "github.com/wailsapp/wails/v2/cmd/wails/internal/commands/doctor" - "github.com/wailsapp/wails/v2/cmd/wails/internal/commands/generate" - "github.com/wailsapp/wails/v2/cmd/wails/internal/commands/initialise" ) -func fatal(message string) { - println(message) - os.Exit(1) -} - func banner(_ *clir.Cli) string { return fmt.Sprintf("%s %s", colour.Green("Wails CLI"), colour.DarkRed(internal.Version)) } -func printFooter() { - println(colour.Green("\nIf Wails is useful to you or your company, please consider sponsoring the project:\nhttps://github.com/sponsors/leaanthony\n")) +func fatal(message string) { + printer := pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.FatalMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.FatalPrefixStyle, + Text: " FATAL ", + }, + } + printer.Println(message) + os.Exit(1) } +func printBulletPoint(text string, args ...any) { + item := pterm.BulletListItem{ + Level: 2, + Text: text, + } + t, err := pterm.DefaultBulletList.WithItems([]pterm.BulletListItem{item}).Srender() + if err != nil { + fatal(err.Error()) + } + t = strings.Trim(t, "\n\r") + pterm.Printf(t, args...) +} + +func printFooter() { + printer := pterm.PrefixPrinter{ + MessageStyle: pterm.NewStyle(pterm.FgLightGreen), + Prefix: pterm.Prefix{ + Style: pterm.NewStyle(pterm.FgRed, pterm.BgLightWhite), + Text: "♥ ", + }, + } + printer.Println("If Wails is useful to you or your company, please consider sponsoring the project:") + pterm.Println("https://github.com/sponsors/leaanthony\n") +} + +func bool2Str(b bool) string { + if b { + return "true" + } + return "false" +} + +var app *clir.Cli + func main() { var err error - app := clir.NewCli("Wails", "Go/HTML Appkit", internal.Version) + app = clir.NewCli("Wails", "Go/HTML Appkit", internal.Version) app.SetBannerFunction(banner) defer printFooter() - build.AddBuildSubcommand(app, os.Stdout) - err = initialise.AddSubcommand(app, os.Stdout) - if err != nil { - fatal(err.Error()) - } + app.NewSubCommandFunction("build", "Builds the application", buildApplication) + app.NewSubCommandFunction("dev", "Runs the application in development mode", devApplication) + app.NewSubCommandFunction("doctor", "Diagnose your environment", diagnoseEnvironment) + app.NewSubCommandFunction("init", "Initialises a new Wails project", initProject) + app.NewSubCommandFunction("update", "Update the Wails CLI", update) - err = doctor.AddSubcommand(app, os.Stdout) - if err != nil { - fatal(err.Error()) - } + show := app.NewSubCommand("show", "Shows various information") + show.NewSubCommandFunction("releasenotes", "Shows the release notes for the current version", showReleaseNotes) - err = dev.AddSubcommand(app, os.Stdout) - if err != nil { - fatal(err.Error()) - } - - err = generate.AddSubcommand(app, os.Stdout) - if err != nil { - fatal(err.Error()) - } - - show.AddSubcommand(app, os.Stdout) - - err = update.AddSubcommand(app, os.Stdout, internal.Version) - if err != nil { - fatal(err.Error()) - } + generate := app.NewSubCommand("generate", "Code Generation Tools") + generate.NewSubCommandFunction("module", "Generates a new Wails module", generateModule) + generate.NewSubCommandFunction("template", "Generates a new Wails template", generateTemplate) command := app.NewSubCommand("version", "The Wails CLI version") command.Action(func() error { - println(internal.Version) + pterm.Println(internal.Version) return nil }) err = app.Run() if err != nil { - println("\n\nERROR: " + err.Error()) + pterm.Println() + pterm.Error.Println(err.Error()) printFooter() os.Exit(1) } diff --git a/v2/cmd/wails/show.go b/v2/cmd/wails/show.go new file mode 100644 index 000000000..c83900c8d --- /dev/null +++ b/v2/cmd/wails/show.go @@ -0,0 +1,27 @@ +package main + +import ( + "github.com/pterm/pterm" + "github.com/wailsapp/wails/v2/cmd/wails/flags" + "github.com/wailsapp/wails/v2/cmd/wails/internal" + "github.com/wailsapp/wails/v2/internal/colour" + "github.com/wailsapp/wails/v2/internal/github" +) + +func showReleaseNotes(f *flags.ShowReleaseNotes) error { + if f.NoColour { + pterm.DisableColor() + colour.ColourEnabled = false + } + + version := internal.Version + if f.Version != "" { + version = f.Version + } + + app.PrintBanner() + releaseNotes := github.GetReleaseNotes(version, f.NoColour) + pterm.Println(releaseNotes) + + return nil +} diff --git a/v2/cmd/wails/update.go b/v2/cmd/wails/update.go new file mode 100644 index 000000000..1bdba7e76 --- /dev/null +++ b/v2/cmd/wails/update.go @@ -0,0 +1,156 @@ +package main + +import ( + "fmt" + "github.com/labstack/gommon/color" + "github.com/pterm/pterm" + "github.com/wailsapp/wails/v2/cmd/wails/flags" + "github.com/wailsapp/wails/v2/internal/colour" + "github.com/wailsapp/wails/v2/internal/shell" + "os" + + "github.com/wailsapp/wails/v2/internal/github" +) + +// AddSubcommand adds the `init` command for the Wails application +func update(f *flags.Update) error { + + if f.NoColour { + colour.ColourEnabled = false + pterm.DisableColor() + + } + // Print banner + app.PrintBanner() + pterm.Println("Checking for updates...") + + var desiredVersion *github.SemanticVersion + var err error + var valid bool + + if len(f.Version) > 0 { + // Check if this is a valid version + valid, err = github.IsValidTag(f.Version) + if err == nil { + if !valid { + err = fmt.Errorf("version '%s' is invalid", f.Version) + } else { + desiredVersion, err = github.NewSemanticVersion(f.Version) + } + } + } else { + if f.PreRelease { + desiredVersion, err = github.GetLatestPreRelease() + } else { + desiredVersion, err = github.GetLatestStableRelease() + if err != nil { + pterm.Println("") + pterm.Println("No stable release found for this major version. To update to the latest pre-release (eg beta), run:") + pterm.Println(" wails update -pre") + return nil + } + } + } + if err != nil { + return err + } + pterm.Println() + + pterm.Println(" Current Version : " + app.Version()) + + if len(f.Version) > 0 { + fmt.Printf(" Desired Version : v%s\n", desiredVersion) + } else { + if f.PreRelease { + fmt.Printf(" Latest Prerelease : v%s\n", desiredVersion) + } else { + fmt.Printf(" Latest Release : v%s\n", desiredVersion) + } + } + + return updateToVersion(desiredVersion, len(f.Version) > 0, app.Version()) +} + +func updateToVersion(targetVersion *github.SemanticVersion, force bool, currentVersion string) error { + + var targetVersionString = "v" + targetVersion.String() + + if targetVersionString == currentVersion { + pterm.Println("\nLooks like you're up to date!\n") + return nil + } + + var desiredVersion string + + if !force { + + compareVersion := currentVersion + + currentVersion, err := github.NewSemanticVersion(compareVersion) + if err != nil { + return err + } + + var success bool + + // Release -> Pre-Release = Massage current version to prerelease format + if targetVersion.IsPreRelease() && currentVersion.IsRelease() { + testVersion, err := github.NewSemanticVersion(compareVersion + "-0") + if err != nil { + return err + } + success, _ = targetVersion.IsGreaterThan(testVersion) + } + // Pre-Release -> Release = Massage target version to prerelease format + if targetVersion.IsRelease() && currentVersion.IsPreRelease() { + // We are ok with greater than or equal + mainversion := currentVersion.MainVersion() + targetVersion, err = github.NewSemanticVersion(targetVersion.String()) + if err != nil { + return err + } + success, _ = targetVersion.IsGreaterThanOrEqual(mainversion) + } + + // Release -> Release = Standard check + if (targetVersion.IsRelease() && currentVersion.IsRelease()) || + (targetVersion.IsPreRelease() && currentVersion.IsPreRelease()) { + + success, _ = targetVersion.IsGreaterThan(currentVersion) + } + + // Compare + if !success { + pterm.Println("Error: The requested version is lower than the current version.") + pterm.Println("If this is what you really want to do, use `wails update -version %s`", targetVersionString) + return nil + } + + desiredVersion = "v" + targetVersion.String() + + } else { + desiredVersion = "v" + targetVersion.String() + } + + pterm.Println() + pterm.Print("Installing Wails CLI " + desiredVersion + "...") + + // Run command in non module directory + homeDir, err := os.UserHomeDir() + if err != nil { + fatal("Cannot find home directory! Please file a bug report!") + } + + sout, serr, err := shell.RunCommand(homeDir, "go", "install", "github.com/wailsapp/wails/v2/cmd/wails@"+desiredVersion) + if err != nil { + pterm.Println("Failed.") + pterm.Error.Println(sout + `\n` + serr) + return err + } + pterm.Println("Done.") + pterm.Println(color.Green("\nMake sure you update your project go.mod file to use " + desiredVersion + ":")) + pterm.Println(color.Green(" require github.com/wailsapp/wails/v2 " + desiredVersion)) + pterm.Println(color.Red("\nTo view the release notes, please run `wails show releasenotes`")) + + return nil +} diff --git a/v2/go.mod b/v2/go.mod index 328334556..7f6b7cc08 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -14,14 +14,13 @@ require ( github.com/jackmordaunt/icns v1.0.0 github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e github.com/labstack/echo/v4 v4.9.0 - github.com/leaanthony/clir v1.0.4 + github.com/leaanthony/clir v1.3.0 github.com/leaanthony/debme v1.2.1 github.com/leaanthony/go-ansi-parser v1.0.1 github.com/leaanthony/gosod v1.0.3 github.com/leaanthony/slicer v1.5.0 github.com/leaanthony/winicon v1.0.0 github.com/matryer/is v1.4.0 - github.com/olekukonko/tablewriter v0.0.5 github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2 github.com/pkg/errors v0.9.1 github.com/tc-hib/winres v0.1.5 @@ -41,25 +40,31 @@ require ( github.com/charmbracelet/glamour v0.5.0 github.com/go-ole/go-ole v1.2.6 github.com/labstack/gommon v0.3.1 + github.com/pterm/pterm v0.12.49 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/samber/lo v1.27.1 - github.com/stretchr/testify v1.7.1 + github.com/stretchr/testify v1.8.0 golang.org/x/tools v0.1.12 ) require ( + atomicgo.dev/cursor v0.1.1 // indirect + atomicgo.dev/keyboard v0.2.8 // indirect bitbucket.org/creachadair/shell v0.0.7 // indirect github.com/Microsoft/go-winio v0.4.16 // indirect github.com/alecthomas/chroma v0.10.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect + github.com/containerd/console v1.0.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect github.com/emirpasic/gods v1.12.0 // indirect github.com/go-git/gcfg v1.5.0 // indirect + github.com/gookit/color v1.5.2 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect github.com/kr/pretty v0.3.0 // indirect + github.com/lithammer/fuzzysearch v1.1.5 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.11 // indirect github.com/mattn/go-isatty v0.0.14 // indirect @@ -69,9 +74,10 @@ require ( github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.9.0 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/sergi/go-diff v1.1.0 // indirect + github.com/sergi/go-diff v1.2.0 // indirect github.com/tidwall/gjson v1.8.0 // indirect github.com/tidwall/match v1.0.3 // indirect github.com/tidwall/pretty v1.1.0 // indirect @@ -79,11 +85,13 @@ require ( github.com/valyala/fasttemplate v1.2.1 // indirect github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae // indirect github.com/xanzy/ssh-agent v0.3.0 // indirect + github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect github.com/yuin/goldmark v1.4.13 // indirect github.com/yuin/goldmark-emoji v1.0.1 // indirect golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/v2/go.sum b/v2/go.sum index fa68ec298..c434fd7af 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -1,5 +1,17 @@ +atomicgo.dev/cursor v0.1.1 h1:0t9sxQomCTRh5ug+hAMCs59x/UmC9QL6Ci5uosINKD4= +atomicgo.dev/cursor v0.1.1/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU= +atomicgo.dev/keyboard v0.2.8 h1:Di09BitwZgdTV1hPyX/b9Cqxi8HVuJQwWivnZUEqlj4= +atomicgo.dev/keyboard v0.2.8/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= bitbucket.org/creachadair/shell v0.0.7 h1:Z96pB6DkSb7F3Y3BBnJeOZH2gazyMTWlvecSD4vDqfk= bitbucket.org/creachadair/shell v0.0.7/go.mod h1:oqtXSSvSYr4624lnnabXHaBsYW6RD80caLi2b3hJk0U= +github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= +github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= +github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= +github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k= +github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI= +github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c= +github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= +github.com/MarvinJWendt/testza v0.4.3 h1:u2XaM4IqGp9dsdUmML8/Z791fu4yjQYzOiufOtJwTII= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= @@ -15,6 +27,7 @@ github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= @@ -23,6 +36,8 @@ github.com/bitfield/script v0.19.0 h1:W24f+FQuPab9gXcW8bhcbo5qO8AtrXyu3XOnR4zhHN github.com/bitfield/script v0.19.0/go.mod h1:ana6F8YOSZ3ImT8SauIzuYSqXgFVkSUJ6kgja+WMmIY= github.com/charmbracelet/glamour v0.5.0 h1:wu15ykPdB7X6chxugG/NNfDUbyyrCLV9XBalj5wdu3g= github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc= +github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -58,6 +73,10 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= +github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= +github.com/gookit/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI= +github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= @@ -71,6 +90,10 @@ github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEE github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck= github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/klauspost/cpuid/v2 v2.1.0 h1:eyi1Ad2aNJMW95zcSbmGg7Cg6cq3ADwLpMAP96d8rF0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -84,8 +107,9 @@ github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITV github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks= github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o= github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= -github.com/leaanthony/clir v1.0.4 h1:Dov2y9zWJmZr7CjaCe86lKa4b5CSxskGAt2yBkoDyiU= github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0= +github.com/leaanthony/clir v1.3.0 h1:L9nPDWrmc/qU9UWZZvRaFajWYuO0np9V5p+5gxyYno0= +github.com/leaanthony/clir v1.3.0/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0= github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= github.com/leaanthony/go-ansi-parser v1.0.1 h1:97v6c5kYppVsbScf4r/VZdXyQ21KQIfeQOk2DgKxGG4= @@ -96,6 +120,8 @@ github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0H github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= github.com/leaanthony/winicon v1.0.0 h1:ZNt5U5dY71oEoKZ97UVwJRT4e+5xo5o/ieKuHuk8NqQ= github.com/leaanthony/winicon v1.0.0/go.mod h1:en5xhijl92aphrJdmRPlh4NI1L6wq3gEm0LpXAPghjU= +github.com/lithammer/fuzzysearch v1.1.5 h1:Ag7aKU08wp0R9QCfF4GoGST9HbmAIeLP7xwMrOBEp1c= +github.com/lithammer/fuzzysearch v1.1.5/go.mod h1:1R1LRNk7yKid1BaQkmuLQaHruxcC4HmAH30Dh61Ih1Q= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= @@ -129,6 +155,15 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= +github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= +github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= +github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU= +github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE= +github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8= +github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= +github.com/pterm/pterm v0.12.49 h1:qeNm0wTWawy6WhKoY8ZKq6qTXFr0s2UtUyRW0yVztEg= +github.com/pterm/pterm v0.12.49/go.mod h1:D4OBoWNqAfXkm5QLTjIgjNiMXPHemLJHnIreGUsWzWg= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -138,17 +173,20 @@ github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDj github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= github.com/samber/lo v1.27.1 h1:sTXwkRiIFIQG+G0HeAvOEnGjqWeWtI9cg5/n51KrxPg= github.com/samber/lo v1.27.1/go.mod h1:it33p9UtPMS7z72fP4gw/EIfQB2eI8ke7GR2wc6+Rhg= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/tc-hib/winres v0.1.5 h1:2dA5yfjdoEA3UyRaOC92HNMt3jap66pLzoW4MjpC/0M= github.com/tc-hib/winres v0.1.5/go.mod h1:pe6dOR40VOrGz8PkzreVKNvEKnlE8t4yR8A8naL+t7A= github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= @@ -174,6 +212,8 @@ github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae h1:tpXvBXC3hpQBDC github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae/go.mod h1:VTAq37rkGeV+WOybvZwjXiJOicICdpLCN8ifpISjK20= github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= @@ -206,17 +246,25 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/v2/internal/frontend/desktop/windows/frontend.go b/v2/internal/frontend/desktop/windows/frontend.go index 93cfbec59..b65358860 100644 --- a/v2/internal/frontend/desktop/windows/frontend.go +++ b/v2/internal/frontend/desktop/windows/frontend.go @@ -341,7 +341,7 @@ func (f *Frontend) WindowSetBackgroundColour(col *options.RGBA) { B: col.B, } - // Webview2 only has 0 and 255 as valid values. + // WebView2 only has 0 and 255 as valid values. if backgroundCol.A > 0 && backgroundCol.A < 255 { backgroundCol.A = 255 } diff --git a/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/native_module.go b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/native_module.go index cf66e869c..8f4c551a9 100644 --- a/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/native_module.go +++ b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/native_module.go @@ -75,7 +75,7 @@ func GetAvailableCoreWebView2BrowserVersionString(path string) (string, error) { // feature-complete and remove the use of the native DLL and go-winloader. version, err := goGetAvailableCoreWebView2BrowserVersionString(path) if errors.Is(err, errNoClientDLLFound) { - // Webview2 is not found + // WebView2 is not found return "", nil } else if err != nil { return "", err @@ -105,7 +105,7 @@ func GetAvailableCoreWebView2BrowserVersionString(path string) (string, error) { if res != 0 { if res == E_FILENOTFOUND { - // Webview2 is not installed + // WebView2 is not installed return "", nil } diff --git a/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/version.go b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/version.go index 2b36c634a..5b3c1f2f2 100644 --- a/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/version.go +++ b/v2/internal/frontend/desktop/windows/go-webview2/webviewloader/version.go @@ -35,7 +35,7 @@ func GetAvailableCoreWebView2BrowserVersionString(browserExecutableFolder string if browserExecutableFolder != "" { clientPath, err := findEmbeddedClientDll(browserExecutableFolder) if errors.Is(err, errNoClientDLLFound) { - // Webview2 is not found + // WebView2 is not found return "", nil } else if err != nil { return "", err diff --git a/v2/internal/github/github.go b/v2/internal/github/github.go index 3c3ba9b06..db3b70c58 100644 --- a/v2/internal/github/github.go +++ b/v2/internal/github/github.go @@ -12,7 +12,7 @@ import ( "strings" ) -func GetReleaseNotes(tagVersion string) string { +func GetReleaseNotes(tagVersion string, noColour bool) string { resp, err := http.Get("https://api.github.com/repos/wailsapp/wails/releases/tags/" + url.PathEscape(tagVersion)) if err != nil { return "Unable to retrieve release notes. Please check your network connection" @@ -34,11 +34,16 @@ func GetReleaseNotes(tagVersion string) string { result := "# Release Notes for " + tagVersion + "\n" + data["body"].(string) var renderer *glamour.TermRenderer - if runtime.GOOS == "windows" { - renderer, err = glamour.NewTermRenderer(glamour.WithStyles(glamour.NoTTYStyleConfig)) + + var termRendererOpts []glamour.TermRendererOption + + if runtime.GOOS == "windows" || noColour { + termRendererOpts = append(termRendererOpts, glamour.WithStyles(glamour.NoTTYStyleConfig)) } else { - renderer, err = glamour.NewTermRenderer(glamour.WithAutoStyle()) + termRendererOpts = append(termRendererOpts, glamour.WithAutoStyle()) } + + renderer, err = glamour.NewTermRenderer(termRendererOpts...) if err != nil { return result } diff --git a/v2/internal/wv2installer/wv2installer.go b/v2/internal/wv2installer/wv2installer.go index 3f195fae5..ce754cee7 100644 --- a/v2/internal/wv2installer/wv2installer.go +++ b/v2/internal/wv2installer/wv2installer.go @@ -10,7 +10,7 @@ import ( "github.com/wailsapp/wails/v2/pkg/options/windows" ) -const MinimumRuntimeVersion string = "94.0.992.31" // Webview2 SDK 1.0.992.28 +const MinimumRuntimeVersion string = "94.0.992.31" // WebView2 SDK 1.0.992.28 type installationStatus int diff --git a/v2/pkg/commands/build/base.go b/v2/pkg/commands/build/base.go index 51f373a64..06ed3046e 100644 --- a/v2/pkg/commands/build/base.go +++ b/v2/pkg/commands/build/base.go @@ -3,6 +3,7 @@ package build import ( "bytes" "fmt" + "github.com/pterm/pterm" "os" "os/exec" "path/filepath" @@ -283,7 +284,7 @@ func (b *BaseBuilder) CompileProject(options *Options) error { cmd := exec.Command(compiler, commands.AsSlice()...) cmd.Stderr = os.Stderr if verbose { - println(" Build command:", compiler, commandPrettifier(commands.AsSlice())) + pterm.Info.Println("Build command:", compiler, commandPrettifier(commands.AsSlice())) cmd.Stdout = os.Stdout } // Set the directory @@ -359,7 +360,7 @@ func (b *BaseBuilder) CompileProject(options *Options) error { }) if verbose { - println(" Environment:", strings.Join(cmd.Env, " ")) + printBulletPoint("Environment:", strings.Join(cmd.Env, " ")) } // Run command @@ -373,7 +374,7 @@ func (b *BaseBuilder) CompileProject(options *Options) error { stdErr := string(output) if strings.Contains(err.Error(), "ld: framework not found UniformTypeIdentifiers") || strings.Contains(stdErr, "ld: framework not found UniformTypeIdentifiers") { - println(` + 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 @@ -388,11 +389,11 @@ Please reinstall by doing the following: return nil } - fmt.Printf("Compressing application: ") + printBulletPoint("Compressing application: ") // Do we have upx installed? if !shell.CommandExists("upx") { - println("Warning: Cannot compress binary: upx not found") + pterm.Warning.Println("Warning: Cannot compress binary: upx not found") return nil } @@ -404,16 +405,16 @@ Please reinstall by doing the following: } if verbose { - println("upx", strings.Join(args, " ")) + pterm.Info.Println("upx", strings.Join(args, " ")) } output, err := exec.Command("upx", args...).Output() if err != nil { return errors.Wrap(err, "Error during compression:") } - println("Done.") + pterm.Println("Done.") if verbose { - println(string(output)) + pterm.Info.Println(string(output)) } return nil @@ -488,7 +489,7 @@ func (b *BaseBuilder) NpmInstallUsingCommand(sourceDir string, installCommand st // Shortcut installation if install == false { if verbose { - println("Skipping npm install") + pterm.Println("Skipping npm install") } return nil } @@ -498,10 +499,10 @@ func (b *BaseBuilder) NpmInstallUsingCommand(sourceDir string, installCommand st stdout, stderr, err := shell.RunCommand(sourceDir, cmd[0], cmd[1:]...) if verbose || err != nil { for _, l := range strings.Split(stdout, "\n") { - fmt.Printf(" %s\n", l) + pterm.Printf(" %s\n", l) } for _, l := range strings.Split(stderr, "\n") { - fmt.Printf(" %s\n", l) + pterm.Printf(" %s\n", l) } } @@ -513,10 +514,10 @@ 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") { - fmt.Printf(" %s\n", l) + pterm.Printf(" %s\n", l) } for _, l := range strings.Split(stderr, "\n") { - fmt.Printf(" %s\n", l) + pterm.Printf(" %s\n", l) } } return err @@ -532,10 +533,10 @@ func (b *BaseBuilder) NpmRunWithEnvironment(projectDir, buildTarget string, verb err := cmd.Run() if verbose || err != nil { for _, l := range strings.Split(stdo.String(), "\n") { - fmt.Printf(" %s\n", l) + pterm.Printf(" %s\n", l) } for _, l := range strings.Split(stde.String(), "\n") { - fmt.Printf(" %s\n", l) + pterm.Printf(" %s\n", l) } } return err @@ -558,13 +559,13 @@ func (b *BaseBuilder) BuildFrontend(outputLogger *clilogger.CLILogger) error { } if installCommand == "" { // No - don't install - outputLogger.Println(" - No Install command. Skipping.") + printBulletPoint("No Install command. Skipping.") } else { // Do install if needed - outputLogger.Print(" - Installing frontend dependencies: ") + printBulletPoint("Installing frontend dependencies: ") if verbose { - outputLogger.Println("") - outputLogger.Println(" Install command: '" + installCommand + "'") + pterm.Println("") + pterm.Info.Println("Install command: '" + installCommand + "'") } if err := b.NpmInstallUsingCommand(frontendDir, installCommand, verbose); err != nil { return err @@ -578,31 +579,31 @@ func (b *BaseBuilder) BuildFrontend(outputLogger *clilogger.CLILogger) error { buildCommand = b.projectData.GetDevBuildCommand() } if buildCommand == "" { - outputLogger.Println(" - No Build command. Skipping.") + printBulletPoint("No Build command. Skipping.") // No - ignore return nil } - outputLogger.Print(" - Compiling frontend: ") + printBulletPoint("Compiling frontend: ") cmd := strings.Split(buildCommand, " ") if verbose { - outputLogger.Println("") - outputLogger.Println(" Build command: '" + buildCommand + "'") + pterm.Println("") + pterm.Info.Println("Build command: '" + buildCommand + "'") } stdout, stderr, err := shell.RunCommand(frontendDir, cmd[0], cmd[1:]...) if verbose || err != nil { for _, l := range strings.Split(stdout, "\n") { - fmt.Printf(" %s\n", l) + pterm.Printf(" %s\n", l) } for _, l := range strings.Split(stderr, "\n") { - fmt.Printf(" %s\n", l) + pterm.Printf(" %s\n", l) } } if err != nil { return err } - outputLogger.Println("Done.") + pterm.Println("Done.") return nil } diff --git a/v2/pkg/commands/build/build.go b/v2/pkg/commands/build/build.go index 931538be3..262446832 100644 --- a/v2/pkg/commands/build/build.go +++ b/v2/pkg/commands/build/build.go @@ -2,14 +2,13 @@ package build import ( "fmt" - "log" + "github.com/pterm/pterm" "os" "path/filepath" "runtime" "strings" "github.com/samber/lo" - "github.com/wailsapp/wails/v2/internal/colour" "github.com/wailsapp/wails/v2/internal/staticanalysis" "github.com/wailsapp/wails/v2/pkg/commands/bindings" @@ -185,14 +184,39 @@ func CreateEmbedDirectories(cwd string, buildOptions *Options) error { } +func fatal(message string) { + printer := pterm.PrefixPrinter{ + MessageStyle: &pterm.ThemeDefault.FatalMessageStyle, + Prefix: pterm.Prefix{ + Style: &pterm.ThemeDefault.FatalPrefixStyle, + Text: " FATAL ", + }, + } + printer.Println(message) + os.Exit(1) +} + +func printBulletPoint(text string, args ...any) { + item := pterm.BulletListItem{ + Level: 2, + Text: text, + } + t, err := pterm.DefaultBulletList.WithItems([]pterm.BulletListItem{item}).Srender() + if err != nil { + fatal(err.Error()) + } + t = strings.Trim(t, "\n\r") + pterm.Printf(t, args...) +} + func GenerateBindings(buildOptions *Options) error { obfuscated := buildOptions.Obfuscated if obfuscated { - buildOptions.Logger.Print(" - Generating obfuscated bindings: ") + printBulletPoint("Generating obfuscated bindings: ") buildOptions.UserTags = append(buildOptions.UserTags, "obfuscated") } else { - buildOptions.Logger.Print(" - Generating bindings: ") + printBulletPoint("Generating bindings: ") } // Generate Bindings @@ -207,39 +231,36 @@ func GenerateBindings(buildOptions *Options) error { } if buildOptions.Verbosity == VERBOSE { - buildOptions.Logger.Println(output) + pterm.Info.Println(output) } - buildOptions.Logger.Println("Done.") + pterm.Println("Done.") return nil } func execBuildApplication(builder Builder, options *Options) (string, error) { - // Extract logger - outputLogger := options.Logger - // If we are building for windows, we will need to generate the asset bundle before // compilation. This will be a .syso file in the project root if options.Pack && options.Platform == "windows" { - outputLogger.Print(" - Generating bundle assets: ") + printBulletPoint("Generating application assets: ") err := packageApplicationForWindows(options) if err != nil { return "", err } - outputLogger.Println("Done.") + pterm.Println("Done.") // When we finish, we will want to remove the syso file defer func() { err := os.Remove(filepath.Join(options.ProjectData.Path, options.ProjectData.Name+"-res.syso")) if err != nil { - log.Fatal(err) + fatal(err.Error()) } }() } // Compile the application - outputLogger.Print(" - Compiling application: ") + printBulletPoint("Compiling application: ") if options.Platform == "darwin" && options.Arch == "universal" { outputFile := builder.OutputFilename(options) @@ -251,7 +272,7 @@ func execBuildApplication(builder Builder, options *Options) (string, error) { options.OutputFile = amd64Filename options.CleanBinDirectory = false if options.Verbosity == VERBOSE { - outputLogger.Println("\nBuilding AMD64 Target: %s", filepath.Join(options.BinDirectory, options.OutputFile)) + pterm.Println("Building AMD64 Target: " + filepath.Join(options.BinDirectory, options.OutputFile)) } err := builder.CompileProject(options) if err != nil { @@ -262,7 +283,7 @@ func execBuildApplication(builder Builder, options *Options) (string, error) { options.OutputFile = arm64Filename options.CleanBinDirectory = false if options.Verbosity == VERBOSE { - outputLogger.Println("Building ARM64 Target: %s", filepath.Join(options.BinDirectory, options.OutputFile)) + pterm.Println("Building ARM64 Target: " + filepath.Join(options.BinDirectory, options.OutputFile)) } err = builder.CompileProject(options) @@ -271,7 +292,7 @@ func execBuildApplication(builder Builder, options *Options) (string, error) { } // Run lipo if options.Verbosity == VERBOSE { - outputLogger.Println(" Running lipo: lipo -create -output %s %s %s", outputFile, amd64Filename, arm64Filename) + pterm.Println("Running lipo: lipo -create -output %s %s %s", outputFile, amd64Filename, arm64Filename) } _, stderr, err := shell.RunCommand(options.BinDirectory, "lipo", "-create", "-output", outputFile, amd64Filename, arm64Filename) if err != nil { @@ -295,19 +316,19 @@ func execBuildApplication(builder Builder, options *Options) (string, error) { } } - outputLogger.Println("Done.") + pterm.Println("Done.") // Do we need to pack the app for non-windows? if options.Pack && options.Platform != "windows" { - outputLogger.Print(" - Packaging application: ") + printBulletPoint("Packaging application: ") // TODO: Allow cross platform build err := packageProject(options, runtime.GOOS) if err != nil { return "", err } - outputLogger.Println("Done.") + pterm.Println("Done.") } if options.Platform == "windows" { @@ -321,7 +342,7 @@ func execBuildApplication(builder Builder, options *Options) (string, error) { tags = append(tags, expWebView2Loader) message = fmt.Sprintf("An experimental Go native WebView2Loader is available. We would love to hear your feedback about it and invite you to test it by building with `-tags %s`", strings.Join(tags, ",")) } - println(colour.Green(" - " + message)) + pterm.Info.Println(message) } return options.CompiledBinary, nil @@ -358,13 +379,13 @@ func executeBuildHook(outputLogger *clilogger.CLILogger, options *Options, hookI // The hook is for host platform } else { // Skip a hook which is not native - outputLogger.Println(" - Non native build hook '%s': Skipping.", hookIdentifier) + printBulletPoint(fmt.Sprintf("Non native build hook '%s': Skipping.", hookIdentifier)) return nil } } } - outputLogger.Print(" - Executing %s build hook '%s': ", hookName, hookIdentifier) + printBulletPoint("Executing %s build hook '%s': ", hookName, hookIdentifier) args := strings.Split(buildHook, " ") for i, arg := range args { newArg := argReplacements[arg] @@ -375,17 +396,17 @@ func executeBuildHook(outputLogger *clilogger.CLILogger, options *Options, hookI } if options.Verbosity == VERBOSE { - outputLogger.Println("%s", strings.Join(args, " ")) + pterm.Info.Println("%s", strings.Join(args, " ")) } stdout, stderr, err := shell.RunCommand(options.BinDirectory, args[0], args[1:]...) if options.Verbosity == VERBOSE { - println(stdout) + pterm.Info.Println(stdout) } if err != nil { return fmt.Errorf("%s - %s", err.Error(), stderr) } - outputLogger.Println("Done.") + pterm.Println("Done.") return nil } diff --git a/v2/pkg/commands/build/nsis_installer.go b/v2/pkg/commands/build/nsis_installer.go index 3b846984f..11f1407a3 100644 --- a/v2/pkg/commands/build/nsis_installer.go +++ b/v2/pkg/commands/build/nsis_installer.go @@ -47,7 +47,7 @@ func GenerateNSISInstaller(options *Options, amd64Binary string, arm64Binary str } if err := webview2runtime.WriteInstallerToFile(webviewSetup); err != nil { - return fmt.Errorf("Unable to write Webview2 Bootstrapper Setup: %w", err) + return fmt.Errorf("Unable to write WebView2 Bootstrapper Setup: %w", err) } if !shell.CommandExists("makensis") { diff --git a/v2/pkg/templates/templates.go b/v2/pkg/templates/templates.go index 8e781427a..d982454d0 100644 --- a/v2/pkg/templates/templates.go +++ b/v2/pkg/templates/templates.go @@ -17,7 +17,6 @@ import ( "github.com/leaanthony/debme" "github.com/leaanthony/gosod" - "github.com/olekukonko/tablewriter" "github.com/wailsapp/wails/v2/internal/fs" "github.com/wailsapp/wails/v2/pkg/clilogger" ) @@ -313,33 +312,6 @@ func gitclone(options *Options) (string, error) { } -// OutputList prints the list of available tempaltes to the given logger -func OutputList(logger *clilogger.CLILogger) error { - templates, err := List() - if err != nil { - return err - } - - table := tablewriter.NewWriter(logger.Writer) - table.SetHeader([]string{"Template", "Short Name", "Description"}) - table.SetAutoWrapText(false) - table.SetAutoFormatHeaders(true) - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetAlignment(tablewriter.ALIGN_LEFT) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - table.SetRowSeparator("") - table.SetHeaderLine(false) - table.SetBorder(false) - table.SetTablePadding("\t") // pad with tabs - table.SetNoWhiteSpace(true) - for _, template := range templates { - table.Append([]string{template.Name, template.ShortName, template.Description}) - } - table.Render() - return nil -} - func generateIDEFiles(options *Options) error { switch options.IDE {