Skip to content

Commit d169351

Browse files
feat: add lockfile and registry packages for BCR compatibility
Add two new packages to support Bazel Central Registry integration: lockfile/: MODULE.bazel.lock compatibility - Parse, write, merge lockfiles matching Bazel's format - Exhaustive Bazel-to-lockfile version mapping (all releases 6.2.0-9.0.0) - Bazel uses exact version matching (no forward/backward compat) - Deterministic JSON output for reproducibility registry/: BCR JSON Schema validation - Embedded schemas for metadata.json and source.json - Type-safe structs matching BCR format - HTTP client with caching support Complete version mapping (from BazelLockFileValue.java): - v1: 6.2.x, 6.3.x (lockfile introduced in 6.2) - v3: 6.4.x-6.6.x, 7.0.x - v6: 7.1.x - v11: 7.2.x-7.4.x (incremental format) - v13: 7.5.x-7.7.x - v16: 8.0.x (LTS) - v18: 8.1.x-8.4.x - v24: 8.5.x - v26: 9.0.x
1 parent 4243bf7 commit d169351

File tree

15 files changed

+2771
-0
lines changed

15 files changed

+2771
-0
lines changed

lockfile/doc.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Package lockfile provides types and operations for Bazel's MODULE.bazel.lock file.
2+
//
3+
// The lockfile captures the resolved state of module dependencies, ensuring
4+
// reproducible builds across machines and time. This package implements
5+
// Bazel's lockfile format (version 26+) for reading, writing, and manipulation.
6+
//
7+
// # Lockfile Structure
8+
//
9+
// A Bazel lockfile contains:
10+
// - lockFileVersion: Schema version for format compatibility
11+
// - registryFileHashes: Integrity hashes for registry files fetched
12+
// - selectedYankedVersions: Explicitly allowed yanked versions
13+
// - moduleExtensions: Cached results of module extension evaluations
14+
// - facts: Additional facts about module extensions
15+
//
16+
// # Usage
17+
//
18+
// Read an existing lockfile:
19+
//
20+
// lf, err := lockfile.ReadFile("MODULE.bazel.lock")
21+
// if err != nil {
22+
// log.Fatal(err)
23+
// }
24+
// fmt.Printf("Lockfile version: %d\n", lf.Version)
25+
//
26+
// Create a new lockfile:
27+
//
28+
// lf := lockfile.New()
29+
// lf.SetRegistryHash("https://bcr.bazel.build/modules/rules_go/0.50.1/MODULE.bazel", hash)
30+
// if err := lf.WriteFile("MODULE.bazel.lock"); err != nil {
31+
// log.Fatal(err)
32+
// }
33+
//
34+
// # Compatibility
35+
//
36+
// This package targets lockfile version 26 (Bazel 7.x/8.x). Older versions
37+
// may have different schemas and are not fully supported.
38+
package lockfile

