#!/usr/bin/env python3 # roomservice: Android device repository management utility. # Copyright (C) 2013 Cybojenix # Copyright (C) 2013 The OmniROM Project # Copyright (C) 2015-2019 ParanoidAndroid Project # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import json import os import sys from xml.etree import ElementTree as ET extra_manifests_dir = '.repo/manifests/' upstream_manifest_path = '.repo/manifest.xml' local_manifests_dir = '.repo/local_manifests' roomservice_manifest_path = local_manifests_dir + '/roomservice.xml' dependencies_json_path = 'vendor/aospa/products/%s/aospa.dependencies' # Indenting code from https://stackoverflow.com/a/4590052 def indent(elem, level=0): i = "\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): elem.text = i + " " if not elem.tail or not elem.tail.strip(): elem.tail = i for elem in elem: indent(elem, level + 1) if not elem.tail or not elem.tail.strip(): elem.tail = i else: if level and (not elem.tail or not elem.tail.strip()): elem.tail = i def recurse_include(manifest): includes = manifest.findall('include') if includes is not None: for file in includes: extra_manifest = ET.parse(extra_manifests_dir + file.get('name')).getroot() for elem in extra_manifest: manifest.append(elem) for elem in recurse_include(extra_manifest): manifest.append(elem) return manifest if __name__ == '__main__': if not os.path.isdir(local_manifests_dir): os.mkdir(local_manifests_dir) if len(sys.argv) <= 1: raise ValueError('The first argument must be the product.') product = sys.argv[1] try: device = product[product.index('_') + 1:] except ValueError: device = product dependencies_json_path %= device if not os.path.isfile(dependencies_json_path): raise ValueError('No dependencies file could be found for the device (%s).' % device) dependencies = json.loads(open(dependencies_json_path, 'r').read()) try: upstream_manifest = ET.parse(upstream_manifest_path).getroot() except (IOError, ET.ParseError): upstream_manifest = ET.Element('manifest') recurse_include(upstream_manifest) try: roomservice_manifest = ET.parse(roomservice_manifest_path).getroot() except (IOError, ET.ParseError): roomservice_manifest = ET.Element('manifest') syncable_projects = [] mentioned_projects = [] # Clean up all the elements. for removable_project in roomservice_manifest.findall('remove-project'): name = removable_project.get('name') path = None for project in upstream_manifest.findall('project'): if project.get('name') == name: path = project.get('path') break if path is None: # The upstream manifest doesn't know this project, so drop it. roomservice_manifest.remove(removable_project) continue found_in_dependencies = False for dependency in dependencies: if dependency.get('target_path') == path: found_in_dependencies = True break if not found_in_dependencies: # We don't need special dependencies for this project, so drop it and sync it up. roomservice_manifest.remove(removable_project) syncable_projects.append(path) for project in roomservice_manifest.findall('project'): if project.get('path') == path: roomservice_manifest.remove(project) break # Make sure our elements are set. for dependency in dependencies: path = dependency.get('target_path') name = dependency.get('repository') remote = dependency.get('remote') revision = dependency.get('revision') clone_depth = dependency.get('clone-depth') # Store path of every repositories mentioned in dependencies. mentioned_projects.append(path) # Make sure the required remote exists in the upstream manifest. found_remote = False for known_remote in upstream_manifest.findall('remote'): if known_remote.get('name') == remote: found_remote = True break if not found_remote: raise ValueError('No remote declaration could be found for the %s project. (%s)' % (name, remote)) modified_project = False found_in_roomservice = False # In case the project was already added, update it. for project in roomservice_manifest.findall('project'): if project.get('name') == name or project.get('path') == path: if found_in_roomservice: roomservice_manifest.remove(project) else: found_in_roomservice = True msg = '' if project.get('path') != path: modified_project = True project.set('path', path) msg += f'--> Path : Updated {project.get("path")} to {path}\n' if project.get('remote') != remote: modified_project = True project.set('remote', remote) msg += f'--> Remote : Updated {project.get("remote")} to {remote}\n' if project.get('revision') != revision: modified_project = True project.set('revision', revision) msg += f'--> Revision : Updated {project.get("revision")} to {revision}\n' if project.get('clone-depth') != clone_depth: modified_project = True project.set('clone-depth', clone_depth) msg += f'--> Clone depth : Updated {project.get("clone-depth")} to {clone_depth}\n' if project.get('name') != name: modified_project = True project.set('name', name) msg += f'--> Repository : Updated {project.get("name")} to {name}\n' if modified_project: print(f'{name} changed:\n{msg}\n') # In case the project was not already added, create it. if not found_in_roomservice: print('Adding dependency:') print(f'--> Repository : {name}') print(f'--> Path : {path}') print(f'--> Revision : {revision}') print(f'--> Remote : {remote}') found_in_roomservice = True modified_project = True attributes = { 'path': path, 'name': name, 'remote': remote, 'revision': revision, } if clone_depth is not None: attributes['clone-depth'] = clone_depth print(f'--> Clone depth : {clone_depth}') print('\n') roomservice_manifest.append( ET.Element('project', attrib=attributes) ) # In case the project also exists in the main manifest, instruct Repo to ignore that one. for project in upstream_manifest.findall('project'): if project.get('path') == path: upstream_name = project.get('name') found_remove_element = False for removable_project in roomservice_manifest.findall('remove-project'): if removable_project.get('name') == upstream_name: found_remove_element = True break for removable_project in upstream_manifest.findall('remove-project'): if removable_project.get('name') == upstream_name: found_remove_element = True break if not found_remove_element: modified_project = True roomservice_manifest.insert(0, ET.Element('remove-project', attrib = { 'name': upstream_name })) # In case anything has changed, set the project as syncable. if modified_project: syncable_projects.append(path) # Output our manifest. indent(roomservice_manifest) open(roomservice_manifest_path, 'w').write('\n'.join([ '', '', ET.tostring(roomservice_manifest).decode() ])) # If roomservice manifest is perfectly fine, check if there are missing repos to be resynced. if len(syncable_projects) == 0: for path in mentioned_projects: if not os.path.exists(path): print('Dependency to be resynced:') print(f'--> Repository Path : {path}\n') syncable_projects.append(path) # Sync the project that have changed and should be synced. if len(syncable_projects) > 0: print('Syncing the dependencies.') if os.system('repo sync --force-sync --quiet --no-clone-bundle --no-tags %s' % ' '.join(syncable_projects)) != 0: raise ValueError('Got an unexpected exit status from the sync process.')