1 #! /usr/bin/python3 2 3 import argparse 4 import py_compile 5 import re 6 import sys 7 import shutil 8 import stat 9 import os 10 import tempfile 11 12 from itertools import chain 13 from pathlib import Path 14 from zipfile import ZipFile, ZIP_DEFLATED 15 import subprocess 16 17 TKTCL_RE = re.compile(r'^(_?tk|tcl).+\.(pyd|dll)', re.IGNORECASE) 18 DEBUG_RE = re.compile(r'_d\.(pyd|dll|exe|pdb|lib)$', re.IGNORECASE) 19 PYTHON_DLL_RE = re.compile(r'python\d\d?\.dll$', re.IGNORECASE) 20 21 DEBUG_FILES = { 22 '_ctypes_test', 23 '_testbuffer', 24 '_testcapi', 25 '_testimportmultiple', 26 '_testmultiphase', 27 'xxlimited', 28 'python3_dstub', 29 } 30 31 EXCLUDE_FROM_LIBRARY = { 32 '__pycache__', 33 'ensurepip', 34 'idlelib', 35 'pydoc_data', 36 'site-packages', 37 'tkinter', 38 'turtledemo', 39 'venv', 40 } 41 42 EXCLUDE_FILE_FROM_LIBRARY = { 43 'bdist_wininst.py', 44 } 45 46 EXCLUDE_FILE_FROM_LIBS = { 47 'ssleay', 48 'libeay', 49 'python3stub', 50 } 51 52 def is_not_debug(p): 53 if DEBUG_RE.search(p.name): 54 return False 55 56 if TKTCL_RE.search(p.name): 57 return False 58 59 return p.stem.lower() not in DEBUG_FILES 60 61 def is_not_debug_or_python(p): 62 return is_not_debug(p) and not PYTHON_DLL_RE.search(p.name) 63 64 def include_in_lib(p): 65 name = p.name.lower() 66 if p.is_dir(): 67 if name in EXCLUDE_FROM_LIBRARY: 68 return False 69 if name.startswith('plat-'): 70 return False 71 if name == 'test' and p.parts[-2].lower() == 'lib': 72 return False 73 if name in {'test', 'tests'} and p.parts[-3].lower() == 'lib': 74 return False 75 return True 76 77 if name in EXCLUDE_FILE_FROM_LIBRARY: 78 return False 79 80 suffix = p.suffix.lower() 81 return suffix not in {'.pyc', '.pyo', '.exe'} 82 83 def include_in_libs(p): 84 if not is_not_debug(p): 85 return False 86 87 return p.stem.lower() not in EXCLUDE_FILE_FROM_LIBS 88 89 def include_in_tools(p): 90 if p.is_dir() and p.name.lower() in {'scripts', 'i18n', 'pynche', 'demo', 'parser'}: 91 return True 92 93 return p.suffix.lower() in {'.py', '.pyw', '.txt'} 94 95 FULL_LAYOUT = [ 96 ('/', 'PCBuild/$arch', 'python.exe', is_not_debug), 97 ('/', 'PCBuild/$arch', 'pythonw.exe', is_not_debug), 98 ('/', 'PCBuild/$arch', 'python27.dll', None), 99 ('DLLs/', 'PCBuild/$arch', '*.pyd', is_not_debug), 100 ('DLLs/', 'PCBuild/$arch', '*.dll', is_not_debug_or_python), 101 ('include/', 'include', '*.h', None), 102 ('include/', 'PC', 'pyconfig.h', None), 103 ('Lib/', 'Lib', '**/*', include_in_lib), 104 ('libs/', 'PCBuild/$arch', '*.lib', include_in_libs), 105 ('Tools/', 'Tools', '**/*', include_in_tools), 106 ] 107 108 EMBED_LAYOUT = [ 109 ('/', 'PCBuild/$arch', 'python*.exe', is_not_debug), 110 ('/', 'PCBuild/$arch', '*.pyd', is_not_debug), 111 ('/', 'PCBuild/$arch', '*.dll', is_not_debug), 112 ('python{0.major}{0.minor}.zip'.format(sys.version_info), 'Lib', '**/*', include_in_lib), 113 ] 114 115 if os.getenv('DOC_FILENAME'): 116 FULL_LAYOUT.append(('Doc/', 'Doc/build/htmlhelp', os.getenv('DOC_FILENAME'), None)) 117 if os.getenv('VCREDIST_PATH'): 118 FULL_LAYOUT.append(('/', os.getenv('VCREDIST_PATH'), 'vcruntime*.dll', None)) 119 EMBED_LAYOUT.append(('/', os.getenv('VCREDIST_PATH'), 'vcruntime*.dll', None)) 120 121 def copy_to_layout(target, rel_sources): 122 count = 0 123 124 if target.suffix.lower() == '.zip': 125 if target.exists(): 126 target.unlink() 127 128 with ZipFile(str(target), 'w', ZIP_DEFLATED) as f: 129 with tempfile.TemporaryDirectory() as tmpdir: 130 for s, rel in rel_sources: 131 if rel.suffix.lower() == '.py': 132 pyc = Path(tmpdir) / rel.with_suffix('.pyc').name 133 try: 134 py_compile.compile(str(s), str(pyc), str(rel), doraise=True, optimize=2) 135 except py_compile.PyCompileError: 136 f.write(str(s), str(rel)) 137 else: 138 f.write(str(pyc), str(rel.with_suffix('.pyc'))) 139 else: 140 f.write(str(s), str(rel)) 141 count += 1 142 143 else: 144 for s, rel in rel_sources: 145 dest = target / rel 146 try: 147 dest.parent.mkdir(parents=True) 148 except FileExistsError: 149 pass 150 if dest.is_file(): 151 dest.chmod(stat.S_IWRITE) 152 shutil.copy(str(s), str(dest)) 153 if dest.is_file(): 154 dest.chmod(stat.S_IWRITE) 155 count += 1 156 157 return count 158 159 def rglob(root, pattern, condition): 160 dirs = [root] 161 recurse = pattern[:3] in {'**/', '**\\'} 162 while dirs: 163 d = dirs.pop(0) 164 for f in d.glob(pattern[3:] if recurse else pattern): 165 if recurse and f.is_dir() and (not condition or condition(f)): 166 dirs.append(f) 167 elif f.is_file() and (not condition or condition(f)): 168 yield f, f.relative_to(root) 169 170 def main(): 171 parser = argparse.ArgumentParser() 172 parser.add_argument('-s', '--source', metavar='dir', help='The directory containing the repository root', type=Path) 173 parser.add_argument('-o', '--out', metavar='file', help='The name of the output self-extracting archive', type=Path, default=None) 174 parser.add_argument('-t', '--temp', metavar='dir', help='A directory to temporarily extract files into', type=Path, default=None) 175 parser.add_argument('-e', '--embed', help='Create an embedding layout', action='store_true', default=False) 176 parser.add_argument('-a', '--arch', help='Specify the architecture to use (win32/amd64)', type=str, default="win32") 177 ns = parser.parse_args() 178 179 source = ns.source or (Path(__file__).resolve().parent.parent.parent) 180 out = ns.out 181 arch = '' if ns.arch == 'win32' else ns.arch 182 assert isinstance(source, Path) 183 assert not out or isinstance(out, Path) 184 assert isinstance(arch, str) 185 186 if ns.temp: 187 temp = ns.temp 188 delete_temp = False 189 else: 190 temp = Path(tempfile.mkdtemp()) 191 delete_temp = True 192 193 if out: 194 try: 195 out.parent.mkdir(parents=True) 196 except FileExistsError: 197 pass 198 try: 199 temp.mkdir(parents=True) 200 except FileExistsError: 201 pass 202 203 layout = EMBED_LAYOUT if ns.embed else FULL_LAYOUT 204 205 try: 206 for t, s, p, c in layout: 207 fs = source / s.replace("$arch", arch) 208 files = rglob(fs, p, c) 209 extra_files = [] 210 if s == 'Lib' and p == '**/*': 211 extra_files.append(( 212 source / 'tools' / 'nuget' / 'distutils.command.bdist_wininst.py', 213 Path('distutils') / 'command' / 'bdist_wininst.py' 214 )) 215 copied = copy_to_layout(temp / t.rstrip('/'), chain(files, extra_files)) 216 print('Copied {} files'.format(copied)) 217 218 if out: 219 total = copy_to_layout(out, rglob(temp, '**/*', None)) 220 print('Wrote {} files to {}'.format(total, out)) 221 finally: 222 if delete_temp: 223 shutil.rmtree(temp, True) 224 225 226 if __name__ == "__main__": 227 sys.exit(int(main() or 0)) 228