Skip to content

Commit ba9326f

Browse files
committed
Evolve Rust pipeline (#1767)
* Use Docker for building instead of Cargo * Accept only one PURL as input * Use the .rlib found in .d files for D2D mapping * Remove unnecessary code Signed-off-by: Chin Yeung Li <tli@nexb.com>
1 parent d7c58e7 commit ba9326f

4 files changed

Lines changed: 177 additions & 207 deletions

File tree

scanpipe/pipelines/scan_rust_package.py

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
from scanpipe.pipes import rust
3131
from scanpipe.pipes import utils
3232

33-
# from scanpipe.pipes.maven import update_package_license_from_resource_if_missing
33+
from scanpipe.pipes.rust import check_input_and_return_purl, fetch_inputs
34+
35+
import shutil
3436

3537

3638
class ScanRustPackage(ScanSinglePackage, DeployToDevelop, ScanCodebase):
@@ -45,17 +47,19 @@ class ScanRustPackage(ScanSinglePackage, DeployToDevelop, ScanCodebase):
4547
the license declared in Cargo.toml.
4648
4749
Compare the crate’s source code against all other crates (MatchCode),
48-
excluding itself, to detect any borrowed code from thirdparty crates.
50+
excluding itself, to detect any borrowed code from third-party crates.
4951
"""
5052

53+
download_inputs = False
54+
5155
@classmethod
5256
def steps(cls):
5357
return (
54-
cls.get_input,
55-
cls.get_package_input,
56-
cls.collect_input_information,
57-
cls.extract_inputs_to_codebase_directory,
58-
cls.extract_archives,
58+
cls.check_input_and_return_purl,
59+
cls.fetch_inputs,
60+
cls.collect_input_info,
61+
cls.extract_input_to_codebase_directory,
62+
cls.check_docker_command,
5963
cls.build_crates,
6064
cls.run_scan,
6165
cls.load_inventory_from_toolkit_scan,
@@ -66,30 +70,40 @@ def steps(cls):
6670
cls.make_summary_from_scan_results,
6771
)
6872

69-
def get_input(self):
70-
"""Get the input file for the Rust package scan pipeline."""
71-
from_files = list(self.project.inputs("from*"))
72-
from_files.extend([input.path for input in self.project.inputsources.all()])
73-
self.from_files = from_files
74-
self.to_files = list()
73+
def check_input_and_return_purl(self):
74+
"""Validate the input is a PURL string and return the PURL object."""
75+
self.purl = check_input_and_return_purl(self.project)
76+
77+
def fetch_inputs(self):
78+
"""Fetch the source of the given PURL."""
79+
self.from_files = fetch_inputs(self.purl)
80+
81+
def collect_input_info(self):
82+
"""Collect information about the input."""
83+
self.input_path = self.from_files
84+
self.collect_input_information()
85+
86+
def check_docker_command(self):
87+
self.have_docker = False
88+
if shutil.which("docker"):
89+
self.have_docker = True
7590

7691
def build_crates(self):
7792
"""
78-
Build the Rust crate using Cargo and put the built files under the
93+
Build the Rust crate using Docker and put the built files under the
7994
"to" directory.
8095
"""
81-
# Find the Cargo.toml file in the codebase directory
82-
codebase_dir = Path(self.project.codebase_path)
83-
cargo_toml_path = None
84-
for path in codebase_dir.rglob("Cargo.toml"):
85-
cargo_toml_path = path
86-
break
87-
if cargo_toml_path:
88-
rust.build_crates(cargo_toml_path, self.project.codebase_path / "to/")
96+
self.d2d_enable = False
97+
if self.have_docker:
98+
if rust.build_crates(self.project.codebase_path):
99+
self.d2d_enable = True
100+
else:
101+
print("Docker command not found. Skipping crate build.")
89102

90103
def add_from_to_tag(self):
91104
"""Update 'from' or 'to' tag to resources based on their path."""
92-
d2d.update_from_to_tag(self.project)
105+
if self.d2d_enable:
106+
d2d.update_from_to_tag(self.project)
93107

94108
def validate_package_license_integrity(self):
95109
"""
@@ -100,8 +114,10 @@ def validate_package_license_integrity(self):
100114

101115
def identify_built_sources(self):
102116
"""Identify the built sources from the '.d' file in the "to" directory."""
103-
d2d.map_rust_paths(self.project)
117+
if self.d2d_enable:
118+
d2d.map_rust_paths(self.project)
104119

105120
def flag_mapped_status(self):
106121
"""Flag the from codebase resources that were mapped."""
107-
flag.flag_mapped_resources(self.project)
122+
if self.d2d_enable:
123+
flag.flag_mapped_resources(self.project)

scanpipe/pipes/d2d.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1948,10 +1948,10 @@ def map_go_paths(project, logger=None):
19481948
def get_rust_file_paths(location):
19491949
"""Retrieve Rust file paths."""
19501950
file_paths = {}
1951-
rust_file_paths = parse_d_file(location) or []
1951+
rust_lib_path, rust_file_paths = parse_d_file(location) or []
19521952
if rust_file_paths:
19531953
file_paths["rust_file_paths"] = rust_file_paths
1954-
return file_paths
1954+
return rust_lib_path, file_paths
19551955

19561956

19571957
def parse_d_file(path):
@@ -1963,31 +1963,57 @@ def parse_d_file(path):
19631963
if ":" not in cleaned_context:
19641964
return []
19651965

1966-
_, dep_paths = cleaned_context.split(":", 1)
1966+
rust_lib, dep_paths = cleaned_context.split(":", 1)
1967+
rust_lib_path = rust_lib.strip()
19671968

19681969
file_paths = []
19691970
for file_path in dep_paths.split():
19701971
file_path = file_path.strip()
19711972
if file_path:
19721973
file_paths.append(file_path)
19731974

1974-
return file_paths
1975+
return rust_lib_path, file_paths
19751976

19761977

19771978
def map_rust_paths(project, logger=None):
19781979
"""Map the path listed in the .d file to the source in ``project``."""
19791980
from_resources = project.codebaseresources.files().from_codebase()
1980-
to_resources = (
1981+
# Fetch the .d files to extract data from
1982+
data_resources = (
19811983
project.codebaseresources.files()
19821984
.to_codebase()
19831985
.exclude(path__contains="/deps/")
19841986
.exclude(path__contains="/build/")
19851987
.filter(path__endswith=".d")
19861988
)
1987-
for resource in to_resources:
1989+
target_rlib_ids = []
1990+
for resource in data_resources:
19881991
try:
1989-
paths = get_rust_file_paths(resource.location_path)
1990-
resource.update_extra_data(paths)
1992+
rlib_path, paths = get_rust_file_paths(resource.location_path)
1993+
absolute_rlib_path = Path(rlib_path)
1994+
rlib_resource = None
1995+
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"))
1999+
else:
2000+
clean_rlib_path = str(absolute_rlib_path.relative_to(project.codebase_path))
2001+
2002+
# We can now safely do an exact path match
2003+
rlib_resource = (
2004+
project.codebaseresources.files()
2005+
.to_codebase()
2006+
.filter(path=clean_rlib_path)
2007+
.first()
2008+
)
2009+
except ValueError:
2010+
pass
2011+
2012+
if rlib_resource:
2013+
rlib_resource.update_extra_data(paths)
2014+
target_rlib_ids.append(rlib_resource.id)
2015+
elif logger:
2016+
logger(f"Warning: Could not find rlib file {absolute_rlib_path.name} in database.")
19912017
except Exception as exception:
19922018
project.add_warning(
19932019
exception=exception,
@@ -1997,6 +2023,8 @@ def map_rust_paths(project, logger=None):
19972023
details={"path": resource.path},
19982024
)
19992025

2026+
to_resources = project.codebaseresources.filter(id__in=target_rlib_ids)
2027+
20002028
if logger:
20012029
logger(
20022030
f"Mapping {to_resources.count():,d} to/ resources using paths "

scanpipe/pipes/rust.py

Lines changed: 99 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,117 @@
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
2324
import subprocess
25+
import logging
26+
import requests
2427

28+
from packageurl import PackageURL
29+
from pathlib import Path
30+
from scanpipe.pipes import fetch
2531
from scanpipe.pipes import run_command_safely
2632

2733

28-
def build_crates(cargo_toml_path, build_dir):
34+
logger = logging.getLogger(__name__)
35+
36+
37+
def build_crates(codebase_dir):
2938
"""
30-
Build the Rust crate using Cargo to ensure that the source code compiles correctly.
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.
3144
32-
This step is crucial for validating the integrity of the source code and ensuring
33-
that it can be successfully built. It also helps to identify any discrepancies
34-
between the source code and the compiled binary, which can be further analyzed
35-
in subsequent steps of the pipeline.
45+
Return True if build successfully, False otherwise.
3646
"""
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+
59+
cargo_toml_path = Path(cargo_toml_path)
60+
build_dir = Path(to_dir)
61+
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()
65+
66+
container_cargo_toml = f"/codebase/{rel_cargo_toml}"
67+
container_build_dir = f"/codebase/{rel_build_dir}"
68+
3769
cmd = [
38-
"cargo",
39-
"build",
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
75+
"rust:latest",
76+
"cargo", "build",
77+
"--release",
4078
"--locked",
41-
"--manifest-path",
42-
str(cargo_toml_path),
43-
"--target-dir",
44-
str(build_dir),
79+
"--manifest-path", container_cargo_toml,
80+
"--target-dir", container_build_dir,
4581
]
4682

4783
try:
4884
run_command_safely(cmd)
4985
except subprocess.SubprocessError as error:
50-
raise RuntimeError(f"Failed to build the Rust crate: {error}")
86+
logger.warning(f"Failed to build the Rust crate in Docker: {error}")
87+
return False
88+
89+
from_dir = codebase_dir / "from"
90+
from_dir.mkdir(exist_ok=True)
91+
for item in codebase_dir.iterdir():
92+
if item != to_dir and item != from_dir:
93+
shutil.move(str(item), str(from_dir / item.name))
94+
return True
95+
96+
97+
def check_input_and_return_purl(project):
98+
"""Validate the input and return a cargo PURL."""
99+
input_sources = project.inputsources.all()
100+
if len(input_sources) != 1:
101+
error_msg = "Only 1 cargo purl is accepted."
102+
raise ValueError(error_msg)
103+
# Strip the qualifiers as this is not needed.
104+
project_input = str(input_sources[0]).split("?")[0]
105+
input_purl = PackageURL.from_string(project_input)
106+
107+
if input_purl.type != "cargo":
108+
error_msg = "Only cargo purl is supported."
109+
raise ValueError(error_msg)
110+
if not input_purl.version:
111+
error_msg = "Version is required."
112+
raise ValueError(error_msg)
113+
114+
return input_purl
115+
116+
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)
122+
123+
if not purl_src_path:
124+
err_msg = f"No source could be resolved for {purl}."
125+
raise ValueError(err_msg)
126+
127+
return purl_src_path
128+
129+
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

0 commit comments

Comments
 (0)