Home | History | Annotate | Download | only in nuget
      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 pathlib import Path
     13 from zipfile import ZipFile, ZIP_DEFLATED
     14 import subprocess
     15 
     16 TKTCL_RE = re.compile(r'^(_?tk|tcl).+\.(pyd|dll)', re.IGNORECASE)
     17 DEBUG_RE = re.compile(r'_d\.(pyd|dll|exe|pdb|lib)$', re.IGNORECASE)
     18 PYTHON_DLL_RE = re.compile(r'python\d\d?\.dll$', re.IGNORECASE)
     19 
     20 DEBUG_FILES = {
     21     '_ctypes_test',
     22     '_testbuffer',
     23     '_testcapi',
     24     '_testimportmultiple',
     25     '_testmultiphase',
     26     'xxlimited',
     27     'python3_dstub',
     28 }
     29 
     30 EXCLUDE_FROM_LIBRARY = {
     31     '__pycache__',
     32     'ensurepip',
     33     'idlelib',
     34     'pydoc_data',
     35     'site-packages',
     36     'tkinter',
     37     'turtledemo',
     38     'venv',
     39 }
     40 
     41 EXCLUDE_FILE_FROM_LIBRARY = {
     42     'bdist_wininst.py',
     43 }
     44 
     45 EXCLUDE_FILE_FROM_LIBS = {
     46     'ssleay',
     47     'libeay',
     48     'python3stub',
     49 }
     50 
     51 def 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 
     60 def is_not_debug_or_python(p):
     61     return is_not_debug(p) and not PYTHON_DLL_RE.search(p.name)
     62 
     63 def 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 
     82 def 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 
     88 def 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 
     94 FULL_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 
    107 EMBED_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 
    114 if os.getenv('DOC_FILENAME'):
    115     FULL_LAYOUT.append(('Doc/', 'Doc/build/htmlhelp', os.getenv('DOC_FILENAME'), None))
    116 if 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 
    120 def 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 
    158 def 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 
    169 def 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 
    218 if __name__ == "__main__":
    219     sys.exit(int(main() or 0))
    220