--- /dev/null
+#!/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()