#!/usr/bin/env python3
"""gexcc-win: MinGW-named compiler driver for Windows x64 programs as native
wasm (gexcc's Windows target), installed by windows/install.sh as
PREFIX/bin/x86_64-w64-mingw32-*.

x86_64-w64-mingw32-gcc / -g++ / -cc / -c++:
  compile (-c, -E, -S, -M*): wcc64 (wasm64, LLP64 lowered) with Wine's
    headers and C runtime, libc++ (build-cxx-win.sh) for C++;
  link (-o X.exe, -shared -o X.dll): winegcc's link (X.so.r.o and its
    export list), then pelink's split module X.wasm against the 64-bit
    core, then X's PE32+ skeleton naming it (winebuild --fake-module
    --wasm-shared), with an import table of the DLL functions it calls
    (peimports.py). Out: X (the skeleton) and X.wasm beside it. -s (or
    GEXCC_WIN_STRIP=1) is a release link: names gex never reads out
    (stripnames.py); GEXCC_WIN_STRIP=0 keeps them.
x86_64-w64-mingw32-windres: Wine's wrc (-o X.res, or a COFF .o made from it).
x86_64-w64-mingw32-{ar,ranlib,nm,objcopy,objdump}: llvm's; strip does
  nothing (a release link strips).

The install's PREFIX/lib/gexcc-win/config names the 64-bit Wine build and
source it builds against, the wasm64 sysroot and the libc++ prefix.
"""
import os, subprocess, sys, tempfile, shutil

here = os.path.dirname(os.path.realpath(__file__))
PREFIX = os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0])))
LIB = PREFIX + '/lib/gexcc-win'
conf = {}
try:
    for line in open(LIB + '/config'):
        k, _, v = line.strip().partition('=')
        if k:
            conf[k] = v
except OSError:
    sys.exit('gexcc-win: no %s/config (toolchain/gexcc/windows/install.sh PREFIX)' % LIB)
B = conf['WINE_BUILD']
S = conf['WINE_SRC']
os.environ.setdefault('GEX_WASM64_SYSROOT', conf['SYSROOT64'])
# (wcc64's and pelink's binaryen: the sysroot's)
os.environ['PATH'] = os.environ['GEX_WASM64_SYSROOT'] + '/binaryen/bin:' + os.environ['PATH']
CXX = os.environ.get('GEXCC_WIN_CXX') or conf.get('CXX', PREFIX + '/cxx')
W = os.path.realpath(here + '/../../../wine')


def tools_dir():
    for line in open(B + '/Makefile'):
        if '/tools/winebuild/winebuild' in line:
            for w in line.split():
                if w.endswith('/tools/winebuild/winebuild'):
                    return w[:-len('/tools/winebuild/winebuild')]
    sys.exit('gexcc-win: no winebuild in ' + B)


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


def defined_params(path, name):
    """The parameter count of the function NAME the wasm object PATH defines, or None."""
    try:
        data = open(path, 'rb').read()
    except OSError:
        return None
    if data[:4] != b'\0asm': return None
    i = 8; types = []; nimp = 0; funcs = []
    while i < len(data):
        sid = data[i]; size, i = _leb(data, i + 1); end = i + size
        if sid == 1:
            n, j = _leb(data, i)
            for _ in range(n):
                np_, j = _leb(data, j + 1); j += np_
                nr, j = _leb(data, j); j += nr
                types.append(np_)
        elif sid == 2:
            n, j = _leb(data, i)
            for _ in range(n):
                for _ in range(2):
                    ln, j = _leb(data, j); j += ln
                kind = data[j]; j += 1
                if kind == 0: _, j = _leb(data, j); nimp += 1
                elif kind in (1, 2):
                    if kind == 1: j += 1
                    fl, j = _leb(data, j); _, j = _leb(data, j)
                    if fl & 1: _, j = _leb(data, j)
                elif kind == 3: j += 2
                elif kind == 4: _, j = _leb(data, j + 1)
        elif sid == 3:
            n, j = _leb(data, i)
            for _ in range(n):
                t, j = _leb(data, j); funcs.append(t)
        elif sid == 0:
            ln, j = _leb(data, i)
            if data[j:j + ln] == b'linking':
                _, j = _leb(data, j + ln)
                while j < end:
                    st = data[j]; sl, j = _leb(data, j + 1); send = j + sl
                    if st == 8:  # WASM_SYMBOL_TABLE
                        n, k = _leb(data, j)
                        for _ in range(n):
                            kind = data[k]; fl, k = _leb(data, k + 1)
                            if kind in (0, 2, 4, 5):  # function, global, tag, table
                                idx, k = _leb(data, k); nm = None
                                if not fl & 0x10 or fl & 0x40:  # defined, or an explicit name
                                    ln2, k = _leb(data, k); nm = data[k:k + ln2]; k += ln2
                                if kind == 0 and nm == name.encode() and not fl & 0x10 and idx >= nimp:
                                    return types[funcs[idx - nimp]]
                            elif kind == 1:  # data
                                ln2, k = _leb(data, k); k += ln2
                                if not fl & 0x10:
                                    for _ in range(3): _, k = _leb(data, k)
                            elif kind == 3:  # section
                                _, k = _leb(data, k)
                    j = send
        i = end
    return None


