|
| 1 | +"" |
| 2 | + |
| 3 | +load("//python/private:normalize_name.bzl", "normalize_name") |
| 4 | +load("//python/private:repo_utils.bzl", "repo_utils") |
| 5 | +load(":generate_whl_library_build_bazel.bzl", "generate_whl_library_build_bazel") |
| 6 | +load(":patch_whl.bzl", "patch_whl") |
| 7 | +load(":pep508_requirement.bzl", "requirement") |
| 8 | +load(":pypi_repo_utils.bzl", "pypi_repo_utils") |
| 9 | +load(":whl_extract.bzl", "whl_extract") |
| 10 | +load(":whl_metadata.bzl", "parse_entry_points", "whl_metadata") |
| 11 | + |
| 12 | +def _get_entry_points(rctx, install_dir_path, metadata): |
| 13 | + dist_info_dir = "{}-{}.dist-info".format( |
| 14 | + metadata.name.replace("-", "_"), |
| 15 | + metadata.version.replace("-", "_"), |
| 16 | + ) |
| 17 | + entry_points_txt = install_dir_path.get_child(dist_info_dir).get_child("entry_points.txt") |
| 18 | + if entry_points_txt.exists: |
| 19 | + return parse_entry_points(rctx.read(entry_points_txt)) |
| 20 | + return {} |
| 21 | + |
| 22 | +def _move_scripts_needing_shebang_rewrite(rctx, entry_points): |
| 23 | + bin_dir = rctx.path("bin") |
| 24 | + if not bin_dir.exists: |
| 25 | + return |
| 26 | + |
| 27 | + ep_names = {name.lower(): True for name in entry_points} |
| 28 | + for script in bin_dir.readdir(): |
| 29 | + if script.is_dir: |
| 30 | + continue |
| 31 | + if script.basename.lower() in ep_names: |
| 32 | + rctx.delete(script) |
| 33 | + continue |
| 34 | + if script.basename.endswith(".exe") or script.basename.endswith(".dll"): |
| 35 | + continue |
| 36 | + content = rctx.read(script) |
| 37 | + if content.startswith("#!python"): |
| 38 | + rewrite_bin_dir = rctx.path("rewrite-bin") |
| 39 | + repo_utils.mkdir(rctx, rewrite_bin_dir) |
| 40 | + repo_utils.rename(rctx, script, rctx.path("rewrite-bin/" + script.basename)) |
| 41 | + |
| 42 | +def _to_purl(*, index, metadata, filename): |
| 43 | + """ |
| 44 | + Produce a PyPI PURL from the metadata. |
| 45 | +
|
| 46 | + https://github.com/package-url/purl-spec/blob/main/types-doc/pypi-definition.md |
| 47 | + """ |
| 48 | + |
| 49 | + # https://github.com/package-url/purl-spec/blob/main/types-doc/pypi-definition.md#name-definition |
| 50 | + name = normalize_name(metadata.name).replace("_", "-") |
| 51 | + |
| 52 | + qualifiers = {} |
| 53 | + if index: |
| 54 | + qualifiers["repository_url"] = index |
| 55 | + if filename: |
| 56 | + qualifiers["file_name"] = filename |
| 57 | + |
| 58 | + return "pkg:pypi/{}@{}?{}".format(name, metadata.version, "&".join(["{}={}".format(key, val) for key, val in qualifiers.items()])) |
| 59 | + |
| 60 | +def _remove_files(rctx, *basenames): |
| 61 | + paths = list(rctx.path(".").readdir()) |
| 62 | + for _ in range(10000000): |
| 63 | + if not paths: |
| 64 | + break |
| 65 | + path = paths.pop() |
| 66 | + |
| 67 | + if path.basename in basenames: |
| 68 | + rctx.delete(path) |
| 69 | + elif path.is_dir: |
| 70 | + paths.extend(path.readdir()) |
| 71 | + |
| 72 | +def patch_and_extract_whl(rctx, *, whl_path, logger, sdist_filename = None): |
| 73 | + """Extract the wheel, apply patches and generate BUILD.bazel files. |
| 74 | +
|
| 75 | + Reused in pip and http wheel download code. |
| 76 | +
|
| 77 | + Args: |
| 78 | + rctx: the repository ctx. |
| 79 | + whl_path: the whl path to extract. |
| 80 | + logger: The logger to use |
| 81 | + sdist_filename: The filename to ignore in the BUILD.bazel files as sources. |
| 82 | +
|
| 83 | + Returns: |
| 84 | + The repository metadata if the extraction is reproducible |
| 85 | + """ |
| 86 | + if rctx.attr.whl_patches: |
| 87 | + patches = {} |
| 88 | + for patch_file, json_args in rctx.attr.whl_patches.items(): |
| 89 | + patch_dst = struct(**json.decode(json_args)) |
| 90 | + if whl_path.basename in patch_dst.whls: |
| 91 | + patches[patch_file] = patch_dst.patch_strip |
| 92 | + |
| 93 | + if patches: |
| 94 | + whl_path = patch_whl( |
| 95 | + rctx, |
| 96 | + whl_path = whl_path, |
| 97 | + patches = patches, |
| 98 | + ) |
| 99 | + |
| 100 | + whl_extract(rctx, whl_path = whl_path, logger = logger) |
| 101 | + |
| 102 | + install_dir_path = whl_path.dirname.get_child("site-packages") |
| 103 | + metadata = whl_metadata( |
| 104 | + install_dir = install_dir_path, |
| 105 | + read_fn = rctx.read, |
| 106 | + logger = logger, |
| 107 | + ) |
| 108 | + rctx.file("metadata.json", json.encode_indent({ |
| 109 | + "name": metadata.name, |
| 110 | + "provides_extra": metadata.provides_extra, |
| 111 | + "requires_dist": metadata.requires_dist, |
| 112 | + "version": metadata.version, |
| 113 | + })) |
| 114 | + namespace_package_files = pypi_repo_utils.find_namespace_package_files(rctx, install_dir_path) |
| 115 | + |
| 116 | + entry_points = _get_entry_points(rctx, install_dir_path, metadata) |
| 117 | + _move_scripts_needing_shebang_rewrite(rctx, entry_points) |
| 118 | + |
| 119 | + build_file_contents = generate_whl_library_build_bazel( |
| 120 | + name = whl_path.basename, |
| 121 | + dep_template = rctx.attr.dep_template, |
| 122 | + sdist_filename = sdist_filename, |
| 123 | + config_load = rctx.attr.config_load, |
| 124 | + metadata_name = metadata.name, |
| 125 | + metadata_version = metadata.version, |
| 126 | + requires_dist = metadata.requires_dist, |
| 127 | + # TODO @aignas 2025-05-17: maybe have a build flag for this instead |
| 128 | + enable_implicit_namespace_pkgs = rctx.attr.enable_implicit_namespace_pkgs, |
| 129 | + # TODO @aignas 2025-04-14: load through the hub: |
| 130 | + annotation = None if not rctx.attr.annotation else struct(**json.decode(rctx.read(rctx.attr.annotation))), |
| 131 | + data_exclude = rctx.attr.pip_data_exclude, |
| 132 | + group_deps = rctx.attr.group_deps, |
| 133 | + group_name = rctx.attr.group_name, |
| 134 | + namespace_package_files = namespace_package_files, |
| 135 | + extras = requirement(rctx.attr.requirement).extras, |
| 136 | + entry_points = entry_points, |
| 137 | + purl = _to_purl( |
| 138 | + index = rctx.attr.index_url, |
| 139 | + metadata = metadata, |
| 140 | + filename = sdist_filename or whl_path.basename, |
| 141 | + ), |
| 142 | + ) |
| 143 | + |
| 144 | + # Delete these in case the wheel had them. They generally don't cause |
| 145 | + # a problem, but let's avoid the chance of that happening. |
| 146 | + rctx.file("WORKSPACE") |
| 147 | + rctx.file("WORKSPACE.bazel") |
| 148 | + rctx.file("MODULE.bazel") |
| 149 | + rctx.file("REPO.bazel", """\ |
| 150 | +repo( |
| 151 | + default_package_metadata = [ |
| 152 | + "//:package_metadata", |
| 153 | + ], |
| 154 | +) |
| 155 | +""") |
| 156 | + |
| 157 | + # BUILD files interfere with globbing and Bazel package boundaries. |
| 158 | + _remove_files(rctx, "BUILD", "BUILD.bazel") |
| 159 | + rctx.file("BUILD.bazel", build_file_contents) |
| 160 | + |
| 161 | + if hasattr(rctx, "repo_metadata"): |
| 162 | + return rctx.repo_metadata(reproducible = True) |
| 163 | + |
| 164 | + return None |
0 commit comments