(root)/
Python-3.12.0/
Tools/
build/
freeze_modules.py
       1  """Freeze modules and regen related files (e.g. Python/frozen.c).
       2  
       3  See the notes at the top of Python/frozen.c for more info.
       4  """
       5  
       6  from collections import namedtuple
       7  import hashlib
       8  import os
       9  import ntpath
      10  import posixpath
      11  import argparse
      12  from update_file import updating_file_with_tmpfile
      13  
      14  
      15  ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
      16  ROOT_DIR = os.path.abspath(ROOT_DIR)
      17  FROZEN_ONLY = os.path.join(ROOT_DIR, 'Tools', 'freeze', 'flag.py')
      18  
      19  STDLIB_DIR = os.path.join(ROOT_DIR, 'Lib')
      20  # If FROZEN_MODULES_DIR or DEEPFROZEN_MODULES_DIR is changed then the
      21  # .gitattributes and .gitignore files needs to be updated.
      22  FROZEN_MODULES_DIR = os.path.join(ROOT_DIR, 'Python', 'frozen_modules')
      23  DEEPFROZEN_MODULES_DIR = os.path.join(ROOT_DIR, 'Python', 'deepfreeze')
      24  
      25  FROZEN_FILE = os.path.join(ROOT_DIR, 'Python', 'frozen.c')
      26  MAKEFILE = os.path.join(ROOT_DIR, 'Makefile.pre.in')
      27  PCBUILD_PROJECT = os.path.join(ROOT_DIR, 'PCbuild', '_freeze_module.vcxproj')
      28  PCBUILD_FILTERS = os.path.join(ROOT_DIR, 'PCbuild', '_freeze_module.vcxproj.filters')
      29  PCBUILD_PYTHONCORE = os.path.join(ROOT_DIR, 'PCbuild', 'pythoncore.vcxproj')
      30  
      31  
      32  OS_PATH = 'ntpath' if os.name == 'nt' else 'posixpath'
      33  
      34  # These are modules that get frozen.
      35  # If you're debugging new bytecode instructions,
      36  # you can delete all sections except 'import system'.
      37  # This also speeds up building somewhat.
      38  TESTS_SECTION = 'Test module'
      39  FROZEN = [
      40      # See parse_frozen_spec() for the format.
      41      # In cases where the frozenid is duplicated, the first one is re-used.
      42      ('import system', [
      43          # These frozen modules are necessary for bootstrapping
      44          # the import system.
      45          'importlib._bootstrap : _frozen_importlib',
      46          'importlib._bootstrap_external : _frozen_importlib_external',
      47          # This module is important because some Python builds rely
      48          # on a builtin zip file instead of a filesystem.
      49          'zipimport',
      50          ]),
      51      # (You can delete entries from here down to the end of the list.)
      52      ('stdlib - startup, without site (python -S)', [
      53          'abc',
      54          'codecs',
      55          # For now we do not freeze the encodings, due # to the noise all
      56          # those extra modules add to the text printed during the build.
      57          # (See https://github.com/python/cpython/pull/28398#pullrequestreview-756856469.)
      58          #'<encodings.*>',
      59          'io',
      60          ]),
      61      ('stdlib - startup, with site', [
      62          '_collections_abc',
      63          '_sitebuiltins',
      64          'genericpath',
      65          'ntpath',
      66          'posixpath',
      67          # We must explicitly mark os.path as a frozen module
      68          # even though it will never be imported.
      69          f'{OS_PATH} : os.path',
      70          'os',
      71          'site',
      72          'stat',
      73          ]),
      74      ('runpy - run module with -m', [
      75          "importlib.util",
      76          "importlib.machinery",
      77          "runpy",
      78      ]),
      79      (TESTS_SECTION, [
      80          '__hello__',
      81          '__hello__ : __hello_alias__',
      82          '__hello__ : <__phello_alias__>',
      83          '__hello__ : __phello_alias__.spam',
      84          '<__phello__.**.*>',
      85          f'frozen_only : __hello_only__ = {FROZEN_ONLY}',
      86          ]),
      87      # (End of stuff you could delete.)
      88  ]
      89  BOOTSTRAP = {
      90      'importlib._bootstrap',
      91      'importlib._bootstrap_external',
      92      'zipimport',
      93  }
      94  
      95  
      96  #######################################
      97  # platform-specific helpers
      98  
      99  if os.path is posixpath:
     100      relpath_for_posix_display = os.path.relpath
     101  
     102      def relpath_for_windows_display(path, base):
     103          return ntpath.relpath(
     104              ntpath.join(*path.split(os.path.sep)),
     105              ntpath.join(*base.split(os.path.sep)),
     106          )
     107  
     108  else:
     109      relpath_for_windows_display = ntpath.relpath
     110  
     111      def relpath_for_posix_display(path, base):
     112          return posixpath.relpath(
     113              posixpath.join(*path.split(os.path.sep)),
     114              posixpath.join(*base.split(os.path.sep)),
     115          )
     116  
     117  
     118  #######################################
     119  # specs
     120  
     121  def parse_frozen_specs():
     122      seen = {}
     123      for section, specs in FROZEN:
     124          parsed = _parse_specs(specs, section, seen)
     125          for item in parsed:
     126              frozenid, pyfile, modname, ispkg, section = item
     127              try:
     128                  source = seen[frozenid]
     129              except KeyError:
     130                  source = FrozenSource.from_id(frozenid, pyfile)
     131                  seen[frozenid] = source
     132              else:
     133                  assert not pyfile or pyfile == source.pyfile, item
     134              yield FrozenModule(modname, ispkg, section, source)
     135  
     136  
     137  def _parse_specs(specs, section, seen):
     138      for spec in specs:
     139          info, subs = _parse_spec(spec, seen, section)
     140          yield info
     141          for info in subs or ():
     142              yield info
     143  
     144  
     145  def _parse_spec(spec, knownids=None, section=None):
     146      """Yield an info tuple for each module corresponding to the given spec.
     147  
     148      The info consists of: (frozenid, pyfile, modname, ispkg, section).
     149  
     150      Supported formats:
     151  
     152        frozenid
     153        frozenid : modname
     154        frozenid : modname = pyfile
     155  
     156      "frozenid" and "modname" must be valid module names (dot-separated
     157      identifiers).  If "modname" is not provided then "frozenid" is used.
     158      If "pyfile" is not provided then the filename of the module
     159      corresponding to "frozenid" is used.
     160  
     161      Angle brackets around a frozenid (e.g. '<encodings>") indicate
     162      it is a package.  This also means it must be an actual module
     163      (i.e. "pyfile" cannot have been provided).  Such values can have
     164      patterns to expand submodules:
     165  
     166        <encodings.*>    - also freeze all direct submodules
     167        <encodings.**.*> - also freeze the full submodule tree
     168  
     169      As with "frozenid", angle brackets around "modname" indicate
     170      it is a package.  However, in this case "pyfile" should not
     171      have been provided and patterns in "modname" are not supported.
     172      Also, if "modname" has brackets then "frozenid" should not,
     173      and "pyfile" should have been provided..
     174      """
     175      frozenid, _, remainder = spec.partition(':')
     176      modname, _, pyfile = remainder.partition('=')
     177      frozenid = frozenid.strip()
     178      modname = modname.strip()
     179      pyfile = pyfile.strip()
     180  
     181      submodules = None
     182      if modname.startswith('<') and modname.endswith('>'):
     183          assert check_modname(frozenid), spec
     184          modname = modname[1:-1]
     185          assert check_modname(modname), spec
     186          if frozenid in knownids:
     187              pass
     188          elif pyfile:
     189              assert not os.path.isdir(pyfile), spec
     190          else:
     191              pyfile = _resolve_module(frozenid, ispkg=False)
     192          ispkg = True
     193      elif pyfile:
     194          assert check_modname(frozenid), spec
     195          assert not knownids or frozenid not in knownids, spec
     196          assert check_modname(modname), spec
     197          assert not os.path.isdir(pyfile), spec
     198          ispkg = False
     199      elif knownids and frozenid in knownids:
     200          assert check_modname(frozenid), spec
     201          assert check_modname(modname), spec
     202          ispkg = False
     203      else:
     204          assert not modname or check_modname(modname), spec
     205          resolved = iter(resolve_modules(frozenid))
     206          frozenid, pyfile, ispkg = next(resolved)
     207          if not modname:
     208              modname = frozenid
     209          if ispkg:
     210              pkgid = frozenid
     211              pkgname = modname
     212              pkgfiles = {pyfile: pkgid}
     213              def iter_subs():
     214                  for frozenid, pyfile, ispkg in resolved:
     215                      if pkgname:
     216                          modname = frozenid.replace(pkgid, pkgname, 1)
     217                      else:
     218                          modname = frozenid
     219                      if pyfile:
     220                          if pyfile in pkgfiles:
     221                              frozenid = pkgfiles[pyfile]
     222                              pyfile = None
     223                          elif ispkg:
     224                              pkgfiles[pyfile] = frozenid
     225                      yield frozenid, pyfile, modname, ispkg, section
     226              submodules = iter_subs()
     227  
     228      info = (frozenid, pyfile or None, modname, ispkg, section)
     229      return info, submodules
     230  
     231  
     232  #######################################
     233  # frozen source files
     234  
     235  class ESC[4;38;5;81mFrozenSource(ESC[4;38;5;149mnamedtuple('FrozenSource', 'id pyfile frozenfile deepfreezefile')):
     236  
     237      @classmethod
     238      def from_id(cls, frozenid, pyfile=None):
     239          if not pyfile:
     240              pyfile = os.path.join(STDLIB_DIR, *frozenid.split('.')) + '.py'
     241              #assert os.path.exists(pyfile), (frozenid, pyfile)
     242          frozenfile = resolve_frozen_file(frozenid, FROZEN_MODULES_DIR)
     243          deepfreezefile = resolve_frozen_file(frozenid, DEEPFROZEN_MODULES_DIR)
     244          return cls(frozenid, pyfile, frozenfile, deepfreezefile)
     245  
     246      @property
     247      def frozenid(self):
     248          return self.id
     249  
     250      @property
     251      def modname(self):
     252          if self.pyfile.startswith(STDLIB_DIR):
     253              return self.id
     254          return None
     255  
     256      @property
     257      def symbol(self):
     258          # This matches what we do in Programs/_freeze_module.c:
     259          name = self.frozenid.replace('.', '_')
     260          return '_Py_M__' + name
     261  
     262      @property
     263      def ispkg(self):
     264          if not self.pyfile:
     265              return False
     266          elif self.frozenid.endswith('.__init__'):
     267              return False
     268          else:
     269              return os.path.basename(self.pyfile) == '__init__.py'
     270  
     271      @property
     272      def isbootstrap(self):
     273          return self.id in BOOTSTRAP
     274  
     275  
     276  def resolve_frozen_file(frozenid, destdir):
     277      """Return the filename corresponding to the given frozen ID.
     278  
     279      For stdlib modules the ID will always be the full name
     280      of the source module.
     281      """
     282      if not isinstance(frozenid, str):
     283          try:
     284              frozenid = frozenid.frozenid
     285          except AttributeError:
     286              raise ValueError(f'unsupported frozenid {frozenid!r}')
     287      # We use a consistent naming convention for all frozen modules.
     288      frozenfile = f'{frozenid}.h'
     289      if not destdir:
     290          return frozenfile
     291      return os.path.join(destdir, frozenfile)
     292  
     293  
     294  #######################################
     295  # frozen modules
     296  
     297  class ESC[4;38;5;81mFrozenModule(ESC[4;38;5;149mnamedtuple('FrozenModule', 'name ispkg section source')):
     298  
     299      def __getattr__(self, name):
     300          return getattr(self.source, name)
     301  
     302      @property
     303      def modname(self):
     304          return self.name
     305  
     306      @property
     307      def orig(self):
     308          return self.source.modname
     309  
     310      @property
     311      def isalias(self):
     312          orig = self.source.modname
     313          if not orig:
     314              return True
     315          return self.name != orig
     316  
     317      def summarize(self):
     318          source = self.source.modname
     319          if source:
     320              source = f'<{source}>'
     321          else:
     322              source = relpath_for_posix_display(self.pyfile, ROOT_DIR)
     323          return {
     324              'module': self.name,
     325              'ispkg': self.ispkg,
     326              'source': source,
     327              'frozen': os.path.basename(self.frozenfile),
     328              'checksum': _get_checksum(self.frozenfile),
     329          }
     330  
     331  
     332  def _iter_sources(modules):
     333      seen = set()
     334      for mod in modules:
     335          if mod.source not in seen:
     336              yield mod.source
     337              seen.add(mod.source)
     338  
     339  
     340  #######################################
     341  # generic helpers
     342  
     343  def _get_checksum(filename):
     344      with open(filename, "rb") as infile:
     345          contents = infile.read()
     346      m = hashlib.sha256()
     347      m.update(contents)
     348      return m.hexdigest()
     349  
     350  
     351  def resolve_modules(modname, pyfile=None):
     352      if modname.startswith('<') and modname.endswith('>'):
     353          if pyfile:
     354              assert os.path.isdir(pyfile) or os.path.basename(pyfile) == '__init__.py', pyfile
     355          ispkg = True
     356          modname = modname[1:-1]
     357          rawname = modname
     358          # For now, we only expect match patterns at the end of the name.
     359          _modname, sep, match = modname.rpartition('.')
     360          if sep:
     361              if _modname.endswith('.**'):
     362                  modname = _modname[:-3]
     363                  match = f'**.{match}'
     364              elif match and not match.isidentifier():
     365                  modname = _modname
     366              # Otherwise it's a plain name so we leave it alone.
     367          else:
     368              match = None
     369      else:
     370          ispkg = False
     371          rawname = modname
     372          match = None
     373  
     374      if not check_modname(modname):
     375          raise ValueError(f'not a valid module name ({rawname})')
     376  
     377      if not pyfile:
     378          pyfile = _resolve_module(modname, ispkg=ispkg)
     379      elif os.path.isdir(pyfile):
     380          pyfile = _resolve_module(modname, pyfile, ispkg)
     381      yield modname, pyfile, ispkg
     382  
     383      if match:
     384          pkgdir = os.path.dirname(pyfile)
     385          yield from iter_submodules(modname, pkgdir, match)
     386  
     387  
     388  def check_modname(modname):
     389      return all(n.isidentifier() for n in modname.split('.'))
     390  
     391  
     392  def iter_submodules(pkgname, pkgdir=None, match='*'):
     393      if not pkgdir:
     394          pkgdir = os.path.join(STDLIB_DIR, *pkgname.split('.'))
     395      if not match:
     396          match = '**.*'
     397      match_modname = _resolve_modname_matcher(match, pkgdir)
     398  
     399      def _iter_submodules(pkgname, pkgdir):
     400          for entry in sorted(os.scandir(pkgdir), key=lambda e: e.name):
     401              matched, recursive = match_modname(entry.name)
     402              if not matched:
     403                  continue
     404              modname = f'{pkgname}.{entry.name}'
     405              if modname.endswith('.py'):
     406                  yield modname[:-3], entry.path, False
     407              elif entry.is_dir():
     408                  pyfile = os.path.join(entry.path, '__init__.py')
     409                  # We ignore namespace packages.
     410                  if os.path.exists(pyfile):
     411                      yield modname, pyfile, True
     412                      if recursive:
     413                          yield from _iter_submodules(modname, entry.path)
     414  
     415      return _iter_submodules(pkgname, pkgdir)
     416  
     417  
     418  def _resolve_modname_matcher(match, rootdir=None):
     419      if isinstance(match, str):
     420          if match.startswith('**.'):
     421              recursive = True
     422              pat = match[3:]
     423              assert match
     424          else:
     425              recursive = False
     426              pat = match
     427  
     428          if pat == '*':
     429              def match_modname(modname):
     430                  return True, recursive
     431          else:
     432              raise NotImplementedError(match)
     433      elif callable(match):
     434          match_modname = match(rootdir)
     435      else:
     436          raise ValueError(f'unsupported matcher {match!r}')
     437      return match_modname
     438  
     439  
     440  def _resolve_module(modname, pathentry=STDLIB_DIR, ispkg=False):
     441      assert pathentry, pathentry
     442      pathentry = os.path.normpath(pathentry)
     443      assert os.path.isabs(pathentry)
     444      if ispkg:
     445          return os.path.join(pathentry, *modname.split('.'), '__init__.py')
     446      return os.path.join(pathentry, *modname.split('.')) + '.py'
     447  
     448  
     449  #######################################
     450  # regenerating dependent files
     451  
     452  def find_marker(lines, marker, file):
     453      for pos, line in enumerate(lines):
     454          if marker in line:
     455              return pos
     456      raise Exception(f"Can't find {marker!r} in file {file}")
     457  
     458  
     459  def replace_block(lines, start_marker, end_marker, replacements, file):
     460      start_pos = find_marker(lines, start_marker, file)
     461      end_pos = find_marker(lines, end_marker, file)
     462      if end_pos <= start_pos:
     463          raise Exception(f"End marker {end_marker!r} "
     464                          f"occurs before start marker {start_marker!r} "
     465                          f"in file {file}")
     466      replacements = [line.rstrip() + '\n' for line in replacements]
     467      return lines[:start_pos + 1] + replacements + lines[end_pos:]
     468  
     469  
     470  def regen_frozen(modules, frozen_modules: bool):
     471      headerlines = []
     472      parentdir = os.path.dirname(FROZEN_FILE)
     473      if frozen_modules:
     474          for src in _iter_sources(modules):
     475              # Adding a comment to separate sections here doesn't add much,
     476              # so we don't.
     477              header = relpath_for_posix_display(src.frozenfile, parentdir)
     478              headerlines.append(f'#include "{header}"')
     479  
     480      externlines = []
     481      bootstraplines = []
     482      stdliblines = []
     483      testlines = []
     484      aliaslines = []
     485      indent = '    '
     486      lastsection = None
     487      for mod in modules:
     488          if mod.isbootstrap:
     489              lines = bootstraplines
     490          elif mod.section == TESTS_SECTION:
     491              lines = testlines
     492          else:
     493              lines = stdliblines
     494              if mod.section != lastsection:
     495                  if lastsection is not None:
     496                      lines.append('')
     497                  lines.append(f'/* {mod.section} */')
     498              lastsection = mod.section
     499  
     500          # Also add a extern declaration for the corresponding
     501          # deepfreeze-generated function.
     502          orig_name = mod.source.id
     503          code_name = orig_name.replace(".", "_")
     504          get_code_name = "_Py_get_%s_toplevel" % code_name
     505          externlines.append("extern PyObject *%s(void);" % get_code_name)
     506  
     507          symbol = mod.symbol
     508          pkg = 'true' if mod.ispkg else 'false'
     509          if not frozen_modules:
     510              line = ('{"%s", NULL, 0, %s, GET_CODE(%s)},'
     511                  ) % (mod.name, pkg, code_name)
     512          else:
     513              line = ('{"%s", %s, (int)sizeof(%s), %s, GET_CODE(%s)},'
     514                  ) % (mod.name, symbol, symbol, pkg, code_name)
     515          lines.append(line)
     516  
     517          if mod.isalias:
     518              if not mod.orig:
     519                  entry = '{"%s", NULL},' % (mod.name,)
     520              elif mod.source.ispkg:
     521                  entry = '{"%s", "<%s"},' % (mod.name, mod.orig)
     522              else:
     523                  entry = '{"%s", "%s"},' % (mod.name, mod.orig)
     524              aliaslines.append(indent + entry)
     525  
     526      for lines in (bootstraplines, stdliblines, testlines):
     527          # TODO: Is this necessary any more?
     528          if lines and not lines[0]:
     529              del lines[0]
     530          for i, line in enumerate(lines):
     531              if line:
     532                  lines[i] = indent + line
     533  
     534      print(f'# Updating {os.path.relpath(FROZEN_FILE)}')
     535      with updating_file_with_tmpfile(FROZEN_FILE) as (infile, outfile):
     536          lines = infile.readlines()
     537          # TODO: Use more obvious markers, e.g.
     538          # $START GENERATED FOOBAR$ / $END GENERATED FOOBAR$
     539          lines = replace_block(
     540              lines,
     541              "/* Includes for frozen modules: */",
     542              "/* End includes */",
     543              headerlines,
     544              FROZEN_FILE,
     545          )
     546          lines = replace_block(
     547              lines,
     548              "/* Start extern declarations */",
     549              "/* End extern declarations */",
     550              externlines,
     551              FROZEN_FILE,
     552          )
     553          lines = replace_block(
     554              lines,
     555              "static const struct _frozen bootstrap_modules[] =",
     556              "/* bootstrap sentinel */",
     557              bootstraplines,
     558              FROZEN_FILE,
     559          )
     560          lines = replace_block(
     561              lines,
     562              "static const struct _frozen stdlib_modules[] =",
     563              "/* stdlib sentinel */",
     564              stdliblines,
     565              FROZEN_FILE,
     566          )
     567          lines = replace_block(
     568              lines,
     569              "static const struct _frozen test_modules[] =",
     570              "/* test sentinel */",
     571              testlines,
     572              FROZEN_FILE,
     573          )
     574          lines = replace_block(
     575              lines,
     576              "const struct _module_alias aliases[] =",
     577              "/* aliases sentinel */",
     578              aliaslines,
     579              FROZEN_FILE,
     580          )
     581          outfile.writelines(lines)
     582  
     583  
     584  def regen_makefile(modules):
     585      pyfiles = []
     586      frozenfiles = []
     587      rules = ['']
     588      deepfreezerules = ["$(DEEPFREEZE_C): $(DEEPFREEZE_DEPS)",
     589                         "\t$(PYTHON_FOR_FREEZE) $(srcdir)/Tools/build/deepfreeze.py \\"]
     590      for src in _iter_sources(modules):
     591          frozen_header = relpath_for_posix_display(src.frozenfile, ROOT_DIR)
     592          frozenfiles.append(f'\t\t{frozen_header} \\')
     593  
     594          pyfile = relpath_for_posix_display(src.pyfile, ROOT_DIR)
     595          pyfiles.append(f'\t\t{pyfile} \\')
     596  
     597          if src.isbootstrap:
     598              freezecmd = '$(FREEZE_MODULE_BOOTSTRAP)'
     599              freezedep = '$(FREEZE_MODULE_BOOTSTRAP_DEPS)'
     600          else:
     601              freezecmd = '$(FREEZE_MODULE)'
     602              freezedep = '$(FREEZE_MODULE_DEPS)'
     603  
     604          freeze = (f'{freezecmd} {src.frozenid} '
     605                      f'$(srcdir)/{pyfile} {frozen_header}')
     606          rules.extend([
     607              f'{frozen_header}: {pyfile} {freezedep}',
     608              f'\t{freeze}',
     609              '',
     610          ])
     611          deepfreezerules.append(f"\t{frozen_header}:{src.frozenid} \\")
     612      deepfreezerules.append('\t-o Python/deepfreeze/deepfreeze.c')
     613      pyfiles[-1] = pyfiles[-1].rstrip(" \\")
     614      frozenfiles[-1] = frozenfiles[-1].rstrip(" \\")
     615  
     616      print(f'# Updating {os.path.relpath(MAKEFILE)}')
     617      with updating_file_with_tmpfile(MAKEFILE) as (infile, outfile):
     618          lines = infile.readlines()
     619          lines = replace_block(
     620              lines,
     621              "FROZEN_FILES_IN =",
     622              "# End FROZEN_FILES_IN",
     623              pyfiles,
     624              MAKEFILE,
     625          )
     626          lines = replace_block(
     627              lines,
     628              "FROZEN_FILES_OUT =",
     629              "# End FROZEN_FILES_OUT",
     630              frozenfiles,
     631              MAKEFILE,
     632          )
     633          lines = replace_block(
     634              lines,
     635              "# BEGIN: freezing modules",
     636              "# END: freezing modules",
     637              rules,
     638              MAKEFILE,
     639          )
     640          lines = replace_block(
     641              lines,
     642              "# BEGIN: deepfreeze modules",
     643              "# END: deepfreeze modules",
     644              deepfreezerules,
     645              MAKEFILE,
     646          )
     647          outfile.writelines(lines)
     648  
     649  
     650  def regen_pcbuild(modules):
     651      projlines = []
     652      filterlines = []
     653      corelines = []
     654      deepfreezerules = ['\t<Exec Command=\'$(PythonForBuild) "$(PySourcePath)Tools\\build\\deepfreeze.py" ^']
     655      for src in _iter_sources(modules):
     656          pyfile = relpath_for_windows_display(src.pyfile, ROOT_DIR)
     657          header = relpath_for_windows_display(src.frozenfile, ROOT_DIR)
     658          intfile = ntpath.splitext(ntpath.basename(header))[0] + '.g.h'
     659          projlines.append(f'    <None Include="..\\{pyfile}">')
     660          projlines.append(f'      <ModName>{src.frozenid}</ModName>')
     661          projlines.append(f'      <IntFile>$(IntDir){intfile}</IntFile>')
     662          projlines.append(f'      <OutFile>$(PySourcePath){header}</OutFile>')
     663          projlines.append(f'    </None>')
     664  
     665          filterlines.append(f'    <None Include="..\\{pyfile}">')
     666          filterlines.append('      <Filter>Python Files</Filter>')
     667          filterlines.append('    </None>')
     668          deepfreezerules.append(f'\t\t "$(PySourcePath){header}:{src.frozenid}" ^')
     669      deepfreezerules.append('\t\t "-o" "$(PySourcePath)Python\\deepfreeze\\deepfreeze.c"\'/>' )
     670  
     671      corelines.append(f'    <ClCompile Include="..\\Python\\deepfreeze\\deepfreeze.c" />')
     672  
     673      print(f'# Updating {os.path.relpath(PCBUILD_PROJECT)}')
     674      with updating_file_with_tmpfile(PCBUILD_PROJECT) as (infile, outfile):
     675          lines = infile.readlines()
     676          lines = replace_block(
     677              lines,
     678              '<!-- BEGIN frozen modules -->',
     679              '<!-- END frozen modules -->',
     680              projlines,
     681              PCBUILD_PROJECT,
     682          )
     683          outfile.writelines(lines)
     684      with updating_file_with_tmpfile(PCBUILD_PROJECT) as (infile, outfile):
     685          lines = infile.readlines()
     686          lines = replace_block(
     687              lines,
     688              '<!-- BEGIN deepfreeze rule -->',
     689              '<!-- END deepfreeze rule -->',
     690              deepfreezerules,
     691              PCBUILD_PROJECT,
     692          )
     693          outfile.writelines(lines)
     694      print(f'# Updating {os.path.relpath(PCBUILD_FILTERS)}')
     695      with updating_file_with_tmpfile(PCBUILD_FILTERS) as (infile, outfile):
     696          lines = infile.readlines()
     697          lines = replace_block(
     698              lines,
     699              '<!-- BEGIN frozen modules -->',
     700              '<!-- END frozen modules -->',
     701              filterlines,
     702              PCBUILD_FILTERS,
     703          )
     704          outfile.writelines(lines)
     705      print(f'# Updating {os.path.relpath(PCBUILD_PYTHONCORE)}')
     706      with updating_file_with_tmpfile(PCBUILD_PYTHONCORE) as (infile, outfile):
     707          lines = infile.readlines()
     708          lines = replace_block(
     709              lines,
     710              '<!-- BEGIN deepfreeze -->',
     711              '<!-- END deepfreeze -->',
     712              corelines,
     713              PCBUILD_FILTERS,
     714          )
     715          outfile.writelines(lines)
     716  
     717  
     718  #######################################
     719  # the script
     720  
     721  parser = argparse.ArgumentParser()
     722  parser.add_argument("--frozen-modules", action="store_true",
     723          help="Use both frozen and deepfrozen modules. (default: uses only deepfrozen modules)")
     724  
     725  def main():
     726      args = parser.parse_args()
     727      frozen_modules: bool = args.frozen_modules
     728      # Expand the raw specs, preserving order.
     729      modules = list(parse_frozen_specs())
     730  
     731      # Regen build-related files.
     732      regen_makefile(modules)
     733      regen_pcbuild(modules)
     734      regen_frozen(modules, frozen_modules)
     735  
     736  
     737  if __name__ == '__main__':
     738      main()