build/moz.configure/windows.configure
author Mike Hommey <mh+mozilla@glandium.org>
Fri, 11 Jan 2019 22:21:24 +0000
changeset 453569 e69490bbd056
parent 453420 f1ec67ff7806
child 455581 ab7f23ee7562
permissions -rw-r--r--
Bug 1519325 - Move D3D compiler DLL detection to python configure. r=froydnj MOZ_D3D_CPU_SUFFIX and MOZ_HAS_WINSDK_WITH_D3D are not used in the build, and nothing includes d3d10.h except some angle code in a preprocessed branch that is only taken for a macro we never define, so we don't move the code corresponding to those. We also simplify the detection code, which is convoluted now that it doesn't search for multiple different DLLs. Differential Revision: https://phabricator.services.mozilla.com/D16295

# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

option('--with-windows-version', nargs=1, default='603',
       help='Windows SDK version to target. Win 8.1 (603) is currently'
            'the minimum supported version.')


@depends('--with-windows-version')
@imports(_from='__builtin__', _import='ValueError')
def valid_windows_version(value):
    if not value:
        die('Cannot build with --without-windows-version')
    try:
        version = int(value[0], 16)
        if version in (0x603,):
            return version
    except ValueError:
        pass

    die('Invalid value for --with-windows-version (%s)', value[0])


option(env='WINDOWSSDKDIR', nargs=1,
       help='Directory containing the Windows SDK')


@depends('WINDOWSSDKDIR', host)
def windows_sdk_dir(value, host):
    if value:
        return value
    if host.kernel != 'WINNT':
        return ()

    return set(x[1] for x in get_registry_values(
        r'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows Kits\Installed Roots'
        r'\KitsRoot*', get_32_and_64_bit=True))

# The Windows SDK 8.1 and 10 have different layouts. The former has
# $SDK/include/$subdir, while the latter has $SDK/include/$version/$subdir.
# The vcvars* scripts don't actually care about the version, they just take
# the last alphanumerically.
# The $SDK/lib directories always have version subdirectories, but while the
# versions match the one in $SDK/include for SDK 10, it's "winv6.3" for SDK
# 8.1.


@imports('os')
@imports('re')
@imports(_from='__builtin__', _import='sorted')
@imports(_from='__builtin__', _import='WindowsError')
def get_sdk_dirs(sdk, subdir):
    def get_dirs_containing(sdk, stem, subdir):
        base = os.path.join(sdk, stem)
        try:
            subdirs = [d for d in os.listdir(base)
                       if os.path.isdir(os.path.join(base, d))]
        except WindowsError:
            subdirs = []
        if not subdirs:
            return ()
        if subdir in subdirs:
            return (base,)
        # At this point, either we have an incomplete or invalid SDK directory,
        # or we exclusively have version numbers in subdirs.
        return tuple(os.path.join(base, s) for s in subdirs
                     if os.path.isdir(os.path.join(base, s, subdir)))

    def categorize(dirs):
        return {os.path.basename(d): d for d in dirs}

    include_dirs = categorize(get_dirs_containing(sdk, 'include', subdir))
    lib_dirs = categorize(get_dirs_containing(sdk, 'lib', subdir))

    if 'include' in include_dirs:
        include_dirs['winv6.3'] = include_dirs['include']
        del include_dirs['include']

    valid_versions = sorted(set(include_dirs) & set(lib_dirs), reverse=True)
    if valid_versions:
        return namespace(
            path=sdk,
            lib=lib_dirs[valid_versions[0]],
            include=include_dirs[valid_versions[0]],
        )


@imports(_from='mozbuild.shellutil', _import='quote')
def valid_windows_sdk_dir_result(value):
    if value:
        return '0x%04x in %s' % (value.version, quote(value.path))


