| # Copyright 2015 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. |
| |
| """Construct a dex manifest from a set of input .dex.zip files. |
| |
| Usage: %s <output manifest> <input zip file>* |
| %s @<params file> |
| |
| Input files must be either .zip files containing one or more .dex files or |
| .dex files. |
| |
| A manifest file is written that contains one line for each input dex in the |
| following form: |
| |
| <input zip> <path in input zip> <path in output zip> <MD5 checksum> |
| |
| or |
| |
| <input dex> - <path in output zip> <SHA-256 checksum> |
| """ |
| |
| import hashlib |
| import os |
| # pylint: disable=g-import-not-at-top |
| try: |
| # python2 without compatibility package |
| from Queue import Queue |
| except ImportError: |
| # python3 |
| from queue import Queue |
| import shutil |
| import sys |
| import tempfile |
| from threading import Thread |
| import zipfile |
| |
| |
| class DexmanifestBuilder(object): |
| """Implementation of the dex manifest builder.""" |
| |
| def __init__(self): |
| self.manifest_lines = [] |
| self.dir_counter = 1 |
| self.output_dex_counter = 1 |
| self.checksums = set() |
| self.tmpdir = None |
| self.queue = Queue() |
| self.threads_list = list() |
| |
| def __enter__(self): |
| self.tmpdir = tempfile.mkdtemp() |
| return self |
| |
| def __exit__(self, unused_type, unused_value, unused_traceback): |
| shutil.rmtree(self.tmpdir, True) |
| |
| def Checksum(self, filename, input_dex_or_zip, zippath): |
| """Compute the SHA-256 checksum of a file. |
| |
| This method could be invoked concurrently. |
| |
| Therefore we need to include other metadata like input_dex_or_zip to |
| keep the context. |
| """ |
| h = hashlib.sha256() |
| with open(filename, "rb") as f: |
| while True: |
| data = f.read(65536) |
| if not data: |
| break |
| |
| h.update(data) |
| |
| return h.hexdigest(), input_dex_or_zip, zippath |
| |
| def AddDexes(self, dex_metadata_list): |
| """Adds all dex file together to the output. |
| |
| Sort the result to make sure the dexes order are always the same given |
| the same input. |
| Args: |
| dex_metadata_list: A list of [fs_checksum, input_dex_or_zip, zippath], |
| where fs_checksum is the SHA-256 checksum for dex file, input_dex_or_zip |
| is the input file written to the manifest, zippath is the zip path |
| written to the manifest or None if the input file is not a .zip. |
| |
| Returns: |
| None. |
| """ |
| dex_metadata_list_sorted = sorted( |
| dex_metadata_list, key=lambda x: (x[1], x[2])) |
| for dex_metadata in dex_metadata_list_sorted: |
| fs_checksum, input_dex_or_zip, zippath = dex_metadata[0], dex_metadata[ |
| 1], dex_metadata[2] |
| if fs_checksum in self.checksums: |
| return |
| self.checksums.add(fs_checksum) |
| zip_dex = "incremental_classes%d.dex" % self.output_dex_counter |
| self.output_dex_counter += 1 |
| self.manifest_lines.append( |
| "%s %s %s %s" % |
| (input_dex_or_zip, zippath if zippath else "-", zip_dex, fs_checksum)) |
| |
| def ComputeChecksumConcurrently(self, input_dex_or_zip, zippath, dex): |
| """Call Checksum concurrently to improve build performance when an app contains multiple dex files.""" |
| t = Thread(target=lambda q, arg1, arg2, arg3: q.put(self.Checksum(arg1, arg2, arg3)), \ |
| args=(self.queue, dex, input_dex_or_zip, zippath)) |
| t.start() |
| self.threads_list.append(t) |
| |
| def Run(self, argv): |
| """Creates a dex manifest.""" |
| if len(argv) < 1: |
| raise Exception("At least one argument expected") |
| |
| if argv[0][0] == "@": |
| if len(argv) != 1: |
| raise IOError("A parameter file should be the only argument") |
| with open(argv[0][1:]) as param_file: |
| argv = [a.strip() for a in param_file.readlines()] |
| |
| for input_filename in argv[1:]: |
| input_filename = input_filename.strip() |
| if input_filename.endswith(".zip"): |
| with zipfile.ZipFile(input_filename, "r") as input_dex_zip: |
| input_dex_dir = os.path.join(self.tmpdir, str(self.dir_counter)) |
| os.makedirs(input_dex_dir) |
| self.dir_counter += 1 |
| |
| for input_dex_dex in input_dex_zip.namelist(): |
| if not input_dex_dex.endswith(".dex"): |
| continue |
| |
| input_dex_zip.extract(input_dex_dex, input_dex_dir) |
| fs_dex = input_dex_dir + "/" + input_dex_dex |
| self.ComputeChecksumConcurrently(input_filename, input_dex_dex, |
| fs_dex) |
| elif input_filename.endswith(".dex"): |
| self.ComputeChecksumConcurrently(input_filename, None, input_filename) |
| # Collect results from all threads |
| for t in self.threads_list: |
| t.join() |
| |
| results = [] |
| while not self.queue.empty(): |
| fs_checksum, input_dex_or_zip, zippath = self.queue.get() |
| results.append([fs_checksum, input_dex_or_zip, zippath]) |
| self.AddDexes(results) |
| |
| with open(argv[0], "wb") as manifest: |
| manifest.write(("\n".join(self.manifest_lines)).encode("utf-8")) |
| |
| |
| def main(argv): |
| with DexmanifestBuilder() as b: |
| b.Run(argv[1:]) |
| |
| |
| if __name__ == "__main__": |
| main(sys.argv) |