#![doc = include_str!("../docs/PUBLISHER.md")]
use crate::errors::*;
use crate::utils;
use crate::utils::{ask_for_choice, get_line_index, parse_toml};
use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
struct CrateToPublish {
name: String,
path: String,
prefix: String,
published_prefix: String,
dir: PathBuf,
}
pub fn publish_crates(file_name: &str, dir: &str, timeout: u64, accept_all: bool, simulation: bool) -> Result<()> {
let file_path = Path::new(file_name);
let working_dir = utils::canonicalize(Path::new(dir))?;
let workspace_manifest_file = utils::canonicalize(working_dir.join(file_path))?;
let workspace_manifest_path = workspace_manifest_file.as_path();
let mut workspace_maifest_content = utils::read_file(workspace_manifest_path)?;
let workspace_manifest_toml = parse_toml(workspace_manifest_path)?;
let Some(workspace) = workspace_manifest_toml.get("workspace") else {
return Err(MaggError::new(format!(
"missing [workspace] section in manifest file: {}",
workspace_manifest_path.display()
)));
};
let Some(workspace_package) = workspace.get("package") else {
return Err(MaggError::new("missing [workspace.package] section"));
};
let Some(workspace_package_version) = workspace_package.get("version") else {
return Err(MaggError::new("missing 'version' in [workspace.package]"));
};
let Some(publish_version) = workspace_package_version.as_str() else {
return Err(MaggError::new("invalid 'version' in [workspace.package]"));
};
let Some(workspace_dependencies) = workspace.get("dependencies") else {
return Err(MaggError::new("missing [workspace.dependencies] section"));
};
let Some(workspace_dependencies_table) = workspace_dependencies.as_table() else {
return Err(MaggError::new("[workspace.dependencies] section is not a table"));
};
let mut crates_to_publish = BTreeMap::new();
for (key, value) in workspace_dependencies_table {
if value.get("path").is_some() {
let name = key.to_string();
if value.get("version").is_some() {
return Err(MaggError::new(format!("dependency '{name}' may not have 'version' set")));
}
let path = utils::strip_quotes(value["path"].as_str().unwrap()).to_string();
let prefix = format!("{} = {{ path = \"{}\"", name, path);
let published_prefix = format!("{} = {{ version = \"{}\"", name, publish_version);
let dir = utils::canonicalize(working_dir.join(Path::new(&path)))?;
let Some(line) = get_line_index(&workspace_maifest_content, &prefix) else {
return Err(MaggError::new(format!("invalid formatting for dependency '{name}'")));
};
crates_to_publish.insert(
line,
CrateToPublish {
name,
path,
prefix,
published_prefix,
dir,
},
);
}
}
if crates_to_publish.is_empty() {
return Err(MaggError::new("no crates to publish"));
}
let publish_names: Vec<String> = crates_to_publish.values().map(|crate_to_publish| crate_to_publish.name.clone()).collect();
for crate_to_publish in crates_to_publish.values() {
let name = crate_to_publish.name.to_string();
let crate_manifest_file = utils::canonicalize(working_dir.join(Path::new(&crate_to_publish.path)).join(file_path))?;
let crate_manifest_toml = parse_toml(crate_manifest_file)?;
let Some(crate_package) = crate_manifest_toml.get("package") else {
return Err(MaggError::new(format!("missing [package] section in dependency '{name}'")));
};
let Some(crate_package_name) = crate_package.get("name") else {
return Err(MaggError::new(format!("missing [package].name attribute in dependency '{name}'")));
};
let Some(package_name) = crate_package_name.as_str() else {
return Err(MaggError::new(format!("invalid [package].name attribute in dependency '{name}'")));
};
if crate_to_publish.name != package_name {
return Err(MaggError::new(format!("expected 'name = \"{name}\"', actual 'name = \"{}\"'", package_name)));
}
let Some(crate_package_version) = crate_package.get("version") else {
return Err(MaggError::new(format!("missing [package].version attribute in dependency '{name}'")));
};
let Some(crate_package_version_workspace) = crate_package_version.get("workspace") else {
return Err(MaggError::new(format!("missing [package].version.workspace attribute in dependency '{name}'")));
};
let Some(crate_package_version_workspace_value) = crate_package_version_workspace.as_bool() else {
return Err(MaggError::new(format!("invalid [package].version.workspace attribute in dependency '{name}'")));
};
if !crate_package_version_workspace_value {
return Err(MaggError::new(format!("[package].version.workspace attribute in crate '{name}' must have value \"true\"")));
}
validate_crate_dependencies(crate_package, "dependencies", &name, &publish_names)?;
validate_crate_dependencies(crate_package, "dev-dependencies", &name, &publish_names)?;
}
println!();
println!("Publish version: {}", publish_version);
if !ask_for_choice("Is this version correct?", accept_all)? {
return Ok(());
}
println!();
println!("Publish crates:");
for crate_to_publish in crates_to_publish.values() {
println!("{} v{} {}", crate_to_publish.name, publish_version, crate_to_publish.dir.display());
}
println!();
if !ask_for_choice("Do you want to publish all these crates?", accept_all)? {
return Ok(());
}
for crate_to_publish in crates_to_publish.values() {
println!(
"\nCrate (dry-run):\n {}\n v{}\n {}",
crate_to_publish.name,
publish_version,
crate_to_publish.dir.display()
);
if ask_for_choice("Perform dry-run before publishing this crate?", accept_all)? {
if simulation {
execute_command("echo", ["simulating <dry-run>"], crate_to_publish.dir.clone())?;
} else {
execute_command("cargo", ["publish", "--dry-run", "--color=always"], crate_to_publish.dir.clone())?;
}
}
println!(
"\nCrate (publish):\n {}\n v{}\n {}",
crate_to_publish.name,
publish_version,
crate_to_publish.dir.display()
);
if ask_for_choice("Publish this crate?", accept_all)? {
if simulation {
execute_command("echo", ["simulating <publish>"], crate_to_publish.dir.clone())?;
} else {
execute_command("cargo", ["publish", "--color=always"], crate_to_publish.dir.clone())?;
}
}
if timeout > 0 && !simulation {
print!("Waiting {} second(s), ", timeout);
let one_second = std::time::Duration::new(1, 0);
for _ in 0..timeout {
utils::step_progress();
std::thread::sleep(one_second);
}
}
workspace_maifest_content = workspace_maifest_content.replace(&crate_to_publish.prefix, &crate_to_publish.published_prefix);
utils::write_file(workspace_manifest_path, &workspace_maifest_content);
}
Ok(())
}
fn validate_crate_dependencies(package: &toml::Value, dependencies: &str, crate_name: &str, publish_names: &[String]) -> Result<()> {
if let Some(crate_package_dependencies) = package.get(dependencies) {
let Some(crate_dependencies_table) = crate_package_dependencies.as_table() else {
return Err(MaggError::new(format!("[package.dependencies] section is not a table in crate {crate_name}")));
};
for (key, value) in crate_dependencies_table {
if publish_names.contains(key) {
let Some(crate_dependency_workspace) = value.get("workspace") else {
return Err(MaggError::new(format!("missing [package.dependencies].{key}.workspace attribute in crate '{crate_name}'")));
};
let Some(crate_dependency_workspace_value) = crate_dependency_workspace.as_bool() else {
return Err(MaggError::new(format!("invalid [package.dependencies].{key}.workspace attribute in crate '{crate_name}'")));
};
if !crate_dependency_workspace_value {
return Err(MaggError::new(format!(
"[package.dependencies].{key}.workspace attribute in crate '{crate_name}' must have value \"true\""
)));
}
}
}
};
Ok(())
}
fn execute_command<S, A, P>(program: S, args: A, dir: P) -> Result<()>
where
S: AsRef<OsStr>,
A: IntoIterator<Item = S>,
P: AsRef<Path>,
{
let mut command = std::process::Command::new(program);
let mut child = command
.args(args)
.current_dir(dir)
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.spawn()
.map_err(|e| MaggError::new(e.to_string()))?;
let exit_status = child.wait().map_err(|e| MaggError::new(e.to_string()))?;
if !exit_status.success() {
return Err(MaggError::new(format!("executing command failed with status code: {}", exit_status)));
}
Ok(())
}