use anyhow::{Context as _, Result};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anodizer_core::artifact::ArtifactRegistry;
use anodizer_core::config::{Config, WorkspaceConfig};
use anodizer_core::context::Context;
use anodizer_core::git::short_commit_str;
use anodizer_core::log::StageLogger;
#[derive(Debug)]
pub(super) enum DistLayout {
Flat,
PerCrate(Vec<String>),
Ambiguous { crate_subdirs: Vec<String> },
}
pub(super) fn detect_dist_layout(dist: &Path, log: &StageLogger) -> Result<DistLayout> {
let has_flat = !discover_sharded_manifests(dist, anodizer_core::dist::CONTEXT_JSON)?.is_empty();
let mut crate_subdirs: Vec<String> = Vec::new();
let entries = std::fs::read_dir(dist).with_context(|| {
format!(
"publish-only: reading dist directory {} to detect layout",
dist.display()
)
})?;
for entry in entries {
let entry = entry?;
let is_dir = match entry.file_type() {
Ok(t) => t.is_dir(),
Err(e) => {
log.verbose(&format!(
"stat of dist entry {} failed: {e}; treating as non-directory",
entry.path().display()
));
false
}
};
if !is_dir {
continue;
}
let subdir = entry.path();
if !discover_sharded_manifests(&subdir, anodizer_core::dist::CONTEXT_JSON)?.is_empty()
&& let Some(name) = entry.file_name().to_str()
{
crate_subdirs.push(name.to_string());
}
}
crate_subdirs.sort();
match (has_flat, crate_subdirs.is_empty()) {
(_, true) => Ok(DistLayout::Flat),
(false, false) => Ok(DistLayout::PerCrate(crate_subdirs)),
(true, false) => Ok(DistLayout::Ambiguous { crate_subdirs }),
}
}
pub(super) fn crate_subdir_has_manifest(dist: &Path, crate_name: &str, log: &StageLogger) -> bool {
let subdir = dist.join(crate_name);
if !subdir.is_dir() {
return false;
}
match discover_sharded_manifests(&subdir, anodizer_core::dist::CONTEXT_JSON) {
Ok(manifests) => !manifests.is_empty(),
Err(e) => {
log.verbose(&format!(
"failed to scan {} for context manifests: {e}; \
treating crate '{crate_name}' as having no per-crate subdir",
subdir.display()
));
false
}
}
}
pub(super) struct RunOpts {
pub dry_run: bool,
}
pub(super) fn run(
ctx: &mut Context,
config: &Config,
log: &StageLogger,
opts: RunOpts,
) -> Result<()> {
log.status("running in publish-only mode (load preserved dist + sign + publish)...");
let dist = config.dist.clone();
run_one_crate_dist(ctx, config, log, &opts, dist)
}
pub(super) fn run_per_crate(
ctx: &mut Context,
config: &Config,
log: &StageLogger,
opts: RunOpts,
dist_base: PathBuf,
crate_order: Vec<String>,
) -> Result<()> {
log.status(&format!(
"iterating {} crate(s) in per-crate publish-only mode — {}",
crate_order.len(),
crate_order.join(", ")
));
let workspace_for: HashMap<String, &WorkspaceConfig> = config
.workspaces
.as_deref()
.map(|ws_list| {
let mut idx = HashMap::new();
for ws in ws_list {
for c in &ws.crates {
idx.insert(c.name.clone(), ws);
}
}
idx
})
.unwrap_or_default();
let mut guard = PerCrateOverlayGuard::capture(ctx);
let baseline_skip_stages = guard.snapshot_skip_stages().to_vec();
for crate_name in &crate_order {
let crate_dist = dist_base.join(crate_name);
log.status(&format!(
"publishing crate '{crate_name}' from {}",
crate_dist.display()
));
guard.reset_overlay_fields();
guard.reset_version_vars();
guard.reset_release_url();
let ctx = guard.ctx_mut();
ctx.artifacts = ArtifactRegistry::new();
ctx.publish_report = None;
ctx.publish_attempted = false;
ctx.verify_release = None;
ctx.options.skip_stages = baseline_skip_stages.clone();
if let Some(ws) = workspace_for.get(crate_name.as_str()) {
crate::commands::helpers::apply_workspace_overlay(&mut ctx.config, ws);
merge_workspace_skip(&mut ctx.options.skip_stages, &ws.skip);
}
ctx.config.dist = crate_dist.clone();
ctx.options.selected_crates = vec![crate_name.clone()];
apply_per_crate_version(ctx, &crate_dist, crate_name, log);
apply_per_crate_tag(ctx, config, crate_name, log);
let per_crate_opts = RunOpts {
dry_run: opts.dry_run,
};
run_per_crate_lifecycle_hooks(ctx, crate_name, HookKind::Before, opts.dry_run, log)?;
run_one_crate_dist(ctx, config, log, &per_crate_opts, crate_dist)?;
run_per_crate_lifecycle_hooks(ctx, crate_name, HookKind::After, opts.dry_run, log)?;
}
drop(guard);
Ok(())
}
mod per_crate;
mod preserved;
use per_crate::*;
use preserved::*;
#[cfg(test)]
mod tests;