Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support cargo package manager #230

Merged
merged 1 commit into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .reuse/dep5
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,8 @@ License: Apache-2.0

Files: tests/test_exclude/*
Copyright: 2024 LG Electronics
License: Apache-2.0

Files: tests/test_cargo/*
Copyright: 2024 LG Electronics
License: Apache-2.0
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,22 @@
Copyright (c) 2021 LG Electronics
SPDX-License-Identifier: Apache-2.0
-->
# FOSSLight Dependency Scanner

<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)
# FOSSLight Dependency Scanner

`<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)

## 💡 Introduction

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.


## 📖 User Guide

We describe the user guide in the [**FOSSLight Guide page**](https://fosslight.org/fosslight-guide-en/scanner/3_dependency.html).
We describe the user guide in the [**FOSSLight Guide page**](https://fosslight.org/fosslight-guide-en/scanner/3_dependency.html).
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.


## 👀 Package Support Level

<table>
<thead>
<tr>
Expand Down Expand Up @@ -133,17 +132,23 @@ In this user guide, you can see how to install the FOSSLight Dependency Scanner
<td>O</td>
<td>X</td>
</tr>
<tr>
<td>Rust</td>
<td>Cargo</td>
<td>Cargo.toml</td>
<td>O</td>
<td>O</td>
<td>O</td>
</tr>
</tbody>
</table>


## 👏 Contributing Guide

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


## 📄 License

Copyright (c) 2020 LG Electronics, Inc.
Copyright (c) 2020 LG Electronics, Inc.
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.
3 changes: 3 additions & 0 deletions src/fosslight_dependency/_analyze_dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from fosslight_dependency.package_manager.Nuget import Nuget
from fosslight_dependency.package_manager.Helm import Helm
from fosslight_dependency.package_manager.Unity import Unity
from fosslight_dependency.package_manager.Cargo import Cargo
import fosslight_util.constant as constant

logger = logging.getLogger(constant.LOGGER_NAME)
Expand Down Expand Up @@ -57,6 +58,8 @@ def analyze_dependency(package_manager_name, input_dir, output_dir, pip_activate
package_manager = Helm(input_dir, output_dir)
elif package_manager_name == const.UNITY:
package_manager = Unity(input_dir, output_dir)
elif package_manager_name == const.CARGO:
package_manager = Cargo(input_dir, output_dir)
else:
logger.error(f"Not supported package manager name: {package_manager_name}")
ret = False
Expand Down
4 changes: 3 additions & 1 deletion src/fosslight_dependency/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@
Nuget (.NET)
Helm (Kubernetes)
Unity (Unity)
Cargo (Rust)

Options:
Optional
-h\t\t\t\t Print help message.
-v\t\t\t\t Print the version of the script.
-m <package_manager>\t Enter the package manager.
\t(npm, maven, gradle, pypi, pub, cocoapods, android, swift, carthage, go, nuget, helm)
\t(npm, maven, gradle, pypi, pub, cocoapods, android, swift, carthage,
\t go, nuget, helm, unity, cargo)
-p <input_path>\t\t Enter the path where the script will be run.
-e <exclude_path>\t\t Enter the path where the analysis will not be performed.
-o <output_path>\t\t Output path
Expand Down
2 changes: 2 additions & 0 deletions src/fosslight_dependency/_package_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,8 @@ def get_url_to_purl(url, pkg_manager, oss_name='', oss_version=''):
elif pkg_manager == 'carthage':
if oss_version:
purl = f'{purl}@{oss_version}'
elif pkg_manager == 'cargo':
purl = f'{purl_prefix}/{oss_name}@{oss_version}'
except Exception:
logger.debug('Fail to get purl. So use the link purl({purl}).')
return purl
Expand Down
4 changes: 3 additions & 1 deletion src/fosslight_dependency/constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
NUGET = 'nuget'
HELM = 'helm'
UNITY = 'unity'
CARGO = 'cargo'

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

# default android app name
Expand Down
143 changes: 143 additions & 0 deletions src/fosslight_dependency/package_manager/Cargo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2021 LG Electronics Inc.
# SPDX-License-Identifier: Apache-2.0

import os
import logging
import json
import re
import subprocess
import fosslight_util.constant as constant
import fosslight_dependency.constant as const
from fosslight_dependency._package_manager import PackageManager
from fosslight_dependency._package_manager import get_url_to_purl
from fosslight_dependency.dependency_item import DependencyItem, change_dependson_to_purl
from fosslight_util.oss_item import OssItem
logger = logging.getLogger(constant.LOGGER_NAME)


class Cargo(PackageManager):
package_manager_name = const.CARGO

dn_url = 'https://crates.io/crates/'
input_file_name = 'tmp_cargo_fosslight_output.json'
tmp_input_file_flag = False
cur_path = ''
cargo_lock_f = 'Cargo.lock'

def __init__(self, input_dir, output_dir):
super().__init__(self.package_manager_name, self.dn_url, input_dir, output_dir)
self.append_input_package_list_file(self.input_file_name)

def __del__(self):
if self.tmp_input_file_flag:
os.remove(self.input_file_name)

def run_plugin(self):
if os.path.exists(self.input_file_name):
logger.info(f"Found {self.input_file_name}, skip the flutter cmd to analyze dependency.")
return True

if not os.path.exists(const.SUPPORT_PACKAE.get(self.package_manager_name)):
logger.error(f"Cannot find the file({const.SUPPORT_PACKAE.get(self.package_manager_name)})")
return False

if os.path.exists(self.cargo_lock_f):
cmd = f'cargo metadata --locked --format-version 1 > {self.input_file_name}'
else:
cmd = f'cargo metadata --format-version 1 > {self.input_file_name}'
ret = subprocess.call(cmd, shell=True)
if ret != 0:
logger.error(f"Failed to run: {cmd}")
os.chdir(self.cur_path)
return False
self.tmp_input_file_flag = True
return True

def parse_oss_information(self, f_name):
json_data = ''

with open(f_name, 'r', encoding='utf8') as cargo_file:
json_f = json.load(cargo_file)
try:
purl_dict = {}
workspace_members_key = 'workspace_members'
resolve_key = 'resolve'
root_key = 'root'
nodes_key = 'nodes'
workspace_members = []
root = ''
resolve_node = []

if workspace_members_key in json_f:
workspace_members = json_f[workspace_members_key]

if resolve_key in json_f:
if root_key in json_f[resolve_key]:
root = json_f[resolve_key][root_key]
if nodes_key in json_f[resolve_key]:
resolve_node = json_f[resolve_key][nodes_key]
if root and resolve_node:
self.direct_dep_list.extend(get_matched_dependencies(root, resolve_node))
else:
self.direct_dep = False
logger.info('Cannot find dependencies relationship (no resolve nodes.)')

for json_data in json_f['packages']:
dep_item = DependencyItem()
oss_item = OssItem()
pkg_id = json_data['id']
oss_origin_name = json_data['name']

oss_item.name = f"{self.package_manager_name}:{oss_origin_name}"
oss_item.version = json_data['version']
oss_item.homepage = f"{self.dn_url}{oss_origin_name}"
oss_item.download_location = json_data['repository']
if oss_item.download_location is None:
oss_item.download_location = oss_item.homepage
dep_item.purl = get_url_to_purl(oss_item.homepage, self.package_manager_name, oss_origin_name, oss_item.version)
purl_dict[f'{oss_origin_name}({oss_item.version})'] = dep_item.purl
if json_data['license'] is not None:
oss_item.license = json_data['license']

if self.direct_dep:
if pkg_id == root:
oss_item.comment = 'root package'
if pkg_id in workspace_members:
oss_item.comment = 'local package'
if len(self.direct_dep_list) > 0:
if pkg_id != root:
if f'{oss_origin_name}({oss_item.version})' in self.direct_dep_list:
oss_item.comment = 'direct'
else:
oss_item.comment = 'transitive'
dep_item.depends_on_raw.extend(get_matched_dependencies(pkg_id, resolve_node))

dep_item.oss_items.append(oss_item)
self.dep_items.append(dep_item)
except Exception as e:
logger.error(f"Fail to parse pub oss information: {e}")
if self.direct_dep:
self.dep_items = change_dependson_to_purl(purl_dict, self.dep_items)

return


def get_matched_dependencies(match_id, resolve_node):
dependencies_list = []
for node in resolve_node:
if match_id == node['id']:
for dep_pkg in node['dependencies']:
try:
match = re.findall(r'^.*#(\S*)@(\S*)', dep_pkg)
dependencies_list.append(f'{match[0][0]}({match[0][1]})')
except:
try:
match = re.findall(r'^(\S*)\s(\S*)\s', dep_pkg)
dependencies_list.append(f'{match[0][0]}({match[0][1]})')
except:
logger.info(f'cannot find name and version for dependencies: {match_id}')
pass
break
return dependencies_list
3 changes: 2 additions & 1 deletion tests/pytest/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"tests/result/nuget1",
"tests/result/nuget2",
"tests/result/pub",
"tests/result/pypi"
"tests/result/pypi",
"tests/result/cargo"
]

remove_directories = set_up_directories
Expand Down
31 changes: 31 additions & 0 deletions tests/pytest/package_manager/test_cargo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 LG Electronics Inc.
# SPDX-License-Identifier: Apache-2.0
import os
import pytest
import subprocess

DIST_PATH = os.path.join(os.environ.get("TOX_PATH"), "dist", "cli.exe")


@pytest.mark.parametrize("input_path, output_path", [
("tests/test_cargo", "tests/result/cargo")
])
@pytest.mark.ubuntu
def test_ubuntu(input_path, output_path):
command = f"fosslight_dependency -p {input_path} -o {output_path}"
result = subprocess.run(command, shell=True, capture_output=True, text=True)
assert result.returncode == 0, f"Command failed: {command}\nstdout: {result.stdout}\nstderr: {result.stderr}"
assert any(os.scandir(output_path)), f"Output file does not exist: {output_path}"


@pytest.mark.parametrize("input_path, output_path", [
(os.path.join("tests", "test_cargo"), os.path.join("tests", "result", "cargo"))
])
@pytest.mark.windows
def test_windows(input_path, output_path):
command = f"{DIST_PATH} -p {input_path} -o {output_path}"
result = subprocess.run(command, capture_output=True, text=True)
assert result.returncode == 0, f"Command failed: {command}\nstdout: {result.stdout}\nstderr: {result.stderr}"
assert any(os.scandir(output_path)), f"Output file does not exist: {output_path}"
53 changes: 53 additions & 0 deletions tests/test_cargo/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
[package]
name = "rustwide"
version = "0.18.0"
edition = "2018"
build = "build.rs"

documentation = "https://docs.rs/rustwide"
repository = "https://github.com/rust-lang/rustwide"
description = "Execute your code on the Rust ecosystem."
license = "MIT OR Apache-2.0"
readme = "README.md"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[features]
unstable = []
unstable-toolchain-ci = []

[dependencies]
http = "1.1.0"
anyhow = { version = "1.0.68", features = ["backtrace"]}
futures-util = "0.3.5"
log = "0.4.6"
tokio = { version = "1.0", features = ["process", "time", "io-util", "rt", "rt-multi-thread"] }
tokio-stream = { version = "0.1", features = ["io-util"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
scopeguard = "1.0.0"
lazy_static = "1.0.0"
tempfile = "3.0.0"
attohttpc = "0.28.0"
flate2 = "1"
tar = "0.4.0"
percent-encoding = "2.1.0"
walkdir = "2.2"
toml = "0.8.12"
fs2 = "0.4.3"
remove_dir_all = "0.8.2"
base64 = "0.22.0"
getrandom = { version = "0.2", features = ["std"] }
thiserror = "1.0.20"
git2 = "0.19.0"

[target.'cfg(unix)'.dependencies]
nix = { version = "0.29.0", features = ["signal", "user"]}

[target.'cfg(windows)'.dependencies]
windows-sys = {version = "0.52.0", features = ["Win32_Foundation", "Win32_System_Threading"]}

[dev-dependencies]
env_logger = "0.11.3"
rand = "0.8.5"
tiny_http = "0.12.0"
1 change: 1 addition & 0 deletions tests/test_cargo/tmp_cargo_fosslight_output.json

Large diffs are not rendered by default.

Loading