#!/usr/bin/env python3
"""gexcc / gexc++: a C/C++ compiler for gex's wasm32 Linux target.

Used like gcc or clang. Compiles with clang --target=wasm32-unknown-linux-musl
against the gexcc sysroot (musl with gex's wasm32 arch, libgex, compiler-rt).
A link writes a program: a position-independent wasm module importing gex's
memory and function table, in the ELF32 container gexc runs
(tools/wine-wasm/wasm-elf.py). The container also carries a host launcher
(hoststub.c): on an x86 Linux build machine the program runs itself under
gexc, so configure checks and test suites run it like a native binary.

Libraries are static. `-shared` writes an archive (with a `gexcc.deps` member
naming the libraries it was linked with), so a project that insists on
building libfoo.so still links: -lfoo then takes the archive, and its
dependencies follow it into the program.

Layout (tools/gexcc/install.sh PREFIX):
  PREFIX/bin/gexcc, gexc++, wasm32-unknown-linux-musl-{cc,gcc,c++,g++,ar,...}
  PREFIX/lib/gexcc/     flags.sh, wasm-elf.py, forkreach.py, hoststub.bin,
                        runner (the gexc path baked into containers)
  PREFIX/sysroot/       include/, lib/ (musl, libgex, builtins), binaryen/,
                        usr/ (where cross-built libraries install)

Environment: GEXCC_SYSROOT overrides the sysroot; GEXCC_RUNNER the gexc a
container runs under (also read by the container itself at run time);
GEXCC_BAKE_RUNNER the runner path linked into containers (empty: none);
GEXCC_KEEP_WASM=1 keeps OUT.wasm beside a linked program; GEXCC_VERBOSE=1
prints every command; GEXCC_NOFORK=1 is -fgex-no-fork (a link whose fork
returns ENOSYS, with no asyncify: rt/nofork.c). -fgex-guest-i386: a program
that runs i386 code of its own (a JIT's: <gex/i386.h>, rt/i386.c)."""
import atexit, os, re, shutil, subprocess, sys, tempfile

TRIPLE = 'wasm32-unknown-linux-musl'
me = os.path.realpath(__file__)
# The compiler, linker and binaryen are build-machine programs: a build that
# points LD_LIBRARY_PATH (or LD_PRELOAD) at the target libraries it just made
# (ncurses' tests, libtool's wrappers) must not hand them to the host's
# loader, which stops at a wasm file ("invalid ELF header")
for v in ('LD_LIBRARY_PATH', 'LD_PRELOAD'):
    os.environ.pop(v, None)
PREFIX = os.path.dirname(os.path.dirname(me))
LIBDIR = os.path.join(PREFIX, 'lib', 'gexcc')
SYSROOT = os.environ.get('GEXCC_SYSROOT') or os.path.join(PREFIX, 'sysroot')
VERBOSE = bool(os.environ.get('GEXCC_VERBOSE'))
name = os.path.basename(sys.argv[0])
CXX = name.endswith(('++', 'cxx')) or name.endswith('-c++')


def die(msg, code=1):
    sys.stderr.write('gexcc: %s\n' % msg)
    sys.exit(code)


def run(cmd):
    if VERBOSE:
        sys.stderr.write('+ %s\n' % ' '.join(cmd))
    return subprocess.call(cmd)


if not os.path.isdir(LIBDIR):
    die('no %s: run tools/gexcc/install.sh PREFIX and use PREFIX/bin/gexcc' % LIBDIR)


def read_flags():
    flags = {}
    for line in open(os.path.join(LIBDIR, 'flags.sh')):
        if '=' in line and not line.startswith('#'):
            k, v = line.strip().split('=', 1)
            v = v.strip('"')
            for kk, vv in flags.items():
                v = v.replace('$' + kk, vv)
            flags[k] = v
    return flags


FLAGS = read_flags()
# Programs and their libraries lower setjmp onto the legacy exception
# instructions: binaryen's asyncify (fork) can instrument try/catch but not
# try_table. The link translates them to the standard ones (finish()).
BASE = [a.replace('-wasm-use-legacy-eh=false', '-wasm-use-legacy-eh=true') for a in FLAGS['WASM_CFLAGS'].split()]
# C++: exceptions and RTTI are on unless asked off (libc++abi over wasm EH).
# C++ exceptions on wasm EH: clang turns on the backend's lowering
# (-wasm-enable-eh) only for emscripten; without it every catch is dropped.
# C gets it too, for -fexceptions cleanups.
BASE += ['-mllvm', '-wasm-enable-eh']
# Default visibility, as on ELF (clang's wasm default is hidden): a library
# built shared exports what it would on Linux, and a static build is the
# same code. A later -fvisibility= wins.
BASE += ['-fvisibility=default']
# A call to an undeclared C library function is a warning, as with GCC on
# glibc in effect: glibc's headers declare much more than they must (its
# <stdlib.h> and <unistd.h> bring in <string.h>'s functions), so programs
# that call strcmp after <stdlib.h> alone build there. Clang gives the
# library function its real type, so the call is right on wasm too.
if not CXX:
    BASE += ['-Wno-error=implicit-function-declaration']
# size_t is unsigned long on wasm32 and unsigned int on every other 32-bit
# Linux target, so code that passes a size_t * where it means a uint32_t *
# (kmod's __builtin_uadd_overflow) is right there and only mismatched here:
# a warning, as same-size pointers were before GCC 14 and clang 16.
if not CXX:
    BASE += ['-Wno-error=incompatible-pointer-types']
# `register` in C++17: GCC warns (it is still in glibc-era sources, libGLU),
# clang's default is an error.
if CXX:
    BASE += ['-Wno-error=register']
# An unused `static inline` function: GCC says nothing, clang warns, and a
# -Werror build (keyutils) stops. Still a warning here.
BASE += ['-Wno-error=unused-function']
if CXX:
    BASE = [a for a in BASE if a != '-fno-exceptions'] + ['-fwasm-exceptions']
# Where cross-built libraries install (--prefix=/usr DESTDIR=SYSROOT); the
# libc's include/ is clang's own sysroot default. Debian's packages put
# per-architecture headers (ffi.h, opensslconf.h) in usr/include/TRIPLET,
# which its GCC searches before usr/include.
INCS = []
for d in ('include/gexcc', 'usr/local/include', 'usr/include/wasm32-linux-musl', 'usr/include'):
    if os.path.isdir(os.path.join(SYSROOT, d)):
        INCS += ['-isystem', os.path.join(SYSROOT, d)]
# libc++'s headers come before the C library's (its <math.h> wraps musl's),
# unless asked not to (libc++'s own build, with its headers)
if CXX and os.path.isdir(os.path.join(SYSROOT, 'include', 'c++', 'v1')) and \
        not any(a in ('-nostdinc++', '-nostdinc') for a in sys.argv[1:]):
    INCS = ['-isystem', os.path.join(SYSROOT, 'include', 'c++', 'v1')] + INCS
# The sysroot's headers by their installed names (/usr/include/...): their
# assert()s and __FILE__s name no path of the build machine's
BASE += ['-ffile-prefix-map=' + os.path.realpath(SYSROOT) + '=']
if os.path.realpath(SYSROOT) != SYSROOT:
    BASE += ['-ffile-prefix-map=' + SYSROOT + '=']
CLANG = ['clang++' if CXX else 'clang'] + BASE + ['--sysroot=' + SYSROOT]
# Loop safepoints (poll/GexPoll.cpp): signals reach code that never makes a
# system call. GEXCC_POLL=0 or -fno-gex-poll leaves them out.
POLL = os.environ.get('GEXCC_POLL', '1') != '0' and '-fno-gex-poll' not in sys.argv[1:]
if POLL and os.path.exists(os.path.join(LIBDIR, 'GexPoll.so')):
    CLANG += ['-fpass-plugin=' + os.path.join(LIBDIR, 'GexPoll.so')]
CLANG_TAIL = INCS
# and after the caller's flags too: Debian's build flags turn the warning
# back into an error (-Werror=implicit-function-declaration, which GCC on
# glibc passes for the same reason as above)
if not CXX:
    CLANG_TAIL = CLANG_TAIL + ['-Wno-error=implicit-function-declaration']
