blob: 910085c01bb2efe0f121364cd8cf1d192e301a35 [file] [log] [blame]
Steve Dower0cd63912018-12-10 18:52:57 -08001"""
2Generates a layout of Python for Windows from a build.
3
4See python make_layout.py --help for usage.
5"""
6
7__author__ = "Steve Dower <steve.dower@python.org>"
8__version__ = "3.8"
9
10import argparse
11import functools
12import os
13import re
14import shutil
15import subprocess
16import sys
17import tempfile
18import zipfile
19
20from pathlib import Path
21
22if __name__ == "__main__":
23 # Started directly, so enable relative imports
24 __path__ = [str(Path(__file__).resolve().parent)]
25
26from .support.appxmanifest import *
27from .support.catalog import *
28from .support.constants import *
29from .support.filesets import *
30from .support.logging import *
31from .support.options import *
32from .support.pip import *
33from .support.props import *
34
35BDIST_WININST_FILES_ONLY = FileNameSet("wininst-*", "bdist_wininst.py")
36BDIST_WININST_STUB = "PC/layout/support/distutils.command.bdist_wininst.py"
37
38TEST_PYDS_ONLY = FileStemSet("xxlimited", "_ctypes_test", "_test*")
39TEST_DIRS_ONLY = FileNameSet("test", "tests")
40
41IDLE_DIRS_ONLY = FileNameSet("idlelib")
42
43TCLTK_PYDS_ONLY = FileStemSet("tcl*", "tk*", "_tkinter")
44TCLTK_DIRS_ONLY = FileNameSet("tkinter", "turtledemo")
45TCLTK_FILES_ONLY = FileNameSet("turtle.py")
46
47VENV_DIRS_ONLY = FileNameSet("venv", "ensurepip")
48
Steve Dower59c2aa22018-12-27 12:44:25 -080049EXCLUDE_FROM_PYDS = FileStemSet("python*", "pyshellext", "vcruntime*")
Steve Dower0cd63912018-12-10 18:52:57 -080050EXCLUDE_FROM_LIB = FileNameSet("*.pyc", "__pycache__", "*.pickle")
51EXCLUDE_FROM_PACKAGED_LIB = FileNameSet("readme.txt")
52EXCLUDE_FROM_COMPILE = FileNameSet("badsyntax_*", "bad_*")
53EXCLUDE_FROM_CATALOG = FileSuffixSet(".exe", ".pyd", ".dll")
54
55REQUIRED_DLLS = FileStemSet("libcrypto*", "libssl*")
56
57LIB2TO3_GRAMMAR_FILES = FileNameSet("Grammar.txt", "PatternGrammar.txt")
58
59PY_FILES = FileSuffixSet(".py")
60PYC_FILES = FileSuffixSet(".pyc")
61CAT_FILES = FileSuffixSet(".cat")
62CDF_FILES = FileSuffixSet(".cdf")
63
64DATA_DIRS = FileNameSet("data")
65
66TOOLS_DIRS = FileNameSet("scripts", "i18n", "pynche", "demo", "parser")
67TOOLS_FILES = FileSuffixSet(".py", ".pyw", ".txt")
68
69
70def get_lib_layout(ns):
71 def _c(f):
72 if f in EXCLUDE_FROM_LIB:
73 return False
74 if f.is_dir():
75 if f in TEST_DIRS_ONLY:
76 return ns.include_tests
77 if f in TCLTK_DIRS_ONLY:
78 return ns.include_tcltk
79 if f in IDLE_DIRS_ONLY:
80 return ns.include_idle
81 if f in VENV_DIRS_ONLY:
82 return ns.include_venv
83 else:
84 if f in TCLTK_FILES_ONLY:
85 return ns.include_tcltk
86 if f in BDIST_WININST_FILES_ONLY:
87 return ns.include_bdist_wininst
88 return True
89
90 for dest, src in rglob(ns.source / "Lib", "**/*", _c):
91 yield dest, src
92
93 if not ns.include_bdist_wininst:
94 src = ns.source / BDIST_WININST_STUB
95 yield Path("distutils/command/bdist_wininst.py"), src
96
97
98def get_tcltk_lib(ns):
99 if not ns.include_tcltk:
100 return
101
102 tcl_lib = os.getenv("TCL_LIBRARY")
103 if not tcl_lib or not os.path.isdir(tcl_lib):
104 try:
105 with open(ns.build / "TCL_LIBRARY.env", "r", encoding="utf-8-sig") as f:
106 tcl_lib = f.read().strip()
107 except FileNotFoundError:
108 pass
109 if not tcl_lib or not os.path.isdir(tcl_lib):
110 warn("Failed to find TCL_LIBRARY")
111 return
112
113 for dest, src in rglob(Path(tcl_lib).parent, "**/*"):
114 yield "tcl/{}".format(dest), src
115
116
117def get_layout(ns):
118 def in_build(f, dest="", new_name=None):
119 n, _, x = f.rpartition(".")
120 n = new_name or n
121 src = ns.build / f
122 if ns.debug and src not in REQUIRED_DLLS:
123 if not src.stem.endswith("_d"):
124 src = src.parent / (src.stem + "_d" + src.suffix)
125 if not n.endswith("_d"):
126 n += "_d"
127 f = n + "." + x
128 yield dest + n + "." + x, src
129 if ns.include_symbols:
130 pdb = src.with_suffix(".pdb")
131 if pdb.is_file():
132 yield dest + n + ".pdb", pdb
133 if ns.include_dev:
134 lib = src.with_suffix(".lib")
135 if lib.is_file():
136 yield "libs/" + n + ".lib", lib
137
138 if ns.include_appxmanifest:
139 yield from in_build("python_uwp.exe", new_name="python")
140 yield from in_build("pythonw_uwp.exe", new_name="pythonw")
141 else:
142 yield from in_build("python.exe", new_name="python")
143 yield from in_build("pythonw.exe", new_name="pythonw")
144
145 yield from in_build(PYTHON_DLL_NAME)
146
147 if ns.include_launchers and ns.include_appxmanifest:
148 if ns.include_pip:
149 yield from in_build("python_uwp.exe", new_name="pip")
150 if ns.include_idle:
151 yield from in_build("pythonw_uwp.exe", new_name="idle")
152
153 if ns.include_stable:
154 yield from in_build(PYTHON_STABLE_DLL_NAME)
155
156 for dest, src in rglob(ns.build, "vcruntime*.dll"):
157 yield dest, src
158
Steve Dower28f6cb32019-01-22 10:49:52 -0800159 yield "LICENSE.txt", ns.source / "LICENSE"
160
Steve Dower0cd63912018-12-10 18:52:57 -0800161 for dest, src in rglob(ns.build, ("*.pyd", "*.dll")):
162 if src.stem.endswith("_d") != bool(ns.debug) and src not in REQUIRED_DLLS:
163 continue
164 if src in EXCLUDE_FROM_PYDS:
165 continue
166 if src in TEST_PYDS_ONLY and not ns.include_tests:
167 continue
168 if src in TCLTK_PYDS_ONLY and not ns.include_tcltk:
169 continue
170
171 yield from in_build(src.name, dest="" if ns.flat_dlls else "DLLs/")
172
173 if ns.zip_lib:
174 zip_name = PYTHON_ZIP_NAME
175 yield zip_name, ns.temp / zip_name
176 else:
177 for dest, src in get_lib_layout(ns):
178 yield "Lib/{}".format(dest), src
179
180 if ns.include_venv:
181 yield from in_build("venvlauncher.exe", "Lib/venv/scripts/nt/", "python")
182 yield from in_build("venvwlauncher.exe", "Lib/venv/scripts/nt/", "pythonw")
183
184 if ns.include_tools:
185
186 def _c(d):
187 if d.is_dir():
188 return d in TOOLS_DIRS
189 return d in TOOLS_FILES
190
191 for dest, src in rglob(ns.source / "Tools", "**/*", _c):
192 yield "Tools/{}".format(dest), src
193
194 if ns.include_underpth:
195 yield PYTHON_PTH_NAME, ns.temp / PYTHON_PTH_NAME
196
197 if ns.include_dev:
198
199 def _c(d):
200 if d.is_dir():
201 return d.name != "internal"
202 return True
203
204 for dest, src in rglob(ns.source / "Include", "**/*.h", _c):
205 yield "include/{}".format(dest), src
206 src = ns.source / "PC" / "pyconfig.h"
207 yield "include/pyconfig.h", src
208
209 for dest, src in get_tcltk_lib(ns):
210 yield dest, src
211
212 if ns.include_pip:
213 pip_dir = get_pip_dir(ns)
214 if not pip_dir.is_dir():
215 log_warning("Failed to find {} - pip will not be included", pip_dir)
216 else:
217 pkg_root = "packages/{}" if ns.zip_lib else "Lib/site-packages/{}"
218 for dest, src in rglob(pip_dir, "**/*"):
219 if src in EXCLUDE_FROM_LIB or src in EXCLUDE_FROM_PACKAGED_LIB:
220 continue
221 yield pkg_root.format(dest), src
222
223 if ns.include_chm:
224 for dest, src in rglob(ns.doc_build / "htmlhelp", PYTHON_CHM_NAME):
225 yield "Doc/{}".format(dest), src
226
227 if ns.include_html_doc:
228 for dest, src in rglob(ns.doc_build / "html", "**/*"):
229 yield "Doc/html/{}".format(dest), src
230
231 if ns.include_props:
232 for dest, src in get_props_layout(ns):
233 yield dest, src
234
235 for dest, src in get_appx_layout(ns):
236 yield dest, src
237
238 if ns.include_cat:
239 if ns.flat_dlls:
240 yield ns.include_cat.name, ns.include_cat
241 else:
242 yield "DLLs/{}".format(ns.include_cat.name), ns.include_cat
243
244
Steve Dower872bd2b2019-01-08 02:38:01 -0800245def _compile_one_py(src, dest, name, optimize, checked=True):
Steve Dower0cd63912018-12-10 18:52:57 -0800246 import py_compile
247
248 if dest is not None:
249 dest = str(dest)
250
Steve Dower872bd2b2019-01-08 02:38:01 -0800251 mode = (
252 py_compile.PycInvalidationMode.CHECKED_HASH
253 if checked
254 else py_compile.PycInvalidationMode.UNCHECKED_HASH
255 )
256
Steve Dower0cd63912018-12-10 18:52:57 -0800257 try:
258 return Path(
259 py_compile.compile(
260 str(src),
261 dest,
262 str(name),
263 doraise=True,
264 optimize=optimize,
Steve Dower872bd2b2019-01-08 02:38:01 -0800265 invalidation_mode=mode,
Steve Dower0cd63912018-12-10 18:52:57 -0800266 )
267 )
268 except py_compile.PyCompileError:
269 log_warning("Failed to compile {}", src)
270 return None
271
272
Steve Dower872bd2b2019-01-08 02:38:01 -0800273def _py_temp_compile(src, ns, dest_dir=None, checked=True):
Steve Dower0cd63912018-12-10 18:52:57 -0800274 if not ns.precompile or src not in PY_FILES or src.parent in DATA_DIRS:
275 return None
276
277 dest = (dest_dir or ns.temp) / (src.stem + ".py")
Steve Dower872bd2b2019-01-08 02:38:01 -0800278 return _compile_one_py(src, dest.with_suffix(".pyc"), dest, optimize=2, checked=checked)
Steve Dower0cd63912018-12-10 18:52:57 -0800279
280
Steve Dower872bd2b2019-01-08 02:38:01 -0800281def _write_to_zip(zf, dest, src, ns, checked=True):
282 pyc = _py_temp_compile(src, ns, checked=checked)
Steve Dower0cd63912018-12-10 18:52:57 -0800283 if pyc:
284 try:
285 zf.write(str(pyc), dest.with_suffix(".pyc"))
286 finally:
287 try:
288 pyc.unlink()
289 except:
290 log_exception("Failed to delete {}", pyc)
291 return
292
293 if src in LIB2TO3_GRAMMAR_FILES:
294 from lib2to3.pgen2.driver import load_grammar
295
296 tmp = ns.temp / src.name
297 try:
298 shutil.copy(src, tmp)
299 load_grammar(str(tmp))
300 for f in ns.temp.glob(src.stem + "*.pickle"):
301 zf.write(str(f), str(dest.parent / f.name))
302 try:
303 f.unlink()
304 except:
305 log_exception("Failed to delete {}", f)
306 except:
307 log_exception("Failed to compile {}", src)
308 finally:
309 try:
310 tmp.unlink()
311 except:
312 log_exception("Failed to delete {}", tmp)
313
314 zf.write(str(src), str(dest))
315
316
317def generate_source_files(ns):
318 if ns.zip_lib:
319 zip_name = PYTHON_ZIP_NAME
320 zip_path = ns.temp / zip_name
321 if zip_path.is_file():
322 zip_path.unlink()
323 elif zip_path.is_dir():
324 log_error(
325 "Cannot create zip file because a directory exists by the same name"
326 )
327 return
328 log_info("Generating {} in {}", zip_name, ns.temp)
329 ns.temp.mkdir(parents=True, exist_ok=True)
330 with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
331 for dest, src in get_lib_layout(ns):
Steve Dower872bd2b2019-01-08 02:38:01 -0800332 _write_to_zip(zf, dest, src, ns, checked=False)
Steve Dower0cd63912018-12-10 18:52:57 -0800333
334 if ns.include_underpth:
335 log_info("Generating {} in {}", PYTHON_PTH_NAME, ns.temp)
336 ns.temp.mkdir(parents=True, exist_ok=True)
337 with open(ns.temp / PYTHON_PTH_NAME, "w", encoding="utf-8") as f:
338 if ns.zip_lib:
339 print(PYTHON_ZIP_NAME, file=f)
340 if ns.include_pip:
341 print("packages", file=f)
342 else:
343 print("Lib", file=f)
344 print("Lib/site-packages", file=f)
345 if not ns.flat_dlls:
346 print("DLLs", file=f)
347 print(".", file=f)
348 print(file=f)
349 print("# Uncomment to run site.main() automatically", file=f)
350 print("#import site", file=f)
351
352 if ns.include_appxmanifest:
353 log_info("Generating AppxManifest.xml in {}", ns.temp)
354 ns.temp.mkdir(parents=True, exist_ok=True)
355
356 with open(ns.temp / "AppxManifest.xml", "wb") as f:
357 f.write(get_appxmanifest(ns))
358
359 with open(ns.temp / "_resources.xml", "wb") as f:
360 f.write(get_resources_xml(ns))
361
362 if ns.include_pip:
363 pip_dir = get_pip_dir(ns)
364 if not (pip_dir / "pip").is_dir():
365 log_info("Extracting pip to {}", pip_dir)
366 pip_dir.mkdir(parents=True, exist_ok=True)
367 extract_pip_files(ns)
368
369 if ns.include_props:
370 log_info("Generating {} in {}", PYTHON_PROPS_NAME, ns.temp)
371 ns.temp.mkdir(parents=True, exist_ok=True)
372 with open(ns.temp / PYTHON_PROPS_NAME, "wb") as f:
373 f.write(get_props(ns))
374
375
376def _create_zip_file(ns):
377 if not ns.zip:
378 return None
379
380 if ns.zip.is_file():
381 try:
382 ns.zip.unlink()
383 except OSError:
384 log_exception("Unable to remove {}", ns.zip)
385 sys.exit(8)
386 elif ns.zip.is_dir():
387 log_error("Cannot create ZIP file because {} is a directory", ns.zip)
388 sys.exit(8)
389
390 ns.zip.parent.mkdir(parents=True, exist_ok=True)
391 return zipfile.ZipFile(ns.zip, "w", zipfile.ZIP_DEFLATED)
392
393
394def copy_files(files, ns):
395 if ns.copy:
396 ns.copy.mkdir(parents=True, exist_ok=True)
397
398 try:
399 total = len(files)
400 except TypeError:
401 total = None
402 count = 0
403
404 zip_file = _create_zip_file(ns)
405 try:
406 need_compile = []
407 in_catalog = []
408
409 for dest, src in files:
410 count += 1
411 if count % 10 == 0:
412 if total:
413 log_info("Processed {:>4} of {} files", count, total)
414 else:
415 log_info("Processed {} files", count)
416 log_debug("Processing {!s}", src)
417
418 if (
419 ns.precompile
420 and src in PY_FILES
421 and src not in EXCLUDE_FROM_COMPILE
422 and src.parent not in DATA_DIRS
423 and os.path.normcase(str(dest)).startswith(os.path.normcase("Lib"))
424 ):
425 if ns.copy:
426 need_compile.append((dest, ns.copy / dest))
427 else:
428 (ns.temp / "Lib" / dest).parent.mkdir(parents=True, exist_ok=True)
429 shutil.copy2(src, ns.temp / "Lib" / dest)
430 need_compile.append((dest, ns.temp / "Lib" / dest))
431
432 if src not in EXCLUDE_FROM_CATALOG:
433 in_catalog.append((src.name, src))
434
435 if ns.copy:
436 log_debug("Copy {} -> {}", src, ns.copy / dest)
437 (ns.copy / dest).parent.mkdir(parents=True, exist_ok=True)
438 try:
439 shutil.copy2(src, ns.copy / dest)
440 except shutil.SameFileError:
441 pass
442
443 if ns.zip:
444 log_debug("Zip {} into {}", src, ns.zip)
445 zip_file.write(src, str(dest))
446
447 if need_compile:
448 for dest, src in need_compile:
449 compiled = [
450 _compile_one_py(src, None, dest, optimize=0),
451 _compile_one_py(src, None, dest, optimize=1),
452 _compile_one_py(src, None, dest, optimize=2),
453 ]
454 for c in compiled:
455 if not c:
456 continue
457 cdest = Path(dest).parent / Path(c).relative_to(src.parent)
458 if ns.zip:
459 log_debug("Zip {} into {}", c, ns.zip)
460 zip_file.write(c, str(cdest))
461 in_catalog.append((cdest.name, cdest))
462
463 if ns.catalog:
464 # Just write out the CDF now. Compilation and signing is
465 # an extra step
466 log_info("Generating {}", ns.catalog)
467 ns.catalog.parent.mkdir(parents=True, exist_ok=True)
468 write_catalog(ns.catalog, in_catalog)
469
470 finally:
471 if zip_file:
472 zip_file.close()
473
474
475def main():
476 parser = argparse.ArgumentParser()
477 parser.add_argument("-v", help="Increase verbosity", action="count")
478 parser.add_argument(
479 "-s",
480 "--source",
481 metavar="dir",
482 help="The directory containing the repository root",
483 type=Path,
484 default=None,
485 )
486 parser.add_argument(
487 "-b", "--build", metavar="dir", help="Specify the build directory", type=Path
488 )
489 parser.add_argument(
490 "--doc-build",
491 metavar="dir",
492 help="Specify the docs build directory",
493 type=Path,
494 default=None,
495 )
496 parser.add_argument(
497 "--copy",
498 metavar="directory",
499 help="The name of the directory to copy an extracted layout to",
500 type=Path,
501 default=None,
502 )
503 parser.add_argument(
504 "--zip",
505 metavar="file",
506 help="The ZIP file to write all files to",
507 type=Path,
508 default=None,
509 )
510 parser.add_argument(
511 "--catalog",
512 metavar="file",
513 help="The CDF file to write catalog entries to",
514 type=Path,
515 default=None,
516 )
517 parser.add_argument(
518 "--log",
519 metavar="file",
520 help="Write all operations to the specified file",
521 type=Path,
522 default=None,
523 )
524 parser.add_argument(
525 "-t",
526 "--temp",
527 metavar="file",
528 help="A temporary working directory",
529 type=Path,
530 default=None,
531 )
532 parser.add_argument(
533 "-d", "--debug", help="Include debug build", action="store_true"
534 )
535 parser.add_argument(
536 "-p",
537 "--precompile",
538 help="Include .pyc files instead of .py",
539 action="store_true",
540 )
541 parser.add_argument(
542 "-z", "--zip-lib", help="Include library in a ZIP file", action="store_true"
543 )
544 parser.add_argument(
545 "--flat-dlls", help="Does not create a DLLs directory", action="store_true"
546 )
547 parser.add_argument(
548 "-a",
549 "--include-all",
550 help="Include all optional components",
551 action="store_true",
552 )
553 parser.add_argument(
554 "--include-cat",
555 metavar="file",
556 help="Specify the catalog file to include",
557 type=Path,
558 default=None,
559 )
560 for opt, help in get_argparse_options():
561 parser.add_argument(opt, help=help, action="store_true")
562
563 ns = parser.parse_args()
564 update_presets(ns)
565
566 ns.source = ns.source or (Path(__file__).resolve().parent.parent.parent)
567 ns.build = ns.build or Path(sys.executable).parent
568 ns.temp = ns.temp or Path(tempfile.mkdtemp())
569 ns.doc_build = ns.doc_build or (ns.source / "Doc" / "build")
570 if not ns.source.is_absolute():
571 ns.source = (Path.cwd() / ns.source).resolve()
572 if not ns.build.is_absolute():
573 ns.build = (Path.cwd() / ns.build).resolve()
574 if not ns.temp.is_absolute():
575 ns.temp = (Path.cwd() / ns.temp).resolve()
576 if not ns.doc_build.is_absolute():
577 ns.doc_build = (Path.cwd() / ns.doc_build).resolve()
578 if ns.include_cat and not ns.include_cat.is_absolute():
579 ns.include_cat = (Path.cwd() / ns.include_cat).resolve()
580
581 if ns.copy and not ns.copy.is_absolute():
582 ns.copy = (Path.cwd() / ns.copy).resolve()
583 if ns.zip and not ns.zip.is_absolute():
584 ns.zip = (Path.cwd() / ns.zip).resolve()
585 if ns.catalog and not ns.catalog.is_absolute():
586 ns.catalog = (Path.cwd() / ns.catalog).resolve()
587
588 configure_logger(ns)
589
590 log_info(
591 """OPTIONS
592Source: {ns.source}
593Build: {ns.build}
594Temp: {ns.temp}
595
596Copy to: {ns.copy}
597Zip to: {ns.zip}
598Catalog: {ns.catalog}""",
599 ns=ns,
600 )
601
602 if ns.include_idle and not ns.include_tcltk:
603 log_warning("Assuming --include-tcltk to support --include-idle")
604 ns.include_tcltk = True
605
606 try:
607 generate_source_files(ns)
608 files = list(get_layout(ns))
609 copy_files(files, ns)
610 except KeyboardInterrupt:
611 log_info("Interrupted by Ctrl+C")
612 return 3
613 except SystemExit:
614 raise
615 except:
616 log_exception("Unhandled error")
617
618 if error_was_logged():
619 log_error("Errors occurred.")
620 return 1
621
622
623if __name__ == "__main__":
624 sys.exit(int(main() or 0))