diff options
Diffstat (limited to 'release/src/main.rs')
| -rw-r--r-- | release/src/main.rs | 514 |
1 files changed, 0 insertions, 514 deletions
diff --git a/release/src/main.rs b/release/src/main.rs deleted file mode 100644 index 22c926b86..000000000 --- a/release/src/main.rs +++ /dev/null | |||
| @@ -1,514 +0,0 @@ | |||
| 1 | use std::collections::{BTreeMap, HashMap}; | ||
| 2 | use std::fs; | ||
| 3 | use std::path::{Path, PathBuf}; | ||
| 4 | use std::process::Command as ProcessCommand; | ||
| 5 | |||
| 6 | use anyhow::{anyhow, bail, Result}; | ||
| 7 | use cargo_semver_checks::ReleaseType; | ||
| 8 | use clap::{Parser, Subcommand}; | ||
| 9 | use petgraph::graph::{Graph, NodeIndex}; | ||
| 10 | use petgraph::visit::Bfs; | ||
| 11 | use petgraph::{Directed, Direction}; | ||
| 12 | use simple_logger::SimpleLogger; | ||
| 13 | use toml_edit::{DocumentMut, Item, Value}; | ||
| 14 | use types::*; | ||
| 15 | |||
| 16 | fn check_publish_dependencies(ctx: &Context) -> Result<()> { | ||
| 17 | for krate in ctx.crates.values() { | ||
| 18 | if krate.publish { | ||
| 19 | for dep_name in &krate.dependencies { | ||
| 20 | if let Some(dep_crate) = ctx.crates.get(dep_name) { | ||
| 21 | if !dep_crate.publish { | ||
| 22 | return Err(anyhow!( | ||
| 23 | "Publishable crate '{}' depends on non-publishable crate '{}'. This is not allowed.", | ||
| 24 | krate.name, | ||
| 25 | dep_name | ||
| 26 | )); | ||
| 27 | } | ||
| 28 | } | ||
| 29 | } | ||
| 30 | } | ||
| 31 | } | ||
| 32 | Ok(()) | ||
| 33 | } | ||
| 34 | |||
| 35 | mod build; | ||
| 36 | mod cargo; | ||
| 37 | mod semver_check; | ||
| 38 | mod types; | ||
| 39 | |||
| 40 | /// Tool to traverse and operate on intra-repo Rust crate dependencies | ||
| 41 | #[derive(Parser, Debug)] | ||
| 42 | #[command(author, version, about)] | ||
| 43 | struct Args { | ||
| 44 | /// Command to perform on each crate | ||
| 45 | #[command(subcommand)] | ||
| 46 | command: Command, | ||
| 47 | } | ||
| 48 | |||
| 49 | #[derive(Debug, Subcommand)] | ||
| 50 | enum Command { | ||
| 51 | /// All crates and their direct dependencies | ||
| 52 | List, | ||
| 53 | /// List all dependencies for a crate | ||
| 54 | Dependencies { | ||
| 55 | /// Crate name to print dependencies for. | ||
| 56 | #[arg(value_name = "CRATE")] | ||
| 57 | crate_name: String, | ||
| 58 | }, | ||
| 59 | /// List all dependencies for a crate | ||
| 60 | Dependents { | ||
| 61 | /// Crate name to print dependencies for. | ||
| 62 | #[arg(value_name = "CRATE")] | ||
| 63 | crate_name: String, | ||
| 64 | }, | ||
| 65 | |||
| 66 | /// Build | ||
| 67 | Build { | ||
| 68 | /// Crate to check. If not specified checks all crates. | ||
| 69 | #[arg(value_name = "CRATE")] | ||
| 70 | crate_name: Option<String>, | ||
| 71 | }, | ||
| 72 | /// SemverCheck | ||
| 73 | SemverCheck { | ||
| 74 | /// Specific crate name to check | ||
| 75 | #[arg(value_name = "CRATE")] | ||
| 76 | crate_name: String, | ||
| 77 | }, | ||
| 78 | /// Prepare to release a crate and all dependents that needs updating | ||
| 79 | /// - Semver checks | ||
| 80 | /// - Bump versions and commit | ||
| 81 | /// - Create tag. | ||
| 82 | PrepareRelease { | ||
| 83 | /// Crate to release. Will traverse that crate an it's dependents. If not specified checks all crates. | ||
| 84 | #[arg(value_name = "CRATE")] | ||
| 85 | crate_name: String, | ||
| 86 | }, | ||
| 87 | } | ||
| 88 | |||
| 89 | fn update_version(c: &mut Crate, new_version: &str) -> Result<()> { | ||
| 90 | let path = c.path.join("Cargo.toml"); | ||
| 91 | c.version = new_version.to_string(); | ||
| 92 | let content = fs::read_to_string(&path)?; | ||
| 93 | let mut doc: DocumentMut = content.parse()?; | ||
| 94 | for section in ["package"] { | ||
| 95 | if let Some(Item::Table(dep_table)) = doc.get_mut(section) { | ||
| 96 | dep_table.insert("version", Item::Value(Value::from(new_version))); | ||
| 97 | } | ||
| 98 | } | ||
| 99 | fs::write(&path, doc.to_string())?; | ||
| 100 | Ok(()) | ||
| 101 | } | ||
| 102 | |||
| 103 | fn update_versions(to_update: &Crate, dep: &CrateId, new_version: &str) -> Result<()> { | ||
| 104 | let path = to_update.path.join("Cargo.toml"); | ||
| 105 | let content = fs::read_to_string(&path)?; | ||
| 106 | let mut doc: DocumentMut = content.parse()?; | ||
| 107 | let mut changed = false; | ||
| 108 | for section in ["dependencies", "dev-dependencies", "build-dependencies"] { | ||
| 109 | if let Some(Item::Table(dep_table)) = doc.get_mut(section) { | ||
| 110 | if let Some(item) = dep_table.get_mut(&dep) { | ||
| 111 | match item { | ||
| 112 | // e.g., foo = "0.1.0" | ||
| 113 | Item::Value(Value::String(_)) => { | ||
| 114 | *item = Item::Value(Value::from(new_version)); | ||
| 115 | changed = true; | ||
| 116 | } | ||
| 117 | // e.g., foo = { version = "...", ... } | ||
| 118 | Item::Value(Value::InlineTable(inline)) => { | ||
| 119 | if inline.contains_key("version") { | ||
| 120 | inline["version"] = Value::from(new_version); | ||
| 121 | changed = true; | ||
| 122 | } | ||
| 123 | } | ||
| 124 | _ => {} // Leave unusual formats untouched | ||
| 125 | } | ||
| 126 | } | ||
| 127 | } | ||
| 128 | } | ||
| 129 | |||
| 130 | if changed { | ||
| 131 | fs::write(&path, doc.to_string())?; | ||
| 132 | println!("🔧 Updated {} to {} in {}", dep, new_version, path.display()); | ||
| 133 | } | ||
| 134 | Ok(()) | ||
| 135 | } | ||
| 136 | |||
| 137 | fn list_crates(root: &PathBuf) -> Result<BTreeMap<CrateId, Crate>> { | ||
| 138 | let mut crates = BTreeMap::new(); | ||
| 139 | discover_crates(root, &mut crates)?; | ||
| 140 | Ok(crates) | ||
| 141 | } | ||
| 142 | |||
| 143 | fn discover_crates(dir: &PathBuf, crates: &mut BTreeMap<CrateId, Crate>) -> Result<()> { | ||
| 144 | let d = std::fs::read_dir(dir)?; | ||
| 145 | for c in d { | ||
| 146 | let entry = c?; | ||
| 147 | if entry.file_type()?.is_dir() { | ||
| 148 | let path = dir.join(entry.path()); | ||
| 149 | let cargo_toml = path.join("Cargo.toml"); | ||
| 150 | |||
| 151 | if cargo_toml.exists() { | ||
| 152 | let content = fs::read_to_string(&cargo_toml)?; | ||
| 153 | |||
| 154 | // Try to parse as a crate, skip if it's a workspace | ||
| 155 | let parsed: Result<ParsedCrate, _> = toml::from_str(&content); | ||
| 156 | if let Ok(parsed) = parsed { | ||
| 157 | let id = parsed.package.name; | ||
| 158 | |||
| 159 | let metadata = &parsed.package.metadata.embassy; | ||
| 160 | |||
| 161 | if metadata.skip { | ||
| 162 | continue; | ||
| 163 | } | ||
| 164 | |||
| 165 | let mut dependencies = Vec::new(); | ||
| 166 | for (k, _) in parsed.dependencies { | ||
| 167 | if k.starts_with("embassy-") { | ||
| 168 | dependencies.push(k); | ||
| 169 | } | ||
| 170 | } | ||
| 171 | |||
| 172 | let mut configs = metadata.build.clone(); | ||
| 173 | if configs.is_empty() { | ||
| 174 | configs.push(BuildConfig { | ||
| 175 | features: vec![], | ||
| 176 | target: None, | ||
| 177 | artifact_dir: None, | ||
| 178 | }) | ||
| 179 | } | ||
| 180 | |||
| 181 | crates.insert( | ||
| 182 | id.clone(), | ||
| 183 | Crate { | ||
| 184 | name: id, | ||
| 185 | version: parsed.package.version, | ||
| 186 | path, | ||
| 187 | dependencies, | ||
| 188 | configs, | ||
| 189 | publish: parsed.package.publish, | ||
| 190 | }, | ||
| 191 | ); | ||
| 192 | } | ||
| 193 | } else { | ||
| 194 | // Recursively search subdirectories, but only for examples, tests, and docs | ||
| 195 | let file_name = entry.file_name(); | ||
| 196 | let dir_name = file_name.to_string_lossy(); | ||
| 197 | if dir_name == "examples" || dir_name == "tests" || dir_name == "docs" { | ||
| 198 | discover_crates(&path, crates)?; | ||
| 199 | } | ||
| 200 | } | ||
| 201 | } | ||
| 202 | } | ||
| 203 | Ok(()) | ||
| 204 | } | ||
| 205 | |||
| 206 | fn build_graph(crates: &BTreeMap<CrateId, Crate>) -> (Graph<CrateId, ()>, HashMap<CrateId, NodeIndex>) { | ||
| 207 | let mut graph = Graph::<CrateId, (), Directed>::new(); | ||
| 208 | let mut node_indices: HashMap<CrateId, NodeIndex> = HashMap::new(); | ||
| 209 | |||
| 210 | // Helper to insert or get existing node | ||
| 211 | let get_or_insert_node = |id: CrateId, graph: &mut Graph<CrateId, ()>, map: &mut HashMap<CrateId, NodeIndex>| { | ||
| 212 | if let Some(&idx) = map.get(&id) { | ||
| 213 | idx | ||
| 214 | } else { | ||
| 215 | let idx = graph.add_node(id.clone()); | ||
| 216 | map.insert(id, idx); | ||
| 217 | idx | ||
| 218 | } | ||
| 219 | }; | ||
| 220 | |||
| 221 | for krate in crates.values() { | ||
| 222 | get_or_insert_node(krate.name.clone(), &mut graph, &mut node_indices); | ||
| 223 | } | ||
| 224 | |||
| 225 | for krate in crates.values() { | ||
| 226 | // Insert crate node if not exists | ||
| 227 | let crate_idx = get_or_insert_node(krate.name.clone(), &mut graph, &mut node_indices); | ||
| 228 | |||
| 229 | // Insert dependencies and connect edges | ||
| 230 | for dep in krate.dependencies.iter() { | ||
| 231 | let dep_idx = get_or_insert_node(dep.clone(), &mut graph, &mut node_indices); | ||
| 232 | graph.add_edge(crate_idx, dep_idx, ()); | ||
| 233 | } | ||
| 234 | } | ||
| 235 | |||
| 236 | (graph, node_indices) | ||
| 237 | } | ||
| 238 | |||
| 239 | struct Context { | ||
| 240 | root: PathBuf, | ||
| 241 | crates: BTreeMap<String, Crate>, | ||
| 242 | graph: Graph<String, ()>, | ||
| 243 | indices: HashMap<String, NodeIndex>, | ||
| 244 | } | ||
| 245 | |||
| 246 | fn find_repo_root() -> Result<PathBuf> { | ||
| 247 | let mut path = std::env::current_dir()?.canonicalize()?; | ||
| 248 | |||
| 249 | loop { | ||
| 250 | // Check if this directory contains a .git directory | ||
| 251 | if path.join(".git").exists() { | ||
| 252 | return Ok(path); | ||
| 253 | } | ||
| 254 | |||
| 255 | // Move to parent directory | ||
| 256 | match path.parent() { | ||
| 257 | Some(parent) => path = parent.to_path_buf(), | ||
| 258 | None => break, | ||
| 259 | } | ||
| 260 | } | ||
| 261 | |||
| 262 | Err(anyhow!( | ||
| 263 | "Could not find repository root. Make sure you're running this tool from within the embassy repository." | ||
| 264 | )) | ||
| 265 | } | ||
| 266 | |||
| 267 | fn load_context() -> Result<Context> { | ||
| 268 | let root = find_repo_root()?; | ||
| 269 | let crates = list_crates(&root)?; | ||
| 270 | let (graph, indices) = build_graph(&crates); | ||
| 271 | |||
| 272 | let ctx = Context { | ||
| 273 | root, | ||
| 274 | crates, | ||
| 275 | graph, | ||
| 276 | indices, | ||
| 277 | }; | ||
| 278 | |||
| 279 | // Check for publish dependency conflicts | ||
| 280 | check_publish_dependencies(&ctx)?; | ||
| 281 | |||
| 282 | Ok(ctx) | ||
| 283 | } | ||
| 284 | |||
| 285 | fn main() -> Result<()> { | ||
| 286 | SimpleLogger::new().init().unwrap(); | ||
| 287 | let args = Args::parse(); | ||
| 288 | let mut ctx = load_context()?; | ||
| 289 | |||
| 290 | match args.command { | ||
| 291 | Command::List => { | ||
| 292 | let ordered = petgraph::algo::toposort(&ctx.graph, None).unwrap(); | ||
| 293 | for node in ordered.iter() { | ||
| 294 | let start = ctx.graph.node_weight(*node).unwrap(); | ||
| 295 | let mut bfs = Bfs::new(&ctx.graph, *node); | ||
| 296 | while let Some(node) = bfs.next(&ctx.graph) { | ||
| 297 | let weight = ctx.graph.node_weight(node).unwrap(); | ||
| 298 | let c = ctx.crates.get(weight).unwrap(); | ||
| 299 | if weight == start { | ||
| 300 | println!("+ {}-{}", weight, c.version); | ||
| 301 | } else { | ||
| 302 | println!("|- {}-{}", weight, c.version); | ||
| 303 | } | ||
| 304 | } | ||
| 305 | println!(""); | ||
| 306 | } | ||
| 307 | } | ||
| 308 | Command::Dependencies { crate_name } => { | ||
| 309 | let idx = ctx.indices.get(&crate_name).expect("unable to find crate in tree"); | ||
| 310 | let mut bfs = Bfs::new(&ctx.graph, *idx); | ||
| 311 | while let Some(node) = bfs.next(&ctx.graph) { | ||
| 312 | let weight = ctx.graph.node_weight(node).unwrap(); | ||
| 313 | let crt = ctx.crates.get(weight).unwrap(); | ||
| 314 | if *weight == crate_name { | ||
| 315 | println!("+ {}-{}", weight, crt.version); | ||
| 316 | } else { | ||
| 317 | println!("|- {}-{}", weight, crt.version); | ||
| 318 | } | ||
| 319 | } | ||
| 320 | } | ||
| 321 | Command::Dependents { crate_name } => { | ||
| 322 | let idx = ctx.indices.get(&crate_name).expect("unable to find crate in tree"); | ||
| 323 | let weight = ctx.graph.node_weight(*idx).unwrap(); | ||
| 324 | let crt = ctx.crates.get(weight).unwrap(); | ||
| 325 | println!("+ {}-{}", weight, crt.version); | ||
| 326 | for parent in ctx.graph.neighbors_directed(*idx, Direction::Incoming) { | ||
| 327 | let weight = ctx.graph.node_weight(parent).unwrap(); | ||
| 328 | let crt = ctx.crates.get(weight).unwrap(); | ||
| 329 | println!("|- {}-{}", weight, crt.version); | ||
| 330 | } | ||
| 331 | } | ||
| 332 | Command::Build { crate_name } => { | ||
| 333 | build::build(&ctx, crate_name.as_deref())?; | ||
| 334 | } | ||
| 335 | Command::SemverCheck { crate_name } => { | ||
| 336 | let c = ctx.crates.get(&crate_name).unwrap(); | ||
| 337 | if !c.publish { | ||
| 338 | bail!("Cannot run semver-check on non-publishable crate '{}'", crate_name); | ||
| 339 | } | ||
| 340 | check_semver(&c)?; | ||
| 341 | } | ||
| 342 | Command::PrepareRelease { crate_name } => { | ||
| 343 | let start = ctx.indices.get(&crate_name).expect("unable to find crate in tree"); | ||
| 344 | |||
| 345 | // Check if the target crate is publishable | ||
| 346 | let start_weight = ctx.graph.node_weight(*start).unwrap(); | ||
| 347 | let start_crate = ctx.crates.get(start_weight).unwrap(); | ||
| 348 | if !start_crate.publish { | ||
| 349 | bail!("Cannot prepare release for non-publishable crate '{}'", crate_name); | ||
| 350 | } | ||
| 351 | |||
| 352 | let mut rgraph = ctx.graph.clone(); | ||
| 353 | rgraph.reverse(); | ||
| 354 | |||
| 355 | let mut bfs = Bfs::new(&rgraph, *start); | ||
| 356 | |||
| 357 | while let Some(node) = bfs.next(&rgraph) { | ||
| 358 | let weight = rgraph.node_weight(node).unwrap(); | ||
| 359 | println!("Preparing {}", weight); | ||
| 360 | let mut c = ctx.crates.get_mut(weight).unwrap(); | ||
| 361 | if c.publish { | ||
| 362 | let ver = semver::Version::parse(&c.version)?; | ||
| 363 | let newver = match check_semver(&c)? { | ||
| 364 | ReleaseType::Major | ReleaseType::Minor => semver::Version::new(ver.major, ver.minor + 1, 0), | ||
| 365 | ReleaseType::Patch => semver::Version::new(ver.major, ver.minor, ver.patch + 1), | ||
| 366 | _ => unreachable!(), | ||
| 367 | }; | ||
| 368 | |||
| 369 | println!("Updating {} from {} -> {}", weight, c.version, newver.to_string()); | ||
| 370 | let newver = newver.to_string(); | ||
| 371 | |||
| 372 | update_version(&mut c, &newver)?; | ||
| 373 | let c = ctx.crates.get(weight).unwrap(); | ||
| 374 | |||
| 375 | // Update all nodes further down the tree | ||
| 376 | let mut bfs = Bfs::new(&rgraph, node); | ||
| 377 | while let Some(dep_node) = bfs.next(&rgraph) { | ||
| 378 | let dep_weight = rgraph.node_weight(dep_node).unwrap(); | ||
| 379 | let dep = ctx.crates.get(dep_weight).unwrap(); | ||
| 380 | update_versions(dep, &c.name, &newver)?; | ||
| 381 | } | ||
| 382 | |||
| 383 | // Update changelog | ||
| 384 | update_changelog(&ctx.root, &c)?; | ||
| 385 | } | ||
| 386 | } | ||
| 387 | |||
| 388 | let weight = rgraph.node_weight(*start).unwrap(); | ||
| 389 | let c = ctx.crates.get(weight).unwrap(); | ||
| 390 | publish_release(&ctx.root, &c, false)?; | ||
| 391 | |||
| 392 | println!("# Please inspect changes and run the following commands when happy:"); | ||
| 393 | |||
| 394 | println!("git commit -a -m 'chore: prepare crate releases'"); | ||
| 395 | let mut bfs = Bfs::new(&rgraph, *start); | ||
| 396 | while let Some(node) = bfs.next(&rgraph) { | ||
| 397 | let weight = rgraph.node_weight(node).unwrap(); | ||
| 398 | let c = ctx.crates.get(weight).unwrap(); | ||
| 399 | if c.publish { | ||
| 400 | println!("git tag {}-v{}", weight, c.version); | ||
| 401 | } | ||
| 402 | } | ||
| 403 | |||
| 404 | println!(""); | ||
| 405 | println!("# Run these commands to publish the crate and dependents:"); | ||
| 406 | |||
| 407 | let mut bfs = Bfs::new(&rgraph, *start); | ||
| 408 | while let Some(node) = bfs.next(&rgraph) { | ||
| 409 | let weight = rgraph.node_weight(node).unwrap(); | ||
| 410 | let c = ctx.crates.get(weight).unwrap(); | ||
| 411 | |||
| 412 | let mut args: Vec<String> = vec![ | ||
| 413 | "publish".to_string(), | ||
| 414 | "--manifest-path".to_string(), | ||
| 415 | c.path.join("Cargo.toml").display().to_string(), | ||
| 416 | ]; | ||
| 417 | |||
| 418 | let config = c.configs.first().unwrap(); // TODO | ||
| 419 | if !config.features.is_empty() { | ||
| 420 | args.push("--features".into()); | ||
| 421 | args.push(config.features.join(",")); | ||
| 422 | } | ||
| 423 | |||
| 424 | if let Some(target) = &config.target { | ||
| 425 | args.push("--target".into()); | ||
| 426 | args.push(target.clone()); | ||
| 427 | } | ||
| 428 | |||
| 429 | /* | ||
| 430 | let mut dry_run = args.clone(); | ||
| 431 | dry_run.push("--dry-run".to_string()); | ||
| 432 | |||
| 433 | println!("cargo {}", dry_run.join(" ")); | ||
| 434 | */ | ||
| 435 | if c.publish { | ||
| 436 | println!("cargo {}", args.join(" ")); | ||
| 437 | } | ||
| 438 | } | ||
| 439 | |||
| 440 | println!(""); | ||
| 441 | println!("# Run this command to push changes and tags:"); | ||
| 442 | println!("git push --tags"); | ||
| 443 | } | ||
| 444 | } | ||
| 445 | Ok(()) | ||
| 446 | } | ||
| 447 | |||
| 448 | fn check_semver(c: &Crate) -> Result<ReleaseType> { | ||
| 449 | let min_version = semver_check::minimum_update(c)?; | ||
| 450 | println!("Version should be bumped to {:?}", min_version); | ||
| 451 | Ok(min_version) | ||
| 452 | } | ||
| 453 | |||
| 454 | fn update_changelog(repo: &Path, c: &Crate) -> Result<()> { | ||
| 455 | let args: Vec<String> = vec![ | ||
| 456 | "release".to_string(), | ||
| 457 | "replace".to_string(), | ||
| 458 | "--config".to_string(), | ||
| 459 | repo.join("release").join("release.toml").display().to_string(), | ||
| 460 | "--manifest-path".to_string(), | ||
| 461 | c.path.join("Cargo.toml").display().to_string(), | ||
| 462 | "--execute".to_string(), | ||
| 463 | "--no-confirm".to_string(), | ||
| 464 | ]; | ||
| 465 | |||
| 466 | let status = ProcessCommand::new("cargo").args(&args).output()?; | ||
| 467 | |||
| 468 | println!("{}", core::str::from_utf8(&status.stdout).unwrap()); | ||
| 469 | eprintln!("{}", core::str::from_utf8(&status.stderr).unwrap()); | ||
| 470 | if !status.status.success() { | ||
| 471 | return Err(anyhow!("release replace failed")); | ||
| 472 | } else { | ||
| 473 | Ok(()) | ||
| 474 | } | ||
| 475 | } | ||
| 476 | |||
| 477 | fn publish_release(_repo: &Path, c: &Crate, push: bool) -> Result<()> { | ||
| 478 | let config = c.configs.first().unwrap(); // TODO | ||
| 479 | |||
| 480 | let mut args: Vec<String> = vec![ | ||
| 481 | "publish".to_string(), | ||
| 482 | "--manifest-path".to_string(), | ||
| 483 | c.path.join("Cargo.toml").display().to_string(), | ||
| 484 | ]; | ||
| 485 | |||
| 486 | args.push("--features".into()); | ||
| 487 | args.push(config.features.join(",")); | ||
| 488 | |||
| 489 | if let Some(target) = &config.target { | ||
| 490 | args.push("--target".into()); | ||
| 491 | args.push(target.clone()); | ||
| 492 | } | ||
| 493 | |||
| 494 | if !push { | ||
| 495 | args.push("--dry-run".to_string()); | ||
| 496 | args.push("--allow-dirty".to_string()); | ||
| 497 | args.push("--keep-going".to_string()); | ||
| 498 | } | ||
| 499 | |||
| 500 | let status = ProcessCommand::new("cargo").args(&args).output()?; | ||
| 501 | |||
| 502 | println!("{}", core::str::from_utf8(&status.stdout).unwrap()); | ||
| 503 | eprintln!("{}", core::str::from_utf8(&status.stderr).unwrap()); | ||
| 504 | if !status.status.success() { | ||
| 505 | return Err(anyhow!("publish failed")); | ||
| 506 | } else { | ||
| 507 | Ok(()) | ||
| 508 | } | ||
| 509 | } | ||
| 510 | |||
| 511 | /// Make the path "Windows"-safe | ||
| 512 | pub fn windows_safe_path(path: &Path) -> PathBuf { | ||
| 513 | PathBuf::from(path.to_str().unwrap().to_string().replace("\\\\?\\", "")) | ||
| 514 | } | ||
