@@ -477,7 +477,7 @@ def pip_library(name:str, version:str, labels:list=[], hashes:list=None, package
477477def python_wheel(name:str, version:str, labels:list=[], hashes:list=None, package_name:str=None,
478478 outs:list=None, post_install_commands:list=None, patch:str|list=None, licences:list=None,
479479 test_only:bool&testonly=False, repo:str=None, zip_safe:bool=True, visibility:list=None,
480- deps:list=[], name_scheme:str=None, strip:list=['*.pyc', 'tests']):
480+ deps:list=[], name_scheme:str=None, strip:list=['*.pyc', 'tests'], binary = False, entry_points={} ):
481481 """Downloads a Python wheel and extracts it.
482482
483483 This is a lightweight pip-free alternative to pip_library which supports cross-compiling.
@@ -509,7 +509,15 @@ def python_wheel(name:str, version:str, labels:list=[], hashes:list=None, packag
509509 name_scheme (str): The templatized wheel naming scheme (available template variables
510510 are `url_base`, `package_name`, `initial`, and `version`).
511511 strip (list): Files to strip after install. Note that these are done at any level.
512+ binary (bool): Whether this wheel should be executable. This assumes that the wheel will contain a __main__ module
513+ with a main() function. If this is not the case, then entry_points should be used.
514+ entry_points (dict|str): Any entry points into this wheel. These relate to the entrypoints.txt in the dist-info of
515+ the wheel, which define a module, and function in the format "module.path:function".
516+ This parameter can be a string, in which case the rule can be ran directly, or a
517+ dictionary, for example `{"protoc", "protoc.__main__:main"}. For the latter, each key can
518+ be ran using annotated labels, e.g. `plz run //third_party/python:protobuf|protoc`
512519 """
520+ binary = binary or entry_points
513521 package_name = package_name or name.replace('-', '_')
514522 initial = package_name[0]
515523 url_base = repo or CONFIG.PYTHON.WHEEL_REPO
@@ -590,8 +598,10 @@ def python_wheel(name:str, version:str, labels:list=[], hashes:list=None, packag
590598 before, _, after = outs[0].partition('/')
591599 if after:
592600 cmd = f'rm -rf {before} && {cmd}'
593- return build_rule(
601+
602+ lib_rule = build_rule(
594603 name = name,
604+ tag = "lib_rule" if binary else None,
595605 srcs = [wheel_rule],
596606 hashes = None if CONFIG.FF_PYTHON_WHEEL_HASHING else hashes,
597607 outs = outs or [name],
@@ -604,7 +614,52 @@ def python_wheel(name:str, version:str, labels:list=[], hashes:list=None, packag
604614 labels = labels + [label] + ["link:plz-out/python/venv"],
605615 provides = {'py': wheel_rule},
606616 )
607-
617+ if binary:
618+ entry_points = entry_points or f"{package_name}.__main__:main"
619+ if isinstance(entry_points, str):
620+ return _wheel_entrypoint_binary(
621+ name = name,
622+ entrypoint = entry_points,
623+ lib_rule = lib_rule,
624+ visibility = visibility,
625+ test_only = test_only,
626+ )
627+
628+ entry_point_binaries = [_wheel_entrypoint_binary(
629+ name = tag(name, f"{alias}_bin"),
630+ entrypoint = ep,
631+ lib_rule = lib_rule,
632+ visibility = visibility,
633+ test_only = test_only,
634+ out = f"{alias}.pex",
635+ ) for alias, ep in entry_points.items()]
636+
637+ return filegroup(
638+ name = name,
639+ srcs = entry_point_binaries,
640+ entry_points = {k: f"{k}.pex" for k in entry_points.keys()},
641+ provides = {'py': wheel_rule}, # So things can still depend on this as a library
642+ binary=True,
643+ test_only=test_only,
644+ )
645+ return lib_rule
646+
647+ def _wheel_entrypoint_binary(name:str, entrypoint:str, lib_rule, visibility, test_only, out=None):
648+ module, _, func = entrypoint.rpartition(":")
649+ main = text_file(
650+ name = tag(name, "main"),
651+ out = f"__{name}_main__.py",
652+ content = f"import {module} as m\nm.{func}()",
653+ test_only = test_only,
654+ )
655+ return python_binary(
656+ name = name,
657+ main = main,
658+ out = out,
659+ deps = [lib_rule],
660+ test_only = test_only,
661+ visibility = visibility,
662+ )
608663
609664def _interpreter_cmd(interpreter:str):
610665 return f'$(out {interpreter})' if interpreter.startswith('//') or interpreter.startswith(':') else interpreter
0 commit comments