From 2197f78bc0c2e6d80ea10fd5d955c252a7e801f4 Mon Sep 17 00:00:00 2001 From: EnumDev Date: Mon, 8 Sep 2025 10:16:23 +0300 Subject: [PATCH] Add 'check-versions' flag to bpm-repo --- src/bpm-repo/go.mod | 5 +- src/bpm-repo/main.go | 188 +++++++++++++++++++++++++++++++- src/bpm-utils-shared/package.go | 36 ++++++ 3 files changed, 223 insertions(+), 6 deletions(-) diff --git a/src/bpm-repo/go.mod b/src/bpm-repo/go.mod index 8ed4a7a..7170ff8 100644 --- a/src/bpm-repo/go.mod +++ b/src/bpm-repo/go.mod @@ -5,10 +5,7 @@ go 1.23 require ( bpm-utils-shared v1.0.0 github.com/spf13/pflag v1.0.10 -) - -require ( - gopkg.in/yaml.v3 v3.0.1 // indirect + gopkg.in/yaml.v3 v3.0.1 ) replace bpm-utils-shared => ../bpm-utils-shared diff --git a/src/bpm-repo/main.go b/src/bpm-repo/main.go index 5669727..ba6e33f 100644 --- a/src/bpm-repo/main.go +++ b/src/bpm-repo/main.go @@ -4,21 +4,30 @@ import ( bpmutilsshared "bpm-utils-shared" "bufio" "fmt" + "io/fs" "log" "maps" "os" + "os/exec" "path" + "path/filepath" "slices" "sort" "strings" + "time" flag "github.com/spf13/pflag" + "gopkg.in/yaml.v3" ) var createRepo = flag.BoolP("create", "c", false, "Create a new BPM repository") var updateDatabases = flag.BoolP("update-databases", "u", false, "Update update source and binary databases in current repository") +var checkVersions = flag.BoolP("check-versions", "v", false, "Get the latest version of each package") var listPackages = flag.BoolP("list", "l", false, "List packages") +var verbose = flag.Bool("verbose", false, "Show additional information about current operation") +var force = flag.BoolP("force", "f", false, "Force current operation to bypass certain conditions") + func main() { // Setup flags and help bpmutilsshared.SetupHelp("bpm-repo ", "Manage BPM repositories and databases") @@ -52,6 +61,14 @@ func main() { } bpmutilsshared.UpdateDatabases(repo) + } else if *checkVersions { + // Get current database + repo := bpmutilsshared.GetRepository() + if repo == "" { + log.Fatal("Error: this command may only be run inside a BPM repository") + } + + checkVersionsFunc(repo) } else if *listPackages { // Get current database repo := bpmutilsshared.GetRepository() @@ -59,7 +76,7 @@ func main() { log.Fatal("Error: this command may only be run inside a BPM repository") } - listPacakges(repo) + listPackagesFunc(repo) } else { bpmutilsshared.ShowHelp() } @@ -80,7 +97,149 @@ func createRepository(name, description string) { fmt.Println("Repository created successfully!") } -func listPacakges(repo string) { +func checkVersionsFunc(repo string) { + // Read environment files + err := readEnvFile(repo) + if err != nil { + log.Fatalf("Error: could not read environment file: %s", err) + } + + // Read version cache + type CachedVersionEntry struct { + LatestVersion string `yaml:"latest_version"` + Timestamp int64 `yaml:"timestamp"` + } + cachedVersions := make(map[string]CachedVersionEntry) + data, err := os.ReadFile(path.Join(repo, ".version-cache")) + if err == nil { + err := yaml.Unmarshal(data, &cachedVersions) + if err != nil { + cachedVersions = make(map[string]CachedVersionEntry) + } + } + + directories := make([]string, 0) + if flag.NArg() > 0 { + for _, dir := range flag.Args() { + if _, err := os.Stat(path.Join(repo, "source", dir, "pkg.info")); err != nil { + log.Fatalf("Error: could not find pkg.info file in directory (%s): %s", dir, err) + } + directories = append(directories, path.Join(repo, "source", dir)) + } + } else { + // Loop through each directory with a 'pkg.info' file + err := filepath.Walk(path.Join(repo, "source"), func(path string, info fs.FileInfo, err error) error { + if filepath.Base(path) == "pkg.info" { + directories = append(directories, filepath.Dir(path)) + } + return nil + }) + if err != nil { + log.Fatalf("Error: could not loop through all packages: %s", err) + } + } + + pkgsWithoutScript := make([]string, 0) + pkgsWithError := make(map[string]error) + pkgsWithUpdates := make(map[string]struct { + OldVersion string + NewVersion string + }, 0) + pkgsUpToDate := 0 + for _, dir := range directories { + pkgInfo, err := bpmutilsshared.ReadPacakgeInfoFromFile(path.Join(dir, "pkg.info")) + if err != nil { + log.Fatalf("Could not read package info: %s", err) + } + + // Check cached latest version + latestVersion := "" + if cachedVersion, ok := cachedVersions[pkgInfo.Name]; ok && !*force && time.Since(time.UnixMilli(cachedVersion.Timestamp)).Milliseconds() < 604800000 { + latestVersion = cachedVersion.LatestVersion + } else { + // Check whether check-version.sh script exists + if _, err := os.Stat(path.Join(dir, "check-version.sh")); err != nil { + pkgsWithoutScript = append(pkgsWithoutScript, pkgInfo.Name) + continue + } + + // Execute check-version.sh script + cmd := exec.Command("bash", "-e", path.Join(dir, "check-version.sh")) + cmd.Environ() + output, err := cmd.Output() + if err != nil { + pkgsWithError[pkgInfo.Name] = err + continue + } + latestVersion = strings.TrimSpace(string(output)) + + // Ensure latest version is valid + if latestVersion == "" || latestVersion == "null" { + pkgsWithError[pkgInfo.Name] = fmt.Errorf("invalid version number \"%s\"", latestVersion) + continue + } + + // Cache latest version + cachedVersions[pkgInfo.Name] = CachedVersionEntry{ + LatestVersion: latestVersion, + Timestamp: time.Now().UnixMilli(), + } + } + + // Compare versions + if pkgInfo.Version != latestVersion { + pkgsWithUpdates[pkgInfo.Name] = struct { + OldVersion string + NewVersion string + }{ + OldVersion: pkgInfo.Version, + NewVersion: latestVersion, + } + continue + } + + pkgsUpToDate++ + } + + // Save cached versions to file + data, err = yaml.Marshal(cachedVersions) + if err == nil { + err := os.WriteFile(path.Join(repo, ".version-cache"), data, 0644) + if err != nil { + log.Printf("Warning: could not write cached versions to file: %s", err) + } + } + + // Print updates + keys := slices.Collect(maps.Keys(pkgsWithUpdates)) + sort.Strings(keys) + for _, pkg := range keys { + fmt.Printf("Update available for package (%s): %s -> %s\n", pkg, pkgsWithUpdates[pkg].OldVersion, pkgsWithUpdates[pkg].NewVersion) + } + + if *verbose { + // Print packages without check-version.sh script + for _, pkg := range pkgsWithoutScript { + log.Printf("Warning: package (%s) has no check-version.sh script\n", pkg) + } + + // Print errors + keys = slices.Collect(maps.Keys(pkgsWithError)) + sort.Strings(keys) + for _, pkg := range keys { + log.Printf("Error: check-version.sh script for package (%s) failed: %s", pkg, pkgsWithError[pkg]) + } + } + + // Print summary + fmt.Println("----- Summary -----") + fmt.Println("Available updates:", len(pkgsWithUpdates)) + fmt.Println("Up to date:", pkgsUpToDate) + fmt.Println("Missing script:", len(pkgsWithoutScript)) + fmt.Println("Errors:", len(pkgsWithError)) +} + +func listPackagesFunc(repo string) { // Read databases sourceDatabase, err := bpmutilsshared.ReadDatabase(path.Join(repo, "source/database.bpmdb")) if err != nil { @@ -122,3 +281,28 @@ func listPacakges(repo string) { } } } + +func readEnvFile(repo string) error { + data, err := os.ReadFile(path.Join(repo, ".env")) + if os.IsNotExist(err) { + return nil + } else if err != nil { + return err + } + + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if line == "" || line[0] == '#' { + continue + } + + splitLine := strings.Split(line, "=") + if len(splitLine) != 2 { + return fmt.Errorf("invalid format") + } + + os.Setenv(splitLine[0], splitLine[1]) + } + + return nil +} diff --git a/src/bpm-utils-shared/package.go b/src/bpm-utils-shared/package.go index 46d96a4..f7886fe 100644 --- a/src/bpm-utils-shared/package.go +++ b/src/bpm-utils-shared/package.go @@ -1,6 +1,7 @@ package bpm_utils_shared import ( + "os" "os/exec" "gopkg.in/yaml.v3" @@ -60,3 +61,38 @@ func ReadPacakgeInfo(path string) (*PackageInfo, error) { return pkgInfo, nil } + +func ReadPacakgeInfoFromFile(path string) (*PackageInfo, error) { + // Extract package info using tar + output, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + pkgInfo := &PackageInfo{ + Name: "", + Description: "", + Version: "", + Revision: 1, + Url: "", + License: "", + Arch: "", + Type: "", + Keep: make([]string, 0), + Depends: make([]string, 0), + MakeDepends: make([]string, 0), + OptionalDepends: make([]string, 0), + Conflicts: make([]string, 0), + Replaces: make([]string, 0), + Provides: make([]string, 0), + SplitPackages: make([]*PackageInfo, 0), + } + + // Unmarshal yaml + err = yaml.Unmarshal(output, pkgInfo) + if err != nil { + return nil, err + } + + return pkgInfo, nil +}