detect projects affected by a change (#113)
first proposal for affected projects detection
This commit is contained in:
parent
762cd6b501
commit
0272b27c28
3 changed files with 204 additions and 0 deletions
138
scripts/choose_projects.py
Executable file
138
scripts/choose_projects.py
Executable file
|
@ -0,0 +1,138 @@
|
||||||
|
#!/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
|
||||||
|
import sys
|
||||||
|
from typing import Dict, List, Set, Tuple
|
||||||
|
|
||||||
|
from unidiff import PatchSet
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
# TODO: We could also try to avoid running tests for llvm for projects that
|
||||||
|
# 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).
|
||||||
|
|
||||||
|
class ChooseProjects:
|
||||||
|
|
||||||
|
# file where dependencies are defined
|
||||||
|
SCRIPT_DIR = os.path.dirname(__file__)
|
||||||
|
DEPENDENCIES_FILE = os.path.join(SCRIPT_DIR, 'llvm-dependencies.yaml')
|
||||||
|
|
||||||
|
def __init__(self, llvm_dir: str):
|
||||||
|
self.llvm_dir = llvm_dir
|
||||||
|
self.usages = dict() # type: Dict[str,List[str]]
|
||||||
|
self.all_projects = [] # type: List[str]
|
||||||
|
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)
|
||||||
|
for user, used_list in config['dependencies'].items():
|
||||||
|
for used in used_list:
|
||||||
|
self.usages.setdefault(used,[]).append(user)
|
||||||
|
self.all_projects = config['allprojects']
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
llvm_dir = os.path.abspath(os.path.expanduser(args.llvmdir))
|
||||||
|
logging.info('Scanning LLVM in {}'.format(llvm_dir))
|
||||||
|
if not self.match_projects_dirs():
|
||||||
|
sys.exit(1)
|
||||||
|
changed_files = self.get_changed_files()
|
||||||
|
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!')
|
||||||
|
print('all')
|
||||||
|
return 0
|
||||||
|
|
||||||
|
affected_projects = self.get_affected_projects(changed_projects)
|
||||||
|
print(';'.join(sorted(affected_projects)))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
def get_changed_files() -> Set[str]:
|
||||||
|
"""get list of changed files from the patch from STDIN."""
|
||||||
|
patch = PatchSet(sys.stdin)
|
||||||
|
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
|
||||||
|
|
||||||
|
def get_changed_projects(self, changed_files: Set[str]) -> Tuple[Set[str],bool]:
|
||||||
|
"""Get list of projects affected by the change."""
|
||||||
|
changed_projects = set()
|
||||||
|
unmapped_changes = False
|
||||||
|
for changed_file in changed_files:
|
||||||
|
project = changed_file.split('/',maxsplit=1)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logging.basicConfig(filename='choose_projects.log', level=logging.INFO)
|
||||||
|
parser = argparse.ArgumentParser(description='Compute the projects affected by a change.')
|
||||||
|
parser.add_argument('llvmdir', default='.')
|
||||||
|
args = parser.parse_args()
|
||||||
|
chooser = ChooseProjects(args.llvmdir)
|
||||||
|
sys.exit(chooser.run())
|
59
scripts/llvm-dependencies.yaml
Normal file
59
scripts/llvm-dependencies.yaml
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
# This mapping is only used to determine which projects need to be rebuild.
|
||||||
|
# E.g. all builds are still in-tree, so 'llvm' will always be included in the
|
||||||
|
# built projects.
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
llvm: []
|
||||||
|
clang:
|
||||||
|
- llvm
|
||||||
|
clang-tools-extra:
|
||||||
|
- clang
|
||||||
|
- llvm
|
||||||
|
# FIXME: "compiler-rt" depends on "llvm" only for configuration, right?
|
||||||
|
# it means we can miss breakages in configuration changes.
|
||||||
|
# Same for libcxx, libc and other projects that don't have 'llvm'
|
||||||
|
# as a dependency.
|
||||||
|
compiler-rt: []
|
||||||
|
libc: []
|
||||||
|
# FIXME: not sure about 'libcxx' and 'libcxxabi'
|
||||||
|
libcxx: []
|
||||||
|
libcxxabi: []
|
||||||
|
libclc: []
|
||||||
|
libunwind: []
|
||||||
|
lld:
|
||||||
|
- llvm
|
||||||
|
lldb:
|
||||||
|
- clang
|
||||||
|
- llvm
|
||||||
|
llgo:
|
||||||
|
- llvm
|
||||||
|
mlir:
|
||||||
|
- llvm
|
||||||
|
openmp: []
|
||||||
|
parallel-libs: []
|
||||||
|
polly:
|
||||||
|
- llvm
|
||||||
|
pstl: []
|
||||||
|
|
||||||
|
# List of all projects in the LLVM monorepository. This list is taken from
|
||||||
|
# llvm/CMakeLists.txt in "set(LLVM_ALL_PROJECTS ..."
|
||||||
|
|
||||||
|
allprojects:
|
||||||
|
- clang
|
||||||
|
- clang-tools-extra
|
||||||
|
- compiler-rt
|
||||||
|
- debuginfo-tests
|
||||||
|
- libc
|
||||||
|
- libclc
|
||||||
|
- libcxx
|
||||||
|
- libcxxabi
|
||||||
|
- libunwind
|
||||||
|
- lld
|
||||||
|
- lldb
|
||||||
|
- llgo
|
||||||
|
- mlir
|
||||||
|
- openmp
|
||||||
|
- parallel-libs
|
||||||
|
- polly
|
||||||
|
- pstl
|
||||||
|
- llvm
|
7
scripts/requirements.txt
Normal file
7
scripts/requirements.txt
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
phabricator==0.7.0
|
||||||
|
lxml==4.4.1
|
||||||
|
gitpython==3.0.5
|
||||||
|
retrying==1.3.3
|
||||||
|
pathspec==0.7.0
|
||||||
|
pyaml==19.12.0
|
||||||
|
unidiff==0.5.5
|
Loading…
Reference in a new issue