blob: b3c66a0998004ff35a0224a53bb7ef37084a6721 [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
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 = None
self._llvm_symbolizer = None
self._symbolizer_exec = None
self._platform = 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 set_build_ids(self, build_ids):
"""Sets the build IDs used to symbolize logs."""
if not os.path.exists(build_ids):
raise Host.ConfigError('Unable to find builds IDs.')
self._ids = 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_platform(self, platform):
"""Sets the platform used for host OS-specific behavior."""
if not os.path.isdir(Host.join('buildtools', platform)):
raise Host.ConfigError('Unsupported host platform: ' + platform)
self._platform = platform
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))
def set_build_dir(self, build_dir):
"""Configure the host using data from a build directory."""
self.set_build_ids(Host.join(build_dir, 'ids.txt'))
self.set_zxtools(Host.join(build_dir + '.zircon', 'tools'))
platform = 'mac-x64' if os.uname()[0] == 'Darwin' else 'linux-x64'
self.set_platform(platform)
self.set_symbolizer(
Host.join('prebuilt', 'tools', 'symbolize', platform, 'symbolize'),
Host.join('buildtools', platform, 'clang', 'bin',
'llvm-symbolizer'))
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 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])
if logfile:
subprocess.Popen(cmd, stdout=logfile, stderr=subprocess.STDOUT)
else:
return subprocess.check_output(cmd, stderr=Host.DEVNULL).strip()
def killall(self, process):
""" Invokes killall on the process name."""
subprocess.call(
['killall', process], stdout=Host.DEVNULL, stderr=Host.DEVNULL)
def symbolize(self, log_in, log_out):
"""Symbolizes backtraces in a log file using the current build."""
if not self._symbolizer_exec:
raise Host.ConfigError('Symbolizer executable not set.')
if not self._ids:
raise Host.ConfigError('Build IDs not set.')
if not self._llvm_symbolizer:
raise Host.ConfigError('LLVM symbolizer not set.')
subprocess.check_call(
[
self._symbolizer_exec, '-ids-rel', '-ids', self._ids,
'-llvm-symbolizer', self._llvm_symbolizer
],
stdin=log_in,
stdout=log_out)
def notify_user(self, title, body):
"""Displays a message to the user in a platform-specific way"""
if not self._platform:
return
elif self._platform == 'mac-x64':
subprocess.call([
'osascript', '-e', 'display notification "' + body +
'" with title "' + title + '"'
])
elif subprocess.call(
['which', 'notify-send'], stdout=Host.DEVNULL,
stderr=Host.DEVNULL) == 0:
subprocess.call(
['notify-send', title, body],
stdout=Host.DEVNULL,
stderr=Host.DEVNULL)
else:
subprocess.call(
['wall', title + '; ' + body],
stdout=Host.DEVNULL,
stderr=Host.DEVNULL)
def snapshot(self):
integration = Host.join('integration')
if not os.path.isdir(integration):
raise Host.ConfigError('Missing integration repo.')
return subprocess.check_output(
['git', 'rev-parse', 'HEAD'], stderr=Host.DEVNULL,
cwd=integration).strip()