diff options
author | Eric Snow <ericsnowcurrently@gmail.com> | 2021-10-28 16:14:37 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-10-28 16:14:37 (GMT) |
commit | 13d9205f4057eeeef80a25d410ad123876dc60cd (patch) | |
tree | 18cbd8603e3f3a29d9f40a0bd8a4648356c205c9 /Tools/freeze | |
parent | 7f61d9d84843e3445f62eb00c47902f0daa30a72 (diff) | |
download | cpython-13d9205f4057eeeef80a25d410ad123876dc60cd.zip cpython-13d9205f4057eeeef80a25d410ad123876dc60cd.tar.gz cpython-13d9205f4057eeeef80a25d410ad123876dc60cd.tar.bz2 |
bpo-45629: Add a test for the "freeze" tool. (gh-29222)
The "freeze" tool has been part of the repo for a long time. However, it hasn't had any tests in the test suite to guard against regressions. We add such a test here. This is especially important as there has been a lot of change recently related to frozen modules, with more to come.
Note that as part of the test we build Python out-of-tree and install it in a temp dir.
https://bugs.python.org/issue45629
Diffstat (limited to 'Tools/freeze')
-rw-r--r-- | Tools/freeze/test/freeze.py | 194 |
1 files changed, 194 insertions, 0 deletions
diff --git a/Tools/freeze/test/freeze.py b/Tools/freeze/test/freeze.py new file mode 100644 index 0000000..18a5d27 --- /dev/null +++ b/Tools/freeze/test/freeze.py @@ -0,0 +1,194 @@ +import os +import os.path +import re +import shlex +import shutil +import subprocess + + +TESTS_DIR = os.path.dirname(__file__) +TOOL_ROOT = os.path.dirname(TESTS_DIR) +SRCDIR = os.path.dirname(os.path.dirname(TOOL_ROOT)) + +MAKE = shutil.which('make') +GIT = shutil.which('git') +FREEZE = os.path.join(TOOL_ROOT, 'freeze.py') +OUTDIR = os.path.join(TESTS_DIR, 'outdir') + + +class UnsupportedError(Exception): + """The operation isn't supported.""" + + +def _run_quiet(cmd, cwd=None): + #print(f'# {" ".join(shlex.quote(a) for a in cmd)}') + return subprocess.run( + cmd, + cwd=cwd, + capture_output=True, + text=True, + check=True, + ) + + +def _run_stdout(cmd, cwd=None): + proc = _run_quiet(cmd, cwd) + return proc.stdout.strip() + + +def find_opt(args, name): + opt = f'--{name}' + optstart = f'{opt}=' + for i, arg in enumerate(args): + if arg == opt or arg.startswith(optstart): + return i + return -1 + + +def ensure_opt(args, name, value): + opt = f'--{name}' + pos = find_opt(args, name) + if value is None: + if pos < 0: + args.append(opt) + else: + args[pos] = opt + elif pos < 0: + args.extend([opt, value]) + else: + arg = args[pos] + if arg == opt: + if pos == len(args) - 1: + raise NotImplementedError((args, opt)) + args[pos + 1] = value + else: + args[pos] = f'{opt}={value}' + + +def git_copy_repo(newroot, oldroot): + if not GIT: + raise UnsupportedError('git') + + if os.path.exists(newroot): + print(f'updating copied repo {newroot}...') + if newroot == SRCDIR: + raise Exception('this probably isn\'t what you wanted') + _run_quiet([GIT, 'clean', '-d', '-f'], newroot) + _run_quiet([GIT, 'reset'], newroot) + _run_quiet([GIT, 'checkout', '.'], newroot) + _run_quiet([GIT, 'pull', '-f', oldroot], newroot) + else: + print(f'copying repo into {newroot}...') + _run_quiet([GIT, 'clone', oldroot, newroot]) + + # Copy over any uncommited files. + text = _run_stdout([GIT, 'status', '-s'], oldroot) + for line in text.splitlines(): + _, _, relfile = line.strip().partition(' ') + relfile = relfile.strip() + isdir = relfile.endswith(os.path.sep) + relfile = relfile.rstrip(os.path.sep) + srcfile = os.path.join(oldroot, relfile) + dstfile = os.path.join(newroot, relfile) + os.makedirs(os.path.dirname(dstfile), exist_ok=True) + if isdir: + shutil.copytree(srcfile, dstfile, dirs_exist_ok=True) + else: + shutil.copy2(srcfile, dstfile) + + +def get_makefile_var(builddir, name): + regex = re.compile(rf'^{name} *=\s*(.*?)\s*$') + filename = os.path.join(builddir, 'Makefile') + try: + infile = open(filename) + except FileNotFoundError: + return None + with infile: + for line in infile: + m = regex.match(line) + if m: + value, = m.groups() + return value or '' + return None + + +def get_config_var(builddir, name): + python = os.path.join(builddir, 'python') + if os.path.isfile(python): + cmd = [python, '-c', + f'import sysconfig; print(sysconfig.get_config_var("{name}"))'] + try: + return _run_stdout(cmd) + except subprocess.CalledProcessError: + pass + return get_makefile_var(builddir, name) + + +################################## +# freezing + +def prepare(script=None, outdir=None): + if not outdir: + outdir = OUTDIR + os.makedirs(outdir, exist_ok=True) + + # Write the script to disk. + if script: + scriptfile = os.path.join(outdir, 'app.py') + with open(scriptfile, 'w') as outfile: + outfile.write(script) + + # Make a copy of the repo to avoid affecting the current build. + srcdir = os.path.join(outdir, 'cpython') + git_copy_repo(srcdir, SRCDIR) + + # We use an out-of-tree build (instead of srcdir). + builddir = os.path.join(outdir, 'python-build') + os.makedirs(builddir, exist_ok=True) + + # Run configure. + print(f'configuring python in {builddir}...') + cmd = [ + os.path.join(srcdir, 'configure'), + *shlex.split(get_config_var(builddir, 'CONFIG_ARGS') or ''), + ] + ensure_opt(cmd, 'cache-file', os.path.join(outdir, 'python-config.cache')) + prefix = os.path.join(outdir, 'python-installation') + ensure_opt(cmd, 'prefix', prefix) + _run_quiet(cmd, builddir) + + if not MAKE: + raise UnsupportedError('make') + + # Build python. + print('building python...') + if os.path.exists(os.path.join(srcdir, 'Makefile')): + # Out-of-tree builds require a clean srcdir. + _run_quiet([MAKE, '-C', srcdir, 'clean']) + _run_quiet([MAKE, '-C', builddir, '-j8']) + + # Install the build. + print(f'installing python into {prefix}...') + _run_quiet([MAKE, '-C', builddir, '-j8', 'install']) + python = os.path.join(prefix, 'bin', 'python3') + + return outdir, scriptfile, python + + +def freeze(python, scriptfile, outdir): + if not MAKE: + raise UnsupportedError('make') + + print(f'freezing {scriptfile}...') + os.makedirs(outdir, exist_ok=True) + _run_quiet([python, FREEZE, '-o', outdir, scriptfile], outdir) + _run_quiet([MAKE, '-C', os.path.dirname(scriptfile)]) + + name = os.path.basename(scriptfile).rpartition('.')[0] + executable = os.path.join(outdir, name) + return executable + + +def run(executable): + return _run_stdout([executable]) |