@depends(c_compiler, windows_sdk_dir, valid_windows_version, 'WINDOWSSDKDIR')
@checking('for Windows SDK', valid_windows_sdk_dir_result)
@imports(_from='__builtin__', _import='sorted')
@imports(_from='textwrap', _import='dedent')
def valid_windows_sdk_dir(compiler, windows_sdk_dir, target_version,
                          windows_sdk_dir_env):
    if windows_sdk_dir_env:
        windows_sdk_dir_env = windows_sdk_dir_env[0]
    sdks = {}
    for d in windows_sdk_dir:
        sdk = get_sdk_dirs(d, 'um')
        if sdk:
            check = dedent('''\
            #include <winsdkver.h>
            WINVER_MAXVER
            ''')
            um_dir = os.path.join(sdk.include, 'um')
            shared_dir = os.path.join(sdk.include, 'shared')
            result = try_preprocess(compiler.wrapper + [compiler.compiler] +
                                    compiler.flags +
                                    ['-I', um_dir, '-I', shared_dir], 'C',
                                    check)
            if result:
                maxver = result.splitlines()[-1]
                try:
                    maxver = int(maxver, 0)
                except Exception:
                    pass
                else:
                    sdks[d] = maxver, sdk
                    continue
        if d == windows_sdk_dir_env:
            raise FatalCheckError(
                'Error while checking the version of the SDK in '
                'WINDOWSSDKDIR (%s). Please verify it contains a valid and '
                'complete SDK installation.' % windows_sdk_dir_env)

    valid_sdks = sorted(sdks, key=lambda x: sdks[x][0], reverse=True)
    if valid_sdks:
        biggest_version, sdk = sdks[valid_sdks[0]]
    if not valid_sdks or biggest_version < target_version:
        if windows_sdk_dir_env:
            raise FatalCheckError(
                'You are targeting Windows version 0x%04x, but your SDK only '
                'supports up to version 0x%04x. Install and use an updated SDK, '
                'or target a lower version using --with-windows-version. '
                'Alternatively, try running the Windows SDK Configuration Tool '
                'and selecting a newer SDK. See '
                'https://developer.mozilla.org/En/Windows_SDK_versions for '
                'details on fixing this.' % (target_version, biggest_version))

        raise FatalCheckError(
            'Cannot find a Windows SDK for version >= 0x%04x.' % target_version)

    return namespace(
        path=sdk.path,
        include=sdk.include,
        lib=sdk.lib,
        version=biggest_version,
    )


@imports(_from='mozbuild.shellutil', _import='quote')
def valid_ucrt_sdk_dir_result(value):
    if value:
        return '%s in %s' % (value.version, quote(value.path))


