blob: 6bb884870b9f4874102cc1866321b84dad28faa7 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2019 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Toolchain and related functionality."""
from __future__ import print_function
import collections
import os
import re
from chromite.lib import cros_build_lib
from chromite.lib import cros_logging as logging
from chromite.lib import gs
from chromite.lib import path_util
ORDERFILE_GS_URL_UNVETTED = \
'gs://chromeos-prebuilt/afdo-job/orderfiles/unvetted'
ORDERFILE_GS_URL_VETTED = \
'gs://chromeos-prebuilt/afdo-job/orderfiles/vetted'
ORDERFILE_COMPRESSION_SUFFIX = '.xz'
AFDO_REGEX = r'^(?P<bef>AFDO_FILE\["%s"\]=")(?P<name>.*)(?P<aft>")'
# NOTE: These regexp are copied from cbuildbot/afdo.py. Keep two copies
# until deployed and then remove the one in cbuildbot/afdo.py.
BENCHMARK_PROFILE_NAME_REGEX = r"""
^chromeos-chrome-amd64-
(\d+)\. # Major
(\d+)\. # Minor
(\d+)\. # Build
(\d+) # Patch
(?:_rc)?-r(\d+) # Revision
(-merged)?\.
afdo(?:\.bz2)?$ # We don't care about the presence of .bz2,
# so we use the ignore-group '?:' operator.
"""
BenchmarkProfileVersion = collections.namedtuple(
'BenchmarkProfileVersion',
['major', 'minor', 'build', 'patch', 'revision', 'is_merged'])
CWP_PROFILE_NAME_REGEX = r"""
^R(\d+)- # Major
(\d+)\. # Build
(\d+)- # Patch
(\d+) # Clock; breaks ties sometimes.
\.afdo(?:\.xz)?$ # We don't care about the presence of xz
"""
CWPProfileVersion = collections.namedtuple('ProfileVersion',
['major', 'build', 'patch', 'clock'])
class Error(Exception):
"""Base module error class."""
class GenerateChromeOrderfileError(Error):
"""Error for GenerateChromeOrderfile class."""
class ProfilesNameHelperError(Error):
"""Error for helper functions related to profile naming."""
def _ParseBenchmarkProfileName(profile_name):
"""Parse the name of a benchmark profile for Chrome.
Args:
profile_name: The name of a benchmark profile. A valid name is
chromeos-chrome-amd64-77.0.3849.0_rc-r1.afdo
Returns:
Named tuple of BenchmarkProfileVersion. With the input above, returns
BenchmarkProfileVersion(
major=77, minor=0, build=3849, patch=0, revision=1, is_merged=False)
"""
pattern = re.compile(BENCHMARK_PROFILE_NAME_REGEX, re.VERBOSE)
match = pattern.match(profile_name)
if not match:
raise ProfilesNameHelperError(
'Unparseable benchmark profile name: %s' % profile_name)
groups = match.groups()
version_groups = groups[:-1]
is_merged = groups[-1]
return BenchmarkProfileVersion(
*[int(x) for x in version_groups], is_merged=bool(is_merged))
def _ParseCWPProfileName(profile_name):
"""Parse the name of a CWP profile for Chrome.
Args:
profile_name: The name of a CWP profile. A valid name is
R77-3809.38-1562580965.afdo
Returns:
Named tuple of CWPProfileVersion. With the input above, returns
CWPProfileVersion(major=77, build=3809, patch=38, clock=1562580965)
"""
pattern = re.compile(CWP_PROFILE_NAME_REGEX, re.VERBOSE)
match = pattern.match(profile_name)
if not match:
raise ProfilesNameHelperError(
'Unparseable CWP profile name: %s' % profile_name)
return CWPProfileVersion(*[int(x) for x in match.groups()])
def _FindChromeEbuild(buildroot=None, board=None):
"""Find the Chrome ebuild in the build root.
Args:
buildroot: Optional. The path to build root, when used outside chroot.
When omitted, the return path is relative inside chroot.
board: Optional. A string of the name of the board.
Returns:
The full path to the versioned ebuild file. The path is either
relative inside chroot, or outside chroot, depending on the buildroot arg.
"""
if board:
equery_prog = 'equery-%s' % board
else:
equery_prog = 'equery'
equery_cmd = [equery_prog, 'w', 'chromeos-chrome']
ebuild_file = cros_build_lib.RunCommand(
equery_cmd, enter_chroot=True, redirect_stdout=True).output.rstrip()
if not buildroot:
return ebuild_file
basepath = '/mnt/host/source'
if not ebuild_file.startswith(basepath):
raise ValueError('Unexpected Chrome ebuild path')
ebuild_path = os.path.relpath(ebuild_file, basepath)
return os.path.join(buildroot, ebuild_path)
def _GetAFDOVersion(chrome_ebuild, is_benchmark):
"""Get the AFDO version in the build root.
Args:
chrome_ebuild: The full path to the Chrome ebuild.
is_benchmark: True if want to get benchmark AFDO version. False if
want to get CWP AFDO version.
Returns:
The AFDO version name in the Chrome ebuild
"""
if is_benchmark:
pattern = re.compile(AFDO_REGEX % 'benchmark')
else:
pattern = re.compile(AFDO_REGEX % 'silvermont')
name = ''
with open(chrome_ebuild) as f:
for line in f:
matched = pattern.match(line)
if matched:
name = matched.group('name')
break
if not name:
if is_benchmark:
raise ProfilesNameHelperError(
'Unable to find benchmark AFDO name in Chrome ebuild.')
else:
raise ProfilesNameHelperError(
'Unable to find CWP AFDO name in Chrome ebuild.')
if is_benchmark:
return _ParseBenchmarkProfileName(name)
else:
return _ParseCWPProfileName(name)
def _GetOrderfileName(buildroot):
"""Construct an orderfile name for the current Chrome OS checkout.
Args:
buildroot: The path to build root.
Returns:
An orderfile name using CWP + benchmark AFDO name.
e.g.
If benchmark AFDO is chromeos-chrome-amd64-77.0.3849.0_rc-r1.afdo,
and CWP AFDO is R77-3809.38-1562580965.afdo, the returned name is:
chromeos-chrome-orderfile-field-77-3809.38-1562580965-\
benchmark-77.0.3849.0-r1
"""
chrome_ebuild = _FindChromeEbuild(buildroot=buildroot)
benchmark_afdo_name = _GetAFDOVersion(chrome_ebuild, is_benchmark=True)
cwp_afdo_name = _GetAFDOVersion(chrome_ebuild, is_benchmark=False)
cwp_piece = 'field-%d-%d.%d-%d' % (cwp_afdo_name.major, cwp_afdo_name.build,
cwp_afdo_name.patch, cwp_afdo_name.clock)
benchmark_piece = 'benchmark-%d.%d.%d.%d-r%d' % (
benchmark_afdo_name.major, benchmark_afdo_name.minor,
benchmark_afdo_name.build, benchmark_afdo_name.patch,
benchmark_afdo_name.revision)
return 'chromeos-chrome-orderfile-%s-%s' % (cwp_piece, benchmark_piece)
class GenerateChromeOrderfile(object):
"""Class to handle generation of orderfile for Chrome.
This class takes orderfile containing symbols ordered by Call-Chain
Clustering (C3), produced when linking Chrome, and uses a toolchain
script to perform post processing to generate an orderfile that can
be used for linking Chrome and creates tarball. The output of this
script is a tarball of the orderfile and a tarball of the NM output
of the built Chrome binary.
"""
PROCESS_SCRIPT = ('/mnt/host/source/src/third_party/toolchain-utils/'
'orderfile/post_process_orderfile.py')
CHROME_BINARY_PATH = ('/var/cache/chromeos-chrome/chrome-src-internal/'
'src/out_${BOARD}/Release/chrome')
INPUT_ORDERFILE_PATH = ('/build/${BOARD}/opt/google/chrome/'
'chrome.orderfile.txt')
def __init__(self, board, output_dir, chroot_path, chroot_args):
self.output_dir = output_dir
self.orderfile_name = _GetOrderfileName(os.path.join(chroot_path, '..'))
self.chrome_binary = self.CHROME_BINARY_PATH.replace('${BOARD}', board)
self.input_orderfile = self.INPUT_ORDERFILE_PATH.replace('${BOARD}', board)
self.chroot_path = chroot_path
self.working_dir = os.path.join(self.chroot_path, 'tmp')
self.working_dir_inchroot = '/tmp'
self.chroot_args = chroot_args
def _CheckArguments(self):
if not os.path.isdir(self.output_dir):
raise GenerateChromeOrderfileError(
'Non-existent directory %s specified for --out-dir', self.output_dir)
chrome_binary_path_outside = os.path.join(self.chroot_path,
self.chrome_binary[1:])
if not os.path.exists(chrome_binary_path_outside):
raise GenerateChromeOrderfileError(
'Chrome binary does not exist at %s in chroot',
chrome_binary_path_outside)
chrome_orderfile_path_outside = os.path.join(self.chroot_path,
self.input_orderfile[1:])
if not os.path.exists(chrome_orderfile_path_outside):
raise GenerateChromeOrderfileError(
'No orderfile generated in the builder.')
def _GenerateChromeNM(self):
"""This command runs inside chroot."""
cmd = ['llvm-nm', '-n', self.chrome_binary]
result_inchroot = os.path.join(self.working_dir_inchroot,
self.orderfile_name + '.nm')
result_out_chroot = os.path.join(self.working_dir,
self.orderfile_name + '.nm')
try:
cros_build_lib.RunCommand(
cmd,
log_stdout_to_file=result_out_chroot,
enter_chroot=True,
chroot_args=self.chroot_args)
except cros_build_lib.RunCommandError:
raise GenerateChromeOrderfileError(
'Unable to run %s to get nm on Chrome binary' % (cmd))
# Return path inside chroot
return result_inchroot
def _PostProcessOrderfile(self, chrome_nm):
"""This command runs inside chroot."""
result = os.path.join(self.working_dir_inchroot,
self.orderfile_name + '.orderfile')
cmd = [
self.PROCESS_SCRIPT, '--chrome', chrome_nm, '--input',
self.input_orderfile, '--output', result
]
try:
cros_build_lib.RunCommand(
cmd, enter_chroot=True, chroot_args=self.chroot_args)
except cros_build_lib.RunCommandError:
raise GenerateChromeOrderfileError(
'Unable to run %s to process orderfile.' % (cmd))
# Return path inside chroot
return result
def _CreateTarball(self, targets):
"""This command runs outside of chroot."""
ret = []
for t in targets:
# The input t is the path inside chroot
input_path = os.path.join(self.working_dir, os.path.basename(t))
# Put output tarball in the out_dir
compressed = os.path.basename(t) + ORDERFILE_COMPRESSION_SUFFIX
output_path = os.path.join(self.output_dir, compressed)
cros_build_lib.CompressFile(input_path, output_path)
# Only return the basename
ret.append(compressed)
return ret
def Perform(self):
"""Generate post-processed Chrome orderfile and create tarball."""
self._CheckArguments()
chrome_nm = self._GenerateChromeNM()
orderfile = self._PostProcessOrderfile(chrome_nm)
self._CreateTarball([chrome_nm, orderfile])
class UpdateChromeEbuildWithOrderfileError(Error):
"""Error for UpdateChromeEbuildWithOrderfile class."""
class UpdateChromeEbuildWithOrderfile(object):
"""Class to update Chrome ebuild with unvetted orderfile."""
# regex to find orderfile declaration within the ebuild file.
CHROME_EBUILD_ORDERFILE_REGEX = (
r'^(?P<bef>UNVETTED_ORDERFILE=")(?P<name>.*)(?P<aft>")')
# and corresponding replacement string
CHROME_EBUILD_ORDERFILE_REPL = r'\g<bef>%s\g<aft>'
def __init__(self, board, orderfile):
self.board = board
if ORDERFILE_COMPRESSION_SUFFIX in orderfile:
self.orderfile = orderfile.replace(ORDERFILE_COMPRESSION_SUFFIX, '')
else:
self.orderfile = orderfile
def _PatchChromeEbuild(self, ebuild_file):
"""Patch the Chrome ebuild to use the orderfile.
Args:
ebuild_file: path to the ebuild file.
"""
original_ebuild = path_util.FromChrootPath(ebuild_file)
modified_ebuild = '%s.new' % original_ebuild
pattern = re.compile(self.CHROME_EBUILD_ORDERFILE_REGEX)
orderfile = self.CHROME_EBUILD_ORDERFILE_REPL % self.orderfile
found = False
with open(original_ebuild) as original, \
open(modified_ebuild, 'w') as modified:
for line in original:
matched = pattern.match(line)
if matched:
found = True
modified.write(pattern.sub(orderfile, line))
else:
modified.write(line)
if not found:
logging.info('Unable to find markers for setting orderfile.')
raise UpdateChromeEbuildWithOrderfileError(
'Chrome ebuild file does not have appropriate orderfile marker')
os.rename(modified_ebuild, original_ebuild)
logging.info('Patched %s with %s', original_ebuild, self.orderfile)
def _UpdateManifest(self, ebuild_file):
"""Regenerate the Manifest file. (To update orderfile)
Args:
ebuild_file: path to the ebuild file
"""
ebuild_prog = 'ebuild-%s' % self.board
cmd = [ebuild_prog, ebuild_file, 'manifest', '--force']
cros_build_lib.RunCommand(cmd, enter_chroot=True)
def Perform(self):
"""Main function to update Chrome ebuild with orderfile"""
ebuild = _FindChromeEbuild(board=self.board)
self._PatchChromeEbuild(ebuild)
# Patch the chrome 9999 ebuild too, as the manifest will use
# 9999 ebuild.
ebuild_9999 = os.path.join(
os.path.dirname(ebuild), 'chromeos-chrome-9999.ebuild')
self._PatchChromeEbuild(ebuild_9999)
# Use 9999 ebuild to update manifest.
self._UpdateManifest(ebuild_9999)
def FindLatestChromeOrderfile(gs_url):
"""Find the latest unvetted Chrome orderfile.
Args:
gs_url: The full path to GS bucket URL.
Returns:
The orderfile name.
"""
gs_context = gs.GSContext()
pattern = '.orderfile'
# Obtain all files from gs_url and filter out those not match
# pattern.
results = [x for x in gs_context.List(gs_url, details=True)
if pattern in x.url]
orderfile_url = max(results, key=lambda x: x.creation_time).url
orderfile_name = os.path.basename(orderfile_url)
logging.info('Latest orderfile in %s is %s', gs_url, orderfile_name)
return orderfile_name
def CheckOrderfileExists(buildroot, orderfile_verify):
"""Checks if the orderfile to be generated/verified already exists.
Args:
buildroot: The path of build root.
orderfile_verify: Whether it's for orderfile_verify or orderfile_generate.
For orderfile verify builder, it verifies the most recent orderfile from
the unvetted bucket. So we checks if it's in the vetted bucket or not.
For orderfile generate builder, it generates an orderfile based on the
Chrome version number. So we need to find out the Chrome is upreved than
the most recent unvetted orderfile or not.
Returns:
True if the orderfile is in the GS bucket already.
"""
gs_context = gs.GSContext()
if orderfile_verify:
orderfile_name = FindLatestChromeOrderfile(ORDERFILE_GS_URL_UNVETTED)
# Check if the latest unvetted orderfile is already verified
return gs_context.Exists(ORDERFILE_GS_URL_VETTED + '/' + orderfile_name)
# For orderfile_generate builder, get the orderfile name from chrome ebuild
orderfile_name = _GetOrderfileName(buildroot)
return gs_context.Exists(ORDERFILE_GS_URL_UNVETTED + '/' + orderfile_name +
ORDERFILE_COMPRESSION_SUFFIX)
def OrderfileUpdateChromeEbuild(board):
"""Update Chrome ebuild with latest unvetted orderfile.
Args:
board: Board type that was built on this machine.
Returns:
True, if the Chrome ebuild is successfully updated.
False, if the latest unvetted orderfile is already verified.
"""
orderfile_name = FindLatestChromeOrderfile(ORDERFILE_GS_URL_UNVETTED)
updater = UpdateChromeEbuildWithOrderfile(board, orderfile_name)
updater.Perform()
return True