build/moz.configure/init.configure
author Mike Hommey <mh+mozilla@glandium.org>
Fri, 16 Nov 2018 01:16:31 +0000
changeset 446945 83e6f0d47a80
parent 446944 fd205107b0f4
child 446946 88ba9f723934
permissions -rw-r--r--
Bug 1502457 - Move MOZILLA_OFFICIAL to init.configure. r=nalexander Depends on D11985 Differential Revision: https://phabricator.services.mozilla.com/D11986

# -*- Mode: python; 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/.

include('util.configure')
include('checks.configure')

option(env='DIST', nargs=1, help='DIST directory')

# Do not allow objdir == srcdir builds.
# ==============================================================


@depends('--help', 'DIST')
@imports(_from='__builtin__', _import='open')
@imports(_from='os.path', _import='exists')
def check_build_environment(help, dist):
    topobjdir = os.path.realpath(os.path.abspath('.'))
    topsrcdir = os.path.realpath(os.path.abspath(
        os.path.join(os.path.dirname(__file__), '..', '..')))

    if dist:
        dist = normsep(dist[0])
    else:
        dist = os.path.join(topobjdir, 'dist')

    result = namespace(
        topsrcdir=topsrcdir,
        topobjdir=topobjdir,
        dist=dist,
    )

    if help:
        return result

    # This limitation has mostly to do with GNU make. Since make can't represent
    # variables with spaces without correct quoting and many paths are used
    # without proper quoting, using paths with spaces commonly results in
    # targets or dependencies being treated as multiple paths. This, of course,
    # undermines the ability for make to perform up-to-date checks and makes
    # the build system not work very efficiently. In theory, a non-make build
    # backend will make this limitation go away. But there is likely a long tail
    # of things that will need fixing due to e.g. lack of proper path quoting.
    if len(topsrcdir.split()) > 1:
        die('Source directory cannot be located in a path with spaces: %s' %
            topsrcdir)
    if len(topobjdir.split()) > 1:
        die('Object directory cannot be located in a path with spaces: %s' %
            topobjdir)

    if topsrcdir == topobjdir:
        die('  ***\n'
            '  * Building directly in the main source directory is not allowed.\n'
            '  *\n'
            '  * To build, you must run configure from a separate directory\n'
            '  * (referred to as an object directory).\n'
            '  *\n'
            '  * If you are building with a mozconfig, you will need to change your\n'
            '  * mozconfig to point to a different object directory.\n'
            '  ***'
            )

    # Check for CRLF line endings.
    with open(os.path.join(topsrcdir, 'configure.py'), 'rb') as fh:
        data = fh.read()
        if '\r' in data:
            die('\n ***\n'
                ' * The source tree appears to have Windows-style line endings.\n'
                ' *\n'
                ' * If using Git, Git is likely configured to use Windows-style\n'
                ' * line endings.\n'
                ' *\n'
                ' * To convert the working copy to UNIX-style line endings, run\n'
                ' * the following:\n'
                ' *\n'
                ' * $ git config core.autocrlf false\n'
                ' * $ git config core.eof lf\n'
                ' * $ git rm --cached -r .\n'
                ' * $ git reset --hard\n'
                ' *\n'
                ' * If not using Git, the tool you used to obtain the source\n'
                ' * code likely converted files to Windows line endings. See\n'
                ' * usage information for that tool for more.\n'
                ' ***')

    # Check for a couple representative files in the source tree
    conflict_files = [
        '*         %s' % f for f in ('Makefile', 'config/autoconf.mk')
        if exists(os.path.join(topsrcdir, f))
    ]
    if conflict_files:
        die('  ***\n'
            '  *   Your source tree contains these files:\n'
            '  %s\n'
            '  *   This indicates that you previously built in the source tree.\n'
            '  *   A source tree build can confuse the separate objdir build.\n'
            '  *\n'
            '  *   To clean up the source tree:\n'
            '  *     1. cd %s\n'
            '  *     2. gmake distclean\n'
            '  ***'
            % ('\n  '.join(conflict_files), topsrcdir)
            )

    return result


set_config('TOPSRCDIR', check_build_environment.topsrcdir)
set_config('TOPOBJDIR', check_build_environment.topobjdir)
set_config('MOZ_BUILD_ROOT', check_build_environment.topobjdir)
set_config('DIST', check_build_environment.dist)

add_old_configure_assignment(
    '_topsrcdir', check_build_environment.topsrcdir)
add_old_configure_assignment(
    '_objdir', check_build_environment.topobjdir)
add_old_configure_assignment(
    'MOZ_BUILD_ROOT', check_build_environment.topobjdir)
add_old_configure_assignment(
    'DIST', check_build_environment.dist)

option(env='MOZ_AUTOMATION', help='Enable options for automated builds')
set_config('MOZ_AUTOMATION', depends_if('MOZ_AUTOMATION')(lambda x: True))


option(env='OLD_CONFIGURE', nargs=1, help='Path to the old configure script')

option(env='MOZCONFIG', nargs=1, help='Mozconfig location')

option('--with-external-source-dir', env='EXTERNAL_SOURCE_DIR', nargs=1,
       help='External directory containing additional build files')


@depends('--with-external-source-dir')
def external_source_dir(value):
    if value:
        return value[0]


