#!/usr/bin/env python3
# -----------------------------------------------------------------------------
# Copyright (c) 2014-2023, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
# -----------------------------------------------------------------------------
"""
Bootloader building script.
"""

import os
import platform
import sys
import re
import sysconfig
from waflib.Configure import conf
from waflib import Logs, Utils, Options
from waflib.Build import BuildContext, InstallContext

# waf does not work unless it is ran from in this folder, so it is safe to assume the current working directory here.
sys.path.insert(0, os.path.realpath("../PyInstaller/"))
import _shared_with_waf  # noqa: E402

# The following two variables are used by the target "waf dist"
VERSION = 'nodist'
APPNAME = 'PyInstallerBootloader'

# These variables are mandatory ('/' are converted automatically)
top = '.'
out = 'build'

# Build variants of bootloader.
# PyInstaller provides debug/release bootloaders and console/windowed variants. Each variant has a different exe name.
variants = {
    'debug': 'run_d',
    'debugw': 'runw_d',
    'release': 'run',
    'releasew': 'runw',
}

# PyInstaller only knows platform.system(), so we need to map waf's DEST_OS to these values.
DESTOS_TO_SYSTEM = {
    'linux': 'Linux',
    'freebsd': 'FreeBSD',
    'openbsd': 'OpenBSD',
    'win32': 'Windows',
    'darwin': 'Darwin',
    'sunos': platform.system(),  # FIXME: inhibits cross-compile
    'hpux': 'HP-UX',
    'aix': 'AIX',
    'cygwin': 'Cygwin',
}

# Map from platform.system() to waf's DEST_OS
SYSTEM_TO_BUILDOS = {
    'Linux': 'linux',
    'FreeBSD': 'freebsd',
    'Windows': 'win32',
    'Darwin': 'darwin',
    'Solaris': 'sunos',
    'SunOS': 'sunos',
    'HP-UX': 'hpux',
    'AIX': 'aix',
    'Cygwin': 'cygwin',
}

# waf's name of the system we are building on
if sys.platform == 'cygwin':
    # Cygwin needs special handling, because platform.system() contains identifiers such as MSYS_NT-10.0-19042 and
    # CYGWIN_NT-10.0-19042 that do not fit PyInstaller's OS naming scheme, nor waf's DEST_OS, which is 'cygwin'.
    BUILD_OS = SYSTEM_TO_BUILDOS.get('Cygwin')
else:
    BUILD_OS = SYSTEM_TO_BUILDOS.get(platform.system(), platform.system())

is_cross = None


def machine(ctx):
    """
    Determine path to bootloader based on C compiler target.
    """
    return _shared_with_waf._pyi_machine(ctx.env.DEST_CPU, DESTOS_TO_SYSTEM[ctx.env.DEST_OS])


def is_msvc_target(ctx):
    """
    clang may be compiled to use either MSVC's headers or mingw's. If it's the former, it inherits some of MSVC's
    quirks.
    """
    if ctx.env.CC_NAME == 'clang':
        from subprocess import run, PIPE
        spec = run(ctx.env.CC + ["-v"], stderr=PIPE).stderr
        return re.search(rb'Target: .*msvc', spec)
    return ctx.env.CC_NAME == 'msvc'


@conf
def is_musl(ctx):
    """Return true if the C compiler targets Linux using musl libc. Always false on non-Linux."""
    if ctx.env.DEST_OS != 'linux':
        return False
    from subprocess import run, PIPE
    spec = run(ctx.env.CC + ["-v"], stderr=PIPE).stderr
    return re.search(rb'--target=\S+-musl', spec) or re.search(rb'Target: .*musl', spec)


def find_program_next_to_cc(ctx, name, var):
    # Tries to find a program in the same directory as cc or on the normal PATH.
    environ = getattr(ctx, 'environ', os.environ)
    path = [os.path.dirname(ctx.env.CC[0])]
    path.extend(environ.get('PATH', '').split(os.pathsep))
    ctx.find_program(name, var=var, path_list=path)


def options(ctx):
    ctx.load('compiler_c')

    ctx.add_option(
        '--debug',
        action='store_true',
        help='Include debugging info for GDB.',
        default=False,
        dest='debug',
    )
    ctx.add_option(
        '--clang',
        action='store_true',
        help='Try to find clang C compiler instead of gcc.',
        default=False,
        dest='clang',
    )
    ctx.add_option(
        '--gcc',
        action='store_true',
        help='Try to find GNU C compiler.',
        default=False,
        dest='gcc',
    )
    ctx.add_option(
        '--target-arch',
        action='store',
        help='Target architecture format (32bit, 64bit). This option allows to build 32-bit bootloader with a 64-bit '
        'compiler and a 64-bit Python.',
        default=None,
        choices=('32bit', '64bit', '64bit-arm'),
        dest='target_arch',
    )
    ctx.add_option(
        '--static-zlib',
        action='store_true',
        help='Statically compile zlib into the bootloaders rather than dynamically linking to the system-wide copy. '
        'This is always done on Windows.',
        default=False,
    )
    ctx.add_option(
        '--tests',
        action='store_true',
        help='Enable cmocka-based tests if cmocka library is found. The tests are disabled by default.',
        default=False,
        dest='enable_tests',
    )
    ctx.add_option(
        '--no-tests',
        action='store_false',
        help='Disable cmocka-based tests, even if cmocka library is found. The tests are disabled by default.',
        default=False,
        dest='enable_tests',
    )

    grp = ctx.add_option_group('macOS-specific options', 'These options have effect only on macOS.')
    grp.add_option(
        '--universal2',
        action='store_true',
        help='When building for 64-bit platform, build universal2 fat binary (x86_64, arm64). This is the default.',
        default=None,
        dest='macos_universal2',
    )
    grp.add_option(
        '--no-universal2',
        action='store_false',
        help="When building for 64-bit platform, build a thin binary. Allows building with older toolchains that do "
        "not support universal2 binaries. The resultant architecture is determined by the compiler's default, which "
        "is usually to build a native executable. This may be overridden by setting the CC environment variable, e.g., "
        "CC='clang -arch=arm64' python waf all",
        default=None,
        dest='macos_universal2',
    )


