From f04963f7529a590d33bccd64a97cae8d2b419d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A1=B0=EC=9A=B0=EC=84=B1?= <32770312+fhdufhdu@users.noreply.github.com> Date: Sat, 3 Aug 2024 16:03:20 +0900 Subject: [PATCH] Add dependency graph generation feature (#214) Signed-off-by: fhdufhdu --- requirements.txt | 4 +- src/fosslight_dependency/_graph_convertor.py | 68 +++++++++++++++++++ src/fosslight_dependency/_help.py | 4 ++ .../run_dependency_scanner.py | 24 ++++++- 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 src/fosslight_dependency/_graph_convertor.py diff --git a/requirements.txt b/requirements.txt index 91e5803b..90ce9e36 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,6 @@ fosslight_util>=1.4.47 PyGithub requirements-parser defusedxml -packageurl-python \ No newline at end of file +packageurl-python +igraph +matplotlib \ No newline at end of file diff --git a/src/fosslight_dependency/_graph_convertor.py b/src/fosslight_dependency/_graph_convertor.py new file mode 100644 index 00000000..1f41aea1 --- /dev/null +++ b/src/fosslight_dependency/_graph_convertor.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2021 LG Electronics Inc. +# SPDX-License-Identifier: Apache-2.0 +from typing import Optional, Tuple +import igraph as ig +import matplotlib.pyplot as plt + + +class GraphConvertor: + def __init__(self, package_list: Optional[list] = None): + self._verticies = {} + self._edges = [] + if package_list: + self.init_list(package_list) + + def init_list(self, package_list: list): + """ + Initialize package_list to self._verticies and self._edges + + Args: + package_list (list): List containing package information + """ + depend_on_package_dict = {} + for idx, package_info in enumerate(package_list): + package_name = package_info[0] + depend_on_packages_str = package_info[-1] + depend_on_packages = list(map((lambda x: x.strip()), depend_on_packages_str.split(","))) + self._verticies[package_name] = idx + depend_on_package_dict[package_name] = depend_on_packages + else: + for package_name, depend_on_packages in depend_on_package_dict.items(): + if not package_name: + pass + else: + package_idx = self._verticies[package_name] + for depend_on_package in depend_on_packages: + if not depend_on_package: + pass + else: + depend_on_package_idx = self._verticies[depend_on_package] + self._edges.append((package_idx, depend_on_package_idx)) + + def save(self, path: str, size: Tuple[(int, int)]): + g = ig.Graph((len(self._verticies)), (self._edges), directed=True) + + g["title"] = "Dependency Graph" + g.vs["name"] = list(self._verticies.keys()) + + fig, ax = plt.subplots(figsize=(tuple(map((lambda x: x / 100), size)))) + fig.tight_layout() + + ig.plot( + g, + target=ax, + layout="kk", + vertex_size=15, + vertex_color=["#FFD2D2"], + vertex_label=(g.vs["name"]), + vertex_label_dist=1.5, + vertex_label_size=7.0, + edge_width=0.5, + edge_color=["#FFD2D2"], + edge_arrow_size=5, + edge_arrow_width=5, + ) + + fig.savefig(path) diff --git a/src/fosslight_dependency/_help.py b/src/fosslight_dependency/_help.py index dbd73924..10f907f7 100644 --- a/src/fosslight_dependency/_help.py +++ b/src/fosslight_dependency/_help.py @@ -37,6 +37,10 @@ \t\t\t\t\t(If you want to generate the specific file name, add the output path with file name.) -f [ ...]\t Output formats (excel, csv, opossum, yaml, spdx-tag, spdx-yaml, spdx-json, spdx-xml) \t\t\t\t Multiple formats can be specified separated by space. + --graph-path \t Enter the path where the graph image will be saved + \t\t\t\t\t(ex. /your/directory/path/filename.{pdf, jpg, png}) (recommend pdf extension) + --graph-size Enter the size of the graph image (The size unit is pixels) + \t\t\t\t\t--graph-path option is required --direct\t\t\t Print the direct/transitive dependency type in comment. \t\tChoice 'True' or 'False'. (default:True) --notice\t\t\t Print the open source license notice text. diff --git a/src/fosslight_dependency/run_dependency_scanner.py b/src/fosslight_dependency/run_dependency_scanner.py index 604235ce..ea707f19 100755 --- a/src/fosslight_dependency/run_dependency_scanner.py +++ b/src/fosslight_dependency/run_dependency_scanner.py @@ -21,6 +21,7 @@ if platform.system() != 'Windows': from fosslight_util.write_spdx import write_spdx from fosslight_util.cover import CoverItem +from fosslight_dependency._graph_convertor import GraphConvertor # Package Name _PKG_NAME = "fosslight_dependency" @@ -92,7 +93,8 @@ def find_package_manager(input_dir, abs_path_to_exclude=[]): def run_dependency_scanner(package_manager='', input_dir='', output_dir_file='', pip_activate_cmd='', pip_deactivate_cmd='', output_custom_dir='', app_name=const.default_app_name, - github_token='', formats=[], direct=True, path_to_exclude=[]): + github_token='', formats=[], direct=True, path_to_exclude=[], graph_path='', + graph_size=(600, 600)): global logger ret = True @@ -233,6 +235,15 @@ def run_dependency_scanner(package_manager='', input_dir='', output_dir_file='', if cover_comment: cover.comment += f', {cover_comment}' + if ret and graph_path: + graph_path = os.path.abspath(graph_path) + try: + converter = GraphConvertor(sheet_list[_sheet_name]) + converter.save(graph_path, graph_size) + logger.info(f"Output graph image file: {graph_path}") + except Exception as e: + logger.error(f'Fail to make graph image: {e}') + combined_paths_and_files = [os.path.join(output_path, file) for file in output_files] results = [] for i, output_extension in enumerate(output_extensions): @@ -276,6 +287,8 @@ def main(): app_name = const.default_app_name github_token = '' format = '' + graph_path = '' + graph_size = (600, 600) direct = True parser = argparse.ArgumentParser(add_help=False) @@ -291,6 +304,8 @@ def main(): parser.add_argument('-n', '--appname', nargs=1, type=str, required=False) parser.add_argument('-t', '--token', nargs=1, type=str, required=False) parser.add_argument('-f', '--format', nargs="*", type=str, required=False) + parser.add_argument('--graph-path', nargs=1, type=str, required=False) + parser.add_argument('--graph-size', nargs=2, type=int, metavar=("WIDTH", "HEIGHT"), required=False) parser.add_argument('--direct', choices=('true', 'false'), default='True', required=False) parser.add_argument('--notice', action='store_true', required=False) @@ -324,6 +339,10 @@ def main(): github_token = ''.join(args.token) if args.format: # -f option format = list(args.format) + if args.graph_path: + graph_path = ''.join(args.graph_path) + if args.graph_size: + graph_size = args.graph_size if args.direct: # --direct option if args.direct == 'true': direct = True @@ -343,7 +362,8 @@ def main(): sys.exit(0) run_dependency_scanner(package_manager, input_dir, output_dir, pip_activate_cmd, pip_deactivate_cmd, - output_custom_dir, app_name, github_token, format, direct, path_to_exclude) + output_custom_dir, app_name, github_token, format, direct, path_to_exclude, + graph_path, graph_size) if __name__ == '__main__':