github.com/tiagovtristao/plz@v13.4.0+incompatible/src/parse/rules/python_rules.build_defs (about)

     1  """ Rules to build Python code.
     2  
     3  The output artifacts for Python rules are .pex files (see https://github.com/pantsbuild/pex).
     4  Pex is a rather nice system for combining Python code and all needed dependencies
     5  (excluding the actual interpreter and possibly some system level bits) into a single file.
     6  
     7  The process of compiling pex files can be a little slow when including many large files, as
     8  often happens when one's binary includes large compiled dependencies (eg. numpy...). Hence
     9  we have a fairly elaborate optimisation whereby each python_library rule builds a little
    10  zipfile containing just its sources, and all of those are combined at the end to produce
    11  the final .pex. This builds at roughly the same pace for a clean build of a single target,
    12  but is drastically faster for building many targets with similar dependencies or rebuilding
    13  a target which has only had small changes.
    14  """
    15  
    16  
    17  def python_library(name:str, srcs:list=[], resources:list=[], deps:list=[], visibility:list=None,
    18                     test_only:bool&testonly=False, zip_safe:bool=True, labels:list&features&tags=[], interpreter:str=None,
    19                     strip:bool=False):
    20      """Generates a Python library target, which collects Python files for use by dependent rules.
    21  
    22      Note that each python_library performs some pre-zipping of its inputs before they're combined
    23      in a python_binary or python_test. Hence while it's of course not required that all dependencies
    24      of those rules are python_library rules, it's often a good idea to wrap any large dependencies
    25      in one to improve incrementality (not necessary for pip_library, of course).
    26  
    27      Args:
    28        name (str): Name of the rule.
    29        srcs (list): Python source files for this rule.
    30        resources (list): Non-Python files that this rule collects which will be included in the final .pex.
    31                          The distinction between this and srcs is fairly arbitrary and historical, but
    32                          semantically quite nice and parallels python_test.
    33        deps (list): Dependencies of this rule.
    34        visibility (list): Visibility specification.
    35        test_only (bool): If True, can only be depended on by tests.
    36        zip_safe (bool): Should be set to False if this library can't be safely run inside a .pex
    37                         (the most obvious reason not is when it contains .so modules).
    38                         See python_binary for more information.
    39        labels (list): Labels to apply to this rule.
    40        interpreter (str): The Python interpreter to use. Defaults to the config setting
    41                           which is normally just 'python', but could be 'python3' or
    42                           'pypy' or whatever.
    43        strip (bool): If True, the original sources are stripped and only bytecode is output.
    44      """
    45      if not zip_safe:
    46          labels.append('py:zip-unsafe')
    47      if srcs or resources:
    48          cmd = '$TOOLS_JARCAT z -d -o ${OUTS} -i .'
    49          interpreter = interpreter or CONFIG.DEFAULT_PYTHON_INTERPRETER
    50          if srcs:
    51              # This is a bit of a hack, but rather annoying. We want to put bytecode in its 'legacy' location
    52              # in python3 because zipimport doesn't look in __pycache__. Unfortunately the flag doesn't exist
    53              # in python2 so we have to guess whether we should apply it or not.
    54              bytecode_flag = '-b' if 'python3' in interpreter or 'pypy3' in interpreter else ''
    55              compile_cmd = f'$TOOLS_INT -S -m compileall {bytecode_flag} -f $SRCS_SRCS'
    56              if strip:
    57                  cmd = ' && '.join([compile_cmd, 'rm -f $SRCS_SRCS', cmd])
    58              else:
    59                  cmd = ' && '.join([compile_cmd, cmd])
    60          # Pre-zip the files for later collection by python_binary.
    61          zip_rule = build_rule(
    62              name=name,
    63              tag='zip',
    64              srcs={
    65                  'SRCS': srcs,
    66                  'RES': resources,
    67              },
    68              outs=[f'.{name}.pex.zip'],
    69              cmd=cmd,
    70              building_description='Compressing...',
    71              requires=['py'],
    72              test_only=test_only,
    73              output_is_complete=True,
    74              tools={
    75                  'int': [interpreter],
    76                  'jarcat': [CONFIG.JARCAT_TOOL],
    77              },
    78          )
    79          deps.append(zip_rule)
    80      elif strip:
    81          raise ParseError("Can't pass strip=True to a python_library with no srcs")
    82  
    83      return filegroup(
    84          name=name,
    85          srcs=resources if strip else (srcs + resources),
    86          deps=deps,
    87          visibility=visibility,
    88          output_is_complete=False,
    89          requires=['py'],
    90          test_only=test_only,
    91          labels=labels,
    92      )
    93  
    94  
    95  def python_binary(name:str, main:str, resources:list=[], out:str=None, deps:list=[],
    96                    visibility:list=None, zip_safe:bool=None, strip:bool=False, interpreter:str=None,
    97                    shebang:str='', labels:list&features&tags=[]):
    98      """Generates a Python binary target.
    99  
   100      This compiles all source files together into a single .pex file which can
   101      be easily copied or deployed. The construction of the .pex is done in parts
   102      by the dependent python_library rules, and this rule simply builds the
   103      metadata for it and concatenates them all together.
   104  
   105      Args:
   106        name (str): Name of the rule.
   107        main (str): Python file which is the entry point and __main__ module.
   108        resources (list): List of static resources to include in the .pex.
   109        out (str): Name of the output file. Default to name + .pex
   110        deps (list): Dependencies of this rule.
   111        visibility (list): Visibility specification.
   112        zip_safe (bool): Allows overriding whether the output is marked zip safe or not.
   113                         If set to explicitly True or False, the output will be marked
   114                         appropriately; by default it will be safe unless any of the
   115                         transitive dependencies are themselves marked as not zip-safe.
   116        strip (bool): Strips source code from the output .pex file, leaving just bytecode.
   117        interpreter (str): The Python interpreter to use. Defaults to the config setting
   118                           which is normally just 'python', but could be 'python3' or
   119                           'pypy' or whatever.
   120        shebang (str): Exact shebang to apply to the generated file. By default we will
   121                       determine something appropriate for the given interpreter.
   122        labels (list): Labels to apply to this rule.
   123      """
   124      shebang = shebang or interpreter or CONFIG.DEFAULT_PYTHON_INTERPRETER
   125      cmd = '$TOOLS_PEX -s "%s" -m "%s" --zip_safe' % (shebang, CONFIG.PYTHON_MODULE_DIR)
   126      pre_build, cmd = _handle_zip_safe(cmd, zip_safe)
   127  
   128      lib_rule = python_library(
   129          name='_%s#lib' % name,
   130          srcs=[main],
   131          resources=resources,
   132          interpreter=interpreter,
   133          deps=deps,
   134          visibility=visibility,
   135      )
   136  
   137      # Use the pex tool to compress the entry point & add all the bootstrap helpers etc.
   138      pex_rule = build_rule(
   139          name = name,
   140          tag = 'pex',
   141          srcs=[main],
   142          outs=[f'.{name}_main.pex.zip'],  # just call it .zip so everything has the same extension
   143          cmd=cmd,
   144          requires=['py', 'pex'],
   145          pre_build=pre_build,
   146          deps=deps,
   147          needs_transitive_deps=True,  # Needed so we can find anything with zip_safe=False on it.
   148          output_is_complete=True,
   149          tools={
   150              'interpreter': [interpreter or CONFIG.DEFAULT_PYTHON_INTERPRETER],
   151              'pex': [CONFIG.PEX_TOOL],
   152          },
   153      )
   154      # This rule concatenates the .pex with all the other precompiled zip files from dependent rules.
   155      cmd = '$TOOL z -i . -s .pex.zip -s .whl --preamble_from="$SRC" --include_other --add_init_py --strict'
   156      if strip:
   157          cmd += ' --strip_py'
   158      build_rule(
   159          name=name,
   160          srcs=[pex_rule],
   161          deps=[lib_rule],
   162          outs=[out or (name + '.pex')],
   163          cmd=cmd,
   164          needs_transitive_deps=True,
   165          binary=True,
   166          output_is_complete=True,
   167          building_description="Creating pex...",
   168          visibility=visibility,
   169          requires=['py', interpreter or CONFIG.DEFAULT_PYTHON_INTERPRETER],
   170          tools=[CONFIG.JARCAT_TOOL],
   171          # This makes the python_library rule the dependency for other python_library or
   172          # python_test rules that try to import it. Does mean that they cannot collect a .pex
   173          # by depending directly on the rule, they'll just get the Python files instead.
   174          # This is not a common case anyway; more usually you'd treat that as a runtime data
   175          # file rather than trying to pack into a pex. Can be worked around with an
   176          # intermediary filegroup rule if really needed.
   177          provides={'py': lib_rule},
   178          labels=labels,
   179      )
   180  
   181  
   182  def python_test(name:str, srcs:list, data:list=[], resources:list=[], deps:list=[], worker:str='',
   183                  labels:list&features&tags=None, size:str=None, flags:str='', visibility:list=None,
   184                  container:bool|dict=False, sandbox:bool=None, timeout:int=0, flaky:bool|int=0,
   185                  test_outputs:list=None, zip_safe:bool=None, interpreter:str=None):
   186      """Generates a Python test target.
   187  
   188      This works very similarly to python_binary; it is also a single .pex file
   189      which is run to execute the tests. The tests are run via either unittest or pytest, depending
   190      on which is set for the test runner, which can be configured either via the python_test_runner
   191      package property or python.testrunner in the config.
   192  
   193      Args:
   194        name (str): Name of the rule.
   195        srcs (list): Source files for this test.
   196        data (list): Runtime data files for the test.
   197        resources (list): Non-Python files to be included in the pex. Note that the distinction
   198                          vs. srcs is important here; srcs are passed to unittest for it to run
   199                          and it may or may not be happy if given non-Python files.
   200        deps (list): Dependencies of this rule.
   201        worker (str): Reference to worker script, A persistent worker process that is used to set up the test.
   202        labels (list): Labels for this rule.
   203        size (str): Test size (enormous, large, medium or small).
   204        flags (str): Flags to apply to the test command.
   205        visibility (list): Visibility specification.
   206        container (bool | dict): If True, the test will be run in a container (eg. Docker).
   207        sandbox (bool): Sandbox the test on Linux to restrict access to namespaces such as network.
   208        timeout (int): Maximum time this test is allowed to run for, in seconds.
   209        flaky (int | bool): True to mark this test as flaky, or an integer for a number of reruns.
   210        test_outputs (list): Extra test output files to generate from this test.
   211        zip_safe (bool): Allows overriding whether the output is marked zip safe or not.
   212                         If set to explicitly True or False, the output will be marked
   213                         appropriately; by default it will be safe unless any of the
   214                         transitive dependencies are themselves marked as not zip-safe.
   215        interpreter (str): The Python interpreter to use. Defaults to the config setting
   216                           which is normally just 'python', but could be 'python3' or
   217                          'pypy' or whatever.
   218      """
   219      timeout, labels = _test_size_and_timeout(size, timeout, labels)
   220      interpreter = interpreter or CONFIG.DEFAULT_PYTHON_INTERPRETER
   221      cmd = '$TOOLS_PEX -t -s "%s" -m "%s" -r "%s" --zip_safe' % (interpreter, CONFIG.PYTHON_MODULE_DIR, CONFIG.PYTHON_TEST_RUNNER)
   222      pre_build, cmd = _handle_zip_safe(cmd, zip_safe)
   223  
   224      # Use the pex tool to compress the entry point & add all the bootstrap helpers etc.
   225      pex_rule = build_rule(
   226          name = name,
   227          tag = 'pex',
   228          srcs=srcs,
   229          outs=[f'.{name}_main.pex.zip'],  # just call it .zip so everything has the same extension
   230          cmd=cmd,
   231          requires=['py'],
   232          test_only=True,
   233          needs_transitive_deps=True,  # needed for zip-safe detection
   234          building_description="Creating pex info...",
   235          pre_build=pre_build,
   236          deps=deps,
   237          tools={
   238              'interpreter': [interpreter or CONFIG.DEFAULT_PYTHON_INTERPRETER],
   239              'pex': [CONFIG.PEX_TOOL],
   240          },
   241      )
   242  
   243      # If there are resources specified, they have to get built into the pex.
   244      deps = [pex_rule]
   245      lib_rule = python_library(
   246          name='_%s#lib' % name,
   247          srcs=srcs,
   248          resources=resources,
   249          interpreter=interpreter,
   250          deps=deps,
   251          test_only=True,
   252          visibility=visibility,
   253      )
   254  
   255      deps = [pex_rule, lib_rule]
   256  
   257      test_cmd = f'$TEST {flags}'
   258      if worker:
   259          test_cmd = f'$(worker {worker}) && {test_cmd} '
   260          deps += [worker]
   261  
   262      # This rule concatenates the .pex with all the other precompiled zip files from dependent rules.
   263      build_rule(
   264          name=name,
   265          srcs=[pex_rule],
   266          deps=deps,
   267          # N.B. the actual test sources are passed as data files as well. This is needed for pytest but
   268          #      is faster for unittest as well (because we don't need to rebuild the pex if they change).
   269          data=data + srcs,
   270          outs=[f'{name}.pex'],
   271          labels=labels,
   272          cmd='$TOOL z -i . -s .pex.zip -s .whl --preamble_from="$SRC" --include_other --add_init_py --strict',
   273          test_cmd=test_cmd,
   274          needs_transitive_deps=True,
   275          output_is_complete=True,
   276          binary=True,
   277          test=True,
   278          container=container,
   279          test_sandbox=sandbox,
   280          building_description="Building pex...",
   281          visibility=visibility,
   282          test_timeout=timeout,
   283          flaky=flaky,
   284          test_outputs=test_outputs,
   285          requires=['py', interpreter or CONFIG.DEFAULT_PYTHON_INTERPRETER],
   286          tools=[CONFIG.JARCAT_TOOL],
   287      )
   288  
   289  
   290  def pip_library(name:str, version:str, hashes:list=None, package_name:str=None, outs:list=None,
   291                  test_only:bool&testonly=False, deps:list=[], post_install_commands:list=None,
   292                  install_subdirectory:bool=False, repo:str=None, use_pypi:bool=None, patch:str|list=None,
   293                  visibility:list=None, zip_safe:bool=True, licences:list=None, pip_flags:str=None,
   294                  strip:list=[]):
   295      """Provides a build rule for third-party dependencies to be installed by pip.
   296  
   297      Args:
   298        name (str): Name of the build rule.
   299        version (str): Specific version of the package to install.
   300        hashes (list): List of acceptable hashes for this target.
   301        package_name (str): Name of the pip package to install. Defaults to the same as 'name'.
   302        outs (list): List of output files / directories. Defaults to [name].
   303        test_only (bool): If True, can only be used by test rules or other test_only libraries.
   304        deps (list): List of rules this library depends on.
   305        post_install_commands (list): Commands run after pip install has completed.
   306        install_subdirectory (bool): Forces the package to install into a subdirectory with this name.
   307        repo (str): Allows specifying a custom repo to fetch from.
   308        use_pypi (bool): If True, will check PyPI as well for packages.
   309        patch (str | list): A patch file or files to be applied after install.
   310        visibility (list): Visibility declaration for this rule.
   311        zip_safe (bool): Flag to indicate whether a pex including this rule will be zip-safe.
   312        licences (list): Licences this rule is subject to. Default attempts to detect from package metadata.
   313        pip_flags (str): List of additional flags to pass to pip.
   314        strip (list): Files to strip after install. Note that these are done at any level.
   315      """
   316      package_name = package_name or name
   317      package_name = f'{package_name}=={version}'
   318      outs = outs or [name]
   319      post_install_commands = post_install_commands or []
   320      post_build = None
   321      use_pypi = CONFIG.USE_PYPI if use_pypi is None else use_pypi
   322      index_flag = '' if use_pypi else '--no-index'
   323      pip_flags = pip_flags or CONFIG.PIP_FLAGS
   324  
   325      repo_flag = ''
   326      repo = repo or CONFIG.PYTHON_DEFAULT_PIP_REPO
   327      if repo:
   328          if repo.startswith('//') or repo.startswith(':'):  # Looks like a build label, not a URL.
   329              repo_flag = f'-f %(location {repo})'
   330              deps.append(repo)
   331          else:
   332              repo_flag = '-f ' + repo
   333  
   334      target = outs[0] if install_subdirectory else '.'
   335  
   336      cmd = '$TOOLS_PIP install --no-deps --no-compile --no-cache-dir --default-timeout=60 --target=' + target
   337      if CONFIG.OS == 'linux':
   338          # Fix for https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=830892
   339          # tl;dr: Debian has broken --target with a custom patch, the only way to fix is to pass --system
   340          # which is itself Debian-specific, so we need to find if we're running on Debian. AAAAARGGGHHHH...
   341          cmd = f'[ -f /etc/debian_version ] && [ $TOOLS_PIP == "/usr/bin/pip3" ] && SYS_FLAG="--system" || SYS_FLAG=""; {cmd} $SYS_FLAG'
   342      elif CONFIG.OS == 'darwin':
   343          # Fix for Homebrew which fails with a superficially similar issue.
   344          # https://github.com/Homebrew/brew/blob/master/docs/Homebrew-and-Python.md suggests fixing with --install-option
   345          # but that prevents downloading binary wheels. This is more fiddly but seems to work.
   346          # Unfortunately it does *not* work similarly on the Debian problem :(
   347          cmd = 'echo "[install]\nprefix=" > .pydistutils.cfg; ' + cmd
   348      cmd += f' -b build {repo_flag} {index_flag} {pip_flags} {package_name}'
   349  
   350      if strip:
   351          cmd += ' && find . %s | xargs rm -rf' % ' -or '.join(['-name "%s"' % s for s in strip])
   352  
   353      if not licences:
   354          cmd += ' && find . -name METADATA -or -name PKG-INFO | grep -v "^./build/" | xargs grep -E "License ?:" | grep -v UNKNOWN | cat'
   355  
   356      if install_subdirectory:
   357          cmd += f' && touch {target}/__init__.py && rm -rf {target}/*.egg-info {target}/*.dist-info'
   358  
   359      if patch:
   360          patches = [patch] if isinstance(patch, str) else patch
   361          if CONFIG.OS == 'freebsd':
   362              # --no-backup-if-mismatch is not supported, but we need to get rid of the .orig
   363              # files for hashes to match correctly.
   364              cmd += ' && ' + ' && '.join([f'patch -p0 < $(location {patch})' for patch in patches])
   365              cmd += ' && find . -name "*.orig" | xargs rm'
   366          else:
   367              cmd += ' && ' + ' && '.join([f'patch -p0 --no-backup-if-mismatch < $(location {patch})' for patch in patches])
   368  
   369      if post_install_commands:
   370          cmd = ' && '.join([cmd] + post_install_commands)
   371  
   372      # TODO(peterebden): --prefix preserves the old behaviour here, but I'm not sure how intuitive that is; leaving it
   373      #                   out puts everything at the top level of the pex, which is more normal for Python really.
   374      cmd += ' && $TOOLS_JARCAT z -d --prefix $PKG_DIR -i ' + ' -i '.join(outs)
   375  
   376      wheel_rule = build_rule(
   377          name = name,
   378          tag = 'wheel',
   379          cmd = cmd,
   380          outs = [name + '.whl'],
   381          srcs = patches if patch else [],
   382          deps = deps,
   383          building_description = 'Fetching...',
   384          hashes = hashes,
   385          requires = ['py'],
   386          test_only = test_only,
   387          licences = licences,
   388          tools = {
   389              'pip': [CONFIG.PIP_TOOL],
   390              'jarcat': [CONFIG.JARCAT_TOOL],
   391          },
   392          post_build = None if licences else _add_licences,
   393          sandbox = False,
   394          labels = ['py:zip-unsafe'] if not zip_safe else None,
   395      )
   396      return build_rule(
   397          name = name,
   398          srcs = [wheel_rule],
   399          outs = outs,
   400          cmd = '$TOOL x $SRCS -s $PKG_DIR',
   401          tools = [CONFIG.JARCAT_TOOL],
   402          labels = ['py', 'pip:' + package_name],
   403          provides = {'py': wheel_rule},
   404          visibility = visibility,
   405          test_only = test_only,
   406          deps = deps,
   407      )
   408  
   409  
   410  def python_wheel(name:str, version:str, hashes:list=None, package_name:str=None, outs:list=None,
   411                   post_install_commands:list=None, patch:str|list=None, licences:list=None,
   412                   test_only:bool&testonly=False, repo:str=None, zip_safe:bool=True, visibility:list=None,
   413                   deps:list=[], name_scheme:str=None, strip:list=['*.pyc', 'tests']):
   414      """Downloads a Python wheel and extracts it.
   415  
   416      This is a lightweight pip-free alternative to pip_library which supports cross-compiling.
   417      Rather than leaning on pip which is difficult to achieve reproducible builds with and
   418      support on different platforms, this rule is a simple wrapper around curl and unzip.
   419      Unless otherwise specified, the wheels are expected to adhere to common naming schemes,
   420      such as:
   421        <package_name>-<version>[-<os>-<arch>].whl
   422        <package_name>-<version>[-<os>_<arch>].whl
   423        <package_name>-<version>.whl
   424  
   425      Args:
   426        name (str): Name of the rule. Also doubles as the name of the package if package_name
   427              is not set.
   428        version (str): Version of the package to install.
   429        hashes (list): List of hashes to verify the package against.
   430        package_name (str): If given, overrides `name` for the name of the package to look for.
   431        outs (list): List of output files. Defaults to a directory named the same as `name`.
   432        post_install_commands (list): Commands to run after 'install'.
   433        patch (str | list): Patch file to apply after install to fix any upstream code that has
   434                            done bad things.
   435        licences (list): Licences that this rule is subject to.
   436        test_only (bool): If True, this library can only be used by tests.
   437        repo (str): Repository to download wheels from.
   438        zip_safe (bool): Flag to indicate whether a pex including this rule will be zip-safe.
   439        visibility (list): Visibility declaration.
   440        deps (list): Dependencies of this rule.
   441        name_scheme (str): The templatized wheel naming scheme (available template variables
   442                           are `url_base`, `package_name`, and `version`).
   443        strip (list): Files to strip after install. Note that these are done at any level.
   444      """
   445      package_name = package_name or name.replace('-', '_')
   446      url_base = repo or CONFIG.PYTHON_WHEEL_REPO
   447      if not url_base:
   448          raise ParseError('python.wheel_repo is not set in the config, must pass repo explicitly '
   449                           'to python_wheel')
   450      urls = []
   451      if name_scheme:
   452          urls.append(name_scheme.format(url_base=url_base,
   453                                         package_name=package_name,
   454                                         version=version))
   455      elif CONFIG.PYTHON_WHEEL_NAME_SCHEME:
   456          urls.append(CONFIG.PYTHON_WHEEL_NAME_SCHEME.format(url_base=url_base,
   457                                                             package_name=package_name,
   458                                                             version=version))
   459      else:
   460          # Populate urls using a reasonable set of possible wheel naming schemes.
   461          # Look for an arch-specific wheel first; in some cases there can be both (e.g. protobuf
   462          # has optional arch-specific bits) and we prefer the one with the cool stuff.
   463          urls.append('{url_base}/{package_name}-{version}-${{OS}}-${{ARCH}}.whl'.format(url_base=url_base,
   464                                                                                         package_name=package_name,
   465                                                                                         version=version))
   466          urls.append('{url_base}/{package_name}-{version}-${{OS}}_${{ARCH}}.whl'.format(url_base=url_base,
   467                                                                                         package_name=package_name,
   468                                                                                         version=version))
   469          urls.append('{url_base}/{package_name}-{version}.whl'.format(url_base=url_base,
   470                                                                       package_name=package_name,
   471                                                                       version=version))
   472  
   473      file_rule = remote_file(
   474          name = name,
   475          _tag = 'download',
   476          out = name + '.whl',
   477          url = urls,
   478          licences = licences if licences else None,
   479      )
   480  
   481      cmd = ['$TOOL x $SRCS_SRC']
   482  
   483      if strip:
   484          cmd += ['find . %s | xargs rm -rf' % ' -or '.join(['-name "%s"' % s for s in strip])]
   485      if not licences:
   486          cmd.append('find . -name METADATA -or -name PKG-INFO | grep -v "^./build/" | '
   487                     'xargs grep -E "License ?:" | grep -v UNKNOWN | cat')
   488      if patch:
   489          patches = [patch] if isinstance(patch, str) else patch
   490          cmd += [f'patch -p0 --no-backup-if-mismatch < $(location {p})' for p in patches]
   491      if post_install_commands:
   492          cmd += post_install_commands
   493  
   494      cmd += ['$TOOL z -d --prefix $PKG -i ' + ' -i '.join(outs or [name])]
   495      label = f'whl:{package_name}=={version}'
   496  
   497      wheel_rule = build_rule(
   498          name = name,
   499          tag = 'wheel',
   500          cmd = ' && '.join(cmd),
   501          outs = [name + '.pex.zip'],
   502          srcs = {
   503              "SRC": [file_rule],
   504              'RES': patches if patch else [],
   505          },
   506          building_description = 'Repackaging...',
   507          requires = ['py'],
   508          deps = deps,
   509          test_only = test_only,
   510          licences = licences,
   511          tools = [CONFIG.JARCAT_TOOL],
   512          post_build = None if licences else _add_licences,
   513          sandbox = False,
   514          labels = ['py:zip-unsafe', label] if not zip_safe else [label],
   515      )
   516      cmd = '$TOOL x $SRCS -s $PKG_DIR'
   517      if outs:
   518          # Hacky solution to handle things being in subdirectories in awkward ways.
   519          before, _, after = outs[0].partition('/')
   520          if after:
   521              cmd = f'rm -rf {before} && {cmd}'
   522      return build_rule(
   523          name = name,
   524          srcs = [wheel_rule],
   525          hashes = hashes,  # TODO(peterebden): Move this onto wheel_rule when we're willing to break hash compatibility.
   526          outs = outs or [name],
   527          tools = [CONFIG.JARCAT_TOOL],
   528          cmd = cmd,
   529          deps = deps,
   530          visibility = visibility,
   531          test_only = test_only,
   532          labels = [label],
   533          provides = {'py': wheel_rule},
   534      )
   535  
   536  
   537  def _handle_zip_safe(cmd, zip_safe):
   538      """Handles the zip safe flag. Returns a tuple of (pre-build function, new command)."""
   539      if zip_safe is None:
   540          return lambda name: (set_command(name, cmd.replace('--zip_safe', ' --nozip_safe'))
   541                               if has_label(name, 'py:zip-unsafe') else None), cmd
   542      elif zip_safe:
   543          return None, cmd
   544      else:
   545          return None, cmd.replace('--zip_safe', ' --nozip_safe')
   546  
   547  
   548  def _add_licences(name, output):
   549      """Annotates a pip_library rule with detected licences after download."""
   550      for line in output:
   551          if line.startswith('License: '):
   552              for licence in line[9:].split(' or '):  # Some are defined this way (eg. "PSF or ZPL")
   553                  add_licence(name, licence)
   554              return
   555          elif line.startswith('Classifier: License'):
   556              # Oddly quite a few packages seem to have UNKNOWN for the licence but this Classifier
   557              # section still seems to know what they are licenced as.
   558              add_licence(name, line.split(' :: ')[-1])
   559              return
   560      log.warning('No licence found for %s, should add licences = [...] to the rule',
   561                  name.lstrip('_').split('#')[0])
   562  
   563  
   564  if CONFIG.BAZEL_COMPATIBILITY:
   565      py_library = python_library
   566      py_binary = python_binary
   567      py_test = python_test