mirror of
https://github.com/EnumeratedDev/bpm-utils.git
synced 2026-09-16 11:06:11 +00:00
376 lines
11 KiB
Go
376 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
bpmutilsshared "bpm-utils-shared"
|
|
"bufio"
|
|
"context"
|
|
"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 currentFlagSet *flag.FlagSet
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
log.Println("Error: no subcommand")
|
|
listSubcommands()
|
|
os.Exit(1)
|
|
}
|
|
|
|
subcommand := os.Args[1]
|
|
|
|
switch subcommand {
|
|
case "create-repo", "c":
|
|
// Setup flags and help
|
|
flagset := flag.NewFlagSet("create-repo", flag.ExitOnError)
|
|
setupFlagsAndHelp(flagset, fmt.Sprintf("bpm-repo %s <options>", subcommand), "Create a new BPM repository", os.Args[:1])
|
|
|
|
// Get current database
|
|
repo := bpmutilsshared.GetRepository()
|
|
if repo != "" {
|
|
log.Fatal("Error: this command cannot be run inside a BPM repository")
|
|
}
|
|
|
|
scanner := bufio.NewReader(os.Stdin)
|
|
fmt.Print("Repository name: ")
|
|
name, err := scanner.ReadString('\n')
|
|
if err != nil {
|
|
log.Fatalf("Error: could not read user input: %s", err)
|
|
}
|
|
fmt.Print("Repository description: ")
|
|
desc, err := scanner.ReadString('\n')
|
|
if err != nil {
|
|
log.Fatalf("Error: could not read user input: %s", err)
|
|
}
|
|
|
|
createRepository(strings.TrimSpace(name), strings.TrimSpace(desc))
|
|
case "update-db", "u":
|
|
// Setup flags and help
|
|
flagset := flag.NewFlagSet("update-db", flag.ExitOnError)
|
|
setupFlagsAndHelp(flagset, fmt.Sprintf("bpm-repo %s <options>", subcommand), "Update update source and binary databases in current repository", os.Args[:1])
|
|
|
|
// Get current database
|
|
repo := bpmutilsshared.GetRepository()
|
|
if repo == "" {
|
|
log.Fatal("Error: this command may only be run inside a BPM repository")
|
|
}
|
|
|
|
bpmutilsshared.UpdateDatabases(repo)
|
|
case "list", "l":
|
|
flagset := flag.NewFlagSet("list", flag.ExitOnError)
|
|
setupFlagsAndHelp(flagset, fmt.Sprintf("bpm-repo %s <options>", subcommand), "List packages", os.Args[:1])
|
|
|
|
// Get current database
|
|
repo := bpmutilsshared.GetRepository()
|
|
if repo == "" {
|
|
log.Fatal("Error: this command may only be run inside a BPM repository")
|
|
}
|
|
|
|
listPackagesFunc(repo)
|
|
case "check-versions", "v":
|
|
// Setup flags and help
|
|
flagset := flag.NewFlagSet("check-versions", flag.ExitOnError)
|
|
flagset.BoolP("verbose", "v", false, "Show additional information about the current operation")
|
|
flagset.BoolP("force", "f", false, "Force current operation to bypass certain conditions")
|
|
setupFlagsAndHelp(flagset, fmt.Sprintf("bpm-repo %s <options>", subcommand), "Manage BPM repositories and databases", os.Args[2:])
|
|
currentFlagSet = flagset
|
|
|
|
// Get current database
|
|
repo := bpmutilsshared.GetRepository()
|
|
if repo == "" {
|
|
log.Fatal("Error: this command may only be run inside a BPM repository")
|
|
}
|
|
|
|
checkVersionsFunc(repo)
|
|
default:
|
|
log.Println("Error: unknown subcommand")
|
|
listSubcommands()
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func createRepository(name, description string) {
|
|
err := os.Mkdir(name, 0755)
|
|
if err != nil {
|
|
log.Fatalf("Error: could not create directory: %s", err)
|
|
}
|
|
|
|
contents := fmt.Sprintf("name: %s\ndescription: %s\n", name, description)
|
|
err = os.WriteFile(path.Join(name, "bpm-repo.conf"), []byte(contents), 0644)
|
|
if err != nil {
|
|
log.Fatalf("Error: could not write to file: %s", err)
|
|
}
|
|
|
|
fmt.Println("Repository created successfully!")
|
|
}
|
|
|
|
func checkVersionsFunc(repo string) {
|
|
// Get flags
|
|
verbose, _ := currentFlagSet.GetBool("verbose")
|
|
force, _ := currentFlagSet.GetBool("force")
|
|
|
|
// 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 currentFlagSet.NArg() > 0 {
|
|
for _, dir := range currentFlagSet.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)
|
|
pkgsIgnored := 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)
|
|
}
|
|
|
|
if verbose {
|
|
fmt.Printf("Checking version for package (%s)...\n", pkgInfo.Name)
|
|
}
|
|
|
|
// 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
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, "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))
|
|
|
|
// Check if package should be ignored
|
|
if latestVersion == "ignore" {
|
|
pkgsIgnored = append(pkgsIgnored, pkgInfo.Name)
|
|
|
|
// Remove cached status
|
|
delete(cachedVersions, pkgInfo.Name)
|
|
|
|
continue
|
|
}
|
|
|
|
// 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 ignored packages
|
|
for _, pkg := range pkgsIgnored {
|
|
log.Printf("Warning: package (%s) was ignored\n", pkg)
|
|
}
|
|
|
|
// 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("Ignored: ", len(pkgsIgnored))
|
|
fmt.Println("Errors:", len(pkgsWithError))
|
|
}
|
|
|
|
func listPackagesFunc(repo string) {
|
|
// Read databases
|
|
sourceDatabase, err := bpmutilsshared.ReadDatabase(path.Join(repo, "source/database.bpmdb"))
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
binaryDatabase, err := bpmutilsshared.ReadDatabase(path.Join(repo, "binary/database.bpmdb"))
|
|
if err != nil {
|
|
binaryDatabase = &bpmutilsshared.BPMDatabase{}
|
|
}
|
|
|
|
// Sort database entries
|
|
entriesSorted := slices.Collect(maps.Values(sourceDatabase.Entries))
|
|
sort.Slice(entriesSorted, func(i, j int) bool {
|
|
return entriesSorted[i].PackageInfo.Name < entriesSorted[j].PackageInfo.Name
|
|
})
|
|
|
|
// Loop through each entry
|
|
for _, entry := range entriesSorted {
|
|
if len(entry.PackageInfo.SplitPackages) > 0 {
|
|
for _, splitPkg := range entry.PackageInfo.SplitPackages {
|
|
// Handle split packages
|
|
additionalText := ""
|
|
if binaryEntry, ok := binaryDatabase.Entries[splitPkg.Name]; !ok {
|
|
additionalText += "(Binary package missing) "
|
|
} else if fmt.Sprintf("%s-%d", binaryEntry.PackageInfo.Version, binaryEntry.PackageInfo.Revision) != fmt.Sprintf("%s-%d", entry.PackageInfo.Version, entry.PackageInfo.Revision) {
|
|
additionalText += "(Binary package version mismatch) "
|
|
}
|
|
fmt.Printf("%s %s-%d %s\n", splitPkg.Name, entry.PackageInfo.Version, entry.PackageInfo.Revision, additionalText)
|
|
}
|
|
} else {
|
|
additionalText := ""
|
|
if binaryEntry, ok := binaryDatabase.Entries[entry.PackageInfo.Name]; !ok {
|
|
additionalText += "(Binary package missing) "
|
|
} else if fmt.Sprintf("%s-%d", binaryEntry.PackageInfo.Version, binaryEntry.PackageInfo.Revision) != fmt.Sprintf("%s-%d", entry.PackageInfo.Version, entry.PackageInfo.Revision) {
|
|
additionalText += "(Binary package version mismatch) "
|
|
}
|
|
fmt.Printf("%s %s-%d %s\n", entry.PackageInfo.Name, entry.PackageInfo.Version, entry.PackageInfo.Revision, additionalText)
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func listSubcommands() {
|
|
fmt.Println("Usage: bpm-repo <subcommand> <options>")
|
|
fmt.Println("Description: Manage BPM repositories and databases")
|
|
fmt.Println("Subcommands:")
|
|
fmt.Println(" c, create-repo Create a new BPM repository")
|
|
fmt.Println(" u, update-db Update update source and binary databases in current repositor")
|
|
fmt.Println(" v, check-versions Manage BPM repositories and databases")
|
|
fmt.Println(" l, list List packages")
|
|
}
|
|
|
|
func setupFlagsAndHelp(flagset *flag.FlagSet, usage, desc string, args []string) {
|
|
flagset.Usage = func() {
|
|
fmt.Println("Usage: " + usage)
|
|
fmt.Println("Description: " + desc)
|
|
fmt.Println("Options:")
|
|
flagset.PrintDefaults()
|
|
}
|
|
flagset.Parse(args)
|
|
}
|