blob: af8d73ee0183a07ade2439f717f8706204797609 [file] [edit]
# SPDX-FileCopyrightText: Copyright 2026 The Pigweed Authors
# SPDX-License-Identifier: Apache-2.0
"""Utility for generating deterministic repository names for Zephyr contextual configurations."""
def normalize_app_label(app_label):
"""Normalizes an application label to a consistent path format.
Args:
app_label: The label of the application (e.g., //apps/blinky) or a string.
Returns:
A normalized string path.
"""
if type(app_label) == "Label":
if app_label.package:
return app_label.package
return app_label.name
norm_app = str(app_label)
# Discard repo part if present (e.g. @@repo//path -> path)
if "//" in norm_app:
norm_app = norm_app.split("//")[-1]
norm_app = norm_app.strip("/").lstrip(":")
# If the label has a target name, and it matches the last part of the package name, strip it.
# e.g., examples/hello_bazel:hello_bazel -> examples/hello_bazel
if ":" in norm_app:
pkg, target = norm_app.split(":", 1)
t_base = target
for ext in [".elf", "_elf", "_bin", ".bin"]:
if t_base.endswith(ext):
t_base = t_base[:-len(ext)]
if pkg.endswith(target) or target == pkg.split("/")[-1] or pkg.endswith(t_base) or t_base == pkg.split("/")[-1]:
norm_app = pkg
return norm_app
def get_app_hash(app_label):
"""Extracts and hashes an application label.
Args:
app_label: The label of the application (e.g., //apps/blinky).
Returns:
An 8-character deterministic hash string for the app.
"""
norm_app = normalize_app_label(app_label)
# 2. Hash to 8 characters for collision resistance.
# We use a 32-bit unsigned hex representation of the Starlark hash.
h = hash(norm_app)
app_hash = "%x" % (h & 0xFFFFFFFF)
app_hash = ("00000000" + app_hash)[-8:]
return app_hash
def sanitize_board_id(board_id):
"""Sanitizes a board ID for use in a repository name.
Args:
board_id: The Zephyr board ID (e.g., nrf52840dk/nrf52840).
Returns:
A sanitized board ID safe for repository names.
"""
return board_id.replace("/", "_").replace("-", "_").replace(".", "_").replace("@", "_")
def get_zc_repo_name_from_precomputed(app_hash, safe_board):
"""Generates a deterministic repository name from a precalculated app hash and a pre-sanitized board ID.
Args:
app_hash: Precomputed app hash from get_app_hash.
safe_board: Precomputed safe board name from sanitize_board_id.
Returns:
A string following the pattern zc_<app_hash>_<safe_board>.
"""
return "zc_%s_%s" % (app_hash, safe_board)
def get_zc_repo_name_from_hash(app_hash, board_id):
"""Generates a deterministic repository name from a precalculated app hash and board ID.
Args:
app_hash: Precomputed app hash from get_app_hash.
board_id: The Zephyr board ID.
Returns:
A string following the pattern zc_<app_hash>_<board_id>.
"""
safe_board = sanitize_board_id(board_id)
return get_zc_repo_name_from_precomputed(app_hash, safe_board)
def get_zc_repo_name(app_label, board_id):
"""Generates a deterministic repository name for an (app, board) combination.
Args:
app_label: The label of the application (e.g., //apps/blinky).
board_id: The Zephyr board ID (e.g., nrf52840dk/nrf52840).
Returns:
A string following the pattern zc_<app_hash>_<board_id>.
"""
app_hash = get_app_hash(app_label)
return get_zc_repo_name_from_hash(app_hash, board_id)
def normalize_repo_name(name):
"""Normalizes a Bzlmod canonical repository name to a simple @name.
Args:
name: The canonical repository name.
Returns:
The normalized repository name starting with @@ for canonical names or @ for apparent names.
"""
if not name:
return ""
# Handle @@+zephyr_patch_file+zephyr -> @zephyr (if we want the apparent name)
# But for absolute references in repo rules, we often want the canonical one.
if name.startswith("@@"):
return name
if name.startswith("+"):
return "@@" + name
if name.endswith("+") or "+" in name or "~" in name:
return "@@" + name
return "@" + name
def parse_label(label_or_str):
"""Parses a label or string into its component parts, including the apparent repo name.
Args:
label_or_str: A Label object or a string representation of a label.
Returns:
A struct containing:
canonical_repo: The full canonical name (e.g., '@@+_repo_rules+hal_atmel' or '@@//').
apparent_repo: The base/apparent name of the repository (e.g., 'hal_atmel' or '').
package: The package path (e.g., 'modules/foo' or '').
name: The target name (e.g., 'my_module' or '').
"""
# Purely manual string parsing for all string inputs to avoid repo mapping issues in tests
canonical_repo = ""
apparent_repo = ""
package = ""
name = ""
if type(label_or_str) == "Label":
lbl = label_or_str
if lbl.workspace_name:
canonical_repo = "@@" + lbl.workspace_name
apparent_repo = lbl.workspace_name
else:
canonical_repo = "@@"
apparent_repo = ""
package = lbl.package
name = lbl.name
else:
label_str = str(label_or_str)
if label_str.startswith("@@"):
# Canonical label
if "//" in label_str:
canonical_repo, rest = label_str.split("//", 1)
if ":" in rest:
package, name = rest.split(":", 1)
else:
package = rest
else:
canonical_repo = label_str
c_name = canonical_repo[2:]
if "~" in c_name:
parts = c_name.split("~")
if len(parts) >= 2:
apparent_repo = parts[0]
elif "+" in c_name:
parts = c_name.split("+")
if len(parts) >= 4 and parts[1] == "":
apparent_repo = parts[3]
elif len(parts) >= 3 and parts[0] == "" and parts[1] == "_repo_rules":
apparent_repo = parts[2]
else:
apparent_repo = parts[-1]
else:
apparent_repo = c_name
elif label_str.startswith("@"):
# Apparent label
if "//" in label_str:
repo_part, rest = label_str.split("//", 1)
apparent_repo = repo_part[1:]
canonical_repo = "@@" + apparent_repo # Fallback for unmapped repos in tests
if ":" in rest:
package, name = rest.split(":", 1)
else:
package = rest
else:
apparent_repo = label_str[1:]
canonical_repo = "@@" + apparent_repo
else:
# Local label (main workspace)
canonical_repo = "@@"
apparent_repo = ""
if label_str.startswith("//"):
rest = label_str[2:]
if ":" in rest:
package, name = rest.split(":", 1)
else:
package = rest
elif ":" in label_str:
package = native.package_name()
name = label_str.split(":", 1)[1]
else:
package = native.package_name()
name = label_str
return struct(
canonical_repo = canonical_repo,
apparent_repo = apparent_repo,
package = package,
name = name,
)
def derive_module_label(dir_label_str, relpath, name):
"""Derives a normalized canonical label for a module.
Args:
dir_label_str: The base directory label string (e.g., '@hal_atmel//' or '//mock_modules_dir').
relpath: Relative path from the base directory to the module.
name: The target name of the module.
Returns:
A canonical label string starting with @@.
"""
info = parse_label(dir_label_str)
if info.package and relpath:
new_pkg = info.package + "/" + relpath
elif info.package:
new_pkg = info.package
else:
new_pkg = relpath
if info.canonical_repo and info.canonical_repo != "@@":
return "%s//%s:%s" % (info.canonical_repo, new_pkg, name)
else:
return "@@//%s:%s" % (new_pkg, name)
def resolve_module_roots(ctx_or_rctx, state):
"""Resolves the absolute paths of all module roots from the state.
Args:
ctx_or_rctx: The module context (mctx) or repository context (rctx).
state: The decoded state JSON dict.
Returns:
A list of string paths to the module roots.
"""
resolved_modules = []
# Process both standard and config-only modules
for m_set in ["modules", "config_only_modules"]:
m_dict = state.get(m_set, {})
if type(m_dict) == "dict":
for m_info in m_dict.values():
m_label_str = m_info.get("original")
if m_label_str:
m_label = Label(m_label_str)
m_path = ctx_or_rctx.path(m_label)
if m_path.basename == "module.yml" and m_path.dirname.basename == "zephyr":
mod_root = m_path.dirname.dirname
else:
mod_root = m_path.dirname
relpath = m_info.get("relpath")
if relpath:
mod_root = mod_root.get_child(relpath)
path_str = str(mod_root)
if path_str not in resolved_modules:
resolved_modules.append(path_str)
return resolved_modules
def get_repo_root(repo_name):
"""Returns the canonical external repository path prefix."""
if not repo_name.startswith("@"):
repo_name = "@" + repo_name
return Label(repo_name + "//:BUILD").workspace_root
def get_zephyr_bazel_root():
"""Returns the root path for zephyr."""
workspace_root = Label("//:BUILD.bazel").workspace_root
return workspace_root + "/" if workspace_root else ""