diff --git a/README.md b/README.md new file mode 100644 index 0000000..fe0d1ea --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +[中文](README.md) | [English](README_en.md) + +# AndroidApexTools + +一个帮助解包android的apex包,并支持重新打包成apex的工具 + +> 项目地址:https://github.com/wcedla/AndroidApexTools + +## 功能说明 + +> 当前仅支持linux和wsl子系统,测试环境为win10的wsl2的ubuntu 22.04 + +### 1. 解包 + +```shell +cd 当前脚本存放位置 +sudo python3 ./deapexer.py extract ./foo.apex +``` + +解包后会在当前脚本路径生成manifest和payload两个文件夹,manifest文件夹里面的内容一般不需要修改,payload就是原apex里面img解包后的文件,可以修改 + +### 2. 打包 + +```shell +cd 当前脚本存放位置 +sudo python3 ./apexer.py --api 33 ./bar.apex +``` + +确保当前路径存在manifest和payload文件夹,api参数表示这个apex的android api版本,是必填项 + + +> 注意:重新打包后的apex签名和系统签名不一致,需要核心破解 \ No newline at end of file diff --git a/README_en.md b/README_en.md new file mode 100644 index 0000000..73b3fc9 --- /dev/null +++ b/README_en.md @@ -0,0 +1,32 @@ + +[中文](README.md) | [English](README_en.md) + +- # AndroidApexTools + + A tool to help unpack and repack Android apex packages + + > Project address: https://github.com/wcedla/AndroidApexTools + + ## **Features** + + > Currently only supports Linux and WSL subsystem, tested on Ubuntu 22.04 in WSL2 on Win10 + + ### **1. Unpack** + + ```shell + cd path/to/script + sudo python3 ./deapexer.py extract ./foo.apex + ``` + + After unpacking, manifest and payload folders will be generated under the script path. The manifest folder contains metadata that usually doesn't need modification. The payload folder contains the extracted img files from the original apex, which can be modified. + + ### **2. Repack** + + ```shell + cd path/to/script + sudo python3 ./apexer.py --api 33 ./bar.apex + ``` + + Make sure there are manifest and payload folders under the current path. The api parameter specifies the Android api version for this apex and is required. + + > Note: The repacked apex will have a different signature from the system signature and needs core patch to work. diff --git a/apex_build_info_pb2.py b/apex_build_info_pb2.py new file mode 100644 index 0000000..8855c49 --- /dev/null +++ b/apex_build_info_pb2.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: apex_build_info.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='apex_build_info.proto', + package='apex.proto', + syntax='proto3', + serialized_options=b'\n\020com.android.apexB\006Protos', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n\x15\x61pex_build_info.proto\x12\napex.proto\"\x95\x02\n\rApexBuildInfo\x12\x1b\n\x13\x61pexer_command_line\x18\x01 \x01(\t\x12\x15\n\rfile_contexts\x18\x02 \x01(\x0c\x12\x18\n\x10\x63\x61nned_fs_config\x18\x03 \x01(\x0c\x12\x18\n\x10\x61ndroid_manifest\x18\x04 \x01(\x0c\x12\x17\n\x0fmin_sdk_version\x18\x05 \x01(\t\x12\x1a\n\x12target_sdk_version\x18\x06 \x01(\t\x12\x13\n\x0bno_hashtree\x18\x07 \x01(\x08\x12!\n\x19override_apk_package_name\x18\x08 \x01(\t\x12\x16\n\x0elogging_parent\x18\t \x01(\t\x12\x17\n\x0fpayload_fs_type\x18\n \x01(\tB\x1a\n\x10\x63om.android.apexB\x06Protosb\x06proto3' +) + + + + +_APEXBUILDINFO = _descriptor.Descriptor( + name='ApexBuildInfo', + full_name='apex.proto.ApexBuildInfo', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='apexer_command_line', full_name='apex.proto.ApexBuildInfo.apexer_command_line', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='file_contexts', full_name='apex.proto.ApexBuildInfo.file_contexts', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='canned_fs_config', full_name='apex.proto.ApexBuildInfo.canned_fs_config', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='android_manifest', full_name='apex.proto.ApexBuildInfo.android_manifest', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='min_sdk_version', full_name='apex.proto.ApexBuildInfo.min_sdk_version', index=4, + number=5, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='target_sdk_version', full_name='apex.proto.ApexBuildInfo.target_sdk_version', index=5, + number=6, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='no_hashtree', full_name='apex.proto.ApexBuildInfo.no_hashtree', index=6, + number=7, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='override_apk_package_name', full_name='apex.proto.ApexBuildInfo.override_apk_package_name', index=7, + number=8, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='logging_parent', full_name='apex.proto.ApexBuildInfo.logging_parent', index=8, + number=9, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='payload_fs_type', full_name='apex.proto.ApexBuildInfo.payload_fs_type', index=9, + number=10, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=38, + serialized_end=315, +) + +DESCRIPTOR.message_types_by_name['ApexBuildInfo'] = _APEXBUILDINFO +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +ApexBuildInfo = _reflection.GeneratedProtocolMessageType('ApexBuildInfo', (_message.Message,), { + 'DESCRIPTOR' : _APEXBUILDINFO, + '__module__' : 'apex_build_info_pb2' + # @@protoc_insertion_point(class_scope:apex.proto.ApexBuildInfo) + }) +_sym_db.RegisterMessage(ApexBuildInfo) + + +DESCRIPTOR._options = None +# @@protoc_insertion_point(module_scope) diff --git a/apex_manifest.py b/apex_manifest.py new file mode 100644 index 0000000..db2b985 --- /dev/null +++ b/apex_manifest.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 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. + +import apex_manifest_pb2 +from google.protobuf import message +from google.protobuf.json_format import MessageToJson +import zipfile + +class ApexManifestError(Exception): + + def __init__(self, errmessage): + # Apex Manifest parse error (extra fields) or if required fields not present + self.errmessage = errmessage + + +def ValidateApexManifest(manifest_pb): + # Checking required fields + if manifest_pb.name == "": + raise ApexManifestError("'name' field is required.") + if manifest_pb.version == 0: + raise ApexManifestError("'version' field is required.") + if manifest_pb.noCode and (manifest_pb.preInstallHook or + manifest_pb.postInstallHook): + raise ApexManifestError( + "'noCode' can't be true when either preInstallHook or postInstallHook is set" + ) + +def ParseApexManifest(file): + try: + with open(file, "rb") as f: + manifest_pb = apex_manifest_pb2.ApexManifest() + manifest_pb.ParseFromString(f.read()) + return manifest_pb + except message.DecodeError as err: + raise ApexManifestError(err) + +def fromApex(apexFilePath): + with zipfile.ZipFile(apexFilePath, 'r') as apexFile: + with apexFile.open('apex_manifest.pb') as manifestFile: + manifest = apex_manifest_pb2.ApexManifest() + manifest.ParseFromString(manifestFile.read()) + return manifest + +def toJsonString(manifest): + return MessageToJson(manifest, indent=2) diff --git a/apex_manifest_pb2.py b/apex_manifest_pb2.py new file mode 100644 index 0000000..9afc3ca --- /dev/null +++ b/apex_manifest_pb2.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: apex_manifest.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='apex_manifest.proto', + package='apex.proto', + syntax='proto3', + serialized_options=b'\n\020com.android.apexB\006Protos', + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n\x13\x61pex_manifest.proto\x12\napex.proto\"\xac\x03\n\x0c\x41pexManifest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\x03\x12\x16\n\x0epreInstallHook\x18\x03 \x01(\t\x12\x1b\n\x0fpostInstallHook\x18\x04 \x01(\tB\x02\x18\x01\x12\x13\n\x0bversionName\x18\x05 \x01(\t\x12\x0e\n\x06noCode\x18\x06 \x01(\x08\x12\x19\n\x11provideNativeLibs\x18\x07 \x03(\t\x12\x19\n\x11requireNativeLibs\x18\x08 \x03(\t\x12\x0f\n\x07jniLibs\x18\t \x03(\t\x12\x1d\n\x15requireSharedApexLibs\x18\n \x03(\t\x12\x1d\n\x15provideSharedApexLibs\x18\x0b \x01(\x08\x12\x46\n\rcapexMetadata\x18\x0c \x01(\x0b\x32/.apex.proto.ApexManifest.CompressedApexMetadata\x12 \n\x18supportsRebootlessUpdate\x18\r \x01(\x08\x1a\x34\n\x16\x43ompressedApexMetadata\x12\x1a\n\x12originalApexDigest\x18\x01 \x01(\tB\x1a\n\x10\x63om.android.apexB\x06Protosb\x06proto3' +) + + + + +_APEXMANIFEST_COMPRESSEDAPEXMETADATA = _descriptor.Descriptor( + name='CompressedApexMetadata', + full_name='apex.proto.ApexManifest.CompressedApexMetadata', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='originalApexDigest', full_name='apex.proto.ApexManifest.CompressedApexMetadata.originalApexDigest', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=412, + serialized_end=464, +) + +_APEXMANIFEST = _descriptor.Descriptor( + name='ApexManifest', + full_name='apex.proto.ApexManifest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='apex.proto.ApexManifest.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='version', full_name='apex.proto.ApexManifest.version', index=1, + number=2, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='preInstallHook', full_name='apex.proto.ApexManifest.preInstallHook', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='postInstallHook', full_name='apex.proto.ApexManifest.postInstallHook', index=3, + number=4, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=b'\030\001', file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='versionName', full_name='apex.proto.ApexManifest.versionName', index=4, + number=5, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='noCode', full_name='apex.proto.ApexManifest.noCode', index=5, + number=6, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='provideNativeLibs', full_name='apex.proto.ApexManifest.provideNativeLibs', index=6, + number=7, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='requireNativeLibs', full_name='apex.proto.ApexManifest.requireNativeLibs', index=7, + number=8, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='jniLibs', full_name='apex.proto.ApexManifest.jniLibs', index=8, + number=9, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='requireSharedApexLibs', full_name='apex.proto.ApexManifest.requireSharedApexLibs', index=9, + number=10, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='provideSharedApexLibs', full_name='apex.proto.ApexManifest.provideSharedApexLibs', index=10, + number=11, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='capexMetadata', full_name='apex.proto.ApexManifest.capexMetadata', index=11, + number=12, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='supportsRebootlessUpdate', full_name='apex.proto.ApexManifest.supportsRebootlessUpdate', index=12, + number=13, type=8, cpp_type=7, label=1, + has_default_value=False, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_APEXMANIFEST_COMPRESSEDAPEXMETADATA, ], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=36, + serialized_end=464, +) + +_APEXMANIFEST_COMPRESSEDAPEXMETADATA.containing_type = _APEXMANIFEST +_APEXMANIFEST.fields_by_name['capexMetadata'].message_type = _APEXMANIFEST_COMPRESSEDAPEXMETADATA +DESCRIPTOR.message_types_by_name['ApexManifest'] = _APEXMANIFEST +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +ApexManifest = _reflection.GeneratedProtocolMessageType('ApexManifest', (_message.Message,), { + + 'CompressedApexMetadata' : _reflection.GeneratedProtocolMessageType('CompressedApexMetadata', (_message.Message,), { + 'DESCRIPTOR' : _APEXMANIFEST_COMPRESSEDAPEXMETADATA, + '__module__' : 'apex_manifest_pb2' + # @@protoc_insertion_point(class_scope:apex.proto.ApexManifest.CompressedApexMetadata) + }) + , + 'DESCRIPTOR' : _APEXMANIFEST, + '__module__' : 'apex_manifest_pb2' + # @@protoc_insertion_point(class_scope:apex.proto.ApexManifest) + }) +_sym_db.RegisterMessage(ApexManifest) +_sym_db.RegisterMessage(ApexManifest.CompressedApexMetadata) + + +DESCRIPTOR._options = None +_APEXMANIFEST.fields_by_name['postInstallHook']._options = None +# @@protoc_insertion_point(module_scope) diff --git a/apexer.py b/apexer.py new file mode 100644 index 0000000..7534ff8 --- /dev/null +++ b/apexer.py @@ -0,0 +1,982 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 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. +"""apexer is a command line tool for creating an APEX file, a package format for system components. + +Typical usage: apexer input_dir output.apex + +""" + +import apex_build_info_pb2 +import argparse +import hashlib +import os +import pkgutil +import re +import shlex +import shutil +import subprocess +import sys +import tempfile +import uuid +import xml.etree.ElementTree as ET +import zipfile +import glob +from apex_manifest import ValidateApexManifest +from apex_manifest import ApexManifestError +from apex_manifest import ParseApexManifest +from manifest import android_ns +from manifest import find_child_with_attribute +from manifest import get_children_with_tag +from manifest import get_indent +from manifest import parse_manifest +from manifest import write_xml +from xml.dom import minidom + +tool_path_list = [] +current_dir = "." +BLOCK_SIZE = 4096 + + +def ParseArgs(argv): + parser = argparse.ArgumentParser(description='Create an APEX file') + parser.add_argument( + '-f', '--force', action='store_true', help='force overwriting output') + parser.add_argument( + '-v', '--verbose', action='store_true', help='verbose execution') + parser.add_argument( + '--manifest', + default='apex_manifest.pb', + help='path to the APEX manifest file (.pb)') + parser.add_argument( + '--manifest_json', + required=False, + help='path to the APEX manifest file (Q compatible .json)') + parser.add_argument( + '--android_manifest', + help='path to the AndroidManifest file. If omitted, a default one is created and used' + ) + parser.add_argument( + '--logging_parent', + help=('specify logging parent as an additional tag.' + 'This value is ignored if the logging_parent meta-data tag is present.')) + parser.add_argument( + '--assets_dir', + help='an assets directory to be included in the APEX' + ) + parser.add_argument( + '--file_contexts', + help='selinux file contexts file. Required for "image" APEXs.') + parser.add_argument( + '--canned_fs_config', + help='canned_fs_config specifies uid/gid/mode of files. Required for ' + + '"image" APEXS.') + parser.add_argument( + '--key', help='path to the private key file. Required for "image" APEXs.') + parser.add_argument( + '--pubkey', + help='path to the public key file. Used to bundle the public key in APEX for testing.' + ) + parser.add_argument( + '--signing_args', + help='the extra signing arguments passed to avbtool. Used for "image" APEXs.' + ) + parser.add_argument('-i', '--input', + dest='input_dir', + metavar='INPUT_DIR', help='the directory having files to be packaged') + parser.add_argument('output', metavar='OUTPUT', help='name of the APEX file') + parser.add_argument( + '--payload_type', + metavar='TYPE', + required=False, + default='image', + choices=['image'], + help='type of APEX payload being built..') + parser.add_argument( + '--payload_fs_type', + metavar='FS_TYPE', + required=False, + default='ext4', + choices=['ext4', 'f2fs', 'erofs'], + help='type of filesystem being used for payload image "ext4", "f2fs" or "erofs"') + parser.add_argument( + '--override_apk_package_name', + required=False, + help='package name of the APK container. Default is the apex name in --manifest.' + ) + parser.add_argument( + '--no_hashtree', + required=False, + action='store_true', + help='hashtree is omitted from "image".' + ) + parser.add_argument( + '--android_jar_path', + required=False, + default='prebuilts/sdk/current/public/android.jar', + help='path to use as the source of the android API.') + apexer_path_in_environ = 'APEXER_TOOL_PATH' in os.environ + parser.add_argument( + '--apexer_tool_path', + required=False, + default=os.environ['APEXER_TOOL_PATH'].split(':') + if apexer_path_in_environ else None, + type=lambda s: s.split(':'), + help="""A list of directories containing all the tools used by apexer (e.g. + mke2fs, avbtool, etc.) separated by ':'. Can also be set using the + APEXER_TOOL_PATH environment variable""") + parser.add_argument( + '--target_sdk_version', + required=False, + help='Default target SDK version to use for AndroidManifest.xml') + parser.add_argument( + '--min_sdk_version', + required=False, + help='Default Min SDK version to use for AndroidManifest.xml') + parser.add_argument( + '--do_not_check_keyname', + required=False, + action='store_true', + help='Do not check key name. Use the name of apex instead of the basename of --key.') + parser.add_argument( + '--include_build_info', + required=False, + action='store_true', + help='Include build information file in the resulting apex.') + parser.add_argument( + '--include_cmd_line_in_build_info', + required=False, + action='store_true', + help='Include the command line in the build information file in the resulting apex. ' + 'Note that this makes it harder to make deterministic builds.') + parser.add_argument( + '--build_info', + required=False, + help='Build information file to be used for default values.') + parser.add_argument( + '--payload_only', + action='store_true', + help='Outputs the payload image/zip only.' + ) + parser.add_argument( + '--unsigned_payload_only', + action='store_true', + help="""Outputs the unsigned payload image/zip only. Also, setting this flag implies + --payload_only is set too.""" + ) + parser.add_argument( + '--unsigned_payload', + action='store_true', + help="""Skip signing the apex payload. Used only for testing purposes.""" + ) + parser.add_argument( + '--test_only', + action='store_true', + help=( + 'Add testOnly=true attribute to application element in ' + 'AndroidManifest file.') + ) + parser.add_argument( + '--api', + required=True, + help='Android Api Version for aapt2(安卓版本,33->安卓13,32->安卓12.1,31->安卓12.0,30->安卓11)') + + return parser.parse_args(argv) + + +def FindBinaryPath(binary): + for path in tool_path_list: + binary_path = os.path.join(path, binary) + if os.path.exists(binary_path): + return binary_path + raise Exception('Failed to find binary ' + binary + ' in path ' + + ':'.join(tool_path_list)) + + +def RunCommand(cmd, verbose=False, env=None, expected_return_values={0}): + env = env or {} + env.update(os.environ.copy()) + + cmd[0] = FindBinaryPath(cmd[0]) + + if verbose: + print('Running: ' + ' '.join(cmd)) + p = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) + output, _ = p.communicate() + output = output.decode() + + if verbose or p.returncode not in expected_return_values: + print(output.rstrip()) + + assert p.returncode in expected_return_values, 'Failed to execute: ' + ' '.join(cmd) + + return (output, p.returncode) + + +def GetDirSize(dir_name): + size = 0 + for dirpath, _, filenames in os.walk(dir_name): + size += RoundUp(os.path.getsize(dirpath), BLOCK_SIZE) + for f in filenames: + path = os.path.join(dirpath, f) + if not os.path.isfile(path): + continue + size += RoundUp(os.path.getsize(path), BLOCK_SIZE) + return size + + +def GetFilesAndDirsCount(dir_name): + count = 0 + for root, dirs, files in os.walk(dir_name): + count += (len(dirs) + len(files)) + return count + + +def RoundUp(size, unit): + assert unit & (unit - 1) == 0 + return (size + unit - 1) & (~(unit - 1)) + + +def PrepareAndroidManifest(package, version, test_only): + template = """\ + + + + + +""" + + test_only_attribute = 'android:testOnly="true"' if test_only else '' + return template.format(package=package, version=version, + test_only_attribute=test_only_attribute) + + +def ValidateAndroidManifest(package, android_manifest): + tree = ET.parse(android_manifest) + manifest_tag = tree.getroot() + package_in_xml = manifest_tag.attrib['package'] + if package_in_xml != package: + raise Exception("Package name '" + package_in_xml + "' in '" + + android_manifest + " differ from package name '" + package + + "' in the apex_manifest.pb") + + +def ValidateGeneratedAndroidManifest(android_manifest, test_only): + tree = ET.parse(android_manifest) + manifest_tag = tree.getroot() + application_tag = manifest_tag.find('./application') + if test_only: + test_only_in_xml = application_tag.attrib[ + '{http://schemas.android.com/apk/res/android}testOnly'] + if test_only_in_xml != 'true': + raise Exception('testOnly attribute must be equal to true.') + + +def ValidateArgs(args): + build_info = None + + if args.build_info is not None: + if not os.path.exists(args.build_info): + print("Build info file '" + args.build_info + "' does not exist") + return False + with open(args.build_info, 'rb') as buildInfoFile: + build_info = apex_build_info_pb2.ApexBuildInfo() + build_info.ParseFromString(buildInfoFile.read()) + + if not os.path.exists(args.manifest): + print("Manifest file '" + args.manifest + "' does not exist") + return False + + if not os.path.isfile(args.manifest): + print("Manifest file '" + args.manifest + "' is not a file") + return False + + if args.android_manifest is not None: + if not os.path.exists(args.android_manifest): + print("Android Manifest file '" + args.android_manifest + + "' does not exist") + return False + + if not os.path.isfile(args.android_manifest): + print("Android Manifest file '" + args.android_manifest + + "' is not a file") + return False + elif build_info is not None: + with tempfile.NamedTemporaryFile(delete=False) as temp: + temp.write(build_info.android_manifest) + args.android_manifest = temp.name + + if not os.path.exists(args.input_dir): + print("Input directory '" + args.input_dir + "' does not exist") + return False + + if not os.path.isdir(args.input_dir): + print("Input directory '" + args.input_dir + "' is not a directory") + return False + + if not args.force and os.path.exists(args.output): + print(args.output + ' already exists. Use --force to overwrite.') + return False + + if args.unsigned_payload_only: + args.payload_only = True; + args.unsigned_payload = True; + + if not args.key and not args.unsigned_payload: + print('Missing --key {keyfile} argument!') + return False + + if not args.file_contexts: + if build_info is not None: + with tempfile.NamedTemporaryFile(delete=False) as temp: + temp.write(build_info.file_contexts) + args.file_contexts = temp.name + else: + print('Missing --file_contexts {contexts} argument, or a --build_info argument!') + return False + + if not args.canned_fs_config: + if not args.canned_fs_config: + if build_info is not None: + with tempfile.NamedTemporaryFile(delete=False) as temp: + temp.write(build_info.canned_fs_config) + args.canned_fs_config = temp.name + else: + print('Missing ----canned_fs_config {config} argument, or a --build_info argument!') + return False + + if not args.target_sdk_version: + if build_info is not None: + if build_info.target_sdk_version: + args.target_sdk_version = build_info.target_sdk_version + + if not args.no_hashtree: + if build_info is not None: + if build_info.no_hashtree: + args.no_hashtree = True + + if not args.min_sdk_version: + if build_info is not None: + if build_info.min_sdk_version: + args.min_sdk_version = build_info.min_sdk_version + + if not args.override_apk_package_name: + if build_info is not None: + if build_info.override_apk_package_name: + args.override_apk_package_name = build_info.override_apk_package_name + + if not args.logging_parent: + if build_info is not None: + if build_info.logging_parent: + args.logging_parent = build_info.logging_parent + + return True + + +def GenerateBuildInfo(args): + build_info = apex_build_info_pb2.ApexBuildInfo() + if (args.include_cmd_line_in_build_info): + build_info.apexer_command_line = str(sys.argv) + + with open(args.file_contexts, 'rb') as f: + build_info.file_contexts = f.read() + + with open(args.canned_fs_config, 'rb') as f: + build_info.canned_fs_config = f.read() + + with open(args.android_manifest, 'rb') as f: + build_info.android_manifest = f.read() + + if args.target_sdk_version: + build_info.target_sdk_version = args.target_sdk_version + + if args.min_sdk_version: + build_info.min_sdk_version = args.min_sdk_version + + if args.no_hashtree: + build_info.no_hashtree = True + + if args.override_apk_package_name: + build_info.override_apk_package_name = args.override_apk_package_name + + if args.logging_parent: + build_info.logging_parent = args.logging_parent + + if args.payload_type == 'image': + build_info.payload_fs_type = args.payload_fs_type + + return build_info + + +def AddLoggingParent(android_manifest, logging_parent_value): + """Add logging parent as an additional tag. + + Args: + android_manifest: A string representing AndroidManifest.xml + logging_parent_value: A string representing the logging + parent value. + Raises: + RuntimeError: Invalid manifest + Returns: + A path to modified AndroidManifest.xml + """ + doc = minidom.parse(android_manifest) + manifest = parse_manifest(doc) + logging_parent_key = 'android.content.pm.LOGGING_PARENT' + elems = get_children_with_tag(manifest, 'application') + application = elems[0] if len(elems) == 1 else None + if len(elems) > 1: + raise RuntimeError('found multiple tags') + elif not elems: + application = doc.createElement('application') + indent = get_indent(manifest.firstChild, 1) + first = manifest.firstChild + manifest.insertBefore(doc.createTextNode(indent), first) + manifest.insertBefore(application, first) + + indent = get_indent(application.firstChild, 2) + last = application.lastChild + if last is not None and last.nodeType != minidom.Node.TEXT_NODE: + last = None + + if not find_child_with_attribute(application, 'meta-data', android_ns, + 'name', logging_parent_key): + ul = doc.createElement('meta-data') + ul.setAttributeNS(android_ns, 'android:name', logging_parent_key) + ul.setAttributeNS(android_ns, 'android:value', logging_parent_value) + application.insertBefore(doc.createTextNode(indent), last) + application.insertBefore(ul, last) + last = application.lastChild + + if last and last.nodeType != minidom.Node.TEXT_NODE: + indent = get_indent(application.previousSibling, 1) + application.appendChild(doc.createTextNode(indent)) + + with tempfile.NamedTemporaryFile(delete=False, mode='w') as temp: + write_xml(temp, doc) + return temp.name + + +def ShaHashFiles(file_paths): + """get hash for a number of files.""" + h = hashlib.sha256() + for file_path in file_paths: + with open(file_path, 'rb') as file: + while True: + chunk = file.read(h.block_size) + if not chunk: + break + h.update(chunk) + return h.hexdigest() + + +def CreateImageExt4(args, work_dir, manifests_dir, img_file): + """Create image for ext4 file system.""" + + lost_found_location = os.path.join(args.input_dir, 'lost+found') + if os.path.exists(lost_found_location): + print('Warning: input_dir contains a lost+found/ root folder, which ' + 'has been known to cause non-deterministic apex builds.') + + # sufficiently big = size + 16MB margin + size_in_mb = (GetDirSize(args.input_dir) // (1024 * 1024)) + size_in_mb += 16 + + # Margin is for files that are not under args.input_dir. this consists of + # n inodes for apex_manifest files and 11 reserved inodes for ext4. + # TOBO(b/122991714) eliminate these details. Use build_image.py which + # determines the optimal inode count by first building an image and then + # count the inodes actually used. + inode_num_margin = GetFilesAndDirsCount(manifests_dir) + 11 + inode_num = GetFilesAndDirsCount(args.input_dir) + inode_num_margin + + cmd = ['mke2fs'] + cmd.extend(['-O', '^has_journal']) # because image is read-only + cmd.extend(['-b', str(BLOCK_SIZE)]) + cmd.extend(['-m', '0']) # reserved block percentage + cmd.extend(['-t', 'ext4']) + cmd.extend(['-I', '256']) # inode size + cmd.extend(['-N', str(inode_num)]) + uu = str(uuid.uuid5(uuid.NAMESPACE_URL, 'www.android.com')) + cmd.extend(['-U', uu]) + cmd.extend(['-E', 'hash_seed=' + uu]) + cmd.append(img_file) + cmd.append(str(size_in_mb) + 'M') + with tempfile.NamedTemporaryFile(dir=work_dir, + suffix='mke2fs.conf') as conf_file: + conf_data = pkgutil.get_data('apexer', os.path.join(current_dir, 'bin/mke2fs.conf')) + conf_file.write(conf_data) + conf_file.flush() + RunCommand(cmd, args.verbose, + {'MKE2FS_CONFIG': conf_file.name, 'E2FSPROGS_FAKE_TIME': '1'}) + + # Compile the file context into the binary form + # compiled_file_contexts = os.path.join(work_dir, 'file_contexts.bin') + # cmd = ['sefcontext_compile'] + # cmd.extend(['-o', compiled_file_contexts]) + # cmd.append(args.file_contexts) + # RunCommand(cmd, args.verbose) + + # Add files to the image file + cmd = ['e2fsdroid'] + cmd.append('-e') # input is not android_sparse_file + cmd.extend(['-f', args.input_dir]) + cmd.extend(['-T', '0']) # time is set to epoch + cmd.extend(['-S', args.file_contexts]) + cmd.extend(['-C', args.canned_fs_config]) + cmd.extend(['-a', '/']) + cmd.append('-s') # share dup blocks + cmd.append(img_file) + RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'}) + + cmd = ['e2fsdroid'] + cmd.append('-e') # input is not android_sparse_file + cmd.extend(['-f', manifests_dir]) + cmd.extend(['-T', '0']) # time is set to epoch + cmd.extend(['-S', args.file_contexts]) + cmd.extend(['-C', args.canned_fs_config]) + cmd.extend(['-a', '/']) + cmd.append('-s') # share dup blocks + cmd.append(img_file) + RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'}) + + # Resize the image file to save space + cmd = ['resize2fs'] + cmd.append('-M') # shrink as small as possible + cmd.append(img_file) + RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'}) + + +def CreateImageF2fs(args, manifests_dir, img_file): + """Create image for f2fs file system.""" + # F2FS requires a ~100M minimum size (necessary for ART, could be reduced + # a bit for other) + # TODO(b/158453869): relax these requirements for readonly devices + size_in_mb = (GetDirSize(args.input_dir) // (1024 * 1024)) + size_in_mb += 100 + + # Create an empty image + cmd = ['/usr/bin/fallocate'] + cmd.extend(['-l', str(size_in_mb) + 'M']) + cmd.append(img_file) + RunCommand(cmd, args.verbose) + + # Format the image to F2FS + cmd = ['make_f2fs'] + cmd.extend(['-g', 'android']) + uu = str(uuid.uuid5(uuid.NAMESPACE_URL, 'www.android.com')) + cmd.extend(['-U', uu]) + cmd.extend(['-T', '0']) + cmd.append('-r') # sets checkpointing seed to 0 to remove random bits + cmd.append(img_file) + RunCommand(cmd, args.verbose) + + # Add files to the image + cmd = ['sload_f2fs'] + cmd.extend(['-C', args.canned_fs_config]) + cmd.extend(['-f', manifests_dir]) + cmd.extend(['-s', args.file_contexts]) + cmd.extend(['-T', '0']) + cmd.append(img_file) + RunCommand(cmd, args.verbose, expected_return_values={0, 1}) + + cmd = ['sload_f2fs'] + cmd.extend(['-C', args.canned_fs_config]) + cmd.extend(['-f', args.input_dir]) + cmd.extend(['-s', args.file_contexts]) + cmd.extend(['-T', '0']) + cmd.append(img_file) + RunCommand(cmd, args.verbose, expected_return_values={0, 1}) + + # TODO(b/158453869): resize the image file to save space + + +def CreateImageErofs(args, work_dir, manifests_dir, img_file): + """Create image for erofs file system.""" + # mkfs.erofs doesn't support multiple input + + tmp_input_dir = os.path.join(work_dir, 'tmp_input_dir') + os.mkdir(tmp_input_dir) + cmd = ['/bin/cp', '-ra'] + cmd.extend(glob.glob(manifests_dir + '/*')) + cmd.extend(glob.glob(args.input_dir + '/*')) + cmd.append(tmp_input_dir) + RunCommand(cmd, args.verbose) + + cmd = ['make_erofs'] + cmd.extend(['-z', 'lz4hc']) + cmd.extend(['--fs-config-file', args.canned_fs_config]) + cmd.extend(['--file-contexts', args.file_contexts]) + uu = str(uuid.uuid5(uuid.NAMESPACE_URL, 'www.android.com')) + cmd.extend(['-U', uu]) + cmd.extend(['-T', '0']) + cmd.extend([img_file, tmp_input_dir]) + RunCommand(cmd, args.verbose) + shutil.rmtree(tmp_input_dir) + + # The minimum image size of erofs is 4k, which will cause an error + # when execute generate_hash_tree in avbtool + cmd = ['/bin/ls', '-lgG', img_file] + output, _ = RunCommand(cmd, verbose=False) + image_size = int(output.split()[2]) + if image_size == 4096: + cmd = ['/usr/bin/fallocate', '-l', '8k', img_file] + RunCommand(cmd, verbose=False) + + +def CreateImage(args, work_dir, manifests_dir, img_file): + """create payload image.""" + if args.payload_fs_type == 'ext4': + CreateImageExt4(args, work_dir, manifests_dir, img_file) + elif args.payload_fs_type == 'f2fs': + CreateImageF2fs(args, manifests_dir, img_file) + elif args.payload_fs_type == 'erofs': + CreateImageErofs(args, work_dir, manifests_dir, img_file) + + +def SignImage(args, manifest_apex, img_file): + """sign payload image. + + Args: + args: apexer options + manifest_apex: apex manifest proto + img_file: unsigned payload image file + """ + + if args.do_not_check_keyname or args.unsigned_payload: + key_name = manifest_apex.name + else: + key_name = os.path.basename(os.path.splitext(args.key)[0]) + + cmd = ['avbtool'] + cmd.append('add_hashtree_footer') + cmd.append('--do_not_generate_fec') + cmd.extend(['--algorithm', 'SHA256_RSA4096']) + cmd.extend(['--hash_algorithm', 'sha256']) + cmd.extend(['--key', args.key]) + cmd.extend(['--prop', 'apex.key:' + key_name]) + # Set up the salt based on manifest content which includes name + # and version + salt = hashlib.sha256(manifest_apex.SerializeToString()).hexdigest() + cmd.extend(['--salt', salt]) + cmd.extend(['--image', img_file]) + if args.no_hashtree: + cmd.append('--no_hashtree') + if args.signing_args: + cmd.extend(shlex.split(args.signing_args)) + RunCommand(cmd, args.verbose) + + # Get the minimum size of the partition required. + # TODO(b/113320014) eliminate this step + info, _ = RunCommand(['avbtool', 'info_image', '--image', img_file], + args.verbose) + vbmeta_offset = int(re.search('VBMeta\ offset:\ *([0-9]+)', info).group(1)) + vbmeta_size = int(re.search('VBMeta\ size:\ *([0-9]+)', info).group(1)) + partition_size = RoundUp(vbmeta_offset + vbmeta_size, + BLOCK_SIZE) + BLOCK_SIZE + + # Resize to the minimum size + # TODO(b/113320014) eliminate this step + cmd = ['avbtool'] + cmd.append('resize_image') + cmd.extend(['--image', img_file]) + cmd.extend(['--partition_size', str(partition_size)]) + RunCommand(cmd, args.verbose) + + +def CreateApexPayload(args, work_dir, content_dir, manifests_dir, + manifest_apex): + """Create payload. + + Args: + args: apexer options + work_dir: apex container working directory + content_dir: the working directory for payload contents + manifests_dir: manifests directory + manifest_apex: apex manifest proto + + Returns: + payload file + """ + img_file = os.path.join(content_dir, 'apex_payload.img') + CreateImage(args, work_dir, manifests_dir, img_file) + if not args.unsigned_payload: + SignImage(args, manifest_apex, img_file) + return img_file + + +def CreateAndroidManifestXml(args, work_dir, manifest_apex): + """Create AndroidManifest.xml file. + + Args: + args: apexer options + work_dir: apex container working directory + manifest_apex: apex manifest proto + + Returns: + AndroidManifest.xml file inside the work dir + """ + android_manifest_file = os.path.join(work_dir, 'AndroidManifest.xml') + if not args.android_manifest: + if args.verbose: + print('Creating AndroidManifest ' + android_manifest_file) + with open(android_manifest_file, 'w') as f: + app_package_name = manifest_apex.name + f.write(PrepareAndroidManifest(app_package_name, manifest_apex.version, + args.test_only)) + args.android_manifest = android_manifest_file + ValidateGeneratedAndroidManifest(args.android_manifest, args.test_only) + else: + ValidateAndroidManifest(manifest_apex.name, args.android_manifest) + shutil.copyfile(args.android_manifest, android_manifest_file) + + # If logging parent is specified, add it to the AndroidManifest. + if args.logging_parent: + android_manifest_file = AddLoggingParent(android_manifest_file, + args.logging_parent) + return android_manifest_file + + +def CreateApex(args, work_dir): + if not ValidateArgs(args): + return False + + if args.verbose: + print('Using tools from ' + str(tool_path_list)) + + def CopyFile(src, dst): + if args.verbose: + print('Copying ' + src + ' to ' + dst) + shutil.copyfile(src, dst) + + try: + manifest_apex = CreateApexManifest(args.manifest) + except ApexManifestError as err: + print("'" + args.manifest + "' is not a valid manifest file") + print(err.errmessage) + return False + + # Create content dir and manifests dir, the manifests dir is used to + # create the payload image + content_dir = os.path.join(work_dir, 'content') + os.mkdir(content_dir) + manifests_dir = os.path.join(work_dir, 'manifests') + os.mkdir(manifests_dir) + + # Create AndroidManifest.xml file first so that we can hash the file + # and store the hashed value in the manifest proto buf that goes into + # the payload image. So any change in this file will ensure changes + # in payload image file + android_manifest_file = CreateAndroidManifestXml( + args, work_dir, manifest_apex) + + # APEX manifest is also included in the image. The manifest is included + # twice: once inside the image and once outside the image (but still + # within the zip container). + with open(os.path.join(manifests_dir, 'apex_manifest.pb'), 'wb') as f: + f.write(manifest_apex.SerializeToString()) + with open(os.path.join(content_dir, 'apex_manifest.pb'), 'wb') as f: + f.write(manifest_apex.SerializeToString()) + if args.manifest_json: + CopyFile(args.manifest_json, + os.path.join(manifests_dir, 'apex_manifest.json')) + CopyFile(args.manifest_json, + os.path.join(content_dir, 'apex_manifest.json')) + + # Create payload + img_file = CreateApexPayload(args, work_dir, content_dir, manifests_dir, + manifest_apex) + + if args.unsigned_payload_only or args.payload_only: + shutil.copyfile(img_file, args.output) + if args.verbose: + if args.unsigned_payload_only: + print('Created (unsigned payload only) ' + args.output) + else: + print('Created (payload only) ' + args.output) + return True + + # copy the public key, if specified + if args.pubkey: + shutil.copyfile(args.pubkey, os.path.join(content_dir, 'apex_pubkey')) + + if args.include_build_info: + build_info = GenerateBuildInfo(args) + with open(os.path.join(content_dir, 'apex_build_info.pb'), 'wb') as f: + f.write(build_info.SerializeToString()) + + apk_file = os.path.join(work_dir, 'apex.apk') + cmd = ['aapt2'] + cmd.append('link') + cmd.extend(['--manifest', android_manifest_file]) + if args.override_apk_package_name: + cmd.extend(['--rename-manifest-package', args.override_apk_package_name]) + # This version from apex_manifest.json is used when versionCode isn't + # specified in AndroidManifest.xml + cmd.extend(['--version-code', str(manifest_apex.version)]) + if manifest_apex.versionName: + cmd.extend(['--version-name', manifest_apex.versionName]) + if args.target_sdk_version: + cmd.extend(['--target-sdk-version', args.target_sdk_version]) + if args.min_sdk_version: + cmd.extend(['--min-sdk-version', args.min_sdk_version]) + else: + # Default value for minSdkVersion. + cmd.extend(['--min-sdk-version', '29']) + if args.assets_dir: + cmd.extend(['-A', args.assets_dir]) + cmd.extend(['-o', apk_file]) + cmd.extend(['-I', args.android_jar_path]) + RunCommand(cmd, args.verbose) + + zip_file = os.path.join(work_dir, 'apex.zip') + CreateZip(content_dir, zip_file) + MergeZips([apk_file, zip_file], args.output + "_unsign") + + # if args.verbose: + # print('Created ' + args.output) + + java_toolchain, java_dep_lib = _get_java_toolchain(work_dir) + cmd = [ + java_toolchain, + "-Djava.library.path=" + os.path.join(current_dir, "bin"), + "-jar", os.path.join(current_dir, "bin/signapk.jar"), + "-a", "4096", "--align-file-size", + os.path.join(current_dir, "key/com.android.example.apex.x509.pem"), + os.path.join(current_dir, "key/com.android.example.apex.pk8"), + args.output + "_unsign", args.output] + RunCommand(cmd, args.verbose) + + os.remove(args.output + "_unsign") + print('创建成功,路径:' + args.output) + + return True + + +def _get_java_toolchain(work_dir): + java_toolchain = "java" + if os.path.isfile("/usr/lib/jvm/java-11-openjdk-amd64/bin/java"): + java_toolchain = "/usr/lib/jvm/java-11-openjdk-amd64/bin/java" + elif os.path.isfile("/usr/lib/jvm/java-17-openjdk-amd64/bin/java"): + java_toolchain = "/usr/lib/jvm/java-17-openjdk-amd64/bin/java" + elif "ANDROID_JAVA_TOOLCHAIN" in os.environ: + java_toolchain = os.path.join(os.environ["ANDROID_JAVA_TOOLCHAIN"], "java") + elif "ANDROID_JAVA_HOME" in os.environ: + java_toolchain = os.path.join(os.environ["ANDROID_JAVA_HOME"], "bin", "java") + elif "JAVA_HOME" in os.environ: + java_toolchain = os.path.join(os.environ["JAVA_HOME"], "bin", "java") + else: + java_toolchain = os.path.join(current_dir, "jdk/java") + + java_dep_lib = os.path.join(work_dir, "lib64") + if "ANDROID_HOST_OUT" in os.environ: + java_dep_lib += ":" + os.path.join(os.environ["ANDROID_HOST_OUT"], "lib64") + if "ANDROID_BUILD_TOP" in os.environ: + java_dep_lib += ":" + os.path.join(os.environ["ANDROID_BUILD_TOP"], + "out/host/linux-x86/lib64") + + return [java_toolchain, java_dep_lib] + + +def CreateApexManifest(manifest_path): + try: + manifest_apex = ParseApexManifest(manifest_path) + ValidateApexManifest(manifest_apex) + return manifest_apex + except IOError: + raise ApexManifestError("Cannot read manifest file: '" + manifest_path + "'") + + +class TempDirectory(object): + + def __enter__(self): + self.name = tempfile.mkdtemp() + return self.name + + def __exit__(self, *unused): + shutil.rmtree(self.name) + + +def CreateZip(content_dir, apex_zip): + with zipfile.ZipFile(apex_zip, 'w', compression=zipfile.ZIP_DEFLATED) as out: + for root, _, files in os.walk(content_dir): + for file in files: + path = os.path.join(root, file) + rel_path = os.path.relpath(path, content_dir) + # "apex_payload.img" shouldn't be compressed + if rel_path == 'apex_payload.img': + out.write(path, rel_path, compress_type=zipfile.ZIP_STORED) + else: + out.write(path, rel_path) + + +def MergeZips(zip_files, output_zip): + with zipfile.ZipFile(output_zip, 'w') as out: + for file in zip_files: + # copy to output_zip + with zipfile.ZipFile(file, 'r') as inzip: + for info in inzip.infolist(): + # reset timestamp for deterministic output + info.date_time = (1980, 1, 1, 0, 0, 0) + # reset filemode for deterministic output. The high 16 bits are for + # filemode. 0x81A4 corresponds to 0o100644(a regular file with + # '-rw-r--r--' permission). + info.external_attr = 0x81A40000 + # "apex_payload.img" should be 4K aligned + if info.filename == 'apex_payload.img': + data_offset = out.fp.tell() + len(info.FileHeader()) + info.extra = b'\0' * (BLOCK_SIZE - data_offset % BLOCK_SIZE) + data = inzip.read(info) + out.writestr(info, data) + + +def main(argv): + args = ParseArgs(argv) + global current_dir + current_dir = os.path.dirname(os.path.abspath(__file__)) + if os.path.abspath(".") == os.path.abspath(current_dir): + current_dir = "./" + + if args.api is not None: + args.android_jar_path = os.path.join(current_dir, "bin/android_" + args.api + ".jar") + else: + print("请添加--api xx参数,xx代表android api版本") + sys.exit(1) + + args.input_dir = os.path.join(current_dir, "payload") + args.manifest = os.path.join(current_dir, "manifest/apex_manifest.pb") + args.build_info = os.path.join(current_dir, "manifest/apex_build_info.pb") + assets_dir = os.path.join(current_dir, "manifest/assets") + if os.path.exists(assets_dir): + args.assets_dir = assets_dir + args.key = os.path.join(current_dir, "key/com.android.example.apex.pem") + args.pubkey = os.path.join(current_dir, "key/com.android.example.apex.avbpubkey") + args.include_build_info = True + args.force = True + + global tool_path_list + bin_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bin") + tool_path_list = bin_path.split(":") + with TempDirectory() as work_dir: + success = CreateApex(args, work_dir) + + if not success: + sys.exit(1) + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/bin/aapt b/bin/aapt new file mode 100644 index 0000000..8c6791a Binary files /dev/null and b/bin/aapt differ diff --git a/bin/aapt2 b/bin/aapt2 new file mode 100644 index 0000000..c49ac76 Binary files /dev/null and b/bin/aapt2 differ diff --git a/bin/android_30.jar b/bin/android_30.jar new file mode 100644 index 0000000..5e69644 Binary files /dev/null and b/bin/android_30.jar differ diff --git a/bin/android_31.jar b/bin/android_31.jar new file mode 100644 index 0000000..af4ff4f Binary files /dev/null and b/bin/android_31.jar differ diff --git a/bin/android_32.jar b/bin/android_32.jar new file mode 100644 index 0000000..34f5d75 Binary files /dev/null and b/bin/android_32.jar differ diff --git a/bin/android_33.jar b/bin/android_33.jar new file mode 100644 index 0000000..0ef27d7 Binary files /dev/null and b/bin/android_33.jar differ diff --git a/bin/android_34.jar b/bin/android_34.jar new file mode 100644 index 0000000..923bafc Binary files /dev/null and b/bin/android_34.jar differ diff --git a/bin/avbtool b/bin/avbtool new file mode 100644 index 0000000..ab1aa09 Binary files /dev/null and b/bin/avbtool differ diff --git a/bin/debugfs_static b/bin/debugfs_static new file mode 100644 index 0000000..0a4c0dc Binary files /dev/null and b/bin/debugfs_static differ diff --git a/bin/e2fsdroid b/bin/e2fsdroid new file mode 100644 index 0000000..17b93cf Binary files /dev/null and b/bin/e2fsdroid differ diff --git a/bin/fsck.erofs b/bin/fsck.erofs new file mode 100644 index 0000000..bce81d8 Binary files /dev/null and b/bin/fsck.erofs differ diff --git a/bin/libconscrypt_openjdk_jni-linux-x86_64.so b/bin/libconscrypt_openjdk_jni-linux-x86_64.so new file mode 100644 index 0000000..a7ff05b Binary files /dev/null and b/bin/libconscrypt_openjdk_jni-linux-x86_64.so differ diff --git a/bin/make_f2fs b/bin/make_f2fs new file mode 100644 index 0000000..6bf3115 Binary files /dev/null and b/bin/make_f2fs differ diff --git a/bin/mke2fs b/bin/mke2fs new file mode 100644 index 0000000..19e0f89 Binary files /dev/null and b/bin/mke2fs differ diff --git a/bin/mke2fs.conf b/bin/mke2fs.conf new file mode 100644 index 0000000..8ea960d --- /dev/null +++ b/bin/mke2fs.conf @@ -0,0 +1,53 @@ +[defaults] + base_features = sparse_super,large_file,filetype,dir_index,ext_attr + default_mntopts = acl,user_xattr + enable_periodic_fsck = 0 + blocksize = 4096 + inode_size = 256 + inode_ratio = 16384 + reserved_ratio = 1.0 + +[fs_types] + ext3 = { + features = has_journal + } + ext4 = { + features = has_journal,extent,huge_file,dir_nlink,extra_isize,uninit_bg + inode_size = 256 + } + ext4dev = { + features = has_journal,extent,huge_file,flex_bg,inline_data,64bit,dir_nlink,extra_isize + inode_size = 256 + options = test_fs=1 + } + small = { + blocksize = 1024 + inode_size = 128 + inode_ratio = 4096 + } + floppy = { + blocksize = 1024 + inode_size = 128 + inode_ratio = 8192 + } + big = { + inode_ratio = 32768 + } + huge = { + inode_ratio = 65536 + } + news = { + inode_ratio = 4096 + } + largefile = { + inode_ratio = 1048576 + blocksize = -1 + } + largefile4 = { + inode_ratio = 4194304 + blocksize = -1 + } + hurd = { + blocksize = 4096 + inode_size = 128 + } diff --git a/bin/resize2fs b/bin/resize2fs new file mode 100644 index 0000000..89351d8 Binary files /dev/null and b/bin/resize2fs differ diff --git a/bin/signapk.jar b/bin/signapk.jar new file mode 100644 index 0000000..33deed2 Binary files /dev/null and b/bin/signapk.jar differ diff --git a/bin/zipalign b/bin/zipalign new file mode 100644 index 0000000..787d5dc Binary files /dev/null and b/bin/zipalign differ diff --git a/deapexer.py b/deapexer.py new file mode 100644 index 0000000..e124cde --- /dev/null +++ b/deapexer.py @@ -0,0 +1,491 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 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. +"""deapexer is a tool that prints out content of an APEX. + +To print content of an APEX to stdout: + deapexer list foo.apex + +To extract content of an APEX to the given directory: + deapexer extract foo.apex +""" +from __future__ import print_function + +import argparse +import apex_manifest +import apex_manifest +import enum +import os +import shutil +import sys +import subprocess +import tempfile +import zipfile + +BLOCK_SIZE = 4096 +current_dir = "." +tempdir = "" + +# See apexd/apex_file.cpp#RetrieveFsType +FS_TYPES = [ + ('f2fs', 1024, b'\x10\x20\xf5\xf2'), + ('ext4', 1024 + 0x38, b'\123\357'), + ('erofs', 1024, b'\xe2\xe1\xf5\xe0'), +] + + +def RetrieveFileSystemType(file): + """Returns filesystem type with magic""" + with open(file, 'rb') as f: + for type, offset, magic in FS_TYPES: + buf = bytearray(len(magic)) + f.seek(offset, os.SEEK_SET) + f.readinto(buf) + if buf == magic: + return type + raise ValueError('Failed to retrieve filesystem type') + + +class ApexImageEntry(object): + + def __init__(self, name, base_dir, permissions, size, ino, extents, + is_directory, is_symlink, security_context): + self._name = name + self._base_dir = base_dir + self._permissions = permissions + self._size = size + self._is_directory = is_directory + self._is_symlink = is_symlink + self._ino = ino + self._extents = extents + self._security_context = security_context + + @property + def name(self): + return self._name + + @property + def root(self): + return self._base_dir == './' and self._name == '.' + + @property + def full_path(self): + if self.root: + return self._base_dir # './' + path = os.path.join(self._base_dir, self._name) + if self.is_directory: + path += '/' + return path + + @property + def is_directory(self): + return self._is_directory + + @property + def is_symlink(self): + return self._is_symlink + + @property + def is_regular_file(self): + return not self.is_directory and not self.is_symlink + + @property + def permissions(self): + return self._permissions + + @property + def size(self): + return self._size + + @property + def ino(self): + return self._ino + + @property + def extents(self): + return self._extents + + @property + def security_context(self): + return self._security_context + + def __str__(self): + ret = '' + if self._is_directory: + ret += 'd' + elif self._is_symlink: + ret += 'l' + else: + ret += '-' + + def mask_as_string(m): + ret = 'r' if m & 4 == 4 else '-' + ret += 'w' if m & 2 == 2 else '-' + ret += 'x' if m & 1 == 1 else '-' + return ret + + ret += mask_as_string(self._permissions >> 6) + ret += mask_as_string((self._permissions >> 3) & 7) + ret += mask_as_string(self._permissions & 7) + + return ret + ' ' + self._size + ' ' + self._name + + +class ApexImageDirectory(object): + + def __init__(self, path, entries, apex): + self._path = path + self._entries = sorted(entries, key=lambda e: e.name) + self._apex = apex + + def list(self, is_recursive=False): + for e in self._entries: + yield e + if e.is_directory and e.name != '.' and e.name != '..': + for ce in self.enter_subdir(e).list(is_recursive): + yield ce + + def enter_subdir(self, entry): + return self._apex._list(self._path + entry.name + '/') + + +class Apex(object): + + def __init__(self, args): + self._debugfs = args.debugfs_path + self._fsckerofs = args.fsckerofs_path + self._apex = args.apex + self._tempdir = tempfile.mkdtemp() + global tempdir + tempdir = self._tempdir + with zipfile.ZipFile(self._apex, 'r') as zip_ref: + zip_ref.extractall(self._tempdir) + self._payload = os.path.join(self._tempdir, 'apex_payload.img') + self._payload_fs_type = RetrieveFileSystemType(self._payload) + self._cache = {} + + def __del__(self): + shutil.rmtree(self._tempdir) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + pass + + def list(self, is_recursive=False): + if self._payload_fs_type not in ['ext4']: + sys.exit(f"{self._payload_fs_type} is not supported for `list`.") + + root = self._list('./') + return root.list(is_recursive) + + def _list(self, path): + if path in self._cache: + return self._cache[path] + res = subprocess.check_output([self._debugfs, '-R', 'ls -l -p %s' % path, self._payload], + text=True, stderr=subprocess.DEVNULL) + entries = [] + for line in res.split('\n'): + if not line: + continue + parts = line.split('/') + if len(parts) != 8: + continue + name = parts[5] + if not name: + continue + ino = parts[1] + bits = parts[2] + size = parts[6] + extents = [] + is_symlink = bits[1] == '2' + is_directory = bits[1] == '4' + + if not is_symlink and not is_directory: + stdout = subprocess.check_output([self._debugfs, '-R', 'dump_extents <%s>' % ino, + self._payload], text=True, stderr=subprocess.DEVNULL) + # Output of dump_extents for an inode fragmented in 3 blocks (length and addresses represent + # block-sized sections): + # Level Entries Logical Physical Length Flags + # 0/ 0 1/ 3 0 - 0 18 - 18 1 + # 0/ 0 2/ 3 1 - 15 20 - 34 15 + # 0/ 0 3/ 3 16 - 1863 37 - 1884 1848 + res = stdout.splitlines() + res.pop(0) # the first line contains only columns names + left_length = int(size) + try: # dump_extents sometimes has an unexpected output + for line in res: + tokens = line.split() + offset = int(tokens[7]) * BLOCK_SIZE + length = min(int(tokens[-1]) * BLOCK_SIZE, left_length) + left_length -= length + extents.append((offset, length)) + if (left_length != 0): # dump_extents sometimes fails to display "hole" blocks + raise ValueError + except: + extents = [] # [] means that we failed to retrieve the file location successfully + + # get 'security.selinux' attribute + entry_path = os.path.join(path, name) + stdout = subprocess.check_output([ + self._debugfs, + '-R', + f'ea_get -V {entry_path} security.selinux', + self._payload + ], text=True, stderr=subprocess.DEVNULL) + security_context = stdout.rstrip('\n\x00') + + entries.append(ApexImageEntry(name, + base_dir=path, + permissions=int(bits[3:], 8), + size=size, + is_directory=is_directory, + is_symlink=is_symlink, + ino=ino, + extents=extents, + security_context=security_context)) + + return ApexImageDirectory(path, entries, self) + + def extract(self, dest): + if self._payload_fs_type == 'erofs': + subprocess.run([self._fsckerofs, '--extract=%s' % (dest), '--overwrite', self._payload], + stdout=subprocess.DEVNULL, check=True) + elif self._payload_fs_type == 'ext4': + # Suppress stderr without failure + try: + subprocess.run(["debugfs", '-R', 'rdump ./ %s' % (dest), self._payload], + capture_output=True, check=True) + except subprocess.CalledProcessError as e: + sys.exit(e.stderr) + else: + # TODO(b/279688635) f2fs is not supported yet. + sys.exit(f"{self._payload_fs_type} is not supported for `extract`.") + + +def RunList(args): + if GetType(args.apex) == ApexType.COMPRESSED: + with tempfile.TemporaryDirectory() as temp: + decompressed_apex = os.path.join(temp, 'temp.apex') + decompress(args.apex, decompressed_apex) + args.apex = decompressed_apex + + RunList(args) + return + + with Apex(args) as apex: + for e in apex.list(is_recursive=True): + # dot(., ..) directories + if not e.root and e.name in ('.', '..'): + continue + res = '' + if args.size: + res += e.size + ' ' + res += e.full_path + if args.extents: + res += ' [' + '-'.join(str(x) for x in e.extents) + ']' + if args.contexts: + res += ' ' + e.security_context + print(res) + + +def RunExtract(args): + if GetType(args.apex) == ApexType.COMPRESSED: + with tempfile.TemporaryDirectory() as temp: + decompressed_apex = os.path.join(temp, "temp.apex") + decompress(args.apex, decompressed_apex) + args.apex = decompressed_apex + + RunExtract(args) + return + + with Apex(args) as apex: + args.dest = current_dir + payload_dir = os.path.join(args.dest, "payload") + manifest_dir = os.path.join(args.dest, "manifest") + if not os.path.exists(args.dest): + os.makedirs(args.dest, mode=0o755) + if not os.path.exists(payload_dir): + os.makedirs(payload_dir, mode=0o755) + if not os.path.exists(manifest_dir): + os.makedirs(manifest_dir, mode=0o755) + + apex.extract(payload_dir) + + build_pb = os.path.join(tempdir, "apex_build_info.pb") + manifest_pb = os.path.join(payload_dir, "apex_manifest.pb") + assets_dir = os.path.join(tempdir, "assets") + + if os.path.exists(build_pb): + target_build_pb_path = os.path.join(manifest_dir, "apex_build_info.pb") + if os.path.exists(target_build_pb_path): + os.remove(target_build_pb_path) + shutil.move(build_pb, manifest_dir, copy_function=shutil.copy2) + + if os.path.exists(manifest_pb): + target_manifest_pb_path = os.path.join(manifest_dir, "apex_manifest.pb") + if os.path.exists(target_manifest_pb_path): + os.remove(target_manifest_pb_path) + shutil.move(manifest_pb, manifest_dir, copy_function=shutil.copy2) + + if os.path.exists(assets_dir): + target_assets_path = os.path.join(manifest_dir, "assets") + if os.path.exists(target_assets_path): + shutil.rmtree(target_assets_path) + shutil.move(assets_dir, manifest_dir, copy_function=shutil.copy2) + + if os.path.isdir(os.path.join(payload_dir, "lost+found")): + shutil.rmtree(os.path.join(payload_dir, "lost+found")) + print("解包结束,输出路径:" + current_dir + "下的payload和manifest文件夹") + + +class ApexType(enum.Enum): + INVALID = 0 + UNCOMPRESSED = 1 + COMPRESSED = 2 + + +def GetType(apex_path): + with zipfile.ZipFile(apex_path, 'r') as zip_file: + names = zip_file.namelist() + has_payload = 'apex_payload.img' in names + has_original_apex = 'original_apex' in names + if has_payload and has_original_apex: + return ApexType.INVALID + if has_payload: + return ApexType.UNCOMPRESSED + if has_original_apex: + return ApexType.COMPRESSED + return ApexType.INVALID + + +def RunInfo(args): + if args.print_type: + res = GetType(args.apex) + if res == ApexType.INVALID: + print(args.apex + ' is not a valid apex') + sys.exit(1) + print(res.name) + else: + manifest = apex_manifest.fromApex(args.apex) + print(apex_manifest.toJsonString(manifest)) + + +def RunDecompress(args): + """RunDecompress takes path to compressed APEX and decompresses it to + produce the original uncompressed APEX at give output path + + See apex_compression_tool.py#RunCompress for details on compressed APEX + structure. + + Args: + args.input: file path to compressed APEX + args.output: file path to where decompressed APEX will be placed + """ + compressed_apex_fp = args.input + decompressed_apex_fp = args.output + return decompress(compressed_apex_fp, decompressed_apex_fp) + + +def decompress(compressed_apex_fp, decompressed_apex_fp): + if os.path.exists(decompressed_apex_fp): + print("Output path '" + decompressed_apex_fp + "' already exists") + sys.exit(1) + + with zipfile.ZipFile(compressed_apex_fp, 'r') as zip_obj: + if 'original_apex' not in zip_obj.namelist(): + print(compressed_apex_fp + ' is not a compressed APEX. Missing ' + "'original_apex' file inside it.") + sys.exit(1) + # Rename original_apex file to what user provided as output filename + original_apex_info = zip_obj.getinfo('original_apex') + original_apex_info.filename = os.path.basename(decompressed_apex_fp) + # Extract the original_apex as desired name + zip_obj.extract(original_apex_info, + path=os.path.dirname(decompressed_apex_fp)) + + +def main(argv): + parser = argparse.ArgumentParser() + global current_dir + current_dir = os.path.dirname(os.path.abspath(__file__)) + debugfs_default = os.path.join(current_dir, "bin/debugfs_static") + fsckerofs_default = os.path.join(current_dir, "bin/fsck.erofs") + parser.add_argument('--debugfs_path', help='The path to debugfs binary', default=debugfs_default) + parser.add_argument('--fsckerofs_path', help='The path to fsck.erofs binary', default=fsckerofs_default) + # TODO(b/279858383) remove the argument + parser.add_argument('--blkid_path', help='NOT USED') + + subparsers = parser.add_subparsers(required=True, dest='cmd') + + parser_list = subparsers.add_parser('list', help='prints content of an APEX to stdout') + parser_list.add_argument('apex', type=str, help='APEX file') + parser_list.add_argument('--size', help='also show the size of the files', action="store_true") + parser_list.add_argument('--extents', help='also show the location of the files', action="store_true") + parser_list.add_argument('-Z', '--contexts', + help='also show the security context of the files', + action='store_true') + parser_list.set_defaults(func=RunList) + + parser_extract = subparsers.add_parser('extract', help='extracts content of an APEX to the given ' + 'directory') + parser_extract.add_argument('apex', type=str, help='APEX file') + parser_extract.add_argument('-d', '--dest', type=str, help='Directory to extract content of APEX to') + parser_extract.set_defaults(func=RunExtract) + + parser_info = subparsers.add_parser('info', help='prints APEX manifest') + parser_info.add_argument('apex', type=str, help='APEX file') + parser_info.add_argument('--print-type', + help='Prints type of the apex (COMPRESSED or UNCOMPRESSED)', + action='store_true') + parser_info.set_defaults(func=RunInfo) + + # Handle sub-command "decompress" + parser_decompress = subparsers.add_parser('decompress', + help='decompresses a compressed ' + 'APEX') + parser_decompress.add_argument('--input', type=str, required=True, + help='path to compressed APEX file that ' + 'will be decompressed') + parser_decompress.add_argument('--output', type=str, required=True, + help='output directory path where ' + 'decompressed APEX will be extracted') + parser_decompress.set_defaults(func=RunDecompress) + + args = parser.parse_args(argv) + + debugfs_required_for_cmd = ['list', 'extract'] + if args.cmd in debugfs_required_for_cmd and not args.debugfs_path: + print('ANDROID_HOST_OUT environment variable is not defined, --debugfs_path must be set', + file=sys.stderr) + sys.exit(1) + + if args.cmd == 'extract': + if not args.fsckerofs_path: + print('ANDROID_HOST_OUT environment variable is not defined, --fsckerofs_path must be set', + file=sys.stderr) + sys.exit(1) + + if not os.path.isfile(args.fsckerofs_path): + print(f'Cannot find fsck.erofs specified at {args.fsckerofs_path}', + file=sys.stderr) + sys.exit(1) + + args.func(args) + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/key/com.android.example.apex.avbpubkey b/key/com.android.example.apex.avbpubkey new file mode 100644 index 0000000..28bc8f7 Binary files /dev/null and b/key/com.android.example.apex.avbpubkey differ diff --git a/key/com.android.example.apex.pem b/key/com.android.example.apex.pem new file mode 100644 index 0000000..bd56778 --- /dev/null +++ b/key/com.android.example.apex.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKgIBAAKCAgEAt4iSfTF+e2khGQf0bUzTMwWFsgaiQbwQB3cvyBlE9XekFXUt +GdOEhC2J0p+930UoF6gjjRRrgGF+8K5iV1m3oEbB3qGz6UUOurvVkt4tq96e/Q5a +ogCOZEuWHjZfs2tQUVNJJtptIp9+0cM768vdf+qnK2JNFIhBqSY0FhjVljKevMcM +w2tWFRZnKPQ3JoRnWqi5CIauQtBcWRFKIApyf41uHGMjpQRd8aTGeLXBRTi/yD73 +HltuKwSF2SXpj1F+9j4stqskQvipjQnid/Wb+nN3CNgyrGuRrtGvz71WWYcK3DLM +jvGLOl06QrN6a7ZfLUN4qQjJ6Is5SLTSw/sfFE7Fpcbg6/Geh+jSvChuo6EUtzoX +Qu42HsVXhrJLQ9/AVTWNmGc9IDr4PMtDiQc4FN8MOpUtR6V/zwrZFoeR3PHl9Z7v +uTxLIcQLIott0mAjPhbNgbFBs5HP1Z8TfFcyZWpShlx+aM1V2mzYQ7sgsWjFKMSQ +wIUk/YZ9QK/H5WKjC5M0yxueCU0ocvWFaAZ4RyS/r/SUyQpvyNXNwUsdp1a8sNxp +LP9U7FG64C+T791yoQJ0sKVbts5SEu/Tojw6miYbH6Fspdo2xxfCbrv6SAbkjlct +afOnEepgTlHet0G+y0N7OZRJ9WRGyLJNgGjmmDy9XSYGAykwwe4Fv348D0cCAwEA +AQKCAgBuFra/78NNpXbb++CK+20oCqTyb3Y+dd8rizuXDElH8Fb1JA9EkZLIckRc +mcMbvPDal9mTU29UV6b8Ga4VdVRnCGpb76TqRKkcK3Vlnm3IzUWSx1xoFmtTD9/h +CX6IMdPApHOZoaWbAg7hJfm4a9XWV9ukc1eG/GBeZPMTWhwr9vsugztNsQG2rnR8 +pVi7eupAADrVOWwn2bG7H1rWM04Q4rXswy7rWd48BzmhyGxA6FRpehNjGzbPCOx8 +n3gkpp7Ad/T8MVYT8fJKDmbQy/ue1EnPfVeQAwok0dRiiNDV7OH/yVzYVVzNSoSa +4+uH1qHqlbE3u3TZT0GyMfzG38f4scsbvG/AhH1fuPsy4QcWyLlMV6KUnk3KPc3Q +yOeRR82qndQMTYQ5/PFiilk7cNbTU0OBjuNpu/t1LIE2J2gGZ5Jw+g2NGtM/xsgC +jOahpRYvZB8fZ/bSjirwwmSSU+v0ZoPDHtt75R/QxqwPG2jai8kaGr7GEXWJfrfv +CktMnb6LoCyNiiiZSMUgdDHOQEkVNmt9fxiVaxsaIL4BygropwlD4WbuyRMevfYz +EffvvmaqC24zJi8WzDszCNLgP/piNhXDyxZX+KaQXj0Do/tzWBBkO0OO6mVGOkX2 +6dadXfhOIggWO8K2lKCUKwWMO9LaKwSwZ4gzcc1a+U9rpE8kUQKCAQEA8lBGLzOL +Ht8+d13SY+NdPbL6qGvoqsKd5BfIhaNbH04Cp2zQs2TWySxmV47df03pGUpQOCKn +tFRxoczUrf1gfFDCCC95+A/crls8QJHG+MScTBH5U8Q0s9ReUo/0xaa55u77x5uS +0fAtdnOdqP8/pf1fSXUJvyLW85LWdkge1c7jk7I5MnWVO2Ak9/GkuRgITSSgVdBa +kr8nU1BCzDY0gOTWo5J1+NqqVH2eYfEI621iD4SAE3n2JrCC4K/Nt2enEJwup2TR +ym15g9nClicUQP5Y67eDfqTZu1d0I0Ezl1tL8UPxcLI+ucN4V6KL8RvqTVMnGX/R +s1FwkPVMQ6dKaQKCAQEAweZeggcSFukr+tTbnzDAHxg4YqiR+30wo7i8NadGu6W/ +EiAdcCdmZYMI9KKc+B/N3cuFqBnaSd7VM7XvINdwZRanRj56Ya8LvQMi0S9YPiRn +T4TXC3EeewN5+SSO0Dkw83tW1PLqgSINy5ijBs5lGoIYMCC+GSA2DuRBiPpcfhqJ +kmC9uFQvrsge8CC8Sb1wHCr0Wz34qhPoTff6ZV8wm11Jkb5+tT7PMS5Ft0sEBsxV +R1JFtLNs0k/YpMb4/OrZFZZSIFCTUVPvHQ1/5BwumVnolBC4LORCaSk1xUOydU9h +bZd4qzIpFteGLGGRT6nEWC1YejLAvcFHVJiKs1F2LwKCAQEAzgnwA8bCLvgIt5rx +gLod2I7NkFRhPIHLm92VRf0HSHEe1Jo0Q7Yk5F56j00NjmgDItwLpg/hpfZ/wOLY +nTFrz4kj0636+jESprcxXn4WQAV+GTjXVqDpZ1fW9EEwEriYLoNbV/kzOIwPPD9G ++iJATrZJRb7dEMdhGy/qaB0fCxKmdDoBZKSSxjAUfzfbpv+GX4IbS5ykx07+81q1 +0crtjgQHdoLdCUN1ve4qtIEt4nHaBfPWq7jy0ycXwlH6jE74wajsCq4xrPy1bKXH +TcHg+PrNRXF/wDoQYboVKL0ST0r0IixxqjAGIhLRy0KN1/CypBlmj8od12oSW1AZ +DxW6sQKCAQEAtIMW8M5MVO/2dam8XFMySMBvncl5PjuqEIFnFjwIaaFAZEtpnIPR +nCeFKtpIb+aL7TQP1hNbWPIOYfm6CUUH6dRRHeAEZvRjZS+KNlxxNkkFtM3itVA2 +JCd0YjFakxbrL4FfsRgEoPtnBGexPiDflvIOOqAA2btXGD3/lNofSXbDJHbTqMsX +KQw9YSfYon2t5UtH+bmTyiKGXi/B+KXJxpnuZ7SEmY9DrHF7jcxUj0+jBKbfJf70 +DEcxVRW3rx2jw6kSA+t/enM9ZDqxGVfzOeit0UpPa9uEyAoJeQAxH20rMq+VMyub +fRxgWOjsMtHFbKGqgPjG3uEU2vi4B4CLGQKCAQEA2Mr5f2AXPR8jca1+Id+CxZpU +bgMML7gW31L4lGX9Teo9z+zSdN7sIwqe42Zla1N9wda8p5ribnJxwRdxcPL8bid5 +LLlls4xXD/jQCQCFL90X59Tm6VD6tm1VyCjL44nRwAqP4vJObSB5rTqJYtkfVmnp +KERF5P0i5yv4Oox0ZOsThou9jtyl1dS50Td0Urhp4LhPdmpDPUq25K1sDDfnGFm6 +IcMPkVznRPUoKQCG9DSQcQqttkSV9Po+qfLa3aHtdndfe88Gd9uom8bsAMTZAfSZ +D4YhqBHSLWrxvtQ8GxkaPITJv7hocwssdFRUj5/UJKJBgUXPBXEXh+fxlDaGQQ== +-----END RSA PRIVATE KEY----- diff --git a/key/com.android.example.apex.pk8 b/key/com.android.example.apex.pk8 new file mode 100644 index 0000000..5486be0 Binary files /dev/null and b/key/com.android.example.apex.pk8 differ diff --git a/key/com.android.example.apex.x509.pem b/key/com.android.example.apex.x509.pem new file mode 100644 index 0000000..37f7e70 --- /dev/null +++ b/key/com.android.example.apex.x509.pem @@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +MIIGLzCCBBegAwIBAgIUCinOba8zoolg92Aq2aAtN90sZ+gwDQYJKoZIhvcNAQEL +BQAwgaUxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH +DA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQLDAdBbmRy +b2lkMSEwHwYDVQQDDBhjb20uYW5kcm9pZC5leGFtcGxlLmFwZXgxIjAgBgkqhkiG +9w0BCQEWE2FuZHJvaWRAYW5kcm9pZC5jb20wIBcNMjAwMTIyMDI0NzM0WhgPNDc1 +NzEyMTgwMjQ3MzRaMIGlMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5p +YTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEQMA4GA1UECgwHQW5kcm9pZDEQMA4G +A1UECwwHQW5kcm9pZDEhMB8GA1UEAwwYY29tLmFuZHJvaWQuZXhhbXBsZS5hcGV4 +MSIwIAYJKoZIhvcNAQkBFhNhbmRyb2lkQGFuZHJvaWQuY29tMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAsGOLcwQbHyLJSpwJ4SJZlfX5HjwJ2APvP3Uo +8GQHAPW/NWTO8ti++IPagyhoQVM2JWAfpudSt/QZWceY2z1YDfz4U9S+xIkFFYEu +BGF0Ay+hkq+8Qn3fTUm7ja3Vc1X0WvU5KL37bt7xUCeYtwi98s06y3aZkVqurnVI +j9jg+zO0SiiUJXEF6/kM02vo5MVYbi0MttOcGV53UqsxiUmQiT5rb3oPCFcvBxWZ +p2h+C0Xl9SIkGaI2fswUjY0GFPOKwYaA1WcYANs0XLHM9U6WwIDlqJOSbIx41TvT +MxWRYDMpls6l3i5UDl97r68fIIMtyrexS0Tdr7wJ3nREF5LzxmG/rwEZGcTjxcw+ +ciynkzUOqqgUMz/M8cxlluluZnGCLp5Yfya9ffeZPwcDQh75j5tID5qTCiPyl5BC +X8UNimIie8pecfpIUD4Yyny49sUkMmsXj3aSulfqTaiznMUHv1HrXGCRpm/qj8Vt +cuWpRtiUDpVT+xlegj0MU//eY0RKbQwRcwnRgFWdz9YnP6Jh+xidsBCIg4uZT+Dg +JskSY0PK9t/QDKFnkHugWmw5Wjb9VXHFo312DSXYHATyGpGRvKV4DDkSW6xxTNFe +m1uajDd7xnmjThsEVeMwhN94bQdc8rHNDzqaf/urNQalrwanpqZ5fkyBnMGcSf7i +H7dnaGMCAwEAAaNTMFEwHQYDVR0OBBYEFLZAI5d74SkLtpGLkuykPjo8zrayMB8G +A1UdIwQYMBaAFLZAI5d74SkLtpGLkuykPjo8zrayMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQELBQADggIBAG0zERfLFUkYZ8ymWgClD3qF/x5GNjKlF1KD8BwY ++SGScbM1s32Oz/9VKVJit9wl3OsX/GWcfy+k/tzCb+4ACxIGG8UU8cow5D0l2hm6 +SbDNZFuxaziV/ffwkm7leC/5nkBsIIiCPB5rdwc0/ssWs+1LVSicmoegedVaYgHv +mX8/gNgFaoaM3ssPveF1UceyxT6dlovac1xYzaC17xWHtlj67amiMV9RDuCYpZ/8 +RYUFjELP67ECDtXrg9aTNZcOBTQjNhQzcU6uucgzsaeZ8xv/PnE3iZjvcPeUQdUj +wfd0a7wmQDENS4k5/C535jx5tsl9KdX3w65+mE+tZ88oIky7L0BBykXRvIMOxtWh +oyX4KPYyWfeUnUvFmMavA9JpV5SE+J1Msz/t06JLwvf8luTx2KjWT3mpa+HFr7vB +LupC2z4L3QtVYIoJWl9YD7/G+JxQDvIjVD7iL9q81Z1/gQbaDfksQ0DcdltfEjGn +0+2QdnSPU6EaJM52DG8rOXHW5b9nWNMAeZter3orRCFRS78yVSnnVfxQcjHkrE15 +KJ5dIEj7VZ8MJAmuxxL2qeTfFQShMFcPbhMi2awmkpDe3W/NeKbAX2EVO2jqKOxW +F0CnoA7KPGs1V8d1iQP+05MtUfo0uPFufA57hrxRvV9FdctCJDtVFSYHEUz2qq21 +ALau +-----END CERTIFICATE----- diff --git a/manifest.py b/manifest.py new file mode 100644 index 0000000..04f7405 --- /dev/null +++ b/manifest.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +# +# Copyright (C) 2018 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. +# +"""A tool for inserting values from the build system into a manifest or a test config.""" + +from __future__ import print_function +from xml.dom import minidom + + +android_ns = 'http://schemas.android.com/apk/res/android' + + +def get_children_with_tag(parent, tag_name): + children = [] + for child in parent.childNodes: + if child.nodeType == minidom.Node.ELEMENT_NODE and \ + child.tagName == tag_name: + children.append(child) + return children + + +def find_child_with_attribute(element, tag_name, namespace_uri, + attr_name, value): + for child in get_children_with_tag(element, tag_name): + attr = child.getAttributeNodeNS(namespace_uri, attr_name) + if attr is not None and attr.value == value: + return child + return None + + +def parse_manifest(doc): + """Get the manifest element.""" + + manifest = doc.documentElement + if manifest.tagName != 'manifest': + raise RuntimeError('expected manifest tag at root') + return manifest + + +def ensure_manifest_android_ns(doc): + """Make sure the manifest tag defines the android namespace.""" + + manifest = parse_manifest(doc) + + ns = manifest.getAttributeNodeNS(minidom.XMLNS_NAMESPACE, 'android') + if ns is None: + attr = doc.createAttributeNS(minidom.XMLNS_NAMESPACE, 'xmlns:android') + attr.value = android_ns + manifest.setAttributeNode(attr) + elif ns.value != android_ns: + raise RuntimeError('manifest tag has incorrect android namespace ' + + ns.value) + + +def parse_test_config(doc): + """ Get the configuration element. """ + + test_config = doc.documentElement + if test_config.tagName != 'configuration': + raise RuntimeError('expected configuration tag at root') + return test_config + + +def as_int(s): + try: + i = int(s) + except ValueError: + return s, False + return i, True + + +def compare_version_gt(a, b): + """Compare two SDK versions. + + Compares a and b, treating codenames like 'Q' as higher + than numerical versions like '28'. + + Returns True if a > b + + Args: + a: value to compare + b: value to compare + Returns: + True if a is a higher version than b + """ + + a, a_is_int = as_int(a.upper()) + b, b_is_int = as_int(b.upper()) + + if a_is_int == b_is_int: + # Both are codenames or both are versions, compare directly + return a > b + else: + # One is a codename, the other is not. Return true if + # b is an integer version + return b_is_int + + +def get_indent(element, default_level): + indent = '' + if element is not None and element.nodeType == minidom.Node.TEXT_NODE: + text = element.nodeValue + indent = text[:len(text)-len(text.lstrip())] + if not indent or indent == '\n': + # 1 indent = 4 space + indent = '\n' + (' ' * default_level * 4) + return indent + + +def write_xml(f, doc): + f.write('\n') + for node in doc.childNodes: + f.write(node.toxml(encoding='utf-8') + '\n')