def check_sizeof_pointer(ctx):
    def check(type, expected):
        # test code taken from autoconf resp. Scons: this is a pretty clever hack to find that a type is of a given
        # size using only compilation. This speeds things up quite a bit compared to straightforward code actually
        # running the code. Plus: This works cross :-)
        fragment = '''
        int main() {
            static int test_array[1 - 2 * !(sizeof(%s) == %d)];
            test_array[0] = 0;
            return 0;
        }''' % (type, expected)
        return ctx.check_cc(fragment=fragment, execute=False, mandatory=False)

    ctx.start_msg("Checking size of pointer")
    for size in (4, 8):
        if check("void *", size):
            break
    else:
        ctx.end_msg(False)
        ctx.fatal(
            "Could not determine pointer size. Use `--target-arch' to manually set the pointer size (32bit or 64bit)."
        )
    ctx.end_msg(size)
    return size


@conf
def detect_arch(ctx):
    """
    Handle --target-arch options, or use the same architecture as the compiler.
    """
    try:
        system = DESTOS_TO_SYSTEM[ctx.env.DEST_OS]
    except KeyError:
        ctx.fatal('Unrecognized target system: %s' % ctx.env.DEST_OS)

    # Get arch values either from CLI or detect it.
    if ctx.options.target_arch:
        arch = ctx.options.target_arch
        ctx.msg('Platform', '%s-%s manually chosen' % (system, arch))
        ctx.env.ARCH_FLAGS_REQUIRED = True
        if arch == '64bit-arm':
            arch = '64bit'
            ctx.env.DEST_CPU = 'arm64'
    else:
        # PyInstaller uses the result of platform.architecture() to determine the bits and this is testing the pointer
        # size (via module struct). We do the same here.
        arch = "%sbit" % (8 * check_sizeof_pointer(ctx))
        _machine = machine(ctx)
        if _machine is not None:
            plat = '%s-%s-%s detected based on compiler' % (system, arch, _machine)
        else:
            plat = '%s-%s detected based on compiler' % (system, arch)
        ctx.msg('Platform', plat)
        ctx.env.ARCH_FLAGS_REQUIRED = False
    if arch not in ('32bit', '64bit'):
        ctx.fatal('Unrecognized target architecture: %s' % arch)

    # Pass return values as environment variables.
    ctx.env.PYI_ARCH = arch  # '32bit' or '64bit'
    ctx.env.PYI_SYSTEM = system


