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