blob: f2e49aba6708a910ed9e2c8eb98d6a1658422033 [file] [log] [blame] [edit]
# pylint: disable=g-direct-third-party-import
# Copyright 2017 The Bazel Authors. All rights reserved.
#
# 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 extracting resource files from an AAR.
An AAR may contain resources under the /res directory. This tool extracts all
of the resources into a directory. If no resources exist, it creates an
empty.xml file that defines no resources.
In the future, this script may be extended to also extract assets.
"""
import os
import zipfile
# Do not edit this line. Copybara replaces it with PY2 migration helper.
from absl import app
from absl import flags
from tools.android import json_worker_wrapper
from tools.android import junction
FLAGS = flags.FLAGS
flags.DEFINE_string("input_aar", None, "Input AAR")
flags.mark_flag_as_required("input_aar")
flags.DEFINE_string("output_res_dir", None, "Output resources directory")
flags.mark_flag_as_required("output_res_dir")
flags.DEFINE_string("output_assets_dir", None, "Output assets directory")
flags.DEFINE_string("output_databinding_br_dir", None,
"Output directory for databinding br files")
flags.DEFINE_string("output_databinding_setter_store_dir", None,
"Output directory for databinding setter_store.json files")
def ExtractResources(aar, output_res_dir):
"""Extract resource from an `aar` file to the `output_res_dir` directory."""
aar_contains_no_resources = True
output_res_dir_abs = os.path.abspath(output_res_dir)
for name in aar.namelist():
if name.startswith("res/") and not name.endswith("/"):
ExtractOneFile(aar, name, output_res_dir_abs)
aar_contains_no_resources = False
if aar_contains_no_resources:
empty_xml_filename = output_res_dir + "/res/values/empty.xml"
WriteFileWithJunctions(empty_xml_filename, b"<resources/>")
def ExtractAssets(aar, output_assets_dir):
"""Extracts assets from an `aar` file to the `output_assets_dir` directory."""
aar_contains_no_assets = True
output_assets_dir_abs = os.path.abspath(output_assets_dir)
for name in aar.namelist():
if name.startswith("assets/") and not name.endswith("/"):
ExtractOneFile(aar, name, output_assets_dir_abs)
aar_contains_no_assets = False
if aar_contains_no_assets:
# aapt will ignore this file and not print an error message, because it
# thinks that it is a swap file. We need to create at least one file so that
# Bazel does not complain that the output tree artifact was not created.
empty_asset_filename = (
output_assets_dir +
"/assets/empty_asset_generated_by_bazel~")
WriteFileWithJunctions(empty_asset_filename, b"")
def ExtractDatabinding(aar, file_suffix, output_databinding_dir):
"""Extracts databinding metadata files from an `aar`."""
output_databinding_dir_abs = os.path.abspath(output_databinding_dir)
for name in aar.namelist():
if name.startswith("data-binding/") and name.endswith(file_suffix):
ExtractOneFile(aar, name, output_databinding_dir_abs)
def WriteFileWithJunctions(filename, content):
"""Writes file including creating any junctions or directories necessary."""
def _WriteFile(filename):
with open(filename, "wb") as openfile:
openfile.write(content)
if os.name == "nt":
# Create a junction to the parent directory, because its path might be too
# long. Creating the junction also creates all parent directories.
with junction.TempJunction(os.path.dirname(filename)) as junc:
filename = os.path.join(junc, os.path.basename(filename))
# Write the file within scope of the TempJunction, otherwise the path in
# `filename` would no longer be valid.
_WriteFile(filename)
else:
os.makedirs(os.path.dirname(filename))
_WriteFile(filename)
def ExtractOneFile(aar, name, abs_output_dir):
"""Extract one file from the aar to the output directory."""
if os.name == "nt":
fullpath = os.path.normpath(os.path.join(abs_output_dir, name))
if name[-1] == "/":
# The zip entry is a directory. Create a junction to it, which also
# takes care of creating the directory and all of its parents in a
# longpath-safe manner.
# We must pretend to have extracted this directory, even if it's
# empty, therefore we mustn't rely on creating it as a parent
# directory of a subsequently extracted zip entry (because there may
# be no such subsequent entry).
with junction.TempJunction(fullpath.rstrip("/")) as juncpath:
pass
else:
# The zip entry is a file. Create a junction to its parent directory,
# then open the compressed entry as a file object, so we can extract
# the data even if the extracted file's path would be too long.
# The tradeoff is that we lose the permission bits of the compressed
# file, but Unix permissions don't mean much on Windows anyway.
with junction.TempJunction(os.path.dirname(fullpath)) as juncpath:
extracted_path = os.path.join(juncpath, os.path.basename(fullpath))
with aar.open(name) as src_fd:
with open(extracted_path, "wb") as dest_fd:
dest_fd.write(src_fd.read())
else:
aar.extract(name, abs_output_dir)
def main(unused_argv):
with zipfile.ZipFile(FLAGS.input_aar, "r") as aar:
ExtractResources(aar, FLAGS.output_res_dir)
if FLAGS.output_assets_dir is not None:
ExtractAssets(aar, FLAGS.output_assets_dir)
if FLAGS.output_databinding_br_dir is not None:
ExtractDatabinding(aar, "br.bin", FLAGS.output_databinding_br_dir)
if FLAGS.output_databinding_setter_store_dir is not None:
ExtractDatabinding(aar, "setter_store.json",
FLAGS.output_databinding_setter_store_dir)
if __name__ == "__main__":
json_worker_wrapper.wrap_worker(FLAGS, main, app.run)