Skip to content

Commit e10bd2a

Browse files
committed
Implement the rust pipeline #1767
* Add comparison logic * Add tests * Update extra_data fields * Better code organization * etc.. Signed-off-by: Chin Yeung Li <tli@nexb.com>
1 parent ba9326f commit e10bd2a

8 files changed

Lines changed: 976 additions & 287 deletions

File tree

scanpipe/pipelines/scan_rust_package.py

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,20 @@
2020
# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
2121
# Visit https://github.com/aboutcode-org/scancode.io for support and download.
2222

23+
import shutil
24+
import tempfile
2325
from pathlib import Path
2426

2527
from scanpipe.pipelines.deploy_to_develop import DeployToDevelop
2628
from scanpipe.pipelines.scan_codebase import ScanCodebase
2729
from scanpipe.pipelines.scan_single_package import ScanSinglePackage
2830
from scanpipe.pipes import d2d
2931
from scanpipe.pipes import flag
30-
from scanpipe.pipes import rust
3132
from scanpipe.pipes import utils
32-
33-
from scanpipe.pipes.rust import check_input_and_return_purl, fetch_inputs
34-
35-
import shutil
33+
from scanpipe.pipes.rust import build_crates
34+
from scanpipe.pipes.rust import check_input_and_return_purl
35+
from scanpipe.pipes.rust import get_cargo_toml_path
36+
from scanpipe.pipes.rust import get_repository_value_from_cargo_toml
3637

3738

3839
class ScanRustPackage(ScanSinglePackage, DeployToDevelop, ScanCodebase):
@@ -60,13 +61,18 @@ def steps(cls):
6061
cls.collect_input_info,
6162
cls.extract_input_to_codebase_directory,
6263
cls.check_docker_command,
64+
cls.get_cargo_toml,
6365
cls.build_crates,
6466
cls.run_scan,
6567
cls.load_inventory_from_toolkit_scan,
6668
cls.add_from_to_tag,
6769
cls.validate_package_license_integrity,
6870
cls.identify_built_sources,
6971
cls.flag_mapped_status,
72+
cls.get_src_repo_download_url,
73+
cls.download_src_repo,
74+
cls.compare_src_repo_with_from_codebase,
75+
cls.update_comparison_summary,
7076
cls.make_summary_from_scan_results,
7177
)
7278

@@ -76,32 +82,49 @@ def check_input_and_return_purl(self):
7682

7783
def fetch_inputs(self):
7884
"""Fetch the source of the given PURL."""
79-
self.from_files = fetch_inputs(self.purl)
85+
self.from_files = utils.fetch_inputs(self.purl)
8086

8187
def collect_input_info(self):
8288
"""Collect information about the input."""
8389
self.input_path = self.from_files
8490
self.collect_input_information()
8591

8692
def check_docker_command(self):
93+
"""Check if the Docker command is available."""
8794
self.have_docker = False
8895
if shutil.which("docker"):
8996
self.have_docker = True
9097

98+
def get_cargo_toml(self):
99+
"""Get the Cargo.toml path from the codebase directory."""
100+
self.cargo_toml_path = None
101+
self.devel_codebase_dir = None
102+
if self.have_docker:
103+
codebase_dir = Path(self.project.codebase_path)
104+
self.devel_codebase_dir = codebase_dir
105+
self.cargo_toml_path = get_cargo_toml_path(codebase_dir)
106+
91107
def build_crates(self):
92108
"""
93109
Build the Rust crate using Docker and put the built files under the
94110
"to" directory.
95111
"""
96112
self.d2d_enable = False
97-
if self.have_docker:
98-
if rust.build_crates(self.project.codebase_path):
113+
if self.cargo_toml_path:
114+
codebase_dir = self.devel_codebase_dir
115+
cargo_toml_path = self.cargo_toml_path
116+
if build_crates(codebase_dir, cargo_toml_path):
99117
self.d2d_enable = True
118+
updated_path = cargo_toml_path.relative_to(codebase_dir)
119+
self.cargo_toml_path = codebase_dir / "from" / updated_path
120+
self.devel_codebase_dir = codebase_dir / "from"
100121
else:
101122
print("Docker command not found. Skipping crate build.")
123+
else:
124+
print("Cargo.toml is not found.")
102125

