Skip to content

Commit 9b5852b

Browse files
authored
Support recursive dependency analysis (#264)
Signed-off-by: 석지영/책임연구원/SW공학(연)Open Source TP <[email protected]>
1 parent c4d4244 commit 9b5852b

File tree

5 files changed

+97
-68
lines changed

5 files changed

+97
-68
lines changed

src/fosslight_dependency/_analyze_dependency.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,11 @@ def analyze_dependency(package_manager_name, input_dir, output_dir, pip_activate
9292
package_manager.parse_oss_information_for_pnpm()
9393
package_dep_item_list.extend(package_manager.dep_items)
9494
if ret:
95-
logger.warning(f"### Complete to analyze: {package_manager_name}")
95+
logger.warning(f"### Complete to analyze: {package_manager_name}({input_dir}: {','.join(manifest_file_name)})")
9696
if package_manager.cover_comment:
9797
cover_comment = package_manager.cover_comment
9898
else:
99-
logger.error(f"### Fail to analyze: {package_manager_name}")
99+
logger.error(f"### Fail to analyze: {package_manager_name}({input_dir}: {','.join(manifest_file_name)})")
100100

101101
del package_manager
102102

src/fosslight_dependency/_help.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
\t\t\t\t\t--graph-path option is required
4747
--direct\t\t\t Print the direct/transitive dependency type in comment.
4848
\t\tChoice 'True' or 'False'. (default:True)
49+
-r\t\t\t\t Recursive mode. Scan all subdirectories for manifest files.
4950
--notice\t\t\t Print the open source license notice text.
5051
5152
Required only for swift, carthage

src/fosslight_dependency/_package_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ def run_gradle_task(self):
108108
cmd_gradle = "./gradlew"
109109
else:
110110
ret_task = False
111+
self.set_direct_dependencies(False)
111112
logger.warning('No gradlew file exists (Skip to find dependencies relationship.).')
112113
if ret_plugin:
113114
logger.warning('Also it cannot run android-dependency-scanning plugin.')

src/fosslight_dependency/package_manager/Npm.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ def __init__(self, input_dir, output_dir):
3131
super().__init__(self.package_manager_name, self.dn_url, input_dir, output_dir)
3232

3333
def __del__(self):
34-
if os.path.isfile(self.input_file_name):
35-
os.remove(self.input_file_name)
34+
if os.path.isfile(os.path.join(self.input_dir, self.input_file_name)):
35+
os.remove(os.path.join(self.input_dir, self.input_file_name))
3636
if self.flag_tmp_node_modules:
37-
shutil.rmtree(node_modules, ignore_errors=True)
38-
if os.path.exists(self.tmp_custom_json):
39-
os.remove(self.tmp_custom_json)
37+
shutil.rmtree(os.path.join(self.input_dir, node_modules), ignore_errors=True)
38+
if os.path.exists(os.path.join(self.input_dir, self.tmp_custom_json)):
39+
os.remove(os.path.join(self.input_dir, self.tmp_custom_json))
4040

4141
def run_plugin(self):
4242
ret = self.start_license_checker()
@@ -122,9 +122,9 @@ def parse_transitive_relationship(self):
122122
if len(rel_json) < 1:
123123
ret = False
124124
else:
125-
self.package_name = f'{rel_json[_name]}({rel_json[_version]})'
125+
self.package_name = f'{rel_json[_name]}({rel_json.get(_version, "")})'
126126
if _dependencies in rel_json:
127-
self.parse_rel_dependencies(rel_json[_name], rel_json[_version], rel_json[_dependencies])
127+
self.parse_rel_dependencies(rel_json[_name], rel_json.get(_version, ""), rel_json[_dependencies])
128128
except Exception as e:
129129
ret = False
130130
err_msg = e

src/fosslight_dependency/run_dependency_scanner.py

Lines changed: 86 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
'OSS Version', 'License', 'Download Location',
3131
'Homepage', 'Copyright Text', 'Exclude',
3232
'Comment', 'Depends On']}
33-
_exclude_dir = ['node_moduels', 'venv']
33+
_exclude_dir = ['node_modules', 'venv', 'Pods', 'Carthage']
3434

