• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import os
2import os.path
3import shlex
4import shutil
5import subprocess
6import sysconfig
7from test import support
8
9
10def get_python_source_dir():
11    src_dir = sysconfig.get_config_var('abs_srcdir')
12    if not src_dir:
13        src_dir = sysconfig.get_config_var('srcdir')
14    return os.path.abspath(src_dir)
15
16
17TESTS_DIR = os.path.dirname(__file__)
18TOOL_ROOT = os.path.dirname(TESTS_DIR)
19SRCDIR = get_python_source_dir()
20
21MAKE = shutil.which('make')
22FREEZE = os.path.join(TOOL_ROOT, 'freeze.py')
23OUTDIR = os.path.join(TESTS_DIR, 'outdir')
24
25
26class UnsupportedError(Exception):
27    """The operation isn't supported."""
28
29
30def _run_quiet(cmd, *, cwd=None):
31    if cwd:
32        print('+', 'cd', cwd, flush=True)
33    print('+', shlex.join(cmd), flush=True)
34    try:
35        return subprocess.run(
36            cmd,
37            cwd=cwd,
38            capture_output=True,
39            text=True,
40            check=True,
41        )
42    except subprocess.CalledProcessError as err:
43        # Don't be quiet if things fail
44        print(f"{err.__class__.__name__}: {err}")
45        print("--- STDOUT ---")
46        print(err.stdout)
47        print("--- STDERR ---")
48        print(err.stderr)
49        print("---- END ----")
50        raise
51
52
53def _run_stdout(cmd):
54    proc = _run_quiet(cmd)
55    return proc.stdout.strip()
56
57
58def find_opt(args, name):
59    opt = f'--{name}'
60    optstart = f'{opt}='
61    for i, arg in enumerate(args):
62        if arg == opt or arg.startswith(optstart):
63            return i
64    return -1
65
66
67def ensure_opt(args, name, value):
68    opt = f'--{name}'
69    pos = find_opt(args, name)
70    if value is None:
71        if pos < 0:
72            args.append(opt)
73        else:
74            args[pos] = opt
75    elif pos < 0:
76        args.extend([opt, value])
77    else:
78        arg = args[pos]
79        if arg == opt:
80            if pos == len(args) - 1:
81                raise NotImplementedError((args, opt))
82            args[pos + 1] = value
83        else:
84            args[pos] = f'{opt}={value}'
85
86
87def copy_source_tree(newroot, oldroot):
88    print(f'copying the source tree from {oldroot} to {newroot}...')
89    if os.path.exists(newroot):
90        if newroot == SRCDIR:
91            raise Exception('this probably isn\'t what you wanted')
92        shutil.rmtree(newroot)
93
94    shutil.copytree(oldroot, newroot, ignore=support.copy_python_src_ignore)
95    if os.path.exists(os.path.join(newroot, 'Makefile')):
96        # Out-of-tree builds require a clean srcdir. "make clean" keeps
97        # the "python" program, so use "make distclean" instead.
98        _run_quiet([MAKE, 'distclean'], cwd=newroot)
99
100
101##################################
102# freezing
103
104def prepare(script=None, outdir=None):
105    print()
106    print("cwd:", os.getcwd())
107
108    if not outdir:
109        outdir = OUTDIR
110    os.makedirs(outdir, exist_ok=True)
111
112    # Write the script to disk.
113    if script:
114        scriptfile = os.path.join(outdir, 'app.py')
115        print(f'creating the script to be frozen at {scriptfile}')
116        with open(scriptfile, 'w', encoding='utf-8') as outfile:
117            outfile.write(script)
118
119    # Make a copy of the repo to avoid affecting the current build
120    # (e.g. changing PREFIX).
121    srcdir = os.path.join(outdir, 'cpython')
122    copy_source_tree(srcdir, SRCDIR)
123
124    # We use an out-of-tree build (instead of srcdir).
125    builddir = os.path.join(outdir, 'python-build')
126    os.makedirs(builddir, exist_ok=True)
127
128    # Run configure.
129    print(f'configuring python in {builddir}...')
130    config_args = shlex.split(sysconfig.get_config_var('CONFIG_ARGS') or '')
131    cmd = [os.path.join(srcdir, 'configure'), *config_args]
132    ensure_opt(cmd, 'cache-file', os.path.join(outdir, 'python-config.cache'))
133    prefix = os.path.join(outdir, 'python-installation')
134    ensure_opt(cmd, 'prefix', prefix)
135    _run_quiet(cmd, cwd=builddir)
136
137    if not MAKE:
138        raise UnsupportedError('make')
139
140    cores = os.process_cpu_count()
141    if cores and cores >= 3:
142        # this test is most often run as part of the whole suite with a lot
143        # of other tests running in parallel, from 1-2 vCPU systems up to
144        # people's NNN core beasts. Don't attempt to use it all.
145        jobs = cores * 2 // 3
146        parallel = f'-j{jobs}'
147    else:
148        parallel = '-j2'
149
150    # Build python.
151    print(f'building python {parallel=} in {builddir}...')
152    _run_quiet([MAKE, parallel], cwd=builddir)
153
154    # Install the build.
155    print(f'installing python into {prefix}...')
156    _run_quiet([MAKE, 'install'], cwd=builddir)
157    python = os.path.join(prefix, 'bin', 'python3')
158
159    return outdir, scriptfile, python
160
161
162def freeze(python, scriptfile, outdir):
163    if not MAKE:
164        raise UnsupportedError('make')
165
166    print(f'freezing {scriptfile}...')
167    os.makedirs(outdir, exist_ok=True)
168    # Use -E to ignore PYTHONSAFEPATH
169    _run_quiet([python, '-E', FREEZE, '-o', outdir, scriptfile], cwd=outdir)
170    _run_quiet([MAKE], cwd=os.path.dirname(scriptfile))
171
172    name = os.path.basename(scriptfile).rpartition('.')[0]
173    executable = os.path.join(outdir, name)
174    return executable
175
176
177def run(executable):
178    return _run_stdout([executable])
179