1use super::*;
2
3async fn discover_packages(
16 workspace_manifest: &Path,
17) -> Result<(Vec<Package>, bool), PublishError> {
18 let content: String = read_to_string(workspace_manifest).await?;
19 let doc: Value = toml::from_str(&content).map_err(|_| PublishError::ManifestParseError)?;
20 let workspace_version: Option<String> = doc
21 .get("workspace")
22 .and_then(|workspace: &Value| workspace.get("package"))
23 .and_then(|package: &Value| package.get("version"))
24 .and_then(|version: &Value| version.as_str())
25 .map(|version: &str| version.to_string());
26 let mut packages: Vec<Package> = Vec::new();
27 if let Some(workspace) = doc.get("workspace")
28 && let Some(members) = workspace
29 .get("members")
30 .and_then(|members_value: &Value| members_value.as_array())
31 {
32 for member in members {
33 if let Some(pattern) = member.as_str() {
34 let base_path: &Path = workspace_manifest.parent().unwrap_or(workspace_manifest);
35 expand_pattern(
36 base_path,
37 pattern,
38 &mut packages,
39 workspace_version.as_deref(),
40 )
41 .await?;
42 }
43 }
44 }
45 let has_root_package: bool = doc.get("package").is_some();
46 if has_root_package {
47 let root_package: Package =
48 read_package_manifest(workspace_manifest, workspace_version.as_deref()).await?;
49 packages.push(root_package);
50 }
51 Ok((packages, has_root_package))
52}
53
54async fn expand_pattern(
67 base_path: &Path,
68 pattern: &str,
69 packages: &mut Vec<Package>,
70 workspace_version: Option<&str>,
71) -> Result<(), PublishError> {
72 if pattern.contains('*') {
73 let parent: &Path = Path::new(pattern).parent().unwrap_or(Path::new("."));
74 let full_parent: PathBuf = base_path.join(parent);
75 if full_parent.is_dir() {
76 let mut entries: ReadDir = read_dir(&full_parent).await?;
77 while let Some(entry) = entries.next_entry().await? {
78 let path: PathBuf = entry.path();
79 if path.is_dir() {
80 let cargo_toml: PathBuf = path.join("Cargo.toml");
81 if cargo_toml.exists() {
82 let package: Package =
83 read_package_manifest(&cargo_toml, workspace_version).await?;
84 packages.push(package);
85 }
86 }
87 }
88 }
89 } else {
90 let cargo_toml: PathBuf = base_path.join(pattern).join("Cargo.toml");
91 if cargo_toml.exists() {
92 let package: Package = read_package_manifest(&cargo_toml, workspace_version).await?;
93 packages.push(package);
94 }
95 }
96 Ok(())
97}
98
99async fn read_package_manifest(
113 manifest_path: &Path,
114 workspace_version: Option<&str>,
115) -> Result<Package, PublishError> {
116 let content: String = read_to_string(manifest_path).await?;
117 let doc: Value = toml::from_str(&content).map_err(|_| PublishError::ManifestParseError)?;
118 let package_table: &Value = doc.get("package").ok_or(PublishError::ManifestParseError)?;
119 let name: String = package_table
120 .get("name")
121 .and_then(|n: &Value| n.as_str())
122 .ok_or(PublishError::ManifestParseError)?
123 .to_string();
124 let version: String = match package_table.get("version") {
125 Some(version_value) => {
126 if let Some(version_str) = version_value.as_str() {
127 version_str.to_string()
128 } else if version_value
129 .get("workspace")
130 .and_then(|workspace_value: &Value| workspace_value.as_bool())
131 .unwrap_or(false)
132 {
133 workspace_version
134 .ok_or(PublishError::ManifestParseError)?
135 .to_string()
136 } else {
137 return Err(PublishError::ManifestParseError);
138 }
139 }
140 None => workspace_version
141 .ok_or(PublishError::ManifestParseError)?
142 .to_string(),
143 };
144 let publish: bool = package_table
145 .get("publish")
146 .and_then(|publish_value: &Value| publish_value.as_bool())
147 .unwrap_or(true);
148 let path: PathBuf = manifest_path
149 .parent()
150 .filter(|p: &&Path| !p.as_os_str().is_empty())
151 .map_or_else(|| PathBuf::from("."), |p: &Path| p.to_path_buf());
152 let local_dependencies: Vec<String> = extract_local_dependencies(&doc, manifest_path)?;
153 Ok(Package {
154 name,
155 version,
156 path,
157 local_dependencies,
158 publish,
159 })
160}
161
162fn extract_local_dependencies(
179 doc: &Value,
180 _manifest_path: &Path,
181) -> Result<Vec<String>, PublishError> {
182 let mut deps: Vec<String> = Vec::new();
183 let dep_sections: [&str; 3] = ["dependencies", "build-dependencies", "dev-dependencies"];
184 for section in &dep_sections {
185 if let Some(table) = doc
186 .get(section)
187 .and_then(|section_value: &Value| section_value.as_table())
188 {
189 for (dep_name, dep_value) in table {
190 let is_local: bool = match dep_value {
191 Value::Table(t) => {
192 let has_path_or_workspace: bool = t.get("path").is_some()
193 || t.get("workspace")
194 .and_then(|workspace_value: &Value| workspace_value.as_bool())
195 .unwrap_or(false);
196 let versioned: bool = t.get("version").is_some();
197 has_path_or_workspace && (*section != "dev-dependencies" || versioned)
198 }
199 _ => false,
200 };
201 if is_local {
202 deps.push(dep_name.clone());
203 }
204 }
205 }
206 }
207 Ok(deps)
208}
209
210fn validate_publish_order(packages: &[Package]) -> Result<(), PublishError> {
223 let position: HashMap<String, usize> = packages
224 .iter()
225 .enumerate()
226 .map(|(index, package): (usize, &Package)| (package.name.clone(), index))
227 .collect();
228 for package in packages {
229 let Some(package_position) = position.get(&package.name) else {
230 continue;
231 };
232 for dep in &package.local_dependencies {
233 if let Some(dep_position) = position.get(dep)
234 && dep_position > package_position
235 {
236 return Err(PublishError::InvalidPublishOrder(format!(
237 "{} depends on {} but is listed before it in [workspace.members]",
238 package.name, dep
239 )));
240 }
241 }
242 }
243 Ok(())
244}
245
246fn position_root_package(packages: &mut Vec<Package>) -> Result<(), PublishError> {
265 let Some(root) = packages.pop() else {
266 return Ok(());
267 };
268 let earliest_dependent: Option<usize> = packages
269 .iter()
270 .enumerate()
271 .filter(|(_, package)| package.local_dependencies.contains(&root.name))
272 .map(|(index, _)| index)
273 .min();
274 let Some(earliest) = earliest_dependent else {
275 packages.push(root);
276 return Ok(());
277 };
278 let member_positions: HashMap<&str, usize> = packages
279 .iter()
280 .enumerate()
281 .map(|(index, package)| (package.name.as_str(), index))
282 .collect();
283 if let Some(max_dep) = root
284 .local_dependencies
285 .iter()
286 .filter_map(|dep| member_positions.get(dep.as_str()))
287 .max()
288 && max_dep >= &earliest
289 {
290 return Err(PublishError::InvalidPublishOrder(format!(
291 "{} must publish after its dependency at members position {} but before dependent at position {}; reorder [workspace.members]",
292 root.name, max_dep, earliest
293 )));
294 }
295 packages.insert(earliest, root);
296 Ok(())
297}
298
299pub async fn resolve_publish_order(manifest_path: &str) -> Result<Vec<Package>, PublishError> {
312 let workspace_manifest: &Path = Path::new(manifest_path);
313 let (mut packages, has_root_package) = discover_packages(workspace_manifest).await?;
314 if has_root_package {
315 position_root_package(&mut packages)?;
316 }
317 validate_publish_order(&packages)?;
318 Ok(packages)
319}
320
321pub fn is_already_published(stderr: &str) -> bool {
333 stderr.contains("already been uploaded")
334 || stderr.contains("is already published")
335 || stderr.contains("already exists on crates.io index")
336}
337
338async fn publish_package_with_retry(package: &Package, max_retries: u32) -> PublishResult {
349 let mut attempt: u32 = 0;
350 let mut last_error: Option<String> = None;
351 while attempt <= max_retries {
352 match publish_single_package(package).await {
353 Ok(()) => {
354 return PublishResult {
355 package_name: package.name.clone(),
356 success: true,
357 error: None,
358 retries: attempt,
359 };
360 }
361 Err(error) => {
362 last_error = Some(error.to_string());
363 attempt += 1;
364 if attempt <= max_retries {
365 sleep(Duration::from_secs(2_u64.pow(attempt))).await;
366 }
367 }
368 }
369 }
370 PublishResult {
371 package_name: package.name.clone(),
372 success: false,
373 error: last_error,
374 retries: attempt - 1,
375 }
376}
377
378async fn publish_single_package(package: &Package) -> Result<(), Box<dyn std::error::Error>> {
388 let output: std::process::Output = Command::new("cargo")
389 .arg("publish")
390 .arg("--allow-dirty")
391 .arg("--no-verify")
392 .current_dir(&package.path)
393 .stdout(Stdio::piped())
394 .stderr(Stdio::piped())
395 .output()
396 .await?;
397 if output.status.success() {
398 return Ok(());
399 }
400 let stderr: String = String::from_utf8_lossy(&output.stderr).to_string();
401 if is_already_published(&stderr) {
402 log::info!("{} is already published, treating as success", package.name);
403 return Ok(());
404 }
405 Err(stderr.into())
406}
407
408pub async fn execute_publish(
423 manifest_path: &str,
424 max_retries: u32,
425) -> Result<Vec<PublishResult>, PublishError> {
426 let path: &Path = Path::new(manifest_path);
427 let path: &Path = match path.parent() {
428 Some(parent) if !parent.as_os_str().is_empty() => parent,
429 _ => Path::new("."),
430 };
431 let workspace_manifest: PathBuf = path.join("Cargo.toml");
432 let sync_report: SyncReport =
433 match execute_sync(workspace_manifest.to_str().unwrap_or("Cargo.toml")).await {
434 Ok(report) => report,
435 Err(error) => return Err(PublishError::SyncFailed(error)),
436 };
437 if sync_report.file_changed {
438 log::info!(
439 "publish: synced workspace dependencies ({} renamed, {} versioned) to v{}",
440 sync_report.renamed_entries.len(),
441 sync_report.versioned_entries.len(),
442 sync_report.workspace_version,
443 );
444 }
445 let ordered_packages: Vec<Package> =
446 resolve_publish_order(workspace_manifest.to_str().unwrap_or("Cargo.toml")).await?;
447 if ordered_packages.is_empty() {
448 return Ok(Vec::new());
449 }
450 let mut results: Vec<PublishResult> = Vec::new();
451 for package in ordered_packages {
452 if !package.publish {
453 log::info!("Skipping {} (publish = false)", package.name);
454 continue;
455 }
456 log::info!("Publishing {} v{}...", package.name, package.version);
457 let result: PublishResult = publish_package_with_retry(&package, max_retries).await;
458 if result.success {
459 if result.retries == 0 {
460 log::info!("Successfully published {}", result.package_name,);
461 } else {
462 log::info!(
463 "Successfully published {} (retried {} times)",
464 result.package_name,
465 result.retries
466 );
467 }
468 } else if let Some(error) = &result.error {
469 log::error!("Failed to publish {}: {error}", result.package_name);
470 } else {
471 log::error!("Failed to publish {}", result.package_name);
472 }
473 results.push(result);
474 }
475 Ok(results)
476}