103126
def add_from_to_tag(self):
104-
"""Update 'from' or 'to' tag to resources based on their path."""
127+
"""Update 'from' and 'to' tag to resources based on their path."""
105128
if self.d2d_enable:
106129
d2d.update_from_to_tag(self.project)
107130

@@ -121,3 +144,67 @@ def flag_mapped_status(self):
121144
"""Flag the from codebase resources that were mapped."""
122145
if self.d2d_enable:
123146
flag.flag_mapped_resources(self.project)
147+
148+
def get_src_repo_download_url(self):
149+
"""
150+
Get the source repository url from Cargo.toml and determine its
151+
download url.
152+
"""
153+
self.src_download_url = None
154+
repository_url = get_repository_value_from_cargo_toml(self.cargo_toml_path)
155+
if not repository_url:
156+
self.project.add_warning(
157+
description="No source repository URL found in Cargo.toml."
158+
)
159+
else:
160+
self.src_download_url = utils.get_download_url(
161+
repository_url, self.purl.version
162+
)
163+
if not self.src_download_url:
164+
self.project.add_warning(
165+
description=(
166+
"Not able to determine the source repository download URL from "
167+
"Cargo.toml."
168+
)
169+
)
170+
171+
def download_src_repo(self):
172+
"""Download the source from the source repo."""
173+
self.src_repo_path = None
174+
if self.src_download_url:
175+
self.src_repo_path = utils.download_src_repo(self.src_download_url)
176+
if not self.src_repo_path:
177+
self.project.add_warning(
178+
description=(
179+
f"The source repository URL "
180+
f"{self.src_download_url} "
181+
f"could not be downloaded. Skipping the source "
182+
f"crate and source repository comparison."
183+
)
184+
)
185+
186+
def compare_src_repo_with_from_codebase(self):
187+
"""Compare the downloaded source repo with the from codebase."""
188+
self.matched_count = 0
189+
self.mismatches = []
190+
if self.src_repo_path:
191+
with tempfile.TemporaryDirectory() as source_repo_path:
192+
self.extract_archive(self.src_repo_path, source_repo_path)
193+
194+
self.matched_count, self.mismatches = utils.compare_directories(
195+
self.devel_codebase_dir, source_repo_path
196+
)
197+
198+
def update_comparison_summary(self):
199+
"""Update the comparison summary in the discovered package."""
200+
if self.src_repo_path:
201+
utils.update_comparison_summary(
202+
self.project,
203+
self.purl,
204+
self.devel_codebase_dir,
205+
self.src_download_url,
206+
self.purl.name,
207+
self.purl.version,
208+
self.matched_count,
209+
self.mismatches,
210+
)

scanpipe/pipes/d2d.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1989,17 +1989,15 @@ def map_rust_paths(project, logger=None):
19891989
target_rlib_ids = []
19901990
for resource in data_resources:
19911991
try:
1992-
rlib_path, paths = get_rust_file_paths(resource.location_path)
1993-
absolute_rlib_path = Path(rlib_path)
1992+
rlib_path_str, paths = get_rust_file_paths(resource.location_path)
1993+
rlib_path = Path(rlib_path_str)
19941994
rlib_resource = None
19951995
try:
1996-
# Docker paths start with "/codebase". Host paths start with project.codebase_path.
1997-
if str(absolute_rlib_path).startswith("/codebase/"):
1998-
clean_rlib_path = str(absolute_rlib_path.relative_to("/codebase"))
1996+
if rlib_path_str.startswith("/codebase/"):
1997+
clean_rlib_path = str(rlib_path.relative_to("/codebase"))
19991998
else:
2000-
clean_rlib_path = str(absolute_rlib_path.relative_to(project.codebase_path))
1999+
clean_rlib_path = str(rlib_path.relative_to(project.codebase_path))
20012000

