#!/usr/bin/env python3
"""clang for 64-bit Wine's LP64 programs.

Like wcc, for code with x86-64 Linux's LP64 layout: compiles for wasm64
(flags64.sh) against GEX_WASM64_SYSROOT (toolchain/sysroot/build64.sh), links a
position-independent wasm64 executable, lowers it to wasm32 with binaryen's
--memory64-lowering (the one place an address becomes a memory index: every
i64 address wrapped to i32 at its access), and wraps it in the ELF64 x86-64
container gexc runs (wasm-elf.py --elf64). The module stays beside it as
OUT.wasm (OUT.64.wasm is the module before lowering).

-Wl,--gex-guest-x86-64 marks a program that runs x86-64 Windows code (the
64-bit Wine loader). No fork (programs spawn by clone/vfork).

Wine's PE side (-D_WIN32) compiles with
Windows x64's data model (the GexLLP64 plugin and its llp64.h,
-mms-bitfields, -fshort-wchar) and the ICALL pass (GexIcall, its Win64
mode), both from the sysroot's lib (toolchain/sysroot/build64.sh). `-shared` links
a position-independent side module (a DLL's code) and lowers it, `-r` one
relocatable object of the inputs (pelink.py).

-mgex-memory64 builds the memory64 flavour:
GEX_MEMORY64 defined, the module left unlowered (no runtime runs it yet;
the sysroot's libgex would need the same define)."""
import os, re, subprocess, sys, tempfile, atexit, shutil

