SRADA-1011: added bundle repackaging script
authorVladislav Eliseev <v.eliseev@samsung.com>
Thu, 11 Aug 2016 16:06:19 +0000 (19:06 +0300)
committerVladislav Eliseev <v.eliseev@samsung.com>
Thu, 18 Aug 2016 16:18:46 +0000 (19:18 +0300)
* added script that repackages specified bundle
  to make them similar to simple jar library:
  - extract embedded libraries and wrap them into bundles
  - remove unused libraries and other embedded libraries
  - clear Bundle-Classpath header
  - update Require-Bundle header
  - remove Bundle-Activator from embedded bundles
  - remove signature files
* added bnd tool v. 3.3.0
  - build from sources https://github.com/bndtools/bnd

Change-Id: I96f344addd7edfcc922f047ceabfb400b55ce3bd

package/bundle-repack.py [new file with mode: 0755]
package/tools/bnd-3.3.0.jar [new file with mode: 0755]

diff --git a/package/bundle-repack.py b/package/bundle-repack.py
new file mode 100755 (executable)
index 0000000..e7dd68c
--- /dev/null
@@ -0,0 +1,606 @@
+#!/usr/bin/env python2
+import zipfile
+import os
+import re
+import shutil
+import tempfile
+
+
+def print3(*kargs):
+    """
+    Helper function to support python2 <-> python3 incompatibility.
+    """
+    print(" ".join(map(str, kargs)))
+
+
+def print_status(msg, status, width, fill):
+    """
+    Helper function to print pretty status messages.
+    """
+    print3((msg + ' ').ljust(width, fill), status)
+
+
+class ZipUtils:
+    @staticmethod
+    def unpack_zip(zip_src, dest_dir):
+        """
+        Unpack zip archive to destination directory.
+        :param zip_src: path to zip archive
+        :param dest_dir: path to destination directory
+        """
+        if not os.path.exists(dest_dir):
+            os.mkdir(dest_dir)
+
+        zfile = zipfile.ZipFile(zip_src)
+        zfile.extractall(dest_dir)
+
+    @staticmethod
+    def pack_zip(src_dir, zip_dst):
+        """
+        Pack source directory to zip archive.
+        :param src_dir: path to source directory
+        :param zip_dst: path to destination archive
+        """
+        if os.path.exists(zip_dst):
+            os.remove(zip_dst)
+
+        zfile = zipfile.ZipFile(zip_dst, 'w')
+        for root, dirs, files in os.walk(src_dir):
+            for file in files:
+                abs_path = os.path.join(root, file)
+                zfile.write(abs_path, os.path.relpath(abs_path, src_dir))
+
+        zfile.close()
+
+
+class Manifest:
+    """
+    Class to work with java MANIFEST.MF files.
+    """
+    HDR_BUNDLE_CLASSPATH = "Bundle-ClassPath"
+    HDR_BUNDLE_SYMBOLICNAME = "Bundle-SymbolicName"
+    HDR_BUNDLE_VERSION = "Bundle-Version"
+    HDR_BUNDLE_ACTIVATOR = "Bundle-Activator"
+    HDR_REQUIRE_BUNDLE = "Require-Bundle"
+    HDR_FRAGMENT_HOST = "Fragment-Host"
+    HDR_INDIVIDUAL_SECTION = "Name"
+
+    @staticmethod
+    def __split_value(value):
+        """
+        Split header value.
+        :param value: value to split
+        :type value: str
+        :rtype: list[list[str]]
+        """
+        result = []
+        i = 0
+        item = ""
+        while i < len(value):
+            if value[i] == '"':
+                item += value[i]
+                i += 1
+                while value[i] != '"':
+                    item += value[i]
+                    i += 1
+                item += value[i]
+            elif value[i] == ',':
+                # TODO seems that not only ',' can separate values
+                result.append(map(str.strip, item.split(';')))
+                item = ""
+            else:
+                item += value[i]
+            i += 1
+
+        if item:
+            result.append(map(str.strip, item.split(';')))
+        return result
+
+    def __parse_manifest_content(self, content_lines):
+        """
+        Manifest parsing function.
+
+        :type content_lines: list[str]
+        :param content_lines: list of lines of manifest file
+        """
+        header, value = "", ""
+        individual_sect_start_idx = None
+        for idx, line in enumerate(content_lines):
+            if len(line.rstrip()) > 0 and line[0] != ' ':
+                # new header
+                if header:
+                    self.headers.append(header)
+                    self.content_map[header] = Manifest.__split_value(value)
+
+                header, value = map(str.strip, line.split(':', 1))
+                if header == Manifest.HDR_INDIVIDUAL_SECTION:
+                    # detected individual sections start
+                    individual_sect_start_idx = idx
+                    break
+            else:
+                value += line.strip()
+
+        if not individual_sect_start_idx:
+            self.headers.append(header)
+            self.content_map[header] = Manifest.__split_value(value)
+        else:
+            self.individual_section = "".join(content_lines[individual_sect_start_idx:])
+
+    def __init__(self, content_lines):
+        """
+        Manifest constructor.
+
+        :type content_lines: list[str]
+        :param content_lines: list of lines of manifest file
+        """
+        self.headers = []  # type: list[str]
+        self.content_map = dict()  # type: dict[str, list[list[str]]]
+        self.individual_section = ""
+
+        self.__parse_manifest_content(content_lines)
+
+    def is_bundle(self):
+        """
+        Tells whether this manifest represents OSGi bundle manifest.
+        """
+        return self.has_header(Manifest.HDR_BUNDLE_SYMBOLICNAME)
+
+    def is_fragment(self):
+        """
+        Tells whether this manifest represents OSGi bundle-fragment manifest.
+        """
+        return self.has_header(Manifest.HDR_FRAGMENT_HOST)
+
+    def has_header(self, header):
+        """
+        Tells whether this manifest has specified header defined.
+
+        :type header: str
+        :param header: manifest header
+        """
+        return header in self.content_map
+
+    def remove_header(self, header):
+        """
+        Remove specified header and corresponding value from manifest.
+        :param header: header to remove
+        """
+        if header in self.content_map:
+            self.headers.remove(header)
+            del self.content_map[header]
+
+    def get_header_value(self, header):
+        """
+        Returns value of specified header in manifest.
+
+        :param header: manifest header
+        :return: value of corresponding header
+        :rtype: list[list[str]]
+        """
+        return self.content_map[header]
+
+    def set_header_value(self, header, value):
+        """
+        Sets value of specified header in manifest.
+
+        :param header: manifest header
+        :param value: value to set
+        :type value: list[list[str]]
+        """
+        if header not in self.headers:
+            self.headers.append(header)
+
+        self.content_map[header] = value
+
+    def serialize(self):
+        """
+        Get manifest written to string.
+
+        :rtype: str
+        """
+        result = ""
+
+        for header in self.headers:
+            result += "{}: {}\n".format(header, ",\n ".join(map(lambda s: ';'.join(s), self.content_map[header])))
+
+        return result + "\n" + self.individual_section
+
+    @staticmethod
+    def write_manifest_in_jar_root(manifest, jar_root):
+        """
+        Write manifest in specified jar root directory.
+
+        :param manifest: manifest to write
+        :type manifest: Manifest
+        :param jar_root: path to jar root directory
+        :type jar_root: str
+        """
+        man_path = os.path.join(jar_root, "META-INF", "MANIFEST.MF")
+        with open(man_path, "w") as man_file:
+            man_file.write(manifest.serialize())
+
+    @staticmethod
+    def write_manifest_in_jar(manifest, jar):
+        """
+        Write manifest in specified jar archive directly.
+        This method packs/unpacks archive.
+
+        :param manifest: manifest to write
+        :type manifest: Manifest
+        :param jar: path to jar archive
+        :type jar: str
+        """
+        tempdir = tempfile.mkdtemp()
+        ZipUtils.unpack_zip(jar, tempdir)
+
+        Manifest.write_manifest_in_jar_root(manifest, tempdir)
+        ZipUtils.pack_zip(tempdir, jar)
+
+    @staticmethod
+    def read_manifest_from_jar(jar):
+        """
+        Read manifest from jar archive (without unpacking).
+        :param jar: path to jar archive
+        :type jar: str
+        :return: manifest from jar archive
+        :rtype: Manifest
+        """
+        zjar = zipfile.ZipFile(jar)
+        with zjar.open(os.path.join("META-INF", "MANIFEST.MF")) as zjar_file:
+            return Manifest(zjar_file.readlines())
+
+    @staticmethod
+    def read_manifest_from_jar_root(jar_root):
+        """
+        Read manifest from specified jar root directory.
+        :param jar_root: path to jar root directory
+        :type jar_root: str
+        :return: manifest from jar archive
+        :rtype: Manifest
+        """
+        man_path = os.path.join(jar_root, "META-INF", "MANIFEST.MF")
+        with open(man_path) as man_file:
+            return Manifest(man_file.readlines())
+
+
+class BundleInfo:
+    """
+    Utility class to store information about bundle.
+    """
+
+    def __init__(self, bundle_name=None, bundle_version=None, bundle_file_name=None, bundle_file_path=None, is_wapped=False):
+        self.bundle_name = bundle_name
+        self.bundle_version = bundle_version
+        self.bundle_file_name = bundle_file_name
+        self.bundle_file_path = bundle_file_path
+        self.is_wrapped = is_wapped
+
+
+class RepackInfo:
+    """
+    Utility class to store information about repackaged bundles.
+    """
+
+    def __init__(self):
+        self.bundle_infos = []  # type: list[BundleInfo]
+
+    def add_bundle(self, bundle_info):
+        """
+        Add bundle info to bundles list.
+
+        :param bundle_info: new bundle info to add
+        :type bundle_info: BundleInfo
+        """
+        self.bundle_infos.append(bundle_info)
+
+
+class Repackager:
+    """
+    Main utility class for repackaging bundles.
+    """
+
+    JAR_NAME_VERSION_RE = re.compile("^([a-zA-Z]\\w*(?:[.\\-][a-zA-Z]\\w*)*)(?:-(\\d+\\.\\d+(?:\\.\\d+)?))?\\.jar$")
+
+    BND_COMMAND = "java -jar ./tools/bnd-3.3.0.jar wrap -b {bsn} -v {ver} -o {out} {jar}"
+
+    def __init__(self, bundle, dest_dir, filter_list=[], check_subdirs=[]):
+        """
+        Repackager constructor.
+
+        :param bundle: bundle to repackage
+        :type bundle: str
+
+        :param dest_dir: directory where to store results
+        :type dest_dir: str
+
+        :param filter_list: list of plugins should be allowed in Require-Bundle header
+        :type filter_list: list[str]
+
+        :param check_subdirs: list of directories which should be suppressed
+        :type check_subdirs: list[str]
+        """
+        self.bundle = bundle  # type: str
+        self.bundle_file_name = os.path.split(self.bundle)[1]
+        self.dest_dir = dest_dir  # type: str
+        self.filter_list = filter_list  # type: list[str]
+        self.check_subdirs = check_subdirs  # type: list[str]
+
+        self.bundle_manifest = None  # type: Manifest
+        self.bundle_root_dir = None  # type: str
+
+        self.repack_info = RepackInfo()
+
+    @staticmethod
+    def wrap_jar_to_bundle(jar_path_src, jar_path_dst):
+        """
+        Wrap specified jar archive in OSGi bundle using bnd-tool.
+        :param jar_path_src: path to source archive
+        :param jar_path_dst: path to destination archive
+        :return: information about wrapped bundle
+        :rtype: BundleInfo
+        """
+        jar_dir, jar_name = os.path.split(jar_path_src)
+
+        match = Repackager.JAR_NAME_VERSION_RE.match(jar_name)
+
+        bundle_name, bundle_version = match.groups()
+        bundle_name = bundle_name.replace("-", ".")
+        bundle_version = "1.0.0" if bundle_version is None else bundle_version
+
+        cmd = Repackager.BND_COMMAND.format(bsn=bundle_name, ver=bundle_version, out=jar_path_dst, jar=jar_path_src)
+        os.system(cmd)
+        return BundleInfo([bundle_name], [bundle_version], bundle_file_name=jar_name, bundle_file_path=jar_path_dst, is_wapped=True)
+
+    def check_subdirectories(self, jar_root):
+        """
+        Check bundle subdirectories for unused libraries in specified jar_root.
+        :type jar_root: path to jar root directory
+        """
+        for subdir in self.check_subdirs:
+            subdir_path = os.path.join(jar_root, subdir)
+            if os.path.exists(subdir_path):
+                for file_name in os.listdir(subdir_path):
+                    lib_path = os.path.join(subdir_path, file_name)
+                    if os.path.isfile(lib_path) and lib_path.endswith(".jar"):
+                        os.remove(lib_path)
+                        print_status(file_name, "REMOVED", 70, '-')
+
+    def filter_bundle_headers(self, jar_root):
+        """
+        Filter headers from manifest in specified jar root.
+        :param jar_root: path to jar root directory
+        """
+        manifest = Manifest.read_manifest_from_jar_root(jar_root)
+
+        try:
+            req_bundle = manifest.get_header_value(Manifest.HDR_REQUIRE_BUNDLE)
+        except KeyError:
+            # print3("No Require-Bundle header in:", os.path.split(jar)[1])
+            return
+
+        req_bundle_new = []
+
+        # check required bundles with accordance to filter list
+        for bundle_v in req_bundle:
+            bundle_name = bundle_v[0]
+            print_status("Require-bundle " + bundle_name, "OK" if bundle_name in self.filter_list else "MISSED", 50, '.')
+            if bundle_name in self.filter_list:
+                req_bundle_new.append(bundle_v)
+
+        # if new require bundle list is empty -- delete header
+        if not req_bundle_new:
+            manifest.remove_header(Manifest.HDR_REQUIRE_BUNDLE)
+        else:
+            manifest.set_header_value(Manifest.HDR_REQUIRE_BUNDLE, req_bundle_new)
+
+        # remove bundle activator header
+        manifest.remove_header(Manifest.HDR_BUNDLE_ACTIVATOR)
+
+        Manifest.write_manifest_in_jar_root(manifest, jar_root)
+
+    def remove_signature_info(self, jar_root):
+        """
+        Remove signature files from specified jar root.
+        :param jar_root: path to jar root directory
+        :return True if any signature was removed, False if there are not signatures
+        :rtype: bool
+        """
+        result = False
+        metainf_dir = os.path.join(jar_root, "META-INF")
+        for f in os.listdir(metainf_dir):
+            if f.endswith(".SF"):
+                os.remove(os.path.join(metainf_dir, f))
+                result = True
+
+        return result
+
+    def update_extracted_bundle(self, bundle_info):
+        """
+        Update bundle that was extracted.
+        :param bundle_info: information about bundle to update
+        :type bundle_info: BundleInfo
+        """
+        tempdir = tempfile.mkdtemp()
+        ZipUtils.unpack_zip(bundle_info.bundle_file_path, tempdir)
+
+        self.check_subdirectories(tempdir)
+        self.filter_bundle_headers(tempdir)
+        if self.remove_signature_info(tempdir):
+            print3("Signature removed:", bundle_info.bundle_name[0])
+
+        ZipUtils.pack_zip(tempdir, bundle_info.bundle_file_path)
+
+    def extract_bundle_libraries(self):
+        """
+        Extract libraries from bundle.
+        """
+        bundle_classpath = self.bundle_manifest.get_header_value(Manifest.HDR_BUNDLE_CLASSPATH)
+        bundle_classpath_new = []
+
+        require_bundle = self.bundle_manifest.get_header_value(Manifest.HDR_REQUIRE_BUNDLE)
+        require_bundle_new = require_bundle[:]
+
+        for lib_v in bundle_classpath:
+            lib = lib_v[0]
+            if lib.endswith(".jar"):
+                # move all jars outside
+                lib_dir, lib_name = os.path.split(lib)
+                lib_path = os.path.join(self.bundle_root_dir, lib)
+                # print3("Analyzing:", lib_name)
+
+                lib_manifest = Manifest.read_manifest_from_jar(lib_path)
+                dest_lib_path = os.path.join(self.dest_dir, lib_name)
+
+                if not lib_manifest.is_bundle():
+                    # wrap to bundle
+                    bundle_info = Repackager.wrap_jar_to_bundle(lib_path, dest_lib_path)
+                else:
+                    # or simply copy otherwise
+                    shutil.copyfile(lib_path, dest_lib_path)
+                    bundle_info = BundleInfo(lib_manifest.get_header_value(Manifest.HDR_BUNDLE_SYMBOLICNAME)[0],
+                                             lib_manifest.get_header_value(Manifest.HDR_BUNDLE_VERSION)[0],
+                                             bundle_file_name=lib_name, bundle_file_path=dest_lib_path, is_wapped=False)
+
+                # add new bundle to filter list
+                self.filter_list.extend(bundle_info.bundle_name)
+
+                # delete source library
+                os.remove(lib_path)
+
+                # add new required bundle
+                if not lib_manifest.is_fragment():
+                    require_bundle_new.append(bundle_info.bundle_name[:1])
+
+                # add new repack info entry
+                self.repack_info.add_bundle(bundle_info)
+
+                print_status(lib_name, "WRAPPED" if bundle_info.is_wrapped else "MOVED", 70, '-')
+            else:
+                # rest leave unchanged
+                bundle_classpath_new.append(lib_v)
+
+        # filter headers in extracted bundles
+        for bundle_info in self.repack_info.bundle_infos:
+            self.update_extracted_bundle(bundle_info)
+
+        # set updated bundle classpath
+        self.bundle_manifest.set_header_value(Manifest.HDR_BUNDLE_CLASSPATH, bundle_classpath_new)
+        # set updated required bundles
+        self.bundle_manifest.set_header_value(Manifest.HDR_REQUIRE_BUNDLE, require_bundle_new)
+
+    def repack(self):
+        """
+        Launches repackaging sequence for bundle.
+        :return: information about repackaged bundles
+        :rtype: RepackInfo
+        """
+        self.bundle_root_dir = tempfile.mkdtemp()
+        print3("Repacking:", self.bundle_file_name)
+
+        # unzip bundle archive
+        ZipUtils.unpack_zip(self.bundle, self.bundle_root_dir)
+
+        # get manifest
+        self.bundle_manifest = Manifest.read_manifest_from_jar_root(self.bundle_root_dir)
+
+        # extract libraries
+        self.extract_bundle_libraries()
+
+        # remove unused libraries
+        self.check_subdirectories(self.bundle_root_dir)
+
+        # save updated manifest
+        Manifest.write_manifest_in_jar_root(self.bundle_manifest, self.bundle_root_dir)
+
+        # pack bundle back to zip
+        ZipUtils.pack_zip(self.bundle_root_dir, self.bundle)
+
+        # remove working directory
+        shutil.rmtree(self.bundle_root_dir)
+
+        # return information about extracted bundles
+        return self.repack_info
+
+
+def update_bundles_info_file(repack_info, bundles_info_path):
+    """
+    Update bundles.info file with new bundle entries.
+
+    :param repack_info: repackaging information
+    :type repack_info: RepackInfo
+
+    :param bundles_info_path: path to bundles.info file
+    :type bundles_info_path: str
+    """
+    print3("Next entries will be added to bundles.info:")
+
+    with open(bundles_info_path, "a") as bundle_file:
+        for bundle_info in repack_info.bundle_infos:
+            entry = "{},{},plugins/{},4,false".format(bundle_info.bundle_name[0], bundle_info.bundle_version[0],
+                                                      bundle_info.bundle_file_name)
+            bundle_file.write(entry + "\n")
+            print3(entry)
+
+
+def get_available_bundles(bundles_info_path):
+    """
+    Get information about available bundles from bundles.info file.
+    :param bundles_info_path: path to bundles.info file
+    """
+    print3("Extracting information from bundles.info")
+    result = []
+
+    with open(bundles_info_path, "r") as bundle_file:
+        for line in bundle_file:
+            if not line.startswith('#'):
+                result.append(line.split(',')[0])
+
+    return result
+
+
+def repack_all(args):
+    """
+    Repack all specified bundles.
+    :param args: script arguments
+    """
+    # prepare output dir
+    if not os.path.exists(args.output_dir):
+        os.mkdir(args.output_dir)
+
+    # extract info about available plugins
+    if args.bundles_info and args.filter:
+        available_bundles = get_available_bundles(args.bundles_info)
+    else:
+        available_bundles = []
+
+    # repack every bundle
+    for bundle in args.bundles:
+        repack_info = Repackager(bundle, args.output_dir, filter_list=available_bundles, check_subdirs=args.check_dirs).repack()
+
+        print3()
+        print3('*' * 80)
+        # update bundles.info entries
+        if args.filter:
+            update_bundles_info_file(repack_info, args.bundles_info)
+
+
+def main():
+    """
+    Module entry point.
+    """
+    import argparse
+
+    parser = argparse.ArgumentParser(description="Repack bundle dependencies")
+    parser.add_argument("-f", "--filter", action="store_true", default=False, help="whether Require-Bundle header should be filtered in extracted bundles (default: False).")
+    parser.add_argument("-c", "--check", metavar="SUBDIR", action="append", default=[], dest="check_dirs", help="bundle subdirectories which should be checked on unused libaries")
+    parser.add_argument("-b", "--binfo", metavar="bundles_info", dest="bundles_info", default=None, help="path to bundles.info file")
+    parser.add_argument("-u", "--update", action="store_true", default=False, help="whether bundles.info should be updated")
+    parser.add_argument("-o", "--output", dest="output_dir", help="distribution plugins directory where to place output bundles", required=True)
+    parser.add_argument(metavar="BUNDLE", dest="bundles", nargs="+", help="bundle to repack")
+
+    args = parser.parse_args()
+    repack_all(args)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/package/tools/bnd-3.3.0.jar b/package/tools/bnd-3.3.0.jar
new file mode 100755 (executable)
index 0000000..68d374a
Binary files /dev/null and b/package/tools/bnd-3.3.0.jar differ