#!/usr/bin/env python3 import argparse import importlib.machinery import json import os import re import subprocess import sys ROOTDIR = os.path.abspath(os.path.dirname(__file__)) symlinks = set(['cc', 'c++', 'ld', 'ar', 'gcc']) def collect_args(basename): env = dict(os.environ) path = env['PATH'] while ROOTDIR + ':' in path: path = path.replace(ROOTDIR + ':', '') env['PATH'] = path with open('build.log', 'a') as fd: json.dump([basename] + sys.argv[1:], fd) fd.write('\n') sys.exit(subprocess.run([basename] + sys.argv[1:], env=env).returncode) def make_symlinks(env): exec_path = os.path.abspath(__file__) for symlink in symlinks: symlink_path = os.path.join(ROOTDIR, symlink) if not os.path.exists(symlink_path): os.symlink(exec_path, symlink_path) if symlink == 'c++': var = 'CXX' else: var = symlink.upper() env[var] = symlink def capture_compile(args): env = dict(os.environ) make_symlinks(env) env['PATH'] = ROOTDIR + ':' + os.environ['PATH'] result = subprocess.run( [os.path.join(args.host[0], 'bin', 'python3'), 'setup.py', 'install'], env=env) if result.returncode != 0: if os.path.exists('build.log'): os.remove('build.log') sys.exit(result.returncode) def handle_command(line, args): # This is a special case to skip the compilation tests in numpy for arg in line: if r'/file.c' in arg or '_configtest' in arg: return if arg == '-print-multiarch': return if line[0] == 'ar': line[0] = 'emar' elif line[0] == 'c++': line[0] = 'em++' else: line[0] = 'emcc' # distutils doesn't use the c++ compiler when compiling c++ for arg in line: if arg.endswith('.cpp'): line[0] = 'em++' break shared = '-shared' in line new_args = [line[0]] if shared: new_args.extend(args.ldflags[0].split()) elif line[0] in ('emcc', 'em++'): new_args.extend(args.cflags[0].split()) skip_next = False for arg in line[1:]: if skip_next: skip_next = False continue if arg.startswith('-I'): if (os.path.abspath(arg[2:]).startswith(args.host[0]) and 'site-packages' not in arg): arg = arg.replace('-I' + args.host[0], '-I' + args.target[0]) if arg[2:].startswith('/usr'): continue if arg.startswith('-L/usr'): continue 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) 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): if os.path.isfile('build.log'): with open('build.log', 'r') as fd: 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: path = os.path.join(root, file) basename, ext = os.path.splitext(file) if ext in ('.o', '.so', '.a'): os.remove(path) def build_wrap(args): if not os.path.isfile('build.log'): capture_compile(args) clean_out_native_artifacts() replay_compile(args) def parse_args(): parser = argparse.ArgumentParser() parser.add_argument('--cflags', type=str, nargs=1, default=['']) parser.add_argument('--ldflags', type=str, nargs=1, default=['']) parser.add_argument('--host', type=str, nargs=1) parser.add_argument('--target', type=str, nargs=1) args = parser.parse_args() return args if __name__ == '__main__': basename = os.path.basename(sys.argv[0]) if basename in symlinks: collect_args(basename) else: args = parse_args() build_wrap(args)