blob: 56ea530d9eeb5a8f0a60c8e8bf51cd4b0b35d452 [file]
# Merges an arbitrary number of MODULE.bazel.lock files.
#
# Input: an array of MODULE.bazel.lock JSON objects (as produced by `jq -s`).
# Output: a single MODULE.bazel.lock JSON object.
#
# This script assumes that all files are valid JSON and have a numeric
# "lockFileVersion" field. It will not fail on any such files, but only
# preserves information for files with a version of 10 or higher.
#
# The first file is considered to be the base when deciding which values to
# keep in case of conflicts.
# Like unique, but preserves the order of the first occurrence of each element.
def stable_unique:
reduce .[] as $item ([]; if index($item) == null then . + [$item] else . end);
# Given an array of objects, shallowly merges the result of applying f to each
# object into a single object, with a few special properties:
# 1. Values are uniquified before merging and then merged with last-wins
# semantics. Assuming that the first value is the base, this ensures that
# later occurrences of the base value do not override other values. For
# example, when this is called with B A1 A2 and A1 contains changes to a
# field but A2 does not (compared to B), the changes in A1 will be preserved.
# 2. Object keys on the top level are sorted lexicographically after merging,
# but are additionally split on ":". This ensures that module extension IDs,
# which start with labels, sort as strings in the same way as they due as
# structured objects in Bazel (that is, //python/extensions:python.bzl
# sorts before //python/extensions/private:internal_deps.bzl).
def shallow_merge(f):
map(f) | stable_unique | add | to_entries | sort_by(.key | split(":")) | from_entries;
(
# Ignore all MODULE.bazel.lock files that do not have the maximum
# lockFileVersion.
(map(.lockFileVersion) | max) as $maxVersion
| map(select(.lockFileVersion == $maxVersion))
# Compute the maximum factsVersion observed across lockfiles per extension
# ID. Missing entries default to 0.
| (
map(.factsVersions // {} | to_entries) | flatten
| if length > 0 then
group_by(.key)
| map({key: .[0].key, value: (map(.value) | max)})
| from_entries
else {} end
) as $maxFactsVersions
# Within each lockfile, drop facts entries whose own factsVersion does not
# match the global maximum: those are at an outdated schema and cannot be
# merged with newer-schema entries.
| map(
if has("facts") then
(.factsVersions // {}) as $fv
| .facts |= with_entries(
.key as $k
| select((($fv[$k]) // 0) == (($maxFactsVersions[$k]) // 0))
)
else . end
)
| {
lockFileVersion: $maxVersion,
registryFileHashes: shallow_merge(.registryFileHashes),
selectedYankedVersions: shallow_merge(.selectedYankedVersions),
# Group extension results by extension ID across all lockfiles with
# shallowly merged factors map, then shallowly merge the results.
moduleExtensions: (map(.moduleExtensions | to_entries)
| flatten
| if length > 0 then group_by(.key) | shallow_merge({(.[0].key): shallow_merge(.value)}) else {} end),
# Group facts by extension ID across all lockfiles (already filtered to
# the latest factsVersion above) and shallowly merge their dicts.
facts: (if any(has("facts")) then
map(.facts // {} | to_entries) | flatten |
if length > 0 then group_by(.key) | shallow_merge({(.[0].key): shallow_merge(.value)}) else {} end
else null end),
# Keep only non-zero versions, mirroring how Bazel writes the lockfile.
factsVersions: (if any(has("factsVersions")) then
$maxFactsVersions | with_entries(select(.value != 0))
else null end),
}
# Filter out null values for missing top-level keys such as facts and
# factsVersions.
| with_entries(select(.value != null))
)? //
# We get here if the lockfiles with the highest lockFileVersion could not be
# processed, for example because all lockfiles have lockFileVersion < 10.
# In this case Bazel 7.2.0+ would ignore all lockfiles, so we might as well
# return the first lockfile for the proper "mismatched version" error
# message.
.[0]