lockfile/io.go

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
package lockfile
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"os"
9+
"sort"
10+
)
11+
12+
// lockfilePermissions is the file permission mode for lockfiles.
13+
// Using 0600 for security (owner read/write only).
14+
const lockfilePermissions = 0o600
15+
16+
// ReadFile reads and parses a lockfile from the given path.
17+
func ReadFile(path string) (*Lockfile, error) {
18+
data, err := os.ReadFile(path)
19+
if err != nil {
20+
return nil, fmt.Errorf("failed to read lockfile: %w", err)
21+
}
22+
return Parse(data)
23+
}
24+
25+
// Parse parses lockfile JSON data.
26+
func Parse(data []byte) (*Lockfile, error) {
27+
var lf Lockfile
28+
if err := json.Unmarshal(data, &lf); err != nil {
29+
return nil, fmt.Errorf("failed to parse lockfile JSON: %w", err)
30+
}
31+
32+
// Initialize nil maps to empty maps for consistency
33+
if lf.RegistryFileHashes == nil {
34+
lf.RegistryFileHashes = make(map[string]string)
35+
}
36+
if lf.SelectedYankedVersions == nil {
37+
lf.SelectedYankedVersions = make(map[string]string)
38+
}
39+
if lf.ModuleExtensions == nil {
40+
lf.ModuleExtensions = make(map[string]ModuleExtensionEntry)
41+
}
42+
if lf.Facts == nil {
43+
lf.Facts = make(map[string]json.RawMessage)
44+
}
45+
46+
return &lf, nil
47+
}
48+
49+
// WriteFile writes the lockfile to the given path with deterministic formatting.
50+
func (l *Lockfile) WriteFile(path string) error {
51+
data, err := l.Marshal()
52+
if err != nil {
53+
return err
54+
}
55+
return os.WriteFile(path, data, lockfilePermissions)
56+
}
57+
58+
// WriteTo writes the lockfile to the given writer.
59+
func (l *Lockfile) WriteTo(w io.Writer) (int64, error) {
60+
data, err := l.Marshal()
61+
if err != nil {
62+
return 0, err
63+
}
64+
n, err := w.Write(data)
65+
return int64(n), err
66+
}
67+
68+
// Marshal serializes the lockfile to JSON with deterministic key ordering.
69+
func (l *Lockfile) Marshal() ([]byte, error) {
70+
// Use a custom marshaling approach for deterministic output
71+
return marshalDeterministic(l)
72+
}
73+
74+
// MarshalIndent serializes the lockfile with indentation for readability.
75+
func (l *Lockfile) MarshalIndent(prefix, indent string) ([]byte, error) {
76+
data, err := l.Marshal()
77+
if err != nil {
78+
return nil, err
79+
}
80+
81+
var buf bytes.Buffer
82+
if err := json.Indent(&buf, data, prefix, indent); err != nil {
83+
return nil, err
84+
}
85+
return buf.Bytes(), nil
86+
}
87+
88+
// marshalDeterministic produces JSON with sorted keys for reproducibility.
89+
func marshalDeterministic(l *Lockfile) ([]byte, error) {
90+
// Create ordered representation
91+
ordered := orderedLockfile{
92+
Version: l.Version,
93+
RegistryFileHashes: sortedStringMap(l.RegistryFileHashes),
94+
SelectedYankedVersions: sortedStringMap(l.SelectedYankedVersions),
95+
ModuleExtensions: sortedExtensions(l.ModuleExtensions),
96+
Facts: sortedFacts(l.Facts),
97+
}
98+
99+
var buf bytes.Buffer
100+
encoder := json.NewEncoder(&buf)
101+
encoder.SetEscapeHTML(false)
102+
encoder.SetIndent("", " ")
103+
if err := encoder.Encode(ordered); err != nil {
104+
return nil, err
105+
}
106+
return buf.Bytes(), nil
107+
}
108+
109+
// orderedLockfile is used for deterministic JSON output.
110+
type orderedLockfile struct {
111+
Version int `json:"lockFileVersion"`
112+
RegistryFileHashes orderedStringMap `json:"registryFileHashes"`
113+
SelectedYankedVersions orderedStringMap `json:"selectedYankedVersions"`
114+
ModuleExtensions orderedExtensionMap `json:"moduleExtensions"`
115+
Facts orderedRawMessageMap `json:"facts"`
116+
}
117+
118+
// orderedStringMap maintains insertion order for JSON marshaling.
119+
type orderedStringMap struct {
120+
keys []string
121+
values map[string]string
122+
}
123+
124+
func sortedStringMap(m map[string]string) orderedStringMap {
125+
keys := make([]string, 0, len(m))
126+
for k := range m {
127+
keys = append(keys, k)
128+
}
129+
sort.Strings(keys)
130+
return orderedStringMap{keys: keys, values: m}
131+
}
132+
133+
func (o orderedStringMap) MarshalJSON() ([]byte, error) {
134+
if len(o.keys) == 0 {
135+
return []byte("{}"), nil
136+
}
137+
138+
var buf bytes.Buffer
139+
buf.WriteByte('{')
140+
for i, k := range o.keys {
141+
if i > 0 {
142+
buf.WriteByte(',')
143+
}
144+
keyJSON, _ := json.Marshal(k)
145+
valJSON, _ := json.Marshal(o.values[k])
146+
buf.Write(keyJSON)
147+
buf.WriteByte(':')
148+
buf.Write(valJSON)
149+
}
150+
buf.WriteByte('}')
151+
return buf.Bytes(), nil
152+
}
153+
154+
// orderedExtensionMap maintains insertion order for module extensions.
155+
type orderedExtensionMap struct {
156+
keys []string
157+
values map[string]ModuleExtensionEntry
158+
}
159+
160+
func sortedExtensions(m map[string]ModuleExtensionEntry) orderedExtensionMap {
161+
keys := make([]string, 0, len(m))
162+
for k := range m {
163+
keys = append(keys, k)
164+
}
165+
sort.Strings(keys)
166+
return orderedExtensionMap{keys: keys, values: m}
167+
}
168+
169+
func (o orderedExtensionMap) MarshalJSON() ([]byte, error) {
170+
if len(o.keys) == 0 {
171+
return []byte("{}"), nil
172+
}
173+
174+
var buf bytes.Buffer
175+
buf.WriteByte('{')
176+
for i, k := range o.keys {
177+
if i > 0 {
178+
buf.WriteByte(',')
179+
}
180+
keyJSON, _ := json.Marshal(k)
181+
valJSON, err := json.Marshal(o.values[k])
182+
if err != nil {
183+
return nil, err
184+
}
185+
buf.Write(keyJSON)
186+
buf.WriteByte(':')
187+
buf.Write(valJSON)
188+
}
189+
buf.WriteByte('}')
190+
return buf.Bytes(), nil
191+
}
192+
193+
// orderedRawMessageMap maintains insertion order for facts.
194+
type orderedRawMessageMap struct {
195+
keys []string
196+
values map[string]json.RawMessage
197+
}
198+
199+
func sortedFacts(m map[string]json.RawMessage) orderedRawMessageMap {
200+
keys := make([]string, 0, len(m))
201+
for k := range m {
202+
keys = append(keys, k)
203+
}
204+
sort.Strings(keys)
205+
return orderedRawMessageMap{keys: keys, values: m}
206+
}
207+
208+
func (o orderedRawMessageMap) MarshalJSON() ([]byte, error) {
209+
if len(o.keys) == 0 {
210+
return []byte("{}"), nil
211+
}
212+
213+
var buf bytes.Buffer
214+
buf.WriteByte('{')
215+
for i, k := range o.keys {
216+
if i > 0 {
217+
buf.WriteByte(',')
218+
}
219+
keyJSON, _ := json.Marshal(k)
220+
buf.Write(keyJSON)
221+
buf.WriteByte(':')
222+
buf.Write(o.values[k])
223+
}
224+
buf.WriteByte('}')
225+
return buf.Bytes(), nil
226+
}
227+
228+
// Exists returns true if a lockfile exists at the given path.
229+
func Exists(path string) bool {
230+
_, err := os.Stat(path)
231+
return err == nil
232+
}
233+
234+
// DefaultPath returns the default lockfile path relative to a workspace root.
235+
func DefaultPath(workspaceRoot string) string {
236+
if workspaceRoot == "" {
237+
return "MODULE.bazel.lock"
238+
}
239+
return workspaceRoot + "/MODULE.bazel.lock"
240+
}

0 commit comments

Comments
 (0)