Files
Pyarmor-Static-Unpack-1shot/scripts/pymultic
John Richards f59d7d015d Adds podman support and defines PYVERS with official containers
We've found that 3.0.x and 3.1.x don't have official containers on
Docker Hub, so we needed to move to a more explicit check.
2021-10-10 19:59:54 -04:00

288 lines
9.0 KiB
Python
Executable File

#!/usr/bin/env python3
# PyMultiC - Python Multiple Compiler
import os
import sys
import subprocess
import shutil
import re
PYVERS = {
'1.0': '1.0.1',
'1.1': '1.1',
'1.2': '1.2',
'1.3': '1.3',
'1.4': '1.4',
'1.5': '1.5.2',
'1.6': '1.6.1',
'2.0': '2.0.1',
'2.1': '2.1.3',
'2.2': '2.2.3',
'2.3': '2.3.7',
'2.4': '2.4.6',
'2.5': '2.5.6',
'2.6': '2.6.9',
'2.7': '2.7.18',
'3.0': '3.0.1',
'3.1': '3.1.5',
'3.2': '3.2.6',
'3.3': '3.3.7',
'3.4': '3.4.10',
'3.5': '3.5.10',
'3.6': '3.6.13',
'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',
}
USE_CONTAINERS = False
CONTAINER_EXES = ['docker', 'podman']
def fetch_python(snekdir, version):
realver = PYVERS[version]
if version in ('1.0', '1.1'):
tarball = 'python{}.tar.gz'.format(realver)
url = '{}/{}'.format(OLD_PYURL, tarball)
elif version in ('1.2', '1.3', '1.4', '1.5'):
tarball = 'python-{}.tar.gz'.format(realver)
url = '{}/{}'.format(OLD_PYURL, tarball)
elif version == '1.6':
tarball = 'Python-{}.tar.gz'.format(realver)
url = None
else:
tarball = 'Python-{}.tgz'.format(realver)
url = '{}/{}/{}'.format(PYURL, realver, tarball)
pyver_dir = os.path.join(snekdir, 'Python-{}'.format(realver))
if os.path.exists(pyver_dir):
return
tb_dir = os.path.join(snekdir, 'tarballs')
if not os.path.exists(tb_dir):
os.makedirs(tb_dir)
tarfile = os.path.join(tb_dir, tarball)
if not os.path.exists(tarfile):
if version == '1.6':
print('Python 1.6.1 cannot be downloaded automatically due to a license agreement')
print('which must be manually accepted.')
print('Please download it from https://www.python.org/download/releases/1.6.1/download/')
print('and place the tarball in {}'.format(tb_dir))
sys.exit(1)
print('Downloading Python {}...'.format(realver))
if subprocess.call(['curl', '-LfO#', url], cwd=tb_dir) != 0:
sys.exit(1)
print('Extracting Python {}...'.format(realver))
if subprocess.call(['tar', 'xaf', tarfile], cwd=snekdir) != 0:
sys.exit(1)
if os.path.exists(os.path.join(snekdir, 'python-{}'.format(realver))) \
and not os.path.exists(pyver_dir):
# The dual check prevents useless renames on case-insensitive
# file systems
os.rename(os.path.join(snekdir, 'python-{}'.format(realver)), pyver_dir)
patch_file = os.path.join(snekdir, 'python-builds', 'Python-{}.patch'.format(realver))
if os.path.exists(patch_file):
if subprocess.call(['patch', '-p1', '-i', patch_file], cwd=pyver_dir) != 0:
sys.exit(1)
def build_python(snekdir, version):
realver = PYVERS[version]
snek = 'Python-{}'.format(realver)
builddir = os.path.join(snekdir, snek)
# NOTE: This has only been tested on a Debian 10 x86_64 system -- it is
# probably not as robust as it should be...
print('Configuring Python {}...'.format(realver))
logfile = os.path.join(snekdir, '{}.conf.log'.format(snek))
with open(logfile, 'wb') as log:
if subprocess.call(['./configure'], stdout=log, stderr=log, cwd=builddir) != 0:
print('... Configuration failed. See {} for details'.format(logfile))
sys.exit(1)
print('Building Python {}...'.format(realver))
logfile = os.path.join(snekdir, '{}.build.log'.format(snek))
with open(logfile, 'wb') as log:
if subprocess.call(['make'], stdout=log, stderr=log, cwd=builddir) != 0:
print('... Build failed. See {} for details'.format(logfile))
sys.exit(1)
def acquire_python(snekdir, version):
snek = 'Python-{}'.format(PYVERS[version])
pyexe = os.path.join(snekdir, snek, 'python')
if not os.path.exists(pyexe):
fetch_python(snekdir, version)
build_python(snekdir, version)
return pyexe
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))
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))
return None
if infile.endswith('.py'):
outfile = os.path.basename(infile)[:-3]
else:
outfile = os.path.basename(infile)
outfile += '.{}.pyc'.format(ver)
if os.path.exists(outfile):
os.unlink(outfile)
print('*** Compiling for Python {}'.format(bcver))
if ver in {'1.0', '1.1', '1.2', '1.3', '1.4'}:
# The hard way -- hope your code is safe...
srcdir = os.path.dirname(os.path.realpath(infile))
comptmp = os.path.join(srcdir, 'pymc_temp.py')
if os.path.exists(comptmp):
os.unlink(comptmp)
shutil.copyfile(infile, comptmp)
cwdsave = os.getcwd()
os.chdir(srcdir)
proc = subprocess.Popen([pyexe, '-c', 'import pymc_temp'])
proc.communicate()
os.chdir(cwdsave)
if os.path.exists(comptmp + 'o'):
shutil.copyfile(comptmp + 'o', outfile)
os.unlink(comptmp + 'o')
elif os.path.exists(comptmp + 'c'):
shutil.copyfile(comptmp + 'c', outfile)
os.unlink(comptmp + 'c')
os.unlink(comptmp)
else:
# The easy way
proc = subprocess.Popen([pyexe, '-c',
"import py_compile; py_compile.compile('{}', '{}')" \
.format(infile, outfile)])
proc.communicate()
return outfile
def container_compile(snekdir, 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]
if infile.endswith('.py'):
outfile = os.path.basename(infile)[:-3]
else:
outfile = os.path.basename(infile)
outfile += '.{}.pyc'.format(ver)
if os.path.exists(outfile):
os.unlink(outfile)
print('*** Compiling for Python {}'.format(fullver))
# The easy way
proc = subprocess.Popen([container_exe, 'run', '--privileged', '--rm', '--name', '{}-{}'.format(infile, ver),
'-v', '{}:/src'.format(snekdir), '-w', '/src', 'python:{}'.format(fullver),
'python', '-c', "import py_compile; py_compile.compile('{}', '{}')".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 (only available for versions >= {})'
.format(PYVER_CONTAINER_MIN))
print()
print('Output is written to input.<version>.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 == '-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(snekdir, ver, infile)
else:
outfile = local_compile(snekdir, ver, infile)
if outfile is None or not os.path.exists(outfile):
result = 1
sys.exit(result)