Skip to content

Commit 3555a26

Browse files
authored
Add allowed prebuilts list (#1846)
commit-id:66715aa1
1 parent 74fec62 commit 3555a26

File tree

9 files changed

+242
-6
lines changed

9 files changed

+242
-6
lines changed

scarb-metadata/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
All notable changes to this project will be documented in this file.
44

55
## Unreleased
6+
- Add `prebuilt_allowed` field to `CompilationUnitCairoPluginMetadata`.
67

78
## 1.13.0 (2024-10-28)
89
- Add `CompilationUnitComponentId`.

scarb-metadata/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,9 @@ pub struct CompilationUnitCairoPluginMetadata {
420420
/// Package ID.
421421
pub package: PackageId,
422422

423+
/// Whether Scarb will attempt to load prebuilt binaries associated with this plugin.
424+
pub prebuilt_allowed: Option<bool>,
425+
423426
/// Additional data not captured by deserializer.
424427
#[cfg_attr(feature = "builder", builder(default))]
425428
#[serde(flatten)]

scarb/src/compiler/compilation_unit.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ pub struct CompilationUnitCairoPlugin {
9797
/// The Scarb plugin [`Package`] to load.
9898
pub package: Package,
9999
pub builtin: bool,
100+
pub prebuilt_allowed: bool,
100101
}
101102

102103
/// Unique identifier of the compilation unit component.

scarb/src/core/manifest/toml_manifest.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,12 @@ impl DefaultForProfile for TomlProfile {
357357
}
358358
}
359359

360+
#[derive(Debug, Default, Deserialize, Serialize, Clone)]
361+
#[serde(rename_all = "kebab-case")]
362+
pub struct TomlToolScarbMetadata {
363+
pub allow_prebuilt_plugins: Option<Vec<String>>,
364+
}
365+
360366
impl TomlManifest {
361367
pub fn read_from_path(path: &Utf8Path) -> Result<Self> {
362368
let contents = fs::read_to_string(path)

scarb/src/core/package/mod.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub use name::*;
1111
use scarb_ui::args::WithManifestPath;
1212

1313
use crate::core::manifest::Manifest;
14-
use crate::core::{Target, TargetKind};
14+
use crate::core::{Target, TargetKind, TomlToolScarbMetadata};
1515
use crate::internal::fsx;
1616

1717
mod id;
@@ -137,6 +137,15 @@ impl Package {
137137
Ok(structured)
138138
}
139139

140+
pub fn scarb_tool_metadata(&self) -> Result<TomlToolScarbMetadata> {
141+
Ok(self
142+
.tool_metadata("scarb")
143+
.cloned()
144+
.map(toml::Value::try_into)
145+
.transpose()?
146+
.unwrap_or_default())
147+
}
148+
140149
pub fn manifest_mut(&mut self) -> &mut Manifest {
141150
&mut Arc::make_mut(&mut self.0).manifest
142151
}

scarb/src/core/resolver.rs

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
use std::collections::HashMap;
2-
1+
use crate::core::lockfile::Lockfile;
2+
use crate::core::{PackageId, Summary, TargetKind};
33
use anyhow::{bail, Result};
44
use indoc::formatdoc;
55
use itertools::Itertools;
6+
use petgraph::algo::kosaraju_scc;
67
use petgraph::graphmap::DiGraphMap;
78
use petgraph::visit::{Dfs, EdgeFiltered, IntoNeighborsDirected, Walker};
89
use smallvec::SmallVec;
9-
10-
use crate::core::lockfile::Lockfile;
11-
use crate::core::{PackageId, Summary, TargetKind};
10+
use std::collections::{HashMap, HashSet};
11+
use std::hash::Hash;
1212

1313
/// Represents a fully-resolved package dependency graph.
1414
///
@@ -72,6 +72,67 @@ impl Resolve {
7272
.neighbors_directed(package_id, petgraph::Direction::Outgoing)
7373
.collect_vec()
7474
}
75+
76+
/// Find all subtress of the graph, that are reachable from nodes, that can be identified by
77+
/// keys from the `start` vector, where each key is a result of applying `key` function to a
78+
/// package id.
79+
pub fn filter_subtrees<T: Sized + Eq + Hash>(
80+
&self,
81+
target_kind: &TargetKind,
82+
start: Vec<T>,
83+
key: impl Fn(PackageId) -> T,
84+
) -> HashSet<T> {
85+
// We want to traverse the graph in topological order, so that for each node we can decide
86+
// if the subtree should be included.
87+
// However, we cannot actually topologically sort the graph, as it's not guaranteed to be
88+
// a DAG (it may contain cycles of dependencies).
89+
// Instead, we use Kosaraju's algorithm to find strongly connected components (scc) of the graph.
90+
// Each of SCCs is a cycle in the original graph.
91+
// The graph of SCCs is a DAG, thus we can traverse it in a topological order.
92+
let scc = self.scc();
93+
let mut allowed_prebuilds = SubTreeFilter::new(start);
94+
for comp in &scc {
95+
if comp.iter().any(|x| allowed_prebuilds.check(&key(*x))) {
96+
allowed_prebuilds.allow(comp.iter().map(|x| key(*x)));
97+
for package in comp {
98+
let deps = self.package_dependencies_for_target_kind(*package, target_kind);
99+
allowed_prebuilds.allow(deps.iter().map(|x| key(*x)));
100+
}
101+
}
102+
}
103+
104+
allowed_prebuilds.0
105+
}
106+
107+
/// Return a vector where each element is a strongly connected component (scc) of the graph.
108+
/// The order of node ids within each scc is arbitrary,
109+
/// but the order of the sccs is their topological order.
110+
fn scc(&self) -> Vec<Vec<PackageId>> {
111+
kosaraju_scc(&self.graph)
112+
.iter()
113+
.map(|scc| scc.iter().copied().collect_vec())
114+
// We need to reverse the iterator here, as kosaraju algorithm returns
115+
// the sccs in a postorder (reversed topological order).
116+
.rev()
117+
.collect_vec()
118+
}
119+
}
120+
121+
#[derive(Debug, Default)]
122+
struct SubTreeFilter<T: Sized + Eq + Hash>(HashSet<T>);
123+
124+
impl<T: Sized + Eq + Hash> SubTreeFilter<T> {
125+
fn new(allowed: Vec<T>) -> Self {
126+
Self(allowed.into_iter().collect())
127+
}
128+
129+
fn allow<I: IntoIterator<Item = T>>(&mut self, iter: I) {
130+
self.0.extend(iter)
131+
}
132+
133+
fn check(&self, key: &T) -> bool {
134+
self.0.contains(key)
135+
}
75136
}
76137

77138
#[derive(Debug, Default, Clone, PartialEq, Eq)]

scarb/src/ops/metadata.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ fn collect_cairo_compilation_unit_metadata(
239239
.map(|c| {
240240
m::CompilationUnitCairoPluginMetadataBuilder::default()
241241
.package(wrap_package_id(c.package.id))
242+
.prebuilt_allowed(c.prebuilt_allowed)
242243
.build()
243244
.unwrap()
244245
})

scarb/src/ops/resolve.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,33 @@ impl WorkspaceResolve {
6363
.map(|id| self.packages[id].clone())
6464
.collect_vec()
6565
}
66+
67+
/// Get all dependencies with allowed prebuilt macros for a given package.
68+
pub fn allowed_prebuilt(
69+
&self,
70+
package: Package,
71+
target_kind: &TargetKind,
72+
) -> Result<AllowedPrebuiltFilter> {
73+
let metadata = package.scarb_tool_metadata()?;
74+
let allowed = metadata.allow_prebuilt_plugins.unwrap_or_default();
75+
let allowed = allowed
76+
.into_iter()
77+
.filter_map(|name| PackageName::try_new(name).ok())
78+
.map(|name| name.to_smol_str())
79+
.collect();
80+
let allowed =
81+
self.resolve
82+
.filter_subtrees(target_kind, allowed, |package_id: PackageId| {
83+
package_id.name.to_smol_str()
84+
});
85+
let allowed_prebuilds = AllowedPrebuiltFilter::new(
86+
allowed
87+
.into_iter()
88+
.map(PackageName::new)
89+
.collect::<HashSet<_>>(),
90+
);
91+
Ok(allowed_prebuilds)
92+
}
6693
}
6794

6895
#[derive(Debug, Default)]
@@ -185,6 +212,19 @@ async fn collect_packages_from_resolve_graph(
185212
Ok(packages)
186213
}
187214

215+
#[derive(Debug, Default)]
216+
pub struct AllowedPrebuiltFilter(HashSet<PackageName>);
217+
218+
impl AllowedPrebuiltFilter {
219+
pub fn new(allowed: HashSet<PackageName>) -> Self {
220+
Self(allowed)
221+
}
222+
223+
pub fn check(&self, package: &Package) -> bool {
224+
self.0.contains(&package.id.name)
225+
}
226+
}
227+
188228
#[tracing::instrument(skip_all, level = "debug")]
189229
pub fn generate_compilation_units(
190230
resolve: &WorkspaceResolve,
@@ -548,6 +588,9 @@ impl<'a> PackageSolutionCollector<'a> {
548588
target_kind: &TargetKind,
549589
ignore_cairo_version: bool,
550590
) -> Result<(Vec<Package>, Vec<CompilationUnitCairoPlugin>)> {
591+
let allowed_prebuilds = self
592+
.resolve
593+
.allowed_prebuilt(self.member.clone(), target_kind)?;
551594
let mut classes = self
552595
.resolve
553596
.solution_of(self.member.id, target_kind)
@@ -602,12 +645,14 @@ impl<'a> PackageSolutionCollector<'a> {
602645
let cairo_plugins = cairo_plugins
603646
.into_iter()
604647
.map(|package| {
648+
let prebuilt_allowed = allowed_prebuilds.check(&package);
605649
// We can safely unwrap as all packages with `PackageClass::CairoPlugin` must define plugin target.
606650
let target = package.target(&TargetKind::CAIRO_PLUGIN).unwrap();
607651
let props: CairoPluginProps = target.props()?;
608652
Ok(CompilationUnitCairoPlugin::builder()
609653
.package(package)
610654
.builtin(props.builtin)
655+
.prebuilt_allowed(prebuilt_allowed)
611656
.build())
612657
})
613658
.collect::<Result<Vec<_>>>()?;

scarb/tests/metadata.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use itertools::Itertools;
66
use serde_json::json;
77

88
use scarb_metadata::{Cfg, DepKind, ManifestMetadataBuilder, Metadata, PackageMetadata};
9+
use scarb_test_support::cairo_plugin_project_builder::CairoPluginProjectBuilder;
910
use scarb_test_support::command::{CommandExt, Scarb};
1011
use scarb_test_support::fsx;
1112
use scarb_test_support::project_builder::{Dep, DepBuilder, ProjectBuilder};
@@ -1443,3 +1444,111 @@ fn can_enable_add_redeposit_gas() {
14431444
.unwrap();
14441445
assert!(add_redeposit_gas);
14451446
}
1447+
1448+
#[test]
1449+
fn prebuilt_plugins_disallowed_by_default() {
1450+
let t = assert_fs::TempDir::new().unwrap();
1451+
1452+
CairoPluginProjectBuilder::default()
1453+
.name("q")
1454+
.scarb_project(|builder| {
1455+
builder
1456+
.name("q")
1457+
.version("1.0.0")
1458+
.manifest_extra("[cairo-plugin]")
1459+
})
1460+
.build(&t.child("q"));
1461+
ProjectBuilder::start()
1462+
.name("y")
1463+
.version("1.0.0")
1464+
.lib_cairo(r"fn f() -> felt252 { z::f() }")
1465+
.dep_cairo_test()
1466+
.dep("q", Dep.path("../q"))
1467+
.build(&t.child("y"));
1468+
ProjectBuilder::start()
1469+
.name("x")
1470+
.version("1.0.0")
1471+
.lib_cairo(r"fn f() -> felt252 { y::f() }")
1472+
.dep_cairo_test()
1473+
.dep("y", Dep.path("y"))
1474+
.dep("q", Dep.path("q"))
1475+
.build(&t);
1476+
1477+
let meta = Scarb::quick_snapbox()
1478+
.arg("--json")
1479+
.arg("metadata")
1480+
.arg("--format-version")
1481+
.arg("1")
1482+
.current_dir(&t)
1483+
.stdout_json::<Metadata>();
1484+
1485+
let cu = meta
1486+
.compilation_units
1487+
.iter()
1488+
.find(|cu| cu.target.name == "x")
1489+
.unwrap();
1490+
1491+
assert_eq!(cu.cairo_plugins.len(), 1);
1492+
assert!(cu.cairo_plugins[0].package.repr.starts_with("q"));
1493+
assert!(!cu.cairo_plugins[0].prebuilt_allowed.unwrap());
1494+
}
1495+
1496+
#[test]
1497+
fn can_allow_prebuilt_plugins_for_subtree() {
1498+
let t = assert_fs::TempDir::new().unwrap();
1499+
1500+
CairoPluginProjectBuilder::default()
1501+
.name("q")
1502+
.scarb_project(|builder| {
1503+
builder
1504+
.name("q")
1505+
.version("1.0.0")
1506+
.manifest_extra("[cairo-plugin]")
1507+
})
1508+
.build(&t.child("q"));
1509+
1510+
ProjectBuilder::start()
1511+
.name("z")
1512+
.version("1.0.0")
1513+
.lib_cairo(r"fn f() -> felt252 { q::f() }")
1514+
.dep_cairo_test()
1515+
.dep("q", Dep.path("../q"))
1516+
.build(&t.child("z"));
1517+
1518+
ProjectBuilder::start()
1519+
.name("y")
1520+
.version("1.0.0")
1521+
.lib_cairo(r"fn f() -> felt252 { z::f() }")
1522+
.dep_cairo_test()
1523+
.dep("z", Dep.path("../z"))
1524+
.build(&t.child("y"));
1525+
1526+
ProjectBuilder::start()
1527+
.name("x")
1528+
.version("1.0.0")
1529+
.lib_cairo(r"fn f() -> felt252 { y::f() }")
1530+
.manifest_extra(indoc! {r#"
1531+
[tool.scarb]
1532+
allow-prebuilt-plugins = ["y"]
1533+
"#})
1534+
.dep_cairo_test()
1535+
.dep("z", Dep.path("z"))
1536+
.dep("y", Dep.path("y"))
1537+
.build(&t);
1538+
1539+
let meta = Scarb::quick_snapbox()
1540+
.arg("--json")
1541+
.arg("metadata")
1542+
.arg("--format-version")
1543+
.arg("1")
1544+
.current_dir(&t)
1545+
.stdout_json::<Metadata>();
1546+
let cu = meta
1547+
.compilation_units
1548+
.iter()
1549+
.find(|cu| cu.target.name == "x")
1550+
.unwrap();
1551+
assert_eq!(cu.cairo_plugins.len(), 1);
1552+
assert!(cu.cairo_plugins[0].package.repr.starts_with("q"));
1553+
assert!(cu.cairo_plugins[0].prebuilt_allowed.unwrap());
1554+
}

0 commit comments

Comments
 (0)