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