1#!/usr/bin/env python 2# 3# Copyright (C) 2023 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16"""apexd_host simulates apexd on host. 17 18Basically the tool scans .apex/.capex files from given directories 19(e.g --system_path) and extracts them under the output directory (--apex_path) 20using the deapexer tool. It also generates apex-info-list.xml file because 21some tools need that file as well to know the partitions of APEX files. 22 23This can be used when handling APEX files on host at buildtime or with 24target-files. For example, check_target_files_vintf tool invokes checkvintf with 25target-files, which, in turn, needs to read VINTF fragment files in APEX files. 26Hence, check_target_files_vintf can use apexd_host before checkvintf. 27 28Example: 29 $ apexd_host --apex_path /path/to/apex --system_path /path/to/system 30""" 31from __future__ import print_function 32 33import argparse 34import glob 35import os 36import subprocess 37import sys 38from xml.dom import minidom 39 40import apex_manifest 41 42 43# This should be in sync with kApexPackageBuiltinDirs in 44# system/apex/apexd/apex_constants.h 45PARTITIONS = ['system', 'system_ext', 'product', 'vendor', 'odm'] 46 47 48def DirectoryType(path): 49 if not os.path.exists(path): 50 return None 51 if not os.path.isdir(path): 52 raise argparse.ArgumentTypeError(f'{path} is not a directory') 53 return os.path.realpath(path) 54 55 56def ExistentDirectoryType(path): 57 if not os.path.exists(path): 58 raise argparse.ArgumentTypeError(f'{path} is not found') 59 return DirectoryType(path) 60 61 62def ParseArgs(): 63 parser = argparse.ArgumentParser() 64 parser.add_argument('--tool_path', help='Tools are searched in TOOL_PATH/bin') 65 parser.add_argument( 66 '--apex_path', 67 required=True, 68 type=ExistentDirectoryType, 69 help='Path to the directory where to activate APEXes', 70 ) 71 for part in PARTITIONS: 72 parser.add_argument( 73 f'--{part}_path', 74 help=f'Path to the directory corresponding /{part} on device', 75 type=DirectoryType, 76 ) 77 return parser.parse_args() 78 79 80class ApexFile(object): 81 """Represents an APEX file.""" 82 83 def __init__(self, path_on_host, path_on_device, partition): 84 self._path_on_host = path_on_host 85 self._path_on_device = path_on_device 86 self._manifest = apex_manifest.fromApex(path_on_host) 87 self._partition = partition 88 89 @property 90 def name(self): 91 return self._manifest.name 92 93 @property 94 def path_on_host(self): 95 return self._path_on_host 96 97 @property 98 def path_on_device(self): 99 return self._path_on_device 100 101 @property 102 def partition(self): 103 return self._partition 104 105 # Helper to create apex-info element 106 @property 107 def attrs(self): 108 return { 109 'moduleName': self.name, 110 'modulePath': self.path_on_device, 111 'partition': self.partition.upper(), 112 'preinstalledModulePath': self.path_on_device, 113 'versionCode': str(self._manifest.version), 114 'versionName': self._manifest.versionName, 115 'isFactory': 'true', 116 'isActive': 'true', 117 'provideSharedApexLibs': ( 118 'true' if self._manifest.provideSharedApexLibs else 'false' 119 ), 120 } 121 122 123def InitTools(tool_path): 124 if tool_path is None: 125 exec_path = os.path.realpath(sys.argv[0]) 126 if exec_path.endswith('.py'): 127 script_name = os.path.basename(exec_path)[:-3] 128 sys.exit( 129 f'Do not invoke {exec_path} directly. Instead, use {script_name}' 130 ) 131 tool_path = os.path.dirname(os.path.dirname(exec_path)) 132 133 def ToolPath(name, candidates): 134 for candidate in candidates: 135 path = os.path.join(tool_path, 'bin', candidate) 136 if os.path.exists(path): 137 return path 138 sys.exit(f'Required tool({name}) not found in {tool_path}') 139 140 tools = { 141 'deapexer': ['deapexer'], 142 'debugfs': ['debugfs', 'debugfs_static'], 143 'fsckerofs': ['fsck.erofs'], 144 } 145 return { 146 tool: ToolPath(tool, candidates) 147 for tool, candidates in tools.items() 148 } 149 150 151def ScanApexes(partition, real_path) -> list[ApexFile]: 152 apexes = [] 153 for path_on_host in glob.glob( 154 os.path.join(real_path, 'apex/*.apex') 155 ) + glob.glob(os.path.join(real_path, 'apex/*.capex')): 156 path_on_device = f'/{partition}/apex/' + os.path.basename(path_on_host) 157 apexes.append(ApexFile(path_on_host, path_on_device, partition)) 158 # sort list for stability 159 return sorted(apexes, key=lambda apex: apex.path_on_device) 160 161 162def ActivateApexes(partitions, apex_dir, tools): 163 # Emit apex-info-list.xml 164 impl = minidom.getDOMImplementation() 165 doc = impl.createDocument(None, 'apex-info-list', None) 166 apex_info_list = doc.documentElement 167 168 # Scan each partition for apexes and activate them 169 for partition, real_path in partitions.items(): 170 apexes = ScanApexes(partition, real_path) 171 172 # Activate each apex with deapexer 173 for apex_file in apexes: 174 # Multi-apex is ignored 175 if os.path.exists(os.path.join(apex_dir, apex_file.name)): 176 continue 177 178 cmd = [tools['deapexer']] 179 cmd += ['--debugfs_path', tools['debugfs']] 180 cmd += ['--fsckerofs_path', tools['fsckerofs']] 181 cmd += [ 182 'extract', 183 apex_file.path_on_host, 184 os.path.join(apex_dir, apex_file.name), 185 ] 186 subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT) 187 188 # Add activated apex_info to apex_info_list 189 apex_info = doc.createElement('apex-info') 190 for name, value in apex_file.attrs.items(): 191 apex_info.setAttribute(name, value) 192 apex_info_list.appendChild(apex_info) 193 194 apex_info_list_file = os.path.join(apex_dir, 'apex-info-list.xml') 195 with open(apex_info_list_file, 'wt', encoding='utf-8') as f: 196 doc.writexml(f, encoding='utf-8', addindent=' ', newl='\n') 197 198 199def main(): 200 args = ParseArgs() 201 202 partitions = {} 203 for part in PARTITIONS: 204 if vars(args).get(f'{part}_path'): 205 partitions[part] = vars(args).get(f'{part}_path') 206 207 tools = InitTools(args.tool_path) 208 ActivateApexes(partitions, args.apex_path, tools) 209 210 211if __name__ == '__main__': 212 main() 213