Skip to content

Commit 1603acd

Browse files
feat: implement full Bazel bzlmod parity features
This commit implements all major features needed for full parity with Bazel's bzlmod implementation: **Phase 1: Lockfile Support (P0)** - Add lockfile/resolution.go with FromResolution() and Compare() - Compute SHA256 hashes of MODULE.bazel files - Track yanked versions in lockfile - Add LockfileMode option (Off, Update, Error, Refresh) **Phase 2: source.json Parsing (P1)** - Add local_path source type to registry/types.go - Add IsLocalPath() method to Source struct - Add source_test.go with comprehensive tests **Phase 3: Bazel Compatibility Validation (P1)** - Add bazel_compat.go with constraint parsing - Support >=, <=, <, >, - (exclusion) operators - Add BazelCompatibilityMode option (Off, Warn, Error) - Validate modules against configured Bazel version **Phase 4: Vendor Directory Full Implementation (P2)** - Add registry_vendor.go with vendor registry - Synthesize metadata from directory structure - Support local_path source.json for vendored modules - Chain vendor registry before remote registries **Phase 5: Registry Mirrors (P2)** - Parse bazel_registry.json for mirror URLs - Add fetchWithMirrors() with fallback logic - Skip 404 errors but try mirrors for server errors **Phase 6: max_compatibility_level Strategy Enumeration (P3)** - Add computePossibleResolutionResultsForOneDepSpec() - Add enumerateStrategies() for cartesian product - Find all valid versions per DepSpec for MVS **Phase 7: Multi-Round Nodep Discovery (P3)** - Add NodepDependencies field to ModuleInfo - Add IsNodepDep field to Dependency - Track unfulfilled nodep edges across rounds - Continue discovery until no new nodeps can be fulfilled All tests pass. Reference: Bazel bzlmod implementation in src/main/java/com/google/devtools/build/lib/bazel/bzlmod/
1 parent 64aae6c commit 1603acd

17 files changed

+3490
-149
lines changed

bazel_compat.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package gobzlmod
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strconv"
7+
"strings"
8+
9+
"github.com/albertocavalcante/go-bzlmod/selection/version"
10+
)
11+
12+
// bazelCompatConstraint represents a parsed bazel_compatibility constraint.
13+
//
14+
// Reference: ModuleFileGlobals.java lines 213-225
15+
// See: https://github.com/bazelbuild/bazel/blob/master/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/ModuleFileGlobals.java
16+
type bazelCompatConstraint struct {
17+
operator string // ">=", "<=", ">", "<", "-"
18+
version string // The version part (e.g., "7.0.0")
19+
}
20+
21+
// bazelCompatConstraintPattern matches bazel_compatibility entries.
22+
// Format: (>=|<=|>|<|-)X.Y.Z
23+
var bazelCompatConstraintPattern = regexp.MustCompile(`^(>=|<=|>|<|-)(\d+\.\d+\.\d+)$`)
24+
25+
// parseBazelCompatConstraint parses a bazel_compatibility constraint string.
26+
func parseBazelCompatConstraint(s string) (*bazelCompatConstraint, error) {
27+
match := bazelCompatConstraintPattern.FindStringSubmatch(s)
28+
if match == nil {
29+
return nil, fmt.Errorf("invalid bazel_compatibility constraint: %q", s)
30+
}
31+
return &bazelCompatConstraint{
32+
operator: match[1],
33+
version: match[2],
34+
}, nil
35+
}
36+
37+
// checkBazelCompatibility checks if the given Bazel version satisfies a constraint.
38+
//
39+
// Reference: BazelModuleResolutionFunction.java lines 298-333
40+
// See: https://github.com/bazelbuild/bazel/blob/master/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleResolutionFunction.java
41+
func (c *bazelCompatConstraint) check(bazelVersion string) bool {
42+
cmp := version.Compare(bazelVersion, c.version)
43+
44+
switch c.operator {
45+
case ">=":
46+
return cmp >= 0
47+
case "<=":
48+
return cmp <= 0
49+
case ">":
50+
return cmp > 0
51+
case "<":
52+
return cmp < 0
53+
case "-":
54+
// Exclusion: the Bazel version must NOT equal the constraint version
55+
return cmp != 0
56+
default:
57+
return false
58+
}
59+
}
60+
61+
// checkBazelCompatibility checks if a Bazel version satisfies all bazel_compatibility constraints.
62+
// Returns (compatible, reason) where reason explains why it's incompatible.
63+
//
64+
// Reference: BazelModuleResolutionFunction.java lines 298-333
65+
func checkBazelCompatibility(bazelVersion string, constraints []string) (bool, string) {
66+
if len(constraints) == 0 {
67+
return true, ""
68+
}
69+
70+
if bazelVersion == "" {
71+
return true, "" // No Bazel version specified, skip validation
72+
}
73+
74+
// Normalize the Bazel version (strip any prerelease/build metadata for comparison)
75+
normalizedBazel := normalizeBazelVersion(bazelVersion)
76+
77+
var failedConstraints []string
78+
for _, constraintStr := range constraints {
79+
constraint, err := parseBazelCompatConstraint(constraintStr)
80+
if err != nil {
81+
// Invalid constraint format, skip it
82+
continue
83+
}
84+
85+
if !constraint.check(normalizedBazel) {
86+
failedConstraints = append(failedConstraints, constraintStr)
87+
}
88+
}
89+
90+
if len(failedConstraints) == 0 {
91+
return true, ""
92+
}
93+
94+
// Build explanation
95+
if len(failedConstraints) == 1 {
96+
return false, fmt.Sprintf("requires %s", failedConstraints[0])
97+
}
98+
return false, fmt.Sprintf("requires %s", strings.Join(failedConstraints, " and "))
99+
}
100+
101+
// normalizeBazelVersion strips prerelease and build metadata from a Bazel version.
102+
// For example, "7.0.0-pre.20231115.1" becomes "7.0.0".
103+
func normalizeBazelVersion(v string) string {
104+
// Find the first hyphen or plus sign
105+
if idx := strings.IndexAny(v, "-+"); idx >= 0 {
106+
v = v[:idx]
107+
}
108+
109+
// Validate it looks like a version (X.Y.Z)
110+
parts := strings.Split(v, ".")
111+
if len(parts) >= 3 {
112+
// Ensure first 3 parts are numeric
113+
for i := 0; i < 3; i++ {
114+
if _, err := strconv.Atoi(parts[i]); err != nil {
115+
return v // Return original if not valid
116+
}
117+
}
118+
return strings.Join(parts[:3], ".")
119+
}
120+
121+
return v
122+
}
123+
124+
// checkModuleBazelCompatibility checks all resolved modules for Bazel compatibility
125+
// and populates the IsBazelIncompatible and BazelIncompatibilityReason fields.
126+
func checkModuleBazelCompatibility(modules []ModuleToResolve, moduleInfoCache map[string]*ModuleInfo, bazelVersion string) {
127+
for i := range modules {
128+
m := &modules[i]
129+
130+
// Get the module's bazel_compatibility constraints from the cache
131+
key := m.Name + "@" + m.Version
132+
if info, ok := moduleInfoCache[key]; ok && len(info.BazelCompatibility) > 0 {
133+
m.BazelCompatibility = info.BazelCompatibility
134+
compatible, reason := checkBazelCompatibility(bazelVersion, info.BazelCompatibility)
135+
if !compatible {
136+
m.IsBazelIncompatible = true
137+
m.BazelIncompatibilityReason = reason
138+
}
139+
}
140+
}
141+
}

0 commit comments

Comments
 (0)