pub mod cm;
pub use cfg_expr;
#[cfg(feature = "targets")]
pub use cfg_expr::target_lexicon;
use cm::DependencyKind as DK;
pub use cm::{Metadata, Package, PackageId};
pub use petgraph;
pub use semver;
pub use camino::{self, Utf8Path, Utf8PathBuf};
use petgraph::{Direction, graph::EdgeIndex, graph::NodeIndex, visit::EdgeRef};
mod builder;
mod errors;
mod pkgspec;
pub use builder::{
Builder, Cmd, LockOptions, NoneFilter, OnFilter, Scope, Target,
features::{Feature, ParsedFeature},
index,
};
pub use errors::Error;
pub use pkgspec::PkgSpec;
use std::fmt;
#[derive(Clone, Default)]
pub struct Kid {
pub repr: String,
components: [(usize, usize); 3],
}
impl Kid {
#[inline]
pub fn name(&self) -> &str {
let (s, e) = self.components[0];
&self.repr[s..e]
}
#[inline]
pub fn version(&self) -> &str {
let (s, e) = self.components[1];
&self.repr[s..e]
}
#[inline]
pub fn source(&self) -> &str {
let (s, e) = self.components[2];
&self.repr[s..e]
}
}
#[allow(clippy::fallible_impl_from)]
impl From<PackageId> for Kid {
fn from(pid: PackageId) -> Self {
let mut repr = pid.repr;
let mut parse = || {
let components = if repr.contains(' ') {
let name = (0, repr.find(' ')?);
let version = (name.1 + 1, repr[name.1 + 1..].find(' ')? + name.1 + 1);
let source = (version.1 + 2, repr.rfind('#').unwrap_or(repr.len() - 1));
[name, version, source]
} else {
let mut vmn = repr.rfind('#')?;
let (name, version) = if let Some(split) = repr[vmn..].find('@') {
((vmn + 1, vmn + split), (vmn + split + 1, repr.len()))
} else {
let begin = repr.rfind('/')? + 1;
let end = if repr.starts_with("git+") {
let end = repr[begin..].rfind('?').map_or(vmn, |q| q + begin);
if repr[end..].contains('%') {
let mut decoded = String::new();
let mut encoded = &repr[end..];
let before = encoded.len();
loop {
let Some(pi) = encoded.find('%') else {
decoded.push_str(encoded);
break;
};
decoded.push_str(&encoded[..pi]);
let Some(encoding) = encoded.get(pi + 1..pi + 3) else {
panic!(
"invalid percent encoding in '{}', '{}' should be exactly 3 digits long",
&repr[end..],
&encoded[pi..]
);
};
let c = match encoding {
"2F" | "2f" => '/',
"21" => '!',
"22" => '"',
"23" => '#',
"24" => '$',
"25" => '%',
"26" => '&',
"27" => '\'',
"28" => '(',
"29" => ')',
"2A" | "2a" => '*',
"2B" | "2b" => '+',
"2C" | "2c" => ',',
"2D" | "2d" => '-',
"2E" | "2e" => '.',
"3B" | "3b" => ';',
"3C" | "3c" => '<',
"3D" | "3d" => '=',
"3E" | "3e" => '>',
"40" => '@',
"5D" | "5d" => ']',
"5F" | "5f" => '_',
"60" => '`',
"7B" | "7b" => '{',
"7C" | "7c" => '|',
"7D" | "7d" => '}',
"20" => ' ',
"3A" | "3a" => ':',
"3F" | "3f" => '?',
"5B" | "5b" => '[',
"5C" | "5c" => '\\',
"5E" | "5e" => '^',
"7E" | "7e" => '~',
_ => panic!(
"unknown percent encoding '%{encoding}' in '{}'",
&repr[end..]
),
};
decoded.push(c);
encoded = &encoded[pi + 3..];
}
repr.truncate(end);
repr.push_str(&decoded);
vmn -= before - decoded.len();
}
end
} else {
vmn
};
((begin, end), (vmn + 1, repr.len()))
};
[name, version, (0, vmn)]
};
Some(components)
};
if let Some(components) = parse() {
Self { repr, components }
} else {
panic!("unable to parse package id '{repr}'");
}
}
}
impl fmt::Debug for Kid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut ds = f.debug_struct("Kid");
ds.field("name", &self.name())
.field("version", &self.version());
let src = self.source();
if src != "registry+https://github.com/rust-lang/crates.io-index" {
ds.field("source", &src);
}
ds.finish()
}
}
impl fmt::Display for Kid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.repr)
}
}
impl std::hash::Hash for Kid {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
state.write(self.repr.as_bytes());
}
}
impl Ord for Kid {
fn cmp(&self, o: &Self) -> std::cmp::Ordering {
let a = &self.repr;
let b = &o.repr;
for (ar, br) in self.components.into_iter().zip(o.components) {
let ord = a[ar.0..ar.1].cmp(&b[br.0..br.1]);
if ord != std::cmp::Ordering::Equal {
return ord;
}
}
std::cmp::Ordering::Equal
}
}
impl PartialOrd for Kid {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Eq for Kid {}
impl PartialEq for Kid {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == std::cmp::Ordering::Equal
}
}
pub type EnabledFeatures = std::collections::BTreeSet<String>;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum DepKind {
Normal,
Build,
Dev,
}
impl From<DK> for DepKind {
fn from(dk: DK) -> Self {
match dk {
DK::Normal => Self::Normal,
DK::Build => Self::Build,
DK::Development => Self::Dev,
}
}
}
impl PartialEq<DK> for DepKind {
fn eq(&self, other: &DK) -> bool {
matches!(
(self, *other),
(Self::Normal, DK::Normal) | (Self::Build, DK::Build) | (Self::Dev, DK::Development)
)
}
}
impl fmt::Display for DepKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Normal => Ok(()),
Self::Build => f.write_str("build"),
Self::Dev => f.write_str("dev"),
}
}
}
pub type NodeId = NodeIndex<u32>;
pub type EdgeId = EdgeIndex<u32>;
pub enum Node<N> {
Krate {
id: Kid,
krate: N,
features: EnabledFeatures,
dep_mapping: Vec<Option<NodeId>>,
},
Feature {
krate_index: NodeId,
name: String,
},
}
impl<N> fmt::Display for Node<N>
where
N: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Krate { krate, .. } => {
write!(f, "crate {krate}")
}
Self::Feature { name, .. } => {
write!(f, "feature {name}")
}
}
}
}
impl<N> fmt::Debug for Node<N>
where
N: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Krate { id, krate, .. } => {
write!(f, "crate {} {krate:?}", id.repr)
}
Self::Feature { name, .. } => {
write!(f, "feature {name}")
}
}
}
}
#[derive(Debug, Clone)]
pub enum Edge {
Dep {
kind: DepKind,
cfg: Option<String>,
},
Feature,
DepFeature {
kind: DepKind,
cfg: Option<String>,
},
}
impl fmt::Display for Edge {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Self::DepFeature { kind, cfg } | Self::Dep { kind, cfg } = self {
match kind {
DepKind::Normal => {}
DepKind::Build => f.write_str("(build)")?,
DepKind::Dev => f.write_str("(dev)")?,
};
if let Some(cfg) = cfg {
write!(f, " '{cfg}'")?;
}
}
Ok(())
}
}
pub struct Krates<N = cm::Package, E = Edge> {
graph: petgraph::Graph<Node<N>, E, petgraph::Directed, u32>,
workspace_members: Vec<Kid>,
workspace_root: Utf8PathBuf,
krates_end: usize,
}
#[allow(clippy::len_without_is_empty)]
impl<N, E> Krates<N, E> {
#[inline]
pub fn len(&self) -> usize {
self.krates_end
}
#[inline]
pub fn workspace_root(&self) -> &Utf8Path {
&self.workspace_root
}
#[inline]
pub fn graph(&self) -> &petgraph::Graph<Node<N>, E> {
&self.graph
}
#[inline]
pub fn krates(&self) -> impl Iterator<Item = &N> {
self.graph.raw_nodes()[..self.krates_end]
.iter()
.filter_map(move |node| {
if let Node::Krate { krate, .. } = &node.weight {
Some(krate)
} else {
None
}
})
}
#[inline]
pub fn get_deps(&self, id: NodeId) -> impl Iterator<Item = (&Node<N>, &E)> {
self.graph
.edges_directed(id, Direction::Outgoing)
.map(move |edge| {
let krate = &self.graph[edge.target()];
(krate, edge.weight())
})
}
#[inline]
pub fn direct_dependencies(&self, nid: NodeId) -> Vec<DirectDependency<'_, N>> {
let graph = self.graph();
let mut direct_dependencies = Vec::new();
let mut stack = vec![nid];
let mut visited = std::collections::BTreeSet::new();
while let Some(nid) = stack.pop() {
for edge in graph.edges_directed(nid, Direction::Outgoing) {
match &graph[edge.target()] {
Node::Krate { krate, .. } => {
if visited.insert(edge.target()) {
direct_dependencies.push(DirectDependency {
krate,
node_id: edge.target(),
edge_id: edge.id(),
});
}
}
Node::Feature { .. } => {
if visited.insert(edge.target()) {
stack.push(edge.target());
}
}
}
}
}
direct_dependencies
}
#[inline]
pub fn direct_dependents(&self, nid: NodeId) -> Vec<DirectDependent<'_, N>> {
let graph = self.graph();
let mut direct_dependents = Vec::new();
let mut stack = vec![nid];
let mut visited = std::collections::BTreeSet::new();
while let Some(nid) = stack.pop() {
for edge in graph.edges_directed(nid, Direction::Incoming) {
match &graph[edge.source()] {
Node::Krate { krate, .. } => {
if visited.insert(edge.source()) {
direct_dependents.push(DirectDependent {
krate,
node_id: edge.source(),
edge_id: edge.id(),
});
}
}
Node::Feature { krate_index, .. } => {
if *krate_index == nid && visited.insert(edge.source()) {
stack.push(edge.source());
}
}
}
}
}
direct_dependents
}
#[inline]
pub fn resolved_dependency(&self, nid: NodeId, dep_index: usize) -> Option<&N> {
if nid.index() >= self.krates_end {
return None;
}
let Node::Krate { dep_mapping, .. } = &self.graph[nid] else {
return None;
};
dep_mapping
.get(dep_index)
.copied()
.flatten()
.and_then(|nid| {
let Node::Krate { krate, .. } = &self.graph[nid] else {
return None;
};
Some(krate)
})
}
#[inline]
pub fn nid_for_kid(&self, kid: &Kid) -> Option<NodeId> {
self.graph.raw_nodes()[..self.krates_end]
.binary_search_by(|rn| {
if let Node::Krate { id, .. } = &rn.weight {
id.cmp(kid)
} else {
unreachable!();
}
})
.ok()
.map(NodeId::new)
}
#[inline]
pub fn node_for_kid(&self, kid: &Kid) -> Option<&Node<N>> {
self.nid_for_kid(kid).map(|nid| &self.graph[nid])
}
#[inline]
pub fn get_node(&self, kid: &Kid, feature: Option<&str>) -> Option<(NodeId, &Node<N>)> {
self.nid_for_kid(kid).and_then(|nid| {
if let Some(feat) = feature {
self.graph
.edges_directed(nid, Direction::Incoming)
.find_map(|edge| {
if let Node::Feature { krate_index, name } = &self.graph[edge.source()] {
if *krate_index == nid && name == feat {
return Some((edge.source(), &self.graph[edge.source()]));
}
}
None
})
} else {
Some((nid, &self.graph[nid]))
}
})
}
#[inline]
pub fn get_enabled_features(&self, kid: &Kid) -> Option<&EnabledFeatures> {
self.node_for_kid(kid).map(|node| {
if let Node::Krate { features, .. } = node {
features
} else {
unreachable!()
}
})
}
#[inline]
pub fn workspace_members(&self) -> impl Iterator<Item = &Node<N>> {
self.workspace_members
.iter()
.filter_map(move |pid| self.nid_for_kid(pid).map(|ind| &self.graph[ind]))
}
}
pub struct DirectDependency<'krates, N> {
pub krate: &'krates N,
pub node_id: NodeId,
pub edge_id: EdgeId,
}
pub struct DirectDependent<'krates, N> {
pub krate: &'krates N,
pub node_id: NodeId,
pub edge_id: EdgeId,
}
pub trait KrateDetails {
fn name(&self) -> &str;
fn version(&self) -> &semver::Version;
}
impl KrateDetails for cm::Package {
fn name(&self) -> &str {
&self.name
}
fn version(&self) -> &semver::Version {
&self.version
}
}
impl<N> Krates<N, Edge>
where
N: std::fmt::Debug,
{
pub fn krates_filtered(&self, filter: DepKind) -> Vec<&N> {
let graph = self.graph();
let mut filtered: std::collections::BTreeMap<_, _> = self
.workspace_members()
.filter_map(|n| {
let Node::Krate { id, krate, .. } = n else {
return None;
};
Some((id, krate))
})
.collect();
let mut stack: Vec<_> = self
.workspace_members
.iter()
.filter_map(|pid| self.nid_for_kid(pid))
.collect();
let mut visited = std::collections::BTreeSet::new();
while let Some(nid) = stack.pop() {
for edge in graph.edges_directed(nid, Direction::Outgoing) {
match edge.weight() {
Edge::Dep { kind, .. } | Edge::DepFeature { kind, .. } => {
if *kind == filter {
continue;
}
}
Edge::Feature => {}
};
match &graph[edge.target()] {
Node::Krate { id, krate, .. } => {
if visited.insert(edge.target()) {
stack.push(edge.target());
filtered.insert(id, krate);
}
}
Node::Feature { .. } => {
if visited.insert(edge.target()) {
stack.push(edge.target());
}
}
}
}
visited.insert(nid);
}
filtered.into_values().collect()
}
}
impl<N, E> Krates<N, E>
where
N: KrateDetails,
{
pub fn search_matches(
&self,
name: impl Into<String>,
req: semver::VersionReq,
) -> impl Iterator<Item = KrateMatch<'_, N>> {
let raw_nodes = &self.graph.raw_nodes()[0..self.krates_end];
let name = name.into();
raw_nodes
.iter()
.enumerate()
.filter_map(move |(index, node)| {
if let Node::Krate { krate, id, .. } = &node.weight {
if krate.name() == name && req.matches(krate.version()) {
return Some(KrateMatch {
node_id: NodeId::new(index),
krate,
kid: id,
});
}
}
None
})
}
pub fn krates_by_name(
&self,
name: impl Into<String>,
) -> impl Iterator<Item = KrateMatch<'_, N>> {
let raw_nodes = &self.graph.raw_nodes()[0..self.krates_end];
let name = name.into();
raw_nodes
.iter()
.enumerate()
.filter_map(move |(index, node)| {
if let Node::Krate { krate, id, .. } = &node.weight {
if krate.name() == name {
return Some(KrateMatch {
node_id: NodeId::new(index),
krate,
kid: id,
});
}
}
None
})
}
}
pub struct KrateMatch<'graph, N> {
pub krate: &'graph N,
pub kid: &'graph Kid,
pub node_id: NodeId,
}
impl<N, E> std::ops::Index<NodeId> for Krates<N, E> {
type Output = N;
#[inline]
fn index(&self, id: NodeId) -> &Self::Output {
match &self.graph[id] {
Node::Krate { krate, .. } => krate,
Node::Feature { .. } => panic!("indexed outside of crate graph"),
}
}
}
impl<N, E> std::ops::Index<usize> for Krates<N, E> {
type Output = N;
#[inline]
fn index(&self, idx: usize) -> &Self::Output {
match &self.graph.raw_nodes()[idx].weight {
Node::Krate { krate, .. } => krate,
Node::Feature { .. } => panic!("indexed outside of crate graph"),
}
}
}
#[derive(Debug)]
struct MdTarget {
inner: String,
cfg: Option<cfg_expr::Expression>,
}
impl From<String> for MdTarget {
fn from(inner: String) -> Self {
let cfg = inner
.starts_with("cfg(")
.then(|| cfg_expr::Expression::parse(&inner).ok())
.flatten();
Self { inner, cfg }
}
}
fn targets_eq(target: &Option<MdTarget>, other: &Option<String>) -> bool {
match (target, other) {
(None, None) => true,
(Some(a), Some(b)) => a.inner.eq(b),
_ => false,
}
}
#[cfg(test)]
mod tests {
#[test]
fn converts_package_ids() {
let ids = [
(
"registry+https://github.com/rust-lang/crates.io-index#ab_glyph@0.2.22",
"ab_glyph",
"0.2.22",
"registry+https://github.com/rust-lang/crates.io-index",
),
(
"git+https://github.com/EmbarkStudios/egui-stylist?rev=3900e8aedc5801e42c1bb747cfd025615bf3b832#0.2.0",
"egui-stylist",
"0.2.0",
"git+https://github.com/EmbarkStudios/egui-stylist?rev=3900e8aedc5801e42c1bb747cfd025615bf3b832",
),
(
"path+file:///home/jake/code/ark/components/allocator#ark-allocator@0.1.0",
"ark-allocator",
"0.1.0",
"path+file:///home/jake/code/ark/components/allocator",
),
(
"git+https://github.com/EmbarkStudios/ash?branch=nv-low-latency2#0.38.0+1.3.269",
"ash",
"0.38.0+1.3.269",
"git+https://github.com/EmbarkStudios/ash?branch=nv-low-latency2",
),
(
"git+https://github.com/EmbarkStudios/fsr-rs?branch=nv-low-latency2#fsr@0.1.7",
"fsr",
"0.1.7",
"git+https://github.com/EmbarkStudios/fsr-rs?branch=nv-low-latency2",
),
(
"git+https://github.com/ComunidadAylas/glsl-lang#0.5.2",
"glsl-lang",
"0.5.2",
"git+https://github.com/ComunidadAylas/glsl-lang",
),
(
"git+https://github.com/vtavernier/glsl-lang?tag=v0.5.2#0.5.2",
"glsl-lang",
"0.5.2",
"git+https://github.com/vtavernier/glsl-lang?tag=v0.5.2",
),
(
"fuser 0.4.1 (git+https://github.com/cberner/fuser?branch=master#b2e7622942e52a28ffa85cdaf48e28e982bb6923)",
"fuser",
"0.4.1",
"git+https://github.com/cberner/fuser?branch=master",
),
(
"fuser 0.4.1 (git+https://github.com/cberner/fuser?rev=b2e7622#b2e7622942e52a28ffa85cdaf48e28e982bb6923)",
"fuser",
"0.4.1",
"git+https://github.com/cberner/fuser?rev=b2e7622",
),
(
"a 0.1.0 (path+file:///home/jake/code/krates/tests/ws/a)",
"a",
"0.1.0",
"path+file:///home/jake/code/krates/tests/ws/a",
),
(
"bindgen 0.59.2 (registry+https://github.com/rust-lang/crates.io-index)",
"bindgen",
"0.59.2",
"registry+https://github.com/rust-lang/crates.io-index",
),
];
for (repr, name, version, source) in ids {
let kid = super::Kid::from(super::PackageId {
repr: repr.to_owned(),
});
assert_eq!(kid.name(), name);
assert_eq!(kid.version(), version);
assert_eq!(kid.source(), source);
}
}
}