set_config('EXTERNAL_SOURCE_DIR', external_source_dir)
add_old_configure_assignment('EXTERNAL_SOURCE_DIR', external_source_dir)

# Read user mozconfig
# ==============================================================
# Note: the dependency on --help is only there to always read the mozconfig,
# even when --help is passed. Without this dependency, the function wouldn't
# be called when --help is passed, and the mozconfig wouldn't be read.


@depends('MOZCONFIG', 'OLD_CONFIGURE',
         check_build_environment, '--with-external-source-dir',
         '--help')
@imports(_from='mozbuild.mozconfig', _import='MozconfigLoader')
def mozconfig(mozconfig, old_configure, build_env,
              external_source_dir, help):
    if not old_configure:
        die('The OLD_CONFIGURE environment variable must be set')

    # Don't read the mozconfig for the js configure (yay backwards
    # compatibility)
    # While the long term goal is that js and top-level use the same configure
    # and the same overall setup, including the possibility to use mozconfigs,
    # figuring out what we want to do wrt mozconfig vs. command line and
    # environment variable is not a clear-cut case, and it's more important to
    # fix the immediate problem mozconfig causes to js developers by
    # "temporarily" returning to the previous behavior of not loading the
    # mozconfig for the js configure.
    # Separately to the immediate problem for js developers, there is also the
    # need to not load a mozconfig when running js configure as a subconfigure.
    # Unfortunately, there is no direct way to tell whether the running
    # configure is the js configure. The indirect way is to look at the
    # OLD_CONFIGURE path, which points to js/src/old-configure.
    # I expect we'll have figured things out for mozconfigs well before
    # old-configure dies.
    if os.path.dirname(os.path.abspath(old_configure[0])).endswith('/js/src'):
        return {'path': None}

    topsrcdir = build_env.topsrcdir
    if external_source_dir:
        topsrcdir = external_source_dir[0]
    loader = MozconfigLoader(topsrcdir)
    mozconfig = mozconfig[0] if mozconfig else None
    mozconfig = loader.find_mozconfig(env={'MOZCONFIG': mozconfig})
    mozconfig = loader.read_mozconfig(mozconfig)

    return mozconfig


set_config('MOZCONFIG', depends(mozconfig)(lambda m: m['path']))


option(env='PYTHON', nargs=1, help='Python interpreter')

# Setup python virtualenv
# ==============================================================


@depends('PYTHON', check_build_environment, mozconfig, '--help')
@imports('os')
@imports('sys')
@imports('subprocess')
@imports(_from='mozbuild.configure.util', _import='LineIO')
@imports(_from='mozbuild.virtualenv', _import='VirtualenvManager')
@imports(_from='mozbuild.virtualenv', _import='verify_python_version')
@imports('distutils.sysconfig')
def virtualenv_python(env_python, build_env, mozconfig, help):
    if help:
        return

    python = env_python[0] if env_python else None

    # Ideally we'd rely on the mozconfig injection from mozconfig_options,
    # but we'd rather avoid the verbosity when we need to reexecute with
    # a different python.
    if mozconfig['path']:
        if 'PYTHON' in mozconfig['env']['added']:
            python = mozconfig['env']['added']['PYTHON']
        elif 'PYTHON' in mozconfig['env']['modified']:
            python = mozconfig['env']['modified']['PYTHON'][1]
        elif 'PYTHON' in mozconfig['vars']['added']:
            python = mozconfig['vars']['added']['PYTHON']
        elif 'PYTHON' in mozconfig['vars']['modified']:
            python = mozconfig['vars']['modified']['PYTHON'][1]

    with LineIO(lambda l: log.error(l)) as out:
        verify_python_version(out)
    topsrcdir, topobjdir = build_env.topsrcdir, build_env.topobjdir
    if topobjdir.endswith('/js/src'):
        topobjdir = topobjdir[:-7]

    virtualenvs_root = os.path.join(topobjdir, '_virtualenvs')
    with LineIO(lambda l: log.info(l), 'replace') as out:
        manager = VirtualenvManager(
            topsrcdir, topobjdir,
            os.path.join(virtualenvs_root, 'init'), out,
            os.path.join(topsrcdir, 'build', 'virtualenv_packages.txt'))

    if python:
        # If we're not in the virtualenv, we need the which module for
        # find_program.
        if normsep(sys.executable) != normsep(manager.python_path):
            sys.path.append(os.path.join(
                topsrcdir, 'third_party', 'python', 'which'))
        found_python = find_program(python)
        if not found_python:
            die('The PYTHON environment variable does not contain '
                'a valid path. Cannot find %s', python)
        python = found_python
    else:
        python = sys.executable

    if not manager.up_to_date(python):
        log.info('Creating Python environment')
        manager.build(python)

    python = normsep(manager.python_path)

    if not normsep(sys.executable).startswith(normsep(virtualenvs_root)):
        log.info('Reexecuting in the virtualenv')
        if env_python:
            del os.environ['PYTHON']
        # One would prefer to use os.execl, but that's completely borked on
        # Windows.
        sys.exit(subprocess.call([python] + sys.argv))

    # We are now in the virtualenv
    if not distutils.sysconfig.get_python_lib():
        die('Could not determine python site packages directory')

    return python


set_config('PYTHON', virtualenv_python)
add_old_configure_assignment('PYTHON', virtualenv_python)

