2020-01-28 16:39:53 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
# Copyright 2020 Google LLC
|
|
|
|
#
|
|
|
|
# Licensed under the the Apache License v2.0 with LLVM Exceptions (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# https://llvm.org/LICENSE.txt
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
|
|
|
|
|
|
|
"""Compute the LLVM_ENABLE_PROJECTS for Cmake from diff.
|
|
|
|
|
|
|
|
This script will compute which projects are affected by the diff proviaded via STDIN.
|
|
|
|
It gets the modified files in the patch, assings them to projects and based on a
|
|
|
|
project dependency graph it will get the transitively affected projects.
|
|
|
|
"""
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import logging
|
|
|
|
import os
|
2020-03-23 09:03:24 +01:00
|
|
|
import platform
|
2020-01-28 16:39:53 +01:00
|
|
|
import sys
|
2020-04-23 11:22:24 +02:00
|
|
|
from typing import Dict, List, Set, Tuple, Optional
|
2020-01-28 16:39:53 +01:00
|
|
|
|
|
|
|
from unidiff import PatchSet
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
# TODO: We could also try to avoid running tests for llvm for projects that
|
2020-03-23 09:03:24 +01:00
|
|
|
# only need various cmake scripts and don't actually depend on llvm (e.g.
|
|
|
|
# libcxx does not need to run llvm tests, but may still need to include llvm).
|
|
|
|
|
2020-01-28 16:39:53 +01:00
|
|
|
|
|
|
|
class ChooseProjects:
|
|
|
|
|
|
|
|
# file where dependencies are defined
|
|
|
|
SCRIPT_DIR = os.path.dirname(__file__)
|
|
|
|
DEPENDENCIES_FILE = os.path.join(SCRIPT_DIR, 'llvm-dependencies.yaml')
|
2020-03-23 09:03:24 +01:00
|
|
|
# projects used if anything goes wrong
|
|
|
|
FALLBACK_PROJECTS = ['all']
|
2020-01-28 16:39:53 +01:00
|
|
|
|
2020-04-23 11:22:24 +02:00
|
|
|
def __init__(self, llvm_dir: Optional[str]):
|
|
|
|
self.llvm_dir = llvm_dir # type: Optional[str]
|
2020-03-23 09:03:24 +01:00
|
|
|
self.defaultProjects = dict() # type: Dict[str, Dict[str, str]]
|
|
|
|
self.dependencies = dict() # type: Dict[str,List[str]]
|
2020-01-28 16:39:53 +01:00
|
|
|
self.usages = dict() # type: Dict[str,List[str]]
|
2020-03-23 09:03:24 +01:00
|
|
|
self.all_projects = [] # type: List[str]
|
|
|
|
self.excluded_projects = set() # type: Set[str]
|
|
|
|
self.operating_system = self._detect_os() # type: str
|
2020-01-28 16:39:53 +01:00
|
|
|
self._load_config()
|
|
|
|
|
|
|
|
def _load_config(self):
|
|
|
|
logging.info('loading project config from {}'.format(self.DEPENDENCIES_FILE))
|
|
|
|
with open(self.DEPENDENCIES_FILE) as dependencies_file:
|
|
|
|
config = yaml.load(dependencies_file, Loader=yaml.SafeLoader)
|
2020-02-03 13:42:19 +01:00
|
|
|
self.dependencies = config['dependencies']
|
|
|
|
for user, used_list in self.dependencies.items():
|
2020-01-28 16:39:53 +01:00
|
|
|
for used in used_list:
|
2020-03-23 09:03:24 +01:00
|
|
|
self.usages.setdefault(used, []).append(user)
|
2020-01-28 16:39:53 +01:00
|
|
|
self.all_projects = config['allprojects']
|
2020-03-23 09:03:24 +01:00
|
|
|
self.excluded_projects = set(config['excludedProjects'][self.operating_system])
|
2020-01-28 16:39:53 +01:00
|
|
|
|
2020-03-23 09:03:24 +01:00
|
|
|
@staticmethod
|
|
|
|
def _detect_os() -> str:
|
|
|
|
"""Detect the current operating system."""
|
|
|
|
if platform.system() == 'Windows':
|
|
|
|
return 'windows'
|
|
|
|
return 'linux'
|
|
|
|
|
|
|
|
def choose_projects(self, patch: str = None) -> List[str]:
|
2020-04-23 11:22:24 +02:00
|
|
|
if self.llvm_dir is None:
|
|
|
|
raise ValueError('path to llvm folder must be set in ChooseProject.')
|
|
|
|
|
2020-03-23 09:03:24 +01:00
|
|
|
llvm_dir = os.path.abspath(os.path.expanduser(self.llvm_dir))
|
2020-01-28 16:39:53 +01:00
|
|
|
logging.info('Scanning LLVM in {}'.format(llvm_dir))
|
|
|
|
if not self.match_projects_dirs():
|
2020-03-23 09:03:24 +01:00
|
|
|
return self.FALLBACK_PROJECTS
|
|
|
|
changed_files = self.get_changed_files(patch)
|
2020-01-28 16:39:53 +01:00
|
|
|
changed_projects, unmapped_changes = self.get_changed_projects(changed_files)
|
|
|
|
|
|
|
|
if unmapped_changes:
|
|
|
|
logging.warning('There were changes that could not be mapped to a project.'
|
|
|
|
'Building all projects instead!')
|
2020-03-23 09:03:24 +01:00
|
|
|
return self.FALLBACK_PROJECTS
|
2020-01-28 16:39:53 +01:00
|
|
|
|
|
|
|
affected_projects = self.get_affected_projects(changed_projects)
|
2020-02-03 13:42:19 +01:00
|
|
|
affected_projects = self.add_dependencies(affected_projects)
|
2020-01-31 18:29:47 +01:00
|
|
|
affected_projects = affected_projects - self.excluded_projects
|
2020-03-23 09:03:24 +01:00
|
|
|
return sorted(affected_projects)
|
2020-01-28 16:39:53 +01:00
|
|
|
|
2020-03-23 09:03:24 +01:00
|
|
|
def run(self):
|
|
|
|
print(';'.join(self.choose_projects()))
|
|
|
|
return 0
|
2020-01-28 16:39:53 +01:00
|
|
|
|
|
|
|
def match_projects_dirs(self) -> bool:
|
|
|
|
"""Make sure that all projects are folders in the LLVM dir.
|
|
|
|
Otherwise we can't create the regex...
|
|
|
|
"""
|
|
|
|
subdirs = os.listdir(self.llvm_dir)
|
|
|
|
for project in self.all_projects:
|
|
|
|
if project not in subdirs:
|
|
|
|
logging.error('Project no found in LLVM root folder: {}'.format(project))
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
@staticmethod
|
2020-03-23 09:03:24 +01:00
|
|
|
def get_changed_files(patch_str: str = None) -> Set[str]:
|
2020-01-28 16:39:53 +01:00
|
|
|
"""get list of changed files from the patch from STDIN."""
|
2020-03-23 09:03:24 +01:00
|
|
|
if patch_str is None:
|
2020-03-24 09:27:43 +01:00
|
|
|
patch_str = sys.stdin
|
2020-03-23 09:03:24 +01:00
|
|
|
patch = PatchSet(patch_str)
|
|
|
|
|
2020-01-28 16:39:53 +01:00
|
|
|
changed_files = set({f.path for f in patch.modified_files + patch.added_files + patch.removed_files})
|
|
|
|
|
|
|
|
logging.info('Files modified by this patch:\n ' + '\n '.join(sorted(changed_files)))
|
|
|
|
return changed_files
|
|
|
|
|
2020-03-23 09:03:24 +01:00
|
|
|
def get_changed_projects(self, changed_files: Set[str]) -> Tuple[Set[str], bool]:
|
2020-01-28 16:39:53 +01:00
|
|
|
"""Get list of projects affected by the change."""
|
|
|
|
changed_projects = set()
|
|
|
|
unmapped_changes = False
|
|
|
|
for changed_file in changed_files:
|
2020-03-23 09:03:24 +01:00
|
|
|
project = changed_file.split('/', maxsplit=1)
|
2020-01-28 16:39:53 +01:00
|
|
|
if project is None or project[0] not in self.all_projects:
|
|
|
|
unmapped_changes = True
|
|
|
|
logging.warning('Could not map file to project: {}'.format(changed_file))
|
|
|
|
else:
|
|
|
|
changed_projects.add(project[0])
|
|
|
|
|
|
|
|
logging.info('Projects directly modified by this patch:\n ' + '\n '.join(sorted(changed_projects)))
|
|
|
|
return changed_projects, unmapped_changes
|
|
|
|
|
2020-03-23 09:03:24 +01:00
|
|
|
def get_affected_projects(self, changed_projects: Set[str]) -> Set[str]:
|
|
|
|
"""Compute transitive closure of affected projects based on the
|
|
|
|
dependencies between the projects."""
|
|
|
|
affected_projects = set(changed_projects)
|
2020-01-28 16:39:53 +01:00
|
|
|
last_len = -1
|
|
|
|
while len(affected_projects) != last_len:
|
|
|
|
last_len = len(affected_projects)
|
|
|
|
changes = set()
|
|
|
|
for project in affected_projects:
|
|
|
|
if project in self.usages:
|
|
|
|
changes.update(self.usages[project])
|
|
|
|
affected_projects.update(changes)
|
|
|
|
|
|
|
|
logging.info('Projects affected by this patch:')
|
|
|
|
logging.info(' ' + '\n '.join(sorted(affected_projects)))
|
|
|
|
|
|
|
|
return affected_projects
|
|
|
|
|
2020-02-03 14:19:11 +01:00
|
|
|
def add_dependencies(self, projects: Set[str]) -> Set[str]:
|
2020-02-03 13:42:19 +01:00
|
|
|
"""Return projects and their dependencies.
|
|
|
|
|
|
|
|
All all dependencies to `projects` so that they can be built.
|
|
|
|
"""
|
|
|
|
result = set(projects)
|
|
|
|
last_len = -1
|
|
|
|
while len(result) != last_len:
|
|
|
|
last_len = len(result)
|
|
|
|
changes = set()
|
|
|
|
for project in result:
|
|
|
|
if project in self.dependencies:
|
|
|
|
changes.update(self.dependencies[project])
|
|
|
|
result.update(changes)
|
|
|
|
return result
|
|
|
|
|
2020-04-23 11:22:24 +02:00
|
|
|
def get_all_enabled_projects(self) -> List[str]:
|
|
|
|
"""Get list of all not-excluded projects for current platform."""
|
|
|
|
result = set(self.all_projects) - self.excluded_projects
|
|
|
|
return sorted(list(result))
|
|
|
|
|
2020-03-23 09:03:24 +01:00
|
|
|
|
2020-01-28 16:39:53 +01:00
|
|
|
if __name__ == "__main__":
|
|
|
|
logging.basicConfig(filename='choose_projects.log', level=logging.INFO)
|
2020-03-23 09:03:24 +01:00
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
description='Compute the projects affected by a change.')
|
2020-01-28 16:39:53 +01:00
|
|
|
parser.add_argument('llvmdir', default='.')
|
|
|
|
args = parser.parse_args()
|
|
|
|
chooser = ChooseProjects(args.llvmdir)
|
2020-03-23 09:03:24 +01:00
|
|
|
sys.exit(chooser.run())
|