@conf
def set_arch_flags(ctx):
    """
    Set proper architecture flag (32-bit or 64-bit), cflags for compiler, and CPU target for compiler.
    """
    def check_arch_cflag(cflag32, cflag64):
        cflag = cflag32 if ctx.env.PYI_ARCH == '32bit' else cflag64
        # features='c' -> only compile, do not link
        if ctx.check_cc(cflags=cflag, features='c', mandatory=ctx.env.ARCH_FLAGS_REQUIRED):
            ctx.env.append_value('CFLAGS', cflag)
        if ctx.check_cc(linkflags=cflag, mandatory=ctx.env.ARCH_FLAGS_REQUIRED):
            ctx.env.append_value('LINKFLAGS', cflag)

    if ctx.env.DEST_OS == 'win32' and ctx.env.CC_NAME == 'msvc':
        # Set msvc linkflags based on architecture.
        if machine(ctx) == 'arm':
            ctx.env.append_value('LINKFLAGS', '/MACHINE:ARM64')
        elif ctx.env.PYI_ARCH == '32bit':
            ctx.env.append_value('LINKFLAGS', '/MACHINE:X86')
            # Set LARGE_ADDRESS_AWARE_FLAG to True. On Windows, this allows 32-bit apps to use 4GB of memory.
            ctx.env.append_value('LINKFLAGS', '/LARGEADDRESSAWARE')
        elif ctx.env.PYI_ARCH == '64bit':
            ctx.env.append_value('LINKFLAGS', '/MACHINE:X64')
        ctx.env['MSVC_TARGETS'] = _msvc_target(ctx)

        # Enable 64-bit porting warnings and other warnings.
        ctx.env.append_value('CFLAGS', '/W3')
        # Treat compiler warnings as errors.
        ctx.env.append_value('CFLAGS', '/WX')
        # Disable warnings about unrecognized pragmas.
        ctx.env.append_value('CFLAGS', '/wd4068')
        # Disable warnings about deprecated POSIX function names.
        ctx.env.append_value('CFLAGS', '/D_CRT_NONSTDC_NO_WARNINGS')
        # Disable warnings about unsafe CRT functions.
        ctx.env.append_value('CFLAGS', '/D_CRT_SECURE_NO_WARNINGS')
        # We use SEH exceptions in winmain.c; make sure they are activated.
        ctx.env.append_value('CFLAGS', '/EHa')
        # Set the PE checksum on resulting binary.
        ctx.env.append_value('LINKFLAGS', '/RELEASE')
        # Treat linker warnings as errors
        ctx.env.append_value('LINKFLAGS', '/WX')

    # Ensure proper architecture flags on macOS.
    elif ctx.env.DEST_OS == 'darwin':
        # Default compiler on macOS is Clang, which does not have '-m32' and '-m64' flags.
        if ctx.env.PYI_ARCH == '32bit':
            mac_arch = ['-arch', 'i386']
        else:
            UNIVERSAL2_FLAGS = ['-arch', 'x86_64', '-arch', 'arm64']
            if ctx.options.macos_universal2 in (None, True):
                # Check for universal2 support if set to auto-detect (neither --universal2 nor --no-universal2 is
                # provided), or if universal2 is explicitly enabled.
                supported = ctx.check_cc(
                    cflags=UNIVERSAL2_FLAGS,
                    linkflags=UNIVERSAL2_FLAGS,
                    features='c cprogram',
                    msg='Checking for universal2 support',
                    mandatory=ctx.options.macos_universal2
                )
                ctx.options.macos_universal2 = supported
            if ctx.options.macos_universal2:
                mac_arch = UNIVERSAL2_FLAGS
            else:
                # Default to whatever the compiler is configured to build.
                mac_arch = []
        ctx.env.append_value('CFLAGS', mac_arch)
        ctx.env.append_value('LINKFLAGS', mac_arch)

    # AIX specific flags
    elif ctx.env.DEST_OS == 'aix':
        if ctx.env.CC_NAME == 'gcc':
            check_arch_cflag('-maix32', '-maix64')
        else:
            # We are using AIX/xlc compiler
            check_arch_cflag('-q32', '-q64')

    elif ctx.env.DEST_OS == 'sunos':
        if ctx.env.CC_NAME == 'gcc':
            check_arch_cflag('-m32', '-m64')
        else:
            # We use SUNWpro C compiler
            check_arch_cflag('-xarch=generic', '-xarch=v9')

    elif ctx.env.DEST_OS == 'hpux':
        if ctx.env.CC_NAME == 'gcc':
            check_arch_cflag('-milp32', '-mlp64')
        else:
            # We use xlc compiler
            pass

    # Other compiler - not msvc.
    else:
        if machine(ctx) == 'sw_64':
            # The gcc has no '-m64' option under sw64 machine, but the __x86_64__ macro needs to be defined.
            ctx.env.append_value('CCDEFINES', '__x86_64__')
        # This ensures proper compilation with 64-bit gcc and 32-bit Python or vice versa or with manually
        # chosen --target-arch. Option -m32/-m64 has to be passed to cflags and linkflages.
        else:
            check_arch_cflag('-m32', '-m64')
        if ctx.env.PYI_ARCH == '32bit' and ctx.env.DEST_OS == 'win32':
            # Set LARGE_ADDRESS_AWARE_FLAG to True. On Windows, this allows 32-bit apps to use 4GB of memory.
            # TODO: verify if this option being as default might cause any side effects.
            ctx.env.append_value('LINKFLAGS', '-Wl,--large-address-aware')


# A bare minimum zlib program. Testing zlib-dev availability requires explicitly using libz-provided symbols instead of
# just compiling 'include <zlib.h>' with `-lz`, because while zlib's development headers may be installed, the library
# itself may not be linkable (e.g., OpenWRT strips their binaries using `sstrip`).
ZLIB_TEST_FRAGMENT = r"""
#include <stdio.h>
#include <zlib.h>

int main ()
{
    printf("Zlib: %s\n", zlibVersion());
    return 0;
}
"""


def _msvc_target(ctx):
    if getattr(ctx.options, "msvc_targets", False):
        return re.findall(r"[^, ]+", ctx.options.msvc_targets)

    try:
        _msvc_names = {"AMD64": "amd64", "x86": "x86", "ARM64": "arm64"}
        host = _msvc_names[os.environ.get("PROCESSOR_ARCHITECTURE", platform.machine())]
    except KeyError:
        return []

    if ctx.options.target_arch == '32bit':
        target = 'x86'
    elif ctx.options.target_arch == '64bit':
        target = 'x64'
    elif ctx.options.target_arch == '64bit-arm':
        target = 'arm64'
    else:
        target = "x64" if host == "amd64" else host

    if host == 'x86':
        if target == 'x86':
            return ['x86']
        elif target == 'x64':
            return ['x86_amd64']
        else:  # arm64
            return ['x86_arm64']
    elif host == 'amd64':
        if target == 'x86':
            return ['amd64_x86', 'x86']
        elif target == 'x64':
            return ['x64', 'x86_amd64']
        else:  # arm64
            return ['amd64_arm64', 'x86_arm64']
    else:  # arm64
        if target == 'x86':
            return ['arm64_x86', 'x86', 'amd64_x86']
        elif target == 'x64':
            return ['arm64_amd64', 'x64', 'x86_amd64']
        else:  # arm64
            return ['arm64']


