blob: 973a6755b7681701f10db843bc319f5fcef908a3 [file] [log] [blame]
Colin Cross72119102019-05-20 13:14:18 -07001#!/usr/bin/env python
2#
3# Copyright (C) 2018 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#
17"""A tool for checking that a manifest agrees with the build system."""
18
19from __future__ import print_function
20
21import argparse
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000022import re
23import subprocess
Colin Cross72119102019-05-20 13:14:18 -070024import sys
25from xml.dom import minidom
26
27
28from manifest import android_ns
29from manifest import get_children_with_tag
30from manifest import parse_manifest
31from manifest import write_xml
32
33
34class ManifestMismatchError(Exception):
35 pass
36
37
38def parse_args():
39 """Parse commandline arguments."""
40
41 parser = argparse.ArgumentParser()
42 parser.add_argument('--uses-library', dest='uses_libraries',
43 action='append',
44 help='specify uses-library entries known to the build system')
45 parser.add_argument('--optional-uses-library',
46 dest='optional_uses_libraries',
47 action='append',
48 help='specify uses-library entries known to the build system with required:false')
49 parser.add_argument('--enforce-uses-libraries',
50 dest='enforce_uses_libraries',
51 action='store_true',
52 help='check the uses-library entries known to the build system against the manifest')
Ulya Trafimovich8c35fcf2021-02-17 16:23:28 +000053 parser.add_argument('--enforce-uses-libraries-relax',
54 dest='enforce_uses_libraries_relax',
55 action='store_true',
56 help='do not fail immediately, just save the error message to file')
57 parser.add_argument('--enforce-uses-libraries-status',
58 dest='enforce_uses_libraries_status',
59 help='output file to store check status (error message)')
Colin Cross72119102019-05-20 13:14:18 -070060 parser.add_argument('--extract-target-sdk-version',
61 dest='extract_target_sdk_version',
62 action='store_true',
63 help='print the targetSdkVersion from the manifest')
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000064 parser.add_argument('--aapt',
65 dest='aapt',
66 help='path to aapt executable')
Colin Cross72119102019-05-20 13:14:18 -070067 parser.add_argument('--output', '-o', dest='output', help='output AndroidManifest.xml file')
68 parser.add_argument('input', help='input AndroidManifest.xml file')
69 return parser.parse_args()
70
71
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000072def enforce_uses_libraries(manifest, required, optional, relax, is_apk = False):
73 """Verify that the <uses-library> tags in the manifest match those provided
74 by the build system.
Colin Cross72119102019-05-20 13:14:18 -070075
76 Args:
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000077 manifest: manifest (either parsed XML or aapt dump of APK)
78 required: required libs known to the build system
79 optional: optional libs known to the build system
80 relax: if true, suppress error on mismatch and just write it to file
81 is_apk: if the manifest comes from an APK or an XML file
Colin Cross72119102019-05-20 13:14:18 -070082 """
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000083 if is_apk:
84 manifest_required, manifest_optional = extract_uses_libs_apk(manifest)
85 else:
86 manifest_required, manifest_optional = extract_uses_libs_xml(manifest)
Colin Cross72119102019-05-20 13:14:18 -070087
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000088 if required is None:
89 required = []
Colin Cross72119102019-05-20 13:14:18 -070090
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000091 if optional is None:
92 optional = []
Colin Cross72119102019-05-20 13:14:18 -070093
94 err = []
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000095 if manifest_required != required:
Colin Cross72119102019-05-20 13:14:18 -070096 err.append('Expected required <uses-library> tags "%s", got "%s"' %
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000097 (', '.join(required), ', '.join(manifest_required)))
Colin Cross72119102019-05-20 13:14:18 -070098
Ulya Trafimovich0aba2522021-03-03 16:38:37 +000099 if manifest_optional != optional:
Colin Cross72119102019-05-20 13:14:18 -0700100 err.append('Expected optional <uses-library> tags "%s", got "%s"' %
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000101 (', '.join(optional), ', '.join(manifest_optional)))
Colin Cross72119102019-05-20 13:14:18 -0700102
103 if err:
Ulya Trafimovich8c35fcf2021-02-17 16:23:28 +0000104 errmsg = '\n'.join(err)
105 if not relax:
106 raise ManifestMismatchError(errmsg)
107 return errmsg
Colin Cross72119102019-05-20 13:14:18 -0700108
Ulya Trafimovich8c35fcf2021-02-17 16:23:28 +0000109 return None
Colin Cross72119102019-05-20 13:14:18 -0700110
Colin Cross72119102019-05-20 13:14:18 -0700111
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000112def extract_uses_libs_apk(badging):
113 """Extract <uses-library> tags from the manifest of an APK."""
114
115 pattern = re.compile("^uses-library(-not-required)?:'(.*)'$", re.MULTILINE)
116
117 required = []
118 optional = []
119 for match in re.finditer(pattern, badging):
120 libname = match.group(2)
121 if match.group(1) == None:
122 required.append(libname)
123 else:
124 optional.append(libname)
125
126 return first_unique_elements(required), first_unique_elements(optional)
127
128
129def extract_uses_libs_xml(xml):
130 """Extract <uses-library> tags from the manifest."""
131
132 manifest = parse_manifest(xml)
133 elems = get_children_with_tag(manifest, 'application')
134 application = elems[0] if len(elems) == 1 else None
135 if len(elems) > 1:
136 raise RuntimeError('found multiple <application> tags')
137 elif not elems:
138 if uses_libraries or optional_uses_libraries:
139 raise ManifestMismatchError('no <application> tag found')
140 return
Colin Cross72119102019-05-20 13:14:18 -0700141
142 libs = get_children_with_tag(application, 'uses-library')
143
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000144 required = [uses_library_name(x) for x in libs if uses_library_required(x)]
145 optional = [uses_library_name(x) for x in libs if not uses_library_required(x)]
Colin Cross72119102019-05-20 13:14:18 -0700146
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000147 return first_unique_elements(required), first_unique_elements(optional)
Colin Cross72119102019-05-20 13:14:18 -0700148
149
150def first_unique_elements(l):
151 result = []
152 [result.append(x) for x in l if x not in result]
153 return result
154
155
156def uses_library_name(lib):
157 """Extract the name attribute of a uses-library tag.
158
159 Args:
160 lib: a <uses-library> tag.
161 """
162 name = lib.getAttributeNodeNS(android_ns, 'name')
163 return name.value if name is not None else ""
164
165
166def uses_library_required(lib):
167 """Extract the required attribute of a uses-library tag.
168
169 Args:
170 lib: a <uses-library> tag.
171 """
172 required = lib.getAttributeNodeNS(android_ns, 'required')
173 return (required.value == 'true') if required is not None else True
174
175
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000176def extract_target_sdk_version(manifest, is_apk = False):
Colin Cross72119102019-05-20 13:14:18 -0700177 """Returns the targetSdkVersion from the manifest.
178
179 Args:
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000180 manifest: manifest (either parsed XML or aapt dump of APK)
181 is_apk: if the manifest comes from an APK or an XML file
Colin Cross72119102019-05-20 13:14:18 -0700182 """
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000183 if is_apk:
184 return extract_target_sdk_version_apk(manifest)
185 else:
186 return extract_target_sdk_version_xml(manifest)
Colin Cross72119102019-05-20 13:14:18 -0700187
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000188
189def extract_target_sdk_version_apk(badging):
190 """Extract targetSdkVersion tags from the manifest of an APK."""
191
192 pattern = re.compile("^targetSdkVersion?:'(.*)'$", re.MULTILINE)
193
194 for match in re.finditer(pattern, badging):
195 return match.group(1)
196
197 raise RuntimeError('cannot find targetSdkVersion in the manifest')
198
199
200def extract_target_sdk_version_xml(xml):
201 """Extract targetSdkVersion tags from the manifest."""
202
203 manifest = parse_manifest(xml)
Colin Cross72119102019-05-20 13:14:18 -0700204
205 # Get or insert the uses-sdk element
206 uses_sdk = get_children_with_tag(manifest, 'uses-sdk')
207 if len(uses_sdk) > 1:
208 raise RuntimeError('found multiple uses-sdk elements')
209 elif len(uses_sdk) == 0:
210 raise RuntimeError('missing uses-sdk element')
211
212 uses_sdk = uses_sdk[0]
213
214 min_attr = uses_sdk.getAttributeNodeNS(android_ns, 'minSdkVersion')
215 if min_attr is None:
216 raise RuntimeError('minSdkVersion is not specified')
217
218 target_attr = uses_sdk.getAttributeNodeNS(android_ns, 'targetSdkVersion')
219 if target_attr is None:
220 target_attr = min_attr
221
222 return target_attr.value
223
224
225def main():
226 """Program entry point."""
227 try:
228 args = parse_args()
229
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000230 # The input can be either an XML manifest or an APK, they are parsed and
231 # processed in different ways.
232 is_apk = args.input.endswith('.apk')
233 if is_apk:
234 aapt = args.aapt if args.aapt != None else "aapt"
235 manifest = subprocess.check_output([aapt, "dump", "badging", args.input])
236 else:
237 manifest = minidom.parse(args.input)
Colin Cross72119102019-05-20 13:14:18 -0700238
239 if args.enforce_uses_libraries:
Ulya Trafimovich8c35fcf2021-02-17 16:23:28 +0000240 # Check if the <uses-library> lists in the build system agree with those
241 # in the manifest. Raise an exception on mismatch, unless the script was
242 # passed a special parameter to suppress exceptions.
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000243 errmsg = enforce_uses_libraries(manifest, args.uses_libraries,
244 args.optional_uses_libraries, args.enforce_uses_libraries_relax,
245 is_apk)
Ulya Trafimovich8c35fcf2021-02-17 16:23:28 +0000246
247 # Create a status file that is empty on success, or contains an error
248 # message on failure. When exceptions are suppressed, dexpreopt command
249 # command will check file size to determine if the check has failed.
250 if args.enforce_uses_libraries_status:
251 with open(args.enforce_uses_libraries_status, 'w') as f:
252 if not errmsg == None:
253 f.write("%s\n" % errmsg)
Colin Cross72119102019-05-20 13:14:18 -0700254
255 if args.extract_target_sdk_version:
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000256 print(extract_target_sdk_version(manifest, is_apk))
Colin Cross72119102019-05-20 13:14:18 -0700257
258 if args.output:
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000259 # XML output is supposed to be written only when this script is invoked
260 # with XML input manifest, not with an APK.
261 if is_apk:
262 raise RuntimeError('cannot save APK manifest as XML')
263
Colin Cross72119102019-05-20 13:14:18 -0700264 with open(args.output, 'wb') as f:
Ulya Trafimovich0aba2522021-03-03 16:38:37 +0000265 write_xml(f, manifest)
Colin Cross72119102019-05-20 13:14:18 -0700266
267 # pylint: disable=broad-except
268 except Exception as err:
269 print('error: ' + str(err), file=sys.stderr)
270 sys.exit(-1)
271
272if __name__ == '__main__':
273 main()