# GCC's warning options clang does not have (-Werror=clobbered, mbedtls):
# GCC knows them, so a build that passes them with -Werror expects no error
CLANG_TAIL = CLANG_TAIL + ['-Wno-unknown-warning-option']
# The C library's predefined macros (__STDC_ISO_10646__: wchar_t is UCS-4),
# which GCC on Linux includes before every translation unit and clang's
# wasm driver does not; libedit refuses to build without them
PREDEF = os.path.join(SYSROOT, 'include', 'stdc-predef.h')
if os.path.exists(PREDEF) and not any(a in ('-nostdinc', '-ffreestanding', '-undef') for a in sys.argv[1:]):
    CLANG_TAIL = CLANG_TAIL + ['-include', PREDEF]


def expand_rsp(args):
    out = []
    for a in args:
        if a.startswith('@') and os.path.isfile(a[1:]):
            import shlex
            out += expand_rsp(shlex.split(open(a[1:]).read()))
        else:
            out.append(a)
    return out


# Function pointer cast emulation (finish below): -fgex-fpcast-emu or
# GEXCC_FPCAST=1, at link time.
FPCAST = os.environ.get('GEXCC_FPCAST', '0') != '0' or '-fgex-fpcast-emu' in sys.argv[1:]
# A link that never forks (rt/nofork.c): -fgex-no-fork or GEXCC_NOFORK=1, at
# link time. Off by default: fork there returns ENOSYS, and no asyncify.
NOFORK = os.environ.get('GEXCC_NOFORK', '0') != '0' or '-fgex-no-fork' in sys.argv[1:]
# A program that runs i386 code of its own (rt/i386.c): -fgex-guest-i386, at
# link time. gex translates i386 in its process (the container's header
# flags, wasm-elf.py --guest-i386 --linux-space).
GUEST_I386 = '-fgex-guest-i386' in sys.argv[1:]
args = [a for a in expand_rsp(sys.argv[1:])
        if a not in ('-fno-gex-poll', '-fgex-poll', '-fgex-fpcast-emu', '-fgex-no-fork', '-fgex-guest-i386')]


def in_sysroot(path):
    """A target library's directory as a build machine with it installed
    would name it (tclConfig.sh's -I/usr/include/tcl8.6): the sysroot's when
    the build machine has none there."""
    if path.startswith('/usr/') and not os.path.exists(path) and os.path.exists(SYSROOT + path):
        return SYSROOT + path
    return path


# The target's own system header directories, as a build machine names them
# (cyrus-sasl's configure adds -I/usr/include for each database it finds).
# They are searched already, from the sysroot; GCC ignores a -I naming a
# system directory, and here it would name the build machine's headers.
SYSTEM_INCS = {'/usr/include', '/usr/local/include', '/usr/include/wasm32-linux-musl'}


def system_inc(opt, path):
    return opt != '-L' and os.path.normpath(path) in SYSTEM_INCS


def sysroot_dirs(argv):
    out, k = [], 0
    while k < len(argv):
        a = argv[k]
        if a in ('-I', '-isystem', '-idirafter', '-L') and k + 1 < len(argv):
            if not system_inc(a, argv[k + 1]):
                out += [a, in_sysroot(argv[k + 1])]
            k += 2; continue
        drop = False
        for opt in ('-isystem', '-idirafter', '-I', '-L'):
            if a.startswith(opt + '/'):
                drop = system_inc(opt, a[len(opt):])
                a = opt + in_sysroot(a[len(opt):]); break
        if not drop:
            out.append(a)
        k += 1
    return out


args = sysroot_dirs(args)


def gcc_only(argv):
    """What Debian's build flags hand GCC that means nothing here: its spec
    files (-specs=, dpkg's PIE ones), link-time optimisation (-flto=auto
    -ffat-lto-objects: wasm-ld would get LLVM bitcode where gexcc's link
    reads wasm objects; the code is optimised per object as before), and
    GCC's `-z KEYWORD`, which is the linker's -z (-Wl,-z,KEYWORD)."""
    out, k = [], 0
    while k < len(argv):
        a = argv[k]
        if a.startswith('-specs=') or a.startswith('-flto') or a in ('-fno-lto', '-ffat-lto-objects',
                                                                      '-fno-fat-lto-objects'):
            k += 1; continue
        if a == '-specs' and k + 1 < len(argv):
            k += 2; continue
        if a == '-z' and k + 1 < len(argv):
            out.append('-Wl,-z,' + argv[k + 1]); k += 2; continue
        out.append(a); k += 1
    return out


args = gcc_only(args)
# -pthread defines _REENTRANT, as GCC's and clang's Linux drivers do (not
# clang's wasm one); configure scripts check it (unbound's AX_PTHREAD)
if '-pthread' in args or '-pthreads' in args:
    BASE += ['-D_REENTRANT']
    CLANG += ['-D_REENTRANT']

# --- queries -------------------------------------------------------------
if args == ['--print-sysroot'] or args == ['-print-sysroot']:
    print(SYSROOT); sys.exit(0)
if args == ['--print-prefix']:
    print(PREFIX); sys.exit(0)
if args == ['-dumpmachine']:
    print(TRIPLE); sys.exit(0)
# Debian's name for the target: its packages put libraries in
# usr/lib/wasm32-linux-musl, which the searches below include.
MULTIARCH = 'wasm32-linux-musl'
if args in (['-print-multiarch'], ['--print-multiarch']):
    print(MULTIARCH); sys.exit(0)
LIBDIRS = [SYSROOT + '/lib', SYSROOT + '/usr/lib', SYSROOT + '/usr/lib/' + MULTIARCH, SYSROOT + '/lib/' + MULTIARCH]
for a in args:
    if a.startswith('-print-file-name='):
        f = a.split('=', 1)[1]
        for d in LIBDIRS:
            if os.path.exists(os.path.join(d, f)):
                print(os.path.join(d, f)); sys.exit(0)
        print(f); sys.exit(0)
    if a == '-print-libgcc-file-name':
        print(SYSROOT + '/lib/libclang_rt.builtins-wasm32.a'); sys.exit(0)
    # GCC's spelling too, and with input files (meson asks with a test
    # file and -c): the answer, and nothing compiled, as GCC does. Meson takes
    # a library directory named here as a system one, so a library found in
    # the multiarch directory gets no run path.
    if a in ('-print-search-dirs', '--print-search-dirs'):
        print('programs: =%s\nlibraries: =%s' % (os.path.dirname(me), ':'.join(LIBDIRS))); sys.exit(0)
    if a.startswith('-print-prog-name='):
        t = a.split('=', 1)[1]
        p = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), TRIPLE + '-' + t)
        print(p if os.path.exists(p) else t); sys.exit(0)
    if a == '-print-multi-os-directory':
        print('.'); sys.exit(0)

# --- as cpp -------------------------------------------------------------------
# Installed as TRIPLE-cpp: the C preprocessor, which reads standard input
# when given no file (xorg-macros' RAWCPP check: `cpp -undef < conftest`)
# and takes `cpp IN OUT`.
if name == 'cpp' or name.endswith('-cpp'):
    CPP_ARG = {'-I', '-D', '-U', '-include', '-imacros', '-isystem', '-idirafter', '-iquote', '-MF', '-MT', '-MQ', '-o',
               '-x', '-std', '-iprefix', '-iwithprefix', '-isysroot', '-target', '--target'}
    files, k = [], 0
    while k < len(args):
        if args[k] in CPP_ARG:
            k += 2; continue
        if args[k] == '-' or not args[k].startswith('-'):
            files.append(k)
        k += 1
    cmd = list(args)
    if len(files) == 2:  # cpp IN OUT
        out_at = files[1]
        cmd = cmd[:out_at] + cmd[out_at + 1:] + ['-o', args[out_at]]
    elif not files:
        cmd.append('-')
    if not any(a == '-x' or a.startswith('-x') for a in cmd):
        cmd = ['-x', 'c'] + cmd
    os.execvp(CLANG[0], CLANG + ['-E'] + cmd + CLANG_TAIL)

# --- compile only -----------------------------------------------------------
SRC_EXT = ('.c', '.cc', '.cpp', '.cxx', '.c++', '.C', '.s', '.S', '.i', '.ii')
# `.symver` (ELF symbol versions, in top-level asm) is a directive the wasm
# assembler does not know, and a wasm module has no symbol versions. A
# compile that fails on one is run again on the preprocessed source with
# those asm statements taken out. The default version's name (`.symver
# impl, name@@V`) is what programs link to, and it is often not the C name
# (fuse's fuse_parse_cmdline is fuse_parse_cmdline_312): in C it becomes an
# alias of the C name. Older versions' names (name@V) are left out.
SYMVER_ASM = re.compile(r'\b(?:__asm__|__asm|asm)\s*(?:volatile\s*|__volatile__\s*)?\(\s*'
                        r'"(?:\\t|\s)*\.symver\b(?:[^"\\]|\\.)*"(?:\s*"(?:[^"\\]|\\.)*")*\s*\)\s*;?')
