blob: 0e51458572f1717041ceef0a577f4a2087acb893 [file] [log] [blame]
#!/usr/bin/env python2.7
# Copyright 2019 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import errno
import json
import os
import re
import subprocess
from process import Process
class Host(object):
"""Represents a local system with a build of Fuchsia.
This class abstracts the details of various repository, tool, and build
paths, as well as details about the host architecture and platform.
Attributes:
fuzzers: The fuzzer binaries available in the current Fuchsia image
build_dir: The build output directory, if present.
"""
# Convenience file descriptor for silencing subprocess output
DEVNULL = open(os.devnull, 'w')
class ConfigError(RuntimeError):
"""Indicates the host is not configured for building Fuchsia."""
pass
@classmethod
def from_build(cls):
"""Uses a local build directory to configure a Host object from it."""
host = Host()
host.set_build_dir(host.find_build_dir())
return host
@classmethod
def join(cls, *segments):
"""Creates a source tree path."""
fuchsia = os.getenv('FUCHSIA_DIR')
if not fuchsia:
raise Host.ConfigError(
'Unable to find FUCHSIA_DIR; have you `fx set`?')
return os.path.join(fuchsia, *segments)
def __init__(self):
self._ids = []
self._llvm_symbolizer = None
self._platform = 'mac-x64' if os.uname()[0] == 'Darwin' else 'linux-x64'
self._symbolizer_exec = None
self._zxtools = None
self.fuzzers = []
self.build_dir = None
@classmethod
def find_build_dir(self):
"""Examines the source tree to locate a build directory."""
build_dir = Host.join('.fx-build-dir')
if not os.path.exists(build_dir):
raise Host.ConfigError(
'Unable to find .fx-build-dir; have you `fx set`?')
with open(build_dir, 'r') as f:
return Host.join(f.read().strip())
def get_host_out_dir(self):
return Host.join(Host.find_build_dir(), 'host_x64')
def add_build_ids(self, build_ids):
"""Sets the build IDs used to symbolize logs."""
if os.path.exists(build_ids):
self._ids.append(build_ids)
def set_zxtools(self, zxtools):
"""Sets the location of the Zircon host tools directory."""
if not os.path.isdir(zxtools):
raise Host.ConfigError('Unable to find Zircon host tools.')
self._zxtools = zxtools
def set_symbolizer(self, executable, symbolizer):
"""Sets the paths to both the wrapper and LLVM symbolizers."""
if not os.path.exists(executable) or not os.access(executable, os.X_OK):
raise Host.ConfigError('Invalid symbolize binary: ' + executable)
if not os.path.exists(symbolizer) or not os.access(symbolizer, os.X_OK):
raise Host.ConfigError('Invalid LLVM symbolizer: ' + symbolizer)
self._symbolizer_exec = executable
self._llvm_symbolizer = symbolizer
def set_fuzzers_json(self, json_file):
"""Sets the path to the build file with fuzzer metadata."""
if not os.path.exists(json_file):
raise Host.ConfigError('Unable to find list of fuzzers.')
self.fuzzers = []
with open(json_file) as f:
fuzz_specs = json.load(f)
for fuzz_spec in fuzz_specs:
pkg = fuzz_spec['fuzzers_package']
for tgt in fuzz_spec['fuzzers']:
self.fuzzers.append((pkg, tgt))
self.fuzzers.sort()
def set_build_dir(self, build_dir):
"""Configure the host using data from a build directory."""
self.set_zxtools(Host.join(build_dir + '.zircon', 'tools'))
clang_dir = os.path.join(
'prebuilt', 'third_party', 'clang', self._platform)
self.set_symbolizer(
Host.join(self.get_host_out_dir(), 'symbolize'),
Host.join(clang_dir, 'bin', 'llvm-symbolizer'))
self.add_build_ids(Host.join('prebuilt_build_ids'))
self.add_build_ids(Host.join(clang_dir, 'lib', 'debug', '.build-id'))
self.add_build_ids(Host.join(build_dir, '.build-id'))
self.add_build_ids(Host.join(build_dir + '.zircon', '.build-id'))
json_file = Host.join(build_dir, 'fuzzers.json')
# fuzzers.json isn't emitted in release builds
if os.path.exists(json_file):
self.set_fuzzers_json(json_file)
self.build_dir = build_dir
def create_process(self, args, **kwargs):
return Process(args, **kwargs)
def fx_command(self, cmd, logfile=None):
"""Executes an `fx` command."""
fx_bin = Host.join('.jiri_root', 'bin', 'fx')
p = self.create_process([fx_bin] + cmd)
if logfile:
p.stdout = logfile
p.stderr = subprocess.STDOUT
p.popen()
else:
p.stderr = Host.DEVNULL
return p.check_output().strip()
def zircon_tool(self, cmd, logfile=None):
"""Executes a tool found in the ZIRCON_BUILD_DIR."""
if not self._zxtools:
raise Host.ConfigError('Zircon host tools unavailable.')
if not os.path.isabs(cmd[0]):
cmd[0] = os.path.join(self._zxtools, cmd[0])
if not os.path.exists(cmd[0]):
raise Host.ConfigError('Unable to find Zircon host tool: ' + cmd[0])
p = self.create_process(cmd)
if logfile:
p.stdout = logfile
p.stderr = subprocess.STDOUT
p.popen()
else:
p.stderr = Host.DEVNULL
return p.check_output().strip()
def killall(self, process):
""" Invokes killall on the process name."""
p = self.create_process(
['killall', process], stdout=Host.DEVNULL, stderr=Host.DEVNULL)
p.call()
def symbolize(self, raw):
"""Symbolizes backtraces in a log file using the current build.
Attributes:
raw: Bytes representing unsymbolized lines.
Returns:
Bytes representing symbolized lines.
"""
if not self._symbolizer_exec:
raise Host.ConfigError('Symbolizer executable not set.')
if len(self._ids) == 0:
raise Host.ConfigError('Build IDs not set.')
if not self._llvm_symbolizer:
raise Host.ConfigError('LLVM symbolizer not set.')
# Symbolize
args = [
self._symbolizer_exec, '-llvm-symbolizer', self._llvm_symbolizer
]
for build_id_dir in self._ids:
args += ['-build-id-dir', build_id_dir]
p = self.create_process(args)
p.stdin = subprocess.PIPE
p.stdout = subprocess.PIPE
proc = p.popen()
out, _ = proc.communicate(raw)
if proc.returncode != 0:
out = ''
return re.sub(r'[0-9\[\]\.]*\[klog\] INFO: ', '', out)
def notify_user(self, title, body):
"""Displays a message to the user in a platform-specific way"""
args = ['which', 'notify-send']
p = self.create_process(args, stdout=Host.DEVNULL, stderr=Host.DEVNULL)
if self._platform == 'mac-x64':
args = [
'osascript', '-e',
'display notification "' + body + '" with title "' + title + '"'
]
elif p.call() == 0:
args = ['notify-send', title, body]
else:
args = ['wall', title + '; ' + body]
return self.create_process(
args, stdout=Host.DEVNULL, stderr=Host.DEVNULL).call()
def snapshot(self):
integration = Host.join('integration')
if not os.path.isdir(integration):
raise Host.ConfigError('Missing integration repo.')
p = self.create_process(
['git', 'rev-parse', 'HEAD'], stderr=Host.DEVNULL, cwd=integration)
return p.check_output().strip()