use std::env;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use pathdiff::diff_paths;
use serde::Serialize;
use thiserror::Error;
use toml::{Table as TomlTable, Value as TomlValue};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DependencyMode {
Local,
CratesIo,
}
impl DependencyMode {
pub fn from_env() -> Self {
match env::var("GREENTIC_DEP_MODE") {
Ok(value) => match value.trim().to_ascii_lowercase().as_str() {
"cratesio" | "crates-io" | "crates_io" => DependencyMode::CratesIo,
"local" => DependencyMode::Local,
"" => DependencyMode::CratesIo,
_ => {
eprintln!("Unknown GREENTIC_DEP_MODE='{value}', defaulting to cratesio mode");
DependencyMode::CratesIo
}
},
Err(_) => DependencyMode::CratesIo,
}
}
pub fn as_str(&self) -> &'static str {
match self {
DependencyMode::Local => "local",
DependencyMode::CratesIo => "cratesio",
}
}
}
const GREENTIC_TYPES_VERSION: &str = "0.4";
const GREENTIC_INTERFACES_GUEST_VERSION: &str = "0.4";
const GREENTIC_INTERFACES_VERSION: &str = "0.4";
#[derive(Debug, Clone)]
pub struct DependencyTemplates {
pub greentic_interfaces: String,
pub greentic_interfaces_guest: String,
pub greentic_types: String,
pub relative_patch_path: Option<String>,
}
#[derive(Debug, Error)]
pub enum DependencyError {
#[error("crates.io dependency mode forbids `path =` entries in {manifest}")]
PathDependency { manifest: PathBuf },
#[error("failed to read manifest {manifest}: {source}")]
Io {
manifest: PathBuf,
#[source]
source: io::Error,
},
}
pub fn resolve_dependency_templates(
mode: DependencyMode,
target_path: &Path,
) -> DependencyTemplates {
match mode {
DependencyMode::Local => resolve_local_templates(target_path),
DependencyMode::CratesIo => DependencyTemplates {
greentic_interfaces: format!("version = \"{GREENTIC_INTERFACES_VERSION}\""),
greentic_interfaces_guest: format!("version = \"{GREENTIC_INTERFACES_GUEST_VERSION}\""),
greentic_types: format!("version = \"{GREENTIC_TYPES_VERSION}\""),
relative_patch_path: None,
},
}
}
fn resolve_local_templates(target_path: &Path) -> DependencyTemplates {
let repo_root = workspace_root();
let interfaces_root = repo_root
.parent()
.map(|parent| parent.join("greentic-interfaces"));
let types_root = repo_root
.parent()
.map(|parent| parent.join("greentic-types"));
let greentic_interfaces = interfaces_root
.as_ref()
.map(|root| root.join("crates/greentic-interfaces"))
.filter(|path| path.exists())
.map(|path| format!(r#"path = "{}""#, absolute_path_string(&path)))
.unwrap_or_else(|| format!("version = \"{GREENTIC_INTERFACES_VERSION}\""));
let greentic_interfaces_guest = interfaces_root
.as_ref()
.map(|root| root.join("crates/greentic-interfaces-guest"))
.filter(|path| path.exists())
.map(|path| format!(r#"path = "{}""#, absolute_path_string(&path)))
.unwrap_or_else(|| format!("version = \"{GREENTIC_INTERFACES_GUEST_VERSION}\""));
let greentic_types = types_root
.filter(|path| path.exists())
.map(|path| format!(r#"path = "{}""#, absolute_path_string(&path)))
.unwrap_or_else(|| format!("version = \"{GREENTIC_TYPES_VERSION}\""));
DependencyTemplates {
greentic_interfaces,
greentic_interfaces_guest,
greentic_types,
relative_patch_path: local_patch_path(target_path),
}
}
fn local_patch_path(scaffold_root: &Path) -> Option<String> {
let repo_root = workspace_root();
let crate_root = repo_root.join("crates/greentic-component");
if !crate_root.exists() {
return None;
}
Some(greentic_component_patch_path(scaffold_root, &repo_root))
}
fn workspace_root() -> PathBuf {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
manifest_dir
.parent()
.and_then(|p| p.parent())
.unwrap_or(manifest_dir)
.to_path_buf()
}
fn greentic_component_patch_path(scaffold_root: &Path, repo_root: &Path) -> String {
let abs = repo_root.join("crates/greentic-component");
format!(r#"path = "{}""#, relative_path_string(scaffold_root, &abs))
}
fn relative_path_string(from: &Path, to: &Path) -> String {
diff_paths(to, from)
.unwrap_or_else(|| to.to_path_buf())
.display()
.to_string()
}
fn absolute_path_string(path: &Path) -> String {
path.canonicalize()
.unwrap_or_else(|_| path.to_path_buf())
.display()
.to_string()
}
pub fn ensure_cratesio_manifest_clean(root: &Path) -> Result<(), DependencyError> {
let manifest = root.join("Cargo.toml");
let contents = fs::read_to_string(&manifest).map_err(|source| DependencyError::Io {
manifest: manifest.clone(),
source,
})?;
let parsed: TomlTable = toml::from_str(&contents).map_err(|source| DependencyError::Io {
manifest: manifest.clone(),
source: io::Error::new(io::ErrorKind::InvalidData, source),
})?;
if manifest_has_path_dependency(&parsed) {
return Err(DependencyError::PathDependency { manifest });
}
Ok(())
}
fn manifest_has_path_dependency(doc: &TomlTable) -> bool {
has_path_dep_table(doc.get("dependencies").and_then(TomlValue::as_table))
|| has_path_dep_table(doc.get("dev-dependencies").and_then(TomlValue::as_table))
|| has_path_dep_table(doc.get("build-dependencies").and_then(TomlValue::as_table))
|| has_path_dep_workspace(doc.get("workspace").and_then(TomlValue::as_table))
|| has_path_dep_patch(doc.get("patch").and_then(TomlValue::as_table))
|| has_path_dep_target(doc.get("target").and_then(TomlValue::as_table))
}
fn has_path_dep_workspace(workspace: Option<&toml::Table>) -> bool {
let Some(workspace) = workspace else {
return false;
};
has_path_dep_table(workspace.get("dependencies").and_then(TomlValue::as_table))
}
fn has_path_dep_patch(patch: Option<&toml::Table>) -> bool {
let Some(patch) = patch else {
return false;
};
patch
.values()
.filter_map(TomlValue::as_table)
.any(|registry| has_path_dep_table(Some(registry)))
}
fn has_path_dep_target(target: Option<&toml::Table>) -> bool {
let Some(target) = target else {
return false;
};
target.values().filter_map(TomlValue::as_table).any(|cfg| {
has_path_dep_table(cfg.get("dependencies").and_then(TomlValue::as_table))
|| has_path_dep_table(cfg.get("dev-dependencies").and_then(TomlValue::as_table))
|| has_path_dep_table(cfg.get("build-dependencies").and_then(TomlValue::as_table))
})
}
fn has_path_dep_table(table: Option<&toml::Table>) -> bool {
let Some(table) = table else {
return false;
};
table.values().any(value_has_path_key)
}
fn value_has_path_key(value: &TomlValue) -> bool {
matches!(value, TomlValue::Table(dep) if dep.contains_key("path"))
}
#[cfg(test)]
mod tests {
use super::*;
use assert_fs::TempDir;
#[test]
fn cratesio_manifest_rejects_path_dependencies() {
let temp = TempDir::new().unwrap();
let manifest = temp.path().join("Cargo.toml");
std::fs::write(&manifest, "[dependencies]\nfoo = { path = \"../foo\" }\n").unwrap();
let err = ensure_cratesio_manifest_clean(temp.path()).unwrap_err();
match err {
DependencyError::PathDependency { manifest: path } => assert_eq!(path, manifest),
other => panic!("unexpected error {other:?}"),
}
}
#[test]
fn cratesio_manifest_accepts_version_dependencies() {
let temp = TempDir::new().unwrap();
std::fs::write(
temp.path().join("Cargo.toml"),
"[dependencies]\nfoo = \"0.1\"\n",
)
.unwrap();
ensure_cratesio_manifest_clean(temp.path()).unwrap();
}
#[test]
fn cratesio_manifest_allows_component_metadata_target() {
let temp = TempDir::new().unwrap();
std::fs::write(
temp.path().join("Cargo.toml"),
r#"[package]
name = "demo"
version = "0.1.0"
[package.metadata.component.target]
world = "greentic:component/component@0.6.0"
"#,
)
.unwrap();
ensure_cratesio_manifest_clean(temp.path()).unwrap();
}
}