def compile_wmain(cmd, obj):
    """Run the compile CMD of OBJ. The CRT calls wmain(argc, argv), and a call
    of another type traps (clang renames only main by its parameters): an
    object whose wmain is wmain(void) or wmain(argc, argv, envp) is compiled
    again with it renamed __gexw_wmainN, which the link's adapter calls
    (rt/wmain.c)."""
    r = subprocess.call(cmd)
    if r: return r
    n = defined_params(obj, 'wmain')
    if n in (0, 3):
        r = subprocess.call(cmd + ['-Dwmain=__gexw_wmain%d' % n])
    return r


tool = os.path.basename(sys.argv[0])
tool = tool[len('x86_64-w64-mingw32-'):] if tool.startswith('x86_64-w64-mingw32-') else tool
args = sys.argv[1:]

if tool in ('ar', 'ranlib', 'nm', 'strip', 'objcopy', 'objdump'):
    if tool == 'strip':
        sys.exit(0)  # names stay until the release link strips them
    os.execvp('llvm-' + tool, ['llvm-' + tool] + args)

if tool == 'windres':
    # windres syntax through wrc: -i/-o/--input/--output, -I, -D, -O coff
    T = tools_dir()
    inp = out = None; incl = []; fmt = None; i = 0
    while i < len(args):
        a = args[i]
        if a in ('-i', '--input'): inp = args[i + 1]; i += 2; continue
        if a in ('-o', '--output'): out = args[i + 1]; i += 2; continue
        if a.startswith('--input='): inp = a.split('=', 1)[1]
        elif a.startswith('--output='): out = a.split('=', 1)[1]
        elif a in ('-O', '--output-format'): fmt = args[i + 1]; i += 2; continue
        elif a.startswith(('-I', '-D', '-U')) and len(a) > 2: incl.append(a)
        elif a in ('-I', '-D', '-U', '--include-dir', '--define'):
            incl.append(('-I' if a in ('-I', '--include-dir') else '-D' if a in ('-D', '--define') else '-U') + args[i + 1]); i += 2; continue
        elif a.startswith('--include-dir='): incl.append('-I' + a.split('=', 1)[1])
        elif a.startswith('--define='): incl.append('-D' + a.split('=', 1)[1])
        elif not a.startswith('-'):
            if inp is None: inp = a
            else: out = a
        i += 1
    res = out if out and out.endswith('.res') else (out or 'a') + '.res'
    cmd = [T + '/tools/wrc/wrc', '-m64', '--nostdinc', '-I' + B + '/include', '-I' + S + '/include', '-I' + here + '/include',
           '--nls-dir=' + S + '/nls', '-D_WIN32', '-D_WIN64', '-o', res] + incl + [inp]
    r = subprocess.call(cmd)
    if r or res == out:
        sys.exit(r)
    # a ".o" the link takes: the .res itself (the link passes .res files
    # to the skeleton)
    shutil.move(res, out)
    sys.exit(0)

cxx = tool in ('g++', 'c++', 'clang++')
if args[:1] == ['-dumpmachine']:
    print('x86_64-w64-mingw32'); sys.exit(0)
