2018-06-20 18:54:47 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
2018-06-22 14:22:00 +00:00
|
|
|
"""Helper for cross-compiling distutils-based Python extensions.
|
|
|
|
|
|
|
|
distutils has never had a proper cross-compilation story. This is a hack, which
|
|
|
|
miraculously works, to get around that.
|
|
|
|
|
|
|
|
The gist is:
|
|
|
|
|
|
|
|
- Compile the package natively, replacing calls to the compiler and linker with
|
|
|
|
wrappers that store the arguments in a log, and then delegate along to the
|
|
|
|
real native compiler and linker.
|
|
|
|
|
|
|
|
- Remove all of the native build products.
|
|
|
|
|
|
|
|
- Play back the log, replacing the native compiler with emscripten and
|
|
|
|
adjusting include paths and flags as necessary for cross-compiling to
|
|
|
|
emscripten. This overwrites the results from the original native compilation.
|
|
|
|
|
|
|
|
While this results in more work than strictly necessary (it builds a native
|
|
|
|
version of the package, even though we then throw it away), it seems to be the
|
|
|
|
only reliable way to automatically build a package that interleaves
|
|
|
|
configuration with build.
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
2018-06-20 18:54:47 +00:00
|
|
|
import argparse
|
2018-06-21 01:20:26 +00:00
|
|
|
import importlib.machinery
|
2018-06-20 18:54:47 +00:00
|
|
|
import json
|
|
|
|
import os
|
2018-08-03 16:48:22 +00:00
|
|
|
from pathlib import Path
|
2018-06-20 18:54:47 +00:00
|
|
|
import re
|
|
|
|
import subprocess
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
2018-06-22 14:22:00 +00:00
|
|
|
import common
|
|
|
|
|
|
|
|
|
2018-08-03 16:48:22 +00:00
|
|
|
ROOTDIR = Path(__file__).parent.resolve()
|
2018-06-20 18:54:47 +00:00
|
|
|
symlinks = set(['cc', 'c++', 'ld', 'ar', 'gcc'])
|
|
|
|
|
|
|
|
|
|
|
|
def collect_args(basename):
|
2018-06-22 14:22:00 +00:00
|
|
|
"""
|
|
|
|
This is called when this script is called through a symlink that looks like
|
|
|
|
a compiler or linker.
|
|
|
|
|
|
|
|
It writes the arguments to the build.log, and then delegates to the real
|
|
|
|
native compiler or linker.
|
|
|
|
"""
|
|
|
|
# Remove the symlink compiler from the PATH, so we can delegate to the
|
|
|
|
# native compiler
|
2018-06-20 18:54:47 +00:00
|
|
|
env = dict(os.environ)
|
|
|
|
path = env['PATH']
|
2018-08-03 16:48:22 +00:00
|
|
|
while str(ROOTDIR) + ':' in path:
|
|
|
|
path = path.replace(str(ROOTDIR) + ':', '')
|
2018-06-20 18:54:47 +00:00
|
|
|
env['PATH'] = path
|
2018-06-22 14:22:00 +00:00
|
|
|
|
2018-06-20 18:54:47 +00:00
|
|
|
with open('build.log', 'a') as fd:
|
|
|
|
json.dump([basename] + sys.argv[1:], fd)
|
|
|
|
fd.write('\n')
|
|
|
|
|
2018-06-22 14:22:00 +00:00
|
|
|
sys.exit(
|
|
|
|
subprocess.run(
|
|
|
|
[basename] + sys.argv[1:],
|
|
|
|
env=env).returncode)
|
2018-06-20 18:54:47 +00:00
|
|
|
|
|
|
|
|
|
|
|
def make_symlinks(env):
|
2018-06-22 14:22:00 +00:00
|
|
|
"""
|
|
|
|
Makes sure all of the symlinks that make this script look like a compiler
|
|
|
|
exist.
|
|
|
|
"""
|
2018-08-03 16:48:22 +00:00
|
|
|
exec_path = Path(__file__).resolve()
|
2018-06-20 18:54:47 +00:00
|
|
|
for symlink in symlinks:
|
2018-08-03 16:48:22 +00:00
|
|
|
symlink_path = ROOTDIR / symlink
|
|
|
|
if not symlink_path.exists():
|
|
|
|
symlink_path.symlink_to(exec_path)
|
2018-06-20 18:54:47 +00:00
|
|
|
if symlink == 'c++':
|
|
|
|
var = 'CXX'
|
|
|
|
else:
|
|
|
|
var = symlink.upper()
|
|
|
|
env[var] = symlink
|
|
|
|
|
|
|
|
|
|
|
|
def capture_compile(args):
|
|
|
|
env = dict(os.environ)
|
|
|
|
make_symlinks(env)
|
2018-08-03 16:48:22 +00:00
|
|
|
env['PATH'] = str(ROOTDIR) + ':' + os.environ['PATH']
|
2018-06-20 18:54:47 +00:00
|
|
|
|
|
|
|
result = subprocess.run(
|
2018-08-03 16:48:22 +00:00
|
|
|
[Path(args.host) / 'bin' / 'python3',
|
2018-06-22 14:22:00 +00:00
|
|
|
'setup.py',
|
|
|
|
'install'], env=env)
|
2018-06-20 18:54:47 +00:00
|
|
|
if result.returncode != 0:
|
2018-08-03 16:48:22 +00:00
|
|
|
build_log_path = Path('build.log')
|
|
|
|
if build_log_path.exists():
|
|
|
|
build_log_path.unlink()
|
2018-06-20 18:54:47 +00:00
|
|
|
sys.exit(result.returncode)
|
|
|
|
|
|
|
|
|
|
|
|
def handle_command(line, args):
|
2018-06-22 14:22:00 +00:00
|
|
|
# This is a special case to skip the compilation tests in numpy that aren't
|
|
|
|
# actually part of the build
|
2018-06-20 18:54:47 +00:00
|
|
|
for arg in line:
|
|
|
|
if r'/file.c' in arg or '_configtest' in arg:
|
|
|
|
return
|
2018-07-26 09:44:09 +00:00
|
|
|
if re.match('/tmp/.*/source\.[bco]+', arg):
|
|
|
|
return
|
2018-06-20 18:54:47 +00:00
|
|
|
if arg == '-print-multiarch':
|
|
|
|
return
|
|
|
|
|
|
|
|
if line[0] == 'ar':
|
2018-06-22 14:22:00 +00:00
|
|
|
new_args = ['emar']
|
2018-06-20 18:54:47 +00:00
|
|
|
elif line[0] == 'c++':
|
2018-06-22 14:22:00 +00:00
|
|
|
new_args = ['em++']
|
2018-06-20 18:54:47 +00:00
|
|
|
else:
|
2018-06-22 14:22:00 +00:00
|
|
|
new_args = ['emcc']
|
2018-06-20 18:54:47 +00:00
|
|
|
# distutils doesn't use the c++ compiler when compiling c++ <sigh>
|
2018-06-22 14:22:00 +00:00
|
|
|
if any(arg.endswith('.cpp') for arg in line):
|
|
|
|
new_args = ['em++']
|
2018-06-20 18:54:47 +00:00
|
|
|
shared = '-shared' in line
|
|
|
|
|
|
|
|
if shared:
|
2018-06-22 18:49:52 +00:00
|
|
|
new_args.extend(args.ldflags.split())
|
2018-06-22 14:22:00 +00:00
|
|
|
elif new_args[0] in ('emcc', 'em++'):
|
2018-06-22 18:49:52 +00:00
|
|
|
new_args.extend(args.cflags.split())
|
2018-06-20 18:54:47 +00:00
|
|
|
|
2018-06-22 14:22:00 +00:00
|
|
|
# Go through and adjust arguments
|
2018-06-20 18:54:47 +00:00
|
|
|
for arg in line[1:]:
|
|
|
|
if arg.startswith('-I'):
|
2018-06-22 14:22:00 +00:00
|
|
|
# Don't include any system directories
|
|
|
|
if arg[2:].startswith('/usr'):
|
|
|
|
continue
|
2018-08-03 16:48:22 +00:00
|
|
|
if (str(Path(arg[2:]).resolve()).startswith(args.host) and
|
2018-06-20 19:05:13 +00:00
|
|
|
'site-packages' not in arg):
|
2018-06-22 18:49:52 +00:00
|
|
|
arg = arg.replace('-I' + args.host, '-I' + args.target)
|
2018-06-22 14:22:00 +00:00
|
|
|
# Don't include any system directories
|
2018-06-20 18:54:47 +00:00
|
|
|
if arg.startswith('-L/usr'):
|
|
|
|
continue
|
2018-06-22 14:22:00 +00:00
|
|
|
# The native build is possibly multithreaded, but the emscripten one
|
|
|
|
# definitely isn't
|
2018-06-20 18:54:47 +00:00
|
|
|
arg = re.sub(r'/python([0-9]\.[0-9]+)m', r'/python\1', arg)
|
|
|
|
if arg.endswith('.o'):
|
|
|
|
arg = arg[:-2] + '.bc'
|
|
|
|
if shared and arg.endswith('.so'):
|
|
|
|
arg = arg[:-3] + '.wasm'
|
|
|
|
output = arg
|
|
|
|
new_args.append(arg)
|
|
|
|
|
|
|
|
print(' '.join(new_args))
|
|
|
|
|
|
|
|
result = subprocess.run(new_args)
|
|
|
|
if result.returncode != 0:
|
|
|
|
sys.exit(result.returncode)
|
|
|
|
|
2018-06-22 14:22:00 +00:00
|
|
|
# Emscripten .so files shouldn't have the native platform slug
|
2018-06-20 18:54:47 +00:00
|
|
|
if shared:
|
|
|
|
renamed = output[:-5] + '.so'
|
|
|
|
for ext in importlib.machinery.EXTENSION_SUFFIXES:
|
|
|
|
if ext == '.so':
|
|
|
|
continue
|
|
|
|
if renamed.endswith(ext):
|
|
|
|
renamed = renamed[:-len(ext)] + '.so'
|
|
|
|
break
|
|
|
|
os.rename(output, renamed)
|
|
|
|
|
|
|
|
|
|
|
|
def replay_compile(args):
|
2018-06-22 14:22:00 +00:00
|
|
|
# If pure Python, there will be no build.log file, which is fine -- just do
|
|
|
|
# nothing
|
2018-08-03 16:48:22 +00:00
|
|
|
build_log_path = Path('build.log')
|
|
|
|
if build_log_path.is_file():
|
|
|
|
with open(build_log_path, 'r') as fd:
|
2018-06-20 18:54:47 +00:00
|
|
|
for line in fd:
|
|
|
|
line = json.loads(line)
|
|
|
|
handle_command(line, args)
|
|
|
|
|
|
|
|
|
|
|
|
def clean_out_native_artifacts():
|
|
|
|
for root, dirs, files in os.walk('.'):
|
|
|
|
for file in files:
|
2018-08-03 16:48:22 +00:00
|
|
|
path = Path(root) / file
|
|
|
|
if path.suffix in ('.o', '.so', '.a'):
|
|
|
|
path.unlink()
|
2018-06-20 18:54:47 +00:00
|
|
|
|
|
|
|
|
|
|
|
def build_wrap(args):
|
2018-08-03 16:48:22 +00:00
|
|
|
build_log_path = Path('build.log')
|
|
|
|
if not build_log_path.is_file():
|
2018-06-20 18:54:47 +00:00
|
|
|
capture_compile(args)
|
|
|
|
clean_out_native_artifacts()
|
|
|
|
replay_compile(args)
|
|
|
|
|
|
|
|
|
|
|
|
def parse_args():
|
2018-06-22 14:22:00 +00:00
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
'Cross compile a Python distutils package. '
|
|
|
|
'Run from the root directory of the package\'s source')
|
|
|
|
parser.add_argument(
|
2018-06-22 18:49:52 +00:00
|
|
|
'--cflags', type=str, nargs='?', default=common.DEFAULTCFLAGS,
|
2018-06-22 14:22:00 +00:00
|
|
|
help='Extra compiling flags')
|
|
|
|
parser.add_argument(
|
2018-06-22 18:49:52 +00:00
|
|
|
'--ldflags', type=str, nargs='?', default=common.DEFAULTLDFLAGS,
|
2018-06-22 14:22:00 +00:00
|
|
|
help='Extra linking flags')
|
|
|
|
parser.add_argument(
|
2018-06-22 18:49:52 +00:00
|
|
|
'--host', type=str, nargs='?', default=common.HOSTPYTHON,
|
2018-06-22 14:22:00 +00:00
|
|
|
help='The path to the host Python installation')
|
|
|
|
parser.add_argument(
|
2018-06-22 18:49:52 +00:00
|
|
|
'--target', type=str, nargs='?', default=common.TARGETPYTHON,
|
2018-06-22 14:22:00 +00:00
|
|
|
help='The path to the target Python installation')
|
2018-06-20 18:54:47 +00:00
|
|
|
args = parser.parse_args()
|
|
|
|
return args
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2018-08-03 16:48:22 +00:00
|
|
|
basename = Path(sys.argv[0]).name
|
2018-06-20 18:54:47 +00:00
|
|
|
if basename in symlinks:
|
|
|
|
collect_args(basename)
|
|
|
|
else:
|
|
|
|
args = parse_args()
|
|
|
|
build_wrap(args)
|