xref: /aosp_15_r20/system/apex/tools/apexd_host.py (revision 33f3758387333dbd2962d7edbd98681940d895da)
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):
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
88  @property
89  def name(self):
90    return self._manifest.name
91
92  @property
93  def path_on_host(self):
94    return self._path_on_host
95
96  @property
97  def path_on_device(self):
98    return self._path_on_device
99
100  # Helper to create apex-info element
101  @property
102  def attrs(self):
103    return {
104        'moduleName': self.name,
105        'modulePath': self.path_on_device,
106        'preinstalledModulePath': self.path_on_device,
107        'versionCode': str(self._manifest.version),
108        'versionName': self._manifest.versionName,
109        'isFactory': 'true',
110        'isActive': 'true',
111        'provideSharedApexLibs': (
112            'true' if self._manifest.provideSharedApexLibs else 'false'
113        ),
114    }
115
116
117def InitTools(tool_path):
118  if tool_path is None:
119    exec_path = os.path.realpath(sys.argv[0])
120    if exec_path.endswith('.py'):
121      script_name = os.path.basename(exec_path)[:-3]
122      sys.exit(
123          f'Do not invoke {exec_path} directly. Instead, use {script_name}'
124      )
125    tool_path = os.path.dirname(os.path.dirname(exec_path))
126
127  def ToolPath(name):
128    path = os.path.join(tool_path, 'bin', name)
129    if not os.path.exists(path):
130      sys.exit(f'Required tool({name}) not found in {tool_path}')
131    return path
132
133  return {
134      tool: ToolPath(tool)
135      for tool in [
136          'deapexer',
137          'debugfs_static',
138          'fsck.erofs',
139      ]
140  }
141
142
143def ScanApexes(partition, real_path) -> list[ApexFile]:
144  apexes = []
145  for path_on_host in glob.glob(
146      os.path.join(real_path, 'apex/*.apex')
147  ) + glob.glob(os.path.join(real_path, 'apex/*.capex')):
148    path_on_device = f'/{partition}/apex/' + os.path.basename(path_on_host)
149    apexes.append(ApexFile(path_on_host, path_on_device))
150  # sort list for stability
151  return sorted(apexes, key=lambda apex: apex.path_on_device)
152
153
154def ActivateApexes(partitions, apex_dir, tools):
155  # Emit apex-info-list.xml
156  impl = minidom.getDOMImplementation()
157  doc = impl.createDocument(None, 'apex-info-list', None)
158  apex_info_list = doc.documentElement
159
160  # Scan each partition for apexes and activate them
161  for partition, real_path in partitions.items():
162    apexes = ScanApexes(partition, real_path)
163
164    # Activate each apex with deapexer
165    for apex_file in apexes:
166      # Multi-apex is ignored
167      if os.path.exists(os.path.join(apex_dir, apex_file.name)):
168        continue
169
170      cmd = [tools['deapexer']]
171      cmd += ['--debugfs_path', tools['debugfs_static']]
172      cmd += ['--fsckerofs_path', tools['fsck.erofs']]
173      cmd += [
174          'extract',
175          apex_file.path_on_host,
176          os.path.join(apex_dir, apex_file.name),
177      ]
178      subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT)
179
180      # Add activated apex_info to apex_info_list
181      apex_info = doc.createElement('apex-info')
182      for name, value in apex_file.attrs.items():
183        apex_info.setAttribute(name, value)
184      apex_info_list.appendChild(apex_info)
185
186  apex_info_list_file = os.path.join(apex_dir, 'apex-info-list.xml')
187  with open(apex_info_list_file, 'wt', encoding='utf-8') as f:
188    doc.writexml(f, encoding='utf-8', addindent='  ', newl='\n')
189
190
191def main():
192  args = ParseArgs()
193
194  partitions = {}
195  for part in PARTITIONS:
196    if vars(args).get(f'{part}_path'):
197      partitions[part] = vars(args).get(f'{part}_path')
198
199  tools = InitTools(args.tool_path)
200  ActivateApexes(partitions, args.apex_path, tools)
201
202
203if __name__ == '__main__':
204  main()
205