cc = os.path.join(W, 'wcc64++' if cxx else 'wcc64')
base = ['-D_WIN32', '-D_UCRT', '-DWINE_UNICODE_NATIVE', '-DWINBOOL=BOOL', '-D__stdcall=', '-D__cdecl=', '-D__fastcall=', '-D__thiscall=', '-D__vectorcall=', '-D__MINGW32__', '-D__MINGW64__', '-Wno-ignored-attributes', '-Wno-unknown-attributes', '-U__linux__', '-U__linux', '-Ulinux', '-U__gnu_linux__', '-U__unix__', '-U__unix', '-Uunix', '-U__ELF__', '-fPIC', '-Wno-unused-command-line-argument',
        # (long is 32-bit here, but what the backend lowers these builtins to
        # returns the target's 64-bit long)
        '-fno-builtin-lround', '-fno-builtin-lroundf', '-fno-builtin-lroundl', '-fno-builtin-lrint',
        '-fno-builtin-lrintf', '-fno-builtin-lrintl',
        # libc++ calls __builtin_lround* directly; the backend types those with a 64-bit long
        *['-D__builtin_%s=%s' % (n, n) for n in ('lround', 'lroundf', 'lroundl', 'lrint', 'lrintf', 'lrintl')],
        # (system dirs: a program's own -I comes first, its config.h before Wine's)
        '-isystem', here + '/include-pre', '-isystem', B + '/include', '-isystem', S + '/include']
if cxx:
    # (COM methods that return a struct, Direct2D's GetSize and the rest, as
    # Windows returns them and Wine's C implementations do: through a
    # pointer the method also returns, the headers' explicit form)
    base = ['-include', here + '/include/gexcc-win-cxx.h', '-nostdinc++', '-isystem', CXX + '/include/c++/v1', '-D_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER',
            '-DWIDL_EXPLICIT_AGGREGATE_RETURNS'] + base
base += ['-isystem', S + '/include/msvcrt', '-idirafter', here + '/include', '-fdeclspec']
# compile-only modes
if any(a in ('-c', '-S', '-E', '-M', '-MM') for a in args) or not any(not a.startswith('-') for a in args):
    if cxx and '-fno-exceptions' not in args:
        base += ['-fexceptions', '-fwasm-exceptions']
    # -c: wmain's type is checked once the object is made (compile_wmain)
    srcs = [a for a in args if a.endswith(('.c', '.cc', '.cpp', '.cxx')) and not a.startswith('-')]
    if '-c' not in args or not srcs or any(a in ('-E', '-S', '-M', '-MM') for a in args):
        os.execv(cc, [cc] + base + args)
    o = args[args.index('-o') + 1] if '-o' in args else None
    if o is None and len(srcs) == 1:
        o = os.path.splitext(os.path.basename(srcs[0]))[0] + '.o'
    if o is None:
        os.execv(cc, [cc] + base + args)
    sys.exit(compile_wmain([cc] + base + args, o))

# link
T = tools_dir()
out = 'a.exe'; shared = False; objs = []; libs = []; libdirs = []; ress = []; srcs = []; defs = []
subsys = None; extra_cflags = []; unicode = False; deflibs = True
# -s (or GEXCC_WIN_STRIP=1): a release build, the module's name and debug sections
# out; GEXCC_WIN_STRIP=0 keeps them whatever the build asks
strip = os.environ.get('GEXCC_WIN_STRIP') == '1'
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 == '-s': strip = True
    elif a == '-mwindows': subsys = 'windows'
    elif a == '-mconsole': subsys = 'console'
    elif a == '-municode': unicode = True
    elif a in ('-nostdlib', '-nodefaultlibs'): deflibs = False
    elif a.startswith('-Wl,'):
        for p in a[4:].split(','):
            if p in ('-s', '--strip-all'): strip = True
            if p.startswith('--subsystem'):
                subsys = p.split('=', 1)[1] if '=' in p else None
    elif a.startswith('-L'): libdirs.append(a[2:] or args[i + 1]); i += (1 if a[2:] else 2); continue
    elif a.startswith('-l'):
        libs.append(a[2:] or args[i + 1]); i += (1 if a[2:] else 2); continue
    elif a.endswith('.res'): ress.append(a)
    elif a.endswith('.def'): defs.append(a)
    elif a.endswith(('.c', '.cc', '.cpp', '.cxx')) and not a.startswith('-'): srcs.append(a)
    elif a.endswith(('.o', '.obj', '.a')) and not a.startswith('-'):
        # a windres "object" is a .res
        with open(a, 'rb') as f:
            head = f.read(32)
        (ress if head[:8] == b'\0\0\0\0\x20\0\0\0' else objs).append(a)
    elif a.startswith(('-I', '-D', '-U', '-O', '-f', '-std', '-m', '-W', '-g')):
        extra_cflags.append(a)
    i += 1
