blob: 49ce7f1b19e58c8ec813f57db91ae28cbe6191c7 [file] [log] [blame]
laszlocsomord93a1462019-11-04 09:14:39 -08001# Lint as: python2, python3
ajmichael69ef6252017-08-29 00:41:40 +02002# pylint: disable=g-direct-third-party-import
3# Copyright 2017 The Bazel Authors. All rights reserved.
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 extracting resource files from an AAR.
18
19An AAR may contain resources under the /res directory. This tool extracts all
20of the resources into a directory. If no resources exist, it creates an
21empty.xml file that defines no resources.
22
23In the future, this script may be extended to also extract assets.
24"""
25
laszlocsomord93a1462019-11-04 09:14:39 -080026from __future__ import absolute_import
27from __future__ import division
28from __future__ import print_function
29
ajmichael69ef6252017-08-29 00:41:40 +020030import os
31import sys
32import zipfile
33
laszlocsomord93a1462019-11-04 09:14:39 -080034# Do not edit this line. Copybara replaces it with PY2 migration helper.
35from absl import app
36from absl import flags
37import six
38
Laszlo Csomorc77f8912017-09-05 09:35:39 +020039from tools.android import junction
ajmichael69ef6252017-08-29 00:41:40 +020040
laszlocsomord93a1462019-11-04 09:14:39 -080041FLAGS = flags.FLAGS
ajmichael69ef6252017-08-29 00:41:40 +020042
laszlocsomord93a1462019-11-04 09:14:39 -080043flags.DEFINE_string("input_aar", None, "Input AAR")
44flags.mark_flag_as_required("input_aar")
45flags.DEFINE_string("output_res_dir", None, "Output resources directory")
46flags.mark_flag_as_required("output_res_dir")
47flags.DEFINE_string("output_assets_dir", None, "Output assets directory")
ahumesky850a8642020-04-30 17:02:22 -070048flags.DEFINE_string("output_databinding_br_dir", None,
49 "Output directory for databinding br files")
50flags.DEFINE_string("output_databinding_setter_store_dir", None,
51 "Output directory for databinding setter_store.json files")
ajmichael69ef6252017-08-29 00:41:40 +020052
53
54def ExtractResources(aar, output_res_dir):
Laszlo Csomorc77f8912017-09-05 09:35:39 +020055 """Extract resource from an `aar` file to the `output_res_dir` directory."""
ajmichael69ef6252017-08-29 00:41:40 +020056 aar_contains_no_resources = True
Laszlo Csomorc60bfdf2017-09-27 07:38:03 -040057 output_res_dir_abs = os.path.abspath(output_res_dir)
ajmichael69ef6252017-08-29 00:41:40 +020058 for name in aar.namelist():
ajmichael56567bb2018-01-18 12:03:18 -080059 if name.startswith("res/") and not name.endswith("/"):
ajmichael19044cf2018-01-16 14:18:01 -080060 ExtractOneFile(aar, name, output_res_dir_abs)
ajmichael69ef6252017-08-29 00:41:40 +020061 aar_contains_no_resources = False
62 if aar_contains_no_resources:
laszlocsomord93a1462019-11-04 09:14:39 -080063 empty_xml_filename = six.ensure_str(
64 output_res_dir) + "/res/values/empty.xml"
ajmichael19044cf2018-01-16 14:18:01 -080065 WriteFileWithJunctions(empty_xml_filename, b"<resources/>")
66
67
68def ExtractAssets(aar, output_assets_dir):
69 """Extracts assets from an `aar` file to the `output_assets_dir` directory."""
70 aar_contains_no_assets = True
71 output_assets_dir_abs = os.path.abspath(output_assets_dir)
72 for name in aar.namelist():
ajmichael56567bb2018-01-18 12:03:18 -080073 if name.startswith("assets/") and not name.endswith("/"):
ajmichael19044cf2018-01-16 14:18:01 -080074 ExtractOneFile(aar, name, output_assets_dir_abs)
75 aar_contains_no_assets = False
76 if aar_contains_no_assets:
77 # aapt will ignore this file and not print an error message, because it
78 # thinks that it is a swap file. We need to create at least one file so that
79 # Bazel does not complain that the output tree artifact was not created.
laszlocsomord93a1462019-11-04 09:14:39 -080080 empty_asset_filename = (
81 six.ensure_str(output_assets_dir) +
82 "/assets/empty_asset_generated_by_bazel~")
ajmichael19044cf2018-01-16 14:18:01 -080083 WriteFileWithJunctions(empty_asset_filename, b"")
84
85
ahumesky850a8642020-04-30 17:02:22 -070086def ExtractDatabinding(aar, file_suffix, output_databinding_dir):
87 """Extracts databinding metadata files from an `aar`."""
88 output_databinding_dir_abs = os.path.abspath(output_databinding_dir)
89 for name in aar.namelist():
90 if name.startswith("data-binding/") and name.endswith(file_suffix):
91 ExtractOneFile(aar, name, output_databinding_dir_abs)
92
93
ajmichael19044cf2018-01-16 14:18:01 -080094def WriteFileWithJunctions(filename, content):
95 """Writes file including creating any junctions or directories necessary."""
laszlocsomorfc98b442018-02-27 09:42:51 -080096 def _WriteFile(filename):
97 with open(filename, "wb") as openfile:
98 openfile.write(content)
99
ajmichael19044cf2018-01-16 14:18:01 -0800100 if os.name == "nt":
101 # Create a junction to the parent directory, because its path might be too
102 # long. Creating the junction also creates all parent directories.
103 with junction.TempJunction(os.path.dirname(filename)) as junc:
104 filename = os.path.join(junc, os.path.basename(filename))
laszlocsomorfc98b442018-02-27 09:42:51 -0800105 # Write the file within scope of the TempJunction, otherwise the path in
106 # `filename` would no longer be valid.
107 _WriteFile(filename)
ajmichael19044cf2018-01-16 14:18:01 -0800108 else:
109 os.makedirs(os.path.dirname(filename))
laszlocsomorfc98b442018-02-27 09:42:51 -0800110 _WriteFile(filename)
ajmichael19044cf2018-01-16 14:18:01 -0800111
112
113def ExtractOneFile(aar, name, abs_output_dir):
114 """Extract one file from the aar to the output directory."""
115 if os.name == "nt":
116 fullpath = os.path.normpath(os.path.join(abs_output_dir, name))
117 if name[-1] == "/":
118 # The zip entry is a directory. Create a junction to it, which also
119 # takes care of creating the directory and all of its parents in a
120 # longpath-safe manner.
121 # We must pretend to have extracted this directory, even if it's
122 # empty, therefore we mustn't rely on creating it as a parent
123 # directory of a subsequently extracted zip entry (because there may
124 # be no such subsequent entry).
125 with junction.TempJunction(fullpath.rstrip("/")) as juncpath:
126 pass
Laszlo Csomorc60bfdf2017-09-27 07:38:03 -0400127 else:
ajmichael19044cf2018-01-16 14:18:01 -0800128 # The zip entry is a file. Create a junction to its parent directory,
129 # then open the compressed entry as a file object, so we can extract
130 # the data even if the extracted file's path would be too long.
131 # The tradeoff is that we lose the permission bits of the compressed
132 # file, but Unix permissions don't mean much on Windows anyway.
133 with junction.TempJunction(os.path.dirname(fullpath)) as juncpath:
134 extracted_path = os.path.join(juncpath, os.path.basename(fullpath))
135 with aar.open(name) as src_fd:
136 with open(extracted_path, "wb") as dest_fd:
137 dest_fd.write(src_fd.read())
138 else:
139 aar.extract(name, abs_output_dir)
ajmichael69ef6252017-08-29 00:41:40 +0200140
141
laszlocsomord93a1462019-11-04 09:14:39 -0800142def main(unused_argv):
ajmichael69ef6252017-08-29 00:41:40 +0200143 with zipfile.ZipFile(FLAGS.input_aar, "r") as aar:
144 ExtractResources(aar, FLAGS.output_res_dir)
ajmichael19044cf2018-01-16 14:18:01 -0800145 if FLAGS.output_assets_dir is not None:
146 ExtractAssets(aar, FLAGS.output_assets_dir)
ahumesky850a8642020-04-30 17:02:22 -0700147 if FLAGS.output_databinding_br_dir is not None:
148 ExtractDatabinding(aar, "br.bin", FLAGS.output_databinding_br_dir)
149 if FLAGS.output_databinding_setter_store_dir is not None:
150 ExtractDatabinding(aar, "setter_store.json",
151 FLAGS.output_databinding_setter_store_dir)
152
ajmichael69ef6252017-08-29 00:41:40 +0200153
154if __name__ == "__main__":
155 FLAGS(sys.argv)
laszlocsomord93a1462019-11-04 09:14:39 -0800156 app.run(main)