hyperlane_cli/publish/
fn.rs1use super::*;
2
3async fn discover_packages(workspace_root: &Path) -> Result<Vec<Package>, PublishError> {
13 let content: String = read_to_string(workspace_root).await?;
14 let doc: Value = toml::from_str(&content).map_err(|_| PublishError::ManifestParseError)?;
15 let mut packages: Vec<Package> = Vec::new();
16 if let Some(workspace) = doc.get("workspace")
17 && let Some(members) = workspace
18 .get("members")
19 .and_then(|members_value: &Value| members_value.as_array())
20 {
21 for member in members {
22 if let Some(pattern) = member.as_str() {
23 let base_path: &Path = workspace_root.parent().unwrap_or(workspace_root);
24 expand_pattern(base_path, pattern, &mut packages).await?;
25 }
26 }
27 }
28 if packages.is_empty() {
29 let package: Package = read_single_package(workspace_root).await?;
30 packages.push(package);
31 }
32 Ok(packages)
33}
34
35async fn expand_pattern(
47 base_path: &Path,
48 pattern: &str,
49 packages: &mut Vec<Package>,
50) -> Result<(), PublishError> {
51 if pattern.contains('*') {
52 let parent: &Path = Path::new(pattern).parent().unwrap_or(Path::new("."));
53 let full_parent: PathBuf = base_path.join(parent);
54 if full_parent.is_dir() {
55 let mut entries: ReadDir = read_dir(&full_parent).await?;
56 while let Some(entry) = entries.next_entry().await? {
57 let path: PathBuf = entry.path();
58 if path.is_dir() {
59 let cargo_toml: PathBuf = path.join("Cargo.toml");
60 if cargo_toml.exists() {
61 let package: Package = read_package_manifest(&cargo_toml).await?;
62 packages.push(package);
63 }
64 }
65 }
66 }
67 } else {
68 let cargo_toml: PathBuf = base_path.join(pattern).join("Cargo.toml");
69 if cargo_toml.exists() {
70 let package: Package = read_package_manifest(&cargo_toml).await?;
71 packages.push(package);
72 }
73 }
74 Ok(())
75}
76
77async fn read_single_package(manifest_path: &Path) -> Result<Package, PublishError> {
87 read_package_manifest(manifest_path).await
88}
89
90async fn read_package_manifest(manifest_path: &Path) -> Result<Package, PublishError> {
100 let content: String = read_to_string(manifest_path).await?;
101 let doc: Value = toml::from_str(&content).map_err(|_| PublishError::ManifestParseError)?;
102 let package_table: &Value = doc.get("package").ok_or(PublishError::ManifestParseError)?;
103 let name: String = package_table
104 .get("name")
105 .and_then(|n: &Value| n.as_str())
106 .ok_or(PublishError::ManifestParseError)?
107 .to_string();
108 let version: String = package_table
109 .get("version")
110 .and_then(|v: &Value| v.as_str())
111 .ok_or(PublishError::ManifestParseError)?
112 .to_string();
113 let path: PathBuf = manifest_path
114 .parent()
115 .filter(|p: &&Path| !p.as_os_str().is_empty())
116 .map_or_else(|| PathBuf::from("."), |p: &Path| p.to_path_buf());
117 let local_dependencies: Vec<String> = extract_local_dependencies(&doc, manifest_path)?;
118 Ok(Package {
119 name,
120 version,
121 path,
122 local_dependencies,
123 })
124}
125
126fn extract_local_dependencies(
137 doc: &Value,
138 _manifest_path: &Path,
139) -> Result<Vec<String>, PublishError> {
140 let mut deps: Vec<String> = Vec::new();
141 let dep_sections: [&str; 3] = ["dependencies", "dev-dependencies", "build-dependencies"];
142 for section in &dep_sections {
143 if let Some(table) = doc
144 .get(section)
145 .and_then(|section_value: &Value| section_value.as_table())
146 {
147 for (dep_name, dep_value) in table {
148 let is_local: bool = match dep_value {
149 Value::Table(t) => {
150 t.get("path").is_some()
151 || t.get("workspace")
152 .and_then(|workspace_value: &Value| workspace_value.as_bool())
153 .unwrap_or(false)
154 }
155 _ => false,
156 };
157 if is_local {
158 deps.push(dep_name.clone());
159 }
160 }
161 }
162 }
163 Ok(deps)
164}
165
166fn topological_sort(packages: &[Package]) -> Result<Vec<Package>, PublishError> {
176 let mut in_degree: HashMap<String, usize> = HashMap::new();
177 let mut graph: HashMap<String, Vec<String>> = HashMap::new();
178 let package_map: HashMap<String, Package> = packages
179 .iter()
180 .map(|package: &Package| (package.name.clone(), package.clone()))
181 .collect();
182 for package in packages {
183 in_degree.entry(package.name.clone()).or_insert(0);
184 for dep in &package.local_dependencies {
185 if package_map.contains_key(dep) {
186 graph
187 .entry(dep.clone())
188 .or_default()
189 .push(package.name.clone());
190 *in_degree.entry(package.name.clone()).or_insert(0) += 1;
191 }
192 }
193 }
194 let mut queue: VecDeque<String> = VecDeque::new();
195 for (name, degree) in &in_degree {
196 if *degree == 0 {
197 queue.push_back(name.clone());
198 }
199 }
200 let mut result: Vec<Package> = Vec::new();
201 while let Some(name) = queue.pop_front() {
202 if let Some(package) = package_map.get(&name) {
203 result.push(package.clone());
204 }
205 if let Some(dependents) = graph.get(&name) {
206 for dependent in dependents {
207 if let Some(degree) = in_degree.get_mut(dependent) {
208 *degree -= 1;
209 if *degree == 0 {
210 queue.push_back(dependent.clone());
211 }
212 }
213 }
214 }
215 }
216 if result.len() != packages.len() {
217 return Err(PublishError::CircularDependency);
218 }
219 Ok(result)
220}
221
222async fn publish_package_with_retry(package: &Package, max_retries: u32) -> PublishResult {
233 let mut attempt: u32 = 0;
234 let mut last_error: Option<String> = None;
235 while attempt <= max_retries {
236 match publish_single_package(package).await {
237 Ok(()) => {
238 return PublishResult {
239 package_name: package.name.clone(),
240 success: true,
241 error: None,
242 retries: attempt,
243 };
244 }
245 Err(error) => {
246 last_error = Some(error.to_string());
247 attempt += 1;
248 if attempt <= max_retries {
249 sleep(Duration::from_secs(2_u64.pow(attempt))).await;
250 }
251 }
252 }
253 }
254 PublishResult {
255 package_name: package.name.clone(),
256 success: false,
257 error: last_error,
258 retries: attempt - 1,
259 }
260}
261
262async fn publish_single_package(package: &Package) -> Result<(), Box<dyn std::error::Error>> {
272 let output: std::process::Output = Command::new("cargo")
273 .arg("publish")
274 .arg("--allow-dirty")
275 .current_dir(&package.path)
276 .stdout(Stdio::piped())
277 .stderr(Stdio::piped())
278 .output()
279 .await?;
280 if output.status.success() {
281 Ok(())
282 } else {
283 let stderr: String = String::from_utf8_lossy(&output.stderr).to_string();
284 Err(stderr.into())
285 }
286}
287
288pub async fn execute_publish(
299 manifest_path: &str,
300 max_retries: u32,
301) -> Result<Vec<PublishResult>, PublishError> {
302 let path: &Path = Path::new(manifest_path);
303 let packages: Vec<Package> = discover_packages(path).await?;
304 if packages.is_empty() {
305 return Ok(Vec::new());
306 }
307 let sorted_packages: Vec<Package> = topological_sort(&packages)?;
308 let mut results: Vec<PublishResult> = Vec::new();
309 for package in sorted_packages {
310 log::info!("Publishing {} v{}...", package.name, package.version);
311 let result: PublishResult = publish_package_with_retry(&package, max_retries).await;
312 if result.success {
313 if result.retries == 0 {
314 log::info!("Successfully published {}", result.package_name,);
315 } else {
316 log::info!(
317 "Successfully published {} (retried {} times)",
318 result.package_name,
319 result.retries
320 );
321 }
322 } else if let Some(error) = &result.error {
323 log::error!("Failed to publish {}: {error}", result.package_name);
324 } else {
325 log::error!("Failed to publish {}", result.package_name);
326 }
327 results.push(result);
328 }
329 Ok(results)
330}