| """ |
| 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", "vcruntime*") |
| 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*", "libffi*") |
| |
| 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 copy_if_modified(src, dest): |
| try: |
| dest_stat = os.stat(dest) |
| except FileNotFoundError: |
| do_copy = True |
| else: |
| src_stat = os.stat(src) |
| do_copy = (src_stat.st_mtime != dest_stat.st_mtime or |
| src_stat.st_size != dest_stat.st_size) |
| |
| if do_copy: |
| shutil.copy2(src, dest) |
| |
| 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 |
| |
| yield "LICENSE.txt", ns.source / "LICENSE" |
| |
| 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, checked=True): |
| import py_compile |
| |
| if dest is not None: |
| dest = str(dest) |
| |
| mode = ( |
| py_compile.PycInvalidationMode.CHECKED_HASH |
| if checked |
| else py_compile.PycInvalidationMode.UNCHECKED_HASH |
| ) |
| |
| try: |
| return Path( |
| py_compile.compile( |
| str(src), |
| dest, |
| str(name), |
| doraise=True, |
| optimize=optimize, |
| invalidation_mode=mode, |
| ) |
| ) |
| except py_compile.PyCompileError: |
| log_warning("Failed to compile {}", src) |
| return None |
| |
| |
| def _py_temp_compile(src, ns, dest_dir=None, checked=True): |
| 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, checked=checked) |
| |
| |
| def _write_to_zip(zf, dest, src, ns, checked=True): |
| pyc = _py_temp_compile(src, ns, checked=checked) |
| 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, checked=False) |
| |
| 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) |
| copy_if_modified(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: |
| copy_if_modified(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)) |