@depends(windows_sdk_dir, 'WINDOWSSDKDIR', c_compiler)
@checking('for Universal CRT SDK', valid_ucrt_sdk_dir_result)
@imports('os')
@imports(_from='__builtin__', _import='sorted')
@imports(_import='mozpack.path', _as='mozpath')
def valid_ucrt_sdk_dir(windows_sdk_dir, windows_sdk_dir_env, c_compiler):
    if windows_sdk_dir_env:
        windows_sdk_dir_env = windows_sdk_dir_env[0]
    sdks = {}
    for d in windows_sdk_dir:
        sdk = get_sdk_dirs(d, 'ucrt')
        if sdk:
            version = os.path.basename(sdk.include)
            # We're supposed to always find a version in the directory, because
            # the 8.1 SDK, which doesn't have a version in the directory, doesn't
            # contain the Universal CRT SDK. When the main SDK is 8.1, there
            # is, however, supposed to be a reduced install of the SDK 10
            # with the UCRT.
            if version != 'include':
                sdks[d] = Version(version), sdk
                continue
        if d == windows_sdk_dir_env:
            # When WINDOWSSDKDIR is set in the environment and we can't find the
            # Universal CRT SDK, chances are this is a start-shell-msvc*.bat
            # setup, where INCLUDE and LIB already contain the UCRT paths.
            ucrt_includes = [
                p for p in os.environ.get('INCLUDE', '').split(os.pathsep)
                if os.path.basename(p).lower() == 'ucrt'
            ]
            ucrt_libs = [
                p for p in os.environ.get('LIB', '').split(os.pathsep)
                if os.path.basename(os.path.dirname(p)).lower() == 'ucrt'
            ]
            if ucrt_includes and ucrt_libs:
                # Pick the first of each, since they are the ones that the
                # compiler would look first. Assume they contain the SDK files.
                include = os.path.dirname(ucrt_includes[0])
                lib = os.path.dirname(os.path.dirname(ucrt_libs[0]))
                path = os.path.dirname(os.path.dirname(include))
                version = os.path.basename(include)
                if version != 'include' and mozpath.basedir(lib, [path]):
                    sdks[d] = Version(version), namespace(
                        path=path,
                        include=include,
                        lib=lib,
                    )
                    continue
            raise FatalCheckError(
                'The SDK in WINDOWSSDKDIR (%s) does not contain the Universal '
                'CRT.' % windows_sdk_dir_env)

    valid_sdks = sorted(sdks, key=lambda x: sdks[x][0], reverse=True)
    if not valid_sdks:
        raise FatalCheckError('Cannot find the Universal CRT SDK. '
                              'Please install it.')

    version, sdk = sdks[valid_sdks[0]]
    minimum_ucrt_version = Version('10.0.15063.0')
    if version < minimum_ucrt_version:
        raise FatalCheckError('Latest Universal CRT SDK version found %s'
                              ' and minimum required is %s. This or a later'
                              ' version can be installed using the Visual'
                              ' Studio installer.'
                              % (version, minimum_ucrt_version))

    broken_ucrt_version = Version('10.0.16299.0')
    working_ucrt_version = Version('10.0.17134.0')
    if (c_compiler.type == 'clang-cl' and version >= broken_ucrt_version and
            version < working_ucrt_version):
        raise FatalCheckError('Found SDK version %s but clang-cl builds'
                              ' currently don\'t work with the SDK version.'
                              ' You should use a different version, either'
                              ' by uninstalling version %s or setting a'
                              ' custom WINDOWSSDKDIR.\n'
                              'Note: Version %s now works with clang-cl.'
                              % (version, version, working_ucrt_version))

    return namespace(
        path=sdk.path,
        include=sdk.include,
        lib=sdk.lib,
        version=version,
    )


@depends(c_compiler, toolchain_search_path)
@imports('os')
def vc_path(c_compiler, toolchain_search_path):
    vc_path_env = os.environ.get('VC_PATH')
    if vc_path_env:
        return os.path.normpath(vc_path_env)

    if c_compiler.type not in ('msvc', 'clang-cl'):
        return

    vc_program = c_compiler.compiler

    # In clang-cl builds, we use the headers and libraries from an MSVC installation.
    if c_compiler.type == 'clang-cl':
        vc_program = find_program('cl.exe', paths=toolchain_search_path)

    result = os.path.dirname(vc_program)
    while True:
        next, p = os.path.split(result)
        if next == result:
            die('Cannot determine the Visual C++ directory the compiler (%s) '
                'is in' % cl)
        result = next
        if p.lower() == 'bin':
            break
    return os.path.normpath(result)


@depends(vc_path, c_compiler)
@checking('for the Debug Interface Access SDK', lambda x: x or 'not found')
@imports('os')
@imports(_from='os.path', _import='isdir')
def dia_sdk_dir(vc_path, c_compiler):
    dia_sdk_dir_env = os.environ.get('DIA_SDK_PATH')
    if dia_sdk_dir_env:
        return os.path.normpath(dia_sdk_dir_env)

    if vc_path:
        if c_compiler.version < '19.10':
            path = os.path.join(os.path.dirname(vc_path), 'DIA SDK')
        else:
            # This would be easier if we had the installationPath that
            # get_vc_paths works with, since 'DIA SDK' is relative to that.
            path = os.path.normpath(os.path.join(
                vc_path, r'..\..\..\..\DIA SDK'))
        if isdir(path):
            return path