# GNU ld's link-time warnings (alsa-lib's and glibc's link_warning): an
# asm statement that declares the .gnu.warning.SYMBOL section and data put
# in it, in ELF assembler syntax. Taken out on the same retry: there is no
# such warning on wasm, and nothing else is lost.
ASM_STMT = re.compile(r'\b(?:__asm__|__asm|asm)\s*(?:volatile\s*|__volatile__\s*)?\(\s*'
                      r'"(?:[^"\\]|\\.)*"(?:\s*"(?:[^"\\]|\\.)*")*\s*\)\s*;?')


def gnu_warning(m):
    """An asm statement whose text (its strings joined) names a
    .gnu.warning section."""
    return '.gnu.warning' in ''.join(re.findall(r'"((?:[^"\\]|\\.)*)"', m.group(0)))


GNUWARN_ATTR = re.compile(r'__attribute__\s*\(\(\s*section\s*\(\s*"\.gnu\.warning(?:[^"\\]|\\.)*"(?:\s*"(?:[^"\\]|\\.)*")*\s*\)\s*\)\)')
DEP_FLAGS = {'-MD', '-MMD', '-MP'}
DEP_ARG = {'-MF', '-MT', '-MQ'}


def compile_cmd(cmd):
    """Run a compile, with the `.symver` retry above. Returns its exit code."""
    r = subprocess.run(cmd, stderr=subprocess.PIPE)
    err = r.stderr
    srcs = [a for a in cmd[1:] if a.endswith(SRC_EXT) and not a.endswith(('.s', '.S')) and os.path.isfile(a)]
    symver = b'.symver' in err and b'unknown directive' in err
    if r.returncode == 0 or not (symver or b'.gnu.warning' in err) or len(srcs) != 1:
        sys.stderr.buffer.write(err)
        return r.returncode
    src = srcs[0]
    tmpd = tempfile.mkdtemp(prefix='gexcc-symver-')
    try:
        ii = os.path.join(tmpd, os.path.basename(src) + ('.ii' if CXX or src.endswith(('.cc', '.cpp', '.cxx', '.c++', '.C', '.ii')) else '.i'))
        pre, skip = [], False
        for a in cmd:
            if skip:
                skip = False; continue
            if a == '-o':
                skip = True; continue
            if a in ('-c', '-S') or (a.startswith('-o') and len(a) > 2):
                continue
            pre.append(a)
        obj = next((cmd[k + 1] for k, a in enumerate(cmd[:-1]) if a == '-o'), None)
        if obj and any(a in DEP_FLAGS for a in cmd) and not any(a.startswith(('-MT', '-MQ')) for a in cmd):
            pre += ['-MT', obj]
        if run(pre + ['-E', '-o', ii]):
            sys.stderr.buffer.write(err)
            return r.returncode
        text = open(ii, encoding='utf-8', errors='surrogateescape').read()
        found = SYMVER_ASM.findall(text)
        aliases = {}
        if not ii.endswith('.ii'):
            for f in found:
                d = ''.join(re.findall(r'"((?:[^"\\]|\\.)*)"', f)).replace('\\t', ' ')
                m = re.search(r'\.symver\s+([A-Za-z_][\w.$]*)\s*,\s*([A-Za-z_][\w.$]*)@@', d)
                if m and m.group(1) != m.group(2):
                    aliases[m.group(2)] = m.group(1)
        body = SYMVER_ASM.sub('', text)
        warns = sum(1 for m in ASM_STMT.finditer(body) if gnu_warning(m))
        body = GNUWARN_ATTR.sub('', ASM_STMT.sub(lambda m: '' if gnu_warning(m) else m.group(0), body))
        if warns:
            sys.stderr.write('gexcc: %s: %d GNU ld link warning(s) left out\n' % (src, warns))
        alias = ''.join('extern __typeof__(%s) %s __attribute__((alias("%s")));\n' % (c, a, c)
                        for a, c in sorted(aliases.items()))
        open(ii, 'w', encoding='utf-8', errors='surrogateescape').write(body + alias)
        if found:
            sys.stderr.write('gexcc: %s: %d .symver directive(s) left out (wasm has no symbol versions)%s\n'
                             % (src, len(found), ', %d default name(s) made aliases' % len(aliases) if aliases else ''))
        out, skip = [], False
        for a in cmd:
            if skip:
                skip = False; continue
            if a in DEP_FLAGS:
                continue
            if a in DEP_ARG:
                skip = True; continue
            if a.startswith(('-MF', '-MT', '-MQ')) and len(a) > 3:
                continue
            out.append(ii if a == src else a)
        out += ['-Wno-unused-command-line-argument']
        if aliases:
            r2 = subprocess.run(out, stderr=subprocess.PIPE)
            if r2.returncode == 0:
                sys.stderr.buffer.write(r2.stderr)
                return 0
            # an alias whose name the source declared with another type:
            # the names are left out, as the versions are
            open(ii, 'w', encoding='utf-8', errors='surrogateescape').write(body)
            sys.stderr.write('gexcc: %s: the default names could not be aliases, left out\n' % src)
        return run(out)
    finally:
        shutil.rmtree(tmpd, True)


NOLINK = ('-c', '-S', '-E', '-M', '-MM', '-fsyntax-only', '--version', '-v', '-###', '--help')
# -v on a link (CMake's compiler ABI check: `-v -Wl,-v` with the object) is
# verbose, not a query: the version on stderr, as gcc prints it, then the
# link itself
if '-v' in args and not any(a in NOLINK and a != '-v' for a in args) and \
        any(not a.startswith('-') for a in args):
    subprocess.run([CLANG[0], '--target=' + TRIPLE, '--version'], stdout=sys.stderr)
    args = [a for a in args if a != '-v']
    # and the link line, as GCC prints collect2's: libtool reads the
    # libraries after the object from it (postdeps) and names them itself
    # when it links a C++ library with -nostdlib
    if not any(a in ('-nostdlib', '-nodefaultlibs') for a in args):
        rt = ['-lc++', '-lc++abi', '-lunwind'] if CXX else []
        named = [a for k, a in enumerate(args) if (not a.startswith('-') or a.startswith('-l'))
                 and (k == 0 or args[k - 1] != '-o')]
        sys.stderr.write(' wasm-ld -L%s/lib %s\n' % (SYSROOT, ' '.join(named + rt + ['-lc'])))
if not args or any(a in NOLINK or a.startswith(('-print-', '-dump')) for a in args):
    if args == ['-v'] or args == ['--version'] or not args:
        os.execvp(CLANG[0], [CLANG[0], '--target=' + TRIPLE] + args)
    if '-c' in args and '-E' not in args:
        sys.exit(compile_cmd(CLANG + args + CLANG_TAIL))
    os.execvp(CLANG[0], CLANG + args + CLANG_TAIL)

# --- link -----------------------------------------------------------------
LIBC_NAMES = {'c', 'm', 'pthread', 'dl', 'rt', 'util', 'crypt', 'xnet', 'resolv', 'gcc', 'gcc_s', 'gcc_eh',
              'atomic', 'ssp', 'ssp_nonshared', 'unwind'}
# Linker options with no meaning for a static wasm module, dropped quietly.
LD_DROP = re.compile(r'^(-rpath|-rpath-link|--rpath|-soname|-h|--?version-script|-z|--as-needed|--no-as-needed|'
                     r'--no-undefined|-O\d|--hash-style|--build-id|--enable-new-dtags|--disable-new-dtags|'
                     r'-Bsymbolic|-Bsymbolic-functions|--start-group|--end-group|--sort-common|--warn-common|'
                     r'--sort-section|--dynamic-list|-E|--export-dynamic|--no-copy-dt-needed-entries|'
                     r'--copy-dt-needed-entries|-Bdynamic|-Bstatic|--eh-frame-hdr|--icf|-x|-X|--discard-all|'
                     r'--discard-locals|--fatal-warnings|--warn-shared-textrel|--no-relax|--relax|--push-state|'
                     r'--pop-state|-dynamic-linker|--dynamic-linker|--allow-shlib-undefined|--exclude-libs|'
                     r'--undefined-version|--no-undefined-version|-nostdlib|-stats|--stats|--no-keep-memory|'
                     r'--reduce-memory-overheads)(=.*)?$')