# Inject mozconfig options
# ==============================================================
# All options defined above this point can't be injected in mozconfig_options
# below, so collect them.


@template
def early_options():
    @dependable
    @imports('__sandbox__')
    def early_options():
        return set(
            option.env
            for option in __sandbox__._options.itervalues()
            if option.env
        )
    return early_options


early_options = early_options()


@depends(mozconfig, 'MOZ_AUTOMATION', '--help')
# This gives access to the sandbox. Don't copy this blindly.
@imports('__sandbox__')
@imports('os')
def mozconfig_options(mozconfig, automation, help):
    if mozconfig['path']:
        if 'MOZ_AUTOMATION_MOZCONFIG' in mozconfig['env']['added']:
            if not automation:
                log.error('%s directly or indirectly includes an in-tree '
                          'mozconfig.', mozconfig['path'])
                log.error('In-tree mozconfigs make strong assumptions about '
                          'and are only meant to be used by Mozilla '
                          'automation.')
                die("Please don't use them.")
        helper = __sandbox__._helper
        log.info('Adding configure options from %s' % mozconfig['path'])
        for arg in mozconfig['configure_args']:
            log.info('  %s' % arg)
            # We could be using imply_option() here, but it has other
            # contraints that don't really apply to the command-line
            # emulation that mozconfig provides.
            helper.add(arg, origin='mozconfig', args=helper._args)

        def add(key, value):
            if key.isupper():
                arg = '%s=%s' % (key, value)
                log.info('  %s' % arg)
                helper.add(arg, origin='mozconfig', args=helper._args)

        for key, value in mozconfig['env']['added'].iteritems():
            add(key, value)
            os.environ[key] = value
        for key, (_, value) in mozconfig['env']['modified'].iteritems():
            add(key, value)
            os.environ[key] = value
        for key, value in mozconfig['vars']['added'].iteritems():
            add(key, value)
        for key, (_, value) in mozconfig['vars']['modified'].iteritems():
            add(key, value)


# Mozilla-Build
# ==============================================================
option(env='MOZILLABUILD', nargs=1,
       help='Path to Mozilla Build (Windows-only)')

option(env='CONFIG_SHELL', nargs=1, help='Path to a POSIX shell')

# It feels dirty replicating this from python/mozbuild/mozbuild/mozconfig.py,
# but the end goal being that the configure script would go away...


@depends('CONFIG_SHELL', 'MOZILLABUILD')
@checking('for a shell')
@imports('sys')
def shell(value, mozillabuild):
    if value:
        return find_program(value[0])
    shell = 'sh'
    if mozillabuild:
        shell = mozillabuild[0] + '/msys/bin/sh'
    if sys.platform == 'win32':
        shell = shell + '.exe'
    return find_program(shell)


# Python 3
# ========

option(env='PYTHON3', nargs=1, help='Python 3 interpreter (3.5 or later)')


@depends('PYTHON3', 'MOZILLABUILD')
@checking('for Python 3',
          callback=lambda x: '%s (%s)' % (x.path, x.str_version) if x else 'no')
@imports(_from='__builtin__', _import='Exception')
@imports(_from='mozbuild.pythonutil', _import='find_python3_executable')
@imports(_from='mozbuild.pythonutil', _import='python_executable_version')
def python3(env_python, mozillabuild):
    python = env_python[0] if env_python else None

    # If Python given by environment variable, it must work.
    if python:
        try:
            version = python_executable_version(python).version
        except Exception as e:
            raise FatalCheckError('could not determine version of PYTHON '
                                  '(%s): %s' % (python, e))
    elif mozillabuild:
        # MozillaBuild provides a Python 3.
        python = normsep('%s/python3/python3.exe' % mozillabuild)

        try:
            version = python_executable_version(python).version
        except Exception as e:
            raise FatalCheckError('could not determine version of '
                                  'MozillaBuild python: %s' % e)
    else:
        # Fall back to the search routine.
        python, version = find_python3_executable(min_version='3.5.0')

        # The API returns a bytes whereas everything in configure is unicode.
        if python:
            python = python.decode('utf-8')

    if not python:
        raise FatalCheckError('Python 3.5 or newer is required to build. '
                              'Ensure a `python3.x` executable is in your '
                              'PATH or define PYTHON3 to point to a Python '
                              '3.5 executable.')

    if version < (3, 5, 0):
        raise FatalCheckError('Python 3.5 or newer is required to build; '
                              '%s is Python %d.%d' % (python, version[0],
                                                      version[1]))

    return namespace(
        path=python,
        version=version,
        str_version='.'.join(str(v) for v in version),
    )


set_config('PYTHON3', depends_if(python3)(lambda p: p.path))
set_config('PYTHON3_VERSION', depends_if(python3)(lambda p: p.str_version))

# Source checkout and version control integration.
# ================================================


@depends(check_build_environment, 'MOZ_AUTOMATION', '--help')
@checking('for vcs source checkout')
@imports('os')
def vcs_checkout_type(build_env, automation, _):
    if os.path.exists(os.path.join(build_env.topsrcdir, '.hg')):
        return 'hg'
    elif os.path.exists(os.path.join(build_env.topsrcdir, '.git')):
        return 'git'
    elif automation:
        raise FatalCheckError('unable to resolve VCS type; must run '
                              'from a source checkout when MOZ_AUTOMATION '
                              'is set')

# Resolve VCS binary for detected repository type.


