summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Lib/test/support/__init__.py11
-rw-r--r--Lib/test/test_tools/test_freeze.py29
-rw-r--r--Tools/freeze/test/freeze.py194
4 files changed, 235 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index c3fc748..b2ad766 100644
--- a/.gitignore
+++ b/.gitignore
@@ -120,6 +120,7 @@ Tools/unicode/data/
Tools/msi/obj
Tools/ssl/amd64
Tools/ssl/win32
+Tools/freeze/test/outdir
# The frozen modules are always generated by the build so we don't
# keep them in the repo. Also see Tools/scripts/freeze_modules.py.
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index 85fd741..fc3c99e 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -372,6 +372,17 @@ def requires_mac_ver(*min_version):
return decorator
+def skip_if_buildbot(reason=None):
+ """Decorator raising SkipTest if running on a buildbot."""
+ if not reason:
+ reason = 'not suitable for buildbots'
+ if sys.platform == 'win32':
+ isbuildbot = os.environ.get('USERNAME') == 'Buildbot'
+ else:
+ isbuildbot = os.environ.get('USER') == 'buildbot'
+ return unittest.skipIf(isbuildbot, reason)
+
+
def system_must_validate_cert(f):
"""Skip the test on TLS certificate validation failures."""
@functools.wraps(f)
diff --git a/Lib/test/test_tools/test_freeze.py b/Lib/test/test_tools/test_freeze.py
new file mode 100644
index 0000000..392a776
--- /dev/null
+++ b/Lib/test/test_tools/test_freeze.py
@@ -0,0 +1,29 @@
+"""Sanity-check tests for the "freeze" tool."""
+
+import sys
+import textwrap
+import unittest
+
+from test import support
+
+from . import imports_under_tool, skip_if_missing
+skip_if_missing('freeze')
+with imports_under_tool('freeze', 'test'):
+ import freeze as helper
+
+
+@unittest.skipIf(sys.platform.startswith('win'), 'not supported on Windows')
+@support.skip_if_buildbot('not all buildbots have enough space')
+class TestFreeze(unittest.TestCase):
+
+ def test_freeze_simple_script(self):
+ script = textwrap.dedent("""
+ import sys
+ print('running...')
+ sys.exit(0)
+ """)
+ outdir, scriptfile, python = helper.prepare(script)
+
+ executable = helper.freeze(python, scriptfile, outdir)
+ text = helper.run(executable)
+ self.assertEqual(text, 'running...')
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])