Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 2 additions & 12 deletions .github/workflows/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,15 +169,5 @@ jobs:
import %workspace%/../../.github/workflows/ci.bazelrc
- id: prepare
run: |
cd e2e/bzlmod-toolchain-circular-dependencies
bazelisk run @bazeldnf_rpms//:fetch-repo
bazelisk run @bazeldnf_rpms//:update-lock-file
- id: test
run: |
cd e2e/bzlmod-toolchain-circular-dependencies
bazelisk build //... || status=$?
if [ ${status} -ne 0 ]; then
echo "::warning::Optional job failed."
echo "optional_fail=true" >> "${GITHUB_OUTPUT}"
echo "optional_fail_status=${status}" >> "${GITHUB_OUTPUT}"
fi
(cd e2e/bzlmod-toolchain-circular-dependencies && make test)
(cd e2e/bzlmod-toolchain-circular-dependencies-fake && make test)
54 changes: 0 additions & 54 deletions .github/workflows/allowed-to-fail.yml

This file was deleted.

12 changes: 11 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,23 @@ e2e-bzlmod-toolchain-circular-dependencies:
) \
done

e2e-bzlmod-toolchain-circular-dependencies-fake:
@for version in 7.x 8.x; do \
( \
cd e2e/bzlmod-toolchain-circular-dependencies-fake && \
echo "Testing $$version bzlmod lock file from args" > /dev/stderr && \
USE_BAZEL_VERSION=$$version bazelisk --batch build //... \
) \
done


e2e: e2e-workspace \
e2e-bzlmod \
e2e-bzlmod-build-toolchain \
e2e-bazel-bzlmod-lock-file \
e2e-bazel-bzlmod-lock-file-from-args \
e2e-bzlmod-toolchain-circular-dependencies
e2e-bzlmod-toolchain-circular-dependencies \
e2e-bzlmod-toolchain-circular-dependencies-fake

fmt: gofmt buildifier

Expand Down
35 changes: 27 additions & 8 deletions bazeldnf/extensions.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -68,7 +69,7 @@ _ALIAS_TEMPLATE = """\
alias(
name = "{name}",
actual = "@{actual_name}//rpm",
visibility = ["//visibility:public"],
visibility = ["{visibility}"],
)
"""

Expand Down Expand Up @@ -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:
Comment thread
kellyma2 marked this conversation as resolved.
visibility = "//:__subpackages__"

repository_ctx.file(
"%s/BUILD.bazel" % name,
_ALIAS_TEMPLATE.format(
name = name,
actual_name = actual_name,
visibility = visibility,
),
)

Expand All @@ -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 = []),
},
)

Expand Down Expand Up @@ -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()):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where are these constraints coming from?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
115 changes: 97 additions & 18 deletions cmd/config_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we care about the order of the targets?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should use ok here instead of requested because that's the normal pattern and this overloads the variable name

continue
}
deps, err := collectDependencies(name, pkg.Dependencies, providers, ignored, allPackages)
if err != nil {
return nil, err
}
Expand All @@ -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)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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),
Expand Down Expand Up @@ -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) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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
Expand All @@ -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
}
Loading
Loading