| # Copyright 2019 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 |
| |
| load("//:specs.bzl", "utils") |
| load("//private:artifact_utilities.bzl", "deduplicate_and_sort_artifacts") |
| load( |
| "//private:coursier_utilities.bzl", |
| "SUPPORTED_PACKAGING_TYPES", |
| "contains_git_conflict_markers", |
| "escape", |
| "is_maven_local_path", |
| ) |
| load("//private:dependency_tree_parser.bzl", "parser") |
| load("//private:java_utilities.bzl", "build_java_argsfile_content", "parse_java_version") |
| load("//private:proxy.bzl", "get_java_proxy_args") |
| load( |
| "//private:versions.bzl", |
| "COURSIER_CLI_BAZEL_MIRROR_URL", |
| "COURSIER_CLI_GITHUB_ASSET_URL", |
| "COURSIER_CLI_SHA256", |
| ) |
| load("//private/rules:urls.bzl", "remove_auth_from_url") |
| load("//private/rules:v1_lock_file.bzl", "v1_lock_file") |
| load("//private/rules:v2_lock_file.bzl", "v2_lock_file") |
| |
| _BUILD = """ |
| # package(default_visibility = [{visibilities}]) # https://github.com/bazelbuild/bazel/issues/13681 |
| |
| load("@bazel_skylib//:bzl_library.bzl", "bzl_library") |
| load("@rules_jvm_external//private/rules:jvm_import.bzl", "jvm_import") |
| {aar_import_statement} |
| |
| {imports} |
| |
| # Required by stardoc if the repo is ever frozen |
| bzl_library( |
| name = "defs", |
| srcs = ["defs.bzl"], |
| deps = [ |
| "@rules_jvm_external//:implementation", |
| ], |
| visibility = ["//visibility:public"], |
| ) |
| """ |
| |
| DEFAULT_AAR_IMPORT_LABEL = "@build_bazel_rules_android//android:rules.bzl" |
| |
| _AAR_IMPORT_STATEMENT = """\ |
| load("%s", "aar_import") |
| """ |
| |
| _BUILD_PIN = """ |
| sh_binary( |
| name = "pin", |
| srcs = ["pin.sh"], |
| args = [ |
| # TODO: change to rlocationpath once rules_jvm_external drops support for Bazel <5.4.0 |
| # "$(rlocationpath :unsorted_deps.json)", |
| # We can use execpath in the meantime, because we know this file is always a source file |
| # in that external repo. |
| "$(execpath :unsorted_deps.json)", |
| ], |
| data = [ |
| ":unsorted_deps.json", |
| ], |
| deps = [ |
| "@bazel_tools//tools/bash/runfiles", |
| ], |
| visibility = ["//visibility:public"], |
| ) |
| """ |
| |
| _BUILD_PIN_ALIAS = """ |
| # Alias to unpinned to allow pinning |
| alias( |
| name = "pin", |
| actual = "{unpinned_pin_target}", |
| ) |
| """ |
| |
| _BUILD_OUTDATED = """ |
| sh_binary( |
| name = "outdated", |
| srcs = ["outdated.sh"], |
| data = [ |
| "@rules_jvm_external//private/tools/prebuilt:outdated_deploy.jar", |
| "outdated.artifacts", |
| "outdated.repositories", |
| ], |
| args = [ |
| "$(location @rules_jvm_external//private/tools/prebuilt:outdated_deploy.jar)", |
| "$(location outdated.artifacts)", |
| "$(location outdated.repositories)", |
| ], |
| visibility = ["//visibility:public"], |
| ) |
| """ |
| |
| EMPTY_FILE_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" |
| |
| def _is_verbose(repository_ctx): |
| return bool(repository_ctx.os.environ.get("RJE_VERBOSE")) |
| |
| def _is_windows(repository_ctx): |
| return repository_ctx.os.name.find("windows") != -1 |
| |
| def _is_linux(repository_ctx): |
| return repository_ctx.os.name.find("linux") != -1 |
| |
| def _is_macos(repository_ctx): |
| return repository_ctx.os.name.find("mac") != -1 |
| |
| def _is_file(repository_ctx, path): |
| return repository_ctx.which("test") and repository_ctx.execute(["test", "-f", path]).return_code == 0 |
| |
| def _is_directory(repository_ctx, path): |
| return repository_ctx.which("test") and repository_ctx.execute(["test", "-d", path]).return_code == 0 |
| |
| def _shell_quote(s): |
| # Lifted from |
| # https://github.com/bazelbuild/bazel-skylib/blob/6a17363a3c27dde70ab5002ad9f2e29aff1e1f4b/lib/shell.bzl#L49 |
| # because this file cannot load symbols from bazel_skylib since commit |
| # 47505f644299aa2483d0df06c2bb2c7aa10d26d4. |
| return "'" + s.replace("'", "'\\''") + "'" |
| |
| def _execute(repository_ctx, cmd, timeout = 600, environment = {}, progress_message = None): |
| if progress_message: |
| repository_ctx.report_progress(progress_message) |
| |
| verbose = _is_verbose(repository_ctx) |
| if verbose: |
| repository_ctx.execute( |
| ["echo", "\n%s" % " ".join([str(c) for c in cmd])], |
| quiet = False, |
| ) |
| |
| return repository_ctx.execute( |
| cmd, |
| timeout = timeout, |
| environment = environment, |
| quiet = not verbose, |
| ) |
| |
| def _execute_with_argsfile( |
| repository_ctx, |
| tool, |
| tool_name, |
| progress_message, |
| error_description, |
| files_to_inspect): |
| # Currently each tool can only be used once per repository. |
| # This could be avoided by adding a disambiguator to the argsfile name. |
| |
| # Avoid argument limits by putting list of files to inspect into a file |
| repository_ctx.file( |
| "{}_argsfile".format(tool_name), |
| "\n".join([str(f) for f in files_to_inspect]) + "\n", |
| executable = False, |
| ) |
| |
| command = _generate_java_jar_command( |
| repository_ctx, |
| repository_ctx.path(tool), |
| ) |
| |
| exec_result = _execute( |
| repository_ctx, |
| command + ["--argsfile", repository_ctx.path("{}_argsfile".format(tool_name))], |
| progress_message = progress_message, |
| ) |
| if exec_result.return_code != 0: |
| fail("Error while " + error_description + ": " + exec_result.stderr) |
| |
| return exec_result.stdout |
| |
| # The representation of a Windows path when read from the parsed Coursier JSON |
| # is delimited by 4 back slashes. Replace them with 1 forward slash. |
| def _normalize_to_unix_path(path): |
| return path.replace("\\", "/") |
| |
| # Relativize an absolute path to an artifact in coursier's default cache location. |
| # After relativizing, also symlink the path into the workspace's output base. |
| # Then return the relative path for further processing |
| def _relativize_and_symlink_file_in_coursier_cache(repository_ctx, absolute_path): |
| # The path manipulation from here on out assumes *nix paths, not Windows. |
| # for artifact_absolute_path in artifact_absolute_paths: |
| # |
| # Also replace '\' with '/` to normalize windows paths to *nix style paths |
| # BUILD files accept only *nix paths, so we normalize them here. |
| # |
| # Also, replace '//' with '/', otherwise parsing of the file path for the |
| # coursier cache will fail if variables like HOME or COURSIER_CACHE have a |
| # trailing slash. |
| # |
| # We assume that coursier uses the default cache location |
| # TODO(jin): allow custom cache locations |
| coursier_cache_path = get_coursier_cache_or_default( |
| repository_ctx, |
| repository_ctx.attr.name.startswith("unpinned_"), |
| ).replace("//", "/") |
| absolute_path_parts = absolute_path.split(coursier_cache_path) |
| if len(absolute_path_parts) != 2: |
| fail("Error while trying to parse the path of file in the coursier cache: " + absolute_path) |
| else: |
| relative_path = absolute_path_parts[1] |
| |
| # Coursier prefixes private repositories with the username, which obfuscates |
| # changes to the pinned json so we remove it from the relative path. |
| credential_marker = relative_path.find("%40") |
| if credential_marker > -1: |
| user_prefix = relative_path[:credential_marker + 3].split("/")[-1] |
| relative_path = relative_path.replace(user_prefix, "") |
| |
| # Make a symlink from the absolute path of the artifact to the relative |
| # path within the output_base/external. |
| artifact_relative_path = "v1" + relative_path |
| repository_ctx.symlink(absolute_path, repository_ctx.path(artifact_relative_path)) |
| return artifact_relative_path |
| |
| # Relativize an absolute path to an artifact in maven local. |
| # After relativizing, also symlink the path into the workspace's output base. |
| # Then return the relative path for further processing |
| def _relativize_and_symlink_file_in_maven_local(repository_ctx, absolute_path): |
| absolute_path_parts = absolute_path.split(".m2/repository") |
| if len(absolute_path_parts) != 2: |
| fail("Error while trying to parse the path of file in maven local: " + absolute_path_parts) |
| else: |
| # Make a symlink from the absolute path of the artifact to the relative |
| # path within the output_base/external. |
| artifact_relative_path = "v1" + absolute_path_parts[1] |
| repository_ctx.symlink(absolute_path, repository_ctx.path(artifact_relative_path)) |
| return artifact_relative_path |
| |
| def _get_aar_import_statement_or_empty_str(repository_ctx): |
| if repository_ctx.attr.use_starlark_android_rules: |
| # parse the label to validate it |
| _ = Label(repository_ctx.attr.aar_import_bzl_label) |
| return _AAR_IMPORT_STATEMENT % repository_ctx.attr.aar_import_bzl_label |
| else: |
| return "" |
| |
| def _java_path(repository_ctx): |
| java_home = repository_ctx.os.environ.get("JAVA_HOME") |
| if java_home != None: |
| return repository_ctx.path(java_home + "/bin/java") |
| elif repository_ctx.which("java") != None: |
| return repository_ctx.which("java") |
| return None |
| |
| # Generate the base `coursier` command depending on the OS, JAVA_HOME or the |
| # location of `java`. |
| def _generate_java_jar_command(repository_ctx, jar_path): |
| coursier_opts = repository_ctx.os.environ.get("COURSIER_OPTS", "") |
| coursier_opts = coursier_opts.split(" ") if len(coursier_opts) > 0 else [] |
| java_path = _java_path(repository_ctx) |
| |
| if java_path != None: |
| # https://github.com/coursier/coursier/blob/master/doc/FORMER-README.md#how-can-the-launcher-be-run-on-windows-or-manually-with-the-java-program |
| # The -noverify option seems to be required after the proguarding step |
| # of the main JAR of coursier. |
| cmd = [java_path, "-noverify", "-jar"] + coursier_opts + _get_java_proxy_args(repository_ctx) + [jar_path] |
| else: |
| # Try to execute coursier directly |
| cmd = [jar_path] + coursier_opts + ["-J%s" % arg for arg in _get_java_proxy_args(repository_ctx)] |
| |
| return cmd |
| |
| # Extract the well-known environment variables http_proxy, https_proxy and |
| # no_proxy and convert them to java.net-compatible property arguments. |
| def _get_java_proxy_args(repository_ctx): |
| # Check both lower- and upper-case versions of the environment variables, preferring the former |
| http_proxy = repository_ctx.os.environ.get("http_proxy", repository_ctx.os.environ.get("HTTP_PROXY")) |
| https_proxy = repository_ctx.os.environ.get("https_proxy", repository_ctx.os.environ.get("HTTPS_PROXY")) |
| no_proxy = repository_ctx.os.environ.get("no_proxy", repository_ctx.os.environ.get("NO_PROXY")) |
| return get_java_proxy_args(http_proxy, https_proxy, no_proxy) |
| |
| def _windows_check(repository_ctx): |
| # TODO(jin): Remove BAZEL_SH usage ASAP. Bazel is going bashless, so BAZEL_SH |
| # will not be around for long. |
| # |
| # On Windows, run msys once to bootstrap it |
| # https://github.com/bazelbuild/rules_jvm_external/issues/53 |
| if (_is_windows(repository_ctx)): |
| bash = repository_ctx.os.environ.get("BAZEL_SH") |
| if (bash == None): |
| fail("Please set the BAZEL_SH environment variable to the path of MSYS2 bash. " + |
| "This is typically `c:\\msys64\\usr\\bin\\bash.exe`. For more information, read " + |
| "https://docs.bazel.build/versions/master/install-windows.html#getting-bazel") |
| |
| def _stable_artifact(artifact): |
| parsed = json.decode(artifact) |
| |
| # Sort the keys to provide a stable order |
| keys = sorted(parsed.keys()) |
| return ":".join(["%s=%s" % (key, parsed[key]) for key in keys]) |
| |
| # Compute a signature of the list of artifacts that will be used to build |
| # the dependency tree. This is used as a check to see whether the dependency |
| # tree needs to be repinned. |
| # Returns a tuple where the first element is the currently used hash, and the |
| # second element is a list of hashes in previous formats. This is to allow for |
| # upgrading rules_jvm_external when the hash inputs change. |
| # |
| # Visible for testing |
| def compute_dependency_inputs_signature(artifacts, repositories, excluded_artifacts): |
| artifact_inputs = [] |
| excluded_artifact_inputs = [] |
| |
| for artifact in sorted(artifacts): |
| artifact_inputs.append(_stable_artifact(artifact)) |
| |
| for artifact in sorted(excluded_artifacts): |
| excluded_artifact_inputs.append(_stable_artifact(artifact)) |
| |
| v1_sig = hash(repr(sorted(artifact_inputs))) ^ hash(repr(sorted(repositories))) |
| |
| hash_parts = [sorted(artifact_inputs), sorted(repositories), sorted(excluded_artifact_inputs)] |
| current_version_sig = 0 |
| for part in hash_parts: |
| current_version_sig ^= hash(repr(part)) |
| |
| return (current_version_sig, [v1_sig]) |
| |
| def get_netrc_lines_from_entries(netrc_entries): |
| netrc_lines = [] |
| for machine, login_dict in sorted(netrc_entries.items()): |
| for login, password in sorted(login_dict.items()): |
| netrc_lines.append("machine {}".format(machine)) |
| netrc_lines.append("login {}".format(login)) |
| if password: |
| netrc_lines.append("password {}".format(password)) |
| return netrc_lines |
| |
| def get_home_netrc_contents(repository_ctx): |
| # Copied with a ctx -> repository_ctx rename from tools/build_defs/repo/http.bzl's _get_auth. |
| # Need to keep updated with improvements in source since we cannot load private methods. |
| if "HOME" in repository_ctx.os.environ: |
| if not repository_ctx.os.name.startswith("windows"): |
| netrcfile = "%s/.netrc" % (repository_ctx.os.environ["HOME"],) |
| if _is_file(repository_ctx, netrcfile): |
| return repository_ctx.read(netrcfile) |
| return "" |
| |
| def _add_outdated_files(repository_ctx, artifacts, repositories): |
| repository_ctx.file( |
| "outdated.artifacts", |
| "\n".join(["{}:{}:{}".format(artifact["group"], artifact["artifact"], artifact["version"]) for artifact in artifacts]) + "\n", |
| executable = False, |
| ) |
| |
| repository_ctx.file( |
| "outdated.repositories", |
| "\n".join([repo["repo_url"] for repo in repositories]) + "\n", |
| executable = False, |
| ) |
| |
| repository_ctx.template( |
| "outdated.sh", |
| repository_ctx.attr._outdated, |
| { |
| "{repository_name}": repository_ctx.name, |
| "{proxy_opts}": " ".join([_shell_quote(arg) for arg in _get_java_proxy_args(repository_ctx)]), |
| }, |
| executable = True, |
| ) |
| |
| def is_repin_required(repository_ctx): |
| env_var_names = repository_ctx.os.environ.keys() |
| return "RULES_JVM_EXTERNAL_REPIN" not in env_var_names and "REPIN" not in env_var_names |
| |
| def _get_fail_if_repin_required(repository_ctx): |
| if not repository_ctx.attr.fail_if_repin_required: |
| return False |
| |
| return is_repin_required(repository_ctx) |
| |
| def print_if_not_repinning(repository_ctx, *args): |
| if is_repin_required(repository_ctx): |
| return |
| print(*args) |
| |
| def _pinned_coursier_fetch_impl(repository_ctx): |
| if not repository_ctx.attr.maven_install_json: |
| fail("Please specify the file label to maven_install.json (e.g." + |
| "//:maven_install.json).") |
| |
| _windows_check(repository_ctx) |
| |
| repositories = [] |
| for repository in repository_ctx.attr.repositories: |
| repositories.append(json.decode(repository)) |
| |
| artifacts = [] |
| for artifact in repository_ctx.attr.artifacts: |
| artifacts.append(json.decode(artifact)) |
| |
| _check_artifacts_are_unique(artifacts, repository_ctx.attr.duplicate_version_warning) |
| |
| # Read Coursier state from maven_install.json. |
| repository_ctx.symlink( |
| repository_ctx.path(repository_ctx.attr.maven_install_json), |
| repository_ctx.path("imported_maven_install.json"), |
| ) |
| lock_file_content = repository_ctx.read(repository_ctx.attr.maven_install_json) |
| if not len(lock_file_content) or contains_git_conflict_markers(repository_ctx.attr.maven_install_json, lock_file_content): |
| maven_install_json_content = { |
| "artifacts": {}, |
| "dependencies": {}, |
| "repositories": {}, |
| "version": "2", |
| } |
| else: |
| maven_install_json_content = json.decode(lock_file_content) |
| |
| if v1_lock_file.is_valid_lock_file(maven_install_json_content): |
| importer = v1_lock_file |
| print_if_not_repinning( |
| repository_ctx, |
| "Lock file should be updated. Please run `REPIN=1 bazel run @unpinned_%s//:pin`" % repository_ctx.name, |
| ) |
| elif v2_lock_file.is_valid_lock_file(maven_install_json_content): |
| importer = v2_lock_file |
| else: |
| fail("Unable to read lock file: %s" % repository_ctx.attr.maven_install_json) |
| |
| # Validation steps for maven_install.json. |
| |
| # Validate that there's a dependency_tree element in the parsed JSON. |
| if not importer.is_valid_lock_file(maven_install_json_content): |
| fail("Failed to parse %s. " % repository_ctx.path(repository_ctx.attr.maven_install_json) + |
| "It is not a valid maven_install.json file. Has this " + |
| "file been modified manually?") |
| |
| input_artifacts_hash = importer.get_input_artifacts_hash(maven_install_json_content) |
| |
| # With Bzlmod, repository_ctx.name is the mangled ("canonical") name of the repository, so we |
| # use the user_provided_name attribute to get the original name (always set by maven_install). |
| user_provided_name = repository_ctx.attr.user_provided_name or repository_ctx.name |
| |
| if user_provided_name == repository_ctx.name: |
| unpinned_repo = "unpinned_" + repository_ctx.name |
| else: |
| # Generate a canonical label pointing to the pin target so that users don't have to use_repo |
| # the unpinned_ repo with Bzlmod. |
| unpinned_repo = "@{}unpinned_{}".format( |
| repository_ctx.name[:-len(user_provided_name)], |
| user_provided_name, |
| ) |
| |
| # pin_target will alias to unpinned_pin_target later on, so we need both. |
| unpinned_pin_target = "@{}//:pin".format(unpinned_repo) |
| pin_target = "@{}//:pin".format(user_provided_name) |
| |
| repin_instructions = " REPIN=1 bazel run %s\n" % pin_target |
| |
| user_provided_repin_instructions = repository_ctx.attr.repin_instructions |
| repin_instructions = user_provided_repin_instructions if user_provided_repin_instructions else ( |
| " REPIN=1 bazel run %s\n" % pin_target |
| ) |
| |
| # Then, check to see if we need to repin our deps because inputs have changed |
| if input_artifacts_hash == None: |
| print_if_not_repinning( |
| repository_ctx, |
| "NOTE: %s_install.json does not contain a signature of the required artifacts. " % user_provided_name + |
| "This feature ensures that the build does not use stale dependencies when the inputs " + |
| "have changed. To generate this signature, run 'bazel run %s'." % pin_target, |
| ) |
| else: |
| computed_artifacts_hash, old_hashes = compute_dependency_inputs_signature( |
| repository_ctx.attr.artifacts, |
| repository_ctx.attr.repositories, |
| repository_ctx.attr.excluded_artifacts, |
| ) |
| if input_artifacts_hash in old_hashes: |
| print_if_not_repinning( |
| repository_ctx, |
| "WARNING: %s_install.json contains an outdated input signature. " % (user_provided_name) + |
| "It is recommended that you regenerate it by running either:\n" + repin_instructions, |
| ) |
| elif computed_artifacts_hash != input_artifacts_hash: |
| if _get_fail_if_repin_required(repository_ctx): |
| fail("%s_install.json contains an invalid input signature and must be regenerated. " % (user_provided_name) + |
| "This typically happens when the maven_install artifacts have been changed but not repinned. " + |
| "PLEASE DO NOT MODIFY THIS FILE DIRECTLY! To generate a new " + |
| "%s_install.json and re-pin the artifacts, either run:\n" % user_provided_name + |
| repin_instructions) |
| else: |
| print_if_not_repinning( |
| repository_ctx, |
| "The inputs to %s_install.json have changed, but the lock file has not been regenerated. " % user_provided_name + |
| "Consider running 'bazel run %s'" % pin_target, |
| ) |
| |
| dep_tree_signature = importer.get_lock_file_hash(maven_install_json_content) |
| |
| if dep_tree_signature == None: |
| print_if_not_repinning( |
| repository_ctx, |
| "NOTE: %s_install.json does not contain a signature entry of the dependency tree. " % user_provided_name + |
| "This feature ensures that the file is not modified manually. To generate this " + |
| "signature, run 'bazel run %s'." % pin_target, |
| ) |
| elif importer.compute_lock_file_hash(maven_install_json_content) != dep_tree_signature: |
| # Then, validate that the signature provided matches the contents of the dependency_tree. |
| # This is to stop users from manually modifying maven_install.json. |
| if _get_fail_if_repin_required(repository_ctx): |
| fail( |
| "%s_install.json contains an invalid signature (expected %s and got %s) and may be corrupted. " % ( |
| user_provided_name, |
| dep_tree_signature, |
| importer.compute_lock_file_hash(maven_install_json_content), |
| ) + |
| "PLEASE DO NOT MODIFY THIS FILE DIRECTLY! To generate a new " + |
| "%s_install.json and re-pin the artifacts, follow these steps: \n\n" % user_provided_name + |
| repin_instructions, |
| ) |
| else: |
| print_if_not_repinning( |
| repository_ctx, |
| "NOTE: %s_install.json does not contain an up to date hash of its contents. " % user_provided_name + |
| "This feature ensures that pinned dependencies are up to date. To generate this " + |
| "signature, run 'bazel run %s'." % pin_target, |
| ) |
| |
| # Create the list of http_file repositories for each of the artifacts |
| # in maven_install.json. This will be loaded additionally like so: |
| # |
| # load("@maven//:defs.bzl", "pinned_maven_install") |
| # pinned_maven_install() |
| http_files = [ |
| "load(\"@bazel_tools//tools/build_defs/repo:http.bzl\", \"http_file\")", |
| "load(\"@bazel_tools//tools/build_defs/repo:utils.bzl\", \"maybe\")", |
| "def pinned_maven_install():", |
| " pass", # Keep it syntactically correct in case of empty dependencies. |
| ] |
| maven_artifacts = [] |
| netrc_entries = importer.get_netrc_entries(maven_install_json_content) |
| |
| for artifact in importer.get_artifacts(maven_install_json_content): |
| http_file_repository_name = escape(artifact["coordinates"]) |
| maven_artifacts.extend([artifact["coordinates"]]) |
| http_files.extend([ |
| " http_file(", |
| " name = \"%s\"," % http_file_repository_name, |
| " sha256 = \"%s\"," % artifact["sha256"], |
| # repository_ctx should point to external/$repository_ctx.name |
| # The http_file should point to external/$http_file_repository_name |
| # File-path is relative defined from http_file traveling to repository_ctx. |
| " netrc = \"../%s/netrc\"," % (repository_ctx.name), |
| ]) |
| if len(artifact["urls"]) == 0 and importer.has_m2local(maven_install_json_content) and artifact.get("file") != None: |
| if _is_windows(repository_ctx): |
| user_home = repository_ctx.os.environ.get("USERPROFILE").replace("\\", "/") |
| else: |
| user_home = repository_ctx.os.environ.get("HOME") |
| m2local_urls = [ |
| "file://%s/.m2/repository/%s" % (user_home, artifact["file"]), |
| ] |
| else: |
| m2local_urls = [] |
| http_files.append(" urls = %s," % repr( |
| [remove_auth_from_url(url) for url in artifact["urls"] + m2local_urls], |
| )) |
| |
| # https://github.com/bazelbuild/rules_jvm_external/issues/1028 |
| # http_rule does not like directories named "build" so prepend v1 to the path. |
| download_path = "v1/%s" % artifact["file"] if artifact["file"] else artifact["file"] |
| http_files.append(" downloaded_file_path = \"%s\"," % download_path) |
| http_files.append(" )") |
| |
| http_files.extend(["maven_artifacts = [\n%s\n]" % (",\n".join([" \"%s\"" % artifact for artifact in maven_artifacts]))]) |
| |
| repository_ctx.file("defs.bzl", "\n".join(http_files), executable = False) |
| repository_ctx.file( |
| "netrc", |
| "\n".join( |
| repository_ctx.attr.additional_netrc_lines + |
| get_home_netrc_contents(repository_ctx).splitlines() + |
| get_netrc_lines_from_entries(netrc_entries), |
| ), |
| executable = False, |
| ) |
| |
| repository_ctx.report_progress("Generating BUILD targets..") |
| (generated_imports, jar_versionless_target_labels) = parser.generate_imports( |
| repository_ctx = repository_ctx, |
| dependencies = importer.get_artifacts(maven_install_json_content), |
| explicit_artifacts = { |
| a["group"] + ":" + a["artifact"] + (":" + a["classifier"] if "classifier" in a else ""): True |
| for a in artifacts |
| }, |
| neverlink_artifacts = { |
| a["group"] + ":" + a["artifact"] + (":" + a["classifier"] if "classifier" in a else ""): True |
| for a in artifacts |
| if a.get("neverlink", False) |
| }, |
| testonly_artifacts = { |
| a["group"] + ":" + a["artifact"] + (":" + a["classifier"] if "classifier" in a else ""): True |
| for a in artifacts |
| if a.get("testonly", False) |
| }, |
| override_targets = repository_ctx.attr.override_targets, |
| skip_maven_local_dependencies = False, |
| ) |
| |
| repository_ctx.template( |
| "compat_repository.bzl", |
| repository_ctx.attr._compat_repository, |
| substitutions = {}, |
| executable = False, |
| ) |
| |
| repository_ctx.file( |
| "BUILD", |
| (_BUILD + _BUILD_PIN_ALIAS + _BUILD_OUTDATED).format( |
| visibilities = ",".join(["\"%s\"" % s for s in (["//visibility:public"] if not repository_ctx.attr.strict_visibility else repository_ctx.attr.strict_visibility_value)]), |
| repository_name = repository_ctx.name, |
| imports = generated_imports, |
| aar_import_statement = _get_aar_import_statement_or_empty_str(repository_ctx), |
| unpinned_pin_target = unpinned_pin_target, |
| ), |
| executable = False, |
| ) |
| |
| _add_outdated_files(repository_ctx, artifacts, repositories) |
| |
| # Generate a compatibility layer of external repositories for all jar artifacts. |
| if repository_ctx.attr.generate_compat_repositories: |
| compat_repositories_bzl = ["load(\"@%s//:compat_repository.bzl\", \"compat_repository\")" % repository_ctx.name] |
| compat_repositories_bzl.append("def compat_repositories():") |
| for versionless_target_label in jar_versionless_target_labels: |
| compat_repositories_bzl.extend([ |
| " compat_repository(", |
| " name = \"%s\"," % versionless_target_label, |
| " generating_repository = \"%s\"," % repository_ctx.name, |
| " )", |
| ]) |
| repository_ctx.file( |
| "compat.bzl", |
| "\n".join(compat_repositories_bzl) + "\n", |
| executable = False, |
| ) |
| |
| def infer_artifact_path_from_primary_and_repos(primary_url, repository_urls): |
| """Returns the artifact path inferred by comparing primary_url with urls in repository_urls. |
| |
| When given a list of repository urls and a primary url that has a repository url as a prefix and a maven artifact |
| path as a suffix, this method will try to determine what the maven artifact path is and return it. |
| |
| This method has some handling for basic http-based auth parsing and will do a url comparison with the user:pass@ |
| portion stripped. |
| |
| Ex. |
| infer_artifact_path_from_primary_and_repos( |
| "http://a:b@c/group/path/to/artifact/file.jar", |
| ["http://c"]) |
| == "group/path/to/artifact/file.jar" |
| |
| Returns: |
| String of the artifact path used by maven to find a particular artifact. Does not have a leading slash (`/`). |
| """ |
| userless_repository_urls = [remove_auth_from_url(r.rstrip("/")) for r in repository_urls] |
| userless_primary_url = remove_auth_from_url(primary_url) |
| primary_artifact_path = None |
| for url in userless_repository_urls: |
| if userless_primary_url.find(url) != -1: |
| primary_artifact_path = userless_primary_url[len(url) + 1:] |
| break |
| return primary_artifact_path |
| |
| def _check_artifacts_are_unique(artifacts, duplicate_version_warning): |
| if duplicate_version_warning == "none": |
| return |
| seen_artifacts = {} |
| duplicate_artifacts = {} |
| for artifact in artifacts: |
| artifact_coordinate = artifact["group"] + ":" + artifact["artifact"] + (":%s" % artifact["classifier"] if artifact.get("classifier") != None else "") |
| if artifact_coordinate in seen_artifacts: |
| # Don't warn if the same version is in the list multiple times |
| if seen_artifacts[artifact_coordinate] != artifact["version"]: |
| if artifact_coordinate in duplicate_artifacts: |
| duplicate_artifacts[artifact_coordinate].append(artifact["version"]) |
| else: |
| duplicate_artifacts[artifact_coordinate] = [artifact["version"]] |
| else: |
| seen_artifacts[artifact_coordinate] = artifact["version"] |
| |
| if duplicate_artifacts: |
| msg_parts = ["Found duplicate artifact versions"] |
| for duplicate in duplicate_artifacts: |
| msg_parts.append(" {} has multiple versions {}".format(duplicate, ", ".join([seen_artifacts[duplicate]] + duplicate_artifacts[duplicate]))) |
| msg_parts.append("Please remove duplicate artifacts from the artifact list so you do not get unexpected artifact versions") |
| if duplicate_version_warning == "error": |
| fail("\n".join(msg_parts)) |
| else: |
| print("\n".join(msg_parts)) |
| |
| # Get the path to the cache directory containing Coursier-downloaded artifacts. |
| # |
| # This method is public for testing. |
| def get_coursier_cache_or_default(repository_ctx, use_unsafe_shared_cache): |
| # If we're not using the unsafe shared cache use 'external/<this repo>/v1/'. |
| # 'v1' is the current version of the Coursier cache. |
| if not use_unsafe_shared_cache: |
| return "v1" |
| |
| os_env = repository_ctx.os.environ |
| coursier_cache_env_var = os_env.get("COURSIER_CACHE") |
| if coursier_cache_env_var: |
| # This is an absolute path. |
| return coursier_cache_env_var |
| |
| # cache locations from https://get-coursier.io/docs/2.0.0-RC5-3/cache.html#default-location |
| # Use linux as the default cache directory |
| default_cache_dir = "%s/.cache/coursier/v1" % os_env.get("HOME") |
| if _is_windows(repository_ctx): |
| default_cache_dir = "%s/Coursier/cache/v1" % os_env.get("LOCALAPPDATA").replace("\\", "/") |
| elif _is_macos(repository_ctx): |
| default_cache_dir = "%s/Library/Caches/Coursier/v1" % os_env.get("HOME") |
| else: |
| # Coursier respects $XDG_CACHE_HOME as a replacement for $HOME/.cache |
| # outside of Windows and macOS. |
| # |
| # - https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables |
| # - https://github.com/dirs-dev/directories-jvm/tree/006ca7ff804ca48f692d59a7fce8599f8a1eadfc#projectdirectories |
| # - https://github.com/coursier/coursier/blob/d5ad55d1dcb025084ba9bd994ea47ceae0608a8f/modules/paths/src/main/java/coursier/paths/CoursierPaths.java#L44-L59 |
| xdg_cache_home = os_env.get("XDG_CACHE_HOME") |
| if xdg_cache_home: |
| return "%s/coursier/v1" % xdg_cache_home |
| |
| # Logic based on # https://github.com/coursier/coursier/blob/f48c1c6b01ac5b720e66e06cf93587b21d030e8c/modules/paths/src/main/java/coursier/paths/CoursierPaths.java#L60 |
| if _is_directory(repository_ctx, default_cache_dir): |
| return default_cache_dir |
| elif _is_directory(repository_ctx, "%s/.coursier" % os_env.get("HOME")): |
| return "%s/.coursier/cache/v1" % os_env.get("HOME") |
| |
| return default_cache_dir |
| |
| def make_coursier_dep_tree( |
| repository_ctx, |
| artifacts, |
| excluded_artifacts, |
| repositories, |
| version_conflict_policy, |
| fail_on_missing_checksum, |
| fetch_sources, |
| fetch_javadoc, |
| timeout, |
| report_progress_prefix = ""): |
| # Set up artifact exclusion, if any. From coursier fetch --help: |
| # |
| # Path to the local exclusion file. Syntax: <org:name>--<org:name>. `--` means minus. Example file content: |
| # com.twitter.penguin:korean-text--com.twitter:util-tunable-internal_2.11 |
| # org.apache.commons:commons-math--com.twitter.search:core-query-nodes |
| # Behavior: If root module A excludes module X, but root module B requires X, module X will still be fetched. |
| artifact_coordinates = [] |
| exclusion_lines = [] |
| forced_versions = [] |
| for a in artifacts: |
| coordinates = utils.artifact_coordinate(a) |
| |
| # Special-case handling of specific versions that are requested. Without this, coursier will claim that |
| # if a lower version of a dependency is requested by a transitive dep than what is specified here, a |
| # version mismatch will occur, and this will fail the resolution. |
| if a.get("force_version", False): |
| forced_versions.append(coordinates) |
| |
| artifact_coordinates.append(coordinates) |
| if "exclusions" in a: |
| for e in a["exclusions"]: |
| exclusion_lines.append(":".join([a["group"], a["artifact"]]) + |
| "--" + |
| ":".join([e["group"], e["artifact"]])) |
| |
| cmd = _generate_java_jar_command(repository_ctx, repository_ctx.path("coursier")) |
| cmd.extend(["fetch"]) |
| |
| cmd.extend(artifact_coordinates) |
| if version_conflict_policy == "pinned": |
| for coord in artifact_coordinates: |
| # Undo any `,classifier=` and/or `,type=` suffix from `utils.artifact_coordinate`. |
| cmd.extend([ |
| "--force-version", |
| ",".join([c for c in coord.split(",") if not c.startswith("classifier=") and not c.startswith("type=")]), |
| ]) |
| else: |
| for coord in forced_versions: |
| cmd.extend([ |
| "--force-version", |
| ",".join([c for c in coord.split(",") if not c.startswith("classifier=") and not c.startswith("type=")]), |
| ]) |
| cmd.extend(["--artifact-type", ",".join(SUPPORTED_PACKAGING_TYPES + ["src", "doc"])]) |
| cmd.append("--verbose" if _is_verbose(repository_ctx) else "--quiet") |
| cmd.append("--no-default") |
| cmd.extend(["--json-output-file", "dep-tree.json"]) |
| |
| if fail_on_missing_checksum: |
| cmd.extend(["--checksum", "SHA-1,MD5"]) |
| else: |
| cmd.extend(["--checksum", "SHA-1,MD5,None"]) |
| |
| if len(exclusion_lines) > 0: |
| repository_ctx.file("exclusion-file.txt", "\n".join(exclusion_lines), False) |
| cmd.extend(["--local-exclude-file", "exclusion-file.txt"]) |
| for repository in repositories: |
| cmd.extend(["--repository", repository["repo_url"]]) |
| if "credentials" in repository: |
| cmd.extend(["--credentials", utils.repo_credentials(repository)]) |
| if repository_ctx.attr.use_credentials_from_home_netrc_file: |
| for credential in utils.netrc_credentials(get_home_netrc_contents(repository_ctx)): |
| cmd.extend(["--credentials", credential]) |
| for a in excluded_artifacts: |
| cmd.extend(["--exclude", ":".join([a["group"], a["artifact"]])]) |
| |
| if fetch_sources or fetch_javadoc: |
| if fetch_sources: |
| cmd.append("--sources") |
| if fetch_javadoc: |
| cmd.append("--javadoc") |
| cmd.append("--default=true") |
| |
| environment = {} |
| if not repository_ctx.attr.name.startswith("unpinned_"): |
| coursier_cache_location = get_coursier_cache_or_default( |
| repository_ctx, |
| False, |
| ) |
| cmd.extend(["--cache", coursier_cache_location]) # Download into $output_base/external/$maven_repo_name/v1 |
| |
| # If not using the shared cache and the user did not specify a COURSIER_CACHE, set the default |
| # value to prevent Coursier from writing into home directories. |
| # https://github.com/bazelbuild/rules_jvm_external/issues/301 |
| # https://github.com/coursier/coursier/blob/1cbbf39b88ee88944a8d892789680cdb15be4714/modules/paths/src/main/java/coursier/paths/CoursierPaths.java#L29-L56 |
| environment = {"COURSIER_CACHE": str(repository_ctx.path(coursier_cache_location))} |
| |
| # If we are using Java 9 or higher we can use an argsfile to avoid command line length limits |
| java_path = _java_path(repository_ctx) |
| if java_path: |
| exec_result = _execute(repository_ctx, [java_path, "-version"]) |
| if (exec_result.return_code != 0): |
| fail("Error while running java -version: " + exec_result.stderr) |
| if parse_java_version(exec_result.stdout + exec_result.stderr) > 8: |
| java_cmd = cmd[0] |
| java_args = cmd[1:] |
| |
| argsfile_content = build_java_argsfile_content(java_args) |
| if _is_verbose(repository_ctx): |
| repository_ctx.execute( |
| ["echo", "\nargsfile_content:\n%s" % argsfile_content], |
| quiet = False, |
| ) |
| |
| repository_ctx.file( |
| "java_argsfile", |
| argsfile_content, |
| executable = False, |
| ) |
| cmd = [java_cmd, "@{}".format(repository_ctx.path("java_argsfile"))] |
| |
| exec_result = _execute( |
| repository_ctx, |
| cmd, |
| timeout = timeout, |
| environment = environment, |
| progress_message = "%sResolving and fetching the transitive closure of %s artifact(s).." % ( |
| report_progress_prefix, |
| len(artifact_coordinates), |
| ), |
| ) |
| if (exec_result.return_code != 0): |
| fail("Error while fetching artifact with coursier: " + exec_result.stderr) |
| |
| return deduplicate_and_sort_artifacts( |
| json.decode(repository_ctx.read(repository_ctx.path("dep-tree.json"))), |
| artifacts, |
| excluded_artifacts, |
| _is_verbose(repository_ctx), |
| ) |
| |
| def remove_prefix(s, prefix): |
| if s.startswith(prefix): |
| return s[len(prefix):] |
| return s |
| |
| def _coursier_fetch_impl(repository_ctx): |
| # Not using maven_install.json, so we resolve and fetch from scratch. |
| # This takes significantly longer as it doesn't rely on any local |
| # caches and uses Coursier's own download mechanisms. |
| |
| # Download Coursier's standalone (deploy) jar from Maven repositories. |
| coursier_download_urls = [ |
| COURSIER_CLI_GITHUB_ASSET_URL, |
| COURSIER_CLI_BAZEL_MIRROR_URL, |
| ] |
| |
| coursier_url_from_env = repository_ctx.os.environ.get("COURSIER_URL") |
| if coursier_url_from_env != None: |
| coursier_download_urls.insert(0, coursier_url_from_env) |
| |
| repository_ctx.download(coursier_download_urls, "coursier", sha256 = COURSIER_CLI_SHA256, executable = True) |
| |
| # Try running coursier once |
| cmd = _generate_java_jar_command(repository_ctx, repository_ctx.path("coursier")) |
| |
| # Add --help because calling the default coursier command on Windows will |
| # hang waiting for input |
| cmd.append("--help") |
| hasher_exec_result = _execute( |
| repository_ctx, |
| cmd, |
| ) |
| if hasher_exec_result.return_code != 0: |
| fail("Unable to run coursier: " + hasher_exec_result.stderr) |
| |
| _windows_check(repository_ctx) |
| |
| # Deserialize the spec blobs |
| repositories = [] |
| for repository in repository_ctx.attr.repositories: |
| repositories.append(json.decode(repository)) |
| |
| artifacts = [] |
| for artifact in repository_ctx.attr.artifacts: |
| artifacts.append(json.decode(artifact)) |
| |
| _check_artifacts_are_unique(artifacts, repository_ctx.attr.duplicate_version_warning) |
| |
| excluded_artifacts = [] |
| for artifact in repository_ctx.attr.excluded_artifacts: |
| excluded_artifacts.append(json.decode(artifact)) |
| |
| # Once coursier finishes a fetch, it generates a tree of artifacts and their |
| # transitive dependencies in a JSON file. We use that as the source of truth |
| # to generate the repository's BUILD file. |
| # |
| # Coursier generates duplicate artifacts sometimes. Deduplicate them using |
| # the file name value as the key. |
| dep_tree = make_coursier_dep_tree( |
| repository_ctx, |
| artifacts, |
| excluded_artifacts, |
| repositories, |
| repository_ctx.attr.version_conflict_policy, |
| repository_ctx.attr.fail_on_missing_checksum, |
| repository_ctx.attr.fetch_sources, |
| repository_ctx.attr.fetch_javadoc, |
| repository_ctx.attr.resolve_timeout, |
| ) |
| |
| files_to_inspect = [] |
| |
| for artifact in dep_tree["dependencies"]: |
| # Some artifacts don't contain files; they are just parent artifacts |
| # to other artifacts. |
| if artifact["file"] == None: |
| continue |
| |
| coord_split = artifact["coord"].split(":") |
| coord_unversioned = "{}:{}".format(coord_split[0], coord_split[1]) |
| |
| # Normalize paths in place here. |
| artifact.update({"file": _normalize_to_unix_path(artifact["file"])}) |
| |
| if is_maven_local_path(artifact["file"]): |
| # This file comes from maven local, so handle it in two different ways depending if |
| # dependency pinning is used: |
| # a) If the repository is unpinned, we keep the file as is, but clear the url to skip it |
| # b) Otherwise, we clear the url and also simlink the file from the maven local directory |
| # to file within the repository rule workspace |
| print("Assuming maven local for artifact: %s" % artifact["coord"]) |
| artifact.update({"url": None}) |
| if not repository_ctx.attr.name.startswith("unpinned_"): |
| artifact.update({"file": _relativize_and_symlink_file_in_maven_local(repository_ctx, artifact["file"])}) |
| |
| files_to_inspect.append(repository_ctx.path(artifact["file"])) |
| continue |
| |
| if repository_ctx.attr.name.startswith("unpinned_"): |
| artifact.update({"file": _relativize_and_symlink_file_in_coursier_cache(repository_ctx, artifact["file"])}) |
| |
| # Coursier saves the artifacts into a subdirectory structure |
| # that mirrors the URL where the artifact's fetched from. Using |
| # this, we can reconstruct the original URL. |
| primary_url_parts = [] |
| |
| # _normalize_to_unix_path uses 2 backslashes, but the paths themselves have a single backslash at this point |
| filepath_parts = _normalize_to_unix_path(artifact["file"]).split("/") |
| protocol = None |
| |
| # Only support http/https transports (or maven local repository) |
| for part in filepath_parts: |
| if part == "http" or part == "https": |
| protocol = part |
| break |
| if protocol == None: |
| fail("Only artifacts downloaded over http(s) are supported: %s - %s (analyzed %s)" % (artifact["coord"], artifact["file"], filepath_parts)) |
| primary_url_parts.extend([protocol, "://"]) |
| for part in filepath_parts[filepath_parts.index(protocol) + 1:]: |
| primary_url_parts.extend([part, "/"]) |
| primary_url_parts.pop() # pop the final "/" |
| |
| # Coursier encodes: |
| # - ':' as '%3A' |
| # - '@' as '%40' |
| # |
| # The primary_url is the url from which Coursier downloaded the jar from. It looks like this: |
| # https://repo1.maven.org/maven2/org/threeten/threetenbp/1.3.3/threetenbp-1.3.3.jar |
| primary_url = "".join(primary_url_parts).replace("%3A", ":").replace("%40", "@") |
| |
| # Coursier prepends the username from the provided credentials if needed to authenticate |
| # with the repository. We remove it from the url and file attributes if only the username is present |
| # and no password, as it has no function and obfuscates changes to the pinned json |
| credential_marker = primary_url.find("@") |
| if credential_marker > -1: |
| potential_credentials = remove_prefix(primary_url[:credential_marker + 1], protocol + "://") |
| if len(potential_credentials.split(":")) == 1: |
| primary_url = primary_url.replace(potential_credentials, "") |
| |
| artifact.update({"url": primary_url}) |
| |
| # The repository for the primary_url has to be one of the repositories provided through |
| # maven_install. Since Maven artifact URLs are standardized, we can make the `http_file` |
| # targets more robust by replicating the primary url for each specified repository url. |
| # |
| # It does not matter if the artifact is on a repository or not, since http_file takes |
| # care of 404s. |
| # |
| # If the artifact does exist, Bazel's HttpConnectorMultiplexer enforces the SHA-256 checksum |
| # is correct. By applying the SHA-256 checksum verification across all the mirrored files, |
| # we get increased robustness in the case where our primary artifact has been tampered with, |
| # and we somehow ended up using the tampered checksum. Attackers would need to tamper *all* |
| # mirrored artifacts. |
| # |
| # See https://github.com/bazelbuild/bazel/blob/77497817b011f298b7f3a1138b08ba6a962b24b8/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpConnectorMultiplexer.java#L103 |
| # for more information on how Bazel's HTTP multiplexing works. |
| # |
| # TODO(https://github.com/bazelbuild/rules_jvm_external/issues/186): Make this work with |
| # basic auth. |
| repository_urls = [] |
| for r in repositories: |
| # filter out m2Local since it's not a valid mirror url |
| if r["repo_url"] != "m2Local": |
| repository_urls.append(r["repo_url"].rstrip("/")) |
| primary_artifact_path = infer_artifact_path_from_primary_and_repos(primary_url, repository_urls) |
| |
| mirror_urls = [url + "/" + primary_artifact_path for url in repository_urls] |
| if primary_url in mirror_urls: |
| # http_file tries URLs in order, so putting the URL that actually worked first |
| # minimizes repository fetch 404s. See: https://github.com/bazelbuild/rules_jvm_external/issues/349 |
| mirror_urls = [primary_url] + [url for url in mirror_urls if url != primary_url] |
| artifact.update({"mirror_urls": mirror_urls}) |
| |
| files_to_inspect.append(repository_ctx.path(artifact["file"])) |
| |
| hasher_stdout = _execute_with_argsfile( |
| repository_ctx, |
| repository_ctx.attr._sha256_hasher, |
| "hasher", |
| "Calculating sha256 checksums..", |
| "obtaining the sha256 checksums", |
| files_to_inspect, |
| ) |
| |
| shas = {} |
| for line in hasher_stdout.splitlines(): |
| parts = line.split(" ") |
| path = str(repository_ctx.path(parts[1])) |
| shas[path] = parts[0] |
| |
| list_packages_stdout = _execute_with_argsfile( |
| repository_ctx, |
| repository_ctx.attr._list_packages, |
| "package_lister", |
| "Indexing jar packages", |
| "indexing jar packages", |
| files_to_inspect, |
| ) |
| |
| jars_to_packages = json.decode(list_packages_stdout) |
| for jar, packages in jars_to_packages.items(): |
| path = str(repository_ctx.path(jar)) |
| if path != jar: |
| jars_to_packages[path] = jars_to_packages.pop(jar) |
| |
| for artifact in dep_tree["dependencies"]: |
| file = artifact["file"] |
| if file == None: |
| continue |
| path = str(repository_ctx.path(file)) |
| |
| if repository_ctx.attr.ignore_empty_files and shas[path] == EMPTY_FILE_SHA256: |
| # Sometimes it happens that coursier sees jar files with 0 bytes. |
| # Treat them as if coursier found no file in the first place. |
| print("Ignoring empty file for artifact: %s" % artifact) |
| artifact["file"] = None |
| |
| # Restore attributes set earlier in this function. |
| if artifact.get("mirror_urls") != None: |
| artifact.pop("mirror_urls") |
| if artifact.get("url") != None: |
| artifact.pop("url") |
| continue |
| artifact.update({"sha256": shas[path]}) |
| artifact.update({"packages": jars_to_packages[path]}) |
| |
| # Keep the original output from coursier for debugging |
| repository_ctx.file( |
| "coursier-deps.json", |
| content = json.encode_indent(dep_tree), |
| ) |
| reformat_lock_file_cmd = _generate_java_jar_command( |
| repository_ctx, |
| repository_ctx.path(repository_ctx.attr._lock_file_converter), |
| ) |
| for repo in repositories: |
| reformat_lock_file_cmd.extend(["--repo", repo["repo_url"]]) |
| reformat_lock_file_cmd.extend(["--json", "coursier-deps.json"]) |
| |
| # But update the format to the latest lock file |
| result = _execute( |
| repository_ctx, |
| cmd = reformat_lock_file_cmd, |
| progress_message = "Updating lock file format", |
| ) |
| if result.return_code: |
| fail("Unable to generate lock file: " + result.stderr) |
| |
| lock_file_contents = json.decode(result.stdout) |
| |
| inputs_hash, _ = compute_dependency_inputs_signature( |
| repository_ctx.attr.artifacts, |
| repository_ctx.attr.repositories, |
| repository_ctx.attr.excluded_artifacts, |
| ) |
| |
| repository_ctx.file( |
| "unsorted_deps.json", |
| content = v2_lock_file.render_lock_file( |
| lock_file_contents, |
| inputs_hash, |
| ), |
| ) |
| |
| repository_ctx.report_progress("Generating BUILD targets..") |
| (generated_imports, jar_versionless_target_labels) = parser.generate_imports( |
| repository_ctx = repository_ctx, |
| dependencies = v2_lock_file.get_artifacts(lock_file_contents), |
| explicit_artifacts = { |
| a["group"] + ":" + a["artifact"] + (":" + a["classifier"] if "classifier" in a else ""): True |
| for a in artifacts |
| }, |
| neverlink_artifacts = { |
| a["group"] + ":" + a["artifact"] + (":" + a["classifier"] if "classifier" in a else ""): True |
| for a in artifacts |
| if a.get("neverlink", False) |
| }, |
| testonly_artifacts = { |
| a["group"] + ":" + a["artifact"] + (":" + a["classifier"] if "classifier" in a else ""): True |
| for a in artifacts |
| if a.get("testonly", False) |
| }, |
| override_targets = repository_ctx.attr.override_targets, |
| # Skip maven local dependencies if generating the unpinned repository |
| skip_maven_local_dependencies = repository_ctx.attr.name.startswith("unpinned_"), |
| ) |
| |
| # This repository rule can be either in the pinned or unpinned state, depending on when |
| # the user invokes artifact pinning. Normalize the repository name here. |
| if repository_ctx.name.startswith("unpinned_"): |
| repository_name = repository_ctx.name[len("unpinned_"):] |
| outdated_build_file_content = "" |
| else: |
| repository_name = repository_ctx.name |
| |
| # Add outdated artifact files if this is a pinned repo |
| outdated_build_file_content = _BUILD_OUTDATED |
| _add_outdated_files(repository_ctx, artifacts, repositories) |
| |
| repository_ctx.file( |
| "BUILD", |
| (_BUILD + _BUILD_PIN + outdated_build_file_content).format( |
| visibilities = ",".join(["\"%s\"" % s for s in (["//visibility:public"] if not repository_ctx.attr.strict_visibility else repository_ctx.attr.strict_visibility_value)]), |
| repository_name = repository_name, |
| imports = generated_imports, |
| aar_import_statement = _get_aar_import_statement_or_empty_str(repository_ctx), |
| ), |
| executable = False, |
| ) |
| |
| # If maven_install.json has already been used in maven_install, |
| # we don't need to instruct user to update WORKSPACE and load pinned_maven_install. |
| # If maven_install.json is not used yet, provide complete instructions. |
| # |
| # Also support custom locations for maven_install.json and update the pin.sh script |
| # accordingly. |
| predefined_maven_install = bool(repository_ctx.attr.maven_install_json) |
| if predefined_maven_install: |
| package_path = repository_ctx.attr.maven_install_json.package |
| file_name = repository_ctx.attr.maven_install_json.name |
| if package_path == "": |
| maven_install_location = file_name # e.g. some.json |
| else: |
| maven_install_location = "/".join([package_path, file_name]) # e.g. path/to/some.json |
| else: |
| # Default maven_install.json file name. |
| maven_install_location = "{repository_name}_install.json" |
| |
| # Expose the script to let users pin the state of the fetch in |
| # `<workspace_root>/maven_install.json`. |
| # |
| # $ bazel run @unpinned_maven//:pin |
| # |
| # Create the maven_install.json export script for unpinned repositories. |
| repository_ctx.template( |
| "pin.sh", |
| repository_ctx.attr._pin, |
| { |
| "{maven_install_location}": "$BUILD_WORKSPACE_DIRECTORY/" + maven_install_location, |
| "{predefined_maven_install}": str(predefined_maven_install), |
| "{repository_name}": repository_name, |
| }, |
| executable = True, |
| ) |
| |
| # Generate 'defs.bzl' with just the dependencies for ':pin'. |
| http_files = [ |
| "load(\"@bazel_tools//tools/build_defs/repo:http.bzl\", \"http_file\")", |
| "load(\"@bazel_tools//tools/build_defs/repo:utils.bzl\", \"maybe\")", |
| "def pinned_maven_install():", |
| " pass", # Ensure we're syntactically correct even if no deps are added |
| ] |
| repository_ctx.file( |
| "defs.bzl", |
| "\n".join(http_files), |
| executable = False, |
| ) |
| |
| # Generate a compatibility layer of external repositories for all jar artifacts. |
| if repository_ctx.attr.generate_compat_repositories: |
| repository_ctx.template( |
| "compat_repository.bzl", |
| repository_ctx.attr._compat_repository, |
| substitutions = {}, |
| executable = False, |
| ) |
| |
| compat_repositories_bzl = ["load(\"@%s//:compat_repository.bzl\", \"compat_repository\")" % repository_ctx.name] |
| compat_repositories_bzl.append("def compat_repositories():") |
| for versionless_target_label in jar_versionless_target_labels: |
| compat_repositories_bzl.extend([ |
| " compat_repository(", |
| " name = \"%s\"," % versionless_target_label, |
| " generating_repository = \"%s\"," % repository_ctx.name, |
| " )", |
| ]) |
| repository_ctx.file( |
| "compat.bzl", |
| "\n".join(compat_repositories_bzl) + "\n", |
| executable = False, |
| ) |
| |
| pinned_coursier_fetch = repository_rule( |
| attrs = { |
| "_compat_repository": attr.label(default = "//private:compat_repository.bzl"), |
| "_outdated": attr.label(default = "//private:outdated.sh"), |
| "user_provided_name": attr.string(), |
| "repositories": attr.string_list(), # list of repository objects, each as json |
| "artifacts": attr.string_list(), # list of artifact objects, each as json |
| "fetch_sources": attr.bool(default = False), |
| "fetch_javadoc": attr.bool(default = False), |
| "generate_compat_repositories": attr.bool(default = False), # generate a compatible layer with repositories for each artifact |
| "maven_install_json": attr.label(allow_single_file = True), |
| "override_targets": attr.string_dict(default = {}), |
| "strict_visibility": attr.bool( |
| doc = """Controls visibility of transitive dependencies. |
| |
| If "True", transitive dependencies are private and invisible to user's rules. |
| If "False", transitive dependencies are public and visible to user's rules. |
| """, |
| default = False, |
| ), |
| "strict_visibility_value": attr.label_list(default = ["//visibility:private"]), |
| "additional_netrc_lines": attr.string_list(doc = "Additional lines prepended to the netrc file used by `http_file` (with `maven_install_json` only).", default = []), |
| "use_credentials_from_home_netrc_file": attr.bool(default = False, doc = "Whether to include coursier credentials gathered from the user home ~/.netrc file"), |
| "fail_if_repin_required": attr.bool(doc = "Whether to fail the build if the maven_artifact inputs have changed but the lock file has not been repinned.", default = False), |
| "use_starlark_android_rules": attr.bool(default = False, doc = "Whether to use the native or Starlark version of the Android rules."), |
| "aar_import_bzl_label": attr.string(default = DEFAULT_AAR_IMPORT_LABEL, doc = "The label (as a string) to use to import aar_import from"), |
| "duplicate_version_warning": attr.string( |
| doc = """What to do if there are duplicate artifacts |
| |
| If "error", then print a message and fail the build. |
| If "warn", then print a warning and continue. |
| If "none", then do nothing. |
| """, |
| default = "warn", |
| values = [ |
| "error", |
| "warn", |
| "none", |
| ], |
| ), |
| "repin_instructions": attr.string( |
| doc = "Instructions to re-pin the repository if required. Many people have wrapper scripts for keeping dependencies up to date, and would like to point users to that instead of the default.", |
| ), |
| "excluded_artifacts": attr.string_list(default = []), # only used for hash generation |
| }, |
| implementation = _pinned_coursier_fetch_impl, |
| ) |
| |
| coursier_fetch = repository_rule( |
| attrs = { |
| "_sha256_hasher": attr.label(default = "//private/tools/prebuilt:hasher_deploy.jar"), |
| "_list_packages": attr.label(default = "//private/tools/prebuilt:list_packages_deploy.jar"), |
| "_lock_file_converter": attr.label(default = "//private/tools/prebuilt:lock_file_converter_deploy.jar"), |
| "_pin": attr.label(default = "//private:pin.sh"), |
| "_compat_repository": attr.label(default = "//private:compat_repository.bzl"), |
| "_outdated": attr.label(default = "//private:outdated.sh"), |
| "user_provided_name": attr.string(), |
| "repositories": attr.string_list(), # list of repository objects, each as json |
| "artifacts": attr.string_list(), # list of artifact objects, each as json |
| "fail_on_missing_checksum": attr.bool(default = True), |
| "fetch_sources": attr.bool(default = False), |
| "fetch_javadoc": attr.bool(default = False), |
| "use_credentials_from_home_netrc_file": attr.bool(default = False, doc = "Whether to include coursier credentials gathered from the user home ~/.netrc file"), |
| "excluded_artifacts": attr.string_list(default = []), # list of artifacts to exclude |
| "generate_compat_repositories": attr.bool(default = False), # generate a compatible layer with repositories for each artifact |
| "version_conflict_policy": attr.string( |
| doc = """Policy for user-defined vs. transitive dependency version conflicts |
| |
| If "pinned", choose the user-specified version in maven_install unconditionally. |
| If "default", follow Coursier's default policy. |
| """, |
| default = "default", |
| values = [ |
| "default", |
| "pinned", |
| ], |
| ), |
| "maven_install_json": attr.label(allow_single_file = True), |
| "override_targets": attr.string_dict(default = {}), |
| "strict_visibility": attr.bool( |
| doc = """Controls visibility of transitive dependencies |
| |
| If "True", transitive dependencies are private and invisible to user's rules. |
| If "False", transitive dependencies are public and visible to user's rules. |
| """, |
| default = False, |
| ), |
| "strict_visibility_value": attr.label_list(default = ["//visibility:private"]), |
| "resolve_timeout": attr.int(default = 600), |
| "use_starlark_android_rules": attr.bool(default = False, doc = "Whether to use the native or Starlark version of the Android rules."), |
| "aar_import_bzl_label": attr.string(default = DEFAULT_AAR_IMPORT_LABEL, doc = "The label (as a string) to use to import aar_import from"), |
| "duplicate_version_warning": attr.string( |
| doc = """What to do if there are duplicate artifacts |
| |
| If "error", then print a message and fail the build. |
| If "warn", then print a warning and continue. |
| If "none", then do nothing. |
| """, |
| default = "warn", |
| values = [ |
| "error", |
| "warn", |
| "none", |
| ], |
| ), |
| "ignore_empty_files": attr.bool(default = False, doc = "Treat jars that are empty as if they were not found."), |
| }, |
| environ = [ |
| "JAVA_HOME", |
| "http_proxy", |
| "HTTP_PROXY", |
| "https_proxy", |
| "HTTPS_PROXY", |
| "no_proxy", |
| "NO_PROXY", |
| "COURSIER_CACHE", |
| "COURSIER_OPTS", |
| "COURSIER_URL", |
| "RJE_VERBOSE", |
| "XDG_CACHE_HOME", |
| ], |
| implementation = _coursier_fetch_impl, |
| ) |