# TODO remove hg.exe once bug 1382940 addresses ambiguous executables case.
hg = check_prog('HG', ('hg.exe', 'hg',), allow_missing=True,
                when=depends(vcs_checkout_type)(lambda x: x == 'hg'))
git = check_prog('GIT', ('git',), allow_missing=True,
                 when=depends(vcs_checkout_type)(lambda x: x == 'git'))


@depends_if(hg)
@checking('for Mercurial version')
@imports('os')
@imports('re')
def hg_version(hg):
    # HGPLAIN in Mercurial 1.5+ forces stable output, regardless of set
    # locale or encoding.
    env = dict(os.environ)
    env['HGPLAIN'] = '1'

    out = check_cmd_output(hg, '--version', env=env)

    match = re.search(r'Mercurial Distributed SCM \(version ([^\)]+)', out)

    if not match:
        raise FatalCheckError(
            'unable to determine Mercurial version: %s' % out)

    # The version string may be "unknown" for Mercurial run out of its own
    # source checkout or for bad builds. But LooseVersion handles it.

    return Version(match.group(1))

# Resolve Mercurial config items so other checks have easy access.
# Do NOT set this in the config because it may contain sensitive data
# like API keys.


@depends_all(check_build_environment, hg, hg_version)
@imports('os')
def hg_config(build_env, hg, version):
    env = dict(os.environ)
    env['HGPLAIN'] = '1'

    # Warnings may get sent to stderr. But check_cmd_output() ignores
    # stderr if exit code is 0. And the command should always succeed if
    # `hg version` worked.
    out = check_cmd_output(hg, 'config', env=env, cwd=build_env.topsrcdir)

    # out is bytes. However, unicode literals are in effect, so implicit
    # type coercion can occur. The underlying Mercurial config file may be
    # in a user-defined encoding. However, HGPLAIN both overrides the decoding
    # inside Mercurial *and* ensures output is utf-8. Because moz.configure
    # is using unicode literals, our returned config object uses unicode
    # keys and values to limit potential for coercion.

    # Mercurial should emit utf-8. But be paranoid and ignore invalid utf-8
    # byte sequences.
    out = out.decode('utf-8', 'replace')

    config = {}

    for line in out.strip().splitlines():
        key, value = [s.strip() for s in line.split('=', 1)]
        config[key] = value

    return config


@depends_if(git)
@checking('for Git version')
@imports('re')
def git_version(git):
    out = check_cmd_output(git, '--version').rstrip()

    match = re.search('git version (.*)$', out)

    if not match:
        raise FatalCheckError('unable to determine Git version: %s' % out)

    return Version(match.group(1))

# Only set VCS_CHECKOUT_TYPE if we resolved the VCS binary.
# Require resolved VCS info when running in automation so automation's
# environment is more well-defined.


@depends(vcs_checkout_type, hg_version, git_version, 'MOZ_AUTOMATION')
def exposed_vcs_checkout_type(vcs_checkout_type, hg, git, automation):
    if vcs_checkout_type == 'hg':
        if hg:
            return 'hg'

        if automation:
            raise FatalCheckError('could not resolve Mercurial binary info')

    elif vcs_checkout_type == 'git':
        if git:
            return 'git'

        if automation:
            raise FatalCheckError('could not resolve Git binary info')
    elif vcs_checkout_type:
        raise FatalCheckError('unhandled VCS type: %s' % vcs_checkout_type)


set_config('VCS_CHECKOUT_TYPE', exposed_vcs_checkout_type)

# Obtain a Repository interface for the current VCS repository.


@depends(check_build_environment, exposed_vcs_checkout_type, hg, git)
@imports(_from='mozversioncontrol', _import='get_repository_object')
def vcs_repository(build_env, vcs_checkout_type, hg, git):
    if vcs_checkout_type == 'hg':
        return get_repository_object(build_env.topsrcdir, hg=hg)
    elif vcs_checkout_type == 'git':
        return get_repository_object(build_env.topsrcdir, git=git)
    elif vcs_checkout_type:
        raise FatalCheckError('unhandled VCS type: %s' % vcs_checkout_type)


@depends_if(vcs_repository)
@checking('for sparse checkout')
def vcs_sparse_checkout(repo):
    return repo.sparse_checkout_present()


set_config('VCS_SPARSE_CHECKOUT', vcs_sparse_checkout)

# Host and target systems
# ==============================================================
option('--host', nargs=1, help='Define the system type performing the build')

option('--target', nargs=1,
       help='Define the system type where the resulting executables will be '
            'used')


