bpo-34977: Add Windows App Store package (GH-11027)

Also adds the PC/layout script for generating layouts on Windows.
diff --git a/PC/layout/__init__.py b/PC/layout/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/PC/layout/__init__.py
diff --git a/PC/layout/__main__.py b/PC/layout/__main__.py
new file mode 100644
index 0000000..f7aa1e6
--- /dev/null
+++ b/PC/layout/__main__.py
@@ -0,0 +1,14 @@
+import sys
+
+try:
+    import layout
+except ImportError:
+    # Failed to import our package, which likely means we were started directly
+    # Add the additional search path needed to locate our module.
+    from pathlib import Path
+
+    sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+
+from layout.main import main
+
+sys.exit(int(main() or 0))
diff --git a/PC/layout/main.py b/PC/layout/main.py
new file mode 100644
index 0000000..217b2b0
--- /dev/null
+++ b/PC/layout/main.py
@@ -0,0 +1,616 @@
+"""
+Generates a layout of Python for Windows from a build.
+
+See python make_layout.py --help for usage.
+"""
+
+__author__ = "Steve Dower <steve.dower@python.org>"
+__version__ = "3.8"
+
+import argparse
+import functools
+import os
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+import zipfile
+
+from pathlib import Path
+
+if __name__ == "__main__":
+    # Started directly, so enable relative imports
+    __path__ = [str(Path(__file__).resolve().parent)]
+
+from .support.appxmanifest import *
+from .support.catalog import *
+from .support.constants import *
+from .support.filesets import *
+from .support.logging import *
+from .support.options import *
+from .support.pip import *
+from .support.props import *
+
+BDIST_WININST_FILES_ONLY = FileNameSet("wininst-*", "bdist_wininst.py")
+BDIST_WININST_STUB = "PC/layout/support/distutils.command.bdist_wininst.py"
+
+TEST_PYDS_ONLY = FileStemSet("xxlimited", "_ctypes_test", "_test*")
+TEST_DIRS_ONLY = FileNameSet("test", "tests")
+
+IDLE_DIRS_ONLY = FileNameSet("idlelib")
+
+TCLTK_PYDS_ONLY = FileStemSet("tcl*", "tk*", "_tkinter")
+TCLTK_DIRS_ONLY = FileNameSet("tkinter", "turtledemo")
+TCLTK_FILES_ONLY = FileNameSet("turtle.py")
+
+VENV_DIRS_ONLY = FileNameSet("venv", "ensurepip")
+
+EXCLUDE_FROM_PYDS = FileStemSet("python*", "pyshellext")
+EXCLUDE_FROM_LIB = FileNameSet("*.pyc", "__pycache__", "*.pickle")
+EXCLUDE_FROM_PACKAGED_LIB = FileNameSet("readme.txt")
+EXCLUDE_FROM_COMPILE = FileNameSet("badsyntax_*", "bad_*")
+EXCLUDE_FROM_CATALOG = FileSuffixSet(".exe", ".pyd", ".dll")
+
+REQUIRED_DLLS = FileStemSet("libcrypto*", "libssl*")
+
+LIB2TO3_GRAMMAR_FILES = FileNameSet("Grammar.txt", "PatternGrammar.txt")
+
+PY_FILES = FileSuffixSet(".py")
+PYC_FILES = FileSuffixSet(".pyc")
+CAT_FILES = FileSuffixSet(".cat")
+CDF_FILES = FileSuffixSet(".cdf")
+
+DATA_DIRS = FileNameSet("data")
+
+TOOLS_DIRS = FileNameSet("scripts", "i18n", "pynche", "demo", "parser")
+TOOLS_FILES = FileSuffixSet(".py", ".pyw", ".txt")
+
+
+def get_lib_layout(ns):
+    def _c(f):
+        if f in EXCLUDE_FROM_LIB:
+            return False
+        if f.is_dir():
+            if f in TEST_DIRS_ONLY:
+                return ns.include_tests
+            if f in TCLTK_DIRS_ONLY:
+                return ns.include_tcltk
+            if f in IDLE_DIRS_ONLY:
+                return ns.include_idle
+            if f in VENV_DIRS_ONLY:
+                return ns.include_venv
+        else:
+            if f in TCLTK_FILES_ONLY:
+                return ns.include_tcltk
+            if f in BDIST_WININST_FILES_ONLY:
+                return ns.include_bdist_wininst
+        return True
+
+    for dest, src in rglob(ns.source / "Lib", "**/*", _c):
+        yield dest, src
+
+    if not ns.include_bdist_wininst:
+        src = ns.source / BDIST_WININST_STUB
+        yield Path("distutils/command/bdist_wininst.py"), src
+
+
+def get_tcltk_lib(ns):
+    if not ns.include_tcltk:
+        return
+
+    tcl_lib = os.getenv("TCL_LIBRARY")
+    if not tcl_lib or not os.path.isdir(tcl_lib):
+        try:
+            with open(ns.build / "TCL_LIBRARY.env", "r", encoding="utf-8-sig") as f:
+                tcl_lib = f.read().strip()
+        except FileNotFoundError:
+            pass
+        if not tcl_lib or not os.path.isdir(tcl_lib):
+            warn("Failed to find TCL_LIBRARY")
+            return
+
+    for dest, src in rglob(Path(tcl_lib).parent, "**/*"):
+        yield "tcl/{}".format(dest), src
+
+
+def get_layout(ns):
+    def in_build(f, dest="", new_name=None):
+        n, _, x = f.rpartition(".")
+        n = new_name or n
+        src = ns.build / f
+        if ns.debug and src not in REQUIRED_DLLS:
+            if not src.stem.endswith("_d"):
+                src = src.parent / (src.stem + "_d" + src.suffix)
+            if not n.endswith("_d"):
+                n += "_d"
+                f = n + "." + x
+        yield dest + n + "." + x, src
+        if ns.include_symbols:
+            pdb = src.with_suffix(".pdb")
+            if pdb.is_file():
+                yield dest + n + ".pdb", pdb
+        if ns.include_dev:
+            lib = src.with_suffix(".lib")
+            if lib.is_file():
+                yield "libs/" + n + ".lib", lib
+
+    if ns.include_appxmanifest:
+        yield from in_build("python_uwp.exe", new_name="python")
+        yield from in_build("pythonw_uwp.exe", new_name="pythonw")
+    else:
+        yield from in_build("python.exe", new_name="python")
+        yield from in_build("pythonw.exe", new_name="pythonw")
+
+    yield from in_build(PYTHON_DLL_NAME)
+
+    if ns.include_launchers and ns.include_appxmanifest:
+        if ns.include_pip:
+            yield from in_build("python_uwp.exe", new_name="pip")
+        if ns.include_idle:
+            yield from in_build("pythonw_uwp.exe", new_name="idle")
+
+    if ns.include_stable:
+        yield from in_build(PYTHON_STABLE_DLL_NAME)
+
+    for dest, src in rglob(ns.build, "vcruntime*.dll"):
+        yield dest, src
+
+    for dest, src in rglob(ns.build, ("*.pyd", "*.dll")):
+        if src.stem.endswith("_d") != bool(ns.debug) and src not in REQUIRED_DLLS:
+            continue
+        if src in EXCLUDE_FROM_PYDS:
+            continue
+        if src in TEST_PYDS_ONLY and not ns.include_tests:
+            continue
+        if src in TCLTK_PYDS_ONLY and not ns.include_tcltk:
+            continue
+
+        yield from in_build(src.name, dest="" if ns.flat_dlls else "DLLs/")
+
+    if ns.zip_lib:
+        zip_name = PYTHON_ZIP_NAME
+        yield zip_name, ns.temp / zip_name
+    else:
+        for dest, src in get_lib_layout(ns):
+            yield "Lib/{}".format(dest), src
+
+        if ns.include_venv:
+            yield from in_build("venvlauncher.exe", "Lib/venv/scripts/nt/", "python")
+            yield from in_build("venvwlauncher.exe", "Lib/venv/scripts/nt/", "pythonw")
+
+    if ns.include_tools:
+
+        def _c(d):
+            if d.is_dir():
+                return d in TOOLS_DIRS
+            return d in TOOLS_FILES
+
+        for dest, src in rglob(ns.source / "Tools", "**/*", _c):
+            yield "Tools/{}".format(dest), src
+
+    if ns.include_underpth:
+        yield PYTHON_PTH_NAME, ns.temp / PYTHON_PTH_NAME
+
+    if ns.include_dev:
+
+        def _c(d):
+            if d.is_dir():
+                return d.name != "internal"
+            return True
+
+        for dest, src in rglob(ns.source / "Include", "**/*.h", _c):
+            yield "include/{}".format(dest), src
+        src = ns.source / "PC" / "pyconfig.h"
+        yield "include/pyconfig.h", src
+
+    for dest, src in get_tcltk_lib(ns):
+        yield dest, src
+
+    if ns.include_pip:
+        pip_dir = get_pip_dir(ns)
+        if not pip_dir.is_dir():
+            log_warning("Failed to find {} - pip will not be included", pip_dir)
+        else:
+            pkg_root = "packages/{}" if ns.zip_lib else "Lib/site-packages/{}"
+            for dest, src in rglob(pip_dir, "**/*"):
+                if src in EXCLUDE_FROM_LIB or src in EXCLUDE_FROM_PACKAGED_LIB:
+                    continue
+                yield pkg_root.format(dest), src
+
+    if ns.include_chm:
+        for dest, src in rglob(ns.doc_build / "htmlhelp", PYTHON_CHM_NAME):
+            yield "Doc/{}".format(dest), src
+
+    if ns.include_html_doc:
+        for dest, src in rglob(ns.doc_build / "html", "**/*"):
+            yield "Doc/html/{}".format(dest), src
+
+    if ns.include_props:
+        for dest, src in get_props_layout(ns):
+            yield dest, src
+
+    for dest, src in get_appx_layout(ns):
+        yield dest, src
+
+    if ns.include_cat:
+        if ns.flat_dlls:
+            yield ns.include_cat.name, ns.include_cat
+        else:
+            yield "DLLs/{}".format(ns.include_cat.name), ns.include_cat
+
+
+def _compile_one_py(src, dest, name, optimize):
+    import py_compile
+
+    if dest is not None:
+        dest = str(dest)
+
+    try:
+        return Path(
+            py_compile.compile(
+                str(src),
+                dest,
+                str(name),
+                doraise=True,
+                optimize=optimize,
+                invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH,
+            )
+        )
+    except py_compile.PyCompileError:
+        log_warning("Failed to compile {}", src)
+        return None
+
+
+def _py_temp_compile(src, ns, dest_dir=None):
+    if not ns.precompile or src not in PY_FILES or src.parent in DATA_DIRS:
+        return None
+
+    dest = (dest_dir or ns.temp) / (src.stem + ".py")
+    return _compile_one_py(src, dest.with_suffix(".pyc"), dest, optimize=2)
+
+
+def _write_to_zip(zf, dest, src, ns):
+    pyc = _py_temp_compile(src, ns)
+    if pyc:
+        try:
+            zf.write(str(pyc), dest.with_suffix(".pyc"))
+        finally:
+            try:
+                pyc.unlink()
+            except:
+                log_exception("Failed to delete {}", pyc)
+        return
+
+    if src in LIB2TO3_GRAMMAR_FILES:
+        from lib2to3.pgen2.driver import load_grammar
+
+        tmp = ns.temp / src.name
+        try:
+            shutil.copy(src, tmp)
+            load_grammar(str(tmp))
+            for f in ns.temp.glob(src.stem + "*.pickle"):
+                zf.write(str(f), str(dest.parent / f.name))
+                try:
+                    f.unlink()
+                except:
+                    log_exception("Failed to delete {}", f)
+        except:
+            log_exception("Failed to compile {}", src)
+        finally:
+            try:
+                tmp.unlink()
+            except:
+                log_exception("Failed to delete {}", tmp)
+
+    zf.write(str(src), str(dest))
+
+
+def generate_source_files(ns):
+    if ns.zip_lib:
+        zip_name = PYTHON_ZIP_NAME
+        zip_path = ns.temp / zip_name
+        if zip_path.is_file():
+            zip_path.unlink()
+        elif zip_path.is_dir():
+            log_error(
+                "Cannot create zip file because a directory exists by the same name"
+            )
+            return
+        log_info("Generating {} in {}", zip_name, ns.temp)
+        ns.temp.mkdir(parents=True, exist_ok=True)
+        with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
+            for dest, src in get_lib_layout(ns):
+                _write_to_zip(zf, dest, src, ns)
+
+    if ns.include_underpth:
+        log_info("Generating {} in {}", PYTHON_PTH_NAME, ns.temp)
+        ns.temp.mkdir(parents=True, exist_ok=True)
+        with open(ns.temp / PYTHON_PTH_NAME, "w", encoding="utf-8") as f:
+            if ns.zip_lib:
+                print(PYTHON_ZIP_NAME, file=f)
+                if ns.include_pip:
+                    print("packages", file=f)
+            else:
+                print("Lib", file=f)
+                print("Lib/site-packages", file=f)
+            if not ns.flat_dlls:
+                print("DLLs", file=f)
+            print(".", file=f)
+            print(file=f)
+            print("# Uncomment to run site.main() automatically", file=f)
+            print("#import site", file=f)
+
+    if ns.include_appxmanifest:
+        log_info("Generating AppxManifest.xml in {}", ns.temp)
+        ns.temp.mkdir(parents=True, exist_ok=True)
+
+        with open(ns.temp / "AppxManifest.xml", "wb") as f:
+            f.write(get_appxmanifest(ns))
+
+        with open(ns.temp / "_resources.xml", "wb") as f:
+            f.write(get_resources_xml(ns))
+
+    if ns.include_pip:
+        pip_dir = get_pip_dir(ns)
+        if not (pip_dir / "pip").is_dir():
+            log_info("Extracting pip to {}", pip_dir)
+            pip_dir.mkdir(parents=True, exist_ok=True)
+            extract_pip_files(ns)
+
+    if ns.include_props:
+        log_info("Generating {} in {}", PYTHON_PROPS_NAME, ns.temp)
+        ns.temp.mkdir(parents=True, exist_ok=True)
+        with open(ns.temp / PYTHON_PROPS_NAME, "wb") as f:
+            f.write(get_props(ns))
+
+
+def _create_zip_file(ns):
+    if not ns.zip:
+        return None
+
+    if ns.zip.is_file():
+        try:
+            ns.zip.unlink()
+        except OSError:
+            log_exception("Unable to remove {}", ns.zip)
+            sys.exit(8)
+    elif ns.zip.is_dir():
+        log_error("Cannot create ZIP file because {} is a directory", ns.zip)
+        sys.exit(8)
+
+    ns.zip.parent.mkdir(parents=True, exist_ok=True)
+    return zipfile.ZipFile(ns.zip, "w", zipfile.ZIP_DEFLATED)
+
+
+def copy_files(files, ns):
+    if ns.copy:
+        ns.copy.mkdir(parents=True, exist_ok=True)
+
+    try:
+        total = len(files)
+    except TypeError:
+        total = None
+    count = 0
+
+    zip_file = _create_zip_file(ns)
+    try:
+        need_compile = []
+        in_catalog = []
+
+        for dest, src in files:
+            count += 1
+            if count % 10 == 0:
+                if total:
+                    log_info("Processed {:>4} of {} files", count, total)
+                else:
+                    log_info("Processed {} files", count)
+            log_debug("Processing {!s}", src)
+
+            if (
+                ns.precompile
+                and src in PY_FILES
+                and src not in EXCLUDE_FROM_COMPILE
+                and src.parent not in DATA_DIRS
+                and os.path.normcase(str(dest)).startswith(os.path.normcase("Lib"))
+            ):
+                if ns.copy:
+                    need_compile.append((dest, ns.copy / dest))
+                else:
+                    (ns.temp / "Lib" / dest).parent.mkdir(parents=True, exist_ok=True)
+                    shutil.copy2(src, ns.temp / "Lib" / dest)
+                    need_compile.append((dest, ns.temp / "Lib" / dest))
+
+            if src not in EXCLUDE_FROM_CATALOG:
+                in_catalog.append((src.name, src))
+
+            if ns.copy:
+                log_debug("Copy {} -> {}", src, ns.copy / dest)
+                (ns.copy / dest).parent.mkdir(parents=True, exist_ok=True)
+                try:
+                    shutil.copy2(src, ns.copy / dest)
+                except shutil.SameFileError:
+                    pass
+
+            if ns.zip:
+                log_debug("Zip {} into {}", src, ns.zip)
+                zip_file.write(src, str(dest))
+
+        if need_compile:
+            for dest, src in need_compile:
+                compiled = [
+                    _compile_one_py(src, None, dest, optimize=0),
+                    _compile_one_py(src, None, dest, optimize=1),
+                    _compile_one_py(src, None, dest, optimize=2),
+                ]
+                for c in compiled:
+                    if not c:
+                        continue
+                    cdest = Path(dest).parent / Path(c).relative_to(src.parent)
+                    if ns.zip:
+                        log_debug("Zip {} into {}", c, ns.zip)
+                        zip_file.write(c, str(cdest))
+                    in_catalog.append((cdest.name, cdest))
+
+        if ns.catalog:
+            # Just write out the CDF now. Compilation and signing is
+            # an extra step
+            log_info("Generating {}", ns.catalog)
+            ns.catalog.parent.mkdir(parents=True, exist_ok=True)
+            write_catalog(ns.catalog, in_catalog)
+
+    finally:
+        if zip_file:
+            zip_file.close()
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-v", help="Increase verbosity", action="count")
+    parser.add_argument(
+        "-s",
+        "--source",
+        metavar="dir",
+        help="The directory containing the repository root",
+        type=Path,
+        default=None,
+    )
+    parser.add_argument(
+        "-b", "--build", metavar="dir", help="Specify the build directory", type=Path
+    )
+    parser.add_argument(
+        "--doc-build",
+        metavar="dir",
+        help="Specify the docs build directory",
+        type=Path,
+        default=None,
+    )
+    parser.add_argument(
+        "--copy",
+        metavar="directory",
+        help="The name of the directory to copy an extracted layout to",
+        type=Path,
+        default=None,
+    )
+    parser.add_argument(
+        "--zip",
+        metavar="file",
+        help="The ZIP file to write all files to",
+        type=Path,
+        default=None,
+    )
+    parser.add_argument(
+        "--catalog",
+        metavar="file",
+        help="The CDF file to write catalog entries to",
+        type=Path,
+        default=None,
+    )
+    parser.add_argument(
+        "--log",
+        metavar="file",
+        help="Write all operations to the specified file",
+        type=Path,
+        default=None,
+    )
+    parser.add_argument(
+        "-t",
+        "--temp",
+        metavar="file",
+        help="A temporary working directory",
+        type=Path,
+        default=None,
+    )
+    parser.add_argument(
+        "-d", "--debug", help="Include debug build", action="store_true"
+    )
+    parser.add_argument(
+        "-p",
+        "--precompile",
+        help="Include .pyc files instead of .py",
+        action="store_true",
+    )
+    parser.add_argument(
+        "-z", "--zip-lib", help="Include library in a ZIP file", action="store_true"
+    )
+    parser.add_argument(
+        "--flat-dlls", help="Does not create a DLLs directory", action="store_true"
+    )
+    parser.add_argument(
+        "-a",
+        "--include-all",
+        help="Include all optional components",
+        action="store_true",
+    )
+    parser.add_argument(
+        "--include-cat",
+        metavar="file",
+        help="Specify the catalog file to include",
+        type=Path,
+        default=None,
+    )
+    for opt, help in get_argparse_options():
+        parser.add_argument(opt, help=help, action="store_true")
+
+    ns = parser.parse_args()
+    update_presets(ns)
+
+    ns.source = ns.source or (Path(__file__).resolve().parent.parent.parent)
+    ns.build = ns.build or Path(sys.executable).parent
+    ns.temp = ns.temp or Path(tempfile.mkdtemp())
+    ns.doc_build = ns.doc_build or (ns.source / "Doc" / "build")
+    if not ns.source.is_absolute():
+        ns.source = (Path.cwd() / ns.source).resolve()
+    if not ns.build.is_absolute():
+        ns.build = (Path.cwd() / ns.build).resolve()
+    if not ns.temp.is_absolute():
+        ns.temp = (Path.cwd() / ns.temp).resolve()
+    if not ns.doc_build.is_absolute():
+        ns.doc_build = (Path.cwd() / ns.doc_build).resolve()
+    if ns.include_cat and not ns.include_cat.is_absolute():
+        ns.include_cat = (Path.cwd() / ns.include_cat).resolve()
+
+    if ns.copy and not ns.copy.is_absolute():
+        ns.copy = (Path.cwd() / ns.copy).resolve()
+    if ns.zip and not ns.zip.is_absolute():
+        ns.zip = (Path.cwd() / ns.zip).resolve()
+    if ns.catalog and not ns.catalog.is_absolute():
+        ns.catalog = (Path.cwd() / ns.catalog).resolve()
+
+    configure_logger(ns)
+
+    log_info(
+        """OPTIONS
+Source: {ns.source}
+Build:  {ns.build}
+Temp:   {ns.temp}
+
+Copy to: {ns.copy}
+Zip to:  {ns.zip}
+Catalog: {ns.catalog}""",
+        ns=ns,
+    )
+
+    if ns.include_idle and not ns.include_tcltk:
+        log_warning("Assuming --include-tcltk to support --include-idle")
+        ns.include_tcltk = True
+
+    try:
+        generate_source_files(ns)
+        files = list(get_layout(ns))
+        copy_files(files, ns)
+    except KeyboardInterrupt:
+        log_info("Interrupted by Ctrl+C")
+        return 3
+    except SystemExit:
+        raise
+    except:
+        log_exception("Unhandled error")
+
+    if error_was_logged():
+        log_error("Errors occurred.")
+        return 1
+
+
+if __name__ == "__main__":
+    sys.exit(int(main() or 0))
diff --git a/PC/layout/support/__init__.py b/PC/layout/support/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/PC/layout/support/__init__.py
diff --git a/PC/layout/support/appxmanifest.py b/PC/layout/support/appxmanifest.py
new file mode 100644
index 0000000..c5dda70
--- /dev/null
+++ b/PC/layout/support/appxmanifest.py
@@ -0,0 +1,487 @@
+"""
+File generation for APPX/MSIX manifests.
+"""
+
+__author__ = "Steve Dower <steve.dower@python.org>"
+__version__ = "3.8"
+
+
+import collections
+import ctypes
+import io
+import os
+import sys
+
+from pathlib import Path, PureWindowsPath
+from xml.etree import ElementTree as ET
+
+from .constants import *
+
+__all__ = []
+
+
+def public(f):
+    __all__.append(f.__name__)
+    return f
+
+
+APPX_DATA = dict(
+    Name="PythonSoftwareFoundation.Python.{}".format(VER_DOT),
+    Version="{}.{}.{}.0".format(VER_MAJOR, VER_MINOR, VER_FIELD3),
+    Publisher=os.getenv(
+        "APPX_DATA_PUBLISHER", "CN=4975D53F-AA7E-49A5-8B49-EA4FDC1BB66B"
+    ),
+    DisplayName="Python {}".format(VER_DOT),
+    Description="The Python {} runtime and console.".format(VER_DOT),
+    ProcessorArchitecture="x64" if IS_X64 else "x86",
+)
+
+PYTHON_VE_DATA = dict(
+    DisplayName="Python {}".format(VER_DOT),
+    Description="Python interactive console",
+    Square150x150Logo="_resources/pythonx150.png",
+    Square44x44Logo="_resources/pythonx44.png",
+    BackgroundColor="transparent",
+)
+
+PYTHONW_VE_DATA = dict(
+    DisplayName="Python {} (Windowed)".format(VER_DOT),
+    Description="Python windowed app launcher",
+    Square150x150Logo="_resources/pythonwx150.png",
+    Square44x44Logo="_resources/pythonwx44.png",
+    BackgroundColor="transparent",
+    AppListEntry="none",
+)
+
+PIP_VE_DATA = dict(
+    DisplayName="pip (Python {})".format(VER_DOT),
+    Description="pip package manager for Python {}".format(VER_DOT),
+    Square150x150Logo="_resources/pythonx150.png",
+    Square44x44Logo="_resources/pythonx44.png",
+    BackgroundColor="transparent",
+    AppListEntry="none",
+)
+
+IDLE_VE_DATA = dict(
+    DisplayName="IDLE (Python {})".format(VER_DOT),
+    Description="IDLE editor for Python {}".format(VER_DOT),
+    Square150x150Logo="_resources/pythonwx150.png",
+    Square44x44Logo="_resources/pythonwx44.png",
+    BackgroundColor="transparent",
+)
+
+APPXMANIFEST_NS = {
+    "": "http://schemas.microsoft.com/appx/manifest/foundation/windows10",
+    "m": "http://schemas.microsoft.com/appx/manifest/foundation/windows10",
+    "uap": "http://schemas.microsoft.com/appx/manifest/uap/windows10",
+    "rescap": "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities",
+    "rescap4": "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities/4",
+    "desktop4": "http://schemas.microsoft.com/appx/manifest/desktop/windows10/4",
+    "desktop6": "http://schemas.microsoft.com/appx/manifest/desktop/windows10/6",
+    "uap3": "http://schemas.microsoft.com/appx/manifest/uap/windows10/3",
+    "uap4": "http://schemas.microsoft.com/appx/manifest/uap/windows10/4",
+    "uap5": "http://schemas.microsoft.com/appx/manifest/uap/windows10/5",
+}
+
+APPXMANIFEST_TEMPLATE = """<?xml version="1.0" encoding="utf-8"?>
+<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
+    xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
+    xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
+    xmlns:rescap4="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities/4"
+    xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4"
+    xmlns:uap4="http://schemas.microsoft.com/appx/manifest/uap/windows10/4"
+    xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5">
+    <Identity Name=""
+              Version=""
+              Publisher=""
+              ProcessorArchitecture="" />
+    <Properties>
+        <DisplayName></DisplayName>
+        <PublisherDisplayName>Python Software Foundation</PublisherDisplayName>
+        <Description></Description>
+        <Logo>_resources/pythonx50.png</Logo>
+    </Properties>
+    <Resources>
+        <Resource Language="en-US" />
+    </Resources>
+    <Dependencies>
+        <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="" />
+    </Dependencies>
+    <Capabilities>
+        <rescap:Capability Name="runFullTrust"/>
+    </Capabilities>
+    <Applications>
+    </Applications>
+    <Extensions>
+    </Extensions>
+</Package>"""
+
+
+RESOURCES_XML_TEMPLATE = r"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--This file is input for makepri.exe. It should be excluded from the final package.-->
+<resources targetOsVersion="10.0.0" majorVersion="1">
+    <packaging>
+        <autoResourcePackage qualifier="Language"/>
+        <autoResourcePackage qualifier="Scale"/>
+        <autoResourcePackage qualifier="DXFeatureLevel"/>
+    </packaging>
+    <index root="\" startIndexAt="\">
+        <default>
+            <qualifier name="Language" value="en-US"/>
+            <qualifier name="Contrast" value="standard"/>
+            <qualifier name="Scale" value="100"/>
+            <qualifier name="HomeRegion" value="001"/>
+            <qualifier name="TargetSize" value="256"/>
+            <qualifier name="LayoutDirection" value="LTR"/>
+            <qualifier name="Theme" value="dark"/>
+            <qualifier name="AlternateForm" value=""/>
+            <qualifier name="DXFeatureLevel" value="DX9"/>
+            <qualifier name="Configuration" value=""/>
+            <qualifier name="DeviceFamily" value="Universal"/>
+            <qualifier name="Custom" value=""/>
+        </default>
+        <indexer-config type="folder" foldernameAsQualifier="true" filenameAsQualifier="true" qualifierDelimiter="$"/>
+        <indexer-config type="resw" convertDotsToSlashes="true" initialPath=""/>
+        <indexer-config type="resjson" initialPath=""/>
+        <indexer-config type="PRI"/>
+    </index>
+</resources>"""
+
+
+SCCD_FILENAME = "PC/classicAppCompat.sccd"
+
+REGISTRY = {
+    "HKCU\\Software\\Python\\PythonCore": {
+        VER_DOT: {
+            "DisplayName": APPX_DATA["DisplayName"],
+            "SupportUrl": "https://www.python.org/",
+            "SysArchitecture": "64bit" if IS_X64 else "32bit",
+            "SysVersion": VER_DOT,
+            "Version": "{}.{}.{}".format(VER_MAJOR, VER_MINOR, VER_MICRO),
+            "InstallPath": {
+                # I have no idea why the trailing spaces are needed, but they seem to be needed.
+                "": "[{AppVPackageRoot}][                    ]",
+                "ExecutablePath": "[{AppVPackageRoot}]python.exe[                    ]",
+                "WindowedExecutablePath": "[{AppVPackageRoot}]pythonw.exe[                    ]",
+            },
+            "Help": {
+                "Main Python Documentation": {
+                    "_condition": lambda ns: ns.include_chm,
+                    "": "[{{AppVPackageRoot}}]Doc\\{}[                    ]".format(
+                        PYTHON_CHM_NAME
+                    ),
+                },
+                "Local Python Documentation": {
+                    "_condition": lambda ns: ns.include_html_doc,
+                    "": "[{AppVPackageRoot}]Doc\\html\\index.html[                    ]",
+                },
+                "Online Python Documentation": {
+                    "": "https://docs.python.org/{}".format(VER_DOT)
+                },
+            },
+            "Idle": {
+                "_condition": lambda ns: ns.include_idle,
+                "": "[{AppVPackageRoot}]Lib\\idlelib\\idle.pyw[                    ]",
+            },
+        }
+    }
+}
+
+
+def get_packagefamilyname(name, publisher_id):
+    class PACKAGE_ID(ctypes.Structure):
+        _fields_ = [
+            ("reserved", ctypes.c_uint32),
+            ("processorArchitecture", ctypes.c_uint32),
+            ("version", ctypes.c_uint64),
+            ("name", ctypes.c_wchar_p),
+            ("publisher", ctypes.c_wchar_p),
+            ("resourceId", ctypes.c_wchar_p),
+            ("publisherId", ctypes.c_wchar_p),
+        ]
+        _pack_ = 4
+
+    pid = PACKAGE_ID(0, 0, 0, name, publisher_id, None, None)
+    result = ctypes.create_unicode_buffer(256)
+    result_len = ctypes.c_uint32(256)
+    r = ctypes.windll.kernel32.PackageFamilyNameFromId(
+        pid, ctypes.byref(result_len), result
+    )
+    if r:
+        raise OSError(r, "failed to get package family name")
+    return result.value[: result_len.value]
+
+
+def _fixup_sccd(ns, sccd, new_hash=None):
+    if not new_hash:
+        return sccd
+
+    NS = dict(s="http://schemas.microsoft.com/appx/2016/sccd")
+    with open(sccd, "rb") as f:
+        xml = ET.parse(f)
+
+    pfn = get_packagefamilyname(APPX_DATA["Name"], APPX_DATA["Publisher"])
+
+    ae = xml.find("s:AuthorizedEntities", NS)
+    ae.clear()
+
+    e = ET.SubElement(ae, ET.QName(NS["s"], "AuthorizedEntity"))
+    e.set("AppPackageFamilyName", pfn)
+    e.set("CertificateSignatureHash", new_hash)
+
+    for e in xml.findall("s:Catalog", NS):
+        e.text = "FFFF"
+
+    sccd = ns.temp / sccd.name
+    sccd.parent.mkdir(parents=True, exist_ok=True)
+    with open(sccd, "wb") as f:
+        xml.write(f, encoding="utf-8")
+
+    return sccd
+
+
+@public
+def get_appx_layout(ns):
+    if not ns.include_appxmanifest:
+        return
+
+    yield "AppxManifest.xml", ns.temp / "AppxManifest.xml"
+    yield "_resources.xml", ns.temp / "_resources.xml"
+    icons = ns.source / "PC" / "icons"
+    yield "_resources/pythonx44.png", icons / "pythonx44.png"
+    yield "_resources/pythonx44$targetsize-44_altform-unplated.png", icons / "pythonx44.png"
+    yield "_resources/pythonx50.png", icons / "pythonx50.png"
+    yield "_resources/pythonx50$targetsize-50_altform-unplated.png", icons / "pythonx50.png"
+    yield "_resources/pythonx150.png", icons / "pythonx150.png"
+    yield "_resources/pythonx150$targetsize-150_altform-unplated.png", icons / "pythonx150.png"
+    yield "_resources/pythonwx44.png", icons / "pythonwx44.png"
+    yield "_resources/pythonwx44$targetsize-44_altform-unplated.png", icons / "pythonwx44.png"
+    yield "_resources/pythonwx150.png", icons / "pythonwx150.png"
+    yield "_resources/pythonwx150$targetsize-150_altform-unplated.png", icons / "pythonwx150.png"
+    sccd = ns.source / SCCD_FILENAME
+    if sccd.is_file():
+        # This should only be set for side-loading purposes.
+        sccd = _fixup_sccd(ns, sccd, os.getenv("APPX_DATA_SHA256"))
+        yield sccd.name, sccd
+
+
+def find_or_add(xml, element, attr=None, always_add=False):
+    if always_add:
+        e = None
+    else:
+        q = element
+        if attr:
+            q += "[@{}='{}']".format(*attr)
+        e = xml.find(q, APPXMANIFEST_NS)
+    if e is None:
+        prefix, _, name = element.partition(":")
+        name = ET.QName(APPXMANIFEST_NS[prefix or ""], name)
+        e = ET.SubElement(xml, name)
+        if attr:
+            e.set(*attr)
+    return e
+
+
+def _get_app(xml, appid):
+    if appid:
+        app = xml.find(
+            "m:Applications/m:Application[@Id='{}']".format(appid), APPXMANIFEST_NS
+        )
+        if app is None:
+            raise LookupError(appid)
+    else:
+        app = xml
+    return app
+
+
+def add_visual(xml, appid, data):
+    app = _get_app(xml, appid)
+    e = find_or_add(app, "uap:VisualElements")
+    for i in data.items():
+        e.set(*i)
+    return e
+
+
+def add_alias(xml, appid, alias, subsystem="windows"):
+    app = _get_app(xml, appid)
+    e = find_or_add(app, "m:Extensions")
+    e = find_or_add(e, "uap5:Extension", ("Category", "windows.appExecutionAlias"))
+    e = find_or_add(e, "uap5:AppExecutionAlias")
+    e.set(ET.QName(APPXMANIFEST_NS["desktop4"], "Subsystem"), subsystem)
+    e = find_or_add(e, "uap5:ExecutionAlias", ("Alias", alias))
+
+
+def add_file_type(xml, appid, name, suffix, parameters='"%1"'):
+    app = _get_app(xml, appid)
+    e = find_or_add(app, "m:Extensions")
+    e = find_or_add(e, "uap3:Extension", ("Category", "windows.fileTypeAssociation"))
+    e = find_or_add(e, "uap3:FileTypeAssociation", ("Name", name))
+    e.set("Parameters", parameters)
+    e = find_or_add(e, "uap:SupportedFileTypes")
+    if isinstance(suffix, str):
+        suffix = [suffix]
+    for s in suffix:
+        ET.SubElement(e, ET.QName(APPXMANIFEST_NS["uap"], "FileType")).text = s
+
+
+def add_application(
+    ns, xml, appid, executable, aliases, visual_element, subsystem, file_types
+):
+    node = xml.find("m:Applications", APPXMANIFEST_NS)
+    suffix = "_d.exe" if ns.debug else ".exe"
+    app = ET.SubElement(
+        node,
+        ET.QName(APPXMANIFEST_NS[""], "Application"),
+        {
+            "Id": appid,
+            "Executable": executable + suffix,
+            "EntryPoint": "Windows.FullTrustApplication",
+            ET.QName(APPXMANIFEST_NS["desktop4"], "SupportsMultipleInstances"): "true",
+        },
+    )
+    if visual_element:
+        add_visual(app, None, visual_element)
+    for alias in aliases:
+        add_alias(app, None, alias + suffix, subsystem)
+    if file_types:
+        add_file_type(app, None, *file_types)
+    return app
+
+
+def _get_registry_entries(ns, root="", d=None):
+    r = root if root else PureWindowsPath("")
+    if d is None:
+        d = REGISTRY
+    for key, value in d.items():
+        if key == "_condition":
+            continue
+        elif isinstance(value, dict):
+            cond = value.get("_condition")
+            if cond and not cond(ns):
+                continue
+            fullkey = r
+            for part in PureWindowsPath(key).parts:
+                fullkey /= part
+                if len(fullkey.parts) > 1:
+                    yield str(fullkey), None, None
+            yield from _get_registry_entries(ns, fullkey, value)
+        elif len(r.parts) > 1:
+            yield str(r), key, value
+
+
+def add_registry_entries(ns, xml):
+    e = find_or_add(xml, "m:Extensions")
+    e = find_or_add(e, "rescap4:Extension")
+    e.set("Category", "windows.classicAppCompatKeys")
+    e.set("EntryPoint", "Windows.FullTrustApplication")
+    e = ET.SubElement(e, ET.QName(APPXMANIFEST_NS["rescap4"], "ClassicAppCompatKeys"))
+    for name, valuename, value in _get_registry_entries(ns):
+        k = ET.SubElement(
+            e, ET.QName(APPXMANIFEST_NS["rescap4"], "ClassicAppCompatKey")
+        )
+        k.set("Name", name)
+        if value:
+            k.set("ValueName", valuename)
+            k.set("Value", value)
+            k.set("ValueType", "REG_SZ")
+
+
+def disable_registry_virtualization(xml):
+    e = find_or_add(xml, "m:Properties")
+    e = find_or_add(e, "desktop6:RegistryWriteVirtualization")
+    e.text = "disabled"
+    e = find_or_add(xml, "m:Capabilities")
+    e = find_or_add(e, "rescap:Capability", ("Name", "unvirtualizedResources"))
+
+
+@public
+def get_appxmanifest(ns):
+    for k, v in APPXMANIFEST_NS.items():
+        ET.register_namespace(k, v)
+    ET.register_namespace("", APPXMANIFEST_NS["m"])
+
+    xml = ET.parse(io.StringIO(APPXMANIFEST_TEMPLATE))
+    NS = APPXMANIFEST_NS
+    QN = ET.QName
+
+    node = xml.find("m:Identity", NS)
+    for k in node.keys():
+        value = APPX_DATA.get(k)
+        if value:
+            node.set(k, value)
+
+    for node in xml.find("m:Properties", NS):
+        value = APPX_DATA.get(node.tag.rpartition("}")[2])
+        if value:
+            node.text = value
+
+    winver = sys.getwindowsversion()[:3]
+    if winver < (10, 0, 17763):
+        winver = 10, 0, 17763
+    find_or_add(xml, "m:Dependencies/m:TargetDeviceFamily").set(
+        "MaxVersionTested", "{}.{}.{}.0".format(*winver)
+    )
+
+    if winver > (10, 0, 17763):
+        disable_registry_virtualization(xml)
+
+    app = add_application(
+        ns,
+        xml,
+        "Python",
+        "python",
+        ["python", "python{}".format(VER_MAJOR), "python{}".format(VER_DOT)],
+        PYTHON_VE_DATA,
+        "console",
+        ("python.file", [".py"]),
+    )
+
+    add_application(
+        ns,
+        xml,
+        "PythonW",
+        "pythonw",
+        ["pythonw", "pythonw{}".format(VER_MAJOR), "pythonw{}".format(VER_DOT)],
+        PYTHONW_VE_DATA,
+        "windows",
+        ("python.windowedfile", [".pyw"]),
+    )
+
+    if ns.include_pip and ns.include_launchers:
+        add_application(
+            ns,
+            xml,
+            "Pip",
+            "pip",
+            ["pip", "pip{}".format(VER_MAJOR), "pip{}".format(VER_DOT)],
+            PIP_VE_DATA,
+            "console",
+            ("python.wheel", [".whl"], 'install "%1"'),
+        )
+
+    if ns.include_idle and ns.include_launchers:
+        add_application(
+            ns,
+            xml,
+            "Idle",
+            "idle",
+            ["idle", "idle{}".format(VER_MAJOR), "idle{}".format(VER_DOT)],
+            IDLE_VE_DATA,
+            "windows",
+            None,
+        )
+
+    if (ns.source / SCCD_FILENAME).is_file():
+        add_registry_entries(ns, xml)
+        node = xml.find("m:Capabilities", NS)
+        node = ET.SubElement(node, QN(NS["uap4"], "CustomCapability"))
+        node.set("Name", "Microsoft.classicAppCompat_8wekyb3d8bbwe")
+
+    buffer = io.BytesIO()
+    xml.write(buffer, encoding="utf-8", xml_declaration=True)
+    return buffer.getbuffer()
+
+
+@public
+def get_resources_xml(ns):
+    return RESOURCES_XML_TEMPLATE.encode("utf-8")
diff --git a/PC/layout/support/catalog.py b/PC/layout/support/catalog.py
new file mode 100644
index 0000000..4312118
--- /dev/null
+++ b/PC/layout/support/catalog.py
@@ -0,0 +1,44 @@
+"""
+File generation for catalog signing non-binary contents.
+"""
+
+__author__ = "Steve Dower <steve.dower@python.org>"
+__version__ = "3.8"
+
+
+import sys
+
+__all__ = ["PYTHON_CAT_NAME", "PYTHON_CDF_NAME"]
+
+
+def public(f):
+    __all__.append(f.__name__)
+    return f
+
+
+PYTHON_CAT_NAME = "python.cat"
+PYTHON_CDF_NAME = "python.cdf"
+
+
+CATALOG_TEMPLATE = r"""[CatalogHeader]
+Name={target.stem}.cat
+ResultDir={target.parent}
+PublicVersion=1
+CatalogVersion=2
+HashAlgorithms=SHA256
+PageHashes=false
+EncodingType=
+
+[CatalogFiles]
+"""
+
+
+def can_sign(file):
+    return file.is_file() and file.stat().st_size
+
+
+@public
+def write_catalog(target, files):
+    with target.open("w", encoding="utf-8") as cat:
+        cat.write(CATALOG_TEMPLATE.format(target=target))
+        cat.writelines("<HASH>{}={}\n".format(n, f) for n, f in files if can_sign(f))
diff --git a/PC/layout/support/constants.py b/PC/layout/support/constants.py
new file mode 100644
index 0000000..88ea410
--- /dev/null
+++ b/PC/layout/support/constants.py
@@ -0,0 +1,28 @@
+"""
+Constants for generating the layout.
+"""
+
+__author__ = "Steve Dower <steve.dower@python.org>"
+__version__ = "3.8"
+
+import struct
+import sys
+
+VER_MAJOR, VER_MINOR, VER_MICRO, VER_FIELD4 = struct.pack(">i", sys.hexversion)
+VER_FIELD3 = VER_MICRO << 8 | VER_FIELD4
+VER_NAME = {"alpha": "a", "beta": "b", "rc": "rc"}.get(
+    sys.version_info.releaselevel, ""
+)
+VER_SERIAL = sys.version_info.serial if VER_NAME else ""
+VER_DOT = "{}.{}".format(VER_MAJOR, VER_MINOR)
+
+PYTHON_DLL_NAME = "python{}{}.dll".format(VER_MAJOR, VER_MINOR)
+PYTHON_STABLE_DLL_NAME = "python{}.dll".format(VER_MAJOR)
+PYTHON_ZIP_NAME = "python{}{}.zip".format(VER_MAJOR, VER_MINOR)
+PYTHON_PTH_NAME = "python{}{}._pth".format(VER_MAJOR, VER_MINOR)
+
+PYTHON_CHM_NAME = "python{}{}{}{}{}.chm".format(
+    VER_MAJOR, VER_MINOR, VER_MICRO, VER_NAME, VER_SERIAL
+)
+
+IS_X64 = sys.maxsize > 2 ** 32
diff --git a/PC/layout/support/distutils.command.bdist_wininst.py b/PC/layout/support/distutils.command.bdist_wininst.py
new file mode 100644
index 0000000..6e9b49f
--- /dev/null
+++ b/PC/layout/support/distutils.command.bdist_wininst.py
@@ -0,0 +1,25 @@
+"""distutils.command.bdist_wininst
+
+Suppress the 'bdist_wininst' command, while still allowing
+setuptools to import it without breaking."""
+
+from distutils.core import Command
+from distutils.errors import DistutilsPlatformError
+
+
+class bdist_wininst(Command):
+    description = "create an executable installer for MS Windows"
+
+    # Marker for tests that we have the unsupported bdist_wininst
+    _unsupported = True
+
+    def initialize_options(self):
+        pass
+
+    def finalize_options(self):
+        pass
+
+    def run(self):
+        raise DistutilsPlatformError(
+            "bdist_wininst is not supported in this Python distribution"
+        )
diff --git a/PC/layout/support/filesets.py b/PC/layout/support/filesets.py
new file mode 100644
index 0000000..47f727c
--- /dev/null
+++ b/PC/layout/support/filesets.py
@@ -0,0 +1,100 @@
+"""
+File sets and globbing helper for make_layout.
+"""
+
+__author__ = "Steve Dower <steve.dower@python.org>"
+__version__ = "3.8"
+
+import os
+
+
+class FileStemSet:
+    def __init__(self, *patterns):
+        self._names = set()
+        self._prefixes = []
+        self._suffixes = []
+        for p in map(os.path.normcase, patterns):
+            if p.endswith("*"):
+                self._prefixes.append(p[:-1])
+            elif p.startswith("*"):
+                self._suffixes.append(p[1:])
+            else:
+                self._names.add(p)
+
+    def _make_name(self, f):
+        return os.path.normcase(f.stem)
+
+    def __contains__(self, f):
+        bn = self._make_name(f)
+        return (
+            bn in self._names
+            or any(map(bn.startswith, self._prefixes))
+            or any(map(bn.endswith, self._suffixes))
+        )
+
+
+class FileNameSet(FileStemSet):
+    def _make_name(self, f):
+        return os.path.normcase(f.name)
+
+
+class FileSuffixSet:
+    def __init__(self, *patterns):
+        self._names = set()
+        self._prefixes = []
+        self._suffixes = []
+        for p in map(os.path.normcase, patterns):
+            if p.startswith("*."):
+                self._names.add(p[1:])
+            elif p.startswith("*"):
+                self._suffixes.append(p[1:])
+            elif p.endswith("*"):
+                self._prefixes.append(p[:-1])
+            elif p.startswith("."):
+                self._names.add(p)
+            else:
+                self._names.add("." + p)
+
+    def _make_name(self, f):
+        return os.path.normcase(f.suffix)
+
+    def __contains__(self, f):
+        bn = self._make_name(f)
+        return (
+            bn in self._names
+            or any(map(bn.startswith, self._prefixes))
+            or any(map(bn.endswith, self._suffixes))
+        )
+
+
+def _rglob(root, pattern, condition):
+    dirs = [root]
+    recurse = pattern[:3] in {"**/", "**\\"}
+    if recurse:
+        pattern = pattern[3:]
+
+    while dirs:
+        d = dirs.pop(0)
+        if recurse:
+            dirs.extend(
+                filter(
+                    condition, (type(root)(f2) for f2 in os.scandir(d) if f2.is_dir())
+                )
+            )
+        yield from (
+            (f.relative_to(root), f)
+            for f in d.glob(pattern)
+            if f.is_file() and condition(f)
+        )
+
+
+def _return_true(f):
+    return True
+
+
+def rglob(root, patterns, condition=None):
+    if isinstance(patterns, tuple):
+        for p in patterns:
+            yield from _rglob(root, p, condition or _return_true)
+    else:
+        yield from _rglob(root, patterns, condition or _return_true)
diff --git a/PC/layout/support/logging.py b/PC/layout/support/logging.py
new file mode 100644
index 0000000..30869b9
--- /dev/null
+++ b/PC/layout/support/logging.py
@@ -0,0 +1,93 @@
+"""
+Logging support for make_layout.
+"""
+
+__author__ = "Steve Dower <steve.dower@python.org>"
+__version__ = "3.8"
+
+import logging
+import sys
+
+__all__ = []
+
+LOG = None
+HAS_ERROR = False
+
+
+def public(f):
+    __all__.append(f.__name__)
+    return f
+
+
+@public
+def configure_logger(ns):
+    global LOG
+    if LOG:
+        return
+
+    LOG = logging.getLogger("make_layout")
+    LOG.level = logging.DEBUG
+
+    if ns.v:
+        s_level = max(logging.ERROR - ns.v * 10, logging.DEBUG)
+        f_level = max(logging.WARNING - ns.v * 10, logging.DEBUG)
+    else:
+        s_level = logging.ERROR
+        f_level = logging.INFO
+
+    handler = logging.StreamHandler(sys.stdout)
+    handler.setFormatter(logging.Formatter("{levelname:8s} {message}", style="{"))
+    handler.setLevel(s_level)
+    LOG.addHandler(handler)
+
+    if ns.log:
+        handler = logging.FileHandler(ns.log, encoding="utf-8", delay=True)
+        handler.setFormatter(
+            logging.Formatter("[{asctime}]{levelname:8s}: {message}", style="{")
+        )
+        handler.setLevel(f_level)
+        LOG.addHandler(handler)
+
+
+class BraceMessage:
+    def __init__(self, fmt, *args, **kwargs):
+        self.fmt = fmt
+        self.args = args
+        self.kwargs = kwargs
+
+    def __str__(self):
+        return self.fmt.format(*self.args, **self.kwargs)
+
+
+@public
+def log_debug(msg, *args, **kwargs):
+    return LOG.debug(BraceMessage(msg, *args, **kwargs))
+
+
+@public
+def log_info(msg, *args, **kwargs):
+    return LOG.info(BraceMessage(msg, *args, **kwargs))
+
+
+@public
+def log_warning(msg, *args, **kwargs):
+    return LOG.warning(BraceMessage(msg, *args, **kwargs))
+
+
+@public
+def log_error(msg, *args, **kwargs):
+    global HAS_ERROR
+    HAS_ERROR = True
+    return LOG.error(BraceMessage(msg, *args, **kwargs))
+
+
+@public
+def log_exception(msg, *args, **kwargs):
+    global HAS_ERROR
+    HAS_ERROR = True
+    return LOG.exception(BraceMessage(msg, *args, **kwargs))
+
+
+@public
+def error_was_logged():
+    return HAS_ERROR
diff --git a/PC/layout/support/options.py b/PC/layout/support/options.py
new file mode 100644
index 0000000..76d9e34
--- /dev/null
+++ b/PC/layout/support/options.py
@@ -0,0 +1,122 @@
+"""
+List of optional components.
+"""
+
+__author__ = "Steve Dower <steve.dower@python.org>"
+__version__ = "3.8"
+
+
+__all__ = []
+
+
+def public(f):
+    __all__.append(f.__name__)
+    return f
+
+
+OPTIONS = {
+    "stable": {"help": "stable ABI stub"},
+    "pip": {"help": "pip"},
+    "distutils": {"help": "distutils"},
+    "tcltk": {"help": "Tcl, Tk and tkinter"},
+    "idle": {"help": "Idle"},
+    "tests": {"help": "test suite"},
+    "tools": {"help": "tools"},
+    "venv": {"help": "venv"},
+    "dev": {"help": "headers and libs"},
+    "symbols": {"help": "symbols"},
+    "bdist-wininst": {"help": "bdist_wininst support"},
+    "underpth": {"help": "a python._pth file", "not-in-all": True},
+    "launchers": {"help": "specific launchers"},
+    "appxmanifest": {"help": "an appxmanifest"},
+    "props": {"help": "a python.props file"},
+    "chm": {"help": "the CHM documentation"},
+    "html-doc": {"help": "the HTML documentation"},
+}
+
+
+PRESETS = {
+    "appx": {
+        "help": "APPX package",
+        "options": [
+            "stable",
+            "pip",
+            "distutils",
+            "tcltk",
+            "idle",
+            "venv",
+            "dev",
+            "launchers",
+            "appxmanifest",
+            # XXX: Disabled for now "precompile",
+        ],
+    },
+    "nuget": {
+        "help": "nuget package",
+        "options": ["stable", "pip", "distutils", "dev", "props"],
+    },
+    "default": {
+        "help": "development kit package",
+        "options": [
+            "stable",
+            "pip",
+            "distutils",
+            "tcltk",
+            "idle",
+            "tests",
+            "tools",
+            "venv",
+            "dev",
+            "symbols",
+            "bdist-wininst",
+            "chm",
+        ],
+    },
+    "embed": {
+        "help": "embeddable package",
+        "options": ["stable", "zip-lib", "flat-dlls", "underpth", "precompile"],
+    },
+}
+
+
+@public
+def get_argparse_options():
+    for opt, info in OPTIONS.items():
+        help = "When specified, includes {}".format(info["help"])
+        if info.get("not-in-all"):
+            help = "{}. Not affected by --include-all".format(help)
+
+        yield "--include-{}".format(opt), help
+
+    for opt, info in PRESETS.items():
+        help = "When specified, includes default options for {}".format(info["help"])
+        yield "--preset-{}".format(opt), help
+
+
+def ns_get(ns, key, default=False):
+    return getattr(ns, key.replace("-", "_"), default)
+
+
+def ns_set(ns, key, value=True):
+    k1 = key.replace("-", "_")
+    k2 = "include_{}".format(k1)
+    if hasattr(ns, k2):
+        setattr(ns, k2, value)
+    elif hasattr(ns, k1):
+        setattr(ns, k1, value)
+    else:
+        raise AttributeError("no argument named '{}'".format(k1))
+
+
+@public
+def update_presets(ns):
+    for preset, info in PRESETS.items():
+        if ns_get(ns, "preset-{}".format(preset)):
+            for opt in info["options"]:
+                ns_set(ns, opt)
+
+    if ns.include_all:
+        for opt in OPTIONS:
+            if OPTIONS[opt].get("not-in-all"):
+                continue
+            ns_set(ns, opt)
diff --git a/PC/layout/support/pip.py b/PC/layout/support/pip.py
new file mode 100644
index 0000000..369a923
--- /dev/null
+++ b/PC/layout/support/pip.py
@@ -0,0 +1,79 @@
+"""
+Extraction and file list generation for pip.
+"""
+
+__author__ = "Steve Dower <steve.dower@python.org>"
+__version__ = "3.8"
+
+
+import os
+import shutil
+import subprocess
+import sys
+
+__all__ = []
+
+
+def public(f):
+    __all__.append(f.__name__)
+    return f
+
+
+@public
+def get_pip_dir(ns):
+    if ns.copy:
+        if ns.zip_lib:
+            return ns.copy / "packages"
+        return ns.copy / "Lib" / "site-packages"
+    else:
+        return ns.temp / "packages"
+
+
+@public
+def extract_pip_files(ns):
+    dest = get_pip_dir(ns)
+    dest.mkdir(parents=True, exist_ok=True)
+
+    src = ns.source / "Lib" / "ensurepip" / "_bundled"
+
+    ns.temp.mkdir(parents=True, exist_ok=True)
+    wheels = [shutil.copy(whl, ns.temp) for whl in src.glob("*.whl")]
+    search_path = os.pathsep.join(wheels)
+    if os.environ.get("PYTHONPATH"):
+        search_path += ";" + os.environ["PYTHONPATH"]
+
+    env = os.environ.copy()
+    env["PYTHONPATH"] = search_path
+
+    output = subprocess.check_output(
+        [
+            sys.executable,
+            "-m",
+            "pip",
+            "--no-color",
+            "install",
+            "pip",
+            "setuptools",
+            "--upgrade",
+            "--target",
+            str(dest),
+            "--no-index",
+            "--no-cache-dir",
+            "-f",
+            str(src),
+            "--only-binary",
+            ":all:",
+        ],
+        env=env,
+    )
+
+    try:
+        shutil.rmtree(dest / "bin")
+    except OSError:
+        pass
+
+    for file in wheels:
+        try:
+            os.remove(file)
+        except OSError:
+            pass
diff --git a/PC/layout/support/props.py b/PC/layout/support/props.py
new file mode 100644
index 0000000..3a047d2
--- /dev/null
+++ b/PC/layout/support/props.py
@@ -0,0 +1,110 @@
+"""
+Provides .props file.
+"""
+
+import os
+
+from .constants import *
+
+__all__ = ["PYTHON_PROPS_NAME"]
+
+
+def public(f):
+    __all__.append(f.__name__)
+    return f
+
+
+PYTHON_PROPS_NAME = "python.props"
+
+PROPS_DATA = {
+    "PYTHON_TAG": VER_DOT,
+    "PYTHON_VERSION": os.getenv("PYTHON_NUSPEC_VERSION"),
+    "PYTHON_PLATFORM": os.getenv("PYTHON_PROPS_PLATFORM"),
+    "PYTHON_TARGET": "",
+}
+
+if not PROPS_DATA["PYTHON_VERSION"]:
+    if VER_NAME:
+        PROPS_DATA["PYTHON_VERSION"] = "{}.{}-{}{}".format(
+            VER_DOT, VER_MICRO, VER_NAME, VER_SERIAL
+        )
+    else:
+        PROPS_DATA["PYTHON_VERSION"] = "{}.{}".format(VER_DOT, VER_MICRO)
+
+if not PROPS_DATA["PYTHON_PLATFORM"]:
+    PROPS_DATA["PYTHON_PLATFORM"] = "x64" if IS_X64 else "Win32"
+
+PROPS_DATA["PYTHON_TARGET"] = "_GetPythonRuntimeFilesDependsOn{}{}_{}".format(
+    VER_MAJOR, VER_MINOR, PROPS_DATA["PYTHON_PLATFORM"]
+)
+
+PROPS_TEMPLATE = r"""<?xml version="1.0" encoding="utf-8"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup Condition="$(Platform) == '{PYTHON_PLATFORM}'">
+    <PythonHome Condition="$(Configuration) == 'Debug'">$([msbuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), "python_d.exe")</PythonHome>
+    <PythonHome Condition="$(PythonHome) == ''">$([msbuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), "python.exe")</PythonHome>
+    <PythonInclude>$(PythonHome)\include</PythonInclude>
+    <PythonLibs>$(PythonHome)\libs</PythonLibs>
+    <PythonTag>{PYTHON_TAG}</PythonTag>
+    <PythonVersion>{PYTHON_VERSION}</PythonVersion>
+
+    <IncludePythonExe Condition="$(IncludePythonExe) == ''">true</IncludePythonExe>
+    <IncludeDistutils Condition="$(IncludeDistutils) == ''">false</IncludeDistutils>
+    <IncludeLib2To3 Condition="$(IncludeLib2To3) == ''">false</IncludeLib2To3>
+    <IncludeVEnv Condition="$(IncludeVEnv) == ''">false</IncludeVEnv>
+
+    <GetPythonRuntimeFilesDependsOn>{PYTHON_TARGET};$(GetPythonRuntimeFilesDependsOn)</GetPythonRuntimeFilesDependsOn>
+  </PropertyGroup>
+
+  <ItemDefinitionGroup Condition="$(Platform) == '{PYTHON_PLATFORM}'">
+    <ClCompile>
+      <AdditionalIncludeDirectories>$(PythonInclude);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
+    </ClCompile>
+    <Link>
+      <AdditionalLibraryDirectories>$(PythonLibs);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
+    </Link>
+  </ItemDefinitionGroup>
+
+  <Target Name="GetPythonRuntimeFiles" Returns="@(PythonRuntime)" DependsOnTargets="$(GetPythonRuntimeFilesDependsOn)" />
+
+  <Target Name="{PYTHON_TARGET}" Returns="@(PythonRuntime)">
+    <ItemGroup>
+      <_PythonRuntimeExe Include="$(PythonHome)\python*.dll" />
+      <_PythonRuntimeExe Include="$(PythonHome)\python*.exe" Condition="$(IncludePythonExe) == 'true'" />
+      <_PythonRuntimeExe>
+        <Link>%(Filename)%(Extension)</Link>
+      </_PythonRuntimeExe>
+      <_PythonRuntimeDlls Include="$(PythonHome)\DLLs\*.pyd" />
+      <_PythonRuntimeDlls Include="$(PythonHome)\DLLs\*.dll" />
+      <_PythonRuntimeDlls>
+        <Link>DLLs\%(Filename)%(Extension)</Link>
+      </_PythonRuntimeDlls>
+      <_PythonRuntimeLib Include="$(PythonHome)\Lib\**\*" Exclude="$(PythonHome)\Lib\**\*.pyc;$(PythonHome)\Lib\site-packages\**\*" />
+      <_PythonRuntimeLib Remove="$(PythonHome)\Lib\distutils\**\*" Condition="$(IncludeDistutils) != 'true'" />
+      <_PythonRuntimeLib Remove="$(PythonHome)\Lib\lib2to3\**\*" Condition="$(IncludeLib2To3) != 'true'" />
+      <_PythonRuntimeLib Remove="$(PythonHome)\Lib\ensurepip\**\*" Condition="$(IncludeVEnv) != 'true'" />
+      <_PythonRuntimeLib Remove="$(PythonHome)\Lib\venv\**\*" Condition="$(IncludeVEnv) != 'true'" />
+      <_PythonRuntimeLib>
+        <Link>Lib\%(RecursiveDir)%(Filename)%(Extension)</Link>
+      </_PythonRuntimeLib>
+      <PythonRuntime Include="@(_PythonRuntimeExe);@(_PythonRuntimeDlls);@(_PythonRuntimeLib)" />
+    </ItemGroup>
+
+    <Message Importance="low" Text="Collected Python runtime from $(PythonHome):%0D%0A@(PythonRuntime->'  %(Link)','%0D%0A')" />
+  </Target>
+</Project>
+"""
+
+
+@public
+def get_props_layout(ns):
+    if ns.include_all or ns.include_props:
+        yield "python.props", ns.temp / "python.props"
+
+
+@public
+def get_props(ns):
+    # TODO: Filter contents of props file according to included/excluded items
+    props = PROPS_TEMPLATE.format_map(PROPS_DATA)
+    return props.encode("utf-8")
diff --git a/PC/layout/support/python.props b/PC/layout/support/python.props
new file mode 100644
index 0000000..4cc7008
--- /dev/null
+++ b/PC/layout/support/python.props
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup Condition="$(Platform) == '$$PYTHON_PLATFORM$$'">
+    <PythonHome>$(MSBuildThisFileDirectory)\..\..\tools</PythonHome>
+    <PythonInclude>$(PythonHome)\include</PythonInclude>
+    <PythonLibs>$(PythonHome)\libs</PythonLibs>
+    <PythonTag>$$PYTHON_TAG$$</PythonTag>
+    <PythonVersion>$$PYTHON_VERSION$$</PythonVersion>
+    
+    <IncludePythonExe Condition="$(IncludePythonExe) == ''">true</IncludePythonExe>
+    <IncludeDistutils Condition="$(IncludeDistutils) == ''">false</IncludeDistutils>
+    <IncludeLib2To3 Condition="$(IncludeLib2To3) == ''">false</IncludeLib2To3>
+    <IncludeVEnv Condition="$(IncludeVEnv) == ''">false</IncludeVEnv>
+
+    <GetPythonRuntimeFilesDependsOn>$$PYTHON_TARGET$$;$(GetPythonRuntimeFilesDependsOn)</GetPythonRuntimeFilesDependsOn>
+  </PropertyGroup>
+
+  <ItemDefinitionGroup Condition="$(Platform) == '$$PYTHON_PLATFORM$$'">
+    <ClCompile>
+      <AdditionalIncludeDirectories>$(PythonInclude);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
+      <RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
+    </ClCompile>
+    <Link>
+      <AdditionalLibraryDirectories>$(PythonLibs);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
+    </Link>
+  </ItemDefinitionGroup>
+
+  <Target Name="GetPythonRuntimeFiles" Returns="@(PythonRuntime)" DependsOnTargets="$(GetPythonRuntimeFilesDependsOn)" />
+
+  <Target Name="$$PYTHON_TARGET$$" Returns="@(PythonRuntime)">
+    <ItemGroup>
+      <_PythonRuntimeExe Include="$(PythonHome)\python*.dll" />
+      <_PythonRuntimeExe Include="$(PythonHome)\vcruntime140.dll" />
+      <_PythonRuntimeExe Include="$(PythonHome)\python*.exe" Condition="$(IncludePythonExe) == 'true'" />
+      <_PythonRuntimeExe>
+        <Link>%(Filename)%(Extension)</Link>
+      </_PythonRuntimeExe>
+      <_PythonRuntimeDlls Include="$(PythonHome)\DLLs\*.pyd" />
+      <_PythonRuntimeDlls Include="$(PythonHome)\DLLs\*.dll" />
+      <_PythonRuntimeDlls>
+        <Link>DLLs\%(Filename)%(Extension)</Link>
+      </_PythonRuntimeDlls>
+      <_PythonRuntimeLib Include="$(PythonHome)\Lib\**\*" Exclude="$(PythonHome)\Lib\**\*.pyc;$(PythonHome)\Lib\site-packages\**\*" />
+      <_PythonRuntimeLib Remove="$(PythonHome)\Lib\distutils\**\*" Condition="$(IncludeDistutils) != 'true'" />
+      <_PythonRuntimeLib Remove="$(PythonHome)\Lib\lib2to3\**\*" Condition="$(IncludeLib2To3) != 'true'" />
+      <_PythonRuntimeLib Remove="$(PythonHome)\Lib\ensurepip\**\*" Condition="$(IncludeVEnv) != 'true'" />
+      <_PythonRuntimeLib Remove="$(PythonHome)\Lib\venv\**\*" Condition="$(IncludeVEnv) != 'true'" />
+      <_PythonRuntimeLib>
+        <Link>Lib\%(RecursiveDir)%(Filename)%(Extension)</Link>
+      </_PythonRuntimeLib>
+      <PythonRuntime Include="@(_PythonRuntimeExe);@(_PythonRuntimeDlls);@(_PythonRuntimeLib)" />
+    </ItemGroup>
+    
+    <Message Importance="low" Text="Collected Python runtime from $(PythonHome):%0D%0A@(PythonRuntime->'  %(Link)','%0D%0A')" />
+  </Target>
+</Project>