Skip to content

Commit

Permalink
Add entry_points as py_binary's (#74)
Browse files Browse the repository at this point in the history
* Add entry_points as py_binary's
* Add test for generated binary
  • Loading branch information
fahhem authored May 31, 2022
1 parent 0d0497a commit ed61e16
Show file tree
Hide file tree
Showing 16 changed files with 1,408 additions and 0 deletions.
6 changes: 6 additions & 0 deletions examples/tests/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,9 @@ py_extension(
requirement("numpy", "//:headers"),
],
)

sh_test(
name = "test_binary",
srcs = ["test_binary.sh"],
data = [requirement("chardet", "//:chardetect")],
)
3 changes: 3 additions & 0 deletions examples/tests/test_binary.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash
set -e
$TEST_SRCDIR/pypi__38__chardet_3_0_4/chardetect --help > /dev/null
41 changes: 41 additions & 0 deletions src/whl.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pip._vendor import pkg_resources

import pkginfo
import installer


# https://github.com/dillon-giacoppo/rules_python_external/blob/master/tools/wheel_wrapper.py
Expand Down Expand Up @@ -165,6 +166,21 @@ def _get_numpy_headers(directory):
)


def get_entry_points(directory):
dist_info = glob.glob(os.path.join(directory, "*.dist-info"))[0]
entry_points_path = os.path.join(dist_info, "entry_points.txt")
entry_points_mapping = {}
if not os.path.exists(entry_points_path):
return entry_points_mapping

with open(entry_points_path) as f:
entry_points = installer.utils.parse_entrypoints(f.read())
for script, module, attribute, script_section in entry_points:
if script_section == "console":
entry_points_mapping[script] = (module, attribute)
return entry_points_mapping


def main():
logging.basicConfig()
parser = argparse.ArgumentParser(
Expand Down Expand Up @@ -223,6 +239,28 @@ def main():
for extra in args.extras or []
]

entry_point_list = [
"""
genrule(
name = "copy_{script}",
srcs = ["bin/{script}"],
outs = ["bin/{script}.py"],
cmd = "cp $(SRCS) $(OUTS)",
)
py_binary(
name = "{script}",
srcs = ["bin/{script}.py"],
imports = ["."],
deps = [":pkg"],
)
""".format(
script=script
)
for script, val in get_entry_points(args.directory).items()
]
entry_points_str = "\n".join(entry_point_list)

# we treat numpy in a special way, inject a rule for numpy headers
if args.package == "numpy":
extras_list.append(_get_numpy_headers(args.directory))
Expand Down Expand Up @@ -260,6 +298,8 @@ def main():
srcs = glob(["*.dist-info/**"]),
)
{entry_points}
{extras}""".format(
requirements=args.requirements,
dependencies=",\n ".join(
Expand All @@ -268,6 +308,7 @@ def main():
for d in dependencies(pkg)
]
),
entry_points=entry_points_str,
extras=extras,
)

Expand Down
19 changes: 19 additions & 0 deletions third_party/py/installer-0.5.1.dist-info/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Copyright (c) 2020 Pradyun Gedam

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
28 changes: 28 additions & 0 deletions third_party/py/installer-0.5.1.dist-info/METADATA
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Metadata-Version: 2.1
Name: installer
Version: 0.5.1
Summary: A library for installing Python wheels.
Author-email: Pradyun Gedam <[email protected]>
Requires-Python: >=3.7
Description-Content-Type: text/markdown
Classifier: License :: OSI Approved :: MIT License
Project-URL: GitHub, https://github.com/pypa/installer

# installer

<!-- start readme-pitch -->

This is a low-level library for installing a Python package from a
[wheel distribution](https://packaging.python.org/glossary/#term-Wheel).
It provides basic functionality and abstractions for handling wheels and
installing packages from wheels.

- Logic for "unpacking" a wheel (i.e. installation).
- Abstractions for various parts of the unpacking process.
- Extensible simple implementations of the abstractions.
- Platform-independent Python script wrapper generation.

<!-- end readme-pitch -->

You can read more in the [documentation](https://installer.rtfd.io/).

4 changes: 4 additions & 0 deletions third_party/py/installer-0.5.1.dist-info/WHEEL
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Wheel-Version: 1.0
Generator: flit 3.2.0
Root-Is-Purelib: true
Tag: py3-none-any
6 changes: 6 additions & 0 deletions third_party/py/installer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""A library for installing Python wheels."""

__version__ = "0.5.1"
__all__ = ["install"]