LD_ARG = {'-rpath', '-rpath-link', '-soname', '-h', '--version-script', '-version-script', '-z', '--sort-section', '--dynamic-list',
          '-dynamic-linker', '--dynamic-linker', '--exclude-libs'}

out = None
srcs, inputs, ld_flags, cflags, libdirs = [], [], [], [], []
shared = relocatable = nostdlib = nostartfiles = nodefaultlibs = False
xlang = None
ld_skip = 0  # a dropped linker option's argument still to come (-Wl,-rpath -Wl,DIR)
ld_want = None  # and which of ld_opts it goes to
LD_KEEP = {'-soname': 'soname', '-h': 'soname', '-rpath': 'rpath'}
ld_opts = {'soname': [], 'rpath': []}
WHOLE = ('--whole-archive', '--no-whole-archive')
export_dynamic = static = False
i = 0
while i < len(args):
    a = args[i]
    nxt = args[i + 1] if i + 1 < len(args) else None
    if a == '-o':
        out = nxt; i += 2; continue
    if a.startswith('-o') and len(a) > 2:
        out = a[2:]
    elif a == '-shared':
        shared = True
    elif a == '-r':
        relocatable = True
    elif a == '-nostdlib':
        nostdlib = True
    elif a == '-nostartfiles':
        nostartfiles = True
    elif a in ('-nodefaultlibs', '-nolibc'):
        nodefaultlibs = True
    elif a.startswith('-Wl,') or a == '-Xlinker':
        if a == '-Xlinker':
            parts = [nxt]; i += 1
        else:
            parts = [x for x in a[4:].split(',') if x]
        k = 0
        while k < len(parts):
            p = parts[k]
            if ld_skip:
                ld_skip -= 1
                if ld_want:
                    ld_opts[ld_want].append(p); ld_want = None
                k += 1; continue
            if p in LD_ARG:
                # -soname, -rpath: kept for a shared link (GEXCC_SHARED)
                want = LD_KEEP.get(p)
                if k + 1 == len(parts):
                    ld_skip = 1; ld_want = want
                elif want:
                    ld_opts[want].append(parts[k + 1])
                k += 2; continue
            m = re.match(r'^(--?soname|--?rpath)=(.*)$', p)
            if m:
                ld_opts['soname' if 'soname' in m.group(1) else 'rpath'].append(m.group(2))
                k += 1; continue
            if p in ('-E', '--export-dynamic'):
                export_dynamic = True
            # --whole-archive applies to the inputs between it and
            # --no-whole-archive (libtool's convenience libraries in a
            # shared library: gnutls's ocsp_output.o, which nothing in the
            # library itself calls): kept in place among the inputs
            if p in WHOLE:
                inputs.append(p); k += 1; continue
            # a library named to the linker (-Wl,-lc) is an input like any
            # other, found the same way
            if p.startswith('-l') and len(p) > 2:
                inputs.append(p); k += 1; continue
            if LD_DROP.match(p):
                k += 1; continue
            ld_flags.append(p); k += 1
        # -Xlinker -rpath -Xlinker DIR: the second half arrives separately
    elif a == '-L':
        libdirs.append(nxt); i += 2; continue
    elif a.startswith('-L'):
        libdirs.append(a[2:])
    elif a == '-l':
        inputs.append('-l' + nxt); i += 2; continue
    elif a.startswith('-l'):
        inputs.append(a)
    elif a == '-x':
        xlang = nxt; cflags += [a, nxt]; i += 2; continue
    elif a in ('-I', '-D', '-U', '-include', '-imacros', '-isystem', '-idirafter', '-iquote', '-MF', '-MT', '-MQ',
               '-target', '--target', '-arch', '-Xclang', '-mllvm', '-iprefix', '-iwithprefix', '-isysroot'):
        cflags += [a, nxt]; i += 2; continue
    elif a == '-' and xlang and xlang != 'none':
        srcs.append((a, xlang))  # the source on stdin (-x LANG -)
    elif not a.startswith('-') and (a.endswith(SRC_EXT) or (xlang and xlang != 'none')):
        srcs.append((a, xlang))
    elif not a.startswith('-') and ld_skip:
        # -Wl,--version-script FILE: GCC hands the linker its arguments in
        # order, so the file is the option's argument, not an input
        ld_skip -= 1
        if ld_want:
            ld_opts[ld_want].append(a); ld_want = None
    elif not a.startswith('-'):
        inputs.append(a)
    elif a in ('-rdynamic', '-export-dynamic'):
        export_dynamic = True
    elif a == '-static':
        static = True
    elif a in ('-pthread', '-pthreads', '-rdynamic', '-static', '-static-pie', '-pie', '-no-pie', '-fPIE', '-fpie',
               '-fPIC', '-fpic', '-s', '-static-libgcc', '-shared-libgcc', '-static-libstdc++', '-symbolic',
               '-fuse-ld=lld', '-Bstatic', '-Bdynamic', '-export-dynamic', '-Wl'):
        pass
    else:
        cflags.append(a)
    i += 1

tmp = tempfile.mkdtemp(prefix='gexcc-')
atexit.register(shutil.rmtree, tmp, True)
objs = []
for k, (s, lang) in enumerate(srcs):
    o = os.path.join(tmp, '%d-%s.o' % (k, os.path.basename(s)))
    pre = ['-x', lang] if lang else []
    # clang's -x applies to what follows: name the language per source
    cf = []
    j = 0
    while j < len(cflags):
        if cflags[j] == '-x':
            j += 2; continue
        cf.append(cflags[j]); j += 1
    r = compile_cmd(CLANG + cf + pre + ['-c', s, '-o', o] + CLANG_TAIL)
    if r:
        sys.exit(r)
    objs.append(o)
if not objs and all(x.startswith('-l') for x in inputs):
    # build systems ask the linker who it is (Meson: -Wl,--version, with
    # the project's link arguments, libraries too)
    if any(f in ('--version', '-v', '--help') for f in ld_flags):
        os.execvp('wasm-ld', ['wasm-ld'] + [f for f in ld_flags if f in ('--version', '-v', '--help')][:1])
    if not inputs:
        die('no input files')
if out is None:
    out = 'a.out'
if out == '/dev/null':
    # a configure-style check that only wants to know the link works: the
    # module is made (and post-processed) in the scratch directory instead
    out = os.path.join(tmp, 'discarded.out')

LIB = SYSROOT + '/lib'
SEARCH = libdirs + [SYSROOT + '/usr/local/lib', SYSROOT + '/usr/lib/' + MULTIARCH, SYSROOT + '/usr/lib',
                    SYSROOT + '/lib/' + MULTIARCH, LIB]
AR_MAGIC = b'!<arch>\n'
# a thin archive (Meson's static libraries: ar csrDT) names its members' files
THIN_MAGIC = b'!<thin>\n'


def is_archive(p):
    try:
        with open(p, 'rb') as f:
            return f.read(8) in (AR_MAGIC, THIN_MAGIC)
    except OSError:
        return False


def resolve(lib):
    """-lNAME to a file: an archive libNAME.so (gexcc's -shared) or libNAME.a;
    with GEXCC_SHARED, a real libNAME.so (a wasm side module) first."""
    shared_ok = os.environ.get('GEXCC_SHARED', '0') != '0' and not static
    for d in SEARCH:
        for ext in ('.so', '.a'):
            p = os.path.join(d, 'lib' + lib + ext)
            if os.path.exists(p) and (ext == '.a' or is_archive(p) or (shared_ok and open(p, 'rb').read(4) == b'\0asm')
                                      or ld_script_inputs(p) is not None):
                return os.path.abspath(p)
    return None


def archive_deps(path):
    """The link inputs recorded in an archive's gexcc.deps.o member."""
    try:
        data = open(path, 'rb').read()
    except OSError:
        return []
    if not data.startswith(AR_MAGIC):
        return []
    a = data.find(b'GEXCC-DEPS\n')
    if a < 0:
        return []
    b = data.find(b'\nGEXCC-END', a)
    return data[a + 11:b].decode().split('\n') if b > 0 else []


