blob: 80c959fd677ab15ed47a4747b1402e375e3caf0d [file] [log] [blame]
#!/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']
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()