if subsys and subsys.startswith('windows'): subsys = 'windows'
elif subsys: subsys = 'console'

tmp = tempfile.mkdtemp(prefix='gexcc-win-')
try:
    for k, s in enumerate(srcs):
        o = os.path.join(tmp, '%d.o' % k)
        r = compile_wmain([cc] + base + (['-fexceptions', '-fwasm-exceptions'] if cxx else []) + extra_cflags + ['-c', s, '-o', o], o)
        if r: sys.exit(r)
        objs.append(o)
    # libraries: a -lNAME found as libNAME.a on a -L path is linked in; the
    # rest are DLL import names (Wine's import libraries)
    def implib(d):
        # a DLL's import library: dlls/NAME (NAME.dll) or dlls/NAME.drv (winspool)
        for x in (d, d + '.drv'):
            if os.path.exists(B + '/dlls/%s/lib%s.a' % (x, d)):
                return B + '/dlls/%s/lib%s.a' % (x, d)
        return B + '/dlls/%s/lib%s.a' % (d, d)
    dlls = []; archives = []
    for l in libs:
        p = next((os.path.join(d, 'lib%s.a' % l) for d in libdirs if os.path.exists(os.path.join(d, 'lib%s.a' % l))), None)
        if not p and os.path.exists(B + '/libs/%s/lib%s.a' % (l, l)):
            p = B + '/libs/%s/lib%s.a' % (l, l)  # Wine's static ones: uuid, dxguid, strmiids
        if p:
            archives.append(p)
        elif l in ('stdc++', 'c++', 'supc++', 'gcc', 'gcc_s', 'gcc_eh', 'mingw32', 'mingwex', 'moldname', 'pthread', 'm', 'msvcrt', 'ucrt', 'winpthread'):
            continue
        else:
            dlls.append(l)
    # MinGW's default libraries (its specs' *lib), after the program's: a
    # program built with MinGW names none of them (kernel32 is always
    # imported, below), and a name they define that another Wine DLL
    # defines too (unicows' SendMessageW, its adapters' types the .spec's
    # 32-bit words) went to that DLL instead
    if deflibs:
        dlls += [d for d in (['gdi32', 'comdlg32'] if subsys == 'windows' else []) + ['advapi32', 'shell32', 'user32']
                 if d not in dlls]
    if cxx:
        archives += [CXX + '/lib/libc++.a', CXX + '/lib/libc++abi.a', CXX + '/lib/libunwind.a']
    archives.append(LIB + '/libgexcc-win.a')
    name = os.path.basename(out)
    if '.' not in name:
        name += '.dll' if shared else '.exe'
    so = os.path.join(tmp, name + '.so')
    imp = []
    for d in dlls + ['ucrtbase', 'kernel32', 'ntdll']:
        lib = implib(d)
        if os.path.exists(lib) and lib not in imp:
            imp.append(lib)
        elif not os.path.exists(lib):
            print('gexcc-win: no import library for %s' % d, file=sys.stderr)
    crt = [B + '/libs/winecrt0/libwinecrt0.a']
    # a wmain of another type, renamed (compile_wmain): the CRT's wmain(argc, argv) calls it
    adapt = []
    if unicode and not shared:
        n = next((n for n in (0, 3) for o in objs if defined_params(o, '__gexw_wmain%d' % n) is not None), None)
        if n is not None: adapt = [LIB + '/wmain%d.o' % n]
    # the entry point runs the C++ constructors first (rt/start.c)
    entry = '__gexw_DllMainCRTStartup' if shared else '__gexw_wmainCRTStartup' if unicode else '__gexw_mainCRTStartup'
    wg = [T + '/tools/winegcc/winegcc', '-o', so, '--wine-objdir', B, '--winebuild', T + '/tools/winebuild/winebuild',
          '--cc-cmd=' + W + '/wcc64', '-b', 'wasm64-unknown-linux-musl', '-mno-cygwin', '-fPIC',
          '-m' + (subsys or 'console'), '-Wl,--entry,' + entry,
          # (Wine's libuuid is one object of every GUID: a program defining
          # some itself, as MinGW's per-GUID libuuid allows, defines them twice)
          '-Wl,--allow-multiple-definition'] + (['-municode'] if unicode else []) + (['-shared'] if shared else []) + defs + objs + [LIB + '/start_%s.o' % ('dll' if shared else 'wmain' if unicode else 'main')] + [LIB + '/wasm32_sjlj.o'] + adapt + archives + imp + crt
    # (run in tmp: winebuild makes its temporary dir in its cwd, and the
    # export lists wcc64 writes beside its objects kept it from removing it)
    wg = [os.path.abspath(a) if not a.startswith('-') and os.path.exists(a) else a for a in wg]
    r = subprocess.call(wg, env=dict(os.environ, TMPDIR=tmp), cwd=tmp)
    if r: sys.exit(r)
    # pelink: its split module against the core
    deps = []
    for d in dlls + ['ucrtbase', 'kernel32', 'ntdll']:
        if os.path.exists(implib(d)):
            x = os.path.basename(os.path.dirname(implib(d)))
            deps.append(x + ('.dll' if '.' not in x else ''))
    core = ['ntdll.dll', 'kernelbase.dll', 'kernel32.dll', 'msvcrt.dll', 'ucrtbase.dll']
    env = dict(os.environ, TMPDIR=tmp, PATH=os.environ['GEX_WASM64_SYSROOT'] + '/binaryen/bin:' + os.environ['PATH'])
    log = os.path.join(tmp, 'pelink.log')
    with open(log, 'w') as lf:
        r = subprocess.call([sys.executable, W + '/pelink.py', '--wasm64', '--split', name + '.wasm', name,
                             '--extra', name, so + '.r.o', so + '.exports', ','.join(deps), '--stub-imports', B, S,
                             os.path.join(tmp, 'core.wasm')] + core, env=env, stdout=lf, stderr=subprocess.STDOUT)
    if r:
        sys.stderr.write(open(log).read()[-4000:]); sys.exit(r)
    # (its warnings: the imports bound to stubs, --stub-imports)
    sys.stderr.writelines(l for l in open(log) if l.startswith('pelink: warning:'))
    odir = os.path.dirname(os.path.abspath(out))
    # (the loader runs an exported __wasm_call_ctors when it installs the
    # module, before the DLLs the constructors call are loaded)
    subprocess.call([sys.executable, here + '/renexp.py', os.path.join(tmp, name + '.wasm'), '__wasm_call_ctors', '__gexw_ctors'])
    if strip and os.environ.get('GEXCC_WIN_STRIP') != '0':
        # (the names a trap backtrace would use: a build without -s keeps them;
        # the table slots' stay, which the loader binds by)
        r = subprocess.call([sys.executable, here + '/stripnames.py', os.path.join(tmp, name + '.wasm'), os.path.join(tmp, name + '.wasm.s')])
        if r: sys.exit(r)
        os.replace(os.path.join(tmp, name + '.wasm.s'), os.path.join(tmp, name + '.wasm'))
    shutil.copy(os.path.join(tmp, name + '.wasm'), os.path.join(odir, name + '.wasm'))
    if os.path.exists(os.path.join(tmp, name + '.wasm.undefined')):
        shutil.copy(os.path.join(tmp, name + '.wasm.undefined'), os.path.join(odir, name + '.wasm.undefined'))
    spec = None
    kind = ['--exe'] + (['--subsystem', subsys] if subsys else [])
    if shared:
        spec = os.path.join(tmp, name.rsplit('.', 1)[0] + '.spec')
        with open(spec, 'w') as f:
            for line in open(so + '.exports'):
                n, t = line.split()[:2]
                if n.startswith('__wine') or n.startswith('__gexw'):
                    continue
                f.write('@ extern %s\n' % n if t == 'data' else '@ cdecl %s()\n' % n)
        kind = ['--dll', '-E', spec]
    skel = os.path.abspath(out)
    r = subprocess.call([T + '/tools/winebuild/winebuild', '-w', '-b', 'wasm64-unknown-linux-musl',
                         '--cc-cmd=' + W + '/wcc64 -D_WIN32'] + kind + ['--fake-module', '--wasm-shared', name + '.wasm',
                         '-F', name, '-o', skel] + [os.path.abspath(r) for r in ress], cwd=tmp)  # (its temporary dir in its cwd)
    if r: sys.exit(r)
    r = subprocess.call([sys.executable, here + '/peimports.py', skel, os.path.join(odir, name + '.wasm.undefined')],
                        stdout=subprocess.DEVNULL)
    sys.exit(r)
finally:
    if not os.environ.get('GEXCC_WIN_KEEP'): shutil.rmtree(tmp, True)
    else: print('gexcc-win: kept', tmp, file=sys.stderr)
