diff --git a/v2/cmd/wails/internal/commands/update/update.go b/v2/cmd/wails/internal/commands/update/update.go new file mode 100644 index 000000000..163784476 --- /dev/null +++ b/v2/cmd/wails/internal/commands/update/update.go @@ -0,0 +1,164 @@ +package update + +import ( + "fmt" + "io" + "log" + "os" + + "github.com/wailsapp/wails/v2/internal/shell" + + "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 Wails.`) + + // Setup flags + var prereleaseRequired bool + command.BoolFlag("pre", "Update to latest Prerelease", &prereleaseRequired) + + var specificVersion string + command.StringFlag("version", "Install a specific version (Overrides other flags)", &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 { + 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("Looks 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 " + 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", "get", "github.com/wailsapp/wails/v2/cmd/wails@"+desiredVersion) + if err != nil { + logger.Println("Failed.") + logger.Println(sout + `\n` + serr) + return err + } + fmt.Println() + logger.Println("Wails updated to " + desiredVersion) + + return nil +} diff --git a/v2/cmd/wails/main.go b/v2/cmd/wails/main.go index b989af219..01eee20bf 100644 --- a/v2/cmd/wails/main.go +++ b/v2/cmd/wails/main.go @@ -3,6 +3,8 @@ package main import ( "os" + "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/debug" @@ -48,6 +50,11 @@ func main() { fatal(err.Error()) } + err = update.AddSubcommand(app, os.Stdout, version) + if err != nil { + fatal(err.Error()) + } + err = app.Run() if err != nil { println("\n\nERROR: " + err.Error()) diff --git a/v2/go.mod b/v2/go.mod index abe0a62af..9de864dd1 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -3,6 +3,7 @@ module github.com/wailsapp/wails/v2 go 1.15 require ( + github.com/Masterminds/semver v1.5.0 github.com/davecgh/go-spew v1.1.1 github.com/fatih/structtag v1.2.0 github.com/fsnotify/fsnotify v1.4.9 diff --git a/v2/go.sum b/v2/go.sum index 08f80d04b..a68cf8d70 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -1,3 +1,5 @@ +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 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= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/v2/internal/github/github.go b/v2/internal/github/github.go new file mode 100644 index 000000000..e91b296e1 --- /dev/null +++ b/v2/internal/github/github.go @@ -0,0 +1,103 @@ +package github + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "sort" + "strings" +) + +// GetVersionTags gets the list of tags on the Wails repo +// It returns a list of sorted tags in descending order +func GetVersionTags() ([]*SemanticVersion, error) { + + result := []*SemanticVersion{} + var err error + + resp, err := http.Get("https://api.github.com/repos/wailsapp/wails/tags") + if err != nil { + return result, err + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return result, err + } + + data := []map[string]interface{}{} + err = json.Unmarshal(body, &data) + if err != nil { + return result, err + } + + // Convert tag data to Version structs + for _, tag := range data { + version := tag["name"].(string) + if !strings.HasPrefix(version, "v2") { + continue + } + semver, err := NewSemanticVersion(version) + if err != nil { + return result, err + } + result = append(result, semver) + } + + // Reverse Sort + sort.Sort(sort.Reverse(SemverCollection(result))) + + return result, err +} + +// GetLatestStableRelease gets the latest stable release on GitHub +func GetLatestStableRelease() (result *SemanticVersion, err error) { + + tags, err := GetVersionTags() + if err != nil { + return nil, err + } + + for _, tag := range tags { + if tag.IsRelease() { + return tag, nil + } + } + + return nil, fmt.Errorf("no release tag found") +} + +// GetLatestPreRelease gets the latest prerelease on GitHub +func GetLatestPreRelease() (result *SemanticVersion, err error) { + + tags, err := GetVersionTags() + if err != nil { + return nil, err + } + + for _, tag := range tags { + if tag.IsPreRelease() { + return tag, nil + } + } + + return nil, fmt.Errorf("no prerelease tag found") +} + +// IsValidTag returns true if the given string is a valid tag +func IsValidTag(tagVersion string) (bool, error) { + if tagVersion[0] == 'v' { + tagVersion = tagVersion[1:] + } + tags, err := GetVersionTags() + if err != nil { + return false, err + } + + for _, tag := range tags { + if tag.String() == tagVersion { + return true, nil + } + } + return false, nil +} diff --git a/v2/internal/github/semver.go b/v2/internal/github/semver.go new file mode 100644 index 000000000..1cf5907fa --- /dev/null +++ b/v2/internal/github/semver.go @@ -0,0 +1,106 @@ +package github + +import ( + "fmt" + + "github.com/Masterminds/semver" +) + +// SemanticVersion is a struct containing a semantic version +type SemanticVersion struct { + Version *semver.Version +} + +// NewSemanticVersion creates a new SemanticVersion object with the given version string +func NewSemanticVersion(version string) (*SemanticVersion, error) { + semverVersion, err := semver.NewVersion(version) + if err != nil { + return nil, err + } + return &SemanticVersion{ + Version: semverVersion, + }, nil +} + +// IsRelease returns true if it's a release version +func (s *SemanticVersion) IsRelease() bool { + // Limit to v2 + if s.Version.Major() != 2 { + return false + } + return len(s.Version.Prerelease()) == 0 && len(s.Version.Metadata()) == 0 +} + +// IsPreRelease returns true if it's a prerelease version +func (s *SemanticVersion) IsPreRelease() bool { + // Limit to v1 + if s.Version.Major() != 2 { + return false + } + return len(s.Version.Prerelease()) > 0 +} + +func (s *SemanticVersion) String() string { + return s.Version.String() +} + +// IsGreaterThan returns true if this version is greater than the given version +func (s *SemanticVersion) IsGreaterThan(version *SemanticVersion) (bool, error) { + // Set up new constraint + constraint, err := semver.NewConstraint("> " + version.Version.String()) + if err != nil { + return false, err + } + + // Check if the desired one is greater than the requested on + success, msgs := constraint.Validate(s.Version) + if !success { + return false, msgs[0] + } + return true, nil +} + +// IsGreaterThanOrEqual returns true if this version is greater than or equal the given version +func (s *SemanticVersion) IsGreaterThanOrEqual(version *SemanticVersion) (bool, error) { + // Set up new constraint + constraint, err := semver.NewConstraint(">= " + version.Version.String()) + if err != nil { + return false, err + } + + // Check if the desired one is greater than the requested on + success, msgs := constraint.Validate(s.Version) + if !success { + return false, msgs[0] + } + return true, nil +} + +// MainVersion returns the main version of any version+prerelease+metadata +// EG: MainVersion("1.2.3-pre") => "1.2.3" +func (s *SemanticVersion) MainVersion() *SemanticVersion { + mainVersion := fmt.Sprintf("%d.%d.%d", s.Version.Major(), s.Version.Minor(), s.Version.Patch()) + result, _ := NewSemanticVersion(mainVersion) + return result +} + +// SemverCollection is a collection of SemanticVersion objects +type SemverCollection []*SemanticVersion + +// Len returns the length of a collection. The number of Version instances +// on the slice. +func (c SemverCollection) Len() int { + return len(c) +} + +// Less is needed for the sort interface to compare two Version objects on the +// slice. If checks if one is less than the other. +func (c SemverCollection) Less(i, j int) bool { + return c[i].Version.LessThan(c[j].Version) +} + +// Swap is needed for the sort interface to replace the Version objects +// at two different positions in the slice. +func (c SemverCollection) Swap(i, j int) { + c[i], c[j] = c[j], c[i] +}