blob: b4c85774d2b42d2c068a0b108d7b3cca4a2727c9 [file] [log] [blame]
ajmichael69ef6252017-08-29 00:41:40 +02001# pylint: disable=g-direct-third-party-import
2# Copyright 2017 The Bazel Authors. All rights reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""A tool for extracting resource files from an AAR.
17
18An AAR may contain resources under the /res directory. This tool extracts all
19of the resources into a directory. If no resources exist, it creates an
20empty.xml file that defines no resources.
21
22In the future, this script may be extended to also extract assets.
23"""
24
25import os
26import sys
27import zipfile
28
Laszlo Csomorc77f8912017-09-05 09:35:39 +020029from tools.android import junction
ajmichael69ef6252017-08-29 00:41:40 +020030from third_party.py import gflags
31
32FLAGS = gflags.FLAGS
33
34gflags.DEFINE_string("input_aar", None, "Input AAR")
35gflags.MarkFlagAsRequired("input_aar")
36gflags.DEFINE_string("output_res_dir", None, "Output resources directory")
37gflags.MarkFlagAsRequired("output_res_dir")
ajmichael19044cf2018-01-16 14:18:01 -080038gflags.DEFINE_string("output_assets_dir", None, "Output assets directory")
ajmichael69ef6252017-08-29 00:41:40 +020039
40
41def ExtractResources(aar, output_res_dir):
Laszlo Csomorc77f8912017-09-05 09:35:39 +020042 """Extract resource from an `aar` file to the `output_res_dir` directory."""
ajmichael69ef6252017-08-29 00:41:40 +020043 aar_contains_no_resources = True
Laszlo Csomorc60bfdf2017-09-27 07:38:03 -040044 output_res_dir_abs = os.path.abspath(output_res_dir)
ajmichael69ef6252017-08-29 00:41:40 +020045 for name in aar.namelist():
ajmichael56567bb2018-01-18 12:03:18 -080046 if name.startswith("res/") and not name.endswith("/"):
ajmichael19044cf2018-01-16 14:18:01 -080047 ExtractOneFile(aar, name, output_res_dir_abs)
ajmichael69ef6252017-08-29 00:41:40 +020048 aar_contains_no_resources = False
49 if aar_contains_no_resources:
50 empty_xml_filename = output_res_dir + "/res/values/empty.xml"
ajmichael19044cf2018-01-16 14:18:01 -080051 WriteFileWithJunctions(empty_xml_filename, b"<resources/>")
52
53
54def ExtractAssets(aar, output_assets_dir):
55 """Extracts assets from an `aar` file to the `output_assets_dir` directory."""
56 aar_contains_no_assets = True
57 output_assets_dir_abs = os.path.abspath(output_assets_dir)
58 for name in aar.namelist():
ajmichael56567bb2018-01-18 12:03:18 -080059 if name.startswith("assets/") and not name.endswith("/"):
ajmichael19044cf2018-01-16 14:18:01 -080060 ExtractOneFile(aar, name, output_assets_dir_abs)
61 aar_contains_no_assets = False
62 if aar_contains_no_assets:
63 # aapt will ignore this file and not print an error message, because it
64 # thinks that it is a swap file. We need to create at least one file so that
65 # Bazel does not complain that the output tree artifact was not created.
66 empty_asset_filename = (output_assets_dir +
67 "/assets/empty_asset_generated_by_bazel~")
68 WriteFileWithJunctions(empty_asset_filename, b"")
69
70
71def WriteFileWithJunctions(filename, content):
72 """Writes file including creating any junctions or directories necessary."""
laszlocsomorfc98b442018-02-27 09:42:51 -080073 def _WriteFile(filename):
74 with open(filename, "wb") as openfile:
75 openfile.write(content)
76
ajmichael19044cf2018-01-16 14:18:01 -080077 if os.name == "nt":
78 # Create a junction to the parent directory, because its path might be too
79 # long. Creating the junction also creates all parent directories.
80 with junction.TempJunction(os.path.dirname(filename)) as junc:
81 filename = os.path.join(junc, os.path.basename(filename))
laszlocsomorfc98b442018-02-27 09:42:51 -080082 # Write the file within scope of the TempJunction, otherwise the path in
83 # `filename` would no longer be valid.
84 _WriteFile(filename)
ajmichael19044cf2018-01-16 14:18:01 -080085 else:
86 os.makedirs(os.path.dirname(filename))
laszlocsomorfc98b442018-02-27 09:42:51 -080087 _WriteFile(filename)
ajmichael19044cf2018-01-16 14:18:01 -080088
89
90def ExtractOneFile(aar, name, abs_output_dir):
91 """Extract one file from the aar to the output directory."""
92 if os.name == "nt":
93 fullpath = os.path.normpath(os.path.join(abs_output_dir, name))
94 if name[-1] == "/":
95 # The zip entry is a directory. Create a junction to it, which also
96 # takes care of creating the directory and all of its parents in a
97 # longpath-safe manner.
98 # We must pretend to have extracted this directory, even if it's
99 # empty, therefore we mustn't rely on creating it as a parent
100 # directory of a subsequently extracted zip entry (because there may
101 # be no such subsequent entry).
102 with junction.TempJunction(fullpath.rstrip("/")) as juncpath:
103 pass
Laszlo Csomorc60bfdf2017-09-27 07:38:03 -0400104 else:
ajmichael19044cf2018-01-16 14:18:01 -0800105 # The zip entry is a file. Create a junction to its parent directory,
106 # then open the compressed entry as a file object, so we can extract
107 # the data even if the extracted file's path would be too long.
108 # The tradeoff is that we lose the permission bits of the compressed
109 # file, but Unix permissions don't mean much on Windows anyway.
110 with junction.TempJunction(os.path.dirname(fullpath)) as juncpath:
111 extracted_path = os.path.join(juncpath, os.path.basename(fullpath))
112 with aar.open(name) as src_fd:
113 with open(extracted_path, "wb") as dest_fd:
114 dest_fd.write(src_fd.read())
115 else:
116 aar.extract(name, abs_output_dir)
ajmichael69ef6252017-08-29 00:41:40 +0200117
118
119def main():
120 with zipfile.ZipFile(FLAGS.input_aar, "r") as aar:
121 ExtractResources(aar, FLAGS.output_res_dir)
ajmichael19044cf2018-01-16 14:18:01 -0800122 if FLAGS.output_assets_dir is not None:
123 ExtractAssets(aar, FLAGS.output_assets_dir)
ajmichael69ef6252017-08-29 00:41:40 +0200124
125if __name__ == "__main__":
126 FLAGS(sys.argv)
127 main()