def ld_script_inputs(path):
    """A GNU ld script in a library's place (libbsd.so: GROUP(libbsd.so.0
    AS_NEEDED(-lmd))): the inputs it names, a target path the build machine
    lacks being the sysroot's. None for anything else."""
    try:
        with open(path, 'rb') as f:
            head = f.read(4096)
    except OSError:
        return None
    if b'\0' in head or not re.search(rb'\b(GROUP|INPUT)\s*\(', head):
        return None
    text = re.sub(r'/\*.*?\*/', ' ', open(path, errors='replace').read(), flags=re.S)
    out = []
    for m in re.finditer(r'\b(?:GROUP|INPUT)\s*\(((?:[^()]|\([^()]*\))*)\)', text):
        for tok in re.split(r'[\s,()]+', m.group(1)):
            if not tok or tok == 'AS_NEEDED':
                continue
            if tok.startswith('-l'):
                out.append(tok)
            elif tok.startswith('/'):
                out.append(in_sysroot(tok))
            else:
                out.append(os.path.join(os.path.dirname(path), tok))
    return out


def expand(items, seen):
    """Link inputs with -l resolved and archives' recorded deps appended."""
    res, missing = [], []
    for it in items:
        if not it:
            continue
        if it in WHOLE:
            res.append(it)
            continue
        if it.startswith('-l'):
            nm = it[2:]
            # libc and its parts are linked last, after libgex, whose
            # definitions (dlopen, dlsym) must win over musl's weak stubs:
            # an -lc mid-line would pull those first
            if nm in LIBC_NAMES:
                continue
            p = resolve(nm)
            if p is None:
                if CXX and nm in ('stdc++', 'c++', 'c++abi', 'supc++'):
                    continue
                missing.append(it)
                continue
            it = p
        elif not it.startswith('-'):
            it = os.path.abspath(it)
        if it in seen:
            continue
        seen.add(it)
        script = ld_script_inputs(it) if not it.startswith('-') else None
        if script is not None:
            sub, miss = expand(script, seen)
            res += sub
            missing += miss
            continue
        res.append(it)
        if is_archive(it):
            sub, miss = expand(archive_deps(it), seen)
            res += sub
            missing += miss
    return res, missing


def imports(path):
    """(module, name) of every function import of the module at path."""
    w = open(path, 'rb').read()

    def leb(p):
        r = sh = 0
        while True:
            x = w[p]; p += 1
            r |= (x & 0x7f) << sh; sh += 7
            if not x & 0x80:
                return r, p

    def nm(p):
        n, p = leb(p)
        return w[p:p + n].decode(), p + n
    res, p = [], 8
    while p < len(w):
        sid = w[p]; n, q = leb(p + 1); end = q + n
        if sid == 2:
            cnt, q = leb(q)
            for _ in range(cnt):
                m, q = nm(q); f, q = nm(q)
                kind = w[q]; q += 1
                if kind == 0:
                    _, q = leb(q); res.append((m, f))
                elif kind == 1:
                    q += 1; fl, q = leb(q); _, q = leb(q)
                    if fl & 1: _, q = leb(q)
                elif kind == 2:
                    fl, q = leb(q); _, q = leb(q)
                    if fl & 1: _, q = leb(q)
                elif kind == 3:
                    q += 2
                elif kind == 4:
                    q += 1; _, q = leb(q)
            break
        p = end
    return res


# Functions above fork's driver (musl's src/process/wasm32/__gex_fork.c): the
# unwind for a fork stops at the driver, so they keep no state.
FORK_ROOTS = ['__gex_start', '__gex_start_c', '__gex_thread_start', '__gex_thread_c',
              '__gex_fork_run', '__gex_fork_drive', '__gex_fork_child']
opt = os.path.join(SYSROOT, 'binaryen', 'bin', 'wasm-opt')
feats = ['--enable-threads', '--enable-bulk-memory', '--enable-mutable-globals', '--enable-sign-ext',
         '--enable-nontrapping-float-to-int', '--enable-exception-handling', '--enable-reference-types']

# --- shared libraries (GEXCC_SHARED=1) -------------------
# -shared writes a wasm side module (dylink.0) and a program links against
# them; gex's loader binds them at run time. -static, or GEXCC_SHARED unset,
# keeps the static way above.
SHARED = os.environ.get('GEXCC_SHARED', '0') != '0' and not static
DYLINK = os.path.join(LIBDIR, 'gex-dylink')
if SHARED and FPCAST:
    die('GEXCC_SHARED and function pointer cast emulation do not mix yet (link -static)')
if SHARED and not os.path.exists(DYLINK):
    die('GEXCC_SHARED: no %s (install.sh copies it from target/release)' % DYLINK)


def is_wasm_so(p):
    """A side module: wasm whose first section is dylink.0. An object file
    (its first section is code's, its custom ones `linking` and relocs)
    is not one, though it is wasm too."""
    try:
        with open(p, 'rb') as f:
            head = f.read(8 + 1 + 5 + 1 + 8)
    except OSError:
        return False
    if head[:4] != b'\0asm' or len(head) < 10 or head[8] != 0:
        return False
    q = 9
    while q < len(head) and head[q] & 0x80:  # the section's size
        q += 1
    q += 1
    return head[q:q + 9] == b'\x08dylink.0'


def sections(path):
    """(name, payload) of every custom section, and the export names."""
    w = open(path, 'rb').read()

    def leb(p):
        r = sh = 0
        while True:
            x = w[p]; p += 1
            r |= (x & 0x7f) << sh; sh += 7
            if not x & 0x80:
                return r, p
    customs, exports, p = [], [], 8
    while p < len(w):
        sid = w[p]; n, q = leb(p + 1); end = q + n
        if sid == 0:
            ln, q2 = leb(q)
            customs.append((w[q2:q2 + ln].decode('utf-8', 'replace'), w[q2 + ln:end]))
        elif sid == 7:
            cnt, q = leb(q)
            for _ in range(cnt):
                ln, q = leb(q); exports.append(w[q:q + ln].decode()); q += ln
                q += 1; _, q = leb(q)
        p = end
    return customs, exports


def _uleb(n):
    out = bytearray()
    while True:
        b = n & 0x7f; n >>= 7
        out.append(b | (0x80 if n else 0))
        if not n:
            return bytes(out)


def rename_data(src, dst, old, new):
    """A copy of the wasm object SRC as DST with its data symbol OLD
    (defined or not) named NEW, in the `linking` section's symbol table;
    relocations name symbols by index, so nothing else changes. False when
    SRC has no such symbol (and DST is not written)."""
    w = open(src, 'rb').read()
    if w[:4] != b'\0asm':
        return False

    def leb(b, p):
        r = sh = 0
        while True:
            x = b[p]; p += 1
            r |= (x & 0x7f) << sh; sh += 7
            if not x & 0x80:
                return r, p

    def name(b, p):
        n, p = leb(b, p)
        return b[p:p + n], p + n
    out, p, hit = bytearray(w[:8]), 8, False
    while p < len(w):
        sid = w[p]; n, q = leb(w, p + 1); end = q + n
        body = w[q:end]
        nm, r = name(body, 0) if sid == 0 else (b'', 0)
        if sid == 0 and nm == b'linking':
            ver, r = leb(body, r)
            nb = bytearray(body[:r])
            while r < len(body):
                kind = body[r]; ln, r2 = leb(body, r + 1); sub = body[r2:r2 + ln]
                if kind == 8:  # WASM_SYMBOL_TABLE
                    cnt, t = leb(sub, 0)
                    ns = bytearray(_uleb(cnt))
                    for _ in range(cnt):
                        start = t
                        k = sub[t]; flags, t = leb(sub, t + 1)
                        head = sub[start:t]
                        if k == 1:  # data: name, then where if defined
                            sym, t = name(sub, t)
                            rest_start = t
                            if not flags & 0x10:
                                for _ in range(3):
                                    _, t = leb(sub, t)
                            if sym == old.encode():
                                sym, hit = new.encode(), True
                            ns += head + _uleb(len(sym)) + sym + sub[rest_start:t]
                        elif k == 3:  # section
                            _, t = leb(sub, t)
                            ns += sub[start:t]
                        else:  # function, global, tag, table: index, name if any
                            _, t = leb(sub, t)
                            if not flags & 0x10 or flags & 0x40:
                                _, t = name(sub, t)
                            ns += sub[start:t]
                    sub = bytes(ns)
                nb += bytes([kind]) + _uleb(len(sub)) + sub
                r = r2 + ln
            body = bytes(nb)
        out += bytes([sid]) + _uleb(len(body)) + body
        p = end
    if hit:
        open(dst, 'wb').write(out)
    return hit