from installer._core import install # noqa
85 changes: 85 additions & 0 deletions third_party/py/installer/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Installer CLI."""

import argparse
import os.path
import sys
import sysconfig
from typing import Dict, Optional, Sequence

import installer
from installer.destinations import SchemeDictionaryDestination
from installer.sources import WheelFile
from installer.utils import get_launcher_kind


def _get_main_parser() -> argparse.ArgumentParser:
"""Construct the main parser."""
parser = argparse.ArgumentParser()
parser.add_argument("wheel", type=str, help="wheel file to install")
parser.add_argument(
"--destdir",
"-d",
metavar="path",
type=str,
help="destination directory (prefix to prepend to each file)",
)
parser.add_argument(
"--compile-bytecode",
action="append",
metavar="level",
type=int,
choices=[0, 1, 2],
help="generate bytecode for the specified optimization level(s) (default=0, 1)",
)
parser.add_argument(
"--no-compile-bytecode",
action="store_true",
help="don't generate bytecode for installed modules",
)
return parser


def _get_scheme_dict(distribution_name: str) -> Dict[str, str]:
"""Calculate the scheme dictionary for the current Python environment."""
scheme_dict = sysconfig.get_paths()

installed_base = sysconfig.get_config_var("base")
assert installed_base

# calculate 'headers' path, not currently in sysconfig - see
# https://bugs.python.org/issue44445. This is based on what distutils does.
# TODO: figure out original vs normalised distribution names
scheme_dict["headers"] = os.path.join(
sysconfig.get_path("include", vars={"installed_base": installed_base}),
distribution_name,
)

return scheme_dict


def _main(cli_args: Sequence[str], program: Optional[str] = None) -> None:
"""Process arguments and perform the install."""
parser = _get_main_parser()
if program:
parser.prog = program
args = parser.parse_args(cli_args)

bytecode_levels = args.compile_bytecode
if args.no_compile_bytecode:
bytecode_levels = []
elif not bytecode_levels:
bytecode_levels = [0, 1]

with WheelFile.open(args.wheel) as source:
destination = SchemeDictionaryDestination(
scheme_dict=_get_scheme_dict(source.distribution),
interpreter=sys.executable,
script_kind=get_launcher_kind(),
bytecode_optimization_levels=bytecode_levels,
destdir=args.destdir,
)
installer.install(source, destination, {})


if __name__ == "__main__": # pragma: no cover
_main(sys.argv[1:], "python -m installer")
135 changes: 135 additions & 0 deletions third_party/py/installer/_core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""Core wheel installation logic."""

import posixpath
from io import BytesIO
from typing import Dict, Tuple, cast

from installer.destinations import WheelDestination
from installer.exceptions import InvalidWheelSource
from installer.records import RecordEntry
from installer.sources import WheelSource
from installer.utils import SCHEME_NAMES, Scheme, parse_entrypoints, parse_metadata_file

__all__ = ["install"]


def _process_WHEEL_file(source: WheelSource) -> Scheme:
"""Process the WHEEL file, from ``source``.
Returns the scheme that the archive root should go in.
"""
stream = source.read_dist_info("WHEEL")
metadata = parse_metadata_file(stream)

# Ensure compatibility with this wheel version.
if not (metadata["Wheel-Version"] and metadata["Wheel-Version"].startswith("1.")):
message = "Incompatible Wheel-Version {}, only support version 1.x wheels."
raise InvalidWheelSource(source, message.format(metadata["Wheel-Version"]))

# Determine where archive root should go.
if metadata["Root-Is-Purelib"] == "true":
return cast(Scheme, "purelib")
else:
return cast(Scheme, "platlib")


def _determine_scheme(
path: str, source: WheelSource, root_scheme: Scheme
) -> Tuple[Scheme, str]:
"""Determine which scheme to place given path in, from source."""
data_dir = source.data_dir

# If it's in not `{distribution}-{version}.data`, then it's in root_scheme.
if posixpath.commonprefix([data_dir, path]) != data_dir:
return root_scheme, path

# Figure out which scheme this goes to.
parts = []
scheme_name = None
left = path
while True:
left, right = posixpath.split(left)
parts.append(right)
if left == source.data_dir:
scheme_name = right
break

if scheme_name not in SCHEME_NAMES:
msg_fmt = "{path} is not contained in a valid .data subdirectory."
raise InvalidWheelSource(source, msg_fmt.format(path=path))

return cast(Scheme, scheme_name), posixpath.join(*reversed(parts[:-1]))


def install(
source: WheelSource,
destination: WheelDestination,
additional_metadata: Dict[str, bytes],
) -> None:
"""Install wheel described by ``source`` into ``destination``.
:param source: wheel to install.
:param destination: where to write the wheel.
:param additional_metadata: additional metadata files to generate, usually
generated by the installer.
"""
root_scheme = _process_WHEEL_file(source)

# RECORD handling
record_file_path = posixpath.join(source.dist_info_dir, "RECORD")
written_records = []

# Write the entry_points based scripts.
if "entry_points.txt" in source.dist_info_filenames:
entrypoints_text = source.read_dist_info("entry_points.txt")
for name, module, attr, section in parse_entrypoints(entrypoints_text):
record = destination.write_script(
name=name,
module=module,
attr=attr,
section=section,
)
written_records.append((Scheme("scripts"), record))

# Write all the files from the wheel.
for record_elements, stream, is_executable in source.get_contents():
source_record = RecordEntry.from_elements(*record_elements)
path = source_record.path
# Skip the RECORD, which is written at the end, based on this info.
if path == record_file_path:
continue

# Figure out where to write this file.
scheme, destination_path = _determine_scheme(
path=path,
source=source,
root_scheme=root_scheme,
)
record = destination.write_file(
scheme=scheme,
path=destination_path,
stream=stream,
is_executable=is_executable,
)
written_records.append((scheme, record))

# Write all the installation-specific metadata
for filename, contents in additional_metadata.items():
path = posixpath.join(source.dist_info_dir, filename)

with BytesIO(contents) as other_stream:
record = destination.write_file(
scheme=root_scheme,
path=path,
stream=other_stream,
is_executable=is_executable,
)
written_records.append((root_scheme, record))

written_records.append((root_scheme, RecordEntry(record_file_path, None, None)))
destination.finalize_installation(
scheme=root_scheme,
record_file_path=record_file_path,
records=written_records,
)
1 change: 1 addition & 0 deletions third_party/py/installer/_scripts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Internal package, containing launcher templates for ``installer.scripts``."""
Loading

0 comments on commit ed61e16

Please sign in to comment.