3535

3636
def get_terminal_size():
@@ -50,7 +50,7 @@ def paginate_file(file_path):
5050
input("Press Enter to see the next page...")
5151

5252

53-
def find_package_manager(input_dir, abs_path_to_exclude=[], manifest_file_name=[]):
53+
def find_package_manager(input_dir, abs_path_to_exclude=[], manifest_file_name=[], recursive=False):
5454
ret = True
5555
if not manifest_file_name:
5656
for value in const.SUPPORT_PACKAE.values():
@@ -62,9 +62,8 @@ def find_package_manager(input_dir, abs_path_to_exclude=[], manifest_file_name=[
6262
found_manifest_file = []
6363
suggested_files = []
6464
for parent, dirs, files in os.walk(input_dir):
65-
if len(files) < 1:
66-
continue
67-
if os.path.basename(parent) in _exclude_dir:
65+
parent_parts = parent.split(os.sep)
66+
if any(ex_dir in parent_parts for ex_dir in _exclude_dir):
6867
continue
6968
if os.path.abspath(parent) in abs_path_to_exclude:
7069
continue
@@ -75,7 +74,8 @@ def find_package_manager(input_dir, abs_path_to_exclude=[], manifest_file_name=[
7574
for exclude_path in abs_path_to_exclude):
7675
continue
7776
if file in manifest_file_name:
78-
found_manifest_file.append(file)
77+
path_with_filename = os.path.join(parent, file)
78+
found_manifest_file.append(path_with_filename)
7979
if file in const.SUGGESTED_PACKAGE.keys():
8080
suggested_files.append(os.path.join(parent, file))
8181
for dir in dirs:
@@ -84,42 +84,53 @@ def find_package_manager(input_dir, abs_path_to_exclude=[], manifest_file_name=[
8484
if len(manifest_l) > 1:
8585
if manifest_l[0] == dir:
8686
if os.path.exists(os.path.join(parent, manifest_f)):
87-
found_manifest_file.append(manifest_f)
88-
if len(found_manifest_file) > 0:
89-
input_dir = parent
90-
break
91-
found_package_manager = defaultdict(list)
92-
for f_idx in found_manifest_file:
87+
found_manifest_file.append(os.path.join(parent, manifest_f))
88+
if not recursive:
89+
if len(found_manifest_file) > 0:
90+
input_dir = parent
91+
break
92+
93+
found_package_manager = defaultdict(lambda: defaultdict(list))
94+
for f_with_path in found_manifest_file:
95+
f_name = os.path.basename(f_with_path)
96+
dir_path = os.path.dirname(f_with_path)
9397
for key, value in const.SUPPORT_PACKAE.items():
9498
if isinstance(value, list):
95-
for v in value:
96-
if f_idx == v:
97-
if key in found_package_manager.keys():
98-
found_package_manager[key].append(f_idx)
99-
else:
100-
found_package_manager[key] = [f_idx]
99+
if f_name in value:
100+
found_package_manager[key][dir_path].append(f_name)
101101
else:
102-
if value == f_idx:
103-
found_package_manager[key] = [f_idx]
102+
if f_name == value:
103+
found_package_manager[key][dir_path].append(f_name)
104+
found_package_manager = {k: dict(v) for k, v in found_package_manager.items()}
104105

105106
# both npm and pnpm are detected, remove npm.
106-
if 'npm' in found_package_manager and 'pnpm' in found_package_manager:
107+
if 'npm' in found_package_manager.keys() and 'pnpm' in found_package_manager.keys():
107108
del found_package_manager['npm']
108109
if len(found_package_manager) >= 1:
109-
manifest_file_w_path = [os.path.join(input_dir, file) for pkg, files in found_package_manager.items() for file in files]
110-
logger.info(f"Found the manifest file({','.join(manifest_file_w_path)}) automatically.")
111-
logger.warning(f"### Set Package Manager = {', '.join(found_package_manager.keys())}")
110+
log_lines = ["\nDetected Manifest Files automatically"]
111+
log_lines = print_package_info(found_package_manager, log_lines)
112+
logger.info('\n'.join(log_lines))
112113
else:
113114
ret = False
114115
logger.info("Cannot find the manifest file.")
115116

116117
return ret, found_package_manager, input_dir, suggested_files
117118

118119

120+
def print_package_info(success_pm, log_lines):
121+
if success_pm:
122+
for pm, dir_dict in success_pm.items():
123+
log_lines.append(f"- {pm}:")
124+
for path, files in dir_dict.items():
125+
file_list = ', '.join(files)
126+
log_lines.append(f" {path}: {file_list}")
127+
return log_lines
128+
129+
119130
def run_dependency_scanner(package_manager='', input_dir='', output_dir_file='', pip_activate_cmd='',
120131
pip_deactivate_cmd='', output_custom_dir='', app_name=const.default_app_name,
121132
github_token='', formats=[], direct=True, path_to_exclude=[], graph_path='',
122-
graph_size=(600, 600)):
133+
graph_size=(600, 600), recursive=False):
123134
global logger
124135

125136
ret = True
@@ -217,9 +228,8 @@ def run_dependency_scanner(package_manager='', input_dir='', output_dir_file='',
217228
try:
218229
ret, found_package_manager, input_dir, suggested_files = find_package_manager(input_dir,
219230
abs_path_to_exclude,
220-
manifest_file_name)
221-
if ret:
222-
os.chdir(input_dir)
231+
manifest_file_name,
232+
recursive)
223233
except Exception as e:
224234
if autodetect:
225235
logger.error(f'Fail to find package manager: {e}')
@@ -228,7 +238,7 @@ def run_dependency_scanner(package_manager='', input_dir='', output_dir_file='',
228238
if not ret:
229239
if not autodetect:
230240
logger.info('Try to analyze dependency without manifest file. (Manual mode)')
231-
found_package_manager[package_manager] = []
241+
found_package_manager[package_manager] = {}
232242
else:
233243
ret = False
234244
if suggested_files:
@@ -243,40 +253,53 @@ def run_dependency_scanner(package_manager='', input_dir='', output_dir_file='',
243253
else:
244254
scan_item.set_cover_comment("No Package manager detected.")
245255

246-
pass_key = 'PASS'
247-
success_pm = []
248-
fail_pm = []
256+
pass_key = ['PASS']
257+
success_pm = defaultdict(lambda: defaultdict(list))
258+
fail_pm = defaultdict(lambda: defaultdict(list))
249259
cover_comment = ''
250-
for pm, manifest_file_name in found_package_manager.items():
251-
if manifest_file_name == pass_key:
252-
continue
253-
ret, package_dep_item_list, cover_comment = analyze_dependency(pm, input_dir, output_path,
254-
pip_activate_cmd, pip_deactivate_cmd,
255-
output_custom_dir, app_name, github_token,
256-
manifest_file_name, direct)
257-
if ret:
258-
success_pm.append(f"{pm} ({', '.join(manifest_file_name)})")
259-
scan_item.append_file_items(package_dep_item_list)
260-
if pm == const.GRADLE:
261-
if const.ANDROID in found_package_manager.keys():
262-
found_package_manager[const.ANDROID] = pass_key
263-
if f"{const.ANDROID} ({', '.join(manifest_file_name)})" in fail_pm:
264-
fail_pm.remove(f"{const.ANDROID} ({', '.join(manifest_file_name)})")
265-
elif pm == const.ANDROID:
266-
if const.GRADLE in found_package_manager.keys():
267-
found_package_manager[const.GRADLE] = pass_key
268-
if f"{const.GRADLE} ({', '.join(manifest_file_name)})" in fail_pm:
269-
fail_pm.remove(f"{const.GRADLE} ({', '.join(manifest_file_name)})")
270-
else:
271-
fail_pm.append(f"{pm} ({', '.join(manifest_file_name)})")
260+
for pm, manifest_file_name_list in found_package_manager.items():
261+
for manifest_dir, manifest_file_name in manifest_file_name_list.items():
262+
input_dir = manifest_dir
263+
if manifest_file_name == pass_key:
264+
continue
265+
os.chdir(input_dir)
266+
ret, package_dep_item_list, cover_comment = analyze_dependency(pm, input_dir, output_path,
267+
pip_activate_cmd, pip_deactivate_cmd,
268+
output_custom_dir, app_name, github_token,
269+
manifest_file_name, direct)
270+
if ret:
271+
success_pm[pm][input_dir].extend(manifest_file_name)
272+
scan_item.append_file_items(package_dep_item_list)
273+
274+
dup_pm = None
275+
if pm == const.GRADLE and const.ANDROID in found_package_manager:
276+
dup_pm = const.ANDROID
277+
elif pm == const.ANDROID and const.GRADLE in found_package_manager:
278+
dup_pm = const.GRADLE
279+
280+
if dup_pm:
281+
if dup_pm in fail_pm and input_dir in fail_pm[dup_pm]:
282+
fail_pm[dup_pm].pop(input_dir, None)
283+
if not fail_pm[dup_pm]:
284+
fail_pm.pop(dup_pm, None)
285+
else:
286+
found_package_manager[dup_pm][manifest_dir] = pass_key
287+
else:
288+
fail_pm[pm][input_dir].extend(manifest_file_name)
272289

290+
success_pm = {k: dict(v) for k, v in success_pm.items()}
291+
fail_pm = {k: dict(v) for k, v in fail_pm.items()}
273292
if len(found_package_manager.keys()) > 0:
274293
if len(success_pm) > 0:
275-
scan_item.set_cover_comment(f"Analyzed Package manager: {', '.join(success_pm)}")
294+
log_lines = ["Success to analyze:"]
295+
log_lines = print_package_info(success_pm, log_lines)
296+
scan_item.set_cover_comment('\n'.join(log_lines))
276297
if len(fail_pm) > 0:
277-
info_msg = 'Check log file(fosslight_log*.txt) ' \
278-
'and https://fosslight.org/fosslight-guide-en/scanner/3_dependency.html#-prerequisite.'
279-
scan_item.set_cover_comment(f"Analysis failed Package manager: {', '.join(fail_pm)} ({info_msg})")
298+
log_lines = ["Fail to analyze:"]
299+
log_lines = print_package_info(fail_pm, log_lines)
300+
scan_item.set_cover_comment('\n'.join(log_lines))
301+
scan_item.set_cover_comment('Check log file(fosslight_log*.txt) '
302+
'and https://fosslight.org/fosslight-guide-en/scanner/3_dependency.html#-prerequisite.')
280303

281304
if ret and graph_path:
282305
graph_path = os.path.abspath(graph_path)
@@ -335,6 +358,7 @@ def main():
335358
graph_path = ''
336359
graph_size = (600, 600)
337360
direct = True
361+
recursive = False
338362

339363
parser = argparse.ArgumentParser(add_help=False)
340364
parser.add_argument('-h', '--help', action='store_true', required=False)
@@ -353,6 +377,7 @@ def main():
353377
parser.add_argument('--graph-size', nargs=2, type=int, metavar=("WIDTH", "HEIGHT"), required=False)
354378
parser.add_argument('--direct', choices=('true', 'false'), default='True', required=False)
355379
parser.add_argument('--notice', action='store_true', required=False)
380+
parser.add_argument('-r', '--recursive', action='store_true', required=False)
356381

357382
args = parser.parse_args()
358383

@@ -405,10 +430,12 @@ def main():
405430
paginate_file(source_file)
406431
shutil.copyfile(source_file, destination_file)
407432
sys.exit(0)
433+
if args.recursive: # -r option
434+
recursive = True
408435

409436
run_dependency_scanner(package_manager, input_dir, output_dir, pip_activate_cmd, pip_deactivate_cmd,
410437
output_custom_dir, app_name, github_token, format, direct, path_to_exclude,
411-
graph_path, graph_size)
438+
graph_path, graph_size, recursive)
412439

413440

414441
if __name__ == '__main__':

0 commit comments

Comments
 (0)