blob: d10a2b0b8f337eca4054d00617d16db73d3d589e [file] [log] [blame]
"""Tools to interact with repo."""
from __future__ import absolute_import
from contextlib import contextmanager
import logging
import os
import shutil
import tempfile
from xml.dom import minidom
import xml.etree.ElementTree as et
import six
DEFAULT_REMOTE = 'eureka'
EUREKA_MANIFEST_PROJECT_NAME = 'eureka/manifest'
LIBASSISTANT_MANIFEST_PROJECT_NAME = 'standalone/manifest'
EUREKA_INTERNAL_HOST_URL = 'https://eureka-internal.googlesource.com/'
EUREKA_PARTNER_HOST_URL = 'https://eureka-partner.googlesource.com/'
EUREKA_MANIFEST_URL = (
EUREKA_INTERNAL_HOST_URL + EUREKA_MANIFEST_PROJECT_NAME)
CATABUILDER_E2E_TESTS_MANIFEST_URL = (
'https://eureka-staging-internal.googlesource.com/'
'catabuilder_e2e_tests_manifest')
GOTHAM_MCU_MANIFEST_URL = (
'https://team.googlesource.com/gotham-cr/manifest')
LIBASSISTANT_MANIFEST_URL = (
'https://libassistant-internal.googlesource.com/' +
LIBASSISTANT_MANIFEST_PROJECT_NAME)
PARTNER_MANIFEST_URL = (
EUREKA_PARTNER_HOST_URL + 'partner/manifest')
PARTNER_AMLOGIC_MANIFEST_URL = (
EUREKA_PARTNER_HOST_URL + 'amlogic/manifest')
CAST_AUDIO_STANDARD_MANIFEST_URL = (
EUREKA_PARTNER_HOST_URL + 'partner/manifest_cast_audio')
CAST_AUDIO_REFERENCE_MANIFEST_URL = (
EUREKA_PARTNER_HOST_URL + 'partner/manifest_cast_audio_reference')
CAST_ASSISTANT_REFERENCE_MANIFEST_URL = (
EUREKA_PARTNER_HOST_URL + 'partner/manifest_cast_assistant_reference')
CAST_TV_MANIFEST_URL = (
EUREKA_PARTNER_HOST_URL + 'partner/manifest_cast_tv')
CAST_TV_MARVELL_MANIFEST_URL = (
EUREKA_PARTNER_HOST_URL + 'partner/manifest_cast_tv_marvell')
CAST_TV_MEDIATEK_MANIFEST_URL = (
EUREKA_PARTNER_HOST_URL + 'partner/manifest_cast_tv_mediatek')
CAST_TV_MSTAR_MANIFEST_URL = (
EUREKA_PARTNER_HOST_URL + 'partner/manifest_cast_tv_mstar')
CAST_TV_NOVATEK_URL = (
EUREKA_PARTNER_HOST_URL + 'partner/manifest_cast_tv_novatek')
CAST_TV_COMCAST_URL = (
EUREKA_PARTNER_HOST_URL + 'partner/manifest_comcast')
ABBEY_MANIFEST_URL = (
EUREKA_INTERNAL_HOST_URL + 'abbey/manifest')
PARTNER_QUALCOMM_MANIFEST_URL = (
EUREKA_PARTNER_HOST_URL + 'qualcomm/manifest')
PARTNER_QUALCOMM_MANIFEST_OTA_URL = (
EUREKA_PARTNER_HOST_URL + 'qualcomm/manifest_ota')
REPO_MANIFEST_PROJECT_PATH = '.repo/manifests'
REPO_MANIFEST_PROJECT_REMOTE = 'origin'
REPO_MANIFEST_URL_TO_PROJECT_NAME = {
EUREKA_MANIFEST_URL: EUREKA_MANIFEST_PROJECT_NAME,
LIBASSISTANT_MANIFEST_URL: LIBASSISTANT_MANIFEST_PROJECT_NAME,
}
MANIFEST_OVERRIDE_XML = 'manifest_contour.xml'
REPO_MANIFEST_URL_TO_REPO_ALIAS = {
ABBEY_MANIFEST_URL: 'abbey',
CAST_ASSISTANT_REFERENCE_MANIFEST_URL: 'cast_assistant_reference',
CAST_AUDIO_REFERENCE_MANIFEST_URL: 'cast_audio_reference',
CAST_AUDIO_STANDARD_MANIFEST_URL: 'cast_audio_new',
CAST_TV_COMCAST_URL: 'cast_tv_comcast',
CAST_TV_MANIFEST_URL: 'cast_tv',
CAST_TV_MARVELL_MANIFEST_URL: 'cast_tv_marvell',
CAST_TV_MEDIATEK_MANIFEST_URL: 'cast_tv_mediatek',
CAST_TV_MSTAR_MANIFEST_URL: 'cast_tv_mstar',
CAST_TV_NOVATEK_URL: 'cast_tv_novatek',
EUREKA_MANIFEST_URL: 'internal',
GOTHAM_MCU_MANIFEST_URL: 'gotham_mcu',
LIBASSISTANT_MANIFEST_URL: 'libassistant',
PARTNER_AMLOGIC_MANIFEST_URL: 'partner_amlogic',
PARTNER_MANIFEST_URL: 'partner',
PARTNER_QUALCOMM_MANIFEST_URL: 'partner_qualcomm',
PARTNER_QUALCOMM_MANIFEST_OTA_URL: 'partner_qualcomm_ota',
CATABUILDER_E2E_TESTS_MANIFEST_URL: 'e2e_tests',
}
class DuplicateLookupError(Exception):
"""Error if the project was included more than once."""
class ManifestParseError(Exception):
"""Error if unable to parse output of 'repo manifest' command."""
class ProjectLookupTable(object):
"""A lookup table for project and project paths."""
def __init__(self, lookup_dict=None, duplicate_keys=None):
"""Create a ProjectLookupTable.
Args:
lookup_dict: Dictionary of key/value pairs.
duplicate_keys: A list of keys that were duplicates.
"""
if not lookup_dict:
lookup_dict = {}
if not duplicate_keys:
duplicate_keys = []
self._lookup_dict = lookup_dict
self._duplicate_keys = duplicate_keys
def __getitem__(self, key):
"""Get the value for the given key.
Args:
key: The key to get a value for.
Returns:
The value for the given key.
Raises:
KeyError: If the key is not included.
DuplicateLookupError: If the key was included multiple times.
"""
return self.get(key)
def __setitem__(self, key, value):
"""Sets the value for the given key."""
if self._lookup_dict.get(key):
self._duplicate_keys.append(key)
self._lookup_dict[key] = value
def __contains__(self, key):
"""Returns true if key is in the underlying dict."""
return key in self._lookup_dict
def get(self, key):
"""Get the value for the given key.
Args:
key: The key to get a value for.
Returns:
The value for the given key.
Raises:
KeyError: If the key is not included.
DuplicateLookupError: If the key was included multiple times.
"""
if key in self._duplicate_keys:
raise DuplicateLookupError(
'Duplicate of {} found in manifest.'.format(key))
if key in self._lookup_dict:
return self._lookup_dict[key]
if key.endswith('/') and key[:-1] in self._lookup_dict:
return self._lookup_dict[key[:-1]]
# For legacy reasons, some project names in the manifest include a '.git'
# suffix. Gerrit strips off this suffix, though, so if we haven't found the
# key yet, try searching with a .git suffix.
key_git = key + '.git'
if key_git in self._lookup_dict:
return self._lookup_dict[key_git]
raise KeyError('{} was not found in the project path lookup {}.'.format(
key, self._lookup_dict))
def get_dict(self):
"""Gets the dictionary representing the lookup table.
Returns:
The underlying dict storage of this object.
"""
return self._lookup_dict
def get_project_path_lookup(executor, manifest_url, manifest=None, **kwargs):
"""Returns a project path lookup table.
Using manifest apgument as a manifest content by default, if None
`repo manifest` will be invoked.
Note that if duplicate projects are included in the manifest only 1 will be
included in the lookup. This will be the latter of the duplicates listed via
the `repo manifest` command.
Args:
executor: Executes the subprocess.
manifest_url: The url where the repo manifest project is hosted.
manifest: Content of manifest file already loaded by build.
**kwargs: Any additional kwargs to pass to the executor.
Returns:
A ProjectLookupTable representing the relative path for the given project.
"""
if not manifest:
manifest = get_manifest(executor, **kwargs)
return parse_manifest_for_path(manifest, manifest_url)
@contextmanager
def normalize_manifest_if_exists(manifest_path):
"""Normalizes the manifest for the duration of the context manager."""
if manifest_path:
temp_manifest = tempfile.NamedTemporaryFile(delete='False').name
shutil.copy2(manifest_path, temp_manifest)
_normalize_manifest(manifest_path)
try:
yield
finally:
if manifest_path:
shutil.copy2(temp_manifest, manifest_path)
os.unlink(temp_manifest)
def _normalize_manifest(manifest_path):
"""Convert manifest to a more user friendly version."""
with open(manifest_path, 'r') as f:
manifest = ''.join([
line.strip() for line in f.readlines()])
root = et.fromstring(manifest)
for remote in root.findall('remote'):
if 'github.googlesource.com' in remote.get('fetch'):
remote.set('fetch', 'https://github.com')
remote.attrib.pop('review', None)
break
normalized_manifest = minidom.parseString(
et.tostring(root)).toprettyxml(encoding='UTF-8', indent=' ')
with open(manifest_path, 'wb') as f:
f.write(normalized_manifest)
def override_manifest_command(override_path=None):
"""Returns command for overriding .repo/manifest.xml by override_path."""
if override_path is None:
override_path = os.path.join(os.pardir, MANIFEST_OVERRIDE_XML)
return ['ln', '-sf', override_path, 'manifest.xml']
def override_manifest(executor, override_path=None):
"""Overriding .repo/manifest.xml by override_path."""
return executor.exec_subprocess(
override_manifest_command(override_path), cwd='.repo')
def get_project_remote_lookup(executor, manifest_url, manifest=None, **kwargs):
"""Returns a project remote lookup table.
Using manifest apgument as a manifest content by default, if None
`repo manifest` will be invoked.
Note that if duplicate projects are included in the manifest only 1 will be
included in the lookup. This will be the latter of the duplicates listed via
the `repo manifest` command.
Args:
executor: Executes the subprocess.
manifest_url: The url where the repo manifest project is hosted.
manifest: Content of manifest file already loaded by build.
**kwargs: Any additional kwargs to pass to the executor.
Returns:
A ProjectLookupTable representing the remote for the given project.
"""
if not manifest:
manifest = get_manifest(executor, **kwargs)
return parse_manifest_for_remote(manifest, manifest_url)
def get_project_revision_lookup(executor, manifest_url, manifest_branch,
manifest=None, **kwargs):
"""Returns a project revision lookup table.
Using manifest apgument as a manifest content by default, if None
`repo manifest` will be invoked.
Note that if duplicate projects are included in the manifest only 1 will be
included in the lookup. This will be the latter of the duplicates listed via
the `repo manifest` command.
Args:
executor: Executes the subprocess.
manifest_url: The url where the repo manifest project is hosted.
manifest_branch: The repo manifest branch of the current checkout.
manifest: Content of manifest file already loaded by build.
**kwargs: Any additional kwargs to pass to the executor.
Returns:
A ProjectLookupTable representing the revision for the given project.
"""
if not manifest:
manifest = get_manifest(executor, **kwargs)
return parse_manifest_for_revision(manifest, manifest_url, manifest_branch)
def get_manifest(executor, retries=5, **kwargs):
"""Returns the manifest file in a string form.
Args:
executor: Executes the subprocess.
retries: number of retries to load manifest.
**kwargs: Any additional kwargs to pass to the executor.
Raises:
ManifestParseError: if all retries fail.
Returns:
A string representing the manifest file.
"""
failures = 0
while retries > failures:
manifest = executor.exec_subprocess(
['repo', 'manifest'],
check_output=True,
**kwargs)
try:
lines = ''.join([line.strip() for line in manifest.splitlines()])
_ = et.fromstring(lines)
return lines
except et.ParseError as error:
failures += 1
logging.warning(('Failed to parse manifest file. Attempt %d/%d. '
'Error was: %s\nManifest file content was:\n%s'),
failures, retries, error, manifest)
raise ManifestParseError(
'Failed to parse manifest after %s retries' % failures)
def is_manifest_project(project):
"""Checks if the project is a Manifest project.
Args:
project: The name of the Gerrit project in which the files live.
Returns:
True if it is a Manifest, False if not.
"""
return project in list(REPO_MANIFEST_URL_TO_PROJECT_NAME.values())
def parse_manifest_for_path(manifest, manifest_url):
"""Parses a repo manifest file for project paths.
Args:
manifest: A string representing a manifest file.
manifest_url: The url where the repo manifest project is hosted.
Returns:
A ProjectLookupTable representing the manifest for the given project.
"""
assert isinstance(manifest, six.string_types)
result = ProjectLookupTable()
# Add an entry to this map representing the repo manifest project.
manifest_project_name = get_manifest_url_mapping(
REPO_MANIFEST_URL_TO_PROJECT_NAME, manifest_url)
if manifest_project_name:
result[manifest_project_name] = REPO_MANIFEST_PROJECT_PATH
if not manifest:
return result
manifest = ''.join([line.strip() for line in manifest.splitlines()])
root = et.fromstring(manifest)
for project in root.findall('project'):
name = project.get('name')
path = project.get('path')
if not path:
path = name
result[name] = path
return result
def parse_manifest_for_remote(manifest, manifest_url):
"""Parses a repo manifest file for project remotes.
Args:
manifest: A string representing a manifest file.
manifest_url: The url where the repo manifest project is hosted.
Returns:
A ProjectLookupTable representing the manifest for the given project.
"""
assert isinstance(manifest, six.string_types)
result = ProjectLookupTable()
# Add an entry to this map representing the repo manifest project.
manifest_project_name = get_manifest_url_mapping(
REPO_MANIFEST_URL_TO_PROJECT_NAME, manifest_url)
if manifest_project_name:
result[manifest_project_name] = REPO_MANIFEST_PROJECT_REMOTE
if not manifest:
return result
manifest = ''.join([line.strip() for line in manifest.splitlines()])
root = et.fromstring(manifest)
default_remote = DEFAULT_REMOTE
n = root.find('default')
if n is not None and n.get('remote') is not None:
default_remote = n.get('remote')
for project in root.findall('project'):
name = project.get('name')
remote = project.get('remote')
if not remote:
remote = default_remote
result[name] = remote
return result
def parse_manifest_for_revision(manifest, manifest_url, manifest_branch):
"""Parses a repo manifest file for project revisions.
Args:
manifest: A string representing a manifest file.
manifest_url: The url where the repo manifest project is hosted.
manifest_branch: The repo manifest branch of the current checkout.
Returns:
A ProjectLookupTable representing the manifest for the given project.
"""
assert isinstance(manifest, six.string_types)
result = ProjectLookupTable()
manifest_project_name = get_manifest_url_mapping(
REPO_MANIFEST_URL_TO_PROJECT_NAME, manifest_url)
if manifest_project_name:
result[manifest_project_name] = manifest_branch
if not manifest:
return result
manifest = ''.join([line.strip() for line in manifest.splitlines()])
root = et.fromstring(manifest)
# Get the default revision frome the manifest.
default_revision = ''
for default_tag in root.findall('default'):
default_revision = default_tag.get('revision', '')
# Create the lookup. If an entry does not explicitly reference a revision,
# assign the default revision.
for project in root.findall('project'):
name = project.get('name')
revision = project.get('revision')
if not revision:
revision = default_revision
result[name] = revision
return result
def requires_repo_sync(project, filenames):
"""Given a list of touched files, determines if a repo sync is needed.
If a change is applied to a repo manifest file (for example, to update the
revision of a project), we may need to run 'repo sync' again before building,
to ensure the project is checked out at the SHA we are trying to test. Return
true if this is the case.
Args:
project: The name of the Gerrit project in which the files live.
filenames: A list of files that were modified in project.
Returns:
True if we should run 'repo sync' again before building.
"""
# Projects that are not manifest projects can be ignored.
if not is_manifest_project(project):
return False
# If any file has been touched, we need to repo sync.
return bool(filenames)
def get_manifest_url_mapping(mapping, manifest_url):
"""Get value from manifest_url mapping regardless of GoB domain name."""
return mapping.get(manifest_url.replace(
'.git.corp.google.com', '.googlesource.com'))
def use_eureka_manifest(manifest_url):
"""Returns True if this manifest points to eureka/manifest."""
project_name = get_manifest_url_mapping(
REPO_MANIFEST_URL_TO_PROJECT_NAME, manifest_url)
return project_name and project_name == EUREKA_MANIFEST_PROJECT_NAME
def manifest_alias(manifest_url):
"""Returns manifest alias."""
return get_manifest_url_mapping(REPO_MANIFEST_URL_TO_REPO_ALIAS, manifest_url)