use anyhow::{Context, Result, bail};
use repolith_actions::cargo_install::{CargoInstall, CargoSource};
use repolith_actions::docker::DockerBuild;
use repolith_actions::git_clone::GitClone;
use repolith_actions::paths::expand_tilde;
use repolith_cache::SqliteCache;
use repolith_core::action::Action;
use repolith_core::cache::Cache;
use repolith_core::manifest::{ActionEntry, Manifest, NodeEntry, action_kind};
use repolith_core::types::{ActionId, BuildError, ExecMode};
use repolith_engine::federation::{FederationHooks, RepolithSync};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Weak};
use tokio::sync::Semaphore;
fn default_install_to() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".repolith")
}
pub struct FactoryCtx {
pub base_dir: PathBuf,
pub chain: Vec<PathBuf>,
pub mode: ExecMode,
pub sem: Arc<Semaphore>,
pub hooks: Arc<dyn FederationHooks>,
}
fn resolve_node_path(base_dir: &Path, p: &Path) -> PathBuf {
base_dir.join(expand_tilde(p))
}
pub fn build_actions_from_manifest(
manifest: &Manifest,
fctx: &FactoryCtx,
) -> Result<Vec<Box<dyn Action>>> {
let mut out: Vec<Box<dyn Action>> = Vec::new();
for node in &manifest.nodes {
let mut prev_id: Option<ActionId> = None;
for (idx, entry) in node.actions.iter().enumerate() {
let kind = action_kind(entry);
let id = ActionId(format!("{}::{}::{}", node.id, kind, idx));
let deps = prev_id.iter().cloned().collect::<Vec<_>>();
out.push(action_for_entry(node, entry, id.clone(), deps, fctx)?);
prev_id = Some(id);
}
}
Ok(out)
}
fn required_node_path(node: &NodeEntry, fctx: &FactoryCtx, what: &str) -> Result<PathBuf> {
node.path
.as_deref()
.map(|p| resolve_node_path(&fctx.base_dir, p))
.with_context(|| format!("node `{}`: {what}", node.id))
}
fn action_for_entry(
node: &NodeEntry,
entry: &ActionEntry,
id: ActionId,
deps: Vec<ActionId>,
fctx: &FactoryCtx,
) -> Result<Box<dyn Action>> {
Ok(match entry {
ActionEntry::GitClone => {
let url = node.git.clone().with_context(|| {
format!(
"node `{}`: git-clone action requires `git` URL on the node",
node.id
)
})?;
let path = node.path.as_deref().map_or_else(
|| fctx.base_dir.join(&node.id),
|p| resolve_node_path(&fctx.base_dir, p),
);
Box::new(GitClone {
id,
repo_url: url,
path,
deps,
})
}
ActionEntry::CargoInstall {
crate_name,
features,
install_to,
} => {
let source = if let Some(p) = &node.path {
CargoSource::Path {
path: resolve_node_path(&fctx.base_dir, p),
}
} else if let Some(url) = &node.git {
CargoSource::Git {
url: url.clone(),
branch: None,
}
} else {
bail!(
"node `{}`: cargo-install action requires either `git` or `path` on the node",
node.id
);
};
Box::new(CargoInstall {
id,
source,
crate_name: crate_name.clone().or_else(|| Some(node.id.clone())),
features: features.clone(),
install_to: install_to
.as_deref()
.map_or_else(default_install_to, expand_tilde),
deps,
})
}
ActionEntry::Docker {
tag,
dockerfile,
context,
} => {
let node_path = required_node_path(
node,
fctx,
"docker action requires `path` on the node (build context base)",
)?;
Box::new(DockerBuild {
id,
tag: tag.clone(),
dockerfile: dockerfile.clone(),
context: context.clone(),
node_path,
deps,
})
}
ActionEntry::Repolith { manifest: rel } => {
let node_path = required_node_path(
node,
fctx,
"repolith action requires `path` on the node (child stack root)",
)?;
Box::new(RepolithSync {
id,
node_path,
manifest_rel: rel.clone(),
chain: fctx.chain.clone(),
mode: fctx.mode,
shared_sem: Arc::clone(&fctx.sem),
hooks: Arc::clone(&fctx.hooks),
deps,
})
}
})
}
pub struct CliFederationHooks {
mode: ExecMode,
sem: Arc<Semaphore>,
me: Weak<CliFederationHooks>,
}
impl CliFederationHooks {
pub fn new(mode: ExecMode, sem: Arc<Semaphore>) -> Arc<Self> {
Arc::new_cyclic(|me| Self {
mode,
sem,
me: me.clone(),
})
}
}
impl FederationHooks for CliFederationHooks {
fn build_actions(
&self,
manifest: &Manifest,
stack_dir: &Path,
chain: &[PathBuf],
) -> std::result::Result<Vec<Box<dyn Action>>, BuildError> {
let hooks = self
.me
.upgrade()
.expect("hooks alive while an action holds them");
let fctx = FactoryCtx {
base_dir: stack_dir.to_path_buf(),
chain: chain.to_vec(),
mode: self.mode,
sem: Arc::clone(&self.sem),
hooks,
};
build_actions_from_manifest(manifest, &fctx)
.map_err(|e| BuildError::Io(format!("child factory ({}): {e:#}", stack_dir.display())))
}
fn open_cache(&self, stack_dir: &Path) -> std::result::Result<Box<dyn Cache>, BuildError> {
let dir = stack_dir.join(".repolith");
std::fs::create_dir_all(&dir)
.map_err(|e| BuildError::Io(format!("create {}: {e}", dir.display())))?;
let path = dir.join("cache.db");
let cache = SqliteCache::open(&path)
.map_err(|e| BuildError::Io(format!("open child cache {}: {e}", path.display())))?;
Ok(Box::new(cache))
}
}