|  | #!/usr/bin/env python3 | 
|  | # | 
|  | # Copyright 2021 The Bazel Authors. All rights reserved. | 
|  | # | 
|  | # 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 | 
|  | # | 
|  | #    http://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. | 
|  |  | 
|  | import argparse | 
|  | import importlib.util | 
|  | import json | 
|  | import locale | 
|  | import pathlib | 
|  | import re | 
|  | import subprocess | 
|  | import sys | 
|  | import tempfile | 
|  | import os | 
|  |  | 
|  | from urllib.parse import urlparse | 
|  | from registry import RegistryClient | 
|  |  | 
|  | # The registry client points to the bazel central registry repo | 
|  | REGISTRY_CLIENT = RegistryClient(pathlib.Path(__file__).resolve().parent.joinpath("../")) | 
|  |  | 
|  | USE_REPO_RULE_IDENTIFIER = "# -- use_repo_rule statements -- #" | 
|  | LOAD_IDENTIFIER = "# -- load statements -- #" | 
|  | REPO_IDENTIFIER = "# -- repo definitions -- #" | 
|  | BAZEL_DEP_IDENTIFIER = "# -- bazel_dep definitions -- #" | 
|  |  | 
|  |  | 
|  | def abort_migration(): | 
|  | info("Abort migration...") | 
|  | exit(2) | 
|  |  | 
|  |  | 
|  | def assertExitCode(exit_code, expected_exit_code, error_message, stderr): | 
|  | if exit_code != expected_exit_code: | 
|  | error(f"Command exited with {exit_code}, expected {expected_exit_code}:") | 
|  | eprint(error_message) | 
|  | eprint(stderr) | 
|  | abort_migration() | 
|  |  | 
|  |  | 
|  | def eprint(*args, **kwargs): | 
|  | """ | 
|  | Print to stderr and flush (just in case). | 
|  | """ | 
|  | print(*args, flush=True, file=sys.stderr, **kwargs) | 
|  |  | 
|  |  | 
|  | BOLD = "\033[1m" | 
|  | GREEN = "\x1b[32m\033[1m" | 
|  | YELLOW = "\x1b[33m\033[1m" | 
|  | RED = "\x1b[31m\033[1m" | 
|  | RESET = "\033[0m" | 
|  |  | 
|  |  | 
|  | def info(msg): | 
|  | eprint(msg) | 
|  |  | 
|  |  | 
|  | def resolved(msg): | 
|  | eprint(f"{GREEN}RESOLVED: {RESET}{msg}") | 
|  |  | 
|  |  | 
|  | def important(msg): | 
|  | eprint(f"{YELLOW}IMPORTANT: {RESET}{msg}") | 
|  |  | 
|  |  | 
|  | def action(msg): | 
|  | eprint(f"{RED}ACTION NEEDED: {RESET}{msg}") | 
|  |  | 
|  |  | 
|  | def warning(msg): | 
|  | eprint(f"{YELLOW}WARNING: {RESET}{msg}") | 
|  |  | 
|  |  | 
|  | def error(msg): | 
|  | eprint(f"{RED}ERROR: {RESET}{msg}") | 
|  |  | 
|  |  | 
|  | def ask_input(msg): | 
|  | return input(f"{YELLOW}ACTION: {RESET}{msg}") | 
|  |  | 
|  |  | 
|  | def yes_or_no(question, default): | 
|  | if not yes_or_no.enable: | 
|  | return default | 
|  |  | 
|  | if default: | 
|  | question += " [Y/n]: " | 
|  | else: | 
|  | question += " [y/N]: " | 
|  |  | 
|  | var = None | 
|  | while var is None: | 
|  | user_input = ask_input(question).strip().lower() | 
|  | if user_input == "y": | 
|  | var = True | 
|  | elif user_input == "n": | 
|  | var = False | 
|  | elif not user_input: | 
|  | var = default | 
|  | else: | 
|  | eprint(f"Invalid selection: {user_input}") | 
|  | return var | 
|  |  | 
|  |  | 
|  | def scratch_file(file_path, lines=None, mode="w"): | 
|  | """Write lines to a file.""" | 
|  | abspath = pathlib.Path(file_path) | 
|  | with open(abspath, mode) as f: | 
|  | if lines: | 
|  | for l in lines: | 
|  | f.write(l) | 
|  | f.write("\n") | 
|  | return abspath | 
|  |  | 
|  |  | 
|  | def append_to_file(filename, content): | 
|  | """ | 
|  | Creates a file with the given filename and content. | 
|  |  | 
|  | Args: | 
|  | filename (str): The name of the file to create. | 
|  | content (str): The content to write to the file. | 
|  | """ | 
|  | try: | 
|  | with open(filename, "a") as f: | 
|  | f.write(content) | 
|  | except OSError as e: | 
|  | error(f"Error creating file '{filename}': {e}") | 
|  |  | 
|  |  | 
|  | def append_migration_info(content): | 
|  | """Adds content to the "migration_info" file in order to help users with details about the migration.""" | 
|  | append_to_file("migration_info.md", content + "\n") | 
|  |  | 
|  |  | 
|  | def execute_command(args, to_print=False, cwd=None, env=None, shell=False, executable=None): | 
|  | if to_print: | 
|  | info("Executing command: " + " ".join(args)) | 
|  | with tempfile.TemporaryFile() as stdout: | 
|  | with tempfile.TemporaryFile() as stderr: | 
|  | proc = subprocess.Popen( | 
|  | args, | 
|  | executable=executable, | 
|  | stdout=stdout, | 
|  | stderr=stderr, | 
|  | cwd=cwd, | 
|  | env=env, | 
|  | shell=shell, | 
|  | ) | 
|  | exit_code = proc.wait() | 
|  |  | 
|  | stdout.seek(0) | 
|  | stdout_result = stdout.read().decode(locale.getpreferredencoding()) | 
|  | stderr.seek(0) | 
|  | stderr_result = stderr.read().decode(locale.getpreferredencoding()) | 
|  | return exit_code, stdout_result, stderr_result | 
|  |  | 
|  |  | 
|  | def print_repo_definition(repo_def, dep): | 
|  | repo_def_str = "\n".join(repo_def) | 
|  | append_migration_info(f""" | 
|  | <details> | 
|  | <summary>Click here to see where and how the repo was declared in the WORKSPACE file</summary> | 
|  |  | 
|  | #### Location | 
|  | ```python | 
|  | {dep["definition_information"]} | 
|  | ``` | 
|  |  | 
|  | #### Definition | 
|  | ```python | 
|  | {repo_def_str} | 
|  | ``` | 
|  | **Tip**: URLs usually show which version was used. | 
|  | </details> | 
|  | """) | 
|  | append_migration_info("___") | 
|  |  | 
|  |  | 
|  | def repo_definition(dep): | 
|  | """Print the repository info to migration_info and return the repository definition.""" | 
|  | # Parse the repository rule class (rule name, and the label for the bzl file where the rule is defined.) | 
|  | rule_class = dep["original_rule_class"] | 
|  | is_macro = False | 
|  | if rule_class.find("%") != -1: | 
|  | # Starlark rule | 
|  | file_label, rule_name = rule_class.split("%") | 
|  | # If the original macro is not publicly visible, we trace back to fine a visible one. | 
|  | if rule_name.startswith("_"): | 
|  | is_macro = True | 
|  | def_info = dep["definition_information"].split("\n") | 
|  | def_info.reverse() | 
|  | for line in def_info: | 
|  | s = re.match(r"^  (.+):[0-9]+:[0-9]+: in ([^\_<].+)$", line) | 
|  | if s: | 
|  | new_file_name, new_rule_name = s.groups() | 
|  | if new_file_name.endswith(file_label.split("//")[1].replace(":", "/")): | 
|  | rule_name = new_rule_name | 
|  | else: | 
|  | warning( | 
|  | f"A visible macro for {rule_name} is defined in a different bzl file `{new_file_name}` " | 
|  | f"other than `{file_label}`, " | 
|  | f"you have to find out the correct label for `{new_file_name}` manually." | 
|  | ) | 
|  | break | 
|  | else: | 
|  | # Native rule | 
|  | file_label = None | 
|  | rule_name = rule_class | 
|  |  | 
|  | # Generate the repository definition lines. | 
|  | repo_def = [] | 
|  | if file_label: | 
|  | repo_def.append(f'load("{file_label}", "{rule_name}")') | 
|  | repo_def.append(f"{rule_name}(") | 
|  | for key, value in dep["original_attributes"].items(): | 
|  | if not key.startswith("generator_"): | 
|  | value_str = json.dumps(value, indent=4) | 
|  | # Fix indentation | 
|  | if value_str.endswith("}") or value_str.endswith("]"): | 
|  | value_str = value_str[:-1] + "  " + value_str[-1] | 
|  | # Fix boolean format | 
|  | if value_str == "false" or value_str == "true": | 
|  | value_str = value_str[0].upper() + value_str[1:] | 
|  | repo_def.append(f"  {key} = {value_str},") | 
|  | repo_def.append(")") | 
|  |  | 
|  | if file_label and file_label.startswith("@@"): | 
|  | file_label = file_label[1:] | 
|  |  | 
|  | return repo_def, file_label, rule_name, is_macro | 
|  |  | 
|  |  | 
|  | def detect_unavailable_repo_error(stderr): | 
|  | PATTERNS = [ | 
|  | re.compile(r"unknown repo '([A-Za-z0-9_-]+)' requested from"), | 
|  | re.compile(r"The repository '@([A-Za-z0-9_-]+)' could not be resolved"), | 
|  | re.compile(r"No repository visible as '@([A-Za-z0-9_-]+)' from main repository"), | 
|  | re.compile(r"This could either mean you have to add the '@([A-Za-z0-9_-]+)' repository"), | 
|  | re.compile(r"no repo visible as '@([A-Za-z0-9_-]+)' here"), | 
|  | ] | 
|  |  | 
|  | for line in stderr.split("\n"): | 
|  | for p in PATTERNS: | 
|  | m = p.search(line) | 
|  | if m: | 
|  | return m.groups()[0] | 
|  |  | 
|  | return None | 
|  |  | 
|  |  | 
|  | def write_at_given_place(filename, new_content, identifier): | 
|  | """Write content to a file at a position marked by the identifier.""" | 
|  | file_content = "" | 
|  | with open(filename, "r") as f: | 
|  | file_content = f.read() | 
|  | file_content = file_content.replace( | 
|  | identifier, | 
|  | new_content + "\n" + identifier, | 
|  | 1, | 
|  | ) | 
|  | with open(filename, "w") as f: | 
|  | f.write(file_content) | 
|  |  | 
|  |  | 
|  | def add_repo_with_use_repo_rule(repo, repo_def, file_label, rule_name): | 
|  | """Introduce a repository with use_repo_rule in the MODULE.bazel file.""" | 
|  | use_repo_rule = f'{rule_name} = use_repo_rule("{file_label}", "{rule_name}")' | 
|  |  | 
|  | # Check if the use_repo_rule is already in the MODULE.bazel file | 
|  | module_bazel_content = open("MODULE.bazel", "r").read() | 
|  | if use_repo_rule not in module_bazel_content: | 
|  | write_at_given_place("MODULE.bazel", use_repo_rule, USE_REPO_RULE_IDENTIFIER) | 
|  |  | 
|  | # Add the repo definition to the MODULE.bazel file | 
|  | write_at_given_place( | 
|  | "MODULE.bazel", | 
|  | "\n".join([""] + repo_def[1:]), | 
|  | REPO_IDENTIFIER, | 
|  | ) | 
|  |  | 
|  |  | 
|  | def add_repo_to_module_extension(repo, repo_def, file_label, rule_name): | 
|  | """Introduce a repository via a module extension.""" | 
|  | # If the repo was not defined in @bazel_tools, | 
|  | # we need to create a separate module extension for it to avoid cycle. | 
|  | if rule_name.startswith("_"): | 
|  | rule_name = rule_name[1:] | 
|  | need_separate_module_extension = not file_label.startswith("@bazel_tools") | 
|  | ext_name = f"extension_for_{rule_name}".replace("-", "_") if need_separate_module_extension else "non_module_deps" | 
|  | ext_bzl_name = ext_name + ".bzl" | 
|  |  | 
|  | # Generate the initial bzl file for the module extension | 
|  | if not pathlib.Path(ext_bzl_name).is_file(): | 
|  | scratch_file( | 
|  | ext_bzl_name, | 
|  | [ | 
|  | LOAD_IDENTIFIER, | 
|  | "", | 
|  | f"def _{ext_name}_impl(ctx):", | 
|  | REPO_IDENTIFIER, | 
|  | "", | 
|  | f"{ext_name} = module_extension(implementation = _{ext_name}_impl)", | 
|  | ], | 
|  | ) | 
|  |  | 
|  | # Add repo definition to the module extension's bzl file | 
|  | imported_rule_statement = f'"{rule_name}"' | 
|  | load_statement = f'load("{file_label}", {imported_rule_statement})' | 
|  | bzl_content = open(ext_bzl_name, "r").read() | 
|  | if imported_rule_statement not in bzl_content: | 
|  | write_at_given_place(ext_bzl_name, load_statement, LOAD_IDENTIFIER) | 
|  | write_at_given_place( | 
|  | ext_bzl_name, | 
|  | "\n".join(["  " + line.replace("\n", "\n  ") for line in repo_def[1:]]), | 
|  | REPO_IDENTIFIER, | 
|  | ) | 
|  |  | 
|  | # Add use_repo statement in the MODULE.bazel file | 
|  | use_ext = f'{ext_name} = use_extension("//:{ext_name}.bzl", "{ext_name}")' | 
|  | module_bazel_content = open("MODULE.bazel", "r").read() | 
|  | ext_identifier = f"# End of extension `{ext_name}`" | 
|  | if use_ext not in module_bazel_content: | 
|  | scratch_file("MODULE.bazel", ["", use_ext, ext_identifier], mode="a") | 
|  | write_at_given_place("MODULE.bazel", f'use_repo({ext_name}, "{repo}")', ext_identifier) | 
|  |  | 
|  |  | 
|  | def url_match_source_repo(source_url, module_name): | 
|  | source_repositories = REGISTRY_CLIENT.get_metadata(module_name).get("repository", []) | 
|  | matched = False | 
|  | parts = urlparse(source_url) | 
|  | for source_repository in source_repositories: | 
|  | if matched: | 
|  | break | 
|  | repo_type, repo_path = source_repository.split(":") | 
|  | # Include repos which were moved to bazel-contrib: | 
|  | # https://github.com/orgs/bazelbuild/discussions/2#discussioncomment-10671359. | 
|  | repo_path = repo_path.replace("bazel-contrib/", "bazelbuild/") | 
|  | if repo_type == "github": | 
|  | matched = ( | 
|  | parts.scheme == "https" | 
|  | and parts.netloc == "github.com" | 
|  | and ( | 
|  | os.path.abspath(parts.path).startswith(f"/{repo_path}/") | 
|  | or os.path.abspath(parts.path).startswith(f"/{repo_path}.git") | 
|  | ) | 
|  | ) | 
|  | elif repo_type == "https": | 
|  | repo = urlparse(source_repository) | 
|  | matched = ( | 
|  | parts.scheme == repo.scheme | 
|  | and parts.netloc == repo.netloc | 
|  | and os.path.abspath(parts.path).startswith(f"{repo.path}/") | 
|  | ) | 
|  | return matched | 
|  |  | 
|  |  | 
|  | def exists_in_file(filename, content): | 
|  | with open(filename, "r") as f: | 
|  | return content in f.read() | 
|  |  | 
|  |  | 
|  | def add_go_extension(repo, origin_attrs, resolved_deps, workspace_name): | 
|  | # Introduce `bazel_gazelle` only once. | 
|  | if not exists_in_file("MODULE.bazel", 'bazel_dep(name = "gazelle'): | 
|  | address_unavailable_repo("bazel_gazelle", resolved_deps, workspace_name) | 
|  |  | 
|  | # Introduce `io_bazel_rules_go` only once. | 
|  | if not exists_in_file("MODULE.bazel", 'bazel_dep(name = "rules_go'): | 
|  | address_unavailable_repo("io_bazel_rules_go", resolved_deps, workspace_name) | 
|  |  | 
|  | # Add go_deps | 
|  | if not exists_in_file("MODULE.bazel", 'use_extension("@bazel_gazelle//:extensions.bzl", "go_deps'): | 
|  | go_deps = """ | 
|  | go_deps = use_extension("@bazel_gazelle//:extensions.bzl", "go_deps") | 
|  | # -- End of go extension -- # | 
|  | """ | 
|  | write_at_given_place("MODULE.bazel", go_deps, REPO_IDENTIFIER) | 
|  |  | 
|  | # Add go_sdk | 
|  | if not exists_in_file("MODULE.bazel", '@io_bazel_rules_go//go:extensions.bzl", "go_sdk'): | 
|  | go_sdk = """go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk") | 
|  | """ | 
|  | write_at_given_place("MODULE.bazel", go_sdk, "# -- End of go extension -- #") | 
|  |  | 
|  | resolved("`" + repo + "` has been introduced as go extension.") | 
|  | append_migration_info("## Migration of `" + repo + "`:") | 
|  |  | 
|  | if os.path.exists("go.mod") and os.path.exists("go.sum"): | 
|  | if exists_in_file("MODULE.bazel", origin_attrs["name"]): | 
|  | append_migration_info("It has already been introduced as a go module with the help of `go.mod`.\n") | 
|  |  | 
|  | if not exists_in_file("MODULE.bazel", 'go_deps.from_file(go_mod = "//:go.mod")'): | 
|  | from_file = """go_deps.from_file(go_mod = "//:go.mod") | 
|  | go_sdk.from_file(go_mod = "//:go.mod") | 
|  | """ | 
|  | write_at_given_place("MODULE.bazel", from_file, "# -- End of go extension -- #") | 
|  | exit_code, stdout, _ = execute_command(["bazel", "mod", "tidy", "--enable_bzlmod"]) | 
|  | assertExitCode(exit_code, 0, "Failed to run `bazel mod tidy`", stdout) | 
|  | append_migration_info("It has been introduced as a go module with the help of `go.mod`:\n") | 
|  | append_migration_info("```\n" + from_file + "```") | 
|  | else: | 
|  | go_module = ["go_deps.module("] | 
|  | if "importpath" in origin_attrs: | 
|  | go_module.append('    path = "' + origin_attrs["importpath"] + '",') | 
|  | if "sum" in origin_attrs: | 
|  | go_module.append('    sum = "' + origin_attrs["sum"] + '",') | 
|  | if "version" in origin_attrs: | 
|  | go_module.append('    version = "' + origin_attrs["version"] + '",') | 
|  | elif "tag" in origin_attrs: | 
|  | go_module.append('    version = "' + origin_attrs["tag"] + '",') | 
|  | go_module.append(")\n") | 
|  |  | 
|  | write_at_given_place( | 
|  | "MODULE.bazel", | 
|  | 'use_repo(go_deps, "' + origin_attrs["name"] + '")', | 
|  | "# -- End of go extension -- #", | 
|  | ) | 
|  | write_at_given_place("MODULE.bazel", "\n".join(go_module), "use_repo(go_deps, ") | 
|  | append_migration_info("It has been introduced as a go module:\n") | 
|  | append_migration_info("```\n" + "\n".join(go_module) + "```") | 
|  |  | 
|  | # Add gazelle_override if needed. | 
|  | gazelle_override_attrs = [] | 
|  | if "build_file_proto_mode" in origin_attrs: | 
|  | gazelle_override_attrs.append('"gazelle:proto ' + origin_attrs["build_file_proto_mode"] + '",') | 
|  | if "build_naming_convention" in origin_attrs: | 
|  | gazelle_override_attrs.append('"gazelle:go_naming_convention ' + origin_attrs["build_naming_convention"] + '",') | 
|  | if gazelle_override_attrs: | 
|  | directives = "\n         ".join(gazelle_override_attrs) | 
|  | gazelle_override = f"""go_deps.gazelle_override( | 
|  | path = "{origin_attrs["importpath"]}", | 
|  | directives = [ | 
|  | {directives} | 
|  | ], | 
|  | ) | 
|  | """ | 
|  | write_at_given_place("MODULE.bazel", gazelle_override, "# -- End of go extension -- #") | 
|  | append_migration_info("Additionally, `gazelle_override` was used for the initial directives:\n") | 
|  | append_migration_info("```\n" + gazelle_override + "```") | 
|  |  | 
|  |  | 
|  | def add_maven_extension(repo, maven_artifacts, resolved_deps, workspace_name): | 
|  | # Introduce `rules_jvm_external` only once. | 
|  | if not exists_in_file("MODULE.bazel", 'bazel_dep(name = "rules_jvm_external'): | 
|  | address_unavailable_repo("rules_jvm_external", resolved_deps, workspace_name) | 
|  |  | 
|  | # Introduce `maven` extension only once. | 
|  | if not exists_in_file("MODULE.bazel", 'maven = use_extension("@rules_jvm_external//:extensions.bzl"'): | 
|  | maven_extension = """ | 
|  | maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") | 
|  | # -- End of maven extensions -- # | 
|  | """ | 
|  | write_at_given_place( | 
|  | "MODULE.bazel", | 
|  | maven_extension, | 
|  | REPO_IDENTIFIER, | 
|  | ) | 
|  |  | 
|  | # Introduce repo rule for `repo` only once. | 
|  | if not exists_in_file("MODULE.bazel", f'use_repo(maven, "{repo}")'): | 
|  | repo_rule = f""" | 
|  | use_repo(maven, "{repo}") | 
|  | # -- End of maven artifacts for repo `{repo}` -- #""" | 
|  | write_at_given_place( | 
|  | "MODULE.bazel", | 
|  | repo_rule, | 
|  | "# -- End of maven extensions", | 
|  | ) | 
|  |  | 
|  | # Translate each maven artifact which is lacking in MODULE.bazel file. | 
|  | for maven_artifact in maven_artifacts: | 
|  | parsed_data = json.loads(maven_artifact) | 
|  | group = parsed_data["group"] | 
|  |  | 
|  | if exists_in_file("MODULE.bazel", 'group = "' + group): | 
|  | continue | 
|  |  | 
|  | append_migration_info("## Migration of `" + group + "` (" + repo + "):") | 
|  | append_migration_info("It has been introduced as a maven artifact:\n") | 
|  |  | 
|  | artifact = f""" | 
|  | maven.artifact( | 
|  | name = "{repo}", | 
|  | group = "{group}", | 
|  | artifact = "{parsed_data["artifact"]}", | 
|  | version = "{parsed_data["version"]}" | 
|  | )""" | 
|  | write_at_given_place( | 
|  | "MODULE.bazel", | 
|  | artifact, | 
|  | f"# -- End of maven artifacts for repo `{repo}` ", | 
|  | ) | 
|  | resolved("`" + group + "` has been introduced as maven extension.") | 
|  | append_migration_info("```" + artifact + "\n```") | 
|  |  | 
|  |  | 
|  | def add_python_extension(repo, origin_attrs, resolved_deps, workspace_name): | 
|  | # Introduce `rules_python` only once. | 
|  | if not exists_in_file("MODULE.bazel", 'bazel_dep(name = "rules_python"'): | 
|  | address_unavailable_repo("rules_python", resolved_deps, workspace_name) | 
|  |  | 
|  | # Introduce `pip` extension only once. | 
|  | if not exists_in_file("MODULE.bazel", 'pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")'): | 
|  | pip_extension = """ | 
|  | pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") | 
|  | # -- End of pip extensions -- # | 
|  | """ | 
|  | write_at_given_place( | 
|  | "MODULE.bazel", | 
|  | pip_extension, | 
|  | REPO_IDENTIFIER, | 
|  | ) | 
|  |  | 
|  | # Determine python version to use. Check for an existing default or use 3.11. | 
|  | python_version = "3.11" | 
|  | try: | 
|  | with open("MODULE.bazel", "r") as f: | 
|  | match = re.search(r'python\.defaults\s*\(\s*python_version\s*=\s*"([^"]+)"', f.read()) | 
|  | except FileNotFoundError: | 
|  | match = None | 
|  |  | 
|  | if match: | 
|  | python_version = match.group(1) | 
|  | important(f"Using existing default python version {python_version} from MODULE.bazel.") | 
|  | else: | 
|  | important( | 
|  | f"{python_version} is used as a default python version. If you need a different version, please change it manually and then rerun the migration tool." | 
|  | ) | 
|  |  | 
|  | py_ext = f""" | 
|  | pip.parse( | 
|  | hub_name = "{repo}", | 
|  | requirements_lock = "{origin_attrs["requirements_lock"]}", | 
|  | python_version = "{python_version}", | 
|  | ) | 
|  | use_repo(pip, "{repo}") | 
|  | """ | 
|  | write_at_given_place( | 
|  | "MODULE.bazel", | 
|  | py_ext, | 
|  | "# -- End of pip extensions -- #", | 
|  | ) | 
|  |  | 
|  | py_toolchain = [] | 
|  | # Introduce `python` extension only once. | 
|  | if not exists_in_file( | 
|  | "MODULE.bazel", 'python = use_extension("@rules_python//python/extensions:python.bzl", "python")' | 
|  | ): | 
|  | py_toolchain.append('python = use_extension("@rules_python//python/extensions:python.bzl", "python")') | 
|  |  | 
|  | # Introduce python default version only once. | 
|  | if not exists_in_file("MODULE.bazel", "python.defaults(python_version ="): | 
|  | py_toolchain.append(f'python.defaults(python_version = "{python_version}")') | 
|  |  | 
|  | # Introduce python toolchain only once. | 
|  | if not exists_in_file("MODULE.bazel", f'python.toolchain(python_version = "{python_version}")'): | 
|  | py_toolchain.append(f'python.toolchain(python_version = "{python_version}")') | 
|  |  | 
|  | py_toolchain_msg = "\n".join(py_toolchain) | 
|  | write_at_given_place( | 
|  | "MODULE.bazel", | 
|  | py_toolchain_msg, | 
|  | "# -- End of pip extensions -- #", | 
|  | ) | 
|  |  | 
|  | resolved("`" + repo + "` has been introduced as python extension.") | 
|  | append_migration_info("## Migration of `" + repo + "`") | 
|  | append_migration_info("It has been introduced as a python extension:\n") | 
|  | append_migration_info("```" + py_ext + "\n" + py_toolchain_msg + "\n```") | 
|  |  | 
|  |  | 
|  | def add_python_repo(repo): | 
|  | append_migration_info("## Migration of `" + repo + "`") | 
|  | append_migration_info("It has been introduced as a python repo:\n") | 
|  |  | 
|  | py_repo = f'use_repo(python, "{repo}")' | 
|  | write_at_given_place( | 
|  | "MODULE.bazel", | 
|  | py_repo, | 
|  | "# -- End of pip extensions -- #", | 
|  | ) | 
|  | resolved("`" + repo + "` has been introduced as a python repo.") | 
|  | append_migration_info("```\n" + py_repo + "\n```") | 
|  |  | 
|  |  | 
|  | def address_unavailable_repo(repo, resolved_deps, workspace_name): | 
|  | # Check if it's the original main repo name | 
|  | if repo == workspace_name: | 
|  | error_message = [] | 
|  | error_message.append( | 
|  | f"Please remove the usages of referring your own repo via `@{repo}//`, " | 
|  | "targets should be referenced directly with `//`. " | 
|  | ) | 
|  | error_message.append( | 
|  | 'If it\'s used in a macro, you can use `Label("//foo/bar")` ' | 
|  | "to make sure it always points to your repo no matter where the macro is used." | 
|  | ) | 
|  | error_message.append( | 
|  | "You can temporarily work around this by adding `repo_name` attribute " | 
|  | "to the `module` directive in your MODULE.bazel file." | 
|  | ) | 
|  | error("\n".join(error_message)) | 
|  | # TODO(kotlaja): Create more visible section for TODO. | 
|  | append_migration_info("TODO: " + "\n".join(error_message)) | 
|  |  | 
|  | # Print the repo definition in the original WORKSPACE file | 
|  | repo_def, file_label, rule_name, is_macro = [], None, None, False | 
|  | urls, maven_artifacts, origin_attrs = [], [], [] | 
|  | for dep in resolved_deps: | 
|  | if dep["original_attributes"]["name"] == repo: | 
|  | repo_def, file_label, rule_name, is_macro = repo_definition(dep) | 
|  | origin_attrs = dep["original_attributes"] | 
|  | urls = origin_attrs.get("urls", []) | 
|  | if "artifacts" in origin_attrs: | 
|  | maven_artifacts = origin_attrs["artifacts"] | 
|  | if origin_attrs.get("url", None): | 
|  | urls.append(origin_attrs["url"]) | 
|  | if origin_attrs.get("remote", None): | 
|  | urls.append(origin_attrs["remote"]) | 
|  | break | 
|  |  | 
|  | if not repo_def: | 
|  | msg = f"Repository definition for `{repo}` is not found in ./resolved_deps.py file, please add `--initial/-i` flag to force update it." | 
|  | error(msg) | 
|  | append_migration_info(msg) | 
|  | return False | 
|  |  | 
|  | # Support go extension. | 
|  | if "bazel_gazelle" in file_label and "go_repository" in file_label: | 
|  | add_go_extension(repo, origin_attrs, resolved_deps, workspace_name) | 
|  | return True | 
|  |  | 
|  | # Support maven extensions. | 
|  | if "rules_jvm_external" in file_label and maven_artifacts: | 
|  | add_maven_extension(repo, maven_artifacts, resolved_deps, workspace_name) | 
|  | return True | 
|  |  | 
|  | # Support python extension. | 
|  | if "requirements_lock" in origin_attrs and "pip_repository" in file_label: | 
|  | add_python_extension(repo, origin_attrs, resolved_deps, workspace_name) | 
|  | return True | 
|  |  | 
|  | # Support python toolchain dependencies. | 
|  | if "generator_function" in origin_attrs and re.match(r"python_.*toolchains", origin_attrs["generator_function"]): | 
|  | add_python_repo(repo) | 
|  | return True | 
|  |  | 
|  | append_migration_info("## Migration of `" + repo + "`:") | 
|  | print_repo_definition(repo_def, dep) | 
|  |  | 
|  | # Check if a module is already available in the registry. | 
|  | found_module = None | 
|  | potential_modules = [] | 
|  | for module_name in REGISTRY_CLIENT.get_all_modules(): | 
|  | if repo == module_name: | 
|  | found_module = module_name | 
|  | append_migration_info("Found perfect name match in BCR: `" + module_name + "`\n") | 
|  | elif any(url_match_source_repo(url, module_name) for url in urls): | 
|  | potential_modules.append(module_name) | 
|  | if potential_modules: | 
|  | append_migration_info("Found partially name matches in BCR: `" + "`, `".join(potential_modules) + ("`\n")) | 
|  | if found_module == None and len(potential_modules) > 0: | 
|  | found_module = potential_modules[0] | 
|  |  | 
|  | if found_module: | 
|  | metadata = REGISTRY_CLIENT.get_metadata(found_module) | 
|  | version = metadata["versions"][-1] | 
|  | repo_name = "" if repo == found_module else f', repo_name = "{repo}"' | 
|  | bazel_dep_line = f'bazel_dep(name = "{found_module}", version = "{version}"{repo_name})' | 
|  |  | 
|  | if not exists_in_file("MODULE.bazel", bazel_dep_line): | 
|  | if yes_or_no( | 
|  | "Do you wish to add the bazel_dep definition to the MODULE.bazel file?", | 
|  | True, | 
|  | ): | 
|  | append_migration_info("It has been introduced as a Bazel module:\n") | 
|  | append_migration_info("\t" + bazel_dep_line + "") | 
|  | resolved("`" + repo + "` has been introduced as a Bazel module.") | 
|  | write_at_given_place("MODULE.bazel", bazel_dep_line, BAZEL_DEP_IDENTIFIER) | 
|  | return True | 
|  | else: | 
|  | append_migration_info("This module has already been added inside the MODULE.bazel file") | 
|  | return True | 
|  | else: | 
|  | append_migration_info("\tIt is not found in BCR. \n") | 
|  |  | 
|  | # Ask user if the dependency should be introduced via use_repo_rule | 
|  | # Only ask if the repo is defined in @bazel_tools or the root module to avoid potential cycle. | 
|  | if ( | 
|  | file_label | 
|  | and not is_macro | 
|  | and file_label.startswith(("//", "@bazel_tools//")) | 
|  | and yes_or_no( | 
|  | "Do you wish to introduce the repository with use_repo_rule in MODULE.bazel (requires Bazel 7.3 or later)?", | 
|  | True, | 
|  | ) | 
|  | ): | 
|  | append_migration_info("\tIt has been introduced with `use_repo_rule`:\n") | 
|  | resolved("`" + repo + "` has been introduced with `use_repo_rule`.") | 
|  | add_repo_with_use_repo_rule(repo, repo_def, file_label, rule_name) | 
|  | return True | 
|  |  | 
|  | # Ask user if the dependency should be introduced via module extension | 
|  | # Only ask when file_label exists, which means it's a starlark repository rule. | 
|  | elif file_label and yes_or_no("Do you wish to introduce the repository with a module extension?", True): | 
|  | append_migration_info("\tIt has been introduced using a module extension:\n") | 
|  | resolved("`" + repo + "` has been introduced using a module extension.") | 
|  | add_repo_to_module_extension(repo, repo_def, file_label, rule_name) | 
|  | return True | 
|  | elif rule_name == "local_repository" and repo != "bazel_tools": | 
|  | append_migration_info("\tIt has been introduced using a module extension since it is local_repository rule:\n") | 
|  | resolved("`" + repo + "` has been introduced using a module extension (local_repository).") | 
|  | add_repo_to_module_extension(repo, repo_def, "@bazel_tools//tools/build_defs/repo:local.bzl", rule_name) | 
|  | return True | 
|  |  | 
|  | append_migration_info("\tPlease manually add this dependency.") | 
|  | return False | 
|  |  | 
|  |  | 
|  | def detect_bind_issue(stderr): | 
|  | """Search for error message that maybe caused by missing bind statements and return the missing target and its location.""" | 
|  | for line in stderr.split("\n"): | 
|  | s = re.search(r"ERROR: (.*): no such package 'external':", line) | 
|  | if s: | 
|  | return s.groups()[0] | 
|  | return None | 
|  |  | 
|  |  | 
|  | def address_bind_issue(bind_target_location, resolved_repos): | 
|  | print("") | 
|  | error( | 
|  | f"A bind target detected at {bind_target_location}! `bind` is already deprecated," | 
|  | " you should reference the actual target directly instead of using //external:<target>" | 
|  | " (details at https://bazel.build/external/migration#bind-targets). After this fix, rerun this tool." | 
|  | ) | 
|  | print("") | 
|  |  | 
|  |  | 
|  | def extract_version_number(bazel_version): | 
|  | """Extracts the semantic version number from a version string | 
|  | Args: | 
|  | bazel_version: the version string that begins with the semantic version | 
|  | e.g. "1.2.3rc1 abc1234" where "abc1234" is a commit hash. | 
|  | Returns: | 
|  | The semantic version string, like "1.2.3". | 
|  | """ | 
|  | for i in range(len(bazel_version)): | 
|  | c = bazel_version[i] | 
|  | if not (c.isdigit() or c == "."): | 
|  | return bazel_version[:i] | 
|  | return bazel_version | 
|  |  | 
|  |  | 
|  | def parse_bazel_version(bazel_version): | 
|  | """Parses a version string into a 3-tuple of ints | 
|  | int tuples can be compared directly using binary operators (<, >). | 
|  | Args: | 
|  | bazel_version: the Bazel version string | 
|  | Returns: | 
|  | An int 3-tuple of a (major, minor, patch) version. | 
|  | """ | 
|  |  | 
|  | version = extract_version_number(bazel_version) | 
|  | return tuple([int(n) for n in version.split(".")]) | 
|  |  | 
|  |  | 
|  | def prepare_migration(initial_flag): | 
|  | """Preparation work before starting the migration.""" | 
|  | exit_code, stdout, _ = execute_command(["bazel", "--version"]) | 
|  | eprint(stdout.strip() + "\n") | 
|  | if exit_code != 0 or not stdout: | 
|  | warning( | 
|  | "Current bazel is not a release version, we recommend using Bazel 7 or newer releases for Bzlmod migration." | 
|  | ) | 
|  | elif parse_bazel_version(stdout.strip().split(" ")[1]) < (6, 0, 0): | 
|  | error("Current Bazel version is too old, please upgrade to Bazel 7 or newer releases for Bzlmod migration.") | 
|  | abort_migration() | 
|  |  | 
|  | # Parse the original workspace name from the WORKSPACE file | 
|  | workspace_name = "main" | 
|  | with open("WORKSPACE", "r") as f: | 
|  | for line in f: | 
|  | s = re.search(r"workspace\(name\s+=\s+[\'\"]([A-Za-z0-9_-]+)[\'\"]", line) | 
|  | if s: | 
|  | workspace_name = s.groups()[0] | 
|  | info(f"Detected original workspace name: {workspace_name}\n") | 
|  |  | 
|  | # Delete MODULE.bazel file if `--initial` flag is set. | 
|  | if initial_flag: | 
|  | delete_file_if_exists("MODULE.bazel") | 
|  | delete_file_if_exists("migration_info.md") | 
|  |  | 
|  | # Create MODULE.bazel file if it doesn't exist already. | 
|  | if not pathlib.Path("MODULE.bazel").is_file(): | 
|  | scratch_file( | 
|  | "MODULE.bazel", | 
|  | [f'module(name = "{workspace_name}", version="")'], | 
|  | ) | 
|  | module_bazel_content = open("MODULE.bazel", "r").read() | 
|  | for identifier in [ | 
|  | BAZEL_DEP_IDENTIFIER, | 
|  | USE_REPO_RULE_IDENTIFIER, | 
|  | REPO_IDENTIFIER, | 
|  | ]: | 
|  | if identifier not in module_bazel_content: | 
|  | scratch_file("MODULE.bazel", ["", identifier], mode="a") | 
|  |  | 
|  | return workspace_name | 
|  |  | 
|  |  | 
|  | def generate_resolved_file(targets, use_bazel_sync): | 
|  | exit_code, _, stderr = execute_command(["bazel", "clean", "--expunge"]) | 
|  | assertExitCode(exit_code, 0, "Failed to run `bazel clean --expunge`", stderr) | 
|  | bazel_nobuild_command = [ | 
|  | "bazel", | 
|  | "build", | 
|  | "--nobuild", | 
|  | "--noenable_bzlmod", | 
|  | "--enable_workspace", | 
|  | "--experimental_repository_resolved_file=resolved_deps.py", | 
|  | ] + targets | 
|  | bazel_sync_comand = [ | 
|  | "bazel", | 
|  | "sync", | 
|  | "--experimental_repository_resolved_file=resolved_deps.py", | 
|  | ] | 
|  | bazel_command = bazel_sync_comand if use_bazel_sync else bazel_nobuild_command | 
|  | exit_code, _, stderr = execute_command(bazel_command) | 
|  | assertExitCode(exit_code, 0, "Failed to run `" + " ".join(bazel_command) + "`", stderr) | 
|  |  | 
|  | # Remove lines containing `"_action_listener":` in the resolved_deps.py file. | 
|  | # Avoiding https://github.com/bazelbuild/bazel-central-registry/issues/2789 | 
|  | with open("resolved_deps.py", "r") as f: | 
|  | lines = f.readlines() | 
|  | with open("resolved_deps.py", "w") as f: | 
|  | for line in lines: | 
|  | if "unknown object com" not in line: | 
|  | f.write(line) | 
|  |  | 
|  |  | 
|  | def load_resolved_deps(targets, use_bazel_sync, force): | 
|  | """Generate and load the resolved file that contains external deps info.""" | 
|  | if not pathlib.Path("resolved_deps.py").is_file() or force: | 
|  | info("Generating ./resolved_deps.py file - It might take a while...") | 
|  | generate_resolved_file(targets, use_bazel_sync) | 
|  | else: | 
|  | info( | 
|  | "Found existing ./resolved_deps.py file - " | 
|  | "If it's out of date, please add `--initial/-i` flag to force update it." | 
|  | ) | 
|  |  | 
|  | spec = importlib.util.spec_from_file_location("resolved_deps", "./resolved_deps.py") | 
|  | module = importlib.util.module_from_spec(spec) | 
|  | sys.modules["resolved_deps"] = module | 
|  | spec.loader.exec_module(module) | 
|  | resolved_deps = module.resolved | 
|  | return resolved_deps | 
|  |  | 
|  |  | 
|  | def parse_file(filename): | 
|  | direct_deps = set() | 
|  | previous_line_has_external = False | 
|  | with open(filename, "r") as file: | 
|  | for line in file: | 
|  | # Parse for "@". | 
|  | matches_at = re.findall(r"@(\w+)//", line) | 
|  | for match_at in matches_at: | 
|  | if match_at != "bazel_tools": | 
|  | direct_deps.add(match_at) | 
|  |  | 
|  | # Parse for "/external/{repo_name}/". | 
|  | matches_external = re.findall(r"/external/(\w+)/", line) | 
|  | if previous_line_has_external == False: | 
|  | # Only first "/external/" is relevant. | 
|  | for match in matches_external: | 
|  | if match != "bazel_tools": | 
|  | direct_deps.add(match) | 
|  | previous_line_has_external = True if matches_external else False | 
|  |  | 
|  | return direct_deps | 
|  |  | 
|  |  | 
|  | def delete_file_if_exists(filename): | 
|  | """Deletes a file if it exists.""" | 
|  | if os.path.exists(filename): | 
|  | try: | 
|  | os.remove(filename) | 
|  | except OSError as e: | 
|  | print(f"Error deleting file '{filename}': {e}") | 
|  |  | 
|  |  | 
|  | def query_direct_targets(args): | 
|  | targets = args.target | 
|  | direct_deps_file = "query_direct_deps" | 
|  | delete_file_if_exists(direct_deps_file) | 
|  |  | 
|  | for target in targets: | 
|  | bazel_command = ["bazel", "query", "--noenable_bzlmod", "--enable_workspace", "--output=build"] + [target] | 
|  | exit_code, stdout, stderr = execute_command(bazel_command) | 
|  | if exit_code != 0 or not stdout: | 
|  | error( | 
|  | "Bazel query: `" | 
|  | + " ".join(bazel_command) | 
|  | + "` contains error:\n" | 
|  | + stderr | 
|  | + "\nDouble check if the target you've specified can be built successfully." | 
|  | ) | 
|  | abort_migration() | 
|  | append_to_file(direct_deps_file, stdout) | 
|  |  | 
|  | direct_deps = parse_file(direct_deps_file) | 
|  | append_migration_info("## Direct dependencies:") | 
|  | append_migration_info("* " + "\n* ".join(map(str, direct_deps))) | 
|  |  | 
|  | return direct_deps | 
|  |  | 
|  |  | 
|  | def run_first_part(initial_flag): | 
|  | # Return true if MODULE.bazel file doesn't exist or if flag `--initial` is set. | 
|  | return not pathlib.Path("MODULE.bazel").is_file() or initial_flag | 
|  |  | 
|  |  | 
|  | def get_error_target(stderr, init_target): | 
|  | pattern = r"Analysis of target '(.*?)' failed" | 
|  | match = re.search(pattern, stderr) | 
|  | if match: | 
|  | return match.group(1) | 
|  | else: | 
|  | return " ".join(init_target) | 
|  |  | 
|  |  | 
|  | def main(argv=None): | 
|  | if argv is None: | 
|  | argv = sys.argv[1:] | 
|  |  | 
|  | parser = argparse.ArgumentParser( | 
|  | prog="migrate_to_bzlmod", | 
|  | description="A helper script for migrating your external dependencies from WORKSPACE to Bzlmod. " | 
|  | + "For given targets, it first tries to generate a list of external dependencies for building your targets, " | 
|  | + "then tries to detect and add missing dependencies in the Bzlmod build. " | 
|  | + "You may still need to fix some problems manually.", | 
|  | epilog=( | 
|  | "Example usage: change into your project directory and run " | 
|  | "`<path to BCR repo>/tools/migrate_to_bzlmod.py --target //foo:bar`" | 
|  | ), | 
|  | ) | 
|  | parser.add_argument( | 
|  | "-s", | 
|  | "--sync", | 
|  | action="store_true", | 
|  | help="use `bazel sync` instead of `bazel build --nobuild` to generate the resolved dependencies. " | 
|  | + "`bazel build --nobuild` only fetches dependencies needed for building specified targets, " | 
|  | + "while `bazel sync` resolves and fetches all dependencies defined in your WORKSPACE file, " | 
|  | + "including bind statements and execution platform & toolchain registrations.", | 
|  | ) | 
|  | parser.add_argument( | 
|  | "-c", | 
|  | "--collaborate", | 
|  | action="store_true", | 
|  | help="collaborate with the user interactively on what to do.", | 
|  | ) | 
|  | parser.add_argument( | 
|  | "-t", | 
|  | "--target", | 
|  | type=str, | 
|  | action="append", | 
|  | help="specify the targets you want to migrate. This flag is repeatable, and the targets are accumulated.", | 
|  | ) | 
|  | parser.add_argument( | 
|  | "-i", | 
|  | "--initial", | 
|  | action="store_true", | 
|  | help="detect direct dependencies, introduce them in MODULE.bazel and rerun generation of resolved dependencies. Running with this flag always overrides the current MODULE.bazel file.", | 
|  | ) | 
|  |  | 
|  | args = parser.parse_args(argv) | 
|  |  | 
|  | if not args.target: | 
|  | parser.print_help() | 
|  | return 1 | 
|  |  | 
|  | run_initial = run_first_part(args.initial) | 
|  | workspace_name = prepare_migration(args.initial) | 
|  |  | 
|  | resolved_deps = load_resolved_deps(args.target, args.sync, args.initial) | 
|  |  | 
|  | yes_or_no.enable = args.collaborate | 
|  | repro_command = "bazel build --enable_bzlmod --noenable_workspace " + " ".join(args.target) | 
|  |  | 
|  | # First part of the migration - Find direct deps with bazel query and add them in MODULE.bazel file. | 
|  | if run_initial: | 
|  | append_migration_info("# Migration info") | 
|  | append_migration_info("Command for local testing:") | 
|  | append_migration_info("```\n" + repro_command + "\n```") | 
|  | print("") | 
|  | direct_deps = query_direct_targets(args) | 
|  |  | 
|  | resolved_repos = [] | 
|  | unresolved_deps = [] | 
|  | for direct_dep in direct_deps: | 
|  | if address_unavailable_repo(direct_dep, resolved_deps, workspace_name): | 
|  | resolved_repos.append(direct_dep) | 
|  | else: | 
|  | unresolved_deps.append(direct_dep) | 
|  |  | 
|  | if unresolved_deps: | 
|  | print(f"{RED}\nThese repos need manual support:") | 
|  | for dep in unresolved_deps: | 
|  | print(f"\t{RED}" + dep) | 
|  | # TODO(kotlaja): Add these repos at the end. | 
|  | else: | 
|  | info( | 
|  | "To create a MODULE.bazel file from scratch, either delete existing MODULE.bazel file or use the `--initial/-i` flag.\n" | 
|  | ) | 
|  |  | 
|  | # Second part of the migration - Build with bzlmod and fix potential errors. | 
|  | while True: | 
|  | # Try to build with Bzlmod enabled | 
|  | targets = args.target | 
|  | bazel_command = [ | 
|  | "bazel", | 
|  | "build", | 
|  | "--nobuild", | 
|  | "--enable_bzlmod", | 
|  | "--noenable_workspace", | 
|  | ] + targets | 
|  | exit_code, _, stderr = execute_command(bazel_command) | 
|  | if exit_code == 0: | 
|  | print("") | 
|  | info( | 
|  | "Congratulations! All external repositories needed for building `" | 
|  | + " ".join(targets) | 
|  | + "` are available with Bzlmod!" | 
|  | ) | 
|  | important("Fix potential build time issues by running the following command:") | 
|  | eprint(f"{BOLD}        `{repro_command}`{RESET}") | 
|  | break | 
|  |  | 
|  | # 1. Detect build failure caused by unavailable repository | 
|  | repo = detect_unavailable_repo_error(stderr) | 
|  | if repo: | 
|  | if address_unavailable_repo(repo, resolved_deps, workspace_name): | 
|  | continue | 
|  |  | 
|  | # 2. Detect build failure caused by unavailable bind statements | 
|  | bind_target_location = detect_bind_issue(stderr) | 
|  | if bind_target_location: | 
|  | if address_bind_issue(bind_target_location, resolved_deps): | 
|  | continue | 
|  |  | 
|  | print("") | 
|  | error("Unrecognized error, please fix manually:\n" + stderr) | 
|  | err_target = get_error_target(stderr, args.target) | 
|  | important("Fix the error, then run this migration tool again. Command for reproducing the error:") | 
|  | eprint(f"{BOLD}    `bazel build --enable_bzlmod --noenable_workspace {err_target}`{RESET}\n") | 
|  | return 1 | 
|  |  | 
|  | print("") | 
|  | important("For details about the migration process, check `migration_info.md` file.\n") | 
|  | return 0 | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | sys.exit(main()) |