python (3.11.7)
       1  """distutils.command.build_scripts
       2  
       3  Implements the Distutils 'build_scripts' command."""
       4  
       5  import os
       6  import re
       7  from stat import ST_MODE
       8  from distutils import sysconfig
       9  from distutils.core import Command
      10  from distutils.dep_util import newer
      11  from distutils.util import convert_path
      12  from distutils import log
      13  import tokenize
      14  
      15  shebang_pattern = re.compile('^#!.*python[0-9.]*([ \t].*)?$')
      16  """
      17  Pattern matching a Python interpreter indicated in first line of a script.
      18  """
      19  
      20  # for Setuptools compatibility
      21  first_line_re = shebang_pattern
      22  
      23  
      24  class ESC[4;38;5;81mbuild_scripts(ESC[4;38;5;149mCommand):
      25  
      26      description = "\"build\" scripts (copy and fixup #! line)"
      27  
      28      user_options = [
      29          ('build-dir=', 'd', "directory to \"build\" (copy) to"),
      30          ('force', 'f', "forcibly build everything (ignore file timestamps"),
      31          ('executable=', 'e', "specify final destination interpreter path"),
      32      ]
      33  
      34      boolean_options = ['force']
      35  
      36      def initialize_options(self):
      37          self.build_dir = None
      38          self.scripts = None
      39          self.force = None
      40          self.executable = None
      41  
      42      def finalize_options(self):
      43          self.set_undefined_options(
      44              'build',
      45              ('build_scripts', 'build_dir'),
      46              ('force', 'force'),
      47              ('executable', 'executable'),
      48          )
      49          self.scripts = self.distribution.scripts
      50  
      51      def get_source_files(self):
      52          return self.scripts
      53  
      54      def run(self):
      55          if not self.scripts:
      56              return
      57          self.copy_scripts()
      58  
      59      def copy_scripts(self):
      60          """
      61          Copy each script listed in ``self.scripts``.
      62  
      63          If a script is marked as a Python script (first line matches
      64          'shebang_pattern', i.e. starts with ``#!`` and contains
      65          "python"), then adjust in the copy the first line to refer to
      66          the current Python interpreter.
      67          """
      68          self.mkpath(self.build_dir)
      69          outfiles = []
      70          updated_files = []
      71          for script in self.scripts:
      72              self._copy_script(script, outfiles, updated_files)
      73  
      74          self._change_modes(outfiles)
      75  
      76          return outfiles, updated_files
      77  
      78      def _copy_script(self, script, outfiles, updated_files):  # noqa: C901
      79          shebang_match = None
      80          script = convert_path(script)
      81          outfile = os.path.join(self.build_dir, os.path.basename(script))
      82          outfiles.append(outfile)
      83  
      84          if not self.force and not newer(script, outfile):
      85              log.debug("not copying %s (up-to-date)", script)
      86              return
      87  
      88          # Always open the file, but ignore failures in dry-run mode
      89          # in order to attempt to copy directly.
      90          try:
      91              f = tokenize.open(script)
      92          except OSError:
      93              if not self.dry_run:
      94                  raise
      95              f = None
      96          else:
      97              first_line = f.readline()
      98              if not first_line:
      99                  self.warn("%s is an empty file (skipping)" % script)
     100                  return
     101  
     102              shebang_match = shebang_pattern.match(first_line)
     103  
     104          updated_files.append(outfile)
     105          if shebang_match:
     106              log.info("copying and adjusting %s -> %s", script, self.build_dir)
     107              if not self.dry_run:
     108                  if not sysconfig.python_build:
     109                      executable = self.executable
     110                  else:
     111                      executable = os.path.join(
     112                          sysconfig.get_config_var("BINDIR"),
     113                          "python%s%s"
     114                          % (
     115                              sysconfig.get_config_var("VERSION"),
     116                              sysconfig.get_config_var("EXE"),
     117                          ),
     118                      )
     119                  post_interp = shebang_match.group(1) or ''
     120                  shebang = "#!" + executable + post_interp + "\n"
     121                  self._validate_shebang(shebang, f.encoding)
     122                  with open(outfile, "w", encoding=f.encoding) as outf:
     123                      outf.write(shebang)
     124                      outf.writelines(f.readlines())
     125              if f:
     126                  f.close()
     127          else:
     128              if f:
     129                  f.close()
     130              self.copy_file(script, outfile)
     131  
     132      def _change_modes(self, outfiles):
     133          if os.name != 'posix':
     134              return
     135  
     136          for file in outfiles:
     137              self._change_mode(file)
     138  
     139      def _change_mode(self, file):
     140          if self.dry_run:
     141              log.info("changing mode of %s", file)
     142              return
     143  
     144          oldmode = os.stat(file)[ST_MODE] & 0o7777
     145          newmode = (oldmode | 0o555) & 0o7777
     146          if newmode != oldmode:
     147              log.info("changing mode of %s from %o to %o", file, oldmode, newmode)
     148              os.chmod(file, newmode)
     149  
     150      @staticmethod
     151      def _validate_shebang(shebang, encoding):
     152          # Python parser starts to read a script using UTF-8 until
     153          # it gets a #coding:xxx cookie. The shebang has to be the
     154          # first line of a file, the #coding:xxx cookie cannot be
     155          # written before. So the shebang has to be encodable to
     156          # UTF-8.
     157          try:
     158              shebang.encode('utf-8')
     159          except UnicodeEncodeError:
     160              raise ValueError(
     161                  "The shebang ({!r}) is not encodable " "to utf-8".format(shebang)
     162              )
     163  
     164          # If the script is encoded to a custom encoding (use a
     165          # #coding:xxx cookie), the shebang has to be encodable to
     166          # the script encoding too.
     167          try:
     168              shebang.encode(encoding)
     169          except UnicodeEncodeError:
     170              raise ValueError(
     171                  "The shebang ({!r}) is not encodable "
     172                  "to the script encoding ({})".format(shebang, encoding)
     173              )