| # Copyright 2025 The Pigweed Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| |
| load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") |
| load("@zephyr-bazel//:naming.bzl", "derive_module_label", "get_app_hash", "get_zc_repo_name_from_precomputed", "parse_label", "sanitize_board_id") |
| load("//bazel/private:repo_rule_python.bzl", "COMMON_PY_REPO_RULE_ATTRS", "get_python") |
| load("//bazel/private:zephyr_kconfig_gen_symbols.bzl", "zephyr_kconfig_schema") |
| load("//bazel/private:zephyr_kconfig_gen_values.bzl", "gen_zephyr_config") |
| load("//bazel/private:zephyr_state.bzl", "zephyr_index_repo", "zephyr_state_repo") |
| |
| # --- Helper Functions --- |
| |
| def _parse_board_id(board_id): |
| """Splits a board_id string into (board_name, qualifiers, board_rev).""" |
| board_name, _, rev_and_qual = board_id.partition("@") |
| board_rev, _, rev_qualifiers = rev_and_qual.partition("/") |
| board_name, slash, name_qualifiers = board_name.partition("/") |
| qualifiers = name_qualifiers if slash else rev_qualifiers |
| return board_name, qualifiers, board_rev |
| |
| def _get_override_candidates(board_name, qualifiers, board_rev, ext): |
| """Returns application boards/ override candidate filenames in precedence order.""" |
| safe_q = qualifiers.lstrip("/").replace("/", "_") |
| safe_rev = board_rev.replace(".", "_") if board_rev else "" |
| |
| bases = [board_name] |
| if safe_q: |
| parts = qualifiers.lstrip("/").split("/") |
| for i in range(0, len(parts)): |
| q_sub = "_".join(parts[:i + 1]) |
| bases.append("%s_%s" % (board_name, q_sub)) |
| |
| candidates = ["%s.%s" % (b, ext) for b in bases] |
| if safe_rev: |
| suffixes = ["_%s" % safe_rev] |
| if board_rev != safe_rev: |
| suffixes.append("@%s" % board_rev) |
| candidates.extend(["%s%s.%s" % (b, s, ext) for b in bases for s in suffixes]) |
| |
| return candidates |
| |
| def find_nearest_build_file(ctx, start_dir): |
| """Finds the nearest directory containing a BUILD or BUILD.bazel file. |
| |
| Args: |
| ctx: The module or repository context. |
| start_dir: The directory to start the search from. |
| |
| Returns: |
| The path to the nearest directory with a BUILD file, or None if not found. |
| """ |
| current = start_dir |
| |
| # Limit search depth to avoid infinite loops |
| for _ in range(15): |
| if current.get_child("BUILD").exists or current.get_child("BUILD.bazel").exists: |
| return current |
| parent = current.dirname |
| if parent == current: |
| break |
| current = parent |
| return None |
| |
| def find_module_root(mctx, start_dir, limit_dir = None): |
| """Finds the nearest directory containing a MODULE.bazel file. |
| |
| Args: |
| mctx: The module context. |
| start_dir: The directory to start the search from. |
| limit_dir: Optional directory to limit the search to (do not traverse above it). |
| |
| Returns: |
| The path to the nearest directory with a MODULE.bazel file, or None if not found. |
| """ |
| current = start_dir |
| |
| # Limit search depth to avoid infinite loops |
| for _ in range(15): |
| if not current: |
| break |
| if current.get_child("MODULE.bazel").exists: |
| return current |
| if limit_dir and current == limit_dir: |
| break |
| parent = current.dirname |
| if not parent or parent == current: |
| break |
| current = parent |
| return None |
| |
| def get_dir_from_label(mctx, label): |
| """Resolves a label to a directory path. |
| |
| If the label points to a file, it returns the directory containing that file. |
| If the label points to a non-existent path (e.g., a rule target), it returns its parent. |
| |
| Args: |
| mctx: The module or repository context. |
| label: The label to resolve. |
| |
| Returns: |
| A path object pointing to the directory. |
| """ |
| path = mctx.path(label) |
| if not path.exists: |
| return path.dirname |
| |
| # Use execute to check if it's a file, as Starlark's path object doesn't have is_file() |
| if mctx.execute(["test", "-f", str(path)]).return_code == 0: |
| return path.dirname |
| return path |
| |
| def get_package_for_path(mctx, path, dir_labels): |
| """Resolves the Bazel package for a given absolute path. |
| |
| Args: |
| mctx: The module context. |
| path: The absolute path to resolve. |
| dir_labels: A list of directory labels that might contain the path. |
| |
| Returns: |
| The Bazel package string (e.g., '@zephyr//arch/arm'), or None. |
| """ |
| path_str = str(path.realpath) |
| |
| best_pkg = None |
| best_len = -1 |
| best_dir_len = -1 |
| |
| for l in dir_labels: |
| dir_path = get_dir_from_label(mctx, l) |
| dir_path_str = str(dir_path.realpath) |
| |
| if path_str.startswith(dir_path_str): |
| # Find the nearest BUILD file |
| build_dir = find_nearest_build_file(mctx, path) |
| if not build_dir: |
| continue |
| |
| build_dir_str = str(build_dir.realpath) |
| |
| # Resolve repo root if external |
| repo_root = None |
| res_repo = "" |
| if l.workspace_name: |
| repo_root = get_dir_from_label(mctx, Label("@@" + l.workspace_name + "//:BUILD.bazel")) |
| if repo_root: |
| repo_root = repo_root.realpath |
| res_repo = str(l).split("//")[0] |
| else: |
| # Local repo (main workspace) |
| repo_root = mctx.path(Label("//:MODULE.bazel")).dirname.realpath |
| |
| # The package is relative to the module root |
| module_root = find_module_root(mctx, dir_path, limit_dir = repo_root) |
| if not module_root: |
| module_root = repo_root |
| |
| if not module_root: |
| continue |
| |
| label_workspace_root = str(module_root.realpath) |
| |
| if build_dir_str.startswith(label_workspace_root): |
| pkg = build_dir_str[len(label_workspace_root):].strip("/") |
| |
| build_file = "BUILD.bazel" |
| if build_dir.get_child("BUILD").exists: |
| build_file = "BUILD" |
| full_pkg = (res_repo + "//" + pkg) if res_repo else ("@@//" + pkg) |
| full_pkg = full_pkg + ":" + build_file |
| |
| # We want the longest match (most specific package) |
| # AND we prefer more specific dir_labels |
| is_better = False |
| if best_pkg == None: |
| is_better = True |
| elif len(dir_path_str) > best_dir_len: |
| is_better = True |
| elif len(dir_path_str) == best_dir_len: |
| if len(build_dir_str) > best_len: |
| is_better = True |
| elif len(build_dir_str) == best_len and not res_repo and best_pkg.startswith("@"): |
| is_better = True |
| |
| if is_better: |
| best_len = len(build_dir_str) |
| best_dir_len = len(dir_path_str) |
| best_pkg = full_pkg |
| |
| return best_pkg |
| |
| # --- Repository Rules --- |
| |
| def _create_zephyr_patch_file_impl(rctx): |
| """Implementation of create_zephyr_patch_file.""" |
| script_path = Label("//:generate_diff.py") |
| rctx.watch(rctx.path(script_path)) |
| |
| bazel_overlay_path = rctx.path(Label("@zephyr-bazel//:bazel_overlay")) |
| rctx.watch_tree(bazel_overlay_path) |
| third_party_zephyr_path = rctx.path(Label("@zephyr-bazel//:third_party/zephyr")) |
| rctx.watch_tree(third_party_zephyr_path) |
| output_file = rctx.attr.filename |
| output_file_path = rctx.path(output_file) |
| |
| rctx.file(output_file) |
| rctx.file("BUILD") |
| |
| args = [ |
| script_path, |
| "--root-dir", |
| bazel_overlay_path, |
| third_party_zephyr_path, |
| "-o", |
| output_file_path, |
| ] |
| if rctx.attr.debug: |
| print(args) |
| python = get_python(rctx) |
| result = python.execute(args) |
| if result.return_code != 0: |
| fail("Failed to generate zephyr-bazel diff file (%s):\n%s" % (result.return_code, result.stderr)) |
| |
| if rctx.attr.debug: |
| print("Generated %s" % (output_file_path)) |
| return [ |
| DefaultInfo( |
| files = depset([output_file_path, rctx.path("BUILD")]), |
| ), |
| ] |
| |
| create_zephyr_patch_file = repository_rule( |
| implementation = _create_zephyr_patch_file_impl, |
| attrs = { |
| "debug": attr.bool(default = False), |
| "filename": attr.string( |
| default = "patch.diff", |
| mandatory = True, |
| ), |
| } | COMMON_PY_REPO_RULE_ATTRS, |
| local = True, |
| ) |
| |
| def _version_header_impl(ctx): |
| """Implementation of version_header.""" |
| version_file = ctx.attr.version_file |
| version_template = ctx.attr.version_template |
| |
| version_content = ctx.read(version_file) |
| template_content = ctx.read(version_template) |
| |
| # Extract version components using string manipulation |
| version_major = 0 |
| version_minor = 0 |
| patchlevel = 0 |
| version_tweak = 0 |
| for line in version_content.splitlines(): |
| if line.startswith("VERSION_MAJOR"): |
| version_major = int(line.split("=")[1].strip()) |
| elif line.startswith("VERSION_MINOR"): |
| version_minor = int(line.split("=")[1].strip()) |
| elif line.startswith("PATCHLEVEL"): |
| patchlevel = int(line.split("=")[1].strip()) |
| elif line.startswith("VERSION_TWEAK"): |
| version_tweak = int(line.split("=")[1].strip()) |
| |
| version_type = ctx.attr.VERSION_TYPE |
| zephyr_version_code = (version_major << 16) | (version_minor << 8) | patchlevel |
| kernelversion = (zephyr_version_code << 8) | version_tweak |
| kernelversion_hex = "0x%x" % kernelversion |
| kernel_version_number_hex = "0x%x" % zephyr_version_code |
| kernel_version_major = version_major |
| kernel_version_minor = version_minor |
| kernel_patchlevel = patchlevel |
| kernel_version_tweak = version_tweak |
| kernel_version_string = "{}.{}.{}-{}".format(version_major, version_minor, patchlevel, version_tweak) |
| kernel_version_extended_string = kernel_version_string + "+0" |
| kernel_version_tweak_string = "{}.{}.{}-0".format(version_major, version_minor, patchlevel) |
| build_version_name = "BUILD_VERSION" |
| |
| template_dict = { |
| "VERSION_TYPE": version_type, |
| "ZEPHYR_VERSION_CODE": str(zephyr_version_code), |
| "KERNELVERSION": kernelversion_hex, |
| "KERNEL_VERSION_NUMBER": kernel_version_number_hex, |
| "KERNEL_VERSION_MAJOR": str(kernel_version_major), |
| "KERNEL_VERSION_MINOR": str(kernel_version_minor), |
| "KERNEL_PATCHLEVEL": str(kernel_patchlevel), |
| "KERNEL_VERSION_TWEAK": str(kernel_version_tweak), |
| "KERNEL_VERSION_STRING": kernel_version_string, |
| "KERNEL_VERSION_EXTENDED_STRING": kernel_version_extended_string, |
| "KERNEL_VERSION_TWEAK_STRING": kernel_version_tweak_string, |
| "KERNEL_VERSION_CUSTOMIZATION": "", |
| "BUILD_VERSION_NAME": build_version_name, |
| "BUILD_VERSION": "v" + kernel_version_string, |
| } |
| |
| template_content = template_content.replace("#cmakedefine", "#define") |
| |
| for variable, value in template_dict.items(): |
| template_content = template_content.replace("@" + variable + "@", value) |
| |
| ctx.file("zephyr/version.h", content = template_content, executable = False) |
| ctx.file( |
| "BUILD.bazel", |
| content = """load("@rules_cc//cc:defs.bzl", "cc_library") |
| |
| cc_library( |
| name = "version", |
| hdrs = ["zephyr/version.h"], |
| includes = ["."], |
| visibility = ["//visibility:public"], |
| ) |
| """, |
| executable = False, |
| ) |
| |
| return ctx.repo_metadata(reproducible = True) |
| |
| version_header = repository_rule( |
| implementation = _version_header_impl, |
| attrs = { |
| "version_file": attr.label( |
| mandatory = True, |
| allow_single_file = True, |
| ), |
| "version_template": attr.label( |
| mandatory = True, |
| allow_single_file = True, |
| ), |
| "VERSION_TYPE": attr.string(default = "KERNEL"), |
| }, |
| ) |
| |
| # --- Module Extensions --- |
| |
| def _zephyr_patch_file_impl(module_ctx): |
| """Implementation of zephyr_patch_file module extension.""" |
| create_zephyr_patch_file( |
| name = "zephyr-patch", |
| filename = "patch.diff", |
| # This is optional, use it to see what's going on under the hood |
| debug = True, |
| ) |
| |
| remote = "https://pigweed.googlesource.com/third_party/github/zephyrproject-rtos/zephyr" |
| commit = None |
| patches = ["@zephyr-patch//:patch.diff"] |
| |
| for mod in module_ctx.modules: |
| for tag in mod.tags.checkout: |
| if not commit: |
| commit = tag.commit |
| if tag.remote: |
| remote = tag.remote |
| for p in tag.patches: |
| if p not in patches: |
| patches.append(p) |
| |
| if not commit: |
| fail("No commit specified for zephyr_patch_file.checkout") |
| |
| git_repository( |
| name = "zephyr", |
| remote = remote, |
| commit = commit, |
| patches = patches, |
| patch_args = ["-p1"], |
| ) |
| |
| version_header( |
| name = "zephyr_version", |
| version_file = "@zephyr//:VERSION", |
| version_template = "@zephyr//:version.h.in", |
| ) |
| |
| return module_ctx.extension_metadata(reproducible = True) |
| |
| _zephyr_checkout = tag_class( |
| attrs = { |
| "commit": attr.string( |
| mandatory = True, |
| ), |
| "remote": attr.string(), |
| "patches": attr.label_list(), |
| }, |
| ) |
| |
| zephyr_patch_file = module_extension( |
| implementation = _zephyr_patch_file_impl, |
| tag_classes = { |
| "checkout": _zephyr_checkout, |
| }, |
| ) |
| |
| def _zephyr_setup_core_impl(mctx): |
| """Implementation of zephyr_setup_core module extension.""" |
| zephyr_root = mctx.path(Label("@zephyr//:BUILD.bazel")).dirname |
| workspace_root = mctx.path(Label("@@//:MODULE.bazel")).dirname |
| python = get_python( |
| mctx, |
| extra_import_paths = [ |
| str(zephyr_root.get_child("scripts")), |
| ], |
| pip_deps = [ |
| Label("@zephyr_bazel_pip_deps//pyyaml"), |
| Label("@zephyr_bazel_pip_deps//jsonschema"), |
| Label("@zephyr_bazel_pip_deps//attrs"), |
| Label("@zephyr_bazel_pip_deps//jsonschema_specifications"), |
| Label("@zephyr_bazel_pip_deps//referencing"), |
| Label("@zephyr_bazel_pip_deps//rpds_py"), |
| Label("@zephyr_bazel_pip_deps//typing_extensions"), |
| ], |
| ) |
| apps_dir_labels = [] |
| boards_dir_labels = [] |
| modules_labels = [] |
| manual_boards = [] |
| |
| for mod in mctx.modules: |
| for tag in mod.tags.env: |
| if mod.is_root: |
| apps_dir_labels.extend(tag.apps_dirs) |
| boards_dir_labels.extend(tag.boards_dirs) |
| modules_labels.extend(tag.modules) |
| manual_boards.extend(tag.manual_boards) |
| |
| # Discover Modules |
| modules_dirs_map = {} |
| for l in modules_labels: |
| p = mctx.path(l) |
| if p.basename == "module.yml" and p.dirname.basename == "zephyr": |
| mod_root = p.dirname.dirname |
| else: |
| mod_root = p.dirname |
| |
| if l.package: |
| pkg_dir = mod_root.get_child(l.package) |
| else: |
| pkg_dir = mod_root |
| |
| modules_dirs_map[str(l)] = { |
| "root": str(mod_root), |
| "package_dir": str(pkg_dir), |
| } |
| |
| scanner_script = mctx.path(Label("@zephyr-bazel//scripts/build:scan_modules.py")) |
| scanner_res = python.execute([ |
| str(scanner_script), |
| "--modules-dirs-json", |
| json.encode(modules_dirs_map), |
| ]) |
| if scanner_res.return_code != 0: |
| fail("scan_modules.py failed: %s" % scanner_res.stderr) |
| discovered = json.decode(scanner_res.stdout) |
| |
| discovered_modules = {} # name -> root |
| discovered_modules_labels = {} |
| discovered_config_only_modules_labels = {} |
| watched_kconfigs = [] |
| |
| # Watch parent directories non-recursively for discovery containers |
| for l in modules_labels: |
| mctx.watch(get_dir_from_label(mctx, l)) |
| |
| for item in discovered: |
| name = item["name"] |
| abs_path_str = item["abs_path"] |
| dir_label_str = item["modules_dir_label"] |
| relpath = item["relpath"] |
| has_zephyr_module_target_in_build = item.get("has_zephyr_module_target_in_build", False) |
| |
| derived_label = derive_module_label(dir_label_str, relpath, name) |
| discovered_modules[name] = abs_path_str |
| module_info = { |
| "target": derived_label, |
| "original": dir_label_str, |
| "relpath": relpath, |
| } |
| |
| # Determine module linking mode: |
| # Modules with a BUILD.bazel file containing a zephyr_module target |
| # with a matching module name are Auto-Linked. |
| # Otherwise, the module is Scan-Only (Kconfig/DTS only). |
| auto_link = has_zephyr_module_target_in_build |
| |
| if auto_link: |
| discovered_modules_labels[name] = module_info |
| else: |
| discovered_config_only_modules_labels[name] = module_info |
| |
| # Watch metadata |
| mctx.watch(mctx.path(abs_path_str).get_child("zephyr").get_child("module.yml")) |
| k1 = mctx.path(abs_path_str).get_child("Kconfig") |
| mctx.watch(k1) |
| watched_kconfigs.append(str(k1)) |
| k2 = mctx.path(abs_path_str).get_child("zephyr").get_child("Kconfig") |
| mctx.watch(k2) |
| watched_kconfigs.append(str(k2)) |
| |
| # Watch for the creation of new module.yml files in subdirectories, |
| # so Bazel knows to re-run the module extension if a new module is added. |
| for l in modules_labels: |
| container = get_dir_from_label(mctx, l) |
| if container.exists: |
| for subdir in container.readdir(): |
| mctx.watch(subdir.get_child("zephyr").get_child("module.yml")) |
| |
| # Discover Boards |
| list_boards_py = zephyr_root.get_child("scripts").get_child("list_boards.py") |
| |
| board_roots = [str(zephyr_root)] |
| for l in boards_dir_labels: |
| b_dir = get_dir_from_label(mctx, l) |
| mctx.watch(b_dir) |
| mctx.watch(b_dir.get_child("boards")) |
| mctx.watch(b_dir.get_child("Kconfig")) |
| board_roots.append(str(b_dir)) |
| |
| cmd = [str(list_boards_py), "--arch-root", str(zephyr_root)] |
| cmd.extend(["--soc-root", str(zephyr_root)]) |
| for m_root in discovered_modules.values(): |
| cmd.extend(["--soc-root", m_root]) |
| cmd.extend(["--arch-root", m_root]) |
| |
| for r in board_roots: |
| cmd.extend(["--board-root", r]) |
| cmd.extend(["--cmakeformat", "{NAME}:{DIR}:{QUALIFIERS}"]) |
| |
| res = python.execute(cmd) |
| if res.return_code != 0: |
| fail("list_boards.py failed: %s" % res.stderr) |
| |
| board_index = {} |
| package_to_boards = {} |
| path_str_dict = {} |
| |
| for line in res.stdout.splitlines(): |
| if not line: |
| continue |
| parts = line.split(":") |
| if len(parts) < 2: |
| continue |
| |
| name = parts[0] |
| if name.startswith("NAME;"): |
| name = name[5:] |
| |
| path_str = parts[1] |
| if path_str.startswith("DIR;"): |
| path_str = path_str[4:] |
| |
| qualifiers = [] |
| if len(parts) >= 3: |
| q_part = parts[2] |
| if q_part.startswith("QUALIFIERS;"): |
| q_part = q_part[11:] |
| qualifiers = [q for q in q_part.split(";") if q] |
| |
| path = mctx.path(path_str) |
| pkg_label = get_package_for_path(mctx, path, boards_dir_labels) |
| pkg_path = "" |
| if not pkg_label: |
| zephyr_root_str = str(mctx.path(zephyr_root).realpath) |
| realpath_str = str(path.realpath) |
| if realpath_str.startswith(zephyr_root_str): |
| zephyr_canonical = str(Label("@zephyr//:BUILD.bazel")).split("//")[0] |
| build_dir = find_nearest_build_file(mctx, path) |
| if build_dir: |
| build_dir_str = str(build_dir.realpath) |
| rel_build_pkg = build_dir_str[len(zephyr_root_str):].strip("/") |
| build_file = "BUILD.bazel" |
| if build_dir.get_child("BUILD").exists: |
| build_file = "BUILD" |
| pkg_path = zephyr_canonical + "//" + rel_build_pkg |
| pkg_label = pkg_path + ":" + build_file |
| else: |
| rel_path = path_str[len(zephyr_root_str):].strip("/") |
| build_file = "BUILD.bazel" |
| if path.get_child("BUILD").exists: |
| build_file = "BUILD" |
| pkg_path = zephyr_canonical + "//" + rel_path |
| pkg_label = pkg_path + ":" + build_file |
| else: |
| continue |
| else: |
| pkg_path = pkg_label.split(":")[0] |
| |
| board_index[name] = pkg_label |
| |
| if pkg_path not in package_to_boards: |
| package_to_boards[pkg_path] = [] |
| if name not in package_to_boards[pkg_path]: |
| package_to_boards[pkg_path].append(name) |
| |
| # Also register qualified names (board/soc) |
| for q in qualifiers: |
| full_board_id = name + "/" + q |
| board_index[full_board_id] = pkg_label |
| |
| # Add to package_to_boards so it's discoverable |
| if full_board_id not in package_to_boards[pkg_path]: |
| package_to_boards[pkg_path].append(full_board_id) |
| |
| path_str_dict[path_str] = { |
| "name": name, |
| "qualifiers": qualifiers, |
| "pkg_label": pkg_label, |
| "pkg_path": pkg_path, |
| } |
| |
| boards_json_file = "discovered_boards.json" |
| board_revisions_file = "board_revisions.json" |
| mctx.file(boards_json_file, json.encode(path_str_dict)) |
| |
| board_meta_script = mctx.path(Label("@zephyr-bazel//scripts/build:parse_board_metadata.py")) |
| board_meta_res = python.execute([ |
| str(board_meta_script), |
| "--boards-json", |
| boards_json_file, |
| "--output-json", |
| board_revisions_file, |
| ]) |
| if board_meta_res.return_code != 0: |
| fail("parse_board_metadata.py failed (%d):\n%s" % (board_meta_res.return_code, board_meta_res.stderr)) |
| board_revisions_map = json.decode(mctx.read(board_revisions_file)) |
| |
| for path_str, bdata in path_str_dict.items(): |
| path = mctx.path(path_str) |
| for b_yml_name in ["board.yml", "board.yaml"]: |
| b_yml_file = path.get_child(b_yml_name) |
| if b_yml_file.exists: |
| mctx.watch(b_yml_file) |
| rev_dir = path.get_child("revisions") |
| if rev_dir.exists: |
| mctx.watch(rev_dir) |
| |
| name = bdata["name"] |
| qualifiers = bdata["qualifiers"] |
| pkg_label = bdata["pkg_label"] |
| pkg_path = bdata["pkg_path"] |
| |
| board_revisions = board_revisions_map.get(path_str, []) |
| |
| all_base_ids = [name] |
| for q in qualifiers: |
| all_base_ids.append(name + "/" + q) |
| |
| for rev in board_revisions: |
| for base_id in all_base_ids: |
| rev_board_id = base_id + "@" + rev |
| board_index[rev_board_id] = pkg_label |
| if rev_board_id not in package_to_boards[pkg_path]: |
| package_to_boards[pkg_path].append(rev_board_id) |
| |
| for mb in manual_boards: |
| base, _, _ = mb.partition("@") |
| base, _, _ = base.partition("/") |
| if base and base in board_index: |
| pkg_label = board_index[base] |
| pkg_path = pkg_label.split(":", 1)[0] |
| board_index[mb] = pkg_label |
| if pkg_path in package_to_boards and mb not in package_to_boards[pkg_path]: |
| package_to_boards[pkg_path].append(mb) |
| # Discover Apps |
| discovered_apps = {} |
| discovered_apps_labels = {} |
| collision_registry = {} # hash -> label |
| |
| for l in apps_dir_labels: |
| dir_path = get_dir_from_label(mctx, l) |
| dir_path.readdir(watch = "yes") |
| res = mctx.execute([ |
| "find", |
| str(dir_path), |
| "-name", |
| "out", |
| "-prune", |
| "-o", |
| "-name", |
| "bazel-*", |
| "-prune", |
| "-o", |
| "-name", |
| ".*", |
| "-prune", |
| "-o", |
| "-name", |
| "prj.conf", |
| "-print", |
| ]) |
| if res.return_code == 0: |
| for line in res.stdout.splitlines(): |
| app_path = mctx.path(line).dirname |
| app_pkg = get_package_for_path(mctx, app_path, apps_dir_labels) |
| if app_pkg: |
| # Robust normalization: strip repo and redundant target name |
| norm_app = str(app_pkg).split(":")[0] |
| if "//" in norm_app: |
| norm_app = norm_app.split("//")[-1] |
| norm_app = norm_app.strip("/").lstrip(":") |
| |
| # Actually let's use the same logic as naming.bzl for the check |
| h = hash(norm_app) |
| app_hash = ("%x" % (h & 0xFFFFFFFF)) |
| app_hash = ("00000000" + app_hash)[-8:] |
| |
| collision_registry[app_hash] = norm_app |
| discovered_apps[norm_app] = str(app_path) |
| discovered_apps_labels[norm_app] = app_pkg |
| mctx.watch(app_path.get_child("prj.conf")) |
| mctx.watch(app_path.get_child("app.overlay")) |
| mctx.watch(app_path.get_child("Kconfig")) |
| mctx.watch(app_path.get_child("sysbuild.yml")) |
| mctx.watch(app_path.get_child("sysbuild.conf")) |
| sysbuild_dir = app_path.get_child("sysbuild") |
| if sysbuild_dir.exists: |
| mctx.watch(sysbuild_dir) |
| else: |
| mctx.watch(app_path) |
| |
| apps_json_file = "discovered_apps.json" |
| test_metadata_file = "test_metadata.json" |
| mctx.file(apps_json_file, json.encode(discovered_apps)) |
| |
| # Parse metadata YAML |
| script_path = mctx.path(Label("@zephyr-bazel//scripts/build:parse_test_metadata.py")) |
| metadata_res = python.execute([ |
| str(script_path), |
| "--apps-json", |
| apps_json_file, |
| "--zephyr-root", |
| str(zephyr_root), |
| "--output-json", |
| test_metadata_file, |
| ]) |
| if metadata_res.return_code != 0: |
| fail("parse_test_metadata.py failed (%d):\n%s" % (metadata_res.return_code, metadata_res.stderr)) |
| test_metadata_content = mctx.read(test_metadata_file) |
| yaml_metadata = json.decode(test_metadata_content) |
| |
| sysbuild_metadata_file = "sysbuild_metadata.json" |
| sysbuild_script_path = mctx.path(Label("@zephyr-bazel//scripts/build:parse_sysbuild_metadata.py")) |
| sysbuild_res = python.execute([ |
| str(sysbuild_script_path), |
| "--apps-json", |
| apps_json_file, |
| "--output-json", |
| sysbuild_metadata_file, |
| ]) |
| if sysbuild_res.return_code != 0: |
| fail("parse_sysbuild_metadata.py failed (%d):\n%s" % (sysbuild_res.return_code, sysbuild_res.stderr)) |
| sysbuild_metadata_content = mctx.read(sysbuild_metadata_file) |
| sysbuild_metadata = json.decode(sysbuild_metadata_content) |
| |
| # Post-process to make conf_fragments relative to workspace root |
| if "custom_repos" in sysbuild_metadata: |
| for repo in sysbuild_metadata["custom_repos"]: |
| parent_app = repo["parent_app"] |
| resolved_conf_fragments = [] |
| for fragment in repo["conf_fragments"]: |
| if not fragment.startswith("/"): |
| resolved_conf_fragments.append(parent_app + "/" + fragment) |
| else: |
| resolved_conf_fragments.append(fragment) |
| repo["conf_fragments"] = resolved_conf_fragments |
| |
| surviving_combinations = {} |
| for app_pkg, app_path in discovered_apps.items(): |
| surviving_combinations[app_pkg] = [] |
| yaml_res = yaml_metadata.get(app_pkg, None) |
| is_oot_app = not app_path.startswith(str(zephyr_root)) |
| |
| for board_id, board_path in board_index.items(): |
| is_oot_board = not board_path.startswith(str(zephyr_root)) |
| |
| # OOT Exemption |
| if is_oot_app and is_oot_board: |
| surviving_combinations[app_pkg].append(board_id) |
| continue |
| |
| # Manual fallback |
| found_manual = False |
| has_rev = "@" in board_id |
| for mb in manual_boards: |
| mb_has_rev = "@" in mb |
| if has_rev != mb_has_rev: |
| continue |
| if has_rev: |
| if board_id == mb: |
| found_manual = True |
| break |
| else: |
| if board_id == mb or board_id.split("/")[0] == mb: |
| found_manual = True |
| break |
| |
| if found_manual: |
| surviving_combinations[app_pkg].append(board_id) |
| continue |
| |
| # YAML |
| if yaml_res != None and len(yaml_res) > 0: |
| if board_id in yaml_res: |
| surviving_combinations[app_pkg].append(board_id) |
| continue |
| |
| # Filesystem |
| boards_dir = mctx.path(app_path).get_child("boards") |
| if boards_dir.exists: |
| # Watch the boards dir so we re-filter if a new file is added. |
| mctx.watch(boards_dir) |
| board_name, qualifiers, board_rev = _parse_board_id(board_id) |
| override_candidates = ( |
| _get_override_candidates(board_name, qualifiers, board_rev, "conf") + |
| _get_override_candidates(board_name, qualifiers, board_rev, "overlay") |
| ) |
| |
| found = False |
| for c in override_candidates: |
| c_file = boards_dir.get_child(c) |
| if c_file.exists: |
| mctx.watch(c_file) |
| found = True |
| break |
| if found: |
| surviving_combinations[app_pkg].append(board_id) |
| continue |
| else: |
| # If the boards dir does not exist we must watch the app dir to catch if a boards dir is added. |
| mctx.watch(app_path) |
| |
| # Generate Repositories |
| state_data = { |
| "zephyr_root": "@zephyr//:BUILD.bazel", |
| "board_index": board_index, |
| "apps": discovered_apps_labels, |
| "modules": discovered_modules_labels, |
| "config_only_modules": discovered_config_only_modules_labels, |
| "oot_dts_roots": [str(l) for l in boards_dir_labels], |
| "package_to_boards": package_to_boards, |
| "manual_boards": manual_boards, |
| "surviving_combinations": surviving_combinations, |
| "sysbuild_metadata": sysbuild_metadata, |
| "translated_conf": sysbuild_metadata.get("translated_confs", {}), |
| } |
| zephyr_state_repo( |
| name = "zephyr_state", |
| content = json.encode(state_data), |
| ) |
| |
| # Resolve canonical name prefix for this extension's repos |
| # The canonical name of this module is zephyr-bazel+ |
| # So the extension prefix for apps is @@zephyr-bazel++zephyr_setup_apps+ |
| repo_prefix = "@@zephyr-bazel++zephyr_setup_apps+" |
| |
| board_id_to_safe = {b: sanitize_board_id(b) for b in board_index.keys()} |
| repo_name_to_canonical = {} |
| for app_pkg, boards in surviving_combinations.items(): |
| app_hash = get_app_hash(app_pkg) |
| for board_id in boards: |
| repo_name = get_zc_repo_name_from_precomputed(app_hash, board_id_to_safe[board_id]) |
| repo_name_to_canonical[repo_name] = repo_prefix + repo_name |
| |
| custom_repos = sysbuild_metadata.get("custom_repos", []) |
| for repo in custom_repos: |
| repo_name_to_canonical[repo["name"]] = repo_prefix + repo["name"] |
| |
| index_content = "PACKAGE_TO_BOARDS = %r\n" % package_to_boards |
| index_content += "REPO_NAME_TO_CANONICAL = %r\n" % repo_name_to_canonical |
| index_content += "SYSBUILD_METADATA = %r\n" % sysbuild_metadata |
| sysbuild_custom_repos = {} |
| for repo in custom_repos: |
| key = "%s:%s:%s" % (repo["parent_app"], repo["helper_name"], repo["board_id"]) |
| sysbuild_custom_repos[key] = repo["name"] |
| for fragment in repo["conf_fragments"]: |
| if not fragment.startswith("/"): |
| fragment_path = mctx.path(str(workspace_root) + "/" + fragment) |
| else: |
| fragment_path = mctx.path(fragment) |
| mctx.watch(fragment_path) |
| |
| index_content += "SYSBUILD_CUSTOM_REPOS = %r\n" % sysbuild_custom_repos |
| |
| zephyr_index_repo( |
| name = "zephyr_index", |
| content = index_content, |
| ) |
| |
| zephyr_kconfig_schema( |
| name = "zephyr_kconfig", |
| state_file = "@zephyr_state//:state.json", |
| kconfigs = watched_kconfigs, |
| ) |
| |
| return mctx.extension_metadata(reproducible = True) |
| |
| def _get_parent_platform(pkg, board_id): |
| clean_board_id, _, _ = board_id.partition("@") |
| board_family, slash, variant = clean_board_id.partition("/") |
| target_name = variant.replace("/", "_") if slash else clean_board_id |
| return pkg + ":" + target_name |
| |
| def _zephyr_setup_apps_impl(mctx): |
| """Implementation of zephyr_setup_apps module extension.""" |
| state_file = mctx.path(Label("@zephyr_state//:state.json")) |
| if not state_file.exists: |
| return mctx.extension_metadata(reproducible = True) |
| state = json.decode(mctx.read(state_file)) |
| |
| discovered_apps = state["apps"] |
| board_index = state["board_index"] |
| package_to_boards = state["package_to_boards"] |
| |
| # Reverse mapping for internal use to find the platform for a board |
| board_to_package = {} |
| for pkg, boards in package_to_boards.items(): |
| for b in boards: |
| if b not in board_to_package: |
| board_to_package[b] = pkg |
| |
| # Resolve canonical name prefix for this extension's repos |
| repo_prefix = "@@zephyr-bazel++zephyr_setup_apps+" |
| |
| # Declare ALL combinations |
| all_repo_names = [] |
| repo_name_to_canonical = {} |
| board_id_to_safe = {b: sanitize_board_id(b) for b in board_index.keys()} |
| surviving_combinations = state["surviving_combinations"] |
| for app_pkg, boards in surviving_combinations.items(): |
| app_hash = get_app_hash(app_pkg) |
| for board_id in boards: |
| repo_name = get_zc_repo_name_from_precomputed(app_hash, board_id_to_safe[board_id]) |
| pkg = board_to_package.get(board_id) |
| if not pkg: |
| continue |
| |
| parent_platform = _get_parent_platform(pkg, board_id) |
| |
| app_fragments = state.get("translated_conf", {}).get(app_pkg, {}) |
| main_fragment = app_fragments.get("main", "") |
| |
| gen_zephyr_config( |
| name = repo_name, |
| app_label = app_pkg, |
| board_id = board_id, |
| parent_platform = parent_platform, |
| state_file = "@zephyr_state//:state.json", |
| extra_kconfig = main_fragment, |
| ) |
| all_repo_names.append(repo_name) |
| repo_name_to_canonical[repo_name] = repo_prefix + repo_name |
| |
| custom_repos = state.get("sysbuild_metadata", {}).get("custom_repos", []) |
| for repo in custom_repos: |
| board_id = repo["board_id"] |
| pkg = board_to_package.get(board_id) |
| if not pkg: |
| continue |
| |
| parent_platform = _get_parent_platform(pkg, board_id) |
| |
| gen_zephyr_config( |
| name = repo["name"], |
| app_label = repo["app_label"], |
| board_id = board_id, |
| parent_platform = parent_platform, |
| state_file = "@zephyr_state//:state.json", |
| conf_fragments = repo["conf_fragments"], |
| extra_kconfig = repo.get("extra_kconfig", ""), |
| ) |
| all_repo_names.append(repo["name"]) |
| repo_name_to_canonical[repo["name"]] = repo_prefix + repo["name"] |
| |
| return mctx.extension_metadata(reproducible = True) |
| |
| _env = tag_class( |
| attrs = { |
| "apps_dirs": attr.label_list(), |
| "boards_dirs": attr.label_list(), |
| "modules": attr.label_list(), |
| "manual_boards": attr.string_list(), |
| }, |
| ) |
| |
| zephyr_setup = module_extension( |
| implementation = _zephyr_setup_core_impl, |
| tag_classes = { |
| "env": _env, |
| }, |
| ) |
| |
| zephyr_setup_apps = module_extension( |
| implementation = _zephyr_setup_apps_impl, |
| ) |
| |
| ############################################################################### |
| |
| def _print_file_impl(ctx): |
| """Implementation of print_file.""" |
| |
| # Get the input file |
| input_file = ctx.file.file |
| |
| # Create an empty output file |
| output_file = ctx.actions.declare_file(ctx.label.name + ".out") |
| |
| # Create a shell action to print the file |
| ctx.actions.run_shell( |
| outputs = [output_file], |
| inputs = [input_file], |
| command = "cat {} > {}".format(input_file.path, output_file.path), |
| ) |
| |
| # Return DefaultInfo to signal dependencies on the input file |
| return DefaultInfo(files = depset([output_file])) |
| |
| # Define the rule |
| print_file = rule( |
| implementation = _print_file_impl, |
| attrs = { |
| "file": attr.label(allow_single_file = True, mandatory = True), |
| }, |
| ) |