@depends(vc_path, valid_windows_sdk_dir, valid_ucrt_sdk_dir, dia_sdk_dir)
@imports('os')
def include_path(vc_path, windows_sdk_dir, ucrt_sdk_dir, dia_sdk_dir):
    if not vc_path:
        return
    atlmfc_dir = os.path.join(vc_path, 'atlmfc', 'include')
    if not os.path.isdir(atlmfc_dir):
        die('Cannot find the ATL/MFC headers in the Visual C++ directory (%s). '
            'Please install them.' % vc_path)

    winrt_dir = os.path.join(windows_sdk_dir.include, 'winrt')
    if not os.path.isdir(winrt_dir):
        die('Cannot find the WinRT headers in the Windows SDK directory (%s). '
            'Please install them.' % windows_sdk_dir.path)

    includes = []
    include_env = os.environ.get('INCLUDE')
    if include_env:
        includes.append(include_env)
    includes.extend((
        os.path.join(vc_path, 'include'),
        atlmfc_dir,
        os.path.join(windows_sdk_dir.include, 'shared'),
        os.path.join(windows_sdk_dir.include, 'um'),
        winrt_dir,
        os.path.join(ucrt_sdk_dir.include, 'ucrt'),
    ))
    if dia_sdk_dir:
        includes.append(os.path.join(dia_sdk_dir, 'include'))
    # Set in the environment for old-configure
    includes = os.pathsep.join(includes)
    os.environ['INCLUDE'] = includes
    return includes


set_config('INCLUDE', include_path)


@template
def lib_path_for(host_or_target):
    compiler = {
        host: host_c_compiler,
        target: c_compiler,
    }[host_or_target]

    @depends(host_or_target, dependable(host_or_target is host), compiler, vc_path,
             valid_windows_sdk_dir, valid_ucrt_sdk_dir, dia_sdk_dir)
    @imports('os')
    def lib_path(target, is_host, c_compiler, vc_path, windows_sdk_dir, ucrt_sdk_dir, dia_sdk_dir):
        if not vc_path:
            return
        sdk_target = {
            'x86': 'x86',
            'x86_64': 'x64',
            'arm': 'arm',
            'aarch64': 'arm64',
        }.get(target.cpu)

        old_target = {
            'x86': '',
            'x86_64': 'amd64',
            'arm': 'arm',
            'aarch64': 'arm64'
        }.get(target.cpu)
        if old_target is None:
            return
        # As old_target can be '', and os.path.join will happily use the empty
        # string, leading to a string ending with a backslash, that Make will
        # interpret as a "string continues on next line" indicator, use variable
        # args.
        old_target = (old_target,) if old_target else ()
        if c_compiler.version < '19.10':
            # MSVC2015
            vc_target = old_target
        else:
            # MSVC2017 switched to use the same target naming as the sdk.
            vc_target = (sdk_target,)

        atlmfc_dir = os.path.join(vc_path, 'atlmfc', 'lib', *vc_target)
        if not os.path.isdir(atlmfc_dir):
            die('Cannot find the ATL/MFC libraries in the Visual C++ directory '
                '(%s). Please install them.' % vc_path)

        libs = []
        lib_env = os.environ.get('LIB')
        if lib_env and not is_host:
            libs.extend(lib_env.split(os.pathsep))
        libs.extend((
            os.path.join(vc_path, 'lib', *vc_target),
            atlmfc_dir,
            os.path.join(windows_sdk_dir.lib, 'um', sdk_target),
            os.path.join(ucrt_sdk_dir.lib, 'ucrt', sdk_target),
        ))
        if dia_sdk_dir:
            # For some reason the DIA SDK still uses the old-style targets
            # even in a newer MSVC.
            libs.append(os.path.join(dia_sdk_dir, 'lib', *old_target))
        return libs

    return lib_path


@depends(lib_path_for(target))
@imports('os')
def lib_path(libs):
    # Set in the environment for old-configure
    libs = os.pathsep.join(libs)
    os.environ['LIB'] = libs
    return libs


set_config('LIB', lib_path)


