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