Source code

Revision control

Other Tools

# -*- coding: utf-8 -*-
# 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
from __future__ import absolute_import, print_function, unicode_literals
import argparse
import json
import os
import textwrap
from mach.base import FailedCommandError, MachError
from mach.decorators import (
from mach.registrar import Registrar
from mozbuild.mozconfig import MozconfigLoader
from mozbuild.base import MachCommandBase
# Command files like this are listed in build/ in alphabetical
# order, but we need to access commands earlier in the sorted order to grab
# their arguments. Force them to load now.
import mozbuild.artifact_commands # NOQA: F401
import mozbuild.build_commands # NOQA: F401
# Use a decorator to copy command arguments off of the named command. Instead
# of a decorator, this could be straight code that edits eg
# MachCommands.build_shell._mach_command.arguments, but that looked uglier.
def inherit_command_args(command, subcommand=None):
'''Decorator for inheriting all command-line arguments from `mach build`.
This should come earlier in the source file than @Command or @SubCommand,
because it relies on that decorator having run first.'''
def inherited(func):
handler = Registrar.command_handlers.get(command)
if handler is not None and subcommand is not None:
handler = handler.subcommand_handlers.get(subcommand)
if handler is None:
raise MachError("{} command unknown or not yet loaded".format(
command if subcommand is None else command + " " + subcommand))
return func
return inherited
class MachCommands(MachCommandBase):
def state_dir(self):
return os.environ.get('MOZBUILD_STATE_PATH',
def tools_dir(self):
return os.path.join(self.state_dir, 'hazard-tools')
def ensure_tools_dir(self):
dir = self.tools_dir
except OSError:
return dir
def sixgill_dir(self):
return os.path.join(self.tools_dir, 'sixgill')
def gcc_dir(self):
return os.path.join(self.tools_dir, 'gcc')
def work_dir(self):
return os.path.join(self.topsrcdir, "analysis")
def ensure_work_dir(self):
dir = self.work_dir
except OSError:
return dir
def script_dir(self):
return os.path.join(self.topsrcdir, "js/src/devtools/rootAnalysis")
@Command('hazards', category='build', order='declaration',
description='Commands for running the static analysis for GC rooting hazards')
def hazards(self):
"""Commands related to performing the GC rooting hazard analysis"""
print("See `mach hazards --help` for a list of subcommands")
@inherit_command_args('artifact', 'toolchain')
@SubCommand('hazards', 'bootstrap',
description='Install prerequisites for the hazard analysis')
def bootstrap(self, **kwargs):
orig_dir = os.getcwd()
kwargs['from_build'] = ('linux64-gcc-sixgill', 'linux64-gcc-8')
'artifact', self._mach_context, subcommand='toolchain',
@SubCommand('hazards', 'build-shell',
description='Build a shell for the hazard analysis')
@CommandArgument('--mozconfig', default=None, metavar='FILENAME',
help='Build with the given mozconfig.')
def build_shell(self, **kwargs):
'''Build a JS shell to use to run the rooting hazard analysis.'''
# The JS shell requires some specific configuration settings to execute
# the hazard analysis code, and configuration is done via mozconfig.
# Subprocesses find MOZCONFIG in the environment, so we can't just
# modify the settings in this process's loaded version. Pass it through
# the environment.
default_mozconfig = 'js/src/devtools/rootAnalysis/mozconfig.haz_shell'
mozconfig_path = kwargs.pop('mozconfig', None) \
or os.environ.get('MOZCONFIG') \
or default_mozconfig
mozconfig_path = os.path.join(self.topsrcdir, mozconfig_path)
loader = MozconfigLoader(self.topsrcdir)
mozconfig = loader.read_mozconfig(mozconfig_path)
# Validate the mozconfig settings in case the user overrode the default.
configure_args = mozconfig['configure_args']
if '--enable-ctypes' not in configure_args:
raise FailedCommandError('ctypes required in hazard JS shell')
# Transmit the mozconfig location to build subprocesses.
os.environ['MOZCONFIG'] = mozconfig_path
# Record the location of the JS shell in the analysis work dir.
self.write_json_file("shell.json", {
'js': os.path.join(mozconfig['topobjdir'], "dist/bin/js")
return self._mach_context.commands.dispatch(
'build', self._mach_context, **kwargs
def check_application(self, requested_app, objdir=None):
'''Verify that the objdir and work dir are for the expected application
work_dir_app = self.read_json_file('app.json')['application']
if work_dir_app != requested_app:
raise FailedCommandError(
'work dir {} is for the wrong app {}'.format(
self.work_dir, work_dir_app
except IOError:
# work dir has not been created or initialized yet, so we're good.
if not objdir:
objdir = self.topobjdir
mozinfo = self.read_json_file(os.path.join(objdir, 'mozinfo.json'))
if mozinfo.get('buildapp') != requested_app:
raise FailedCommandError(
'objdir {} is for the wrong app {}, clobber required'.format(
objdir, mozinfo.get('buildapp')
except (OSError, IOError):
def write_json_file(self, filename, data):
work = self.ensure_work_dir()
with open(os.path.join(work, filename), "wt") as fh:
json.dump(data, fh)
def read_json_file(self, filename):
with open(os.path.join(self.work_dir, filename)) as fh:
return json.load(fh)
def ensure_shell(self):
return self.read_json_file("shell.json")['js']
except OSError:
raise FailedCommandError(
'must build the JS shell with `mach hazards build-shell` first'
@SubCommand('hazards', 'gather',
description='Gather analysis data by compiling the given application')
@CommandArgument('--application', default='browser',
help='Build the given application.')
def gather_hazard_data(self, application=None):
'''Gather analysis information by compiling the tree'''
shell_path = self.ensure_shell()
objdir = os.path.join(self.topsrcdir, "obj-analyzed")
self.check_application(application, objdir)
self.write_json_file('app.json', {'application': application})
with open(os.path.join(self.work_dir, ""), "wt") as fh:
data = textwrap.dedent('''\
js = "{js}"
analysis_scriptdir = "{script_dir}"
objdir = "{objdir}"
source = "{srcdir}"
sixgill = "{sixgill_dir}/usr/libexec/sixgill"
sixgill_bin = "{sixgill_dir}/usr/bin"
gcc_bin = "{gcc_dir}/bin"
buildscript = '{srcdir}/mach hazards compile --application={app}'.format(
args = [
os.path.join(self.script_dir, ""),
'dbs', '--upto', 'dbs',
'--buildcommand=' + buildscript,
return self.run_process(args=args, cwd=self.work_dir, pass_thru=True)
@SubCommand('hazards', 'compile', description=argparse.SUPPRESS)
@CommandArgument('--mozconfig', default=None, metavar='FILENAME',
help='Build with the given mozconfig.')
@CommandArgument('--application', default='browser',
help='Build the given application.')
def inner_compile(self, **kwargs):
'''Build a source tree and gather analysis information while running
under the influence of the analysis collection server.'''
env = os.environ
# Check whether we are running underneath the manager (and therefore
# have a server to talk to).
if 'XGILL_CONFIG' not in env:
raise Exception(
'no sixgill manager detected. `mach hazards compile` ' +
'should only be run from `mach hazards gather`'
app = kwargs.pop('application')
default_mozconfig = 'js/src/devtools/rootAnalysis/mozconfig.%s' % app
mozconfig_path = kwargs.pop('mozconfig', None) \
or env.get('MOZCONFIG') \
or default_mozconfig
mozconfig_path = os.path.join(self.topsrcdir, mozconfig_path)
# Validate the mozconfig.
# Require an explicit --enable-application=APP (even if you just
# want to build the default browser application.)
loader = MozconfigLoader(self.topsrcdir)
mozconfig = loader.read_mozconfig(mozconfig_path)
configure_args = mozconfig['configure_args']
if '--enable-application=%s' % app not in configure_args:
raise Exception('mozconfig %s builds wrong project' % mozconfig_path)
if not any('--with-compiler-wrapper' in a for a in configure_args):
raise Exception('mozconfig must wrap compiles')
# Communicate mozconfig to build subprocesses.
env['MOZCONFIG'] = os.path.join(self.topsrcdir, mozconfig_path)
# hazard mozconfigs need to find binaries in .mozbuild
env['MOZBUILD_STATE_PATH'] = self.state_dir
# Force the use of hazard-compatible installs of tools.
gccbin = os.path.join(self.gcc_dir, 'bin')
env['CC'] = os.path.join(gccbin, 'gcc')
env['CXX'] = os.path.join(gccbin, 'g++')
env['PATH'] = '{sixgill_dir}/usr/bin:{gccbin}:{PATH}'.format(
env['LD_LIBRARY_PATH'] = '{}/lib64'.format(self.gcc_dir)
return self._mach_context.commands.dispatch(
'build', self._mach_context, **kwargs
@SubCommand('hazards', 'analyze',
description='Analyzed gathered data for rooting hazards')
def analyze(self):
'''Analyzed gathered data for rooting hazards'''
args = [
os.path.join(self.script_dir, ""),
return self.run_process(args=args, cwd=self.work_dir, pass_thru=True)