@imports(_from='mozbuild.configure.constants', _import='CPU')
@imports(_from='mozbuild.configure.constants', _import='CPU_bitness')
@imports(_from='mozbuild.configure.constants', _import='Endianness')
@imports(_from='mozbuild.configure.constants', _import='Kernel')
@imports(_from='mozbuild.configure.constants', _import='OS')
@imports(_from='__builtin__', _import='KeyError')
@imports(_from='__builtin__', _import='ValueError')
def split_triplet(triplet, allow_unknown=False):
    # The standard triplet is defined as
    #   CPU_TYPE-MANUFACTURER-OPERATING_SYSTEM
    # There is also a quartet form:
    #   CPU_TYPE-MANUFACTURER-KERNEL-OPERATING_SYSTEM
    # But we can consider the "KERNEL-OPERATING_SYSTEM" as one.
    # Additionally, some may omit "unknown" when the manufacturer
    # is not specified and emit
    #   CPU_TYPE-OPERATING_SYSTEM
    parts = triplet.split('-', 2)
    if len(parts) == 3:
        cpu, _, os = parts
    elif len(parts) == 2:
        cpu, os = parts
    else:
        die("Unexpected triplet string: %s" % triplet)

    # Autoconf uses config.sub to validate and canonicalize those triplets,
    # but the granularity of its results has never been satisfying to our
    # use, so we've had our own, different, canonicalization. We've also
    # historically not been very consistent with how we use the canonicalized
    # values. Hopefully, this will help us make things better.
    # The tests are inherited from our decades-old autoconf-based configure,
    # which can probably be improved/cleaned up because they are based on a
    # mix of uname and config.guess output, while we now only use the latter,
    # which presumably has a cleaner and leaner output. Let's refine later.
    os = os.replace('/', '_')
    if 'android' in os:
        canonical_os = 'Android'
        canonical_kernel = 'Linux'
    elif os.startswith('linux'):
        canonical_os = 'GNU'
        canonical_kernel = 'Linux'
    elif os.startswith('kfreebsd') and os.endswith('-gnu'):
        canonical_os = 'GNU'
        canonical_kernel = 'kFreeBSD'
    elif os.startswith('gnu'):
        canonical_os = canonical_kernel = 'GNU'
    elif os.startswith('mingw'):
        canonical_os = canonical_kernel = 'WINNT'
    elif os.startswith('darwin'):
        canonical_kernel = 'Darwin'
        canonical_os = 'OSX'
    elif os.startswith('ios'):
        canonical_kernel = 'Darwin'
        canonical_os = 'iOS'
    elif os.startswith('dragonfly'):
        canonical_os = canonical_kernel = 'DragonFly'
    elif os.startswith('freebsd'):
        canonical_os = canonical_kernel = 'FreeBSD'
    elif os.startswith('netbsd'):
        canonical_os = canonical_kernel = 'NetBSD'
    elif os.startswith('openbsd'):
        canonical_os = canonical_kernel = 'OpenBSD'
    elif os.startswith('solaris'):
        canonical_os = canonical_kernel = 'SunOS'
    elif allow_unknown:
        canonical_os = canonical_kernel = os
    else:
        die('Unknown OS: %s' % os)

    # The CPU granularity is probably not enough. Moving more things from
    # old-configure will tell us if we need more
    if cpu.endswith('86') or (cpu.startswith('i') and '86' in cpu):
        canonical_cpu = 'x86'
        endianness = 'little'
    elif cpu in ('x86_64', 'ia64'):
        canonical_cpu = cpu
        endianness = 'little'
    elif cpu in ('s390', 's390x'):
        canonical_cpu = cpu
        endianness = 'big'
    elif cpu in ('powerpc64', 'ppc64', 'powerpc64le', 'ppc64le'):
        canonical_cpu = 'ppc64'
        endianness = 'little' if 'le' in cpu else 'big'
    elif cpu in ('powerpc', 'ppc', 'rs6000') or cpu.startswith('powerpc'):
        canonical_cpu = 'ppc'
        endianness = 'big'
    elif cpu in ('Alpha', 'alpha', 'ALPHA'):
        canonical_cpu = 'Alpha'
        endianness = 'little'
    elif cpu.startswith('hppa') or cpu == 'parisc':
        canonical_cpu = 'hppa'
        endianness = 'big'
    elif cpu.startswith('sparc64') or cpu.startswith('sparcv9'):
        canonical_cpu = 'sparc64'
        endianness = 'big'
    elif cpu.startswith('sparc') or cpu == 'sun4u':
        canonical_cpu = 'sparc'
        endianness = 'big'
    elif cpu.startswith('arm'):
        canonical_cpu = 'arm'
        endianness = 'big' if cpu.startswith(('armeb', 'armbe')) else 'little'
    elif cpu in ('mips', 'mipsel'):
        canonical_cpu = 'mips32'
        endianness = 'little' if 'el' in cpu else 'big'
    elif cpu in ('mips64', 'mips64el'):
        canonical_cpu = 'mips64'
        endianness = 'little' if 'el' in cpu else 'big'
    elif cpu.startswith('aarch64'):
        canonical_cpu = 'aarch64'
        endianness = 'little'
    elif cpu == 'sh4':
        canonical_cpu = 'sh4'
        endianness = 'little'
    elif allow_unknown:
        canonical_cpu = cpu
        endianness = 'unknown'
    else:
        die('Unknown CPU type: %s' % cpu)

    def sanitize(cls, value):
        try:
            return cls(value)
        except (KeyError, ValueError):
            if allow_unknown:
                return value
            raise

    def bitness(cpu):
        return CPU_bitness[cpu]

    return namespace(
        alias=triplet,
        cpu=sanitize(CPU, canonical_cpu),
        bitness=sanitize(bitness, canonical_cpu),
        kernel=sanitize(Kernel, canonical_kernel),
        os=sanitize(OS, canonical_os),
        endianness=sanitize(Endianness, endianness),
        raw_cpu=cpu,
        raw_os=os,
        # Toolchains, most notably for cross compilation may use cpu-os
        # prefixes.
        toolchain='%s-%s' % (cpu, os),
    )


