|
| 1 | +import os |
| 2 | +import os.path |
| 3 | +import re |
| 4 | +import shlex |
| 5 | +import shutil |
| 6 | +import subprocess |
| 7 | + |
| 8 | + |
| 9 | +TESTS_DIR = os.path.dirname(__file__) |
| 10 | +TOOL_ROOT = os.path.dirname(TESTS_DIR) |
| 11 | +SRCDIR = os.path.dirname(os.path.dirname(TOOL_ROOT)) |
| 12 | + |
| 13 | +MAKE = shutil.which('make') |
| 14 | +GIT = shutil.which('git') |
| 15 | +FREEZE = os.path.join(TOOL_ROOT, 'freeze.py') |
| 16 | +OUTDIR = os.path.join(TESTS_DIR, 'outdir') |
| 17 | + |
| 18 | + |
| 19 | +class UnsupportedError(Exception): |
| 20 | + """The operation isn't supported.""" |
| 21 | + |
| 22 | + |
| 23 | +def _run_quiet(cmd, cwd=None): |
| 24 | + #print(f'# {" ".join(shlex.quote(a) for a in cmd)}') |
| 25 | + return subprocess.run( |
| 26 | + cmd, |
| 27 | + cwd=cwd, |
| 28 | + capture_output=True, |
| 29 | + text=True, |
| 30 | + check=True, |
| 31 | + ) |
| 32 | + |
| 33 | + |
| 34 | +def _run_stdout(cmd, cwd=None): |
| 35 | + proc = _run_quiet(cmd, cwd) |
| 36 | + return proc.stdout.strip() |
| 37 | + |
| 38 | + |
| 39 | +def find_opt(args, name): |
| 40 | + opt = f'--{name}' |
| 41 | + optstart = f'{opt}=' |
| 42 | + for i, arg in enumerate(args): |
| 43 | + if arg == opt or arg.startswith(optstart): |
| 44 | + return i |
| 45 | + return -1 |
| 46 | + |
| 47 | + |
| 48 | +def ensure_opt(args, name, value): |
| 49 | + opt = f'--{name}' |
| 50 | + pos = find_opt(args, name) |
| 51 | + if value is None: |
| 52 | + if pos < 0: |
| 53 | + args.append(opt) |
| 54 | + else: |
| 55 | + args[pos] = opt |
| 56 | + elif pos < 0: |
| 57 | + args.extend([opt, value]) |
| 58 | + else: |
| 59 | + arg = args[pos] |
| 60 | + if arg == opt: |
| 61 | + if pos == len(args) - 1: |
| 62 | + raise NotImplementedError((args, opt)) |
| 63 | + args[pos + 1] = value |
| 64 | + else: |
| 65 | + args[pos] = f'{opt}={value}' |
| 66 | + |
| 67 | + |
| 68 | +def git_copy_repo(newroot, oldroot): |
| 69 | + if not GIT: |
| 70 | + raise UnsupportedError('git') |
| 71 | + |
| 72 | + if os.path.exists(newroot): |
| 73 | + print(f'updating copied repo {newroot}...') |
| 74 | + if newroot == SRCDIR: |
| 75 | + raise Exception('this probably isn\'t what you wanted') |
| 76 | + _run_quiet([GIT, 'clean', '-d', '-f'], newroot) |
| 77 | + _run_quiet([GIT, 'reset'], newroot) |
| 78 | + _run_quiet([GIT, 'checkout', '.'], newroot) |
| 79 | + _run_quiet([GIT, 'pull', '-f', oldroot], newroot) |
| 80 | + else: |
| 81 | + print(f'copying repo into {newroot}...') |
| 82 | + _run_quiet([GIT, 'clone', oldroot, newroot]) |
| 83 | + |
| 84 | + # Copy over any uncommited files. |
| 85 | + text = _run_stdout([GIT, 'status', '-s'], oldroot) |
| 86 | + for line in text.splitlines(): |
| 87 | + _, _, relfile = line.strip().partition(' ') |
| 88 | + relfile = relfile.strip() |
| 89 | + isdir = relfile.endswith(os.path.sep) |
| 90 | + relfile = relfile.rstrip(os.path.sep) |
| 91 | + srcfile = os.path.join(oldroot, relfile) |
| 92 | + dstfile = os.path.join(newroot, relfile) |
| 93 | + os.makedirs(os.path.dirname(dstfile), exist_ok=True) |
| 94 | + if isdir: |
| 95 | + shutil.copytree(srcfile, dstfile, dirs_exist_ok=True) |
| 96 | + else: |
| 97 | + shutil.copy2(srcfile, dstfile) |
| 98 | + |
| 99 | + |
| 100 | +def get_makefile_var(builddir, name): |
| 101 | + regex = re.compile(rf'^{name} *=\s*(.*?)\s*$') |
| 102 | + filename = os.path.join(builddir, 'Makefile') |
| 103 | + try: |
| 104 | + infile = open(filename) |
| 105 | + except FileNotFoundError: |
| 106 | + return None |
| 107 | + with infile: |
| 108 | + for line in infile: |
| 109 | + m = regex.match(line) |
| 110 | + if m: |
| 111 | + value, = m.groups() |
| 112 | + return value or '' |
| 113 | + return None |
| 114 | + |
| 115 | + |
| 116 | +def get_config_var(builddir, name): |
| 117 | + python = os.path.join(builddir, 'python') |
| 118 | + if os.path.isfile(python): |
| 119 | + cmd = [python, '-c', |
| 120 | + f'import sysconfig; print(sysconfig.get_config_var("{name}"))'] |
| 121 | + try: |
| 122 | + return _run_stdout(cmd) |
| 123 | + except subprocess.CalledProcessError: |
| 124 | + pass |
| 125 | + return get_makefile_var(builddir, name) |
| 126 | + |
| 127 | + |
| 128 | +################################## |
| 129 | +# freezing |
| 130 | + |
| 131 | +def prepare(script=None, outdir=None): |
| 132 | + if not outdir: |
| 133 | + outdir = OUTDIR |
| 134 | + os.makedirs(outdir, exist_ok=True) |
| 135 | + |
| 136 | + # Write the script to disk. |
| 137 | + if script: |
| 138 | + scriptfile = os.path.join(outdir, 'app.py') |
| 139 | + with open(scriptfile, 'w') as outfile: |
| 140 | + outfile.write(script) |
| 141 | + |
| 142 | + # Make a copy of the repo to avoid affecting the current build. |
| 143 | + srcdir = os.path.join(outdir, 'cpython') |
| 144 | + git_copy_repo(srcdir, SRCDIR) |
| 145 | + |
| 146 | + # We use an out-of-tree build (instead of srcdir). |
| 147 | + builddir = os.path.join(outdir, 'python-build') |
| 148 | + os.makedirs(builddir, exist_ok=True) |
| 149 | + |
| 150 | + # Run configure. |
| 151 | + print(f'configuring python in {builddir}...') |
| 152 | + cmd = [ |
| 153 | + os.path.join(srcdir, 'configure'), |
| 154 | + *shlex.split(get_config_var(builddir, 'CONFIG_ARGS') or ''), |
| 155 | + ] |
| 156 | + ensure_opt(cmd, 'cache-file', os.path.join(outdir, 'python-config.cache')) |
| 157 | + prefix = os.path.join(outdir, 'python-installation') |
| 158 | + ensure_opt(cmd, 'prefix', prefix) |
| 159 | + _run_quiet(cmd, builddir) |
| 160 | + |
| 161 | + if not MAKE: |
| 162 | + raise UnsupportedError('make') |
| 163 | + |
| 164 | + # Build python. |
| 165 | + print('building python...') |
| 166 | + if os.path.exists(os.path.join(srcdir, 'Makefile')): |
| 167 | + # Out-of-tree builds require a clean srcdir. |
| 168 | + _run_quiet([MAKE, '-C', srcdir, 'clean']) |
| 169 | + _run_quiet([MAKE, '-C', builddir, '-j8']) |
| 170 | + |
| 171 | + # Install the build. |
| 172 | + print(f'installing python into {prefix}...') |
| 173 | + _run_quiet([MAKE, '-C', builddir, '-j8', 'install']) |
| 174 | + python = os.path.join(prefix, 'bin', 'python3') |
| 175 | + |
| 176 | + return outdir, scriptfile, python |
| 177 | + |
| 178 | + |
| 179 | +def freeze(python, scriptfile, outdir): |
| 180 | + if not MAKE: |
| 181 | + raise UnsupportedError('make') |
| 182 | + |
| 183 | + print(f'freezing {scriptfile}...') |
| 184 | + os.makedirs(outdir, exist_ok=True) |
| 185 | + _run_quiet([python, FREEZE, '-o', outdir, scriptfile], outdir) |
| 186 | + _run_quiet([MAKE, '-C', os.path.dirname(scriptfile)]) |
| 187 | + |
| 188 | + name = os.path.basename(scriptfile).rpartition('.')[0] |
| 189 | + executable = os.path.join(outdir, name) |
| 190 | + return executable |
| 191 | + |
| 192 | + |
| 193 | +def run(executable): |
| 194 | + return _run_stdout([executable]) |
0 commit comments