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 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