# This defines a fake target/host namespace for when running with --help
@depends('--help')
def help_host_target(help):
    if help:
        return namespace(
            alias='unknown-unknown-unknown',
            cpu='unknown',
            bitness='unknown',
            kernel='unknown',
            os='unknown',
            endianness='unknown',
            raw_cpu='unknown',
            raw_os='unknown',
            toolchain='unknown-unknown',
        )


@imports('subprocess')
def config_sub(shell, triplet):
    config_sub = os.path.join(os.path.dirname(__file__), '..',
                              'autoconf', 'config.sub')
    return subprocess.check_output([shell, config_sub, triplet]).strip()


@depends('--host', shell)
@checking('for host system type', lambda h: h.alias)
@imports('subprocess')
def host(value, shell):
    if not value:
        config_guess = os.path.join(os.path.dirname(__file__), '..',
                                    'autoconf', 'config.guess')
        host = subprocess.check_output([shell, config_guess]).strip()
    else:
        host = value[0]

    return split_triplet(config_sub(shell, host))


host = help_host_target | host


@depends('--target', host, shell)
@checking('for target system type', lambda t: t.alias)
def target(value, host, shell):
    if not value:
        return host
    return split_triplet(config_sub(shell, value[0]))


target = help_host_target | target


@depends(host, target)
@checking('whether cross compiling')
def cross_compiling(host, target):
    return host != target


set_config('CROSS_COMPILE', cross_compiling)
set_define('CROSS_COMPILE', cross_compiling)
add_old_configure_assignment('CROSS_COMPILE', cross_compiling)


@depends(target)
def have_64_bit(target):
    if target.bitness == 64:
        return True


set_config('HAVE_64BIT_BUILD', have_64_bit)
set_define('HAVE_64BIT_BUILD', have_64_bit)
add_old_configure_assignment('HAVE_64BIT_BUILD', have_64_bit)


@depends(host)
def host_os_kernel_major_version(host):
    versions = host.raw_os.split('.')
    version = ''.join(x for x in versions[0] if x.isdigit())
    return version


set_config('HOST_MAJOR_VERSION', host_os_kernel_major_version)

# Autoconf needs these set


@depends(host)
def host_for_old_configure(host):
    return '--host=%s' % host.alias


add_old_configure_arg(host_for_old_configure)


@depends(target)
def target_for_old_configure(target):
    target_alias = target.alias
    # old-configure does plenty of tests against $target and $target_os
    # and expects darwin for iOS, so make it happy.
    if target.os == 'iOS':
        target_alias = target_alias.replace('-ios', '-darwin')
    return '--target=%s' % target_alias


add_old_configure_arg(target_for_old_configure)


# These variables are for compatibility with the current moz.builds and
# old-configure. Eventually, we'll want to canonicalize better.
@depends(target)
def target_variables(target):
    if target.kernel == 'kFreeBSD':
        os_target = 'GNU/kFreeBSD'
        os_arch = 'GNU_kFreeBSD'
    elif target.kernel == 'Darwin' or (target.kernel == 'Linux' and
                                       target.os == 'GNU'):
        os_target = target.kernel
        os_arch = target.kernel
    else:
        os_target = target.os
        os_arch = target.kernel

    if target.kernel == 'Darwin' and target.cpu == 'x86':
        os_test = 'i386'
    else:
        os_test = target.raw_cpu

    return namespace(
        OS_TARGET=os_target,
        OS_ARCH=os_arch,
        OS_TEST=os_test,
        INTEL_ARCHITECTURE=target.cpu in ('x86', 'x86_64') or None,
    )


set_config('OS_TARGET', target_variables.OS_TARGET)
add_old_configure_assignment('OS_TARGET',
                             target_variables.OS_TARGET)
set_config('OS_ARCH', target_variables.OS_ARCH)
add_old_configure_assignment('OS_ARCH',
                             target_variables.OS_ARCH)
set_config('OS_TEST', target_variables.OS_TEST)
add_old_configure_assignment('OS_TEST',
                             target_variables.OS_TEST)
set_config('CPU_ARCH', target.cpu)
add_old_configure_assignment('CPU_ARCH', target.cpu)
set_config('INTEL_ARCHITECTURE', target_variables.INTEL_ARCHITECTURE)
set_config('TARGET_CPU', target.raw_cpu)
set_config('TARGET_OS', target.raw_os)
set_config('TARGET_ENDIANNESS', target.endianness)


@depends(host)
def host_variables(host):
    if host.kernel == 'kFreeBSD':
        os_arch = 'GNU_kFreeBSD'
    else:
        os_arch = host.kernel
    return namespace(
        HOST_OS_ARCH=os_arch,
    )


set_config('HOST_CPU_ARCH', host.cpu)
set_config('HOST_OS_ARCH', host_variables.HOST_OS_ARCH)
add_old_configure_assignment('HOST_OS_ARCH',
                             host_variables.HOST_OS_ARCH)


@depends(target)
def target_is_windows(target):
    if target.kernel == 'WINNT':
        return True


set_define('_WINDOWS', target_is_windows)
set_define('WIN32', target_is_windows)
set_define('XP_WIN', target_is_windows)
set_define('XP_WIN32', target_is_windows)


@depends(target)
def target_is_unix(target):
    if target.kernel != 'WINNT':
        return True


