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