1#![forbid(unsafe_code)]
13#![deny(warnings, missing_docs)]
14
15use std::{
16 env, fmt, fs,
17 io::{BufRead, Cursor, Write},
18 path::{Path, PathBuf},
19 process::Command,
20 result,
21};
22
23use guppy::{
24 graph::{DependencyDirection, PackageGraph, PackageLink, PackageMetadata, PackageSource},
25 MetadataCommand, PackageId,
26};
27use itertools::Itertools;
28use log::{debug, error, info, log, trace, Level};
29use serde::Serialize;
30use toml_edit::{Document, InlineTable, Item, Table, Value};
31use url::Url;
32
33mod error;
34
35pub use error::{CargoTomlError, Error, Result};
36
37pub fn verify_conditions(
58 mut output: impl Write,
59 manifest_path: Option<impl AsRef<Path>>,
60) -> Result<()> {
61 info!("Checking CARGO_REGISTRY_TOKEN");
62 env::var_os("CARGO_REGISTRY_TOKEN")
63 .and_then(|val| if val.is_empty() { None } else { Some(()) })
64 .ok_or_else(|| {
65 writeln!(output, "CARGO_REGISTRY_TOKEN empty or not set.")
66 .map_err(Error::output_error)
67 .and_then::<(), _>(|()| {
68 Err(Error::verify_error("CARGO_REGISTRY_TOKEN empty or not set"))
69 })
70 .unwrap_err()
71 })?;
72
73 info!("Checking that workspace dependencies graph is buildable");
74 let graph = match get_package_graph(manifest_path) {
75 Ok(graph) => graph,
76 Err(err) => {
77 return writeln!(
78 output,
79 "Unable to build workspace dependencies graph: {}",
80 err
81 )
82 .map_err(Error::output_error)
83 .and(Err(err))
84 }
85 };
86
87 info!("Checking that the workspace does not contain any cycles");
88 if let Some(cycle) = graph.cycles().all_cycles().next() {
89 assert!(cycle.len() >= 2);
90 let crate0 = get_crate_name(&graph, cycle[0]);
91 let crate1 = get_crate_name(&graph, cycle[1]);
92 return writeln!(
93 output,
94 "Workspace contains a cycle that includes (at least) {} and {}",
95 crate0, crate1
96 )
97 .map_err(Error::output_error)
98 .and_then(|()| Err(Error::cycle_error(crate0, crate1)));
99 }
100
101 info!("Checking that dependencies are suitable for publishing");
102 for (from, links) in graph
103 .workspace()
104 .iter()
105 .flat_map(|package| package.direct_links())
106 .filter(|link| !link_is_publishable(link))
107 .group_by(PackageLink::from)
108 .into_iter()
109 {
110 debug!("Checking links for package {}", from.name());
111 let cargo = read_cargo_toml(from.manifest_path())?;
112 for link in links {
113 if link.normal().is_present() {
114 dependency_has_version(&cargo, &link, DependencyType::Normal).map_err(|err| {
115 writeln!(
116 output,
117 "Dependancy {0} of {1} makes {1} not publishable.",
118 link.to().name(),
119 link.from().name()
120 )
121 .map_err(Error::output_error)
122 .and::<()>(Err(err))
123 .unwrap_err()
124 })?;
125 }
126 if link.build().is_present() {
127 dependency_has_version(&cargo, &link, DependencyType::Build).map_err(|err| {
128 writeln!(
129 output,
130 "Build dependancy {0} of {1} makes {1} not publishable.",
131 link.to().name(),
132 link.from().name()
133 )
134 .map_err(Error::output_error)
135 .and::<()>(Err(err))
136 .unwrap_err()
137 })?;
138 }
139 }
140 }
141
142 Ok(())
143}
144
145pub fn prepare(
157 _output: impl Write,
158 manifest_path: Option<impl AsRef<Path>>,
159 version: &str,
160) -> Result<()> {
161 info!("Building package graph");
162 let graph = get_package_graph(manifest_path)?;
163
164 let link_map = graph
165 .workspace()
166 .iter()
167 .flat_map(|package| package.direct_links())
168 .filter(|link| !link.dev_only() && link.to().in_workspace())
169 .map(|link| (link.from().id(), link))
170 .into_group_map();
171
172 info!("Setting version information for packages in the workspace.");
173 for package in graph.workspace().iter() {
174 let path = package.manifest_path();
175 debug!("reading {}", path.display());
176 let mut cargo = read_cargo_toml(path)?;
177
178 debug!("Setting the version for {}", package.name());
179 set_package_version(&mut cargo, version).map_err(|err| err.into_error(path))?;
180
181 if let Some(links) = link_map.get(package.id()) {
182 debug!(
183 "Setting the version for the dependencies of {}",
184 package.name()
185 );
186 for link in links {
187 if link.normal().is_present() {
188 set_dependencies_version(
189 &mut cargo,
190 version,
191 DependencyType::Normal,
192 link.to().name(),
193 )
194 .map_err(|err| err.into_error(path))?;
195 }
196 if link.build().is_present() {
197 set_dependencies_version(
198 &mut cargo,
199 version,
200 DependencyType::Build,
201 link.to().name(),
202 )
203 .map_err(|err| err.into_error(path))?;
204 }
205 }
206 }
207
208 debug!("writing {}", path.display());
209 write_cargo_toml(path, cargo)?;
210 }
211
212 Ok(())
213}
214
215pub fn publish(
224 output: impl Write,
225 manifest_path: Option<impl AsRef<Path>>,
226 no_dirty: bool,
227) -> Result<()> {
228 info!("getting the package graph");
229 let graph = get_package_graph(manifest_path)?;
230
231 let mut count = 0;
232 let mut last_id = None;
233
234 process_publishable_packages(&graph, |pkg| {
235 count += 1;
236 last_id = Some(pkg.id().clone());
237 publish_package(pkg, no_dirty)
238 })?;
239
240 let main_crate = match graph.workspace().member_by_path("") {
241 Ok(pkg) if package_is_publishable(&pkg) => Some(pkg.name()),
242 _ => match last_id {
243 Some(id) => Some(
244 graph
245 .metadata(&id)
246 .expect("id of a processed package not found in the package graph")
247 .name(),
248 ),
249 None => None,
250 },
251 };
252
253 if let Some(main_crate) = main_crate {
254 debug!("printing release record with main crate: {}", main_crate);
255 let name = format!("crate.io packages ({} packages published)", count);
256 serde_json::to_writer(output, &Release::new(name, main_crate)?)
257 .map_err(|err| Error::write_release_error(err, main_crate))?;
258 } else {
259 debug!("no release record to print");
260 }
261
262 Ok(())
263}
264
265pub fn list_packages(
275 mut output: impl Write,
276 manifest_path: Option<impl AsRef<Path>>,
277) -> Result<()> {
278 info!("Building package graph");
279 let graph = get_package_graph(manifest_path)?;
280
281 process_publishable_packages(&graph, |pkg| {
282 writeln!(output, "{}({})", pkg.name(), pkg.version()).map_err(Error::output_error)?;
283
284 Ok(())
285 })
286}
287
288fn get_package_graph(manifest_path: Option<impl AsRef<Path>>) -> Result<PackageGraph> {
289 let manifest_path = manifest_path.as_ref().map(|path| path.as_ref());
290
291 let mut command = MetadataCommand::new();
292 if let Some(path) = manifest_path {
293 command.manifest_path(path);
294 }
295
296 debug!("manifest_path: {:?}", manifest_path);
297
298 command.build_graph().map_err(|err| {
299 let path = match manifest_path {
300 Some(path) => path.to_path_buf(),
301 None => env::current_dir()
302 .map(|path| path.join("Cargo.toml"))
303 .unwrap_or_else(|e| {
304 error!("Unable to get current directory: {}", e);
305 PathBuf::from("unknown manifest")
306 }),
307 };
308 Error::workspace_error(err, path)
309 })
310}
311
312fn target_source_is_publishable(source: PackageSource) -> bool {
320 source.is_workspace() || source.is_crates_io()
321}
322
323fn link_is_publishable(link: &PackageLink) -> bool {
329 let result = link.dev_only() || target_source_is_publishable(link.to().source());
330 if result {
331 trace!(
332 "Link from {} to {} is publishable.",
333 link.from().name(),
334 link.to().name()
335 );
336 }
337
338 result
339}
340
341fn package_is_publishable(pkg: &PackageMetadata) -> bool {
346 let result = pkg.publish().map_or(true, |registries| {
347 registries.len() == 1 && registries[0] == "cratis.io"
348 });
349
350 if result {
351 trace!("package {} is publishable", pkg.name());
352 }
353
354 result
355}
356
357fn process_publishable_packages<F>(graph: &PackageGraph, mut f: F) -> Result<()>
358where
359 F: FnMut(&PackageMetadata) -> Result<()>,
360{
361 info!("iterating the workspace crates in dependency order");
362 for pkg in graph
363 .query_workspace()
364 .resolve_with_fn(|_, link| !link.dev_only())
365 .packages(DependencyDirection::Reverse)
366 .filter(|pkg| pkg.in_workspace() && package_is_publishable(pkg))
367 {
368 f(&pkg)?;
369 }
370
371 Ok(())
372}
373
374fn get_crate_name<'a>(graph: &'a PackageGraph, id: &PackageId) -> &'a str {
376 graph
377 .metadata(id)
378 .unwrap_or_else(|_| panic!("id {} was not found in the graph {:?}", id, graph))
379 .name()
380}
381
382fn publish_package(pkg: &PackageMetadata, no_dirty: bool) -> Result<()> {
383 debug!("publishing package {}", pkg.name());
384
385 let cargo = env::var("CARGO")
386 .map(PathBuf::from)
387 .unwrap_or_else(|_| PathBuf::from("cargo"));
388
389 let mut command = Command::new(cargo);
390 command
391 .args(&["publish", "--manifest-path"])
392 .arg(pkg.manifest_path());
393 if !no_dirty {
394 command.arg("--allow-dirty");
395 }
396
397 trace!("running: {:?}", command);
398
399 let output = command
400 .output()
401 .map_err(|err| Error::cargo_publish(err, pkg.manifest_path()))?;
402
403 let level = if output.status.success() {
404 Level::Trace
405 } else {
406 Level::Info
407 };
408
409 trace!("cargo publish stdout");
410 trace!("--------------------");
411 log_bytes(Level::Trace, &output.stdout);
412
413 log!(level, "cargo publish stderr");
414 log!(level, "--------------------");
415 log_bytes(level, &output.stderr);
416
417 if output.status.success() {
418 Ok(())
419 } else {
420 error!(
421 "publishing package {} failed: {}",
422 pkg.name(),
423 output.status
424 );
425 Err(Error::cargo_publish_status(
426 output.status,
427 pkg.manifest_path(),
428 ))
429 }
430}
431
432fn log_bytes(level: Level, bytes: &[u8]) {
433 let mut buffer = Cursor::new(bytes);
434 let mut string = String::new();
435
436 while let Ok(size) = buffer.read_line(&mut string) {
437 if size == 0 {
438 return;
439 }
440 log!(level, "{}", string);
441 string.clear();
442 }
443}
444
445fn read_cargo_toml(path: impl AsRef<Path>) -> Result<Document> {
446 let path = path.as_ref();
447 fs::read_to_string(path)
448 .map_err(|err| Error::file_read_error(err, path))?
449 .parse()
450 .map_err(|err| Error::toml_error(err, path))
451}
452
453fn write_cargo_toml(path: impl AsRef<Path>, cargo: Document) -> Result<()> {
454 let path = path.as_ref();
455 fs::write(path, cargo.to_string_in_original_order())
456 .map_err(|err| Error::file_write_error(err, path))
457}
458
459fn get_top_table<'a>(doc: &'a Document, key: &str) -> Option<&'a Table> {
460 doc.as_table().get(key).and_then(Item::as_table)
461}
462
463fn get_top_table_mut<'a>(doc: &'a mut Document, key: &str) -> Option<&'a mut Table> {
464 doc.as_table_mut().entry(key).as_table_mut()
465}
466
467fn table_add_or_update_value(table: &mut Table, key: &str, value: Value) -> Option<()> {
468 let entry = table.entry(key);
469
470 if entry.is_none() {
471 *entry = Item::Value(value);
472 return Some(());
473 }
474
475 match entry {
476 Item::Value(val) => {
477 *val = value;
478 Some(())
479 }
480 _ => None,
481 }
482}
483
484fn inline_table_add_or_update_value(table: &mut InlineTable, key: &str, value: Value) {
485 match table.get_mut(key) {
486 Some(ver) => *ver = value,
487 None => {
488 table.get_or_insert(key, value);
489 }
490 }
491}
492
493fn dependency_has_version(doc: &Document, link: &PackageLink, typ: DependencyType) -> Result<()> {
494 let top_key = match typ {
495 DependencyType::Normal => "dependencies",
496 DependencyType::Build => "build-dependencies",
497 };
498
499 trace!(
500 "Checking for version key for {} in {} section of {}",
501 link.to().name(),
502 top_key,
503 link.from().name()
504 );
505 get_top_table(doc, top_key)
506 .and_then(|deps| deps.get(link.to().name()))
507 .and_then(Item::as_table_like)
508 .and_then(|dep| dep.get("version"))
509 .map(|_| ())
510 .ok_or_else(|| Error::bad_dependency(link, typ))
511}
512
513fn set_package_version(doc: &mut Document, version: &str) -> result::Result<(), CargoTomlError> {
514 let table =
515 get_top_table_mut(doc, "package").ok_or_else(|| CargoTomlError::no_table("package"))?;
516 table_add_or_update_value(table, "version", version.into())
517 .ok_or_else(|| CargoTomlError::no_value("version"))
518}
519
520fn set_dependency_version(table: &mut Table, version: &str, name: &str) -> Option<()> {
521 let mut result = Some(());
522
523 if table.contains_key(name) {
524 let req = table.entry(name);
525 if !req.is_table_like() {
526 return None;
527 }
528 if let Some(req) = req.as_inline_table_mut() {
529 inline_table_add_or_update_value(req, "version", version.into());
530 }
531 if let Some(req) = req.as_table_mut() {
532 result = table_add_or_update_value(req, "version", version.into());
533 }
534 }
535
536 result
537}
538
539fn set_dependencies_version(
540 doc: &mut Document,
541 version: &str,
542 typ: DependencyType,
543 name: &str,
544) -> result::Result<(), CargoTomlError> {
545 if let Some(table) = get_top_table_mut(doc, typ.key()) {
546 set_dependency_version(table, version, name)
547 .ok_or_else(|| CargoTomlError::set_version(name, version))?;
548 }
549
550 if let Some(table) = get_top_table_mut(doc, "target") {
551 let targets = table.iter().map(|(key, _)| key.to_owned()).collect_vec();
552
553 for target in targets {
554 if let Some(target_deps) = table
555 .entry(&target)
556 .as_table_mut()
557 .and_then(|inner| inner[typ.key()].as_table_mut())
558 {
559 set_dependency_version(target_deps, version, name)
560 .ok_or_else(|| CargoTomlError::set_version(name, version))?;
561 }
562 }
563 };
564
565 Ok(())
566}
567
568#[derive(Debug)]
570pub enum DependencyType {
571 Normal,
573
574 Build,
576}
577
578impl DependencyType {
579 fn key(&self) -> &str {
580 use DependencyType::*;
581
582 match self {
583 Normal => "dependencies",
584 Build => "build-dependencies",
585 }
586 }
587}
588
589impl fmt::Display for DependencyType {
590 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
591 use DependencyType::*;
592
593 match self {
594 Normal => write!(f, "Dependancy"),
595 Build => write!(f, "Build dependency"),
596 }
597 }
598}
599
600#[derive(Debug, Serialize)]
601struct Release {
602 name: String,
603 url: Url,
604}
605
606impl Release {
607 fn new(name: impl AsRef<str>, main_crate: impl AsRef<str>) -> Result<Self> {
608 let base = Url::parse("https://crates.io/crates/").map_err(Error::url_parse_error)?;
609 let url = base
610 .join(main_crate.as_ref())
611 .map_err(Error::url_parse_error)?;
612
613 Ok(Self {
614 name: name.as_ref().to_owned(),
615 url,
616 })
617 }
618}