def configure(ctx):
    ctx.msg('Python Version', sys.version.replace(os.linesep, ''))
    # For MSVC the target arch must have already been set when the compiler is searched.
    ctx.env['MSVC_TARGETS'] = _msvc_target(ctx)
    ctx.msg('MSVC target(s)', ctx.env['MSVC_TARGETS'])

    # ** C compiler **

    # Allow to use Clang if preferred.
    if ctx.options.clang:
        ctx.load('clang')
    # Allow to use gcc if preferred.
    elif ctx.options.gcc:
        ctx.load('gcc')
    else:
        ctx.load('compiler_c')  # Any available C compiler.

    global is_cross
    is_cross = (BUILD_OS != ctx.env.DEST_OS)

    if is_cross:
        ctx.msg('System', 'Assuming cross-compilation for %s' % DESTOS_TO_SYSTEM[ctx.env.DEST_OS])

        if ctx.env.DEST_OS in ('hpux', 'sunos'):
            # For SunOS and HP-UX we determine some settings from Python's sysconfig. For cross-compiling, somebody
            # needs to implement options to overwrite these values as they may be wrong.
            # For SunOS/Solaris mapping DEST_OS to system is not yet known.
            ctx.fatal(
                'Cross-compiling for target %s is not yet supported. If you want this feature, please help '
                'implementing it. See the wscript file for details.' % ctx.env.DEST_OS
            )

    if ctx.env.DEST_OS == 'darwin':
        # macOS 10.13 is the oldest version supported by Apple. According to macOS docs, setting the
        # MACOSX_DEPLOYMENT_TARGET environment variable is equivalent to the following gcc option:
        # -mmacosx-version-min=10.13
        #
        # MACOSX_DEPLOYMENT_TARGET must be set before the compiler toolchain is used for the first time (i.e., the
        # check_sizeof_pointer() call made by the detect_arch() call below), otherwise it will be automatically
        # set to the toolchain's default value.
        if not os.environ.get('MACOSX_DEPLOYMENT_TARGET'):
            ctx.msg('MacOS deployment target', 'Setting to 10.13')
            os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.13'
        else:
            ctx.msg('MacOS deployment target', 'Already set - %s' % (os.environ.get('MACOSX_DEPLOYMENT_TARGET')))

    # Detect architecture after completing compiler search
    ctx.detect_arch()

    # Set proper architecture and CPU for C compiler
    ctx.set_arch_flags()

    # ** Other Tools **

    if ctx.env.DEST_OS == 'win32':
        # Do not embed manifest file when using MSVC (Visual Studio). Manifest file will be added in the phase of
        # packaging python application by PyInstaller.
        ctx.env.MSVC_MANIFEST = False

    # ** C Compiler optimizations **
    # TODO Set proper optimization flags for MSVC (Visual Studio).

    if ctx.options.debug:
        if ctx.env.DEST_OS == 'win32' and ctx.env.CC_NAME == 'msvc':
            # Include information for debugging in MSVC/msdebug
            ctx.env.append_value('CFLAGS', '/Z7')
            ctx.env.append_value('CFLAGS', '/Od')
            ctx.env.append_value('LINKFLAGS', '/DEBUG')
        else:
            # Include gcc debugging information for debugging in GDB.
            ctx.env.append_value('CFLAGS', '-g')
    else:
        if ctx.env.DEST_OS != 'sunos':
            ctx.env.append_value('CFLAGS', '-O2')
        else:
            # Solaris SUN CC doesn't support '-O2' flag
            ctx.env.append_value('CFLAGS', '-O')

    if ctx.env.CC_NAME == 'gcc':
        # These flags are specific to gcc!
        # Turn on all warnings to improve code quality and avoid errors. Unused variables and unused functions are still
        # accepted to avoid even more conditional code. If you are ever tempted to change this, review the commit
        # history of this place first.
        ctx.env.append_value(
            'CFLAGS', [
                '-Wall',
                '-Werror',
                '-Wno-error=unused-variable',
                '-Wno-error=unused-function',
            ]
        )

        # -Wno-error=unused-but-set-variable is not available on old gcc versions (e.g., 4.4.3)
        if ctx.check_cc(cflags='-Wno-error=unused-but-set-variable', execute=False, mandatory=False):
            ctx.env.append_value('CFLAGS', ['-Wno-error=unused-but-set-variable'])
    elif ctx.env.CC_NAME == 'clang':
        # For clang, also enable extra warnings and treat them as errors. Similarly to gcc, do not treat warnings about
        # unused variables and unused functions as errors.
        ctx.env.append_value(
            'CFLAGS', [
                '-Wall',
                '-Werror',
                '-Wno-error=unused-variable',
                '-Wno-error=unused-function',
            ]
        )

    # ** Defines, includes **

    if not ctx.env.DEST_OS == 'win32':
        # Defines common for Unix and Unix-like platforms. For details see:
        #   http://man.he.net/man7/feature_test_macros
        ctx.env.append_value('DEFINES', '_REENTRANT')

        # mkdtemp() is available only if _BSD_SOURCE is defined.
        ctx.env.append_value('DEFINES', '_BSD_SOURCE')

        if ctx.env.DEST_OS == 'linux':
            # GCC 5.x complains about _BSD_SOURCE under Linux:
            #     _BSD_SOURCE and _SVID_SOURCE are deprecated, use _DEFAULT_SOURCE
            ctx.env.append_value('DEFINES', '_DEFAULT_SOURCE')

            # TODO: What other platforms support _FORTIFY_SOURCE macro? macOS?
            # TODO: macOS's CLang appears to support this macro as well. See:
            # https://marc.info/?l=cfe-dev&m=122032133830183

            # For security, enable the _FORTIFY_SOURCE macro detecting buffer overflows in various string and memory
            # manipulation functions.
            if ctx.options.debug:
                ctx.env.append_value('CFLAGS', '-U_FORTIFY_SOURCE')
            elif ctx.env.CC_NAME == 'gcc':
                # Undefine this macro if already defined by default to avoid "macro redefinition" errors.
                ctx.env.append_value('CFLAGS', '-U_FORTIFY_SOURCE')

                # Define this macro.
                ctx.env.append_value('DEFINES', '_FORTIFY_SOURCE=2')
        # On macOS, mkdtemp() is available only with _DARWIN_C_SOURCE.
        elif ctx.env.DEST_OS == 'darwin':
            ctx.env.append_value('DEFINES', '_DARWIN_C_SOURCE')

        # Determine which semaphore API to use for syncing onefile parent and child process on non-Windows platforms;
        # this is required to ensure consistent signal forwarding behavior. We prefer to use POSIX semaphores; however,
        # they are unavailable on macOS, where we need to fall back to old SysV semaphore API.
        semaphore_api = None
        if not ctx.env.DEST_OS == 'darwin':
            if ctx.check(header_name='semaphore.h', mandatory=False):
                semaphore_api = 'posix'
        if not semaphore_api:
            if ctx.check(header_name='sys/sem.h', mandatory=False):
                semaphore_api = 'sysv'
        if not semaphore_api:
            semaphore_api = 'none'  # For the message

        ctx.msg("Using semaphore API", semaphore_api)
        if semaphore_api == 'sysv':
            ctx.env.append_value('DEFINES', 'PYI_USE_SYSV_SEMAPHORE')
        elif semaphore_api == 'posix':
            ctx.env.append_value('DEFINES', 'PYI_USE_POSIX_SEMAPHORE')

    if ctx.env.DEST_OS == 'win32':
        ctx.env.append_value('DEFINES', 'WIN32')
        ctx.env.append_value('CPPPATH', '../zlib')
        # Set minimum feature level for Windows headers to Windows 7.
        # As per MSDN: "If you define NTDDI_VERSION, you must also define _WIN32_WINNT."
        # https://docs.microsoft.com/en-us/windows/win32/winprog/using-the-windows-headers#macros-for-conditional-declarations
        ctx.env.append_value('DEFINES', 'NTDDI_VERSION=0x06010000')
        ctx.env.append_value('DEFINES', '_WIN32_WINNT=0x0601')

    elif ctx.env.DEST_OS == 'sunos':
        ctx.env.append_value('DEFINES', 'SUNOS')
        if ctx.env.CC_NAME == 'gcc':
            # On Solaris using gcc, the linker options for shared and static libraries are slightly different from
            # other platforms.
            ctx.env['SHLIB_MARKER'] = '-Wl,-Bdynamic'
            ctx.env['STLIB_MARKER'] = '-Wl,-Bstatic'
            # On Solaris using gcc, the compiler needs to be gnu99
            ctx.env.append_value('CFLAGS', '-std=gnu99')

    elif ctx.env.DEST_OS == 'aix':
        ctx.env.append_value('DEFINES', 'AIX')
        # On AIX some APIs are restricted if _ALL_SOURCE is not defined. In the case of PyInstaller, we need the AIX
        # specific flag RTLD_MEMBER for dlopen(), which is used to load a shared object from a library archive.
        # We need to load the Python library like this:
        #  dlopen("libpython2.7.a(libpython2.7.so)", RTLD_MEMBER)
        ctx.env.append_value('DEFINES', '_ALL_SOURCE')

        # On AIX using gcc, the linker options for shared and static libraries are slightly different from
        # other platforms.
        ctx.env['SHLIB_MARKER'] = '-Wl,-bdynamic'
        ctx.env['STLIB_MARKER'] = '-Wl,-bstatic'

    elif ctx.env.DEST_OS == 'hpux':
        ctx.env.append_value('DEFINES', 'HPUX')
        if ctx.env.CC_NAME == 'gcc':
            if ctx.env.PYI_ARCH == '32bit':
                ctx.env.append_value('LIBPATH', '/usr/local/lib/hpux32')
                ctx.env.append_value('STATICLIBPATH', '/usr/local/lib/hpux32')
            else:
                ctx.env.append_value('LIBPATH', '/usr/local/lib/hpux64')
                ctx.env.append_value('STATICLIBPATH', '/usr/local/lib/hpux64')

    # ** Libraries **

    if ctx.env.DEST_OS == 'win32':
        if ctx.env.CC_NAME == 'msvc':
            ctx.check_libs_msvc('user32 comctl32 kernel32 advapi32 gdi32', mandatory=True)
        else:
            ctx.check_cc(lib='user32', mandatory=True)
            ctx.check_cc(lib='comctl32', mandatory=True)
            ctx.check_cc(lib='kernel32', mandatory=True)
            ctx.check_cc(lib='advapi32', mandatory=True)
            ctx.check_cc(lib='gdi32', mandatory=True)
    else:
        # On most platforms, the libdl symbols are moved to libc. The POSIX
        # standard states that libdl should always be linkable against, even if
        # it's empty, but not all provide appropriate libdl.a stub archives.
        # Treat it as optional.
        ctx.check_cc(lib='dl', mandatory=False)
        # libdl to be thread-safe requires libpthread! Assume it must be available if libdl is available.
        ctx.check_cc(lib='pthread', mandatory=bool(ctx.env.LIB_DL))
        if ctx.env.DEST_OS == 'freebsd' and sysconfig.get_config_var('HAVE_PTHREAD_H'):
            # On FreeBSD if python has threads: libthr needs to be loaded in the main process, so the bootloader needs
            # to be link to thr.
            ctx.check_cc(lib='thr', mandatory=True)
        elif ctx.env.DEST_OS == 'hpux' and sysconfig.get_config_var('HAVE_PTHREAD_H'):
            ctx.check_cc(lib='pthread', mandatory=True)
        ctx.check_cc(lib='m', mandatory=True)

        # Opting out of dynamically linked zlib can be done either with the --static-zlib option or with the
        # PYI_STATIC_ZLIB=1 environment variable.
        static_zlib = bool(int(os.environ.get("PYI_STATIC_ZLIB", '0'))) or ctx.options.static_zlib
        if static_zlib:
            # This serves as a signal to later on when the C code gets compiled.
            ctx.env.LIB_Z = None
        else:
            ctx.check_cc(lib='z', mandatory=False, uselib_store='Z', fragment=ZLIB_TEST_FRAGMENT)
            if not ctx.env.LIB_Z:
                ctx.fatal(
                    "The zlib development package is either missing or the shared library cannot be linked against. "
                    "For security (and marginally better filesize), you should install the zlib-dev or zlib-devel "
                    "packages with your system package manager, and try again. If you cannot do this (for example, "
                    "distributions such as OpenWRT use sstrip on libraries, making linking impossible), then either "
                    "use the --static-zlib option or set the PYI_STATIC_ZLIB=1 environment variable. If you are "
                    "installing directly with pip, then use the environment variable."
                )

    ctx.recurse("tests")

    # ** Functions and headers **

    # Check for presence of stdbool.h
    ctx.check(header_name='stdbool.h', mandatory=False)

    # The old ``function_name`` parameter to ``check_cc`` is no longer supported. This code is based on old waf
    # source at
    # https://gitlab.com/ita1024/waf/commit/62fe305d04ed37b1be1a3327a74b2fee6c458634#255b2344e5268e6a34bedd2f8c4680798344fec7.
    SNIP_FUNCTION = '''
    #include <%s>

    int main(int argc, char **argv) {
        void (*p)();

        (void)argc; (void)argv;
        p=(void(*)())(%s);
        return !p;
    }
'''
    # OS support for these functions varies.
    for header, function_name in (
        ('stdlib.h', 'unsetenv'),
        # Most systems have `mkdtemp` defined in `stdlib.h`; except for macOS, where it is defined in `unistd.h`.
        ('unistd.h' if ctx.env.DEST_OS == 'darwin' else 'stdlib.h', 'mkdtemp'),
        ('libgen.h', 'dirname'),
        ('libgen.h', 'basename'),
    ):
        ctx.check(
            fragment=SNIP_FUNCTION % (header, function_name),
            mandatory=False,
            define_name=ctx.have_define(function_name),
            msg='Checking for function %s' % function_name
        )

    # ** CFLAGS **

    if ctx.env.DEST_OS == 'win32':
        if ctx.env.CC_NAME == 'msvc':
            # Use Unicode entry point wmain/wWinMain and wchar_t WinAPI
            ctx.env.append_value('CFLAGS', '-DUNICODE')
            ctx.env.append_value('CFLAGS', '-D_UNICODE')

            # Specify CONSOLE subsystem, without major/minor version. At the time of writing, the compiler's default
            # 6.00 (x86, x64) and 6.02 (ARM) should be fine for our purposes.
            ctx.env.append_value('LINKFLAGS', '/SUBSYSTEM:CONSOLE')

            # Enable support for Control Flow Guard
            # https://docs.microsoft.com/en-us/windows/win32/secbp/control-flow-guard
            ctx.env.append_value('CFLAGS', '/guard:cf')
            ctx.env.append_value('LINKFLAGS', '/guard:cf')

            # Increase the executable's stack size from the default 1 MiB (0x100000) to 2 MB (0x1E8480) to match
            # the stack size under the Python interpreter.
            # https://github.com/python/cpython/blob/a9e0b2b49374df91c40fe409508cfcdc6332450e/PCbuild/python.vcxproj#L97
            ctx.env.append_value('LINKFLAGS', '/stack:2000000')
        else:
            # Use Visual C++ compatible alignment
            ctx.env.append_value('CFLAGS', '-mms-bitfields')

            # Define UNICODE and _UNICODE for wchar_t WinAPI
            ctx.env.append_value('CFLAGS', '-municode')

            # NOTE: the following two defines are added on the off-chance
            # that we are building with clang + MSVC. For pure MSVC, they
            # are specified in `set_arch_flags()`.
            # Disable warnings about deprecated POSIX function names.
            ctx.env.append_value('CFLAGS', '-D_CRT_NONSTDC_NO_WARNINGS')
            # Disable warnings about unsafe CRT functions.
            ctx.env.append_value('CFLAGS', '-D_CRT_SECURE_NO_WARNINGS')

            # Use Unicode entry point wmain/wWinMain
            ctx.env.append_value('LINKFLAGS', '-municode')

            # Set the executable's stack size to 2 MB (0x1E8480) to match the stack size under the Python interpreter.
            # The MSYS2/MINGW64 gcc toolchain seems to use 2 MiB (0x200000) stack by default, so we are slightly
            # decreasing the stack size here, in order to keep it synchronized with the python itself and with the
            # MSVC-compiled bootloaders.
            if not is_msvc_target(ctx):
                ctx.env.append_value('LINKFLAGS', '-Wl,--stack,2000000')

    # On linux link only with needed libraries.
    # On some platforms (macOS, Solaris, AIX), -Wl,--as-needed is detected as supported during configuration,
    # but fails during build. So use it only with linux.
    if ctx.env.DEST_OS == 'linux' and ctx.check_cc(linkflags='-Wl,--as-needed', mandatory=False):
        ctx.env.append_value('LINKFLAGS', '-Wl,--as-needed')

    if not is_msvc_target(ctx):
        # This tool allows reducing the size of executables.
        find_program_next_to_cc(ctx, 'strip', var='STRIP')
        ctx.load('strip', tooldir='tools')

    def windowed(name, baseenv):
        """Setup windowed environment based on `baseenv`."""
        ctx.setenv(name, baseenv)  # Inherit from `baseenv`.
        ctx.env.append_value('DEFINES', 'WINDOWED')

        if ctx.env.DEST_OS == 'win32':
            if ctx.env.CC_NAME != 'msvc':
                # For MinGW disable console window on Windows - MinGW option.
                # This option is for linker, and thus needs to be specified under
                # LINKFLAGS.
                #
                # While gcc does not seem to mind having it under CFLAGS
                # as well, clang seems to complain about it:
                # `clang: warning: argument unused during compilation:
                # '-mwindows' [-Wunused-command-line-argument]`. Recent
                # versions, when coupled with MSVC linker, raise an error:
                # `clang: error: unsupported option '-mwindows' for target
                # 'x86_64-pc-windows-msvc'`.
                #
                # NOTE: under both contemporary gcc and clang, the
                # subsystem seems automatically inferred from available
                # entry-point, so explicitly specifiying it here is likely
                # redundant.
                ctx.env.append_value('LINKFLAGS', '-mwindows')
            else:
                # Remove any /SUBSYSTEM flag already present in LINKFLAGS
                _link_flags = ctx.env._get_list_value_for_modification('LINKFLAGS')
                _subsystem = [x for x in _link_flags if x.startswith('/SUBSYSTEM:')]
                for parameter in _subsystem:
                    _link_flags.remove(parameter)

                # Specify WINDOWS subsystem, without major/minor version. At the time of writing, the compiler's
                # default 6.00 (x86, x64) and 6.02 (ARM) should be fine for our purposes.
                ctx.env.append_value('LINKFLAGS', '/SUBSYSTEM:WINDOWS')
        elif ctx.env.DEST_OS == 'darwin':
            # To support catching AppleEvents and running as ordinary macOS GUI app, we have to link against the
            # Carbon framework. This linkage only needs to be there for the windowed bootloaders.
            ctx.env.append_value('LINKFLAGS', '-framework')
            ctx.env.append_value('LINKFLAGS', 'Carbon')
            # TODO Do we need to link with this framework?
            # conf.env.append_value('LINKFLAGS', '-framework')
            # conf.env.append_value('LINKFLAGS', 'ApplicationServices')

    # ** DEBUG and RELEASE environments **
    basic_env = ctx.env

    # * Setup DEBUG environment *
    ctx.setenv('debug', basic_env)  # Ensure env contains shared values.
    debug_env = ctx.env
    # This defines enable verbose console output of the bootloader.
    ctx.env.append_value('DEFINES', ['LAUNCH_DEBUG'])
    ctx.env.append_value('DEFINES', 'NDEBUG')

    # * Setup windowed DEBUG environment *
    windowed('debugw', debug_env)

    # * Setup RELEASE environment *
    ctx.setenv('release', basic_env)  # Ensure env contains shared values.
    release_env = ctx.env
    ctx.env.append_value('DEFINES', 'NDEBUG')

    # * Setup windowed RELEASE environment *
    windowed('releasew', release_env)