def soname_of(path):
    """A side module's soname: its name section's module name."""
    for name, data in sections(path)[0]:
        if name == 'name' and data[:1] == b'\0':
            p = 1
            n = 0; sh = 0
            while True:
                x = data[p]; p += 1; n |= (x & 0x7f) << sh; sh += 7
                if not x & 0x80:
                    break
            q = p; ln = 0; sh = 0
            while True:
                x = data[q]; q += 1; ln |= (x & 0x7f) << sh; sh += 7
                if not x & 0x80:
                    break
            return data[q:q + ln].decode()
    return os.path.basename(path)


def whole_closed(ins):
    """The inputs with a --whole-archive left open closed after them, so
    the libraries gexcc adds are not taken whole."""
    marks = [x for x in ins if x in WHOLE]
    return ins + ['--no-whole-archive'] if marks and marks[-1] == '--whole-archive' else ins


def by_soname(paths):
    """Each side module as a link of its soname: wasm-ld records a NEEDED
    library by the name it was given, and the loader looks for sonames."""
    d = os.path.join(tmp, 'sonames')
    os.makedirs(d, exist_ok=True)
    out = []
    for p in paths:
        if is_wasm_so(p) and not is_archive(p):
            q = os.path.join(d, soname_of(p))
            if not os.path.exists(q):
                os.symlink(os.path.abspath(p), q)
            out.append(q)
        else:
            out.append(p)
    return out


def finish_shared(wasm, sos, lib):
    """A shared-mode module after wasm-ld: a library's imports of its own
    functions bound (gex-dylink selfbind), asyncify for fork where it can
    be reached (from the libraries' gex.forkreach summaries, every indirect
    call counting), exception handling to exnref, and a library's own
    summary added."""
    if lib and run([DYLINK, 'selfbind', wasm, wasm]):
        die('gex-dylink selfbind failed')
    # GEXCC_LENIENT_CALLS=1: indirect calls with more trailing arguments than
    # the function takes cut as C makes them (gex-dylink lenient-calls), for
    # GLib, whose callers pass GObject's initialisers and a GFunc more than
    # they take; before asyncify, which then instruments the helpers
    lenient = os.environ.get('GEXCC_LENIENT_CALLS', '0') != '0'
    if lenient and run([DYLINK, 'lenient-calls', wasm, wasm]):
        die('gex-dylink lenient-calls failed')
    if NOFORK:
        no_fork_point(wasm)
        if run([opt, wasm, '-g', '--strip-dwarf'] + feats + (['--enable-gc'] if lenient else [])
               + ['--translate-to-exnref', '-o', wasm]):
            die('wasm-opt failed')
        if lib:
            # its summary: nothing in it reaches fork
            txt = os.path.join(tmp, 'forkreach.txt')
            open(txt, 'w').write('gex.forkreach 1\n')
            if run([DYLINK, 'section', wasm, 'gex.forkreach', txt, wasm]):
                die('gex-dylink section failed')
        return
    may, known = set(), set()
    for so in sos:
        if not is_wasm_so(so):
            continue
        cs, ex = sections(so)
        known |= set(ex)
        for name, data in cs:
            if name == 'gex.forkreach':
                may |= set(data.decode().split('\n')[1:]) - {''}
    also = sorted('env.' + f for m, f in imports(wasm) if m == 'env' and (f in may or f not in known)
                  and f not in ('__indirect_function_table',))
    lst = os.path.join(tmp, 'onlylist')
    summ = os.path.join(tmp, 'forkreach')
    dis = os.path.join(os.path.dirname(opt), 'wasm-dis')
    reach = subprocess.run([sys.executable, os.path.join(LIBDIR, 'forkreach.py'), '--also', ','.join(also), '--indirect',
                            '--ignore', '__gex_call_handler,__pthread_exit', '--exports', summ, dis, wasm, 'gex.fork_point']
                           + FORK_ROOTS, capture_output=True, text=True)
    passes = []
    if reach.returncode == 0:
        only = [f for f in reach.stdout.strip().split(',') if f]
        if only:
            open(lst, 'w').write('\n'.join(only) + '\n')
            passes += ['--asyncify', '--pass-arg=asyncify-relocatable',
                       '--pass-arg=asyncify-imports@' + ','.join(['gex.fork_point'] + also),
                       '--pass-arg=asyncify-onlylist@@' + lst, '-O2']
    else:
        # not analysable: everything that may reach an import or an indirect call
        open(summ, 'w').write('\n'.join(sections(wasm)[1]) + '\n')
        defined = set(sections(wasm)[1])
        passes += ['--asyncify', '--pass-arg=asyncify-relocatable']
        roots = [f for f in FORK_ROOTS if f in defined]
        if roots:
            passes += ['--pass-arg=asyncify-removelist@' + ','.join(roots)]
        passes += ['-O2']
    passes += ['--translate-to-exnref']
    if run([opt, wasm, '-g', '--strip-dwarf'] + feats + (['--enable-gc'] if lenient else []) + passes + ['-o', wasm]):
        die('wasm-opt failed')
    if lib:
        txt = os.path.join(tmp, 'forkreach.txt')
        names = open(summ).read().split() if os.path.exists(summ) else []
        open(txt, 'w').write('gex.forkreach 1\n' + ''.join(n + '\n' for n in names))
        if run([DYLINK, 'section', wasm, 'gex.forkreach', txt, wasm]):
            die('gex-dylink section failed')


def no_fork_point(wasm):
    """A -fgex-no-fork link must not reach fork's driver: one that does
    would unwind through frames asyncify left alone."""
    if ('gex', 'fork_point') in imports(wasm):
        die('-fgex-no-fork: %s still reaches fork (it imports gex.fork_point)' % os.path.basename(out))


WASM_C = {'i32': 'int', 'i64': 'long long', 'f32': 'float', 'f64': 'double'}


def link_shared(cmd):
    """wasm-ld for a shared-mode link. A function an object declares with
    one type and a side module defines with another stops wasm-ld, where an
    ELF link (which knows no types) goes through: configure's link tests
    declare `char f(void)` for every function they look for. As wasm-ld
    does for a static library's function of the wrong type, the link gets
    a stub of the object's type that traps when called, with a warning.
    Unless the two differ only in the object's declaring no result
    (gettext's `extern void tputs (...)` for ncurses' int one): then the
    stub calls the library's and drops what it returns, as the ELF call
    does."""
    stubs = []
    for _ in range(64):
        # (first: a definition the side module's then meets defined)
        if VERBOSE:
            sys.stderr.write('+ %s\n' % ' '.join(cmd[:1] + stubs + cmd[1:]))
        p = subprocess.run(cmd[:1] + stubs + cmd[1:], capture_output=True, text=True)
        if p.returncode == 0:
            sys.stderr.write(p.stderr)
            return 0
        # wasm-ld crashes writing debug sections whose relocations name a
        # function the link imports from a side module (harfbuzz's tests:
        # DWARF locations in hb_set_reference, defined in libharfbuzz.so):
        # the link again without debug information
        if 'CustomSection::writeTo' in p.stderr and '--strip-debug' not in cmd:
            sys.stderr.write('gexcc: warning: wasm-ld failed on the debug sections; %s is linked without them\n'
                             % os.path.basename(out))
            cmd = cmd + ['--strip-debug']
            continue
        found = re.findall(r'function signature mismatch: (\S+)\n>>> defined as \(([^)]*)\) -> (\S+) in (\S+)\n'
                           r'>>> defined as \(([^)]*)\) -> (\S+) in (\S+)', p.stderr)
        found = [f for f in found if '/sonames/' in f[6] or is_wasm_so(f[6])]
        # A program's own variable named as a library's function (hunspell's
        # `int warn`, libc's warn()): on ELF the program's definition is the
        # one its own code reaches, and so it is here, under a name of its
        # own in the program's objects
        clash = [m[0] for m in re.findall(
            r'symbol type mismatch: (\S+)\n>>> defined as WASM_SYMBOL_TYPE_(DATA|FUNCTION) in (\S+)\n'
            r'>>> defined as WASM_SYMBOL_TYPE_(DATA|FUNCTION) in (\S+)', p.stderr)
            if {m[1], m[3]} == {'DATA', 'FUNCTION'}
            and is_wasm_so(m[2] if m[1] == 'FUNCTION' else m[4])]
        renamed = False
        for name in clash:
            for i, a in enumerate(cmd):
                if a.startswith('-') or not os.path.isfile(a) or is_wasm_so(a) or is_archive(a):
                    continue
                o = os.path.join(tmp, 'own-%d-%s' % (i, os.path.basename(a)))
                if rename_data(a, o, name, '__gexcc_own.' + name):
                    cmd = cmd[:i] + [o] + cmd[i + 1:]
                    renamed = True
        if renamed:
            continue
        if not found:
            sys.stderr.write(p.stderr)
            return p.returncode
        for name, params, ret, _, lparams, lret, so in found:
            c = os.path.join(tmp, 'mismatch-%d.c' % len(stubs))
            types = [WASM_C[t.strip()] for t in params.split(',') if t.strip()]
            args = ', '.join('%s a%d' % (t, i) for i, t in enumerate(types))
            drop = ret == 'void' and lret != 'void' and params == lparams
            if drop:
                # the library's, imported under its own name; the stub is
                # the object's alone (hidden), not another definition
                open(c, 'w').write('__attribute__((import_name("%s"))) %s __gexcc_lib_%s(%s);\n'
                                   '__attribute__((visibility("hidden"))) void %s(%s) { __gexcc_lib_%s(%s); }\n'
                                   % (name, WASM_C[lret], name, ', '.join(types) or 'void', name, args or 'void',
                                      name, ', '.join('a%d' % i for i in range(len(types)))))
            else:
                # (says so on stderr when reached: a call that gets here is
                # a miscompile the build let through)
                msg = 'gexcc: %s called through a declaration of the wrong type (stub)\n' % name
                open(c, 'w').write('long write(int, const void *, unsigned long);\n'
                                   '%s %s(%s) { write(2, "%s", %d); __builtin_trap(); }\n'
                                   % (WASM_C.get(ret, 'void'), name, args or 'void', msg.replace('\n', '\\n'), len(msg)))
            o = c[:-2] + '.o'
            if subprocess.call(['clang', '--target=' + TRIPLE, '-fPIC', '-c', '-w', '-fno-builtin', '-o', o, c]
                               + [a for a in BASE if a.startswith('-m') and a != '-mllvm']):
                sys.stderr.write(p.stderr)
                return p.returncode
            if drop:
                sys.stderr.write('gexcc: note: %s is declared (%s) -> void but %s returns %s: its result is dropped\n'
                                 % (name, params, os.path.basename(so), lret))
            else:
                sys.stderr.write('gexcc: warning: %s is declared (%s) -> %s but %s defines another type: '
                                 'calls to it trap\n' % (name, params, ret, os.path.basename(so)))
            stubs.append(o)
    return 1


