diff --git a/src/bpm/main.go b/src/bpm/main.go index 04330c7..6c35085 100644 --- a/src/bpm/main.go +++ b/src/bpm/main.go @@ -239,23 +239,22 @@ func showPackageInfo() { } for n, pkg := range packages { - if showDatabaseInfo { - // Split package name and required version - pkgName, _, _ := bpmlib.SplitPkgNameAndVersion(pkg) + // Deconstruct package string + d, err := bpmlib.DeconstructPackageString(pkg) + if err != nil { + log.Printf("Error: could not deconstruct package string: %s\n", err) + exitCode = 1 + return + } - entry := bpmlib.ResolveDatabaseEntry(pkgName, rootDir) + if showDatabaseInfo { + entry := bpmlib.ResolveDatabaseEntry(d, rootDir) if entry == nil { log.Printf("Error: could not find package (%s) in any database\n", pkg) exitCode = 1 return } - if !bpmlib.EvaluateDependency(pkg, entry.Info.Version) { - log.Printf("Error: could not find package (%s) in any database\n", pkg) - exitCode = 1 - continue - } - if n != 0 { fmt.Println() } @@ -266,8 +265,8 @@ func showPackageInfo() { var bpmpkg *bpmlib.BPMPackage isFile := false - if stat, err := os.Stat(pkg); err == nil && !stat.IsDir() { - bpmpkg, err = bpmlib.ReadPackage(pkg) + if stat, err := os.Stat(d.PkgName); err == nil && !stat.IsDir() { + bpmpkg, err = bpmlib.ReadPackage(d.PkgName) if err != nil { log.Printf("Error: could not read package: %s\n", err) exitCode = 1 @@ -275,13 +274,10 @@ func showPackageInfo() { } isFile = true } else { - // Split package name and required version - pkgName, _, _ := bpmlib.SplitPkgNameAndVersion(pkg) - - if providers := bpmlib.GetVirtualPackageInfo(pkgName, rootDir); len(providers) > 0 { + if providers := bpmlib.GetVirtualPackageInfo(d.PkgName, rootDir); len(providers) > 0 { bpmpkg = bpmlib.GetPackage(providers[0].Name, rootDir) } else { - bpmpkg = bpmlib.GetPackage(pkgName, rootDir) + bpmpkg = bpmlib.GetPackage(d.PkgName, rootDir) } } @@ -291,7 +287,7 @@ func showPackageInfo() { return } - if !bpmlib.EvaluateDependency(pkg, bpmpkg.PkgInfo.Version) { + if !bpmlib.EvaluatePackageString(bpmpkg.PkgInfo, d) { log.Printf("Error: package (%s) is not installed\n", pkg) exitCode = 1 return @@ -301,7 +297,7 @@ func showPackageInfo() { fmt.Println() } if isFile { - abs, err := filepath.Abs(pkg) + abs, err := filepath.Abs(d.PkgName) if err != nil { log.Printf("Error: could not get absolute path of file (%s)\n", abs) exitCode = 1 @@ -632,7 +628,7 @@ func installPackages() { // Create installation operation operation, err := bpmlib.InstallPackages(rootDir, ir, reinstallPackages, installRuntime, force, !skipChecks, verbose, packages...) - if errors.As(err, &bpmlib.PackageNotFoundErr{}) || errors.As(err, &bpmlib.DependencyNotFoundErr{}) || errors.As(err, &bpmlib.PackageConflictErr{}) { + if errors.As(err, &bpmlib.PackageNotResolvedErr{}) || errors.As(err, &bpmlib.PackageConflictErr{}) { log.Printf("Error: %s", err) exitCode = 1 return @@ -787,7 +783,7 @@ func removePackages() { // Create remove operation operation, err := bpmlib.RemovePackages(rootDir, force, cleanupPackages, packages...) - if errors.As(err, &bpmlib.PackageNotFoundErr{}) || errors.As(err, &bpmlib.DependencyNotFoundErr{}) || errors.As(err, &bpmlib.PackageConflictErr{}) { + if errors.As(err, &bpmlib.PackageNotResolvedErr{}) || errors.As(err, &bpmlib.PackageConflictErr{}) { log.Printf("Error: %s", err) exitCode = 1 return @@ -931,7 +927,7 @@ func doCleanup() { // Create cleanup operation operation, err := bpmlib.CleanupPackages(cleanupMakeDepends, rootDir) - if errors.As(err, &bpmlib.PackageNotFoundErr{}) || errors.As(err, &bpmlib.DependencyNotFoundErr{}) || errors.As(err, &bpmlib.PackageConflictErr{}) { + if errors.As(err, &bpmlib.PackageNotResolvedErr{}) || errors.As(err, &bpmlib.PackageConflictErr{}) { log.Printf("Error: %s", err) exitCode = 1 return @@ -1095,7 +1091,7 @@ func updatePackages() { // Create update operation operation, err := bpmlib.UpdatePackages(rootDir, !noSync, allowDowngrades, force, !skipChecks, verbose) - if errors.As(err, &bpmlib.PackageNotFoundErr{}) || errors.As(err, &bpmlib.DependencyNotFoundErr{}) || errors.As(err, &bpmlib.PackageConflictErr{}) { + if errors.As(err, &bpmlib.PackageNotResolvedErr{}) || errors.As(err, &bpmlib.PackageConflictErr{}) { log.Printf("Error: %s", err) exitCode = 1 return @@ -1300,8 +1296,16 @@ func compilePackage() { // Compile packages for _, sourcePackage := range sourcePackages { - if _, err := os.Stat(sourcePackage); os.IsNotExist(err) { - log.Printf("Error: file (%s) does not exist!", sourcePackage) + // Deconstruct package string + d, err := bpmlib.DeconstructPackageString(sourcePackage) + if err != nil { + log.Printf("Error: could not deconstruct package string: %s\n", err) + exitCode = 1 + return + } + + if _, err := os.Stat(d.PkgName); os.IsNotExist(err) { + log.Printf("Error: file (%s) does not exist!", d.PkgName) exitCode = 1 return } @@ -1321,6 +1325,12 @@ func compilePackage() { return } + if !bpmlib.EvaluatePackageString(bpmpkg.PkgInfo, d) { + log.Printf("Error: could not evaluate package: %s\n", sourcePackage) + exitCode = 1 + return + } + // Get common, make and check dependencies totalDepends := make([]string, 0) totalDepends = append(totalDepends, bpmpkg.PkgInfo.Depends...) @@ -1350,10 +1360,13 @@ func compilePackage() { // Ignore packages that can't be found in any databases if force { unmetDepends = slices.DeleteFunc(unmetDepends, func(s string) bool { - if bpmlib.ResolveDatabaseEntry(s, rootDir) == nil { - return true + // Deconstruct package string + d, err := bpmlib.DeconstructPackageString(s) + if err != nil { + return false } - return false + + return bpmlib.ResolveDatabaseEntry(d, rootDir) == nil }) } @@ -1485,7 +1498,7 @@ func compilePackage() { return } - outputBpmPackages, err := bpmlib.CompileSourcePackage(sourcePackage, outputDirectory, compilationJobs, skipChecks, keepCompilationFiles, verbose) + outputBpmPackages, err := bpmlib.CompileSourcePackage(sourcePackage, outputDirectory, d.Flags, compilationJobs, skipChecks, keepCompilationFiles, verbose) if err != nil { // Remove unused packages cleanupFunc() diff --git a/src/bpmlib/compilation.go b/src/bpmlib/compilation.go index 85bc2bd..861a49f 100644 --- a/src/bpmlib/compilation.go +++ b/src/bpmlib/compilation.go @@ -24,7 +24,7 @@ import ( var rootCompilationUID = "65534" var rootCompilationGID = "65534" -func CompileSourcePackage(archiveFilename, outputDirectory string, compilationJobs int, skipChecks, keepCompilationFiles, verbose bool) (outputBpmPackages map[string]string, err error) { +func CompileSourcePackage(archiveFilename, outputDirectory string, flags map[string]string, compilationJobs int, skipChecks, keepCompilationFiles, verbose bool) (outputBpmPackages map[string]string, err error) { // Set compilation jobs if compilationJobs <= 0 || compilationJobs > runtime.NumCPU() { compilationJobs = runtime.NumCPU() @@ -146,6 +146,15 @@ func CompileSourcePackage(archiveFilename, outputDirectory string, compilationJo env = append(env, "BPM_PKG_URL="+bpmpkg.PkgInfo.Url) env = append(env, "BPM_PKG_ARCH="+bpmpkg.PkgInfo.OutputArch) env = append(env, "BPM_JOBS="+strconv.Itoa(compilationJobs)) + for _, flag := range bpmpkg.PkgInfo.Flags { + if value, ok := flags[flag.Name]; ok { + env = append(env, "BPM_PKG_FLAG_"+strings.ToUpper(flag.Name)+"="+value) + fmt.Printf("Package flag: %s=%s\n", flag.Name, value) + } else { + env = append(env, "BPM_PKG_FLAG_"+strings.ToUpper(flag.Name)+"="+flag.DefaultValue) + fmt.Printf("Package flag: %s=%s\n", flag.Name, flag.DefaultValue) + } + } env = append(env, CompilationBPMConfig.CompilationEnvironment...) // Set common flags used for limiting job count @@ -381,6 +390,31 @@ func CompileSourcePackage(archiveFilename, outputDirectory string, compilationJo pkgInfo.SplitPackages = nil pkgInfo.Downloads = nil + // Set built flag values and add their dependencies + for i, flag := range pkgInfo.Flags { + if value, ok := flags[flag.Name]; ok { + flag.BuiltValue = value + } else { + flag.BuiltValue = flag.DefaultValue + } + + acceptedValueIndex := slices.IndexFunc(flag.AcceptedValues, func(acceptedValue PackageAcceptedValue) bool { + return acceptedValue.Value == flag.BuiltValue + }) + if acceptedValueIndex < 0 { + return nil, fmt.Errorf("flag value not accepted: %s=%s", flag.Name, flag.BuiltValue) + } + acceptedValue := flag.AcceptedValues[acceptedValueIndex] + + pkgInfo.Flags[i] = flag + + pkgInfo.Depends = append(pkgInfo.Depends, acceptedValue.Depends...) + pkgInfo.RuntimeDepends = append(pkgInfo.RuntimeDepends, acceptedValue.RuntimeDepends...) + pkgInfo.OptionalDepends = append(pkgInfo.OptionalDepends, acceptedValue.OptionalDepends...) + pkgInfo.MakeDepends = append(pkgInfo.MakeDepends, acceptedValue.MakeDepends...) + pkgInfo.CheckDepends = append(pkgInfo.CheckDepends, acceptedValue.CheckDepends...) + } + // Marshal package info pkgInfoBytes, err := yaml.Marshal(pkgInfo) if err != nil { diff --git a/src/bpmlib/databases.go b/src/bpmlib/databases.go index 04d105e..1fd47f9 100644 --- a/src/bpmlib/databases.go +++ b/src/bpmlib/databases.go @@ -42,7 +42,7 @@ type BPMDatabaseEntry struct { Database *BPMDatabase } -var BPMDatabases = make(map[string]*BPMDatabase) +var BPMDatabases = make([]*BPMDatabase, 0) func (db *BPMDatabase) ContainsPackage(pkg string) bool { _, ok := db.Entries[pkg] @@ -135,7 +135,7 @@ func (db *configDatabase) ReadLocalDatabase() error { } } - BPMDatabases[db.Name] = database + BPMDatabases = append(BPMDatabases, database) return nil } @@ -207,61 +207,76 @@ func ReadLocalDatabaseFiles() (err error) { return nil } -func ResolveDatabaseEntry(pkg string, rootDir string) *BPMDatabaseEntry { - entry := GetDatabaseEntry(pkg) - if entry != nil { - return entry - } +func ResolveDatabaseEntry(d DeconstructedPackageString, rootDir string) *BPMDatabaseEntry { + results := SearchDatabaseEntries(d.PkgName) + for _, result := range results { - installedProviders := GetVirtualPackageInfo(pkg, rootDir) - if len(installedProviders) > 0 { - entry := GetDatabaseEntry(installedProviders[0].Name) - if entry != nil { - return entry + if EvaluatePackageString(result.Info, d) { + return result } } - databaseProviders := GetDatabaseVirtualPackageEntry(pkg) - if len(databaseProviders) > 0 { - entry := GetDatabaseEntry(databaseProviders[0].Info.Name) - if entry != nil { - return entry + installedProviders := GetVirtualPackageInfo(d.PkgName, rootDir) + for _, provider := range installedProviders { + results := SearchDatabaseEntries(provider.Name) + for _, result := range results { + if EvaluatePackageString(result.Info, d) { + return result + } + } + } + + databaseProviders := SearchDatabaseVirtualPackageProviders(d.PkgName) + for _, provider := range databaseProviders { + results := SearchDatabaseEntries(provider.Info.Name) + for _, result := range results { + if EvaluatePackageString(result.Info, d) { + return result + } } } return nil } -func GetDatabaseEntry(pkg string) *BPMDatabaseEntry { +func SearchDatabaseEntries(pkg string) (results []*BPMDatabaseEntry) { split := strings.Split(pkg, "/") if len(split) == 1 { pkgName := strings.TrimSpace(split[0]) if pkgName == "" { - return nil + return results } for _, db := range BPMDatabases { if db.ContainsPackage(pkgName) { - return db.Entries[pkgName] + results = append(results, db.Entries[pkgName]) } } - return nil + return results } else if len(split) == 2 { dbName := strings.TrimSpace(split[0]) pkgName := strings.TrimSpace(split[1]) if dbName == "" || pkgName == "" { - return nil + return results } - db := BPMDatabases[dbName] - if db == nil || !db.ContainsPackage(pkgName) { - return nil + dbIndex := slices.IndexFunc(BPMDatabases, func(db *BPMDatabase) bool { + return db.Name == dbName + }) + if dbIndex < 0 { + return results } - return db.Entries[pkgName] - } else { - return nil + + db := BPMDatabases[dbIndex] + if !db.ContainsPackage(pkgName) { + return results + } + + results = append(results, db.Entries[pkgName]) } + + return results } -func GetDatabaseVirtualPackageEntry(vpkg string) (providers []*BPMDatabaseEntry) { +func SearchDatabaseVirtualPackageProviders(vpkg string) (providers []*BPMDatabaseEntry) { for _, db := range BPMDatabases { providers = append(providers, db.VirtualPackages[vpkg]...) } @@ -336,8 +351,12 @@ func (entry *BPMDatabaseEntry) GetEntryDependants() (dependants []string) { // Add installed package to list if its dependencies include pkgName if slices.ContainsFunc(e.Info.Depends, func(n string) bool { - n, _, _ = SplitPkgNameAndVersion(n) - return n == entry.Info.Name + d, err := DeconstructPackageString(n) + if err != nil { + return false + } + + return d.PkgName == entry.Info.Name }) { dependantsMap[e.Info.Name] = append(dependantsMap[e.Info.Name], db.Name) continue @@ -345,8 +364,12 @@ func (entry *BPMDatabaseEntry) GetEntryDependants() (dependants []string) { // Add installed package to list if its runtime dependencies include pkgName if slices.ContainsFunc(e.Info.RuntimeDepends, func(n string) bool { - n, _, _ = SplitPkgNameAndVersion(n) - return n == entry.Info.Name + d, err := DeconstructPackageString(n) + if err != nil { + return false + } + + return d.PkgName == entry.Info.Name }) { dependantsMap[e.Info.Name] = append(dependantsMap[e.Info.Name], db.Name) continue @@ -356,8 +379,12 @@ func (entry *BPMDatabaseEntry) GetEntryDependants() (dependants []string) { for _, vpkg := range entry.Info.Provides { // Add installed package to list if its dependencies contain a provided virtual package if slices.ContainsFunc(e.Info.Depends, func(n string) bool { - n, _, _ = SplitPkgNameAndVersion(n) - return n == vpkg + d, err := DeconstructPackageString(n) + if err != nil { + return false + } + + return d.PkgName == vpkg }) { dependantsMap[e.Info.Name] = append(dependantsMap[e.Info.Name], db.Name) break @@ -365,8 +392,12 @@ func (entry *BPMDatabaseEntry) GetEntryDependants() (dependants []string) { // Add installed package to list if its runtime dependencies contain a provided virtual package if slices.ContainsFunc(e.Info.RuntimeDepends, func(n string) bool { - n, _, _ = SplitPkgNameAndVersion(n) - return n == vpkg + d, err := DeconstructPackageString(n) + if err != nil { + return false + } + + return d.PkgName == vpkg }) { dependantsMap[e.Info.Name] = append(dependantsMap[e.Info.Name], db.Name) break @@ -399,13 +430,13 @@ func (entry *BPMDatabaseEntry) GetEntryOptionalDependants() (dependants []string for _, db := range BPMDatabases { for _, e := range db.Entries { if slices.ContainsFunc(e.Info.OptionalDepends, func(n string) bool { - // Remove optional dependency comment - n = strings.SplitN(n, ":", 2)[0] + // Deconstruct package string + d, err := DeconstructPackageString(n) + if err != nil { + return false + } - // Remove required version - n, _, _ = SplitPkgNameAndVersion(n) - - return n == entry.Info.Name + return d.PkgName == entry.Info.Name }) { dependantsMap[e.Info.Name] = append(dependantsMap[e.Info.Name], e.Database.Name) } @@ -436,8 +467,13 @@ func (entry *BPMDatabaseEntry) GetEntryMakeDependants() (dependants []string) { for _, db := range BPMDatabases { for _, e := range db.Entries { if slices.ContainsFunc(e.Info.MakeDepends, func(n string) bool { - n, _, _ = SplitPkgNameAndVersion(n) - return n == entry.Info.Name + // Deconstruct package string + d, err := DeconstructPackageString(n) + if err != nil { + return false + } + + return d.PkgName == entry.Info.Name }) { dependantsMap[e.Info.Name] = append(dependantsMap[e.Info.Name], e.Database.Name) } @@ -498,7 +534,7 @@ func (entry *BPMDatabaseEntry) CreateReadableInfo(rootDir string, showBytes bool builder.WriteString(" - " + val) // Show virtual package providers - if providers := GetDatabaseVirtualPackageEntry(val); len(providers) > 0 { + if providers := SearchDatabaseVirtualPackageProviders(val); len(providers) > 0 { builder.WriteString(" (") for i, vpkg := range providers { if i == len(providers)-1 { @@ -528,6 +564,64 @@ func (entry *BPMDatabaseEntry) CreateReadableInfo(rootDir string, showBytes bool } builder.WriteString("Type: " + entry.Info.Type + "\n") + // Flags + if entry.Info.Type == "binary" { + var flags []string + for _, flag := range entry.Info.Flags { + flags = append(flags, fmt.Sprintf("%s=%s", flag.Name, flag.BuiltValue)) + } + builderWriteArray("Built with flags:", flags, false) + } else { + if len(entry.Info.Flags) > 0 { + builder.WriteString("Available flags (") + builder.WriteString(strconv.Itoa(len(entry.Info.Flags))) + builder.WriteString("):\n") + for _, flag := range entry.Info.Flags { + builder.WriteString(" - Flag: ") + builder.WriteString(flag.Name) + builder.WriteRune('\n') + if flag.DefaultValue != "" { + builder.WriteString(" Default value: ") + builder.WriteString(flag.DefaultValue) + builder.WriteRune('\n') + } + if len(flag.AcceptedValues) > 0 { + builder.WriteString(" Accepted values (") + builder.WriteString(strconv.Itoa(len(flag.AcceptedValues))) + builder.WriteString("):\n") + + for i, acceptedValue := range flag.AcceptedValues { + writeValueDepends := func(text string, depends []string) { + if len(depends) > 0 { + builder.WriteString(" ") + builder.WriteString(text) + builder.WriteString(" (") + builder.WriteString(strconv.Itoa(len(depends))) + builder.WriteString("): ") + for _, depend := range depends { + if i != 0 { + builder.WriteString(", ") + } + builder.WriteString(depend) + } + builder.WriteRune('\n') + } + } + + builder.WriteString(" - Value: ") + builder.WriteString(acceptedValue.Value) + builder.WriteRune('\n') + writeValueDepends("Dependencies", acceptedValue.Depends) + writeValueDepends("Make Dependencies", acceptedValue.MakeDepends) + writeValueDepends("Check Dependencies", acceptedValue.CheckDepends) + writeValueDepends("Runtime Dependencies", acceptedValue.RuntimeDepends) + writeValueDepends("Optional Dependencies", acceptedValue.OptionalDepends) + } + } + } + } + } + // Dependencies builderWriteDependencyArray("Dependencies", entry.Info.Depends) if entry.Info.Type == "source" { @@ -546,7 +640,7 @@ func (entry *BPMDatabaseEntry) CreateReadableInfo(rootDir string, showBytes bool } // Show virtual package providers - if providers := GetDatabaseVirtualPackageEntry(dependSplit[0]); len(providers) > 0 { + if providers := SearchDatabaseVirtualPackageProviders(dependSplit[0]); len(providers) > 0 { builder.WriteString(" (") for i, vpkg := range providers { if i == len(providers)-1 { diff --git a/src/bpmlib/dependencies.go b/src/bpmlib/dependencies.go index b497132..bbd895b 100644 --- a/src/bpmlib/dependencies.go +++ b/src/bpmlib/dependencies.go @@ -1,6 +1,8 @@ package bpmlib import ( + "fmt" + "maps" "slices" "strings" ) @@ -21,8 +23,13 @@ func (pkgInfo *PackageInfo) GetPackageDependants(rootDir string, skipMultiplePro // Add installed package to list if its dependencies include pkgName if slices.ContainsFunc(installedPkg.Depends, func(n string) bool { - n, _, _ = SplitPkgNameAndVersion(n) - return n == pkgInfo.Name + // Deconstruct package string + d, err := DeconstructPackageString(n) + if err != nil { + return false + } + + return d.PkgName == pkgInfo.Name }) { dependants = append(dependants, installedPkg.Name) continue @@ -30,8 +37,13 @@ func (pkgInfo *PackageInfo) GetPackageDependants(rootDir string, skipMultiplePro // Add installed package to list if its runtime dependencies include pkgName if slices.ContainsFunc(installedPkg.RuntimeDepends, func(n string) bool { - n, _, _ = SplitPkgNameAndVersion(n) - return n == pkgInfo.Name + // Deconstruct package string + d, err := DeconstructPackageString(n) + if err != nil { + return false + } + + return d.PkgName == pkgInfo.Name }) { dependants = append(dependants, installedPkg.Name) continue @@ -45,8 +57,13 @@ func (pkgInfo *PackageInfo) GetPackageDependants(rootDir string, skipMultiplePro // Add installed package to list if its dependencies contain a provided virtual package if slices.ContainsFunc(installedPkg.Depends, func(n string) bool { - n, _, _ = SplitPkgNameAndVersion(n) - return n == vpkg + // Deconstruct package string + d, err := DeconstructPackageString(n) + if err != nil { + return false + } + + return d.PkgName == vpkg }) { dependants = append(dependants, installedPkg.Name) break @@ -54,8 +71,13 @@ func (pkgInfo *PackageInfo) GetPackageDependants(rootDir string, skipMultiplePro // Add installed package to list if its runtime dependencies contain a provided virtual package if slices.ContainsFunc(installedPkg.RuntimeDepends, func(n string) bool { - n, _, _ = SplitPkgNameAndVersion(n) - return n == vpkg + // Deconstruct package string + d, err := DeconstructPackageString(n) + if err != nil { + return false + } + + return d.PkgName == vpkg }) { dependants = append(dependants, installedPkg.Name) break @@ -82,13 +104,13 @@ func (pkgInfo *PackageInfo) GetPackageOptionalDependants(rootDir string) (depend // Add installed package to list if its optional dependencies include pkgName if slices.ContainsFunc(installedPkg.OptionalDepends, func(n string) bool { - // Remove optional dependency comment - n = strings.SplitN(n, ":", 2)[0] + // Deconstruct package string + d, err := DeconstructPackageString(n) + if err != nil { + return false + } - // Remove required version - n, _, _ = SplitPkgNameAndVersion(n) - - return n == pkgInfo.Name + return d.PkgName == pkgInfo.Name }) { dependants = append(dependants, installedPkg.Name) continue @@ -98,13 +120,13 @@ func (pkgInfo *PackageInfo) GetPackageOptionalDependants(rootDir string) (depend for _, vpkg := range pkgInfo.Provides { // Add installed package to list if its optional dependencies contain a provided virtual package if slices.ContainsFunc(installedPkg.OptionalDepends, func(n string) bool { - // Remove optional dependency comment - n = strings.SplitN(n, ":", 2)[0] + // Deconstruct package string + d, err := DeconstructPackageString(n) + if err != nil { + return false + } - // Remove required version - n, _, _ = SplitPkgNameAndVersion(n) - - return n == vpkg + return d.PkgName == vpkg }) { dependants = append(dependants, installedPkg.Name) break @@ -117,49 +139,74 @@ func (pkgInfo *PackageInfo) GetPackageOptionalDependants(rootDir string) (depend type ResolvedPackage struct { DatabaseEntry *BPMDatabaseEntry + Flags map[string]string InstallationReason InstallationReason } -func ResolveDependencies(pkgInfo *PackageInfo, resolvedVirtualPackages map[string]string, includeRuntimeDepends bool, rootDir string) (resolved []ResolvedPackage, unresolved []string) { +func ResolveDependencies(pkgInfo *PackageInfo, flags, resolvedVirtualPackages map[string]string, includeRuntimeDepends bool, rootDir string) (resolved []ResolvedPackage, unresolved map[string]string) { + unresolved = make(map[string]string) visited := make([]string, 0) - var dfs func(resolvedPkg *PackageInfo) - dfs = func(pkgInfo *PackageInfo) { + var dfs func(resolvedPkg *PackageInfo, flags map[string]string) + dfs = func(pkgInfo *PackageInfo, flags map[string]string) { checkDependencies := func(dependencies []string, installationReason InstallationReason) { for _, depend := range dependencies { - // Split dependency name and required version - dependName, _, _ := SplitPkgNameAndVersion(depend) - - // Ignore if package is already installed - if IsPackageInstalled(dependName, rootDir) && EvaluateDependency(depend, GetPackageInfo(dependName, rootDir).Version) { - continue - } else if providers := GetVirtualPackageInfo(dependName, rootDir); len(providers) > 0 { + // Deconstruct package string + d, err := DeconstructPackageString(depend) + if err != nil { + unresolved[depend] = "could not deconstruct package string: " + err.Error() continue } // Check resolved virtual packages - if resolvedPkg, ok := resolvedVirtualPackages[dependName]; ok { - dependName = resolvedPkg + if resolvedPkg, ok := resolvedVirtualPackages[d.PkgName]; ok { + d.PkgName = resolvedPkg } // Find database entry for dependency - dependEntry := ResolveDatabaseEntry(dependName, rootDir) + dependEntry := ResolveDatabaseEntry(d, rootDir) if dependEntry == nil { - unresolved = append(unresolved, depend) - continue - } - - // Ensure entry has required version - if !EvaluateDependency(depend, dependEntry.Info.Version) { - unresolved = append(unresolved, depend) + unresolved[depend] = "could not find in any database" continue } + d.PkgName = dependEntry.Info.Name // Skip ignored packages in config if rootDir == "/" && slices.Contains(MainBPMConfig.IgnorePackages, dependEntry.Info.Name) { continue } + // Add user defined package flags and resolve entry again if package is already installed + if installedInfo := GetPackageInfo(dependEntry.Info.Name, rootDir); installedInfo != nil { + d.Flags, err = CombineFlags(getPackageLocalInfo(dependEntry.Info.Name, rootDir).Flags, d.Flags) + if err != nil { + unresolved[depend] = "could not combine flags: " + err.Error() + continue + } + + dependEntry = ResolveDatabaseEntry(d, rootDir) + if dependEntry == nil { + unresolved[depend] = "could not find in any database" + continue + } + + shouldIgnore := true + + // Skip if no update/downgrade is available + if installedInfo.GetFullVersion() != dependEntry.Info.GetFullVersion() { + shouldIgnore = false + } + + // Skip if no new package flags + if !maps.Equal(getPackageLocalInfo(dependEntry.Info.Name, rootDir).Flags, d.Flags) { + shouldIgnore = false + } + + if shouldIgnore { + continue + } + } + // Resolve virtual packages for _, vpkg := range dependEntry.Info.Provides { if _, ok := resolvedVirtualPackages[vpkg]; !ok { @@ -169,8 +216,8 @@ func ResolveDependencies(pkgInfo *PackageInfo, resolvedVirtualPackages map[strin // Resolve entry dependencies if !slices.Contains(visited, dependEntry.Info.Name) { - dfs(dependEntry.Info) - resolved = append(resolved, ResolvedPackage{DatabaseEntry: dependEntry, InstallationReason: installationReason}) + dfs(dependEntry.Info, d.Flags) + resolved = append(resolved, ResolvedPackage{DatabaseEntry: dependEntry, Flags: d.Flags, InstallationReason: installationReason}) } } } @@ -184,69 +231,196 @@ func ResolveDependencies(pkgInfo *PackageInfo, resolvedVirtualPackages map[strin if pkgInfo.Type == "source" { checkDependencies(pkgInfo.MakeDepends, InstallationReasonMakeDependency) checkDependencies(pkgInfo.CheckDepends, InstallationReasonMakeDependency) + + // Resolve flag-specific dependencies + for _, flag := range pkgInfo.Flags { + acceptedValueIndex := slices.IndexFunc(flag.AcceptedValues, func(acceptedValue PackageAcceptedValue) bool { + if value, ok := flags[flag.Name]; ok { + return acceptedValue.Value == value + } else { + return acceptedValue.Value == flag.DefaultValue + } + }) + if acceptedValueIndex < 0 { + continue + } + acceptedValue := flag.AcceptedValues[acceptedValueIndex] + + checkDependencies(acceptedValue.Depends, InstallationReasonDependency) + checkDependencies(acceptedValue.RuntimeDepends, InstallationReasonDependency) + checkDependencies(acceptedValue.MakeDepends, InstallationReasonMakeDependency) + checkDependencies(acceptedValue.CheckDepends, InstallationReasonMakeDependency) + } } } - dfs(pkgInfo) + dfs(pkgInfo, flags) return resolved, unresolved } -func SplitPkgNameAndVersion(pkg string) (string, string, string) { - if strings.Contains(pkg, ">=") { - pkgSplit := strings.SplitN(pkg, ">=", 2) - pkgName := pkgSplit[0] - pkgVersion := pkgSplit[1] - - return pkgName, ">=", pkgVersion - } else if strings.Contains(pkg, ">") { - pkgSplit := strings.SplitN(pkg, ">", 2) - pkgName := pkgSplit[0] - pkgVersion := pkgSplit[1] - - return pkgName, ">", pkgVersion - } else if strings.Contains(pkg, "<=") { - pkgSplit := strings.SplitN(pkg, "<=", 2) - pkgName := pkgSplit[0] - pkgVersion := pkgSplit[1] - - return pkgName, "<=", pkgVersion - } else if strings.Contains(pkg, "<") { - pkgSplit := strings.SplitN(pkg, "<", 2) - pkgName := pkgSplit[0] - pkgVersion := pkgSplit[1] - - return pkgName, "<", pkgVersion - } else if strings.Contains(pkg, "=") { - pkgSplit := strings.SplitN(pkg, "=", 2) - pkgName := pkgSplit[0] - pkgVersion := pkgSplit[1] - - return pkgName, "=", pkgVersion - } - - return pkg, "", "" +type DeconstructedPackageString struct { + PkgName string + Description string + Flags map[string]string + RequiredVersionOperator string + RequiredVersion string } -func EvaluateDependency(pkg, matchVersion string) bool { - _, comparisonSymbol, pkgVersion := SplitPkgNameAndVersion(pkg) +func DeconstructPackageString(pkg string) (ret DeconstructedPackageString, err error) { + // Initialize values + ret.Flags = make(map[string]string) - switch comparisonSymbol { - case ">=": - return CompareVersions(matchVersion, pkgVersion) >= 0 - case ">": - return CompareVersions(matchVersion, pkgVersion) > 0 - case "<=": - return CompareVersions(matchVersion, pkgVersion) <= 0 - case "<": - return CompareVersions(matchVersion, pkgVersion) < 0 - case "=": - if cutPkgVersion, ok := strings.CutSuffix(pkgVersion, "*"); ok { - return strings.HasPrefix(matchVersion, cutPkgVersion) + // Get dependency description + if i := strings.IndexRune(pkg, ':'); i > 0 { + ret.Description = pkg[i+1:] + pkg = pkg[0:i] + } + + if left := strings.IndexRune(pkg, '['); left > 0 { + if right := strings.IndexRune(pkg, ']'); right > left { + flagsStr := pkg[left+1 : right] + pkg = pkg[0:left] + pkg[right+1:] + + if flagsStr != "" { + for flag := range strings.SplitSeq(flagsStr, ",") { + flagSplit := strings.SplitN(flag, "=", 2) + if len(flagSplit) != 2 { + return ret, fmt.Errorf("could not parse flag: %s", flag) + } + + ret.Flags[flagSplit[0]] = flagSplit[1] + } + } } else { - return CompareVersions(matchVersion, pkgVersion) == 0 + return ret, fmt.Errorf("could not find closing square bracket for set flags") + } + } + + if strings.Contains(pkg, ">=") { + ret.RequiredVersionOperator = ">=" + } else if strings.Contains(pkg, ">") { + ret.RequiredVersionOperator = ">" + } else if strings.Contains(pkg, "<=") { + ret.RequiredVersionOperator = "<=" + } else if strings.Contains(pkg, "<") { + ret.RequiredVersionOperator = "<" + } else if strings.Contains(pkg, "=") { + ret.RequiredVersionOperator = "=" + } + if ret.RequiredVersionOperator != "" { + pkgSplit := strings.SplitN(pkg, ret.RequiredVersionOperator, 2) + if len(pkgSplit) != 2 { + return ret, fmt.Errorf("could not parse required version: %s", pkg) + } + pkg = pkgSplit[0] + ret.RequiredVersion = pkgSplit[1] + } + + ret.PkgName = pkg + + return ret, nil +} + +func EvaluatePackageString(pkgInfo *PackageInfo, match DeconstructedPackageString) bool { + // Validate flags + for flag, value := range match.Flags { + pkgFlagIndex := slices.IndexFunc(pkgInfo.Flags, func(f PackageFlag) bool { + return f.Name == flag + }) + if pkgFlagIndex < 0 { + return false + } + pkgFlag := pkgInfo.Flags[pkgFlagIndex] + + if pkgInfo.Type == "binary" { + if pkgFlag.BuiltValue != value { + return false + } + } + + if len(pkgFlag.AcceptedValues) == 0 { + continue + } + + if !slices.ContainsFunc(pkgFlag.AcceptedValues, func(acceptedFlag PackageAcceptedValue) bool { + return acceptedFlag.Value == value + }) { + return false + } + } + + // Validate version + switch match.RequiredVersionOperator { + case ">=": + return CompareVersions(match.RequiredVersion, pkgInfo.Version) >= 0 + case ">": + return CompareVersions(match.RequiredVersion, pkgInfo.Version) > 0 + case "<=": + return CompareVersions(match.RequiredVersion, pkgInfo.Version) <= 0 + case "<": + return CompareVersions(match.RequiredVersion, pkgInfo.Version) < 0 + case "=": + if cutPkgVersion, ok := strings.CutSuffix(match.RequiredVersion, "*"); ok { + return strings.HasPrefix(pkgInfo.Version, cutPkgVersion) + } else { + return CompareVersions(pkgInfo.Version, match.RequiredVersion) == 0 } default: return true } } + +func GetBuiltFlags(pkg, rootDir string) map[string]string { + builtFlags := make(map[string]string) + + pkgInfo := GetPackageInfo(pkg, rootDir) + if pkgInfo == nil { + return nil + } + + for _, flag := range pkgInfo.Flags { + builtFlags[flag.Name] = flag.BuiltValue + } + + return builtFlags +} + +func RemoveInvalidFlags(pkgInfo *PackageInfo, userFlags map[string]string) (ret map[string]string) { + ret = make(map[string]string) + + for userFlag, userValue := range userFlags { + flagIndex := slices.IndexFunc(pkgInfo.Flags, func(flag PackageFlag) bool { + return flag.Name == userFlag + }) + if flagIndex < 0 { + continue + } + flag := pkgInfo.Flags[flagIndex] + + if len(flag.AcceptedValues) > 0 && !slices.ContainsFunc(flag.AcceptedValues, func(acceptedValue PackageAcceptedValue) bool { + return acceptedValue.Value == userValue + }) { + continue + } + + ret[userFlag] = userValue + } + + return ret +} + +func CombineFlags(flags, newFlags map[string]string) (map[string]string, error) { + ret := make(map[string]string) + maps.Copy(ret, flags) + + for flag, value := range newFlags { + if oldValue, ok := flags[flag]; ok && value != oldValue { + return ret, fmt.Errorf("flag already defined: %s=%s/%s", flag, oldValue, value) + } + + ret[flag] = value + } + + return ret, nil +} diff --git a/src/bpmlib/errors.go b/src/bpmlib/errors.go index 1bb89c7..ec569f4 100644 --- a/src/bpmlib/errors.go +++ b/src/bpmlib/errors.go @@ -2,26 +2,25 @@ package bpmlib import ( "fmt" + "maps" "slices" "strings" ) -type PackageNotFoundErr struct { - packages []string +type PackageNotResolvedErr struct { + packages map[string]string } -func (e PackageNotFoundErr) Error() string { - slices.Sort(e.packages) - return "The following packages were not found in any databases: " + strings.Join(e.packages, ", ") -} +func (e PackageNotResolvedErr) Error() (ret string) { + ret = "The following packages could not be resolved:" -type DependencyNotFoundErr struct { - dependencies []string -} + keys := slices.Collect(maps.Keys(e.packages)) + slices.Sort(keys) + for _, key := range keys { + ret += "\n " + key + ": " + e.packages[key] + } -func (e DependencyNotFoundErr) Error() string { - slices.Sort(e.dependencies) - return "The following dependencies were not found in any databases: " + strings.Join(e.dependencies, ", ") + return ret } type PackageConflictErr struct { diff --git a/src/bpmlib/general.go b/src/bpmlib/general.go index 541827c..07aa7ce 100644 --- a/src/bpmlib/general.go +++ b/src/bpmlib/general.go @@ -16,7 +16,7 @@ func InstallPackages(rootDir string, forceInstallationReason InstallationReason, // Setup operation struct operation = &BPMOperation{ Actions: make([]OperationAction, 0), - UnresolvedDepends: make([]string, 0), + UnresolvedDepends: make(map[string]string, 0), ModifiedFiles: make(map[string]string), RunChecks: runChecks, RootDir: rootDir, @@ -27,21 +27,58 @@ func InstallPackages(rootDir string, forceInstallationReason InstallationReason, packages = removeDuplicates(packages) // Resolve packages - pkgsNotFound := make([]string, 0) + unresolvedPackages := make(map[string]string) resolvedVirtualPackages := make(map[string]string) for _, pkg := range packages { - if stat, err := os.Stat(pkg); err == nil && !stat.IsDir() { - bpmpkg, err := ReadPackage(pkg) + d, err := DeconstructPackageString(pkg) + if err != nil { + return nil, fmt.Errorf("could not deconstruct package string: %s", err) + } + + if stat, err := os.Stat(d.PkgName); err == nil && !stat.IsDir() { + bpmpkg, err := ReadPackage(d.PkgName) if err != nil { return nil, fmt.Errorf("could not read package: %s", err) } - if bpmpkg.PkgInfo.Type == "source" && bpmpkg.PkgInfo.IsSplitPackage() { + // Add user defined package flags + if !reinstallPackages && IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) { + d.Flags, err = CombineFlags(getPackageLocalInfo(bpmpkg.PkgInfo.Name, rootDir).Flags, d.Flags) + if err != nil { + return nil, fmt.Errorf("could not combine flags for package (%s): %s", bpmpkg.PkgInfo.Name, err) + } + } + + if !EvaluatePackageString(bpmpkg.PkgInfo, d) { + unresolvedPackages[pkg] = "could not evaluate package" + continue + } + + if bpmpkg.PkgInfo.IsSplitPackage() { for _, splitPkg := range bpmpkg.PkgInfo.SplitPackages { if !reinstallPackages && IsPackageInstalled(splitPkg.Name, rootDir) && GetPackageInfo(splitPkg.Name, rootDir).GetFullVersion() == splitPkg.GetFullVersion() { continue } + // Check if installation can be ignored + if !reinstallPackages && IsPackageInstalled(splitPkg.Name, rootDir) { + shouldIgnore := true + + // Skip if no update/downgrade is available + if GetPackageInfo(splitPkg.Name, rootDir).GetFullVersion() != splitPkg.GetFullVersion() { + shouldIgnore = false + } + + // Skip if no new package flags + if !maps.Equal(getPackageLocalInfo(splitPkg.Name, rootDir).Flags, d.Flags) { + shouldIgnore = false + } + + if shouldIgnore { + continue + } + } + // Set package installation reason installationReason := forceInstallationReason if installationReason == InstallationReasonUnknown { @@ -52,9 +89,17 @@ func InstallPackages(rootDir string, forceInstallationReason InstallationReason, } } + // Resolve virtual packages + for _, vpkg := range splitPkg.Provides { + if _, ok := resolvedVirtualPackages[vpkg]; !ok { + resolvedVirtualPackages[vpkg] = splitPkg.Name + } + } + operation.Actions = append(operation.Actions, &InstallPackageAction{ File: pkg, InstallationReason: installationReason, + Flags: d.Flags, BpmPackage: bpmpkg, SplitPackageToInstall: splitPkg.Name, }) @@ -62,8 +107,23 @@ func InstallPackages(rootDir string, forceInstallationReason InstallationReason, continue } - if !reinstallPackages && IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) && GetPackageInfo(bpmpkg.PkgInfo.Name, rootDir).GetFullVersion() == bpmpkg.PkgInfo.GetFullVersion() { - continue + // Check if installation can be ignored + if !reinstallPackages && IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) { + shouldIgnore := true + + // Skip if no update/downgrade is available + if GetPackageInfo(bpmpkg.PkgInfo.Name, rootDir).GetFullVersion() != bpmpkg.PkgInfo.GetFullVersion() { + shouldIgnore = false + } + + // Skip if no new package flags + if !maps.Equal(getPackageLocalInfo(bpmpkg.PkgInfo.Name, rootDir).Flags, d.Flags) { + shouldIgnore = false + } + + if shouldIgnore { + continue + } } // Set package installation reason @@ -86,30 +146,54 @@ func InstallPackages(rootDir string, forceInstallationReason InstallationReason, operation.Actions = append(operation.Actions, &InstallPackageAction{ File: pkg, InstallationReason: installationReason, + Flags: d.Flags, BpmPackage: bpmpkg, }) } else { - // Split package name and required version - pkgName, _, _ := SplitPkgNameAndVersion(pkg) - // Check resolved virtual packages - if resolvedPkg, ok := resolvedVirtualPackages[pkgName]; ok { - pkgName = resolvedPkg + if resolvedPkg, ok := resolvedVirtualPackages[d.PkgName]; ok { + d.PkgName = resolvedPkg } - entry := ResolveDatabaseEntry(pkgName, rootDir) + entry := ResolveDatabaseEntry(d, rootDir) if entry == nil { - pkgsNotFound = append(pkgsNotFound, pkg) + unresolvedPackages[pkg] = "could not find in any database" continue } - if !EvaluateDependency(pkg, entry.Info.Version) { - pkgsNotFound = append(pkgsNotFound, pkg) - continue + pkgInfo := GetPackageInfo(entry.Info.Name, rootDir) + + // Add user defined package flags and resolve entry again if package is already installed + if !reinstallPackages && pkgInfo != nil { + d.Flags, err = CombineFlags(getPackageLocalInfo(entry.Info.Name, rootDir).Flags, d.Flags) + if err != nil { + return nil, fmt.Errorf("could not combine flags for package (%s): %s", entry.Info.Name, err) + } + + entry = ResolveDatabaseEntry(d, rootDir) + if entry == nil { + unresolvedPackages[pkg] = "could not find in any database" + continue + } } - if !reinstallPackages && IsPackageInstalled(entry.Info.Name, rootDir) && GetPackageInfo(entry.Info.Name, rootDir).GetFullVersion() == entry.Info.GetFullVersion() { - continue + // Check if installation can be ignored + if !reinstallPackages && pkgInfo != nil { + shouldIgnore := true + + // Skip if no update/downgrade is available + if GetPackageInfo(entry.Info.Name, rootDir).GetFullVersion() != entry.Info.GetFullVersion() { + shouldIgnore = false + } + + // Skip if no new package flags + if !maps.Equal(getPackageLocalInfo(entry.Info.Name, rootDir).Flags, d.Flags) { + shouldIgnore = false + } + + if shouldIgnore { + continue + } } // Set package installation reason @@ -131,23 +215,24 @@ func InstallPackages(rootDir string, forceInstallationReason InstallationReason, operation.Actions = append(operation.Actions, &FetchPackageAction{ InstallationReason: installationReason, + Flags: d.Flags, DatabaseEntry: entry, }) } } // Return error if not all packages are found - if len(pkgsNotFound) != 0 { - return nil, PackageNotFoundErr{pkgsNotFound} + if len(unresolvedPackages) != 0 { + return nil, PackageNotResolvedErr{unresolvedPackages} } // Resolve dependencies operation.ResolveDependencies(installRuntimeDependencies) if len(operation.UnresolvedDepends) != 0 { if !forceInstallation { - return nil, DependencyNotFoundErr{operation.UnresolvedDepends} + return nil, PackageNotResolvedErr{operation.UnresolvedDepends} } else if verbose { - log.Printf("Warning: %s", DependencyNotFoundErr{operation.UnresolvedDepends}) + log.Printf("Warning: %s", PackageNotResolvedErr{operation.UnresolvedDepends}) } } @@ -197,7 +282,7 @@ func InstallPackages(rootDir string, forceInstallationReason InstallationReason, func RemovePackages(rootDir string, force, cleanupDependencies bool, packages ...string) (operation *BPMOperation, err error) { operation = &BPMOperation{ Actions: make([]OperationAction, 0), - UnresolvedDepends: make([]string, 0), + UnresolvedDepends: make(map[string]string), ModifiedFiles: make(map[string]string), RootDir: rootDir, compiledPackages: make(map[string]string), @@ -275,7 +360,7 @@ func RemovePackages(rootDir string, force, cleanupDependencies bool, packages .. func CleanupPackages(cleanupMakeDepends bool, rootDir string) (operation *BPMOperation, err error) { operation = &BPMOperation{ Actions: make([]OperationAction, 0), - UnresolvedDepends: make([]string, 0), + UnresolvedDepends: make(map[string]string), ModifiedFiles: make(map[string]string), RootDir: rootDir, compiledPackages: make(map[string]string), @@ -403,7 +488,7 @@ func UpdatePackages(rootDir string, syncDatabase, allowDowngrades, forceInstalla operation = &BPMOperation{ Actions: make([]OperationAction, 0), - UnresolvedDepends: make([]string, 0), + UnresolvedDepends: make(map[string]string), ModifiedFiles: make(map[string]string), RunChecks: runChecks, RootDir: rootDir, @@ -417,16 +502,21 @@ func UpdatePackages(rootDir string, syncDatabase, allowDowngrades, forceInstalla continue } + // Get installed package built flags + flags := getPackageLocalInfo(pkg, rootDir).Flags + var entry *BPMDatabaseEntry // Check if installed package can be replaced and install that instead if e := FindReplacement(pkg); e != nil { entry = e - } else if entry = ResolveDatabaseEntry(pkg, rootDir); entry == nil { + } else if entry = ResolveDatabaseEntry(DeconstructedPackageString{PkgName: pkg, Flags: flags}, rootDir); entry == nil { continue } - installedInfo := GetPackageInfo(pkg, rootDir) - if installedInfo == nil { + // Remove invalid flags + flags = RemoveInvalidFlags(entry.Info, flags) + + if installedInfo := GetPackageInfo(pkg, rootDir); installedInfo == nil { return nil, fmt.Errorf("could not get package info for package (%s)", pkg) } else { comparison := CompareVersions(entry.Info.GetFullVersion(), installedInfo.GetFullVersion()) @@ -438,8 +528,11 @@ func UpdatePackages(rootDir string, syncDatabase, allowDowngrades, forceInstalla } } + bpmpkg := GetPackage(pkg, rootDir) + operation.Actions = append(operation.Actions, &FetchPackageAction{ - InstallationReason: GetPackage(pkg, rootDir).LocalInfo.GetInstallationReason(), + InstallationReason: bpmpkg.LocalInfo.GetInstallationReason(), + Flags: flags, DatabaseEntry: entry, }) } @@ -450,9 +543,9 @@ func UpdatePackages(rootDir string, syncDatabase, allowDowngrades, forceInstalla operation.ResolveDependencies(true) if len(operation.UnresolvedDepends) != 0 { if !forceInstallation { - return nil, DependencyNotFoundErr{operation.UnresolvedDepends} + return nil, PackageNotResolvedErr{operation.UnresolvedDepends} } else if verbose { - log.Printf("Warning: %s", DependencyNotFoundErr{operation.UnresolvedDepends}) + log.Printf("Warning: %s", PackageNotResolvedErr{operation.UnresolvedDepends}) } } diff --git a/src/bpmlib/operations.go b/src/bpmlib/operations.go index e2561d4..beb44f9 100644 --- a/src/bpmlib/operations.go +++ b/src/bpmlib/operations.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "log" + "maps" "os" "path" "slices" @@ -13,7 +14,7 @@ import ( type BPMOperation struct { Actions []OperationAction - UnresolvedDepends []string + UnresolvedDepends map[string]string ModifiedFiles map[string]string CompilationJobs int RunChecks bool @@ -91,26 +92,29 @@ func (operation *BPMOperation) ResolveDependencies(installRuntimeDepends bool) { newActions := make([]OperationAction, 0) for _, value := range operation.Actions { var pkgInfo *PackageInfo + var flags map[string]string if value.GetActionType() == "install" { action := value.(*InstallPackageAction) pkgInfo = action.BpmPackage.PkgInfo + flags = action.Flags } else if value.GetActionType() == "fetch" { action := value.(*FetchPackageAction) pkgInfo = action.DatabaseEntry.Info + flags = action.Flags } else { continue } - resolved, unresolved := ResolveDependencies(pkgInfo, resolvedVirtualPackages, installRuntimeDepends, operation.RootDir) + resolved, unresolved := ResolveDependencies(pkgInfo, flags, resolvedVirtualPackages, installRuntimeDepends, operation.RootDir) - // Append unresolved dependencies - operation.UnresolvedDepends = append(operation.UnresolvedDepends, unresolved...) - operation.UnresolvedDepends = removeDuplicates(operation.UnresolvedDepends) + // Copy unresolved dependencies + maps.Copy(operation.UnresolvedDepends, unresolved) for _, resolvedPkg := range resolved { if ActionSliceIndex(newActions, resolvedPkg.DatabaseEntry.Info.Name) == -1 { // Dependency not in actions slice var action OperationAction = &FetchPackageAction{ InstallationReason: resolvedPkg.InstallationReason, + Flags: resolvedPkg.Flags, DatabaseEntry: resolvedPkg.DatabaseEntry, } newActions = append(newActions, action) @@ -192,15 +196,18 @@ func (operation *BPMOperation) Cleanup(cleanupMakeDepends bool) error { // Loop through all dependencies for _, depend := range depends { - // Remove required version - depend, _, _ = SplitPkgNameAndVersion(depend) + // Deconstruct package string + d, err := DeconstructPackageString(depend) + if err != nil { + return fmt.Errorf("could not deconstruct package string: %s", err) + } // Resolve dependency var dependPkgInfo *PackageInfo - if providers := GetVirtualPackageInfo(depend, operation.RootDir); len(providers) > 0 { + if providers := GetVirtualPackageInfo(d.PkgName, operation.RootDir); len(providers) > 0 { dependPkgInfo = providers[0] } else { - dependPkgInfo = GetPackageInfo(depend, operation.RootDir) + dependPkgInfo = GetPackageInfo(d.PkgName, operation.RootDir) } if dependPkgInfo == nil { continue @@ -225,7 +232,7 @@ func (operation *BPMOperation) Cleanup(cleanupMakeDepends bool) error { if !slices.Contains(visited, pkg) { bpmpkg := GetPackage(pkg, operation.RootDir) if bpmpkg == nil { - return errors.New("Error: could not find installed package (" + pkg + ")") + return fmt.Errorf("could not find installed package (%s)", pkg) } operation.Actions = append(operation.Actions, &RemovePackageAction{BpmPackage: bpmpkg}) } @@ -358,15 +365,30 @@ func (operation *BPMOperation) ShowOperationSummary() { for _, value := range operation.Actions { var pkgInfo *PackageInfo + var flagsStr string var installationReason = InstallationReasonUnknown if value.GetActionType() == "install" { installationReason = value.(*InstallPackageAction).InstallationReason pkgInfo = value.(*InstallPackageAction).BpmPackage.PkgInfo + if len(value.(*InstallPackageAction).Flags) > 0 { + flagsSlice := make([]string, 0) + for flag, value := range value.(*InstallPackageAction).Flags { + flagsSlice = append(flagsSlice, flag+"="+value) + } + flagsStr = "[" + strings.Join(flagsSlice, ",") + "]" + } if value.(*InstallPackageAction).SplitPackageToInstall != "" { pkgInfo = pkgInfo.GetSplitPackageInfo(value.(*InstallPackageAction).SplitPackageToInstall) } } else if value.GetActionType() == "fetch" { installationReason = value.(*FetchPackageAction).InstallationReason + if len(value.(*FetchPackageAction).Flags) > 0 { + flagsSlice := make([]string, 0) + for flag, value := range value.(*FetchPackageAction).Flags { + flagsSlice = append(flagsSlice, flag+"="+value) + } + flagsStr = "[" + strings.Join(flagsSlice, ",") + "]" + } pkgInfo = value.(*FetchPackageAction).DatabaseEntry.Info } else { pkgInfo = value.(*RemovePackageAction).BpmPackage.PkgInfo @@ -388,15 +410,15 @@ func (operation *BPMOperation) ShowOperationSummary() { installedInfo := GetPackageInfo(pkgInfo.Name, operation.RootDir) if installedInfo == nil { - fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%t\n", pkgInfo.Name, pkgInfo.GetFullVersion(), "Install", installationReasonStr, pkgInfo.Type == "source") + fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%t\n", pkgInfo.Name+flagsStr, pkgInfo.GetFullVersion(), "Install", installationReasonStr, pkgInfo.Type == "source") } else { comparison := CompareVersions(pkgInfo.GetFullVersion(), installedInfo.GetFullVersion()) if comparison < 0 { - fmt.Fprintf(writer, "%s\t%s -> %s\t%s\t%s\t%t\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), "Downgrade", installationReasonStr, pkgInfo.Type == "source") + fmt.Fprintf(writer, "%s\t%s -> %s\t%s\t%s\t%t\n", pkgInfo.Name+flagsStr, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), "Downgrade", installationReasonStr, pkgInfo.Type == "source") } else if comparison > 0 { - fmt.Fprintf(writer, "%s\t%s -> %s\t%s\t%s\t%t\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), "Upgrade", installationReasonStr, pkgInfo.Type == "source") + fmt.Fprintf(writer, "%s\t%s -> %s\t%s\t%s\t%t\n", pkgInfo.Name+flagsStr, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), "Upgrade", installationReasonStr, pkgInfo.Type == "source") } else { - fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%t\n", pkgInfo.Name, pkgInfo.GetFullVersion(), "Reinstall", installationReasonStr, pkgInfo.Type == "source") + fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%t\n", pkgInfo.Name+flagsStr, pkgInfo.GetFullVersion(), "Reinstall", installationReasonStr, pkgInfo.Type == "source") } } } @@ -462,12 +484,14 @@ func (operation *BPMOperation) GetOptionalDependencies() (optionalDepends map[st } for _, depend := range pkgInfo.OptionalDepends { - // Get optional dependency name - dependSplit := strings.SplitN(depend, ":", 2) - dependName, _, _ := SplitPkgNameAndVersion(dependSplit[0]) + // Deconstruct package string + d, err := DeconstructPackageString(depend) + if err != nil { + continue + } // Skip if dependency is already installed - if IsPackageInstalled(dependName, operation.RootDir) || len(GetVirtualPackageInfo(dependName, operation.RootDir)) > 0 { + if IsPackageInstalled(d.PkgName, operation.RootDir) || len(GetVirtualPackageInfo(d.PkgName, operation.RootDir)) > 0 { continue } @@ -484,9 +508,9 @@ func (operation *BPMOperation) GetOptionalDependencies() (optionalDepends map[st return false } - if pkgInfo.Name == dependName { + if pkgInfo.Name == d.PkgName { return true - } else if slices.Contains(pkgInfo.Provides, dependName) { + } else if slices.Contains(pkgInfo.Provides, d.PkgName) { return true } @@ -497,21 +521,21 @@ func (operation *BPMOperation) GetOptionalDependencies() (optionalDepends map[st // Skip if not a new dependency of the package if installedPkg := GetPackage(pkgInfo.Name, operation.RootDir); installedPkg != nil && slices.ContainsFunc(installedPkg.PkgInfo.OptionalDepends, func(n string) bool { - // Remove optional dependency comment - n = strings.SplitN(n, ":", 2)[0] + // Deconstruct package string + d2, err := DeconstructPackageString(depend) + if err != nil { + return false + } - // Remove required version - n, _, _ = SplitPkgNameAndVersion(n) - - return n == dependName + return d2.PkgName == d.PkgName }) { continue } - if len(dependSplit) == 2 { - optionalDepends[pkgInfo.Name] = append(optionalDepends[pkgInfo.Name], fmt.Sprintf("%s (%s)", dependSplit[0], dependSplit[1])) + if d.Description != "" { + optionalDepends[pkgInfo.Name] = append(optionalDepends[pkgInfo.Name], fmt.Sprintf("%s (%s)", d.PkgName, d.Description)) } else { - optionalDepends[pkgInfo.Name] = append(optionalDepends[pkgInfo.Name], dependName) + optionalDepends[pkgInfo.Name] = append(optionalDepends[pkgInfo.Name], d.PkgName) } } } @@ -648,6 +672,7 @@ func (operation *BPMOperation) FetchPackages() (err error) { operation.Actions[i] = &InstallPackageAction{ File: fetchedPackages[entry.Filepath], InstallationReason: action.(*FetchPackageAction).InstallationReason, + Flags: action.(*FetchPackageAction).Flags, BpmPackage: bpmpkg, SplitPackageToInstall: entry.Info.Name, } @@ -655,6 +680,7 @@ func (operation *BPMOperation) FetchPackages() (err error) { operation.Actions[i] = &InstallPackageAction{ File: fetchedPackages[entry.Filepath], InstallationReason: action.(*FetchPackageAction).InstallationReason, + Flags: action.(*FetchPackageAction).Flags, BpmPackage: bpmpkg, } } @@ -735,6 +761,7 @@ func (operation *BPMOperation) Execute(verbose, force bool) (err error) { } else if action.GetActionType() == "install" { value := action.(*InstallPackageAction) fileToInstall := value.File + compilationFlags := value.Flags bpmpkg := value.BpmPackage var err error @@ -759,7 +786,7 @@ func (operation *BPMOperation) Execute(verbose, force bool) (err error) { // Compile source package if not compiled already if _, ok := operation.compiledPackages[pkgNameToInstall]; !ok { - outputBpmPackages, err := CompileSourcePackage(value.File, compiledDir, operation.CompilationJobs, !operation.RunChecks, false, verbose) + outputBpmPackages, err := CompileSourcePackage(value.File, compiledDir, compilationFlags, operation.CompilationJobs, !operation.RunChecks, false, verbose) if err != nil { return fmt.Errorf("could not compile source package (%s): %s\n", value.File, err) } @@ -779,9 +806,9 @@ func (operation *BPMOperation) Execute(verbose, force bool) (err error) { } if value.InstallationReason != InstallationReasonManual { - err = installPackage(fileToInstall, value.InstallationReason, operation.RootDir, verbose, true) + err = installPackage(fileToInstall, value.InstallationReason, value.Flags, operation.RootDir, verbose, true) } else { - err = installPackage(fileToInstall, value.InstallationReason, operation.RootDir, verbose, force) + err = installPackage(fileToInstall, value.InstallationReason, value.Flags, operation.RootDir, verbose, force) } if err != nil { return fmt.Errorf("could not install package (%s): %s\n", bpmpkg.PkgInfo.Name, err) @@ -799,6 +826,7 @@ type OperationAction interface { type InstallPackageAction struct { File string InstallationReason InstallationReason + Flags map[string]string SplitPackageToInstall string BpmPackage *BPMPackage } @@ -809,6 +837,7 @@ func (action *InstallPackageAction) GetActionType() string { type FetchPackageAction struct { InstallationReason InstallationReason + Flags map[string]string DatabaseEntry *BPMDatabaseEntry } diff --git a/src/bpmlib/packages.go b/src/bpmlib/packages.go index e3e0ff5..56e1f2a 100644 --- a/src/bpmlib/packages.go +++ b/src/bpmlib/packages.go @@ -49,10 +49,27 @@ type PackageInfo struct { Replaces []string `yaml:"replaces,omitempty"` Provides []string `yaml:"provides,omitempty"` Options []string `yaml:"options,omitempty"` + Flags []PackageFlag `yaml:"flags,omitempty"` Downloads []PackageDownload `yaml:"downloads,omitempty"` SplitPackages []*PackageInfo `yaml:"split_packages,omitempty"` } +type PackageFlag struct { + Name string `yaml:"name"` + DefaultValue string `yaml:"default_value"` + BuiltValue string `yaml:"built_value"` + AcceptedValues []PackageAcceptedValue `yaml:"accepted_values,omitempty"` +} + +type PackageAcceptedValue struct { + Value string `yaml:"value"` + Depends []string `yaml:"depends,omitempty"` + RuntimeDepends []string `yaml:"runtime_depends,omitempty"` + OptionalDepends []string `yaml:"optional_depends,omitempty"` + MakeDepends []string `yaml:"make_depends,omitempty"` + CheckDepends []string `yaml:"check_depends,omitempty"` +} + type PackageDownload struct { Url string `yaml:"url"` Type string `yaml:"type,omitempty"` @@ -79,9 +96,10 @@ type PackageFileEntry struct { } type PackageLocalInfo struct { - InstallationReason string `yaml:"installation_reason"` - InstalledOn int64 `yaml:"installed_on"` - LastUpdatedOn int64 `yaml:"last_updated_on"` + InstallationReason string `yaml:"installation_reason"` + Flags map[string]string `yaml:"flags"` + InstalledOn int64 `yaml:"installed_on"` + LastUpdatedOn int64 `yaml:"last_updated_on"` } func (pkg *BPMPackage) GetInstalledSize() int64 { @@ -595,6 +613,70 @@ func (pkgInfo *PackageInfo) CreateReadableInfo(rootDir string) string { } builder.WriteString("Type: " + pkgInfo.Type + "\n") + // Flags + if pkgInfo.Type == "binary" { + var flags []string + for _, flag := range pkgInfo.Flags { + if pkgInfo != GetPackageInfo(pkgInfo.Name, rootDir) { + flags = append(flags, fmt.Sprintf("%s=%s", flag.Name, flag.BuiltValue)) + } else if _, ok := getPackageLocalInfo(pkgInfo.Name, rootDir).Flags[flag.Name]; ok { + flags = append(flags, fmt.Sprintf("%s=%s (User defined)", flag.Name, flag.BuiltValue)) + } else { + flags = append(flags, fmt.Sprintf("%s=%s (Default value)", flag.Name, flag.BuiltValue)) + } + } + builderWriteArray("Built with flags:", flags, false) + } else { + if len(pkgInfo.Flags) > 0 { + builder.WriteString("Available flags (") + builder.WriteString(strconv.Itoa(len(pkgInfo.Flags))) + builder.WriteString("):\n") + for _, flag := range pkgInfo.Flags { + builder.WriteString(" - Flag: ") + builder.WriteString(flag.Name) + builder.WriteRune('\n') + if flag.DefaultValue != "" { + builder.WriteString(" Default value: ") + builder.WriteString(flag.DefaultValue) + builder.WriteRune('\n') + } + if len(flag.AcceptedValues) > 0 { + builder.WriteString(" Accepted values (") + builder.WriteString(strconv.Itoa(len(flag.AcceptedValues))) + builder.WriteString("):\n") + + for i, acceptedValue := range flag.AcceptedValues { + writeValueDepends := func(text string, depends []string) { + if len(depends) > 0 { + builder.WriteString(" ") + builder.WriteString(text) + builder.WriteString(" (") + builder.WriteString(strconv.Itoa(len(depends))) + builder.WriteString("): ") + for _, depend := range depends { + if i != 0 { + builder.WriteString(", ") + } + builder.WriteString(depend) + } + builder.WriteRune('\n') + } + } + + builder.WriteString(" - Value: ") + builder.WriteString(acceptedValue.Value) + builder.WriteRune('\n') + writeValueDepends("Dependencies", acceptedValue.Depends) + writeValueDepends("Make Dependencies", acceptedValue.MakeDepends) + writeValueDepends("Check Dependencies", acceptedValue.CheckDepends) + writeValueDepends("Runtime Dependencies", acceptedValue.RuntimeDepends) + writeValueDepends("Optional Dependencies", acceptedValue.OptionalDepends) + } + } + } + } + } + // Dependencies builderWriteDependencyArray("Dependencies", pkgInfo.Depends) if pkgInfo.Type == "source" { @@ -870,7 +952,7 @@ func extractPackage(bpmpkg *BPMPackage, verbose bool, filename, rootDir string) return nil } -func installPackage(filename string, installationReason InstallationReason, rootDir string, verbose, force bool) error { +func installPackage(filename string, installationReason InstallationReason, flags map[string]string, rootDir string, verbose, force bool) error { if _, err := os.Stat(filename); os.IsNotExist(err) { return err } @@ -1074,6 +1156,7 @@ func installPackage(filename string, installationReason InstallationReason, root } localInfo.LastUpdatedOn = time.Now().Unix() localInfo.InstallationReason = string(installationReason) + localInfo.Flags = flags SetPackageLocalInfo(bpmpkg.PkgInfo.Name, localInfo, rootDir)