# TODO Use 'strip' command to decrease the size of compiled bootloaders.
def build(ctx):
    if not ctx.variant:
        ctx.fatal('Call "python waf all" to compile all bootloaders.')

    exe_name = variants[ctx.variant]

    install_path = os.path.join(os.getcwd(), '../PyInstaller/bootloader', ctx.env.PYI_SYSTEM + "-" + ctx.env.PYI_ARCH)
    install_path = os.path.normpath(install_path)

    if machine(ctx) and ctx.env.DEST_OS != 'darwin':
        install_path += '-' + machine(ctx)

    if ctx.is_musl():
        install_path += "-musl"

    if not ctx.env.LIB_Z:
        # If the operating system does not provide zlib, build our own. The configure phase defines whether or not zlib
        # is mandatory for the given platform.
        ctx.objects(source=ctx.path.ant_glob('zlib/*.c'), target='STATIC_ZLIB', includes='zlib')

    # By default strip final executables to make them smaller.
    features = 'strip'
    if is_msvc_target(ctx):
        # Do not strip bootloaders when using MSVC.
        features = ''

    ctx.objects(source=ctx.path.ant_glob('src/*.c', excl="src/main.c"), includes='src windows zlib', target="OBJECTS")

    ctx.env.link_with_dynlibs = []
    ctx.env.link_with_staticlibs = []
    if ctx.env.DEST_OS == 'win32':
        # On Windows we need to link library zlib statically.
        ctx.program(
            source=['src/main.c'],
            target=exe_name,
            install_path=install_path,
            use='OBJECTS USER32 COMCTL32 KERNEL32 ADVAPI32 GDI32 Z STATIC_ZLIB',
            includes='src windows zlib',
            features=features
        )
    else:
        # Linux, Darwin (macOS), ...
        # Only the libs found will actually be used, so it's safe to list all here. The decision if a lib is required
        # for a specific platform is made in the configure phase.
        libs = [
            'DL',
            'M',  # math
            'Z',  # zlib
            'PTHREAD',  # important! needs for libdl to be thread-safe
            'THR',  # may be used on FreBSD
        ]
        staticlibs = []
        if ctx.env.DEST_OS == 'aix':
            # link statically with zlib, case sensitive
            libs.remove('Z')
            staticlibs.append('z')

        ctx.env.link_with_dynlibs = libs
        ctx.env.link_with_staticlibs = staticlibs

        ctx.program(
            source='src/main.c',
            target=exe_name,
            includes='src',
            use=libs + ["OBJECTS", "STATIC_ZLIB"],
            stlib=staticlibs,
            install_path=install_path,
            features=features
        )

    # This warning is deliberately at the end of the build, in the hopes that it will be more visible amongst all the
    # other stuff written to stdout.
    if machine(ctx) and machine(ctx) == "unknown":
        Logs.log.warning(
            "The target architecture reported by the compiler '%s' was not recognised and defaulted to 'unknown'. You "
            "should verify that `PyInstaller.compat.machine` is also 'unknown' on the target machine. If it is not, "
            "please add '%s' to `PyInstaller._shared_with_waf._pyi_machine()` and submit a pull request. Without this "
            "match, PyInstaller will not be able to find the bootloaders you just built!" %
            (ctx.env.DEST_CPU, ctx.env.DEST_CPU)
        )

    ctx.recurse("tests")


class make_all(BuildContext):
    """
    Do build and install in one step.
    """
    cmd = 'make_all'

    def execute_build(ctx):
        Options.commands = ['build_debug', 'build_release']
        # On Windows and macOS we also need console/windowed bootloaders. On other platforms they make no sense.
        if ctx.env.DEST_OS in ('win32', 'darwin'):
            Options.commands += ['build_debugw', 'build_releasew']
        # Install bootloaders.
        Options.commands += ['install_debug', 'install_release']
        if ctx.env.DEST_OS in ('win32', 'darwin'):
            Options.commands += ['install_debugw', 'install_releasew']


def all(ctx):
    """
    Do configure, build and install in one step.
    """
    # `all` is run prior to `configure`, thus it does not get a build context. Therefore another command `make_all` is
    # required, which gets the build context, and can make decisions based on the outcome of `configure`.
    Options.commands = ['distclean', 'configure', 'make_all']


# Set up building several variants of bootloader.
for x in variants:

    class BootloaderContext(BuildContext):
        cmd = 'build' + '_' + x
        variant = x

    class BootloaderInstallContext(InstallContext):
        cmd = 'install' + '_' + x
        variant = x