@depends(lib_path_for(host))
@imports(_from='mozbuild.shellutil', _import='quote')
def host_linker_libpaths(libs):
    return ['-LIBPATH:%s' % quote(l) for l in libs]


set_config('HOST_LINKER_LIBPATHS', host_linker_libpaths)


option(env='MT', nargs=1, help='Path to the Microsoft Manifest Tool')


@depends(valid_windows_sdk_dir, valid_ucrt_sdk_dir)
@imports(_from='os', _import='environ')
@imports('platform')
def sdk_bin_path(valid_windows_sdk_dir, valid_ucrt_sdk_dir):
    if not valid_windows_sdk_dir:
        return

    vc_host = {
        'x86': 'x86',
        'AMD64': 'x64',
    }.get(platform.machine())

    # From version 10.0.15063.0 onwards the bin path contains the version number.
    versioned_bin = ('bin' if valid_ucrt_sdk_dir.version < '10.0.15063.0'
                     else os.path.join('bin', str(valid_ucrt_sdk_dir.version)))
    result = [
        environ['PATH'],
        os.path.join(valid_windows_sdk_dir.path, versioned_bin, vc_host)
    ]
    if vc_host == 'x64':
        result.append(
            os.path.join(valid_windows_sdk_dir.path, versioned_bin, 'x86'))
    return result


mt = check_prog('MT', ('mt.exe',), input='MT',
                paths=sdk_bin_path)


# Check that MT is not something unexpected like "magnetic tape manipulation
# utility".
@depends(mt)
@checking('whether MT is really Microsoft Manifest Tool', lambda x: bool(x))
@imports('subprocess')
def valid_mt(path):
    try:
        out = subprocess.check_output([path]).splitlines()
        out = '\n'.join(l for l in out
                        if 'Microsoft (R) Manifest Tool' in l)
        if out:
            return path
    except subprocess.CalledProcessError:
        pass
    raise FatalCheckError('%s is not Microsoft Manifest Tool')


set_config('MSMANIFEST_TOOL', depends(valid_mt)(lambda x: bool(x)))


@template
def linker_progs_for(host_or_target):
    compiler = {
        host: host_c_compiler,
        target: c_compiler,
    }[host_or_target]

    @depends(compiler)
    def linker_progs(compiler):
        if compiler.type == 'msvc':
            return ('link', 'lld-link')
        if compiler.type == 'clang-cl':
            return ('lld-link', 'link')
    return linker_progs


link = check_prog('LINKER', linker_progs_for(target),
                  paths=toolchain_search_path)

host_link = check_prog('HOST_LINKER', linker_progs_for(host),
                       paths=host_toolchain_search_path)

add_old_configure_assignment('LINKER', link)


check_prog('MAKECAB', ('makecab.exe',))


@depends(c_compiler, using_sccache)
def need_showincludes_prefix(info, using_sccache):
    # sccache does its own -showIncludes prefix checking.
    # clang-cl uses a gcc-style dependency scheme, see toolchain.configure.
    if info.type == 'msvc' and not using_sccache:
        return True


@depends(c_compiler, when=need_showincludes_prefix)
@imports(_from='re', _import='compile', _as='re_compile')
def msvc_showincludes_prefix(c_compiler):
    pattern = re_compile(br'^([^:]*:.*[ :] )(.*\\stdio.h)$')
    output = try_invoke_compiler([c_compiler.compiler], 'C', '#include <stdio.h>\n',
                                 ['-nologo', '-c', '-Fonul', '-showIncludes'])
    for line in output.splitlines():
        if line.endswith(b'\\stdio.h'):
            m = pattern.match(line)
            if m:
                return m.group(1)
    # We should have found the prefix and returned earlier
    die('Cannot find cl -showIncludes prefix.')


set_config('CL_INCLUDES_PREFIX', msvc_showincludes_prefix)

# Make sure that the build system can handle non-ASCII characters in
# environment variables to prevent silent breakage on non-English systems.
set_config('NONASCII', b'\241\241')