1 import os
2 import os.path
3 import shlex
4 import shutil
5 import subprocess
6 import sysconfig
7 from test import support
8
9
10 def 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
17 TESTS_DIR = os.path.dirname(__file__)
18 TOOL_ROOT = os.path.dirname(TESTS_DIR)
19 SRCDIR = get_python_source_dir()
20
21 MAKE = shutil.which('make')
22 FREEZE = os.path.join(TOOL_ROOT, 'freeze.py')
23 OUTDIR = os.path.join(TESTS_DIR, 'outdir')
24
25
26 class ESC[4;38;5;81mUnsupportedError(ESC[4;38;5;149mException):
27 """The operation isn't supported."""
28
29
30 def _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
53 def _run_stdout(cmd):
54 proc = _run_quiet(cmd)
55 return proc.stdout.strip()
56
57
58 def 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
67 def 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
87 def 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
104 def 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.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
162 def 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
177 def run(executable):
178 return _run_stdout([executable])