diff --git a/cmd/check.go b/cmd/check.go index f2c6628..62129de 100644 --- a/cmd/check.go +++ b/cmd/check.go @@ -17,9 +17,10 @@ func newCheckCmd() *cobra.Command { cmd := &cobra.Command{ Use: "check", Short: "Check the latest version of the binary installed by 'go install'", - Long: `Check the latest version of the binary installed by 'go install' + Long: `Check the latest version and build toolchain of the binary installed by 'go install' check subcommand checks if the binary is the latest version +and if it has been built with the current version of go installed, and displays the name of the binary that needs to be updated. However, do not update`, ValidArgsFunction: completePathBinaries, @@ -92,7 +93,7 @@ func doCheck(pkgs []goutil.Package, cpus int) int { err = fmt.Errorf(" %s %w", p.Name, err) } p.Version.Latest = latestVer - if !goutil.IsAlreadyUpToDate(*p.Version) { + if !p.IsAlreadyUpToDate() { mu.Lock() needUpdatePkgs = append(needUpdatePkgs, p) mu.Unlock() diff --git a/cmd/import.go b/cmd/import.go index c79e847..219d034 100644 --- a/cmd/import.go +++ b/cmd/import.go @@ -42,7 +42,7 @@ Finally, you execute the export subcommand in this state.`, return cmd } -func runImport(cmd *cobra.Command, args []string) int { +func runImport(cmd *cobra.Command, _ []string) int { dryRun, err := cmd.Flags().GetBool("dry-run") if err != nil { print.Err(fmt.Errorf("%s: %w", "can not parse command line argument (--dry-run)", err)) diff --git a/cmd/root_test.go b/cmd/root_test.go index d17701a..75d033e 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -533,7 +533,7 @@ func TestExecute_Import_WithInputOption(t *testing.T) { contain := false for _, v := range got { - if strings.Contains(v, "gup:INFO : [1/1] github.com/nao1215/gup") { + if strings.Contains(v, "[1/1] github.com/nao1215/gup") { contain = true } } @@ -694,7 +694,7 @@ func TestExecute_Update(t *testing.T) { contain := false for _, v := range got { - if strings.Contains(v, "gup:INFO : [1/1] github.com/nao1215/gal/cmd/gal") { + if strings.Contains(v, "[1/1] github.com/nao1215/gal/cmd/gal") { contain = true } } @@ -791,7 +791,7 @@ func TestExecute_Update_DryRunAndNotify(t *testing.T) { contain := false for _, v := range got { - if strings.Contains(v, "gup:INFO : [1/1] github.com/nao1215/posixer") { + if strings.Contains(v, "[1/1] github.com/nao1215/posixer") { contain = true } } diff --git a/cmd/update.go b/cmd/update.go index 99e0bbe..e581886 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -27,7 +27,8 @@ func newUpdateCmd() *cobra.Command { Long: `Update binaries installed by 'go install' If you execute '$ gup update', gup gets the package path of all commands -under $GOPATH/bin and automatically updates commands to the latest version.`, +under $GOPATH/bin and automatically updates commands to the latest version, +using the current installed Go toolchain.`, Run: func(cmd *cobra.Command, args []string) { OsExit(gup(cmd, args)) }, diff --git a/go.mod b/go.mod index 3211c4d..c56c1c9 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/mattn/go-colorable v0.1.13 github.com/nao1215/gorky v0.2.1 github.com/pkg/errors v0.9.1 + github.com/shogo82148/pointer v1.3.0 github.com/spf13/cobra v1.8.1 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 golang.org/x/sync v0.7.0 diff --git a/go.sum b/go.sum index 1822856..27852a8 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shogo82148/pointer v1.3.0 h1:LW5V2jUAjFNjS8e7k/PgFoh3EavOSB/vvN85aGue5+I= +github.com/shogo82148/pointer v1.3.0/go.mod h1:agZ5JFpavFPXznbWonIvbG78NDfvDTFppe+7o53up5w= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= diff --git a/internal/config/config.go b/internal/config/config.go index 31eadb1..d2f1f42 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -14,6 +14,7 @@ import ( "github.com/adrg/xdg" "github.com/nao1215/gup/internal/cmdinfo" "github.com/nao1215/gup/internal/goutil" + "github.com/shogo82148/pointer" ) // ConfigFileName is gup command configuration file @@ -40,7 +41,8 @@ func ReadConfFile(path string) ([]goutil.Package, error) { pkgs := []goutil.Package{} for _, v := range contents { pkg := goutil.Package{} - ver := goutil.Version{Current: "", Latest: ""} + binVer := goutil.Version{Current: "", Latest: ""} + goVer := goutil.Version{Current: "", Latest: ""} v = deleteComment(v) if isBlank(v) { @@ -55,7 +57,8 @@ func ReadConfFile(path string) ([]goutil.Package, error) { equalIdx := strings.Index(v, "=") pkg.Name = strings.TrimSpace(v[:equalIdx-1]) pkg.ImportPath = strings.TrimSpace(v[equalIdx+1:]) - pkg.Version = &ver + pkg.Version = pointer.Ptr(binVer) + pkg.GoVersion = pointer.Ptr(goVer) pkgs = append(pkgs, pkg) } diff --git a/internal/goutil/examples_test.go b/internal/goutil/examples_test.go index dde9457..afb76a6 100644 --- a/internal/goutil/examples_test.go +++ b/internal/goutil/examples_test.go @@ -161,20 +161,25 @@ func ExampleInstall() { } func ExampleIsAlreadyUpToDate() { - // Create Version object with Current and Latest package version + // Create Version object with Current and Latest package and Go versions ver := goutil.Version{ Current: "v1.9.0", Latest: "v1.9.1", } + goVer := goutil.Version{ + Current: "go1.21.1", + Latest: "go1.22.4", + } + pkg := goutil.Package{Version: &ver, GoVersion: &goVer} // Check if Current is already up to date (expected: false) - if goutil.IsAlreadyUpToDate(ver) { + if pkg.IsAlreadyUpToDate() { fmt.Println("Example IsAlreadyUpToDate: already up to date.") } else { - fmt.Println("Example IsAlreadyUpToDate: outdated. Newer latest version exists.") + fmt.Println("Example IsAlreadyUpToDate: outdated. Newer latest version or installed Go toolchain exists.") } - // Output: Example IsAlreadyUpToDate: outdated. Newer latest version exists. + // Output: Example IsAlreadyUpToDate: outdated. Newer latest version or installed Go toolchain exists. } func ExampleNewGoPaths() { @@ -251,33 +256,10 @@ func ExampleGoPaths_StartDryRunMode() { } // ---------------------------------------------------------------------------- -// Type: Package +// +// Type: Package +// // ---------------------------------------------------------------------------- - -func ExamplePackage_CurrentToLatestStr() { - // Set the paths of the target binary - packages := goutil.GetPackageInformation([]string{"../../cmd/testdata/check_success/gal"}) - if len(packages) == 0 { - log.Fatal("example GetPackageInformation failed. The returned package information is nil") - } - - // test with the first package found - pkgInfo := packages[0] - - wantContain := "Already up-to-date" - got := pkgInfo.CurrentToLatestStr() - - if !strings.Contains(got, wantContain) { - log.Fatalf( - "example Package.CurrentToLatestStr failed. \nwant contain: %s\n got: %s", - wantContain, got, - ) - } - - fmt.Println("Example Package.CurrentToLatestStr: OK") - // Output: Example Package.CurrentToLatestStr: OK -} - func ExamplePackage_SetLatestVer() { packages := goutil.GetPackageInformation([]string{"../../cmd/testdata/check_success/gal"}) if len(packages) == 0 { @@ -308,26 +290,3 @@ func ExamplePackage_SetLatestVer() { fmt.Println("Example Package.SetLatestVer: OK") // Output: Example Package.SetLatestVer: OK } - -func ExamplePackage_VersionCheckResultStr() { - packages := goutil.GetPackageInformation([]string{"../../cmd/testdata/check_success/gal"}) - if len(packages) == 0 { - log.Fatal("example GetPackageInformation failed. The returned package information is nil") - } - - // test with the first package found - pkgInfo := packages[0] - - wantContain := "Already up-to-date" - got := pkgInfo.VersionCheckResultStr() - - if !strings.Contains(got, wantContain) { - log.Fatalf( - "example Package.VersionCheckResultStr failed. \nwant contain: %s\n got: %s", - wantContain, got, - ) - } - - fmt.Println("Example Package.VersionCheckResultStr: OK") - // Output: Example Package.VersionCheckResultStr: OK -} diff --git a/internal/goutil/goutil.go b/internal/goutil/goutil.go index ca6a557..3e0d9d9 100644 --- a/internal/goutil/goutil.go +++ b/internal/goutil/goutil.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "strings" "github.com/fatih/color" @@ -48,6 +49,8 @@ type Package struct { ModulePath string // Version store Package version (current and latest). Version *Version + // GoVersion stores version of Go toolchain + GoVersion *Version } // Version is package version information. @@ -73,30 +76,70 @@ func (p *Package) SetLatestVer() { // CurrentToLatestStr returns string about the current version and the latest version func (p *Package) CurrentToLatestStr() string { - if IsAlreadyUpToDate(*p.Version) { - return "Already up-to-date: " + color.GreenString(p.Version.Latest) + if p.IsAlreadyUpToDate() { + return "Already up-to-date: " + color.GreenString(p.Version.Latest) + " / " + color.GreenString(p.GoVersion.Current) } - return color.GreenString(p.Version.Current) + " to " + color.YellowString(p.Version.Latest) + var ret string + if p.Version.Current != p.Version.Latest { + ret += color.GreenString(p.Version.Current) + " to " + color.YellowString(p.Version.Latest) + } + if p.GoVersion.Current != p.GoVersion.Latest { + if len(ret) != 0 { + ret += ", " + } + ret += color.GreenString(p.GoVersion.Current) + " to " + color.YellowString(p.GoVersion.Latest) + } + return ret } // VersionCheckResultStr returns string about command version check. func (p *Package) VersionCheckResultStr() string { - if IsAlreadyUpToDate(*p.Version) { - return "Already up-to-date: " + color.GreenString(p.Version.Latest) + if p.IsAlreadyUpToDate() { + return "Already up-to-date: " + color.GreenString(p.Version.Latest) + " / " + color.GreenString(p.GoVersion.Current) + } + var ret string + // TODO: yellow only if latest > current + if p.Version.Current == p.Version.Latest { + ret += color.GreenString(p.Version.Current) + } else { + ret += "current: " + color.GreenString(p.Version.Current) + ", latest: " + if versionUpToDate(p.Version.Current, p.Version.Latest) { + ret += color.GreenString(p.Version.Latest) + } else { + ret += color.YellowString(p.Version.Latest) + } } - return "current: " + color.GreenString(p.Version.Current) + ", latest: " + color.YellowString(p.Version.Latest) + ret += " / " + if p.GoVersion.Current == p.GoVersion.Latest { + ret += color.GreenString(p.GoVersion.Current) + } else { + ret += "current: " + color.GreenString(p.GoVersion.Current) + ", installed: " + if versionUpToDate(p.GoVersion.Current, p.GoVersion.Latest) { + ret += color.GreenString(p.GoVersion.Latest) + } else { + ret += color.YellowString(p.GoVersion.Latest) + } + } + return ret } // IsAlreadyUpToDate return whether binary is already up to date or not. -func IsAlreadyUpToDate(ver Version) bool { - if ver.Current == ver.Latest { +func (p *Package) IsAlreadyUpToDate() bool { + if p.Version.Current == p.Version.Latest && p.GoVersion.Current == p.GoVersion.Latest { return true } - return strings.Compare( - strings.TrimLeft(ver.Current, "v"), - strings.TrimLeft(ver.Latest, "v"), - ) >= 0 + return versionUpToDate( + strings.TrimLeft(p.Version.Current, "v"), + strings.TrimLeft(p.Version.Latest, "v"), + ) && versionUpToDate( + strings.TrimLeft(p.GoVersion.Current, "go"), + strings.TrimLeft(p.GoVersion.Latest, "go"), + ) +} + +func versionUpToDate(current, available string) bool { + return current >= available } // NewGoPaths return GoPaths instance. @@ -288,6 +331,10 @@ func BinaryPathList(path string) ([]string, error) { // GetPackageInformation return golang package information. func GetPackageInformation(binList []string) []Package { pkgs := []Package{} + goVer, err := GetInstalledGoVersion() + if err != nil { + goVer = "unknown" + } for _, v := range binList { info, err := buildinfo.ReadFile(v) if err != nil { @@ -299,8 +346,11 @@ func GetPackageInformation(binList []string) []Package { ImportPath: info.Path, ModulePath: info.Main.Path, Version: NewVersion(), + GoVersion: NewVersion(), } pkg.Version.Current = info.Main.Version + pkg.GoVersion.Current = info.GoVersion + pkg.GoVersion.Latest = goVer pkgs = append(pkgs, pkg) } return pkgs @@ -318,3 +368,23 @@ func GetPackageVersion(cmdName string) string { } return info.Main.Version } + +var goVersionRegex = regexp.MustCompile(`(^|\s)(go[1-9]\S+)`) + +func GetInstalledGoVersion() (string, error) { + var stdout, stderr bytes.Buffer + cmd := exec.Command(goExe, "version") + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + return "", fmt.Errorf("can't check go version:\n%s", stderr.String()) + } + + if m := goVersionRegex.FindStringSubmatch(stdout.String()); m != nil { + return m[2], nil + } + + return "", fmt.Errorf("can't find go version string in %q", strings.TrimSpace(stdout.String())) +} diff --git a/internal/goutil/goutil_test.go b/internal/goutil/goutil_test.go index 33ef876..afd12e5 100644 --- a/internal/goutil/goutil_test.go +++ b/internal/goutil/goutil_test.go @@ -283,46 +283,58 @@ func TestInstallMaster_golden(t *testing.T) { func TestIsAlreadyUpToDate_golden(t *testing.T) { for i, test := range []struct { - curr string - latest string - expect bool + curr string + latest string + currGo string + latestGo string + expect bool }{ // Regular cases - {curr: "v1.9.0", latest: "v1.9.1", expect: false}, - {curr: "v1.9.0", latest: "v1.9.0", expect: true}, - {curr: "v1.9.1", latest: "v1.9.0", expect: true}, + {curr: "v1.9.0", latest: "v1.9.1", currGo: "go1.22.4", latestGo: "go1.22.4", expect: false}, + {curr: "v1.9.0", latest: "v1.9.0", currGo: "go1.22.4", latestGo: "go1.22.4", expect: true}, + {curr: "v1.9.1", latest: "v1.9.0", currGo: "go1.22.4", latestGo: "go1.22.4", expect: true}, + {curr: "v1.9.0", latest: "v1.9.0", currGo: "go1.22.1", latestGo: "go1.22.4", expect: false}, // Irregular cases (untagged versions) { - curr: "v0.0.0-20220913151710-7c6e287988f3", - latest: "v0.0.0-20210608161538-9736a4bde949", - expect: true, + curr: "v0.0.0-20220913151710-7c6e287988f3", + latest: "v0.0.0-20210608161538-9736a4bde949", + currGo: "go1.22.4", + latestGo: "go1.22.4", + expect: true, }, { - curr: "v0.0.0-20210608161538-9736a4bde949", - latest: "v0.0.0-20220913151710-7c6e287988f3", - expect: false, + curr: "v0.0.0-20210608161538-9736a4bde949", + latest: "v0.0.0-20220913151710-7c6e287988f3", + currGo: "go1.22.4", + latestGo: "go1.22.4", + expect: false, }, // Compatibility between go-style semver and pure-semver - {curr: "v1.9.0", latest: "1.9.1", expect: false}, - {curr: "v1.9.1", latest: "1.9.0", expect: true}, - {curr: "1.9.0", latest: "v1.9.1", expect: false}, - {curr: "1.9.1", latest: "v1.9.0", expect: true}, + {curr: "v1.9.0", latest: "1.9.1", currGo: "go1.22.4", latestGo: "go1.22.4", expect: false}, + {curr: "v1.9.1", latest: "1.9.0", currGo: "go1.22.4", latestGo: "go1.22.4", expect: true}, + {curr: "1.9.0", latest: "v1.9.1", currGo: "go1.22.4", latestGo: "go1.22.4", expect: false}, + {curr: "1.9.1", latest: "v1.9.0", currGo: "go1.22.4", latestGo: "go1.22.4", expect: true}, // Issue #36 - {curr: "v1.9.1-0.20220908165354-f7355b5d2afa", latest: "v1.9.0", expect: true}, + {curr: "v1.9.1-0.20220908165354-f7355b5d2afa", latest: "v1.9.0", currGo: "go1.22.4", latestGo: "go1.22.4", expect: true}, } { verTmp := Version{ Current: test.curr, Latest: test.latest, } + goVerTmp := Version{ + Current: test.currGo, + Latest: test.latestGo, + } + pkg := Package{Version: &verTmp, GoVersion: &goVerTmp} want := test.expect - got := IsAlreadyUpToDate(verTmp) + got := pkg.IsAlreadyUpToDate() // Assert to be equal if want != got { t.Errorf( - "case #%v failed. got: (\"%v\" >= \"%v\") = %v, want: %v", - i, test.curr, test.latest, got, want, + "case #%v failed. got: (\"%v\" >= \"%v\" / \"%v\" >= \"%v\") = %v, want: %v", + i, test.curr, test.latest, test.currGo, test.latestGo, got, want, ) } } @@ -545,6 +557,10 @@ func TestPackage_CurrentToLatestStr_not_up_to_date(t *testing.T) { Current: "v0.0.1", Latest: "v1.9.1", }, + GoVersion: &Version{ + Current: "go1.22.4", + Latest: "go1.22.4", + }, } // Assert to contain the expected message @@ -565,6 +581,10 @@ func TestPackage_VersionCheckResultStr_not_up_to_date(t *testing.T) { Current: "v0.0.1", Latest: "v1.9.1", }, + GoVersion: &Version{ + Current: "go1.22.4", + Latest: "go1.22.4", + }, } // Assert to contain the expected message @@ -575,3 +595,27 @@ func TestPackage_VersionCheckResultStr_not_up_to_date(t *testing.T) { t.Errorf("got: %v, want: %v", got, wantContain) } } + +func TestPackage_VersionCheckResultStr_go_not_up_to_date(t *testing.T) { + pkgInfo := Package{ + Name: "foo", + ImportPath: "github.com/dummy_name/dummy", + ModulePath: "github.com/dummy_name/dummy/foo", + Version: &Version{ + Current: "v1.9.1", + Latest: "v1.9.1", + }, + GoVersion: &Version{ + Current: "go1.22.1", + Latest: "go1.22.4", + }, + } + + // Assert to contain the expected message + wantContain := "current: go1.22.1, installed: go1.22.4" + got := pkgInfo.VersionCheckResultStr() + + if !strings.Contains(got, wantContain) { + t.Errorf("got: %v, want: %v", got, wantContain) + } +} diff --git a/internal/print/print.go b/internal/print/print.go index f3f6f80..2e3ec19 100644 --- a/internal/print/print.go +++ b/internal/print/print.go @@ -20,9 +20,11 @@ var ( // Info print information message at STDOUT in green. // This function is used to print some information (that is not error) to the user. +// +// NOTE: When we executed gup update, the standard output became quite wide. +// To make the information more readable for the user, I removed the 'gup:INFO:' part. func Info(msg string) { - fmt.Fprintf(Stdout, "%s:%s: %s\n", - cmdinfo.Name, color.GreenString("INFO "), msg) + fmt.Fprintf(Stdout, "%s\n", msg) } // Warn print warning message at STDERR in yellow. diff --git a/internal/print/print_test.go b/internal/print/print_test.go index 13bcf83..92c282b 100644 --- a/internal/print/print_test.go +++ b/internal/print/print_test.go @@ -27,7 +27,7 @@ func TestInfo(t *testing.T) { args: args{ msg: "test message", }, - want: []string{"gup:INFO : test message", ""}, + want: []string{"test message", ""}, }, } for _, tt := range tests {