blob: 56d6f3807b3a7c872c141252e051d088c3f467c1 [file] [log] [blame]
Steve Dowerf70fdd22015-04-14 18:34:04 -04001import argparse
2import re
3import sys
4import shutil
5import os
6import tempfile
7
8from pathlib import Path
9from zipfile import ZipFile, ZIP_DEFLATED
10import subprocess
11
12TKTCL_RE = re.compile(r'^(_?tk|tcl).+\.(pyd|dll)', re.IGNORECASE)
13DEBUG_RE = re.compile(r'_d\.(pyd|dll|exe)$', re.IGNORECASE)
14PYTHON_DLL_RE = re.compile(r'python\d\d?\.dll$', re.IGNORECASE)
15
16def is_not_debug(p):
17 return not DEBUG_RE.search(p.name) and not TKTCL_RE.search(p.name)
18
19def is_not_debug_or_python(p):
20 return is_not_debug(p) and not PYTHON_DLL_RE.search(p.name)
21
22def include_in_lib(p):
23 name = p.name.lower()
24 if p.is_dir():
25 if name in {'__pycache__', 'ensurepip', 'idlelib', 'pydoc_data', 'tkinter', 'turtledemo'}:
26 return False
27 if name.startswith('plat-'):
28 return False
29 if name == 'test' and p.parts[-2].lower() == 'lib':
30 return False
31 return True
32
Steve Dower777af302015-04-19 19:50:35 -070033 suffix = p.suffix.lower()
34 if suffix == '.pyd':
35 return name not in {
36 '_ctypes_test.pyd',
37 '_testbuffer.pyd',
38 '_testcapi.pyd',
39 '_testimportmultiple.pyd',
40 'xxlimited.pyd',
41 }
42 return suffix not in {'.pyc', '.pyo'}
Steve Dowerf70fdd22015-04-14 18:34:04 -040043
44def include_in_tools(p):
45 if p.is_dir() and p.name.lower() in {'scripts', 'i18n', 'pynche', 'demo', 'parser'}:
46 return True
47
48 return p.suffix.lower() in {'.py', '.pyw', '.txt'}
49
50FULL_LAYOUT = [
51 ('/', 'PCBuild/$arch', 'python*.exe', is_not_debug),
52 ('/', 'PCBuild/$arch', 'python*.dll', is_not_debug),
53 ('DLLs/', 'PCBuild/$arch', '*.pyd', is_not_debug),
54 ('DLLs/', 'PCBuild/$arch', '*.dll', is_not_debug),
55 ('include/', 'include', '*.h', None),
56 ('include/', 'PC', 'pyconfig.h', None),
57 ('Lib/', 'Lib', '**/*', include_in_lib),
58 ('Tools/', 'Tools', '**/*', include_in_tools),
59]
60
61if os.getenv('DOC_FILENAME'):
62 FULL_LAYOUT.append(('Doc/', 'Doc/build/htmlhelp', os.getenv('DOC_FILENAME'), None))
63
64EMBED_LAYOUT = [
65 ('/', 'PCBuild/$arch', 'python*.exe', is_not_debug),
66 ('/', 'PCBuild/$arch', '*.pyd', is_not_debug),
67 ('/', 'PCBuild/$arch', '*.dll', is_not_debug),
68 ('python35.zip', 'Lib', '**/*', include_in_lib),
69]
70
71def copy_to_layout(target, source, rel_sources):
72 count = 0
73
74 if target.suffix.lower() == '.zip':
75 if target.exists():
76 target.unlink()
77
78 with ZipFile(str(target), 'w', ZIP_DEFLATED) as f:
79 for s, rel in rel_sources:
80 f.write(str(s), str(rel))
81 count += 1
82
83 else:
84 for s, rel in rel_sources:
85 try:
86 (target / rel).parent.mkdir(parents=True)
87 except FileExistsError:
88 pass
89 shutil.copy(str(s), str(target / rel))
90 count += 1
91
92 return count
93
94def rglob(root, pattern, condition):
95 dirs = [root]
96 recurse = pattern[:3] in {'**/', '**\\'}
97 while dirs:
98 d = dirs.pop(0)
99 for f in d.glob(pattern[3:] if recurse else pattern):
100 if recurse and f.is_dir() and (not condition or condition(f)):
101 dirs.append(f)
102 elif f.is_file() and (not condition or condition(f)):
103 yield f, f.relative_to(root)
104
105def main():
106 parser = argparse.ArgumentParser()
107 parser.add_argument('-s', '--source', metavar='dir', help='The directory containing the repository root', type=Path)
108 parser.add_argument('-o', '--out', metavar='file', help='The name of the output self-extracting archive', type=Path, required=True)
109 parser.add_argument('-t', '--temp', metavar='dir', help='A directory to temporarily extract files into', type=Path, default=None)
110 parser.add_argument('-e', '--embed', help='Create an embedding layout', action='store_true', default=False)
111 parser.add_argument('-a', '--arch', help='Specify the architecture to use (win32/amd64)', type=str, default="win32")
112 parser.add_argument('--rar', help='Full path to WinRAR compressor (rar.exe)', type=Path, default=Path("rar.exe"))
113 ns = parser.parse_args()
114
115 source = ns.source or (Path(__file__).parent.parent.parent)
116 out = ns.out
117 arch = ns.arch
118 rar = getattr(ns, 'rar')
119 assert isinstance(source, Path)
120 assert isinstance(out, Path)
121 assert isinstance(arch, str)
122 assert isinstance(rar, Path)
123
124 if ns.temp:
125 temp = ns.temp
126 delete_temp = False
127 else:
128 temp = Path(tempfile.mkdtemp())
129 delete_temp = True
130
131 try:
132 out.parent.mkdir(parents=True)
133 except FileExistsError:
134 pass
135 try:
136 temp.mkdir(parents=True)
137 except FileExistsError:
138 pass
139
140 layout = EMBED_LAYOUT if ns.embed else FULL_LAYOUT
141
142 try:
143 for t, s, p, c in layout:
144 s = source / s.replace("$arch", arch)
145 copied = copy_to_layout(
146 temp / t.rstrip('/'),
147 source,
148 rglob(s, p, c)
149 )
150 print('Copied {} files'.format(copied))
151
152 if rar and rar.is_file():
153 subprocess.check_call([
154 str(rar),
155 "a",
156 "-m5", "-ed", "-ep1", "-s", "-r",
157 "-sfxwincon.sfx",
158 str(out),
159 str(temp / '*')
160 ])
161 finally:
162 if delete_temp:
163 shutil.rmtree(temp, True)
164
165
166if __name__ == "__main__":
167 sys.exit(int(main() or 0))