use std::path::{Path, PathBuf};
use std::process::Command;
use cloacina_constructor_contract::ProviderManifest;
use super::constructor_provider::{
package_constructor_provider, ProviderPackageError, ProviderPackageOptions,
PROVIDER_MANIFEST_FILE,
};
pub const PROVIDERS_DIR: &str = "providers";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderRef {
pub name: String,
pub version: Option<String>,
}
impl ProviderRef {
pub fn parse(from: &str) -> Self {
match from.split_once('@') {
Some((name, ver)) => Self {
name: name.to_string(),
version: Some(ver.to_string()),
},
None => Self {
name: from.to_string(),
version: None,
},
}
}
}
#[derive(Debug, Clone)]
pub struct BundledProvider {
pub from: String,
pub crate_dir: PathBuf,
pub provider_name: String,
pub version: String,
pub bundled_dir: PathBuf,
pub constructors: Vec<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum ProviderBundleError {
#[error("cargo metadata failed: {0}")]
Metadata(String),
#[error("{0}")]
NotFound(String),
#[error(transparent)]
Package(#[from] ProviderPackageError),
#[error("{0}")]
Io(String),
}
pub fn provider_runtime_for_crate(
crate_dir: &Path,
) -> cloacina_constructor_contract::ProviderRuntime {
use cloacina_constructor_contract::ProviderRuntime;
let Ok(raw) = std::fs::read_to_string(crate_dir.join("Cargo.toml")) else {
return ProviderRuntime::Wasm;
};
let Ok(value) = raw.parse::<toml::Value>() else {
return ProviderRuntime::Wasm;
};
match value
.get("package")
.and_then(|p| p.get("metadata"))
.and_then(|m| m.get("cloacina"))
.and_then(|c| c.get("runtime"))
.and_then(|r| r.as_str())
{
Some("native") => ProviderRuntime::Native,
_ => ProviderRuntime::Wasm,
}
}
pub fn resolve_provider_crate(
consumer_dir: &Path,
provider: &ProviderRef,
) -> Result<PathBuf, ProviderBundleError> {
let out = Command::new("cargo")
.args(["metadata", "--format-version", "1"])
.current_dir(consumer_dir)
.output()
.map_err(|e| ProviderBundleError::Metadata(format!("spawn cargo metadata: {e}")))?;
if !out.status.success() {
return Err(ProviderBundleError::Metadata(
String::from_utf8_lossy(&out.stderr).trim().to_string(),
));
}
let meta: serde_json::Value = serde_json::from_slice(&out.stdout)
.map_err(|e| ProviderBundleError::Metadata(format!("parse cargo metadata JSON: {e}")))?;
let packages = meta
.get("packages")
.and_then(|p| p.as_array())
.ok_or_else(|| ProviderBundleError::Metadata("cargo metadata has no `packages`".into()))?;
let mut matches: Vec<(String, PathBuf)> = Vec::new();
for pkg in packages {
let name = pkg.get("name").and_then(|v| v.as_str()).unwrap_or_default();
if name != provider.name {
continue;
}
let version = pkg
.get("version")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let manifest_path = pkg
.get("manifest_path")
.and_then(|v| v.as_str())
.map(PathBuf::from);
if let Some(mp) = manifest_path {
if let Some(dir) = mp.parent() {
matches.push((version, dir.to_path_buf()));
}
}
}
if matches.is_empty() {
return Err(ProviderBundleError::NotFound(format!(
"provider crate '{}' is not a dependency in the consumer's graph ({}). \
Add it to the workflow crate's [dependencies].",
provider.name,
consumer_dir.display()
)));
}
if let Some(want) = &provider.version {
let segment_prefix = format!("{want}.");
let filtered: Vec<&(String, PathBuf)> = matches
.iter()
.filter(|(v, _)| v == want || v.starts_with(&segment_prefix))
.collect();
return match filtered.first() {
Some((_, dir)) => Ok((*dir).clone()),
None => Err(ProviderBundleError::NotFound(format!(
"provider '{}@{}' — the resolved graph has '{}' at version(s) [{}], not {}",
provider.name,
want,
provider.name,
matches
.iter()
.map(|(v, _)| v.as_str())
.collect::<Vec<_>>()
.join(", "),
want
))),
};
}
Ok(matches.into_iter().next().unwrap().1)
}
pub fn bundle_providers(
consumer_dir: &Path,
provider_refs: &[ProviderRef],
dest: &Path,
release: bool,
) -> Result<Vec<BundledProvider>, ProviderBundleError> {
let providers_dir = dest.join(PROVIDERS_DIR);
std::fs::create_dir_all(&providers_dir).map_err(|e| {
ProviderBundleError::Io(format!(
"create providers dir {}: {e}",
providers_dir.display()
))
})?;
let mut seen: Vec<String> = Vec::new();
let mut bundled: Vec<BundledProvider> = Vec::new();
for provider in provider_refs {
if seen.contains(&provider.name) {
continue;
}
seen.push(provider.name.clone());
let crate_dir = resolve_provider_crate(consumer_dir, provider)?;
let staging = tempfile::TempDir::new()
.map_err(|e| ProviderBundleError::Io(format!("create staging dir: {e}")))?;
let archive = staging.path().join(format!("{}.cloacina", provider.name));
let opts = ProviderPackageOptions {
crate_dir: crate_dir.clone(),
output: Some(archive.clone()),
sign_key: None,
manifest_bin: "emit_manifest".to_string(),
release,
runtime: provider_runtime_for_crate(&crate_dir),
};
let result = package_constructor_provider(&opts)?;
let bundled_dir =
fidius_core::package::unpack_package(&archive, &providers_dir).map_err(|e| {
ProviderBundleError::Io(format!(
"unpack provider '{}' into bundle: {e}",
provider.name
))
})?;
let manifest_path = bundled_dir.join(PROVIDER_MANIFEST_FILE);
let manifest_raw = std::fs::read_to_string(&manifest_path).map_err(|e| {
ProviderBundleError::Io(format!("read bundled {}: {e}", manifest_path.display()))
})?;
let manifest = ProviderManifest::from_json(&manifest_raw)
.map_err(|e| ProviderBundleError::Io(format!("parse bundled provider.json: {e}")))?;
bundled.push(BundledProvider {
from: provider.name.clone(),
crate_dir,
provider_name: manifest.name.clone(),
version: manifest.version.clone(),
bundled_dir,
constructors: result.constructors,
});
}
Ok(bundled)
}
#[derive(Debug, Clone)]
pub struct PackedProvider {
pub from: String,
pub provider_name: String,
pub version: String,
pub constructors: Vec<String>,
pub archive: Vec<u8>,
pub runtime: cloacina_constructor_contract::ProviderRuntime,
}
pub fn pack_providers(
consumer_dir: &Path,
provider_refs: &[ProviderRef],
release: bool,
) -> Result<Vec<PackedProvider>, ProviderBundleError> {
let mut seen: Vec<String> = Vec::new();
let mut packed: Vec<PackedProvider> = Vec::new();
for provider in provider_refs {
if seen.contains(&provider.name) {
continue;
}
seen.push(provider.name.clone());
let crate_dir = resolve_provider_crate(consumer_dir, provider)?;
let staging = tempfile::TempDir::new()
.map_err(|e| ProviderBundleError::Io(format!("create staging dir: {e}")))?;
let archive_path = staging.path().join(format!("{}.cloacina", provider.name));
let runtime = provider_runtime_for_crate(&crate_dir);
let opts = ProviderPackageOptions {
crate_dir,
output: Some(archive_path.clone()),
sign_key: None,
manifest_bin: "emit_manifest".to_string(),
release,
runtime,
};
let result = package_constructor_provider(&opts)?;
let archive = std::fs::read(&archive_path).map_err(|e| {
ProviderBundleError::Io(format!(
"read packed provider archive for '{}': {e}",
provider.name
))
})?;
packed.push(PackedProvider {
from: provider.name.clone(),
provider_name: result.provider_name,
version: result.provider_version,
constructors: result.constructors,
archive,
runtime,
});
}
Ok(packed)
}
pub fn pack_providers_from_specs(
specs: &[(String, String)],
release: bool,
) -> Result<Vec<PackedProvider>, ProviderBundleError> {
if specs.is_empty() {
return Ok(Vec::new());
}
let scratch = tempfile::TempDir::new()
.map_err(|e| ProviderBundleError::Io(format!("create scratch consumer dir: {e}")))?;
let mut manifest = String::from(
"# Synthesized by cloacina provider bundling (CLOACI-T-0836/T-0831) to resolve\n\
# a Python package's [providers] deps through cargo.\n\
[workspace]\n\n\
[package]\n\
name = \"cloacina-provider-fetch\"\n\
version = \"0.0.0\"\n\
edition = \"2021\"\n\n\
[dependencies]\n",
);
for (name, spec) in specs {
manifest.push_str(&format!("{name} = {spec}\n"));
}
std::fs::write(scratch.path().join("Cargo.toml"), manifest)
.map_err(|e| ProviderBundleError::Io(format!("write scratch Cargo.toml: {e}")))?;
std::fs::create_dir_all(scratch.path().join("src"))
.map_err(|e| ProviderBundleError::Io(format!("create scratch src: {e}")))?;
std::fs::write(
scratch.path().join("src/lib.rs"),
"// provider fetch stub\n",
)
.map_err(|e| ProviderBundleError::Io(format!("write scratch lib.rs: {e}")))?;
let refs: Vec<ProviderRef> = specs
.iter()
.map(|(name, _)| ProviderRef {
name: name.clone(),
version: None, })
.collect();
pack_providers(scratch.path(), &refs, release)
}
pub fn discover_provider_refs(source_dir: &Path) -> Vec<ProviderRef> {
let mut refs: Vec<ProviderRef> = Vec::new();
let mut stack = vec![source_dir.to_path_buf()];
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if path.file_name().and_then(|n| n.to_str()) != Some("target") {
stack.push(path);
}
} else if path.extension().and_then(|x| x.to_str()) == Some("rs") {
let Ok(raw) = std::fs::read_to_string(&path) else {
continue;
};
let text: String = raw
.lines()
.filter(|l| !l.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n");
for anchor in ["constructor!", "#[reactor("] {
let mut rest = text.as_str();
while let Some(pos) = rest.find(anchor) {
let window = &rest[pos..rest.len().min(pos + 2048)];
if let Some(from) = extract_from_literal(window) {
let parsed = ProviderRef::parse(&from);
if !refs.iter().any(|r| r == &parsed) {
refs.push(parsed);
}
}
rest = &rest[pos + anchor.len()..];
}
}
}
}
}
refs
}
fn extract_from_literal(window: &str) -> Option<String> {
let mut search = window;
while let Some(idx) = search.find("from") {
let boundary_ok = idx == 0
|| matches!(
search.as_bytes()[idx - 1],
b' ' | b'\t' | b'\n' | b'\r' | b',' | b'(' | b'{'
);
let after = search[idx + 4..].trim_start();
if boundary_ok {
if let Some(rest) = after.strip_prefix('=') {
let rest = rest.trim_start();
if let Some(rest) = rest.strip_prefix('"') {
let end = rest.find('"')?;
return Some(rest[..end].to_string());
}
}
}
search = &search[idx + 4..];
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn discovers_constructor_from_refs_in_source() {
let src = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../examples/constructor-contract/packaged-consumer-fixture");
let refs = discover_provider_refs(&src);
assert_eq!(
refs,
vec![ProviderRef {
name: "cloacina-provider-fs".into(),
version: Some("0.1.0".into())
}],
"the packaged consumer fixture declares exactly one provider ref"
);
}
#[test]
fn discovery_ignores_unanchored_from_strings() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("src/lib.rs"),
r#"// from = "not-a-provider" (comment, no macro anchor)
fn f() { let _x = ("from", "also-not"); }"#,
)
.unwrap();
assert!(discover_provider_refs(dir.path()).is_empty());
}
#[test]
fn discovery_survives_comments_and_preceding_from_substrings() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join("src")).unwrap();
std::fs::write(
dir.path().join("src/lib.rs"),
r#"/// Doc example must NOT register a phantom ref:
/// constructor!(from = "phantom-provider");
fn consumer() {}
constructor!(
// Reads from disk on startup
id = "reader_from_disk",
from = "real-provider",
);"#,
)
.unwrap();
let refs = discover_provider_refs(dir.path());
assert_eq!(
refs,
vec![ProviderRef {
name: "real-provider".into(),
version: None
}],
"comment 'from's and value-literal 'from's must not shadow the real field, \
and doc-comment examples must not false-positive"
);
}
#[test]
fn provider_runtime_marker_selects_native() {
use cloacina_constructor_contract::ProviderRuntime;
let dir = tempfile::TempDir::new().unwrap();
assert_eq!(
provider_runtime_for_crate(dir.path()),
ProviderRuntime::Wasm
);
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"p\"\nversion = \"0.1.0\"\n",
)
.unwrap();
assert_eq!(
provider_runtime_for_crate(dir.path()),
ProviderRuntime::Wasm
);
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"p\"\nversion = \"0.1.0\"\n\n\
[package.metadata.cloacina]\nruntime = \"native\"\n",
)
.unwrap();
assert_eq!(
provider_runtime_for_crate(dir.path()),
ProviderRuntime::Native
);
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"p\"\nversion = \"0.1.0\"\n\n\
[package.metadata.cloacina]\nruntime = \"exotic\"\n",
)
.unwrap();
assert_eq!(
provider_runtime_for_crate(dir.path()),
ProviderRuntime::Wasm
);
}
#[test]
fn provider_ref_parses_name_and_optional_version() {
assert_eq!(
ProviderRef::parse("cloacina-provider-fs"),
ProviderRef {
name: "cloacina-provider-fs".into(),
version: None
}
);
assert_eq!(
ProviderRef::parse("cloacina-provider-fs@0.1.0"),
ProviderRef {
name: "cloacina-provider-fs".into(),
version: Some("0.1.0".into())
}
);
}
}