# The features shared memory needs, said outright: an empty link (libbsd's
# `-shared -x assembler /dev/null`, to learn the output format) has no
# object to infer them from.
LDC_SHARED = ['wasm-ld', '--experimental-pic', '--shared-memory', '--import-memory', '--max-memory=4294967296',
              '--extra-features=atomics,bulk-memory']


def own_unwind():
    """libunwind ahead of the side modules: every C++ library has its own
    copy and exports its landing-pad context, which is thread-local in the
    module that defines it but plain data as another module's export, so a
    module whose code throws or catches takes libunwind's own (each module
    unwinds through its own copy, as -Bsymbolic has a library do)."""
    if (nostdlib or nodefaultlibs) and not cxx_rt_named or not os.path.exists(LIB + '/libunwind.a'):
        return []
    return [LIB + '/libunwind.a']


# The C++ runtime named outright (libtool's -nostdlib link of a C++ library,
# with the libraries it learned from the -v link line): taken as though the
# defaults were asked for
cxx_rt_named = CXX and any(i in ('-lc++', '-lstdc++', '-lc++abi', '-lsupc++', '-lunwind') for i in inputs)
libc_named = '-lc' in inputs


def shared_undef():
    """setjmp's exception tag and C++'s are libc's (libgex, libgexcc), one
    for the process so a longjmp or a throw crosses modules: imported (a
    .so's symbols carry no tags)."""
    f = os.path.join(tmp, 'undef-tags')
    open(f, 'w').write('__c_longjmp\n__cpp_exception\n')
    return ['--allow-undefined-file=' + f]

# -fgex-no-fork: the link's own fork, ahead of the libraries (rt/nofork.c)
NOFORK_O = [os.path.join(LIBDIR, 'nofork.o')] if NOFORK and not relocatable else []
if NOFORK_O and not os.path.exists(NOFORK_O[0]):
    die('-fgex-no-fork: no %s (tools/gexcc/install.sh builds it)' % NOFORK_O[0])

if shared and SHARED:
    objs = objs + NOFORK_O
    ins, missing = expand(inputs, set())
    ins = whole_closed([x for x in ins if not x.startswith('-L')] + missing)
    soname = (ld_opts['soname'] or [os.path.basename(out)])[-1]
    tail = []
    if not (nostdlib or nodefaultlibs) or libc_named:
        tail = [LIB + '/libc.so', LIB + '/libgexcc.a', LIB + '/libclang_rt.builtins-wasm32.a']
        if CXX:
            for l in ('c++', 'c++abi', 'unwind'):
                if os.path.exists('%s/lib%s.a' % (LIB, l)):
                    tail.insert(0, '%s/lib%s.a' % (LIB, l))
    pre = [] if (nostdlib or nostartfiles) and not libc_named else [os.path.join(LIBDIR, 'dso.o')]
    if not (nostdlib or nodefaultlibs) or libc_named:
        pre += shared_undef()
    wasm = os.path.join(tmp, 'out.so')
    sos = by_soname(ins + tail)
    # As ld -shared: an undefined symbol is the program's or another
    # library's, bound at load (libfl's main calls the program's yylex),
    # unless the link asked for every symbol defined (-z defs)
    wl = ','.join(a[4:] for a in sys.argv[1:] if a.startswith('-Wl,'))
    if not re.search(r'(^|,)(--no-undefined|-z,defs|-zdefs|-z defs)(,|$)', wl):
        pre += ['--unresolved-symbols=import-dynamic']
    # its thread-local block, as a program's: libc (a library the program
    # starts with) or gex's dlopen gives every thread one for each module
    # that exports these (wasm-ld drops __wasm_init_tls unexported, and a
    # module without it has its thread-locals at address 0 in every thread
    # but the first)
    tls = ['--export-if-defined=__wasm_init_tls', '--export-if-defined=__tls_size',
           '--export-if-defined=__tls_align', '--export-if-defined=__gex_set_tls_base']
    r = link_shared(LDC_SHARED + ['-shared', '-Bsymbolic', '--soname=' + soname, '-o', wasm] + tls
            + ['--rpath=' + x for x in ld_opts['rpath']] + objs + pre + own_unwind() + sos + ld_flags)
    if r:
        sys.exit(r)
    finish_shared(wasm, sos, True)
    shutil.copy(wasm, out)
    os.chmod(out, 0o755)
    sys.exit(0)

if relocatable:
    ins, miss = expand(inputs, set())
    sys.exit(run(['wasm-ld', '-r', '-o', out] + objs + whole_closed(ins) + [f for f in ld_flags if not f.startswith('--export')]))

if shared:
    # A static library under the shared name (see the module docstring).
    deps = []
    members = list(objs)
    for it in inputs:
        if it in WHOLE:
            continue
        if it.startswith('-l'):
            deps.append(it)
        elif is_archive(it):
            # an archive given by path: recorded, not copied (its members
            # would collide with the same archive linked again later)
            deps.append(os.path.abspath(it))
        else:
            members.append(it)
    # the -L directories, so the program's link finds the -l names
    deps = ['-L' + os.path.abspath(d) for d in libdirs] + deps
    # the deps go in a custom section of an empty wasm object (wasm-ld
    # reads every member; it never pulls this one, which defines nothing)
    tmpd = os.path.join(tmp, 'deps')
    os.makedirs(tmpd)
    txt = os.path.join(tmpd, 'deps.txt')
    open(txt, 'w').write('GEXCC-DEPS\n' + '\n'.join(deps) + '\nGEXCC-END')
    dfile = os.path.join(tmpd, 'gexcc.deps.o')
    r = run(['llvm-objcopy', '--add-section=gexcc.deps=' + txt, os.path.join(LIBDIR, 'empty.o'), dfile])
    if r:
        sys.exit(r)
    if os.path.exists(out):
        os.unlink(out)
    # members by path: objects in different directories may share a name
    r = run(['llvm-ar', 'crsP', out] + members + [dfile])
    sys.exit(r)

