github.phpd.cn/thought-machine/please@v12.2.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 = '$TOOLS_INT -S -m compileall %s -f $SRCS_SRCS' % bytecode_flag
    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=['.%s.pex.zip' % name],
    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, interpreter:str=None, shebang:str='',
    97                    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        interpreter (str): The Python interpreter to use. Defaults to the config setting
   117                           which is normally just 'python', but could be 'python3' or
   118                           'pypy' or whatever.
   119        shebang (str): Exact shebang to apply to the generated file. By default we will
   120                       determine something appropriate for the given interpreter.
   121        labels (list): Labels to apply to this rule.
   122      """
   123      shebang = shebang or interpreter or CONFIG.DEFAULT_PYTHON_INTERPRETER
   124      cmd = '$TOOLS_PEX -s "%s" -m "%s" --zip_safe' % (shebang, CONFIG.PYTHON_MODULE_DIR)
   125      pre_build, cmd = _handle_zip_safe(cmd, zip_safe)
   126  
   127      lib_rule = python_library(
   128          name='_%s#lib' % name,
   129          srcs=[main],
   130          resources=resources,
   131          interpreter=interpreter,
   132          deps=deps,
   133          visibility=visibility,
   134      )
   135  
   136      # Use the pex tool to compress the entry point & add all the bootstrap helpers etc.
   137      pex_rule = build_rule(
   138          name = name,
   139          tag = 'pex',
   140          srcs=[main],
   141          outs=['.%s_main.pex.zip' % name],  # just call it .zip so everything has the same extension
   142          cmd=cmd,
   143          requires=['py', 'pex'],
   144          pre_build=pre_build,
   145          deps=deps,
   146          needs_transitive_deps=True,  # Needed so we can find anything with zip_safe=False on it.
   147          output_is_complete=True,
   148          tools={
   149              'interpreter': [interpreter or CONFIG.DEFAULT_PYTHON_INTERPRETER],
   150              'pex': [CONFIG.PEX_TOOL],
   151          },
   152      )
   153      # This rule concatenates the .pex with all the other precompiled zip files from dependent rules.
   154      build_rule(
   155          name=name,
   156          srcs=[pex_rule],
   157          deps=[lib_rule],
   158          outs=[out or (name + '.pex')],
   159          cmd=_PYTHON_BINARY_CMDS,
   160          needs_transitive_deps=True,
   161          binary=True,
   162          output_is_complete=True,
   163          building_description="Creating pex...",
   164          visibility=visibility,
   165          requires=['py', interpreter or CONFIG.DEFAULT_PYTHON_INTERPRETER],
   166          tools=[CONFIG.JARCAT_TOOL],
   167          # This makes the python_library rule the dependency for other python_library or
   168          # python_test rules that try to import it. Does mean that they cannot collect a .pex
   169          # by depending directly on the rule, they'll just get the Python files instead.
   170          # This is not a common case anyway; more usually you'd treat that as a runtime data
   171          # file rather than trying to pack into a pex. Can be worked around with an
   172          # intermediary filegroup rule if really needed.
   173          provides={'py': lib_rule},
   174          labels=labels,
   175      )
   176  
   177  
   178  def python_test(name:str, srcs:list, data:list=[], resources:list=[], deps:list=[],
   179                  labels:list&features&tags=None, size:str=None, flags:str='', visibility:list=None,
   180                  container:bool|dict=False, sandbox:bool=None, timeout:int=0, flaky:bool|int=0,
   181                  test_outputs:list=None, zip_safe:bool=None, interpreter:str=None):
   182      """Generates a Python test target.
   183  
   184      This works very similarly to python_binary; it is also a single .pex file
   185      which is run to execute the tests. The tests are run via either unittest or pytest, depending
   186      on which is set for the test runner, which can be configured either via the python_test_runner
   187      package property or python.testrunner in the config.
   188  
   189      Args:
   190        name (str): Name of the rule.
   191        srcs (list): Source files for this test.
   192        data (list): Runtime data files for the test.
   193        resources (list): Non-Python files to be included in the pex. Note that the distinction
   194                          vs. srcs is important here; srcs are passed to unittest for it to run
   195                          and it may or may not be happy if given non-Python files.
   196        deps (list): Dependencies of this rule.
   197        labels (list): Labels for this rule.
   198        size (str): Test size (enormous, large, medium or small).
   199        flags (str): Flags to apply to the test command.
   200        visibility (list): Visibility specification.
   201        container (bool | dict): If True, the test will be run in a container (eg. Docker).
   202        sandbox (bool): Sandbox the test on Linux to restrict access to namespaces such as network.
   203        timeout (int): Maximum time this test is allowed to run for, in seconds.
   204        flaky (int | bool): True to mark this test as flaky, or an integer for a number of reruns.
   205        test_outputs (list): Extra test output files to generate from this test.
   206        zip_safe (bool): Allows overriding whether the output is marked zip safe or not.
   207                         If set to explicitly True or False, the output will be marked
   208                         appropriately; by default it will be safe unless any of the
   209                         transitive dependencies are themselves marked as not zip-safe.
   210        interpreter (str): The Python interpreter to use. Defaults to the config setting
   211                           which is normally just 'python', but could be 'python3' or
   212                          'pypy' or whatever.
   213      """
   214      timeout, labels = _test_size_and_timeout(size, timeout, labels)
   215      interpreter = interpreter or CONFIG.DEFAULT_PYTHON_INTERPRETER
   216      cmd = '$TOOLS_PEX -t -s "%s" -m "%s" -r "%s" --zip_safe' % (interpreter, CONFIG.PYTHON_MODULE_DIR, CONFIG.PYTHON_TEST_RUNNER)
   217      pre_build, cmd = _handle_zip_safe(cmd, zip_safe)
   218  
   219      # Use the pex tool to compress the entry point & add all the bootstrap helpers etc.
   220      pex_rule = build_rule(
   221          name = name,
   222          tag = 'pex',
   223          srcs=srcs,
   224          outs=['.%s_main.pex.zip' % name],  # just call it .zip so everything has the same extension
   225          cmd=cmd,
   226          requires=['py'],
   227          test_only=True,
   228          needs_transitive_deps=True,  # needed for zip-safe detection
   229          building_description="Creating pex info...",
   230          pre_build=pre_build,
   231          deps=deps,
   232          tools={
   233              'interpreter': [interpreter or CONFIG.DEFAULT_PYTHON_INTERPRETER],
   234              'pex': [CONFIG.PEX_TOOL],
   235          },
   236      )
   237  
   238      # If there are resources specified, they have to get built into the pex.
   239      deps = [pex_rule]
   240      if resources:
   241          # Test library itself.
   242          lib_rule = python_library(
   243              name='_%s#lib' % name,
   244              resources=resources,
   245              interpreter=interpreter,
   246              deps=deps,
   247              test_only=True,
   248          )
   249          deps = [pex_rule, lib_rule]
   250  
   251      # This rule concatenates the .pex with all the other precompiled zip files from dependent rules.
   252      build_rule(
   253          name=name,
   254          srcs=[pex_rule],
   255          deps=deps,
   256          # N.B. the actual test sources are passed as data files as well. This is needed for pytest but
   257          #      is faster for unittest as well (because we don't need to rebuild the pex if they change).
   258          data=data + srcs,
   259          outs=['%s.pex' % name],
   260          labels=labels,
   261          cmd=_PYTHON_BINARY_CMDS,
   262          test_cmd = '$TEST ' + flags,
   263          needs_transitive_deps=True,
   264          output_is_complete=True,
   265          binary=True,
   266          test=True,
   267          container=container,
   268          test_sandbox=sandbox,
   269          building_description="Building pex...",
   270          visibility=visibility,
   271          test_timeout=timeout,
   272          flaky=flaky,
   273          test_outputs=test_outputs,
   274          requires=['py', interpreter or CONFIG.DEFAULT_PYTHON_INTERPRETER],
   275          tools=[CONFIG.JARCAT_TOOL],
   276      )
   277  
   278  
   279  def pip_library(name:str, version:str, hashes:list=None, package_name:str=None, outs:list=None,
   280                  test_only:bool&testonly=False, env:dict=None, deps:list=[], post_install_commands:list=None,
   281                  install_subdirectory:bool=False, repo:str=None, use_pypi:bool=None, patch:str|list=None,
   282                  visibility:list=None, zip_safe:bool=True, licences:list=None, pip_flags:str=None):
   283      """Provides a build rule for third-party dependencies to be installed by pip.
   284  
   285      Args:
   286        name (str): Name of the build rule.
   287        version (str): Specific version of the package to install.
   288        hashes (list): List of acceptable hashes for this target.
   289        package_name (str): Name of the pip package to install. Defaults to the same as 'name'.
   290        outs (list): List of output files / directories. Defaults to [name].
   291        test_only (bool): If True, can only be used by test rules or other test_only libraries.
   292        env (dict): Deprecated, has no effect.
   293        deps (list): List of rules this library depends on.
   294        post_install_commands (list): Commands run after pip install has completed.
   295        install_subdirectory (bool): Forces the package to install into a subdirectory with this name.
   296        repo (str): Allows specifying a custom repo to fetch from.
   297        use_pypi (bool): If True, will check PyPI as well for packages.
   298        patch (str | list): A patch file or files to be applied after install.
   299        visibility (list): Visibility declaration for this rule.
   300        zip_safe (bool): Flag to indicate whether a pex including this rule will be zip-safe.
   301        licences (list): Licences this rule is subject to. Default attempts to detect from package metadata.
   302        pip_flags (str): List of additional flags to pass to pip.
   303      """
   304      package_name = '%s==%s' % (package_name or name, version)
   305      outs = outs or [name]
   306      post_install_commands = post_install_commands or []
   307      post_build = None
   308      use_pypi = CONFIG.USE_PYPI if use_pypi is None else use_pypi
   309      index_flag = '' if use_pypi else '--no-index'
   310      pip_flags = pip_flags or CONFIG.PIP_FLAGS
   311  
   312      repo_flag = ''
   313      repo = repo or CONFIG.PYTHON_DEFAULT_PIP_REPO
   314      if repo:
   315          if repo.startswith('//') or repo.startswith(':'):  # Looks like a build label, not a URL.
   316              repo_flag = '-f %(location %s)' % repo
   317              deps.append(repo)
   318          else:
   319              repo_flag = '-f ' + repo
   320  
   321      target = outs[0] if install_subdirectory else '.'
   322  
   323      cmd = '$TOOLS_PIP install --no-deps --no-compile --no-cache-dir --default-timeout=60 --target=' + target
   324      if CONFIG.OS == 'linux':
   325          # Fix for https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=830892
   326          # tl;dr: Debian has broken --target with a custom patch, the only way to fix is to pass --system
   327          # which is itself Debian-specific, so we need to find if we're running on Debian. AAAAARGGGHHHH...
   328          cmd = '[ -f /etc/debian_version ] && [ $TOOLS_PIP == "/usr/bin/pip3" ] && SYS_FLAG="--system" || SYS_FLAG=""; ' + cmd + ' $SYS_FLAG'
   329      elif CONFIG.OS == 'darwin':
   330          # Fix for Homebrew which fails with a superficially similar issue.
   331          # https://github.com/Homebrew/brew/blob/master/docs/Homebrew-and-Python.md suggests fixing with --install-option
   332          # but that prevents downloading binary wheels. This is more fiddly but seems to work.
   333          # Unfortunately it does *not* work similarly on the Debian problem :(
   334          cmd = 'echo "[install]\nprefix=" > setup.cfg; ' + cmd
   335      cmd += ' -q -b build %s %s %s %s' % (repo_flag, index_flag, pip_flags, package_name)
   336      cmd += ' && find . -name "*.pyc" -or -name "tests" | xargs rm -rf'
   337  
   338      if not licences:
   339          cmd += ' && find . -name METADATA -or -name PKG-INFO | grep -v "^./build/" | xargs grep -E "License ?:" | grep -v UNKNOWN | cat'
   340  
   341      if install_subdirectory:
   342          cmd += ' && touch %s/__init__.py && rm -rf %s/*.egg-info %s/*.dist-info' % (target, target, target)
   343  
   344      if patch:
   345          patches = [patch] if isinstance(patch, str) else patch
   346          if CONFIG.OS == 'freebsd':
   347              # --no-backup-if-mismatch is not supported, but we need to get rid of the .orig
   348              # files for hashes to match correctly.
   349              cmd += ' && ' + ' && '.join(['patch -p0 < $(location %s)' % patch for patch in patches])
   350              cmd += ' && find . -name "*.orig" | xargs rm'
   351          else:
   352              cmd += ' && ' + ' && '.join(['patch -p0 --no-backup-if-mismatch < $(location %s)' % patch for patch in patches])
   353  
   354      if post_install_commands:
   355          cmd = ' && '.join([cmd] + post_install_commands)
   356  
   357      # TODO(peterebden): --prefix preserves the old behaviour here, but I'm not sure how intuitive that is; leaving it
   358      #                   out puts everything at the top level of the pex, which is more normal for Python really.
   359      cmd += ' && $TOOLS_JARCAT z -d --prefix $PKG_DIR -i ' + ' -i '.join(outs)
   360  
   361      wheel_rule = build_rule(
   362          name = name,
   363          tag = 'wheel',
   364          cmd = cmd,
   365          outs = [name + '.whl'],
   366          srcs = patches if patch else [],
   367          deps = deps,
   368          building_description = 'Fetching...',
   369          hashes = hashes,
   370          requires = ['py'],
   371          test_only = test_only,
   372          licences = licences,
   373          tools = {
   374              'pip': [CONFIG.PIP_TOOL],
   375              'jarcat': [CONFIG.JARCAT_TOOL],
   376          },
   377          post_build = None if licences else _add_licences,
   378          sandbox = False,
   379          labels = ['py:zip-unsafe'] if not zip_safe else None,
   380      )
   381      return build_rule(
   382          name = name,
   383          srcs = [wheel_rule],
   384          outs = outs,
   385          cmd = '$TOOL x $SRCS -s $PKG_DIR',
   386          tools = [CONFIG.JARCAT_TOOL],
   387          labels = ['py', 'pip:' + package_name],
   388          provides = {'py': wheel_rule},
   389          visibility = visibility,
   390          test_only = test_only,
   391          deps = deps,
   392      )
   393  
   394  
   395  def python_wheel(name:str, version:str, hashes:list=None, package_name:str=None, outs:list=None,
   396                   post_install_commands:list=None, patch:str|list=None, licences:list=None,
   397                   test_only:bool&testonly=False, repo:str=None, zip_safe:bool=True, visibility:list=None,
   398                   deps:list=[], name_scheme:str=None):
   399      """Downloads a Python wheel and extracts it.
   400  
   401      This is a lightweight pip-free alternative to pip_library which supports cross-compiling.
   402      Rather than leaning on pip which is difficult to achieve reproducible builds with and
   403      support on different platforms, this rule is a simple wrapper around curl and unzip.
   404      Unless otherwise specified, the wheels are expected to adhere to common naming schemes,
   405      such as:
   406        <package_name>-<version>[-<os>-<arch>].whl
   407        <package_name>-<version>[-<os>_<arch>].whl
   408        <package_name>-<version>.whl
   409  
   410      Args:
   411        name (str): Name of the rule. Also doubles as the name of the package if package_name
   412              is not set.
   413        version (str): Version of the package to install.
   414        hashes (list): List of hashes to verify the package against.
   415        package_name (str): If given, overrides `name` for the name of the package to look for.
   416        outs (list): List of output files. Defaults to a directory named the same as `name`.
   417        post_install_commands (list): Commands to run after 'install'.
   418        patch (str | list): Patch file to apply after install to fix any upstream code that has
   419                            done bad things.
   420        licences (list): Licences that this rule is subject to.
   421        test_only (bool): If True, this library can only be used by tests.
   422        repo (str): Repository to download wheels from.
   423        zip_safe (bool): Flag to indicate whether a pex including this rule will be zip-safe.
   424        visibility (list): Visibility declaration.
   425        deps (list): Dependencies of this rule.
   426        name_scheme (str): The templatized wheel naming scheme (available template variables
   427                           are `url_base`, `package_name`, and `version`).
   428      """
   429      package_name = package_name or name.replace('-', '_')
   430      url_base = repo or CONFIG.PYTHON_WHEEL_REPO
   431      if not url_base:
   432          raise ParseError('python.wheel_repo is not set in the config, must pass repo explicitly '
   433                           'to python_wheel')
   434      urls = []
   435      if name_scheme:
   436          urls.append(name_scheme.format(url_base=url_base,
   437                                         package_name=package_name,
   438                                         version=version))
   439      elif CONFIG.PYTHON_WHEEL_NAME_SCHEME:
   440          urls.append(CONFIG.PYTHON_WHEEL_NAME_SCHEME.format(url_base=url_base,
   441                                                             package_name=package_name,
   442                                                             version=version))
   443      else:
   444          # Populate urls using a reasonable set of possible wheel naming schemes.
   445          # Look for an arch-specific wheel first; in some cases there can be both (e.g. protobuf
   446          # has optional arch-specific bits) and we prefer the one with the cool stuff.
   447          urls.append('{url_base}/{package_name}-{version}-${{OS}}-${{ARCH}}.whl'.format(url_base=url_base,
   448                                                                                         package_name=package_name,
   449                                                                                         version=version))
   450          urls.append('{url_base}/{package_name}-{version}-${{OS}}_${{ARCH}}.whl'.format(url_base=url_base,
   451                                                                                         package_name=package_name,
   452                                                                                         version=version))
   453          urls.append('{url_base}/{package_name}-{version}.whl'.format(url_base=url_base,
   454                                                                       package_name=package_name,
   455                                                                       version=version))
   456  
   457      curl_commands = ' || '.join(['curl -fsSO {url}'.format(url=url) for url in urls])
   458      cmd = [
   459          curl_commands,
   460          '$TOOL x *.whl',
   461          # Strip any bytecode, it might lead to nondeterminism. Similarly some wheels annoyingly
   462          # contain test code that we don't want.
   463          'find . -name "*.pyc" -or -name "tests" | xargs rm -rf',
   464      ]
   465      if not licences:
   466          cmd.append('find . -name METADATA -or -name PKG-INFO | grep -v "^./build/" | '
   467                     'xargs grep -E "License ?:" | grep -v UNKNOWN | cat')
   468      if patch:
   469          patches = [patch] if isinstance(patch, str) else patch
   470          cmd += ['patch -p0 --no-backup-if-mismatch < $(location %s)' % p for p in patches]
   471      if post_install_commands:
   472          cmd += post_install_commands
   473      cmd += ['$TOOL z -d --prefix $PKG -i ' + ' -i '.join(outs or [name])]
   474  
   475      wheel_rule = build_rule(
   476          name = name,
   477          tag = 'wheel',
   478          cmd = ' && '.join(cmd),
   479          outs = [name + '.whl'],
   480          srcs = patches if patch else None,
   481          building_description = 'Downloading...',
   482          requires = ['py'],
   483          deps = deps,
   484          test_only = test_only,
   485          licences = licences,
   486          tools = [CONFIG.JARCAT_TOOL],
   487          post_build = None if licences else _add_licences,
   488          sandbox = False,
   489          labels = ['py:zip-unsafe'] if not zip_safe else None,
   490      )
   491      cmd = '$TOOL x $SRCS -s $PKG_DIR'
   492      if outs:
   493          # Hacky solution to handle things being in subdirectories in awkward ways.
   494          before, _, after = outs[0].partition('/')
   495          if after:
   496              cmd = 'rm -rf %s && %s' % (before, cmd)
   497      return build_rule(
   498          name = name,
   499          srcs = [wheel_rule],
   500          hashes = hashes,  # TODO(peterebden): Move this onto wheel_rule when we're willing to break hash compatibility.
   501          outs = outs or [name],
   502          tools = [CONFIG.JARCAT_TOOL],
   503          cmd = cmd,
   504          deps = deps,
   505          visibility = visibility,
   506          test_only = test_only,
   507          labels = ['whl:%s==%s' % (package_name, version)],
   508          provides = {'py': wheel_rule},
   509      )
   510  
   511  
   512  def _handle_zip_safe(cmd, zip_safe):
   513      """Handles the zip safe flag. Returns a tuple of (pre-build function, new command)."""
   514      if zip_safe is None:
   515          return lambda name: (set_command(name, cmd.replace('--zip_safe', ' --nozip_safe'))
   516                               if has_label(name, 'py:zip-unsafe') else None), cmd
   517      elif zip_safe:
   518          return None, cmd
   519      else:
   520          return None, cmd.replace('--zip_safe', ' --nozip_safe')
   521  
   522  
   523  def _add_licences(name, output):
   524      """Annotates a pip_library rule with detected licences after download."""
   525      for line in output:
   526          if line.startswith('License: '):
   527              for licence in line[9:].split(' or '):  # Some are defined this way (eg. "PSF or ZPL")
   528                  add_licence(name, licence)
   529              return
   530          elif line.startswith('Classifier: License'):
   531              # Oddly quite a few packages seem to have UNKNOWN for the licence but this Classifier
   532              # section still seems to know what they are licenced as.
   533              add_licence(name, line.split(' :: ')[-1])
   534              return
   535      log.warning('No licence found for %s, should add licences = [...] to the rule',
   536                  name.lstrip('_').split('#')[0])
   537  
   538  
   539  # The commands that we use for python_binary and python_test rules.
   540  _PYTHON_BINARY_CMDS = {
   541      'opt': '$TOOL z -i . -o $OUTS -s .pex.zip -s .whl --preamble_from="$SRC" --include_other --add_init_py --strict',
   542      'stripped': '$TOOL z -i . -o $OUTS -s .pex.zip -s .whl --preamble_from="$SRC" --include_other --add_init_py --strict -e .py -x "*.py"',
   543  }
   544  
   545  
   546  if CONFIG.BAZEL_COMPATIBILITY:
   547      py_library = python_library
   548      py_binary = python_binary
   549      py_test = python_test