diff --git a/scripts/pymultic b/scripts/pymultic index c595548..bb18fbe 100755 --- a/scripts/pymultic +++ b/scripts/pymultic @@ -33,12 +33,32 @@ PYVERS = { '3.7': '3.7.10', '3.8': '3.8.9', '3.9': '3.9.4', + '3.10': '3.10.0', } OLD_PYTHONS = ('1.0', '1.1', '1.2', '1.3', '1.4', '1.5') OLD_PYURL = 'https://legacy.python.org/download/releases/src' PYURL = 'https://www.python.org/ftp/python' +# Not all versions of Python have an official container. +PYVER_CONTAINERS = { + '2.7', + '3.2', + '3.3', + '3.4', + '3.5', + '3.6', + '3.7', + '3.8', + '3.9', + '3.10', +} +CONTAINER_EXES = ['podman', 'docker'] +CONTAINER_EXE_EXTRA_ARGS = { + 'podman': [], + 'docker': ['-u', '{}:{}'.format(os.getuid(), os.getgid())], # Docker requires extra magic for permissions. +} + def fetch_python(snekdir, version): realver = PYVERS[version] @@ -122,58 +142,19 @@ def acquire_python(snekdir, version): return pyexe -if len(sys.argv) < 2: - print('Usage: {} [versions] input.py'.format(sys.argv[0])) - print('Compile input.py for one or more python versions') - print('Output is written to input..pyc for each version successfully compiled') - print() - print('Version is X.Y (e.g. 3.4), not including the patch version') - sys.exit(1) - -RE_PYVER = re.compile(r'\d\.\d') - -pythons = [] -infile = None -for arg in sys.argv[1:]: - if RE_PYVER.match(arg): - if arg in PYVERS.keys(): - pythons.append(arg) - else: - print('Unknown Python version: {}'.format(arg)) - sys.exit(1) - elif arg.startswith('-'): - print("WARNING: Unrecognized argument '{}'".format(arg)) - else: - infile = arg - -if infile is None: - print('No input file specified') - sys.exit(1) -elif not os.path.exists(infile): - print('Error: Input file {} does not exist'.format(infile)) - sys.exit(1) - -if len(pythons) == 0: - print('At least one Python version is required') - sys.exit(1) - -snekdir = os.path.dirname(os.path.realpath(__file__)) -result = 0 -for ver in pythons: +def local_compile(snekdir, ver, infile): pyexe = acquire_python(snekdir, ver) proc = subprocess.Popen([pyexe, '-c', 'import sys; print(sys.version)'], stdout=subprocess.PIPE) out, _ = proc.communicate() if proc.returncode != 0: print('Could not determine Python version for {}'.format(ver)) - result = 1 - continue + return None bcver = str(out, 'iso-8859-1').split(' ', 1)[0] if not bcver.startswith(ver): print('Python {} reported itself as version {}!'.format(ver, bcver)) - result = 1 - continue + return None if infile.endswith('.py'): outfile = os.path.basename(infile)[:-3] @@ -210,7 +191,107 @@ for ver in pythons: .format(infile, outfile)]) proc.communicate() - if not os.path.exists(outfile): + return outfile + + +def container_compile(ver, infile): + if ver not in PYVER_CONTAINERS: + print('Container compilation not supported for version {}'.format(ver)) + return None + + container_exe = None + for ce in CONTAINER_EXES: + if shutil.which(ce) is not None: + container_exe = ce + break + + if container_exe is None: + print('Cannot find {} in $PATH'.format(' or '.join(CONTAINER_EXES))) + return None + + fullver = PYVERS[ver] + + indir = os.path.dirname(os.path.abspath(infile)) + infile = os.path.basename(infile) + + if infile.endswith('.py'): + outfile = infile[:-3] + else: + outfile = infile + outfile += '.{}.pyc'.format(ver) + if os.path.exists(outfile): + os.unlink(outfile) + + print('*** Compiling for Python {}'.format(fullver)) + proc = subprocess.Popen([container_exe, 'run'] + CONTAINER_EXE_EXTRA_ARGS[container_exe] + + ['--rm', '--name', '{}'.format(outfile), + '-v', '{}:/indir:Z'.format(indir), + '-v', '{}:/outdir:Z'.format(os.getcwd()), '-w', '/outdir', + 'python:{}'.format(fullver), + 'python', '-c', + "import py_compile; py_compile.compile('/indir/{}', '{}')".format(infile, outfile)]) + proc.communicate() + + return outfile + + +if len(sys.argv) < 2: + print('Usage: {} [-c] [versions] input.py'.format(sys.argv[0])) + print('Compile input.py for one or more python versions') + print() + print('-c\tuse prebuilt containers for running different versions of Python') + print('\t(not available for all versions)') + print() + print('Output is written to input..pyc for each version successfully compiled') + print() + print('Version is X.Y (e.g. 3.4), not including the patch version') + sys.exit(1) + +RE_PYVER = re.compile(r'\d\.\d') + +pythons = [] +infile = None +use_containers = False +for arg in sys.argv[1:]: + if RE_PYVER.match(arg): + if arg in PYVERS.keys(): + pythons.append(arg) + else: + print('Unknown Python version: {}'.format(arg)) + sys.exit(1) + elif arg == '-c': + use_containers = True + elif arg.startswith('-'): + print("WARNING: Unrecognized argument '{}'".format(arg)) + else: + infile = arg + +if infile is None: + print('No input file specified') + sys.exit(1) +elif not os.path.exists(infile): + print('Error: Input file {} does not exist'.format(infile)) + sys.exit(1) + +if len(pythons) == 0: + print('At least one Python version is required') + sys.exit(1) + +snekdir = os.path.dirname(os.path.realpath(__file__)) +result = 0 +for ver in pythons: + compile_with_container = use_containers + if use_containers and ver not in PYVER_CONTAINERS: + print('Warning: No supported container for {} - using local build'.format(ver)) + compile_with_container = False + + outfile = None + if compile_with_container: + outfile = container_compile(ver, infile) + else: + outfile = local_compile(snekdir, ver, infile) + + if outfile is None or not os.path.exists(outfile): result = 1 sys.exit(result)