set_define('XP_UNIX', target_is_unix)


@depends(target)
def target_is_darwin(target):
    if target.kernel == 'Darwin':
        return True


set_define('XP_DARWIN', target_is_darwin)


@depends(target)
def target_is_ios(target):
    if target.kernel == 'Darwin' and target.os == 'iOS':
        return True


set_define('XP_IOS', target_is_ios)


@depends(target)
def target_is_osx(target):
    if target.kernel == 'Darwin' and target.os == 'OSX':
        return True


set_define('XP_MACOSX', target_is_osx)


@depends(target)
def target_is_linux(target):
    if target.kernel == 'Linux':
        return True


set_define('XP_LINUX', target_is_linux)


@depends(target)
def target_is_openbsd(target):
    if target.kernel == 'OpenBSD':
        return True


set_define('XP_OPENBSD', target_is_openbsd)

@depends(target)
def target_is_netbsd(target):
    if target.kernel == 'NetBSD':
        return True


set_define('XP_NETBSD', target_is_netbsd)

@depends(target)
def target_is_freebsd(target):
    if target.kernel == 'FreeBSD':
        return True


set_define('XP_FREEBSD', target_is_freebsd)

@depends(target)
def target_is_solaris(target):
    if target.kernel == 'SunOS':
        return True


set_define('XP_SOLARIS', target_is_solaris)

# The application/project to build
# ==============================================================
option('--enable-application', nargs=1, env='MOZ_BUILD_APP',
       help='Application to build. Same as --enable-project.')


@depends('--enable-application', '--help')
def application(app, help):
    if app:
        return app


imply_option('--enable-project', application)


@depends(check_build_environment, '--help')
def default_project(build_env, help):
    if build_env.topobjdir.endswith('/js/src'):
        return 'js'
    return 'browser'


option('--enable-project', nargs=1, default=default_project,
       help='Project to build')


@depends('--enable-project', '--with-external-source-dir',
         check_build_environment, '--help')
@imports(_from='os.path', _import='exists')
def include_project_configure(project, external_source_dir, build_env, help):
    if not project:
        die('--enable-project is required.')

    base_dir = build_env.topsrcdir
    if external_source_dir:
        base_dir = os.path.join(base_dir, external_source_dir[0])

    path = os.path.join(base_dir, project[0], 'moz.configure')
    if not exists(path):
        die('Cannot find project %s', project[0])
    return path


@depends(include_project_configure, check_build_environment, '--help')
def build_project(include_project_configure, build_env, help):
    ret = os.path.dirname(os.path.relpath(include_project_configure,
                                          build_env.topsrcdir))
    return ret


set_config('MOZ_BUILD_APP', build_project)
set_define('MOZ_BUILD_APP', build_project)
add_old_configure_assignment('MOZ_BUILD_APP', build_project)


# This is temporary until js/src/configure and configure are merged.
# Use instead of option() in js/moz.configure and more generally, for
# options that are shared between configure and js/src/configure.
@template
def js_option(*args, **kwargs):
    opt = option(*args, **kwargs)

    @depends(opt.option, build_project, when=kwargs.get('when'))
    def js_option(value, build_project):
        if build_project != 'js':
            return value.format(opt.option)

    add_old_configure_arg(js_option)


js_option(env='MOZILLA_OFFICIAL',
          help='Build an official release')


@depends('MOZILLA_OFFICIAL')
def mozilla_official(official):
    if official:
        return True


set_config('MOZILLA_OFFICIAL', mozilla_official)
set_define('MOZILLA_OFFICIAL', mozilla_official)
add_old_configure_assignment('MOZILLA_OFFICIAL', mozilla_official)


# set RELEASE_OR_BETA and NIGHTLY_BUILD variables depending on the cycle we're in
# The logic works like this:
# - if we have "a1" in GRE_MILESTONE, we're building Nightly (define NIGHTLY_BUILD)
# - otherwise, if we have "a" in GRE_MILESTONE, we're building Nightly or Aurora
# - otherwise, we're building Release/Beta (define RELEASE_OR_BETA)
@depends(check_build_environment, build_project, '--help')
@imports(_from='__builtin__', _import='open')
@imports('os')
@imports('re')
def milestone(build_env, build_project, _):
    versions = []
    paths = ['config/milestone.txt']
    if build_project == 'js':
        paths = paths * 3
    else:
        paths += [
            'browser/config/version.txt',
            'browser/config/version_display.txt',
        ]
    for f in ('version.txt', 'version_display.txt'):
        f = os.path.join(build_project, f)
        if not os.path.exists(os.path.join(build_env.topsrcdir, f)):
            break
        paths.append(f)

    for p in paths:
        with open(os.path.join(build_env.topsrcdir, p), 'r') as fh:
            content = fh.read().splitlines()
            if not content:
                die('Could not find a version number in {}'.format(p))
            versions.append(content[-1])

    milestone, firefox_version, firefox_version_display = versions[:3]

    # version.txt content from the project directory if there is one, otherwise
    # the firefox version.
    app_version = versions[3] if len(versions) > 3 else firefox_version
    # version_display.txt content from the project directory if there is one,
    # otherwise version.txt content from the project directory, otherwise the
    # firefox version for display.
    app_version_display = versions[-1] if len(versions) > 3 else firefox_version_display

    is_nightly = is_release_or_beta = None

    if 'a1' in milestone:
        is_nightly = True
    elif 'a' not in milestone:
        is_release_or_beta = True

    major_version = milestone.split('.')[0]
    m = re.search(r"([ab]\d+)", milestone)
    ab_patch = m.group(1) if m else ''

    # Only expose the major version milestone in the UA string and hide the
    # patch leve (bugs 572659 and 870868).
    #
    # Only expose major milestone and alpha version in the symbolversion
    # string; as the name suggests, we use it for symbol versioning on Linux.
    return namespace(version=milestone,
                     uaversion='%s.0' % major_version,
                     symbolversion='%s%s' % (major_version, ab_patch),
                     is_nightly=is_nightly,
                     is_release_or_beta=is_release_or_beta,
                     app_version=app_version,
                     app_version_display=app_version_display)