# A program.
objs = objs + NOFORK_O
if GUEST_I386:
    I386_O = os.path.join(LIBDIR, 'i386.o')
    if not os.path.exists(I386_O):
        die('-fgex-guest-i386: no %s (tools/gexcc/install.sh builds it with an x86 gexc)' % I386_O)
    objs = objs + [I386_O]
ins, missing = expand(inputs, set())
# -L from archives' deps: they only feed resolve(), already applied
ins = [x for x in ins if not x.startswith('-L')]
if missing:
    # wasm-ld says which are missing, as ld would
    ins += missing
ins = whole_closed(ins)
crt = [] if (nostdlib or nostartfiles) else [LIB + '/crt1.o']
libs = []
if not (nostdlib or nodefaultlibs):
    if CXX:
        for l in ('c++', 'c++abi', 'unwind'):
            if os.path.exists('%s/lib%s.a' % (LIB, l)):
                libs.append('%s/lib%s.a' % (LIB, l))
    # gex's support library first: its dlopen, __dlsym and vfork replace
    # libc's; the setjmp runtime and the signal frames are only there.
    libs += [LIB + '/libgex.a', LIB + '/libc.a', LIB + '/libclang_rt.builtins-wasm32.a']
    # the C++ exception tag (a shared program imports libc.so's)
    if os.path.exists(LIB + '/libgexcc_eh.a'):
        libs.append(LIB + '/libgexcc_eh.a')
    # gexcc's runtime: the loop safepoint (rt/poll.c)
    if os.path.exists(LIB + '/libgexcc.a'):
        libs.insert(0, LIB + '/libgexcc.a')
        # thread_local destructors (rt/cxa_thread_atexit.c): libc++abi's
        # reference is weak, which pulls nothing out of an archive
        if CXX:
            ld_flags += ['-u', '__cxa_thread_atexit_impl']
# main(argc, argv, envp): see rt/main3.c
if not nostartfiles and not nostdlib:
    cand = objs + [x for x in ins if not x.startswith('-') and not is_archive(x)]
    if cand:
        nm = subprocess.run(['llvm-nm', '-j', '--defined-only', '--extern-only'] + cand,
                            capture_output=True, text=True).stdout.split()
        if 'main' in nm and '__main_argc_argv' not in nm and '__main_void' not in nm:
            objs.append(os.path.join(LIBDIR, 'main3.o'))
wasm = os.path.join(tmp, 'out.wasm')
if SHARED:
    # against libc.so (libgex and libgexcc are in it); crt1 calls into it,
    # it calls the program's __gex_start_c, and libc gives the program's
    # thread-local block its place (dso.o: its __tls_base setter)
    if not (nostdlib or nodefaultlibs):
        libs = [l for l in libs if not l.endswith(('/libgex.a', '/libc.a', '/libgexcc.a', '/libgexcc_eh.a'))]
        libs = [LIB + '/libc_nonshared.a', LIB + '/libc.so', LIB + '/libgexcc.a'] + libs
    sos = by_soname(ins + libs)
    exp = ['--export=__gex_start_c', '--export-if-defined=__wasm_init_tls', '--export-if-defined=__tls_size',
           '--export-if-defined=__tls_align', '--export-if-defined=__gex_set_tls_base']
    if export_dynamic:
        exp.append('--export-dynamic')
    cmd = LDC_SHARED + ['-pie', '--no-entry', '-o', wasm] + exp + ['--rpath=' + x for x in ld_opts['rpath']] \
        + crt + objs + [os.path.join(LIBDIR, 'dso.o')] + own_unwind() + sos + ld_flags + shared_undef()
    r = link_shared(cmd)
    if r:
        sys.exit(r)
    finish_shared(wasm, sos, False)
else:
    cmd = ['wasm-ld', '--experimental-pic', '--shared-memory', '--import-memory', '--max-memory=4294967296',
           '-pie', '--no-entry', '-o', wasm] + crt + objs + ins + ld_flags + libs
    # -rdynamic: the program's symbols for dlsym (gex's loader finds them)
    if export_dynamic:
        cmd.append('--export-dynamic')
    r = run(cmd)
    if r:
        sys.exit(r)


if FPCAST:
    # Calls through a function pointer of another type trap in wasm, where C
    # on a real machine gets away with them (bash's unwind-protect cleanups,
    # old K&R code). binaryen's --fpcast-emu makes every indirect call and
    # every table entry one uniform signature. gex calls the two entries
    # (__gex_start, __gex_thread_start) with their own signatures, so their
    # table entries go back to the real functions afterwards.
    # every indirect call passes binaryen's maximum number of parameters
    # (16 by default); a program with a wider function (GTK's marshallers
    # take 19) asks for the width it needs
    # A program linked -rdynamic gives dlsym its functions (GtkBuilder's
    # signal handlers): each export gets a thunk too, exported for the
    # loader (gex-dylink fpcast-prep/-post)
    thunks = export_dynamic and os.path.exists(DYLINK)
    if thunks and run([DYLINK, 'fpcast-prep', wasm, wasm]):
        die('gex-dylink fpcast-prep failed')
    cmd = [opt, wasm, '-g', '--strip-dwarf'] + feats + ['--fpcast-emu', '-o', wasm]
    p = subprocess.run(cmd, capture_output=True, text=True)
    # (binaryen names one function's need at a time: raise until it passes)
    for _ in range(64):
        m = re.findall(r'max-func-params needs to be at least (\d+)', p.stderr + p.stdout)
        if not (p.returncode and m):
            break
        p = subprocess.run(cmd[:-2] + ['--pass-arg=max-func-params@' + str(max(map(int, m)) + 4), '-o', wasm],
                           capture_output=True, text=True)
    if p.returncode:
        sys.stderr.write(p.stderr)
        die('wasm-opt --fpcast-emu failed')
    if thunks and run([DYLINK, 'fpcast-post', wasm, wasm]):
        die('gex-dylink fpcast-post failed')
    r = run([sys.executable, os.path.join(LIBDIR, 'elemfix.py'), wasm, '__gex_start', '__gex_thread_start'])
    if r:
        die('elemfix.py failed after --fpcast-emu')
passes = []
if SHARED:
    pass
elif NOFORK:
    no_fork_point(wasm)
elif ('gex', 'fork_point') in imports(wasm):
    passes += ['--asyncify', '--pass-arg=asyncify-imports@gex.fork_point']
    dis = os.path.join(os.path.dirname(opt), 'wasm-dis')
    reach = subprocess.run([sys.executable, os.path.join(LIBDIR, 'forkreach.py'), dis, wasm, 'gex.fork_point'] + FORK_ROOTS,
                           capture_output=True, text=True)
    if reach.returncode == 0:
        lst = os.path.join(tmp, 'onlylist')
        open(lst, 'w').write(reach.stdout.strip().replace(',', '\n') + '\n')
        passes += ['--pass-arg=asyncify-onlylist@@' + lst]
    else:
        passes += ['--pass-arg=asyncify-removelist@' + ','.join(FORK_ROOTS)]
    passes += ['-O2']
if not SHARED:
    passes += ['--translate-to-exnref']
# names kept (-g): gex binds the entries by name. (DWARF dropped: binaryen
# cannot rewrite clang's version 5.)
r = run([opt, wasm, '-g', '--strip-dwarf'] + feats + passes + ['-o', wasm])
if r:
    die('wasm-opt failed')
cont = ['--host-stub', os.path.join(LIBDIR, 'hoststub.bin')]
# GEXCC_BAKE_RUNNER (set, even empty) bakes that path instead: a package
# built for an image says none, since the build machine's runner path means
# nothing there (on the build machine its containers still find the runner
# through GEXCC_RUNNER or gexc on PATH)
rf = os.path.join(LIBDIR, 'runner')
if 'GEXCC_BAKE_RUNNER' in os.environ:
    if os.environ['GEXCC_BAKE_RUNNER']:
        cont += ['--runner', os.environ['GEXCC_BAKE_RUNNER']]
elif os.path.exists(rf):
    cont += ['--runner', open(rf).read().strip()]
if GUEST_I386:
    cont += ['--guest-i386', '--linux-space']
r = run([sys.executable, os.path.join(LIBDIR, 'wasm-elf.py'), wasm, out] + cont)
if r:
    sys.exit(r)
if os.environ.get('GEXCC_KEEP_WASM'):
    shutil.copy(wasm, out + '.wasm')
