-
Notifications
You must be signed in to change notification settings - Fork 33
Fix circular dependencies #116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
12270fc
2785697
b34a52e
6671594
f354844
06cc856
cbdca02
e40f809
9297d0b
4e0b823
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ based on: https://github.com/bazel-contrib/rules-template/blob/0dadcb716f06f6728 | |
| """ | ||
|
|
||
| load("@bazel_features//:features.bzl", "bazel_features") | ||
| load("@bazel_skylib//lib:versions.bzl", "versions") | ||
| load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_jar") | ||
| load("//internal:rpm.bzl", null_rpm_repository = "null_rpm", rpm_repository = "rpm") | ||
| load(":repositories.bzl", "bazeldnf_register_toolchains") | ||
|
|
@@ -68,7 +69,7 @@ _ALIAS_TEMPLATE = """\ | |
| alias( | ||
| name = "{name}", | ||
| actual = "@{actual_name}//rpm", | ||
| visibility = ["//visibility:public"], | ||
| visibility = ["{visibility}"], | ||
| ) | ||
| """ | ||
|
|
||
|
|
@@ -120,18 +121,27 @@ def _alias_repository_impl(repository_ctx): | |
| architecture = repository_ctx.attr.architecture, | ||
| ), | ||
| ) | ||
|
|
||
| requested = dict([[x, 1] for x in repository_ctx.attr.requested]) | ||
|
|
||
| for rpm in repository_ctx.attr.rpms: | ||
| actual_name = rpm.repo_name | ||
| name = rpm.repo_name | ||
|
|
||
| if repository_ctx.attr.repository_prefix: | ||
| name = actual_name.split(repository_ctx.attr.repository_prefix, 1)[-1] | ||
|
|
||
| visibility = "//visibility:public" | ||
|
|
||
| if repository_ctx.attr.requested and name not in requested: | ||
| visibility = "//:__subpackages__" | ||
|
|
||
| repository_ctx.file( | ||
| "%s/BUILD.bazel" % name, | ||
| _ALIAS_TEMPLATE.format( | ||
| name = name, | ||
| actual_name = actual_name, | ||
| visibility = visibility, | ||
| ), | ||
| ) | ||
|
|
||
|
|
@@ -147,15 +157,16 @@ def _alias_repository_impl(repository_ctx): | |
| _alias_repository = repository_rule( | ||
| implementation = _alias_repository_impl, | ||
| attrs = { | ||
| "rpms": attr.label_list(default = []), | ||
| "lock_file": attr.label(), | ||
| "rpms_to_install": attr.string_list(), | ||
| "architecture": attr.string(values = ["i686", "x86_64", "aarch64", ""]), | ||
| "cache_dir": attr.string(), | ||
| "excludes": attr.string_list(), | ||
| "lock_file": attr.label(), | ||
| "nobest": attr.bool(default = False), | ||
| "requested": attr.string_list(), | ||
| "repofile": attr.label(), | ||
| "repository_prefix": attr.string(), | ||
| "nobest": attr.bool(default = False), | ||
| "cache_dir": attr.string(), | ||
| "architecture": attr.string(values = ["i686", "x86_64", "aarch64", ""]), | ||
| "rpms_to_install": attr.string_list(), | ||
| "rpms": attr.label_list(default = []), | ||
| }, | ||
| ) | ||
|
|
||
|
|
@@ -192,13 +203,19 @@ def _handle_lock_file(config, module_ctx, registered_rpms = {}): | |
| content = module_ctx.read(config.lock_file) | ||
| lock_file_json = json.decode(content) | ||
|
|
||
| if not config.ignore_deps: | ||
| if versions.is_at_least("7", versions.get()) and not versions.is_at_least("7.4.0", versions.get()): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. where are these constraints coming from?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't remember exactly why, what I do remember is setting the bazel version to 7.0 and so until 7.4.0 with bazelisk and it was failing with some weird error, same for 8.x
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably worth digging into this a bit more. We're effectively saying that we no longer support pre 7.4 versions of bazel? That's a fairly strong assertion. |
||
| fail("ignore_deps requires Bazel 7.4+ for Bazel 7") | ||
| if versions.is_at_least("8", versions.get()) and not versions.is_at_least("8.1.0", versions.get()): | ||
| fail("ignore_deps requires Bazel 8.1+ for Bazel 8") | ||
|
|
||
| for rpm in lock_file_json.get("rpms", []): | ||
| dependencies = rpm.pop("dependencies", []) | ||
| if config.ignore_deps: | ||
| dependencies = [] | ||
| else: | ||
| dependencies = [x.replace("+", "plus") for x in dependencies] | ||
| dependencies = ["@{}{}//rpm".format(config.rpm_repository_prefix, x) for x in dependencies] | ||
| dependencies = ["@{}{}//rpm:rpm-file".format(config.rpm_repository_prefix, x) for x in dependencies] | ||
|
|
||
| rpm_name = rpm.pop("name", None) | ||
| if not rpm_name: | ||
|
|
@@ -235,6 +252,7 @@ def _handle_lock_file(config, module_ctx, registered_rpms = {}): | |
| registered_rpms[name] = 1 | ||
|
|
||
| repository_args["rpms"] = ["@@%s//rpm" % x for x in registered_rpms.keys()] | ||
| repository_args["requested"] = [x.replace("+", "plus") for x in lock_file_json.get("targets", [])] | ||
|
|
||
| _alias_repository( | ||
| **repository_args | ||
|
|
@@ -278,6 +296,7 @@ def _bazeldnf_extension(module_ctx): | |
| _alias_repository( | ||
| name = name, | ||
| rpms = ["@@%s//rpm" % x for x in rpms], | ||
| requested = rpms, | ||
| ) | ||
| repos.append(name) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,11 +50,21 @@ func toConfig(install, forceIgnored []*api.Package, targets []string, cmdline [] | |
| } | ||
|
|
||
| providers := collectProviders(forceIgnored, install) | ||
| packageNames := sortedKeys(allPackages) | ||
| sortedPackages := make([]*bazeldnf.RPM, 0, len(packageNames)) | ||
| for _, name := range packageNames { | ||
| slices.Sort(targets) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we care about the order of the targets?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To make the lock file generation consistent and stable, we don't want multiple runs of the same required packages against the same db to be changing due to sorting differences |
||
| sortedPackages := make([]*bazeldnf.RPM, 0, len(allPackages)) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe move this down to where sortedPackages is constructed so it's a bit more clear what's happening |
||
|
|
||
| requested := make(map[string]bool) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should use empty struct instead of bool here as the former uses zero bytes |
||
| for _, pkg := range targets { | ||
| requested[pkg] = true | ||
| } | ||
|
|
||
| // make sure all requested packages have their full dependency tree set | ||
| for _, name := range sortedKeys(allPackages) { | ||
| pkg := allPackages[name] | ||
| deps, err := collectDependencies(name, pkg.Dependencies, providers, ignored) | ||
| if _, requested := requested[name]; !requested { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should use |
||
| continue | ||
| } | ||
| deps, err := collectDependencies(name, pkg.Dependencies, providers, ignored, allPackages) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
@@ -64,6 +74,22 @@ func toConfig(install, forceIgnored []*api.Package, targets []string, cmdline [] | |
| sortedPackages = append(sortedPackages, pkg) | ||
| } | ||
|
|
||
| // now for non requested packages make sure we don't get cycles | ||
| for _, name := range sortedKeys(allPackages) { | ||
| pkg := allPackages[name] | ||
| if _, requested := requested[name]; requested { | ||
| continue | ||
| } | ||
|
|
||
| pkg.SetDependencies(nil) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why set dependencies to nil here? |
||
|
|
||
| sortedPackages = append(sortedPackages, pkg) | ||
| } | ||
|
|
||
| slices.SortFunc(sortedPackages, func(a, b *bazeldnf.RPM) int { | ||
| return cmp.Compare(a.Name, b.Name) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isn't the sorting already implicit by virtue of having constructed sortedPackages with the sorted keys? |
||
| }) | ||
|
|
||
| lockFile := bazeldnf.Config{ | ||
| CommandLineArguments: cmdline, | ||
| ForceIgnored: sortedKeys(ignored), | ||
|
|
@@ -92,9 +118,19 @@ func collectProviders(pkgSets ...[]*api.Package) map[string]string { | |
| return providers | ||
| } | ||
|
|
||
| func collectDependencies(pkg string, requires []string, providers map[string]string, ignored map[string]bool) ([]string, error) { | ||
| func collectDependencies(pkg string, requires []string, providers map[string]string, ignored map[string]bool, allPackages map[string]*bazeldnf.RPM) ([]string, error) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we use something like https://pkg.go.dev/github.com/dominikbraun/graph to do a topological sort of the dependencies here and just get the result of that? |
||
| logrus.Debugf("Collecting dependencies for %s", pkg) | ||
| depSet := make(map[string]bool) | ||
| for _, req := range requires { | ||
| explored := make(map[string]bool) | ||
| for len(requires) > 0 { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we just use something like https://pkg.go.dev/github.com/dominikbraun/graph and do a topological sort? |
||
| req := requires[0] | ||
| requires = requires[1:] | ||
| logrus.Debugf("Processing dependency %s, pending %d", req, len(requires)) | ||
| if explored[req] { | ||
| logrus.Debugf("Ignoring already explored %s", req) | ||
| continue | ||
| } | ||
| explored[req] = true | ||
| if ignored[req] { | ||
| logrus.Debugf("Ignoring dependency %s", req) | ||
| continue | ||
|
|
@@ -104,28 +140,71 @@ func collectDependencies(pkg string, requires []string, providers map[string]str | |
| if !ok { | ||
| return nil, fmt.Errorf("could not find provider for %s", req) | ||
| } | ||
| logrus.Debugf("Found provider %s for %s", provider, req) | ||
| if ignored[provider] { | ||
| logrus.Debugf("Ignoring provider %s for %s", provider, req) | ||
| logrus.Debugf("Ignoring provider %s", provider) | ||
| continue | ||
| } | ||
| depSet[provider] = true | ||
| requires = append(requires, allPackages[provider].Dependencies...) | ||
| } | ||
| return sortedKeys(depSet), nil | ||
| } | ||
|
|
||
| deps := sortedKeys(depSet) | ||
| func removeCyclicDependencies(targets []string, allPackages []*bazeldnf.RPM) []*bazeldnf.RPM { | ||
| allPackagesMap := make(map[string]*bazeldnf.RPM) | ||
|
|
||
| found := map[string]bool{pkg: true} | ||
| for _, installPackage := range allPackages { | ||
| allPackagesMap[installPackage.Name] = installPackage | ||
| } | ||
|
|
||
| for _, target := range targets { | ||
| visitedMap := make(map[string]bool) | ||
| recursionStack := make(map[string]bool) | ||
|
|
||
| removeCyclicDependenciesHelper(allPackagesMap, target, visitedMap, recursionStack) | ||
| } | ||
|
|
||
| return allPackages | ||
| } | ||
|
|
||
| func removeCyclicDependenciesHelper(allPackages map[string]*bazeldnf.RPM, pkg string, visitedMap, recursionStack map[string]bool) bool { | ||
| /* | ||
| * This is a recursive function that removes cyclic dependencies from the | ||
| * dependency graph in the case cycles are found | ||
| */ | ||
| visitedMap[pkg] = true | ||
| recursionStack[pkg] = true | ||
|
|
||
| if _, ok := allPackages[pkg]; !ok { | ||
| return false | ||
| } | ||
|
|
||
| if allPackages[pkg].Dependencies == nil { | ||
| return false | ||
| } | ||
|
|
||
| cleanDependencies := make([]string, 0, len(allPackages[pkg].Dependencies)) | ||
|
|
||
| // RPMs may have circular dependencies, even depend on themselves. | ||
| // we need to ignore such dependencies | ||
| nonCyclicDeps := make([]string, 0, len(deps)) | ||
| for _, dep := range deps { | ||
| if found[dep] { | ||
| for _, dep := range allPackages[pkg].Dependencies { | ||
| if _, visited := visitedMap[dep]; !visited { | ||
| if removeCyclicDependenciesHelper(allPackages, dep, visitedMap, recursionStack) { | ||
| // ignore cycle | ||
| logrus.Debugf("Ignoring cyclic dependency %s -> %s", pkg, dep) | ||
| continue | ||
| } | ||
| } else if _, recursed := recursionStack[dep]; recursed { | ||
| // ignore cycle | ||
| logrus.Debugf("Ignoring cyclic dependency in recursion stack %s -> %s", pkg, dep) | ||
| continue | ||
| } | ||
|
|
||
| nonCyclicDeps = append(nonCyclicDeps, dep) | ||
| cleanDependencies = append(cleanDependencies, dep) | ||
| } | ||
|
|
||
| return nonCyclicDeps, nil | ||
| recursionStack[pkg] = false | ||
|
|
||
| newPkg := allPackages[pkg].Clone() | ||
| newPkg.SetDependencies(cleanDependencies) | ||
| allPackages[pkg] = newPkg | ||
|
|
||
| return false | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.