set_config('GRE_MILESTONE', milestone.version)
set_config('NIGHTLY_BUILD', milestone.is_nightly)
set_define('NIGHTLY_BUILD', milestone.is_nightly)
add_old_configure_assignment('NIGHTLY_BUILD', milestone.is_nightly)
set_config('RELEASE_OR_BETA', milestone.is_release_or_beta)
set_define('RELEASE_OR_BETA', milestone.is_release_or_beta)
add_old_configure_assignment('RELEASE_OR_BETA',
                             milestone.is_release_or_beta)
set_define('MOZILLA_VERSION', depends(milestone)(lambda m: '"%s"' % m.version))
set_config('MOZILLA_VERSION', milestone.version)
set_define('MOZILLA_VERSION_U', milestone.version)
set_define('MOZILLA_UAVERSION', depends(milestone)(lambda m: '"%s"' % m.uaversion))
set_config('MOZILLA_SYMBOLVERSION', milestone.symbolversion)
# JS configure still wants to look at these.
add_old_configure_assignment('MOZILLA_VERSION', milestone.version)
add_old_configure_assignment('MOZILLA_SYMBOLVERSION', milestone.symbolversion)

set_config('MOZ_APP_VERSION', milestone.app_version)
set_config('MOZ_APP_VERSION_DISPLAY', milestone.app_version_display)
add_old_configure_assignment('MOZ_APP_VERSION', milestone.app_version)

# The app update channel is 'default' when not supplied. The value is used in
# the application's confvars.sh (and is made available to a project specific
# moz.configure).
option('--enable-update-channel',
       nargs=1,
       help='Select application update channel',
       default='default')


@depends('--enable-update-channel')
def update_channel(channel):
    if channel[0] == '':
        return 'default'
    return channel[0].lower()


set_config('MOZ_UPDATE_CHANNEL', update_channel)
set_define('MOZ_UPDATE_CHANNEL', update_channel)
add_old_configure_assignment('MOZ_UPDATE_CHANNEL', update_channel)


# A template providing a shorthand for setting a variable. The created
# option will only be settable with imply_option.
# It is expected that a project-specific moz.configure will call imply_option
# to set a value other than the default.
# If required, the set_as_define and set_for_old_configure arguments
# will additionally cause the variable to be set using set_define and
# add_old_configure_assignment. util.configure would be an appropriate place for
# this, but it uses add_old_configure_assignment, which is defined in this file.
@template
def project_flag(env=None, set_for_old_configure=False,
                 set_as_define=False, **kwargs):

    if not env:
        configure_error(
            "A project_flag must be passed a variable name to set.")

    opt = option(env=env, possible_origins=('implied',), **kwargs)

    @depends(opt.option)
    def option_implementation(value):
        if value:
            if len(value):
                return value
            return bool(value)

    set_config(env, option_implementation)
    if set_as_define:
        set_define(env, option_implementation)
    if set_for_old_configure:
        add_old_configure_assignment(env, option_implementation)

# milestone.is_nightly corresponds to cases NIGHTLY_BUILD is set.


@depends(milestone, '--help')
def enabled_in_nightly(milestone, _):
    return milestone.is_nightly

# Set the MOZ_CONFIGURE_OPTIONS variable with all the options that
# were passed somehow (environment, command line, mozconfig)


@dependable
@imports(_from='mozbuild.shellutil', _import='quote')
@imports('__sandbox__')
def all_configure_options():
    result = []
    previous = None
    for option in __sandbox__._options.itervalues():
        # __sandbox__._options contains items for both option.name and
        # option.env. But it's also an OrderedDict, meaning both are
        # consecutive.
        # Also ignore OLD_CONFIGURE and MOZCONFIG because they're not
        # interesting.
        if option == previous or option.env in ('OLD_CONFIGURE', 'MOZCONFIG'):
            continue
        previous = option
        value = __sandbox__._value_for(option)
        # We only want options that were explicitly given on the command
        # line, the environment, or mozconfig, and that differ from the
        # defaults.
        if (value is not None and value.origin not in ('default', 'implied') and
                value != option.default):
            result.append(__sandbox__._raw_options[option])
        # We however always include options that are sent to old configure
        # because we don't know their actual defaults. (Keep the conditions
        # separate for ease of understanding and ease of removal)
        elif (option.help == 'Help missing for old configure options' and
                option in __sandbox__._raw_options):
            result.append(__sandbox__._raw_options[option])

    return quote(*result)


set_config('MOZ_CONFIGURE_OPTIONS', all_configure_options)