#!/usr/bin/env python # # Copyright (C) 2023 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # 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. """apexd_host simulates apexd on host. Basically the tool scans .apex/.capex files from given directories (e.g --system_path) and extracts them under the output directory (--apex_path) using the deapexer tool. It also generates apex-info-list.xml file because some tools need that file as well to know the partitions of APEX files. This can be used when handling APEX files on host at buildtime or with target-files. For example, check_target_files_vintf tool invokes checkvintf with target-files, which, in turn, needs to read VINTF fragment files in APEX files. Hence, check_target_files_vintf can use apexd_host before checkvintf. Example: $ apexd_host --apex_path /path/to/apex --system_path /path/to/system """ from __future__ import print_function import argparse import glob import os import subprocess import sys from xml.dom import minidom import apex_manifest # This should be in sync with kApexPackageBuiltinDirs in # system/apex/apexd/apex_constants.h PARTITIONS = ['system', 'system_ext', 'product', 'vendor', 'odm'] def DirectoryType(path): if not os.path.exists(path): return None if not os.path.isdir(path): raise argparse.ArgumentTypeError(f'{path} is not a directory') return os.path.realpath(path) def ExistentDirectoryType(path): if not os.path.exists(path): raise argparse.ArgumentTypeError(f'{path} is not found') return DirectoryType(path) def ParseArgs(): parser = argparse.ArgumentParser() parser.add_argument('--tool_path', help='Tools are searched in TOOL_PATH/bin') parser.add_argument( '--apex_path', required=True, type=ExistentDirectoryType, help='Path to the directory where to activate APEXes', ) for part in PARTITIONS: parser.add_argument( f'--{part}_path', help=f'Path to the directory corresponding /{part} on device', type=DirectoryType, ) return parser.parse_args() class ApexFile(object): """Represents an APEX file.""" def __init__(self, path_on_host, path_on_device): self._path_on_host = path_on_host self._path_on_device = path_on_device self._manifest = apex_manifest.fromApex(path_on_host) @property def name(self): return self._manifest.name @property def path_on_host(self): return self._path_on_host @property def path_on_device(self): return self._path_on_device # Helper to create apex-info element @property def attrs(self): return { 'moduleName': self.name, 'modulePath': self.path_on_device, 'preinstalledModulePath': self.path_on_device, 'versionCode': str(self._manifest.version), 'versionName': self._manifest.versionName, 'isFactory': 'true', 'isActive': 'true', 'provideSharedApexLibs': ( 'true' if self._manifest.provideSharedApexLibs else 'false' ), } def InitTools(tool_path): if tool_path is None: exec_path = os.path.realpath(sys.argv[0]) if exec_path.endswith('.py'): script_name = os.path.basename(exec_path)[:-3] sys.exit( f'Do not invoke {exec_path} directly. Instead, use {script_name}' ) tool_path = os.path.dirname(os.path.dirname(exec_path)) def ToolPath(name): path = os.path.join(tool_path, 'bin', name) if not os.path.exists(path): sys.exit(f'Required tool({name}) not found in {tool_path}') return path return { tool: ToolPath(tool) for tool in [ 'deapexer', 'debugfs_static', 'fsck.erofs', ] } def ScanApexes(partition, real_path) -> list[ApexFile]: apexes = [] for path_on_host in glob.glob( os.path.join(real_path, 'apex/*.apex') ) + glob.glob(os.path.join(real_path, 'apex/*.capex')): path_on_device = f'/{partition}/apex/' + os.path.basename(path_on_host) apexes.append(ApexFile(path_on_host, path_on_device)) # sort list for stability return sorted(apexes, key=lambda apex: apex.path_on_device) def ActivateApexes(partitions, apex_dir, tools): # Emit apex-info-list.xml impl = minidom.getDOMImplementation() doc = impl.createDocument(None, 'apex-info-list', None) apex_info_list = doc.documentElement # Scan each partition for apexes and activate them for partition, real_path in partitions.items(): apexes = ScanApexes(partition, real_path) # Activate each apex with deapexer for apex_file in apexes: # Multi-apex is ignored if os.path.exists(os.path.join(apex_dir, apex_file.name)): continue cmd = [tools['deapexer']] cmd += ['--debugfs_path', tools['debugfs_static']] cmd += ['--fsckerofs_path', tools['fsck.erofs']] cmd += [ 'extract', apex_file.path_on_host, os.path.join(apex_dir, apex_file.name), ] subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT) # Add activated apex_info to apex_info_list apex_info = doc.createElement('apex-info') for name, value in apex_file.attrs.items(): apex_info.setAttribute(name, value) apex_info_list.appendChild(apex_info) apex_info_list_file = os.path.join(apex_dir, 'apex-info-list.xml') with open(apex_info_list_file, 'wt', encoding='utf-8') as f: doc.writexml(f, encoding='utf-8', addindent=' ', newl='\n') def main(): args = ParseArgs() partitions = {} for part in PARTITIONS: if vars(args).get(f'{part}_path'): partitions[part] = vars(args).get(f'{part}_path') tools = InitTools(args.tool_path) ActivateApexes(partitions, args.apex_path, tools) if __name__ == '__main__': main()