blob: d7195fd681b6cd0634e0ca7c5864e5147ad90e4a [file]
"""A simple whl extractor."""
load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config")
load("//python/private:repo_utils.bzl", "repo_utils")
load(":whl_metadata.bzl", "find_whl_metadata")
# Mapping of wheel .data categories to their extraction destination (relative to
# repository root).
_DATA_CATEGORIES = {
# category: repo_dest_dir
"data": "data",
"headers": "include",
# In theory there may be directory collisions in platlib/purelib, so it is
# best to merge the paths here. What is more, this code has to be reasonably
# efficient because some packages like to explicitly indicate if something
# is in `platlib` or `purelib` (e.g. libclang wheel).
"platlib": "site-packages",
"purelib": "site-packages",
"scripts": "bin",
}
def whl_extract(rctx, *, whl_path, logger):
"""Extract whls in Starlark.
Args:
rctx: the repository ctx.
whl_path: the whl path to extract.
logger: The logger to use
"""
install_dir_path = rctx.path("site-packages")
repo_utils.extract(
rctx,
archive = whl_path,
output = install_dir_path,
supports_whl_extraction = rp_config.supports_whl_extraction,
extract_needs_chmod = rp_config.extract_needs_chmod,
)
metadata_file = find_whl_metadata(
install_dir = install_dir_path,
logger = logger,
)
# Get the <prefix>.dist_info dir name
dist_info_dir = metadata_file.dirname
rctx.file(
dist_info_dir.get_child("INSTALLER"),
"https://github.com/bazel-contrib/rules_python#pipstar",
)
# Get the <prefix>.dist_info dir name
data_dir = dist_info_dir.dirname.get_child(
dist_info_dir.basename[:-len(".dist-info")] + ".data",
)
if data_dir.exists:
for prefix, dest_prefix in _DATA_CATEGORIES.items():
src = data_dir.get_child(prefix)
if not src.exists:
# The prefix does not exist in the wheel, we can continue
continue
dest_dir = rctx.path(dest_prefix)
repo_utils.mkdir(rctx, dest_dir)
for (src, dest) in merge_trees(src, dest_dir):
logger.debug(lambda: "Renaming: {} -> {}".format(src, dest))
repo_utils.rename(rctx, src, dest)
# Move RECORD to rewrite-record so gen_wheel_record can generate
# the platform-specific RECORD file at build time.
record_file = dist_info_dir.get_child("RECORD")
if record_file.exists:
rewrite_record_dir = rctx.path(
"rewrite-record/" + dist_info_dir.basename,
)
repo_utils.mkdir(rctx, rewrite_record_dir)
repo_utils.rename(
rctx,
record_file,
rewrite_record_dir.get_child("RECORD"),
)
# Ensure that there is no data dir left
rctx.delete(data_dir)
def merge_trees(src, dest):
"""Merge src into the destination path.
This will attempt to merge-move src files to the destination directory if there are
existing files. Fails at directory depth is 10000 or if there are collisions.
Args:
src: {type}`path` a src path to rename.
dest: {type}`path` a dest path to rename to.
Returns:
A list of tuples for src and destination paths.
"""
ret = []
remaining = [(src, dest)]
collisions = []
for _ in range(10000):
if collisions or not remaining:
break
tmp = []
for (s, d) in remaining:
if not d.exists:
ret.append((s, d))
continue
if not s.is_dir or not d.is_dir:
collisions.append(s)
continue
for file_or_dir in s.readdir():
tmp.append((file_or_dir, d.get_child(file_or_dir.basename)))
remaining = tmp
if remaining:
fail("Exceeded maximum directory depth of 10000 during tree merge.")
if collisions:
fail("Detected collisions between {} and {}: {}".format(src, dest, collisions))
return ret