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