here = os.path.dirname(os.path.realpath(__file__))
flags = {}
for line in open(os.path.join(here, '..', 'toolchain', 'sysroot', 'flags64.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
sysroot = os.environ.get('GEX_WASM64_SYSROOT') or sys.exit('wcc64: set GEX_WASM64_SYSROOT')
CC = ['clang'] + flags['WASM64_CFLAGS'].split() + ['--sysroot=' + sysroot, '-isystem', sysroot + '/include']
# No build host's paths in what ships (wcc's maps: this tree, the sysroot,
# GEX_WCC_MAP's FROM=TO pairs).
_maps = [(os.path.realpath(os.path.join(here, '..')), '/gex'), (os.path.realpath(sysroot), '/sysroot')]
_maps += [tuple(m.split('=', 1)) for m in os.environ.get('GEX_WCC_MAP', '').split(':') if '=' in m]
CC += ['-ffile-prefix-map=%s=%s' % m for m in _maps]
if os.path.basename(sys.argv[0]).endswith('++'):
    CC[0] = 'clang++'
args = sys.argv[1:]
mem64 = '-mgex-memory64' in args
if mem64:
    args = [a for a in args if a != '-mgex-memory64']
    CC += ['-DGEX_MEMORY64=1']
if '-D_WIN32' in args:
    lib = os.path.join(sysroot, 'lib')
    # not the sysroot's LP64 libc headers: Wine's (include/msvcrt) and clang's
    CC = CC[:1] + flags['WASM64_CFLAGS'].split() + (['-DGEX_MEMORY64=1'] if mem64 else [])
    CC += ['-nostdlibinc', '-fplugin=' + os.path.join(lib, 'GexLLP64.so'), '-include', os.path.join(lib, 'llp64.h'),
           '-mms-bitfields', '-fshort-wchar']
    # C++ on the PE side (Wine's libc++, icu) sees a Windows target, as
    # MinGW's does: Linux's predefined macros would send libc++'s headers to
    # glibc's (C keeps them, as it always has). Static destructors are not
    # registered: a wasm module's terminators never run (gexrt's empty
    # .CRT$XT table), and Windows has no __cxa_atexit
    if CC[0] == 'clang++':
        CC += ['-U__linux__', '-U__linux', '-Ulinux', '-U__gnu_linux__', '-U__unix__', '-U__unix', '-Uunix', '-U__ELF__',
               '-DWINE_UNICODE_NATIVE', '-fno-c++-static-destructors']
    if not os.environ.get('GEX_WASM_NO_ICALL'):
        CC += ['-fpass-plugin=' + os.path.join(lib, 'GexIcall.so')]
# The snapshot flavour's sysroot (toolchain/sysroot/build64.sh GEX_SNAPSHOT=1, which
# leaves lib/gex-snapshot): code takes the legacy exception instructions,
# which asyncify can instrument (the program's link below, and pelink.py's
# for the native PE core, translate them to the standard ones), and is
# compiled with __GEX_SNAPSHOT__ (the PE core's roots: libs/gexrt). A
# program's system calls, through gex.syscall as the default build's, are
# where a session snapshot unwinds a thread, at one that can block
# (libgex-snap/call.c). Only there: another sysroot's programs are built
# as before, byte for byte.
SNAPSHOT = os.path.exists(os.path.join(sysroot, 'lib', 'gex-snapshot'))
if SNAPSHOT:
    CC = [a.replace('-wasm-use-legacy-eh=false', '-wasm-use-legacy-eh=true') for a in CC] + ['-D__GEX_SNAPSHOT__']
if any(a in ('-c', '-S', '-E', '-M', '-MM', '--version', '-v') or a.startswith(('-print-', '-dump'))
       for a in args) or not args:
    os.execvp(CC[0], CC + args)

out = 'a.out'
srcs, ld_in, ld_flags, cflags = [], [], [], []
guest = False
shared = relocatable = False
i = 0
while i < len(args):
    a = args[i]
    if a == '-o':
        out = args[i + 1]; i += 2; continue
    if a == '-shared':
        shared = True
    elif a == '-r':
        relocatable = True
    elif a.startswith('-Wl,'):
        parts = [x for x in a[4:].split(',') if x]
        guest = guest or '--gex-guest-x86-64' in parts
        ld_flags += [x for x in parts if x not in ('--start-group', '--end-group', '--gex-guest-x86-64')]
    elif a.startswith(('-l', '-L')):
        if a in ('-l', '-L'):
            ld_in += [a + args[i + 1]]; i += 2; continue
        if a not in ('-lc', '-lm', '-lpthread', '-ldl', '-lrt', '-lutil'):
            ld_in.append(a)
    elif a.endswith(('.c', '.cc', '.cpp', '.s', '.S')) and not a.startswith('-'):
        srcs.append(a)
    elif a.endswith(('.o', '.a', '.so', '.lo')) and not a.startswith('-'):
        ld_in.append(a)
    elif a in ('-pthread', '-rdynamic', '-static', '-pie', '-no-pie', '-fPIE', '-fpie'):
        pass
    elif a in ('-I', '-D', '-U', '-include', '-isystem', '-MF', '-MT', '-x'):
        cflags += [a, args[i + 1]]; i += 2; continue
    else:
        cflags.append(a)
    i += 1

tmp = tempfile.mkdtemp(prefix='wcc64-')
atexit.register(shutil.rmtree, tmp, True)
objs = []
for k, s in enumerate(srcs):
    o = os.path.join(tmp, '%d.o' % k)
    r = subprocess.call(CC + cflags + ['-c', s, '-o', o])
    if r:
        sys.exit(r)
    objs.append(o)

def ld(cmd, name):
    """wasm-ld CMD; a direct call whose type is not its callee's fails the
    link. wasm-ld only warns ("function signature mismatch") and links the
    call to a stub that traps: a Wine DLL declaring a function apart from
    its definition (msvcp60's string destructors) traps on the first call."""
    p = subprocess.run(cmd, stderr=subprocess.PIPE, text=True)
    sys.stderr.write(p.stderr)
    if p.returncode:
        return p.returncode
    # A configure test links a function through autoconf's own `char f();`
    # on purpose (it only asks whether the symbol exists), so its program
    # is not judged.
    # The C runtime calls main or wmain with every argument it has; a
    # program's own main may take fewer, as C allows.
    out = os.path.basename(cmd[cmd.index('-o') + 1])
    bad = [m for m in re.findall(r'function signature mismatch: (\S+)', p.stderr) if m not in ('main', 'wmain')]
    if bad and not out.startswith('conftest'):
        sys.stderr.write('%s: %s: a call through a declaration whose type is not the function\'s '
                         '(wasm-ld links it to a trap); make the two agree\n' % (name, cmd[cmd.index('-o') + 1]))
        return 1
    return 0


lib = sysroot + '/lib'
opt = os.environ.get('GEX_WASM_OPT') or 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',
         '--enable-memory64']
if relocatable:
    # one relocatable object of the inputs (an archive's members only as
    # referenced): a DLL for pelink.py, lowered with the whole PE module
    cmd = ['wasm-ld', '-mwasm64', '-r', '-L' + lib, '-o', out] + objs + ld_in + [f for f in ld_flags if not f.startswith('--export')]
    sys.exit(ld(cmd, 'wcc64'))
if shared:
    # a side module (a DLL's code behind its skeleton), lowered in place
    w64 = out + '.64.wasm'
    cmd = ['wasm-ld', '-mwasm64', '--experimental-pic', '--shared-memory', '--import-memory',
           '--max-memory=' + ('17179869184' if mem64 else '4294967296'), '-L' + lib, '-shared', '--no-entry',
           '-o', w64] + objs + ld_in + ld_flags + [lib + '/libclang_rt.builtins-wasm64.a']
    r = ld(cmd, 'wcc64')
    if r:
        sys.exit(r)
    r = subprocess.call([opt, w64, '-g', '--strip-dwarf'] + feats + ([] if mem64 else ['--memory64-lowering']) + ['-O2', '-o', out])
    sys.exit(r and 'wcc64: wasm-opt failed on ' + w64)
w64 = out + '.64.wasm'
wasm = out + '.wasm'
# libgex before libc: its setjmp runtime, dlopen registry and signal frames.
# The snapshot flavour's part last (its sysroot's libgex-snap.a): the
# default build's data and table slots keep their places.
snap_a = [lib + '/libgex-snap.a'] if SNAPSHOT else []
cmd = ['wasm-ld', '-mwasm64', '--experimental-pic', '--shared-memory', '--import-memory',
       '--max-memory=' + ('17179869184' if mem64 else '4294967296'), '-L' + lib, '-pie', '--no-entry', '-o', w64, lib + '/crt1.o'] + \
    objs + ld_in + ld_flags + [lib + '/libgex.a', '-lc', lib + '/libclang_rt.builtins-wasm64.a'] + snap_a
r = ld(cmd, 'wcc64')
if r:
    sys.exit(r)
# (the snapshot flavour's roots stay apart from the libc's start they
# run, which asyncify instruments: neither -O2 inlines it into a root)
apart = ['--no-inline=__gex_snap_libc_start'] if SNAPSHOT and not mem64 else []
# Names kept (-g): gex binds the entries by name.
r = subprocess.call([opt, w64, '-g', '--strip-dwarf'] + feats + ([] if mem64 else ['--memory64-lowering']) + apart + ['-O2', '-o', wasm])
if r:
    sys.exit('wcc64: wasm-opt failed on ' + w64)
if SNAPSHOT and not mem64:
    # Asyncify over the lowered module, only what can reach a blocking call
    # by a type-correct call graph (forkreach.py, as wcc's fork), less the
    # thread's start and the snapshot's own roots, where an unwind ends (the
    # host's entry, or libgex-snap's run of a thread's function); then the
    # standard exception instructions.
    dis = os.path.join(os.path.dirname(opt), 'wasm-dis')
    roots = ['__gex_start', '__gex_start_c', '__gex_thread_start', '__gex_thread_c', '__gex_snap_resume64',
             '__gex_snap_run', '__gex_snap_run_loop', '__gex_snap_resume_run']
    # (and through the host's call-outs, which a thread stopped inside guest
    # code a call of Wine's runs unwinds through)
    reach = subprocess.run([sys.executable, os.path.join(here, '..', 'toolchain', 'gexcc', 'forkreach.py'), '--quoted', '--also', 'gex.call_out', dis, wasm, 'gex.syscall'] + roots,
                           capture_output=True, text=True)
    passes = ['--asyncify', '--pass-arg=asyncify-imports@gex.syscall,gex.call_out']
    if reach.returncode == 0:
        lst = os.path.join(tmp, 'onlylist')
        open(lst, 'w').write(reach.stdout.strip() + '\n')
        passes += ['--pass-arg=asyncify-onlylist@@' + lst]
    else:
        passes += ['--pass-arg=asyncify-removelist@' + ','.join(roots)]
    feats32 = [f for f in feats if f != '--enable-memory64']
    r = subprocess.call([opt, wasm, '-g'] + feats32 + apart + passes + ['-O2', '--translate-to-exnref', '-o', wasm])
    if r:
        sys.exit('wcc64: asyncify failed on ' + wasm)
sys.exit(subprocess.call([sys.executable, os.path.join(here, '..', 'toolchain', 'gexcc', 'wasm-elf.py'), wasm, out, '--elf64'] +
                         (['--guest-x86-64'] if guest else [])))