2002-
# We can now safely do an exact path match
20032001
rlib_resource = (
20042002
project.codebaseresources.files()
20052003
.to_codebase()
@@ -2013,7 +2011,9 @@ def map_rust_paths(project, logger=None):
20132011
rlib_resource.update_extra_data(paths)
20142012
target_rlib_ids.append(rlib_resource.id)
20152013
elif logger:
2016-
logger(f"Warning: Could not find rlib file {absolute_rlib_path.name} in database.")
2014+
logger(
2015+
f"Warning: Could not find rlib file {rlib_path_str} in database."
2016+
)
20172017
except Exception as exception:
20182018
project.add_warning(
20192019
exception=exception,

scanpipe/pipes/fetch.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ def get_request_session(uri):
6868

6969
# Set a default User-Agent to avoid 403 Forbidden errors on strict
7070
# registries like crates.io that block default python-requests headers.
71-
session.headers.update({
72-
"User-Agent": "ScanCode.io (https://github.com/aboutcode-org/scancode.io)"
73-
})
71+
session.headers.update(
72+
{"User-Agent": "ScanCode.io (https://github.com/aboutcode-org/scancode.io)"}
73+
)
7474

7575
netloc = urlparse(uri).netloc
7676

scanpipe/pipes/rust.py

Lines changed: 50 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -20,64 +20,58 @@
2020
# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
2121
# Visit https://github.com/aboutcode-org/scancode.io for support and download.
2222

23+
import logging
2324
import shutil
2425
import subprocess
25-
import logging
26-
import requests
26+
from pathlib import Path
2727

28+
import tomllib
2829
from packageurl import PackageURL
29-
from pathlib import Path
30-
from scanpipe.pipes import fetch
31-
from scanpipe.pipes import run_command_safely
3230

31+
from scanpipe.pipes import run_command_safely
3332

3433
logger = logging.getLogger(__name__)
3534

3635

37-
def build_crates(codebase_dir):
36+
def build_crates(codebase_dir, cargo_toml_path):
3837
"""
39-
Build the Rust crate from sources in an isolated Docker container.
40-
41-
Uses the official rust image to safely sandbox the build process and
42-
injects RUSTFLAGS to force DWARF debug symbol generation (-C debuginfo=2)
43-
required for binary-to-source mapping.
44-
45-
Return True if build successfully, False otherwise.
38+
Build the Rust crate from source in an isolated Docker container.
39+
Use the official Rust image for the build process.
40+
Return True if the build succeeds, False otherwise.
4641
"""
47-
48-
# Find the Cargo.toml file in the codebase directory
49-
codebase_dir = Path(codebase_dir)
50-
cargo_toml_path = None
51-
for path in codebase_dir.rglob("Cargo.toml"):
52-
cargo_toml_path = path
53-
break
54-
if cargo_toml_path:
55-
to_dir = codebase_dir / "to"
56-
else:
57-
return False
58-
42+
to_dir = codebase_dir / "to"
5943
cargo_toml_path = Path(cargo_toml_path)
6044
build_dir = Path(to_dir)
6145

62-
# Calculate paths relative to the container's mounted /codebase directory
63-
rel_cargo_toml = cargo_toml_path.relative_to(codebase_dir).as_posix()
64-
rel_build_dir = build_dir.relative_to(codebase_dir).as_posix()
46+
# Get the relative paths
47+
relative_cargo_toml = cargo_toml_path.relative_to(codebase_dir).as_posix()
48+
relative_build_dir = build_dir.relative_to(codebase_dir).as_posix()
6549

66-
container_cargo_toml = f"/codebase/{rel_cargo_toml}"
67-
container_build_dir = f"/codebase/{rel_build_dir}"
50+
container_cargo_toml = f"/codebase/{relative_cargo_toml}"
51+
container_build_dir = f"/codebase/{relative_build_dir}"
6852

53+
# Since we will use the .d file for deployment and development file
54+
# mapping, we will not require building with DWARF debug symbols. If we
55+
# later decide to include DWARF, we can add the following to the
56+
# command:
57+
# "--env", "RUSTFLAGS=-C debuginfo=2",
6958
cmd = [
70-
"docker", "run",
71-
"--rm", # Automatically remove the container when it exits
72-
"--volume", f"{codebase_dir}:/codebase",
73-
"--workdir", "/codebase",
74-
"--env", "RUSTFLAGS=-C debuginfo=2", # Force DWARF generation in release mode
59+
"docker",
60+
"run",
61+
"--rm",
62+
"--volume",
63+
f"{codebase_dir}:/codebase",
64+
"--workdir",
65+
"/codebase",
7566
"rust:latest",
76-
"cargo", "build",
67+
"cargo",
68+
"build",
7769
"--release",
7870
"--locked",
79-
"--manifest-path", container_cargo_toml,
80-
"--target-dir", container_build_dir,
71+
"--manifest-path",
72+
container_cargo_toml,
73+
"--target-dir",
74+
container_build_dir,
8175
]
8276

8377
try:
@@ -86,6 +80,7 @@ def build_crates(codebase_dir):
8680
logger.warning(f"Failed to build the Rust crate in Docker: {error}")
8781
return False
8882

83+
# Move the development code under the /codebase/from/
8984
from_dir = codebase_dir / "from"
9085
from_dir.mkdir(exist_ok=True)
9186
for item in codebase_dir.iterdir():
@@ -100,7 +95,7 @@ def check_input_and_return_purl(project):
10095
if len(input_sources) != 1:
10196
error_msg = "Only 1 cargo purl is accepted."
10297
raise ValueError(error_msg)
103-
# Strip the qualifiers as this is not needed.
98+
# Strip the qualifiers if present as this is not needed
10499
project_input = str(input_sources[0]).split("?")[0]
105100
input_purl = PackageURL.from_string(project_input)
106101

@@ -114,23 +109,23 @@ def check_input_and_return_purl(project):
114109
return input_purl
115110

116111

117-
def fetch_inputs(purl):
118-
"""Fetch the source for the given input purl"""
119-
purl_str = PackageURL.to_string(purl)
120-
121-
purl_src_path = fetch_path(purl_str)
112+
def get_repository_value_from_cargo_toml(cargo_toml_path):
113+
"""Get the repository value from Cargo.toml."""
114+
path = Path(cargo_toml_path)
115+
if not path.exists():
116+
raise FileNotFoundError(f"{cargo_toml_path} not found")
122117

123-
if not purl_src_path:
124-
err_msg = f"No source could be resolved for {purl}."
125-
raise ValueError(err_msg)
118+
with path.open("rb") as f:
119+
data = tomllib.load(f)
126120

127-
return purl_src_path
121+
return data.get("package", {}).get("repository", "")
128122

129123

130-
def fetch_path(purl):
131-
"""Fetch the purl and return the location of the fetched tarball"""
132-
try:
133-
return fetch.fetch_url(url=purl).path
134-
except (ValueError, requests.RequestException) as e:
135-
logger.warning("Failed to fetch package: %s - %s", purl, e)
136-
return None
124+
def get_cargo_toml_path(codebase_dir):
125+
"""Get the Cargo.toml path from the codebase directory."""
126+
cargo_toml_path = None
127+
# There is only one "Cargo.toml" per published package
128+
for path in codebase_dir.rglob("Cargo.toml"):
129+
cargo_toml_path = path
130+
break
131+
return cargo_toml_path

0 commit comments

Comments
 (0)