Skip to content

Commit b925468

Browse files
authored
Support cargo package manager (#230)
Signed-off-by: jiyeong.seok <[email protected]>
1 parent e297a36 commit b925468

File tree

11 files changed

+259
-12
lines changed

11 files changed

+259
-12
lines changed

.reuse/dep5

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,8 @@ License: Apache-2.0
116116

117117
Files: tests/test_exclude/*
118118
Copyright: 2024 LG Electronics
119+
License: Apache-2.0
120+
121+
Files: tests/test_cargo/*
122+
Copyright: 2024 LG Electronics
119123
License: Apache-2.0

README.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,22 @@
22
Copyright (c) 2021 LG Electronics
33
SPDX-License-Identifier: Apache-2.0
44
-->
5-
# FOSSLight Dependency Scanner
65

7-
<img src="https://img.shields.io/pypi/l/fosslight_dependency" alt="License" /> <a href="https://pypi.org/project/fosslight-dependency/"><img src="https://img.shields.io/pypi/v/fosslight_dependency" alt="Current python package version." /></a> <img src="https://img.shields.io/pypi/pyversions/fosslight_dependency" /> [![REUSE status](https://api.reuse.software/badge/github.com/fosslight/fosslight_dependency_scanner)](https://api.reuse.software/info/github.com/fosslight/fosslight_dependency_scanner)
6+
# FOSSLight Dependency Scanner
87

8+
`<img src="https://img.shields.io/pypi/l/fosslight_dependency" alt="License" />` `<a href="https://pypi.org/project/fosslight-dependency/"><img src="https://img.shields.io/pypi/v/fosslight_dependency" alt="Current python package version." />``</a>` `<img src="https://img.shields.io/pypi/pyversions/fosslight_dependency" />` [![REUSE status](https://api.reuse.software/badge/github.com/fosslight/fosslight_dependency_scanner)](https://api.reuse.software/info/github.com/fosslight/fosslight_dependency_scanner)
99

1010
## 💡 Introduction
1111

1212
This is the tool that supports the analysis of dependencies for multiple package managers. It detects the manifest file of package managers automatically and analyzes the dependencies with using open source tools. Then, it generates the report file that contains OSS information of dependencies.
1313

14-
1514
## 📖 User Guide
1615

17-
We describe the user guide in the [**FOSSLight Guide page**](https://fosslight.org/fosslight-guide-en/scanner/3_dependency.html).
16+
We describe the user guide in the [**FOSSLight Guide page**](https://fosslight.org/fosslight-guide-en/scanner/3_dependency.html).
1817
In this user guide, you can see how to install the FOSSLight Dependency Scanner and how to set up the prerequisite step and run it according to the package manager of your project. Also, you can check the results of the FOSSLight Dependency Scanner.
1918

20-
2119
## 👀 Package Support Level
20+
2221
<table>
2322
<thead>
2423
<tr>
@@ -133,17 +132,23 @@ In this user guide, you can see how to install the FOSSLight Dependency Scanner
133132
<td>O</td>
134133
<td>X</td>
135134
</tr>
135+
<tr>
136+
<td>Rust</td>
137+
<td>Cargo</td>
138+
<td>Cargo.toml</td>
139+
<td>O</td>
140+
<td>O</td>
141+
<td>O</td>
142+
</tr>
136143
</tbody>
137144
</table>
138145

139-
140146
## 👏 Contributing Guide
141147

142-
We always welcome your contributions.
148+
We always welcome your contributions.
143149
Please see the [CONTRIBUTING guide](https://github.com/fosslight/fosslight_dependency_scanner/blob/main/CONTRIBUTING.md) for how to contribute.
144150

145-
146151
## 📄 License
147152

148-
Copyright (c) 2020 LG Electronics, Inc.
153+
Copyright (c) 2020 LG Electronics, Inc.
149154
FOSSLight Dependency Scanner is licensed under Apache-2.0, as found in the [LICENSE](https://github.com/fosslight/fosslight_dependency_scanner/blob/main/LICENSE) file.

src/fosslight_dependency/_analyze_dependency.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from fosslight_dependency.package_manager.Nuget import Nuget
2020
from fosslight_dependency.package_manager.Helm import Helm
2121
from fosslight_dependency.package_manager.Unity import Unity
22+
from fosslight_dependency.package_manager.Cargo import Cargo
2223
import fosslight_util.constant as constant
2324

2425
logger = logging.getLogger(constant.LOGGER_NAME)
@@ -57,6 +58,8 @@ def analyze_dependency(package_manager_name, input_dir, output_dir, pip_activate
5758
package_manager = Helm(input_dir, output_dir)
5859
elif package_manager_name == const.UNITY:
5960
package_manager = Unity(input_dir, output_dir)
61+
elif package_manager_name == const.CARGO:
62+
package_manager = Cargo(input_dir, output_dir)
6063
else:
6164
logger.error(f"Not supported package manager name: {package_manager_name}")
6265
ret = False

src/fosslight_dependency/_help.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@
2424
Nuget (.NET)
2525
Helm (Kubernetes)
2626
Unity (Unity)
27+
Cargo (Rust)
2728
2829
Options:
2930
Optional
3031
-h\t\t\t\t Print help message.
3132
-v\t\t\t\t Print the version of the script.
3233
-m <package_manager>\t Enter the package manager.
33-
\t(npm, maven, gradle, pypi, pub, cocoapods, android, swift, carthage, go, nuget, helm)
34+
\t(npm, maven, gradle, pypi, pub, cocoapods, android, swift, carthage,
35+
\t go, nuget, helm, unity, cargo)
3436
-p <input_path>\t\t Enter the path where the script will be run.
3537
-e <exclude_path>\t\t Enter the path where the analysis will not be performed.
3638
-o <output_path>\t\t Output path

src/fosslight_dependency/_package_manager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,8 @@ def get_url_to_purl(url, pkg_manager, oss_name='', oss_version=''):
286286
elif pkg_manager == 'carthage':
287287
if oss_version:
288288
purl = f'{purl}@{oss_version}'
289+
elif pkg_manager == 'cargo':
290+
purl = f'{purl_prefix}/{oss_name}@{oss_version}'
289291
except Exception:
290292
logger.debug('Fail to get purl. So use the link purl({purl}).')
291293
return purl

src/fosslight_dependency/constant.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
NUGET = 'nuget'
2424
HELM = 'helm'
2525
UNITY = 'unity'
26+
CARGO = 'cargo'
2627

2728
# Supported package name and manifest file
2829
SUPPORT_PACKAE = {
@@ -38,7 +39,8 @@
3839
GO: 'go.mod',
3940
NUGET: ['packages.config', os.path.join('obj', 'project.assets.json')],
4041
HELM: 'Chart.yaml',
41-
UNITY: os.path.join('Library', 'PackageManager', 'ProjectCache')
42+
UNITY: os.path.join('Library', 'PackageManager', 'ProjectCache'),
43+
CARGO: 'Cargo.toml'
4244
}
4345

4446
# default android app name
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
# Copyright (c) 2021 LG Electronics Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
import os
7+
import logging
8+
import json
9+
import re
10+
import subprocess
11+
import fosslight_util.constant as constant
12+
import fosslight_dependency.constant as const
13+
from fosslight_dependency._package_manager import PackageManager
14+
from fosslight_dependency._package_manager import get_url_to_purl
15+
from fosslight_dependency.dependency_item import DependencyItem, change_dependson_to_purl
16+
from fosslight_util.oss_item import OssItem
17+
logger = logging.getLogger(constant.LOGGER_NAME)
18+
19+
20+
class Cargo(PackageManager):
21+
package_manager_name = const.CARGO
22+
23+
dn_url = 'https://crates.io/crates/'
24+
input_file_name = 'tmp_cargo_fosslight_output.json'
25+
tmp_input_file_flag = False
26+
cur_path = ''
27+
cargo_lock_f = 'Cargo.lock'
28+
29+
def __init__(self, input_dir, output_dir):
30+
super().__init__(self.package_manager_name, self.dn_url, input_dir, output_dir)
31+
self.append_input_package_list_file(self.input_file_name)
32+
33+
def __del__(self):
34+
if self.tmp_input_file_flag:
35+
os.remove(self.input_file_name)
36+
37+
def run_plugin(self):
38+
if os.path.exists(self.input_file_name):
39+
logger.info(f"Found {self.input_file_name}, skip the flutter cmd to analyze dependency.")
40+
return True
41+
42+
if not os.path.exists(const.SUPPORT_PACKAE.get(self.package_manager_name)):
43+
logger.error(f"Cannot find the file({const.SUPPORT_PACKAE.get(self.package_manager_name)})")
44+
return False
45+
46+
if os.path.exists(self.cargo_lock_f):
47+
cmd = f'cargo metadata --locked --format-version 1 > {self.input_file_name}'
48+
else:
49+
cmd = f'cargo metadata --format-version 1 > {self.input_file_name}'
50+
ret = subprocess.call(cmd, shell=True)
51+
if ret != 0:
52+
logger.error(f"Failed to run: {cmd}")
53+
os.chdir(self.cur_path)
54+
return False
55+
self.tmp_input_file_flag = True
56+
return True
57+
58+
def parse_oss_information(self, f_name):
59+
json_data = ''
60+
61+
with open(f_name, 'r', encoding='utf8') as cargo_file:
62+
json_f = json.load(cargo_file)
63+
try:
64+
purl_dict = {}
65+
workspace_members_key = 'workspace_members'
66+
resolve_key = 'resolve'
67+
root_key = 'root'
68+
nodes_key = 'nodes'
69+
workspace_members = []
70+
root = ''
71+
resolve_node = []
72+
73+
if workspace_members_key in json_f:
74+
workspace_members = json_f[workspace_members_key]
75+
76+
if resolve_key in json_f:
77+
if root_key in json_f[resolve_key]:
78+
root = json_f[resolve_key][root_key]
79+
if nodes_key in json_f[resolve_key]:
80+
resolve_node = json_f[resolve_key][nodes_key]
81+
if root and resolve_node:
82+
self.direct_dep_list.extend(get_matched_dependencies(root, resolve_node))
83+
else:
84+
self.direct_dep = False
85+
logger.info('Cannot find dependencies relationship (no resolve nodes.)')
86+
87+
for json_data in json_f['packages']:
88+
dep_item = DependencyItem()
89+
oss_item = OssItem()
90+
pkg_id = json_data['id']
91+
oss_origin_name = json_data['name']
92+
93+
oss_item.name = f"{self.package_manager_name}:{oss_origin_name}"
94+
oss_item.version = json_data['version']
95+
oss_item.homepage = f"{self.dn_url}{oss_origin_name}"
96+
oss_item.download_location = json_data['repository']
97+
if oss_item.download_location is None:
98+
oss_item.download_location = oss_item.homepage
99+
dep_item.purl = get_url_to_purl(oss_item.homepage, self.package_manager_name, oss_origin_name, oss_item.version)
100+
purl_dict[f'{oss_origin_name}({oss_item.version})'] = dep_item.purl
101+
if json_data['license'] is not None:
102+
oss_item.license = json_data['license']
103+
104+
if self.direct_dep:
105+
if pkg_id == root:
106+
oss_item.comment = 'root package'
107+
if pkg_id in workspace_members:
108+
oss_item.comment = 'local package'
109+
if len(self.direct_dep_list) > 0:
110+
if pkg_id != root:
111+
if f'{oss_origin_name}({oss_item.version})' in self.direct_dep_list:
112+
oss_item.comment = 'direct'
113+
else:
114+
oss_item.comment = 'transitive'
115+
dep_item.depends_on_raw.extend(get_matched_dependencies(pkg_id, resolve_node))
116+
117+
dep_item.oss_items.append(oss_item)
118+
self.dep_items.append(dep_item)
119+
except Exception as e:
120+
logger.error(f"Fail to parse pub oss information: {e}")
121+
if self.direct_dep:
122+
self.dep_items = change_dependson_to_purl(purl_dict, self.dep_items)
123+
124+
return
125+
126+
127+
def get_matched_dependencies(match_id, resolve_node):
128+
dependencies_list = []
129+
for node in resolve_node:
130+
if match_id == node['id']:
131+
for dep_pkg in node['dependencies']:
132+
try:
133+
match = re.findall(r'^.*#(\S*)@(\S*)', dep_pkg)
134+
dependencies_list.append(f'{match[0][0]}({match[0][1]})')
135+
except:
136+
try:
137+
match = re.findall(r'^(\S*)\s(\S*)\s', dep_pkg)
138+
dependencies_list.append(f'{match[0][0]}({match[0][1]})')
139+
except:
140+
logger.info(f'cannot find name and version for dependencies: {match_id}')
141+
pass
142+
break
143+
return dependencies_list

tests/pytest/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"tests/result/nuget1",
2222
"tests/result/nuget2",
2323
"tests/result/pub",
24-
"tests/result/pypi"
24+
"tests/result/pypi",
25+
"tests/result/cargo"
2526
]
2627

2728
remove_directories = set_up_directories
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
# Copyright (c) 2024 LG Electronics Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
import os
6+
import pytest
7+
import subprocess
8+
9+
DIST_PATH = os.path.join(os.environ.get("TOX_PATH"), "dist", "cli.exe")
10+
11+
12+
@pytest.mark.parametrize("input_path, output_path", [
13+
("tests/test_cargo", "tests/result/cargo")
14+
])
15+
@pytest.mark.ubuntu
16+
def test_ubuntu(input_path, output_path):
17+
command = f"fosslight_dependency -p {input_path} -o {output_path}"
18+
result = subprocess.run(command, shell=True, capture_output=True, text=True)
19+
assert result.returncode == 0, f"Command failed: {command}\nstdout: {result.stdout}\nstderr: {result.stderr}"
20+
assert any(os.scandir(output_path)), f"Output file does not exist: {output_path}"
21+
22+
23+
@pytest.mark.parametrize("input_path, output_path", [
24+
(os.path.join("tests", "test_cargo"), os.path.join("tests", "result", "cargo"))
25+
])
26+
@pytest.mark.windows
27+
def test_windows(input_path, output_path):
28+
command = f"{DIST_PATH} -p {input_path} -o {output_path}"
29+
result = subprocess.run(command, capture_output=True, text=True)
30+
assert result.returncode == 0, f"Command failed: {command}\nstdout: {result.stdout}\nstderr: {result.stderr}"
31+
assert any(os.scandir(output_path)), f"Output file does not exist: {output_path}"

tests/test_cargo/Cargo.toml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
[package]
2+
name = "rustwide"
3+
version = "0.18.0"
4+
edition = "2018"
5+
build = "build.rs"
6+
7+
documentation = "https://docs.rs/rustwide"
8+
repository = "https://github.com/rust-lang/rustwide"
9+
description = "Execute your code on the Rust ecosystem."
10+
license = "MIT OR Apache-2.0"
11+
readme = "README.md"
12+
13+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
14+
15+
[features]
16+
unstable = []
17+
unstable-toolchain-ci = []
18+
19+
[dependencies]
20+
http = "1.1.0"
21+
anyhow = { version = "1.0.68", features = ["backtrace"]}
22+
futures-util = "0.3.5"
23+
log = "0.4.6"
24+
tokio = { version = "1.0", features = ["process", "time", "io-util", "rt", "rt-multi-thread"] }
25+
tokio-stream = { version = "0.1", features = ["io-util"] }
26+
serde = { version = "1.0", features = ["derive"] }
27+
serde_json = "1.0"
28+
scopeguard = "1.0.0"
29+
lazy_static = "1.0.0"
30+
tempfile = "3.0.0"
31+
attohttpc = "0.28.0"
32+
flate2 = "1"
33+
tar = "0.4.0"
34+
percent-encoding = "2.1.0"
35+
walkdir = "2.2"
36+
toml = "0.8.12"
37+
fs2 = "0.4.3"
38+
remove_dir_all = "0.8.2"
39+
base64 = "0.22.0"
40+
getrandom = { version = "0.2", features = ["std"] }
41+
thiserror = "1.0.20"
42+
git2 = "0.19.0"
43+
44+
[target.'cfg(unix)'.dependencies]
45+
nix = { version = "0.29.0", features = ["signal", "user"]}
46+
47+
[target.'cfg(windows)'.dependencies]
48+
windows-sys = {version = "0.52.0", features = ["Win32_Foundation", "Win32_System_Threading"]}
49+
50+
[dev-dependencies]
51+
env_logger = "0.11.3"
52+
rand = "0.8.5"
53+
tiny_http = "0.12.0"

0 commit comments

Comments
 (0)