# 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/.
import argparse import html import json import logging import os import re import textwrap import webbrowser
# Command files like this are listed in build/mach_initialize.py 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 import mozhttpd from mach.base import FailedCommandError, MachError from mach.decorators import Command, CommandArgument, SubCommand from mach.registrar import Registrar from mozbuild.base import BuildEnvironmentNotFoundException from mozbuild.mozconfig import MozconfigLoader
# 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 isnotNoneand subcommand isnotNone:
handler = handler.subcommand_handlers.get(subcommand) if handler isNone: raise MachError( "{} command unknown or not yet loaded".format(
command if subcommand isNoneelse command + " " + subcommand
)
)
func._mach_command.arguments.extend(handler.arguments) return func
def tools_dir(): if os.environ.get("MOZ_FETCHES_DIR"): # In automation, tools are provided by toolchain dependencies. return os.path.join(os.environ["HOME"], os.environ["MOZ_FETCHES_DIR"])
# In development, `mach hazard bootstrap` installs the tools separately # to avoid colliding with the "main" compiler versions, which can # change separately (and the precompiled sixgill and compiler version # must match exactly). return os.path.join(state_dir(), "hazard-tools")
def ensure_dir_exists(dir):
os.makedirs(dir, exist_ok=True) return dir
# Force the use of hazard-compatible installs of tools. def setup_env_for_tools(env):
gccbin = os.path.join(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(
sixgill_dir=sixgill_dir(), gccbin=gccbin, PATH=env["PATH"]
)
def setup_env_for_shell(env, shell): """Add JS shell directory to dynamic lib search path""" for var in ("LD_LIBRARY_PATH", "DYLD_LIBRARY_PATH"):
env[var] = ":".join(p for p in (env.get(var), os.path.dirname(shell)) if p)
@Command( "hazards",
category="build",
order="declaration",
description="Commands for running the static analysis for GC rooting hazards",
) def hazards(command_context): """Commands related to performing the GC rooting hazard analysis"""
print("See `mach hazards --help` for a list of subcommands")
@SubCommand("hazards", "clobber", description="Clean up hazard-related files")
@CommandArgument("--project", default="browser", help="Build the given project.")
@CommandArgument("--application", dest="project", help="Build the given project.")
@CommandArgument("--haz-objdir", default=None, help="Hazard analysis objdir.")
@CommandArgument( "--work-dir", default=None, help="Directory for output and working files."
)
@CommandArgument( "what",
default=["objdir", "work"],
nargs="*",
help="Target to clobber, must be one of {{{}}} (default " "objdir and work).".format(", ".join(CLOBBER_CHOICES)),
) def clobber(command_context, what, **kwargs): from mozbuild.controller.clobber import Clobberer
what = set(what) if"all"in what:
what.update(CLOBBER_CHOICES)
invalid = what - CLOBBER_CHOICES if invalid:
print( "Unknown clobber target(s): {}. Choose from {{{}}}".format( ", ".join(invalid), ", ".join(CLOBBER_CHOICES)
)
) return 1
@inherit_command_args("build")
@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(command_context, **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.
# Validate the mozconfig settings in case the user overrode the default.
configure_args = mozconfig["configure_args"] if"--enable-ctypes"notin configure_args: raise FailedCommandError( "ctypes required in hazard JS shell, mozconfig=" + mozconfig_path
)
# Transmit the mozconfig location to build subprocesses.
os.environ["MOZCONFIG"] = mozconfig_path
setup_env_for_tools(os.environ)
# Set a default objdir for the shell, for developer builds.
os.environ.setdefault( "MOZ_OBJDIR", os.path.join(command_context.topsrcdir, "obj-haz-shell")
)
def read_json_file(filename): with open(filename) as fh: return json.load(fh)
def ensure_shell(command_context, objdir): if objdir isNone:
objdir = os.path.join(command_context.topsrcdir, "obj-haz-shell")
try:
binaries = read_json_file(os.path.join(objdir, "binaries.json"))
info = [b for b in binaries["programs"] if b["program"] == "js"][0] return os.path.join(objdir, info["install_target"], "js") except (OSError, KeyError): raise FailedCommandError( """\
no shell found in %s -- must build the JS shell with `mach hazards build-shell` first"""
% objdir
)
# Require an explicit --enable-project/application=APP (even if you just # want to build the default browser project.) if ( "--enable-project=%s" % app notin configure_args and"--enable-application=%s" % app notin configure_args
): raise FailedCommandError(
textwrap.dedent(
f"""\
mozconfig {mozconfig_path} builds wrong project.
unset MOZCONFIG to use the default {default_mozconfig}\ """
)
)
ifnot any("--with-compiler-wrapper"in a for a in configure_args): raise FailedCommandError( "mozconfig must wrap compiles with --with-compiler-wrapper"
)
return mozconfig_path
@inherit_command_args("build")
@SubCommand( "hazards", "gather",
description="Gather analysis data by compiling the given project",
)
@CommandArgument("--project", default="browser", help="Build the given project.")
@CommandArgument("--application", dest="project", help="Build the given project.")
@CommandArgument( "--haz-objdir", default=None, help="Write object files to this directory."
)
@CommandArgument( "--work-dir", default=None, help="Directory for output and working files."
) def gather_hazard_data(command_context, **kwargs): """Gather analysis information by compiling the tree"""
project = kwargs["project"]
objdir = get_objdir(command_context, kwargs)
@inherit_command_args("build")
@SubCommand("hazards", "compile", description=argparse.SUPPRESS)
@CommandArgument( "--mozconfig",
default=None,
metavar="FILENAME",
help="Build with the given mozconfig.",
)
@CommandArgument("--project", default="browser", help="Build the given project.")
@CommandArgument("--application", dest="project", help="Build the given project.")
@CommandArgument( "--haz-objdir",
default=os.environ.get("HAZ_OBJDIR"),
help="Write object files to this directory.",
) def inner_compile(command_context, **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"notin env: raise FailedCommandError( "no sixgill manager detected. `mach hazards compile` "
+ "should only be run from `mach hazards gather`"
)
@SubCommand( "hazards", "analyze", description="Analyzed gathered data for rooting hazards"
)
@CommandArgument( "--project",
default="browser",
help="Analyze the output for the given project.",
)
@CommandArgument("--application", dest="project", help="Build the given project.")
@CommandArgument( "--shell-objdir",
default=None,
help="objdir containing the optimized JS shell for running the analysis.",
)
@CommandArgument( "--work-dir", default=None, help="Directory for output and working files."
)
@CommandArgument( "--jobs", "-j", default=None, type=int, help="Number of parallel analyzers."
)
@CommandArgument( "--verbose", "-v",
default=False,
action="store_true",
help="Display executed commands.",
)
@CommandArgument( "--from-stage",
default=None,
help="Stage to begin running at ('list' to see all).",
)
@CommandArgument( "extra",
nargs=argparse.REMAINDER,
default=(),
help="Remaining non-optional arguments to analyze.py script",
) def analyze(
command_context,
project,
shell_objdir,
work_dir,
jobs,
verbose,
from_stage,
extra,
): """Analyzed gathered data for rooting hazards"""
def annotated_source(filename, query): """The index page has URLs of the format <http://.../path/to/source.cpp?L=m-n#m>.
The `#m` part will be stripped off and used by the browser to jump to the correct line.
The `?L=m-n` or `?L=m` parameter will be processed here on the server to highlight
the given line range."""
linequery = query.replace("L=", "") if"-"in linequery:
line0, line1 = linequery.split("-", 1) else:
line0, line1 = linequery or"0", linequery or"0"
line0 = int(line0)
line1 = int(line1)
fh = open(filename, "rt")
out = "
"
for lineno, line in enumerate(fh, 1):
processed = f"{lineno} if line0 <= lineno and lineno <= line1:
processed += " style='background: yellow'"
processed += ">" + html.escape(line.rstrip()) + "\n"
out += processed
return out
@SubCommand( "hazards", "view", description="Display a web page describing any hazards found"
)
@CommandArgument( "--project",
default="browser",
help="Analyze the output for the given project.",
)
@CommandArgument("--application", dest="project", help="Build the given project.")
@CommandArgument( "--haz-objdir", default=None, help="Write object files to this directory."
)
@CommandArgument( "--work-dir", default=None, help="Directory for output and working files."
)
@CommandArgument("--port", default=6006, help="Port of the web server")
@CommandArgument( "--serve-only",
default=False,
action="store_true",
help="Serve only, do not navigate to page",
) def view_hazards(command_context, project, haz_objdir, work_dir, port, serve_only):
work_dir = get_work_dir(command_context, project, work_dir)
haztop = os.path.basename(work_dir) if haz_objdir isNone:
haz_objdir = os.environ.get("HAZ_OBJDIR") if haz_objdir isNone:
haz_objdir = os.path.join(command_context.topsrcdir, "obj-analyzed-" + project)
httpd = None
def serve_source_file(request, path):
info = {"req": path}
# Allow files to be served from the source directory or the objdir.
roots = (command_context.topsrcdir, haz_objdir)
try: # Validate the path. Some source files have weird characters in their paths (eg "+"), but they # all start with an alphanumeric or underscore.
command_context.log(
logging.DEBUG, "view-hazards", {"path": path}, "Raw path: {path}"
)
path_component = r"\w[\w\-\.\+]*" ifnot re.match(f"({path_component}/)*{path_component}$", path): raise ValueError("invalid path")
# Resolve the path to under one of the roots, and # ensure that the actual file really is underneath a root directory. for rootdir in roots:
fullpath = os.path.join(rootdir, path)
info["path"] = fullpath
fullpath = os.path.realpath(fullpath) if os.path.isfile(fullpath): # symlinks between roots are ok, but not symlinks outside of the roots.
tops = [
d for d in roots if fullpath.startswith(os.path.realpath(d) + "/")
] if len(tops) > 0: break# Found a file underneath a root. else: raise IOError("not found")
html = annotated_source(fullpath, request.query)
log("serve '{req}' -> 200 {path}") return (
200,
{"Content-type": "text/html", "Content-length": len(html)},
html,
) except (IOError, ValueError):
log("serve '{req}' -> 404 {path}", logging.ERROR) return (
404,
{"Content-type": "text/plain"}, "We don't have that around here. Don't be asking for it.",
)
httpd = mozhttpd.MozHttpd(
port=port,
docroot=None,
path_mappings={"/" + haztop: work_dir},
urlhandlers=[ # Treat everything not starting with /haz-browser/ (or /haz-js/) # as a source file to be processed. Everything else is served # as a plain file.
{ "method": "GET", "path": "/(?!haz-" + project + "/)(.*)", "function": serve_source_file,
},
],
log_requests=True,
)
# The mozhttpd request handler class eats log messages.
httpd.handler_class.log_message = lambda self, format, *args: command_context.log(
logging.INFO, "view-hazards", {}, format % args
)
print("Serving at %s:%s" % (httpd.host, httpd.port))
httpd.start(block=False)
url = httpd.get_url(f"/{haztop}/hazards.html")
display_url = True ifnot serve_only: try:
webbrowser.get().open_new_tab(url)
display_url = False except Exception: pass if display_url:
print("Please open %s in a browser." % url)
print("Hit CTRL+c to stop server.")
httpd.server.join()
¤ Dauer der Verarbeitung: 0.18 Sekunden
(vorverarbeitet)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.