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