1#![forbid(unsafe_code)]
13#![deny(warnings, missing_docs)]
14
15use std::{
16 collections::HashMap,
17 env, fmt, fs,
18 io::{BufRead, Cursor},
19 path::{Path, PathBuf},
20 process::Command,
21 result,
22};
23
24use guppy::{
25 graph::{DependencyDirection, PackageGraph, PackageLink, PackageMetadata, PackageSource},
26 MetadataCommand, PackageId,
27};
28use log::{debug, error, info, log, trace, Level};
29use serde::Serialize;
30use toml_edit::{DocumentMut, InlineTable, Item, Table, Value};
31use url::Url;
32
33#[cfg(feature = "napi-rs")]
34use napi_derive::napi;
35
36mod error;
37mod itertools;
38mod logger;
39
40pub use error::{CargoTomlError, Error, Result};
41
42pub use logger::LoggerBuilder;
43
44use crate::itertools::Itertools;
45
46#[cfg(feature = "napi-rs")]
66#[napi]
67pub fn verify_conditions() -> Result<()> {
68 let maybe_manifest_path: Option<&'static str> = None;
69
70 internal_verify_conditions(None, maybe_manifest_path)
71}
72
73#[cfg(not(feature = "napi-rs"))]
93pub fn verify_conditions(manifest_path: Option<impl AsRef<Path>>) -> Result<()> {
94 internal_verify_conditions(None, manifest_path)
95}
96
97#[cfg(not(feature = "napi-rs"))]
122pub fn verify_conditions_with_alternate(
123 alternate_registry: Option<&str>,
124 manifest_path: Option<impl AsRef<Path>>,
125) -> Result<()> {
126 internal_verify_conditions(alternate_registry, manifest_path)
127}
128
129fn internal_verify_conditions(
130 alternate_registry: Option<&str>,
131 manifest_path: Option<impl AsRef<Path>>,
132) -> Result<()> {
133 let cargo_config = cargo_config2::Config::load()?;
134
135 let registry_token_set = match alternate_registry {
136 Some(alternate_registry_id) => {
137 let registry_value = cargo_config
140 .registries
141 .get(alternate_registry_id)
142 .or_else(|| {
143 let uppercased_registry = alternate_registry_id.to_uppercase();
144 cargo_config.registries.get(&uppercased_registry)
145 });
146
147 registry_value.and_then(|registry| registry.token.as_ref().map(|_| ()))
148 }
149 None => cargo_config.registry.token.map(|_| ()),
150 };
151
152 debug!("Checking cargo registry token is set");
153 registry_token_set.ok_or_else(|| {
154 let registry_id = alternate_registry.unwrap_or("crates-io");
155
156 Error::verify_error(format!(
157 "Registry token for {} empty or not set.",
158 ®istry_id
159 ))
160 })?;
161
162 debug!("Checking that workspace dependencies graph is buildable");
163 let graph = get_package_graph(manifest_path)?;
164
165 debug!("Checking that the workspace does not contain any cycles");
166 if let Some(cycle) = graph.cycles().all_cycles().next() {
167 assert!(cycle.len() >= 2);
168 let crate0 = get_crate_name(&graph, cycle[0]);
169 let crate1 = get_crate_name(&graph, cycle[1]);
170 let workspace_error = Error::WorkspaceCycles {
171 crate1: crate0.to_owned(),
172 crate2: crate1.to_owned(),
173 };
174
175 return Err(workspace_error.into());
176 }
177
178 debug!("Checking that dependencies are suitable for publishing");
179 for (from, links) in graph
180 .workspace()
181 .iter()
182 .flat_map(|package| package.direct_links())
183 .filter(|link| !link_is_publishable(link))
184 .chunk_by(PackageLink::from)
185 .into_iter()
186 {
187 debug!("Checking links for package {}", from.name());
188 let cargo = read_cargo_toml(from.manifest_path().as_std_path())?;
189 for link in links {
190 if link.normal().is_present() {
191 dependency_has_version(&cargo, &link, DependencyType::Normal)?;
192 }
193 if link.build().is_present() {
194 dependency_has_version(&cargo, &link, DependencyType::Build)?;
195 }
196 }
197 }
198
199 Ok(())
200}
201
202#[cfg(feature = "napi-rs")]
215#[napi]
216pub fn prepare(next_release_version: String) -> Result<()> {
217 let manifest_path: Option<&Path> = None;
218 internal_prepare(manifest_path, next_release_version)
219}
220
221#[cfg(not(feature = "napi-rs"))]
234pub fn prepare(manifest_path: Option<&Path>, next_release_version: String) -> Result<()> {
235 internal_prepare(manifest_path, next_release_version)
236}
237
238fn internal_prepare(manifest_path: Option<&Path>, next_release_version: String) -> Result<()> {
239 debug!("Building package graph");
240 let graph = get_package_graph(manifest_path)?;
241
242 let link_map = graph
243 .workspace()
244 .iter()
245 .flat_map(|package| package.direct_links())
246 .filter(|link| !link.dev_only() || !link.version_req().comparators.is_empty())
249 .filter(|link| link.to().in_workspace())
250 .map(|link| (link.from().id(), link))
251 .into_group_map();
252
253 debug!("Setting version information for packages in the workspace.");
254 for package in graph.workspace().iter() {
255 let path = package.manifest_path();
256 debug!("reading {}", path.as_str());
257 let mut cargo = read_cargo_toml(path.as_std_path())?;
258
259 info!(
260 "Setting the version of {} to {}",
261 package.name(),
262 &next_release_version
263 );
264 set_package_version(&mut cargo, &next_release_version)
265 .map_err(|err| err.into_error(path))?;
266
267 if let Some(links) = link_map.get(package.id()) {
268 for link in links {
269 if link.normal().is_present() {
270 info!(
271 "Upgrading dependency of {} to {}@{}",
272 link.to().name(),
273 package.name(),
274 &next_release_version
275 );
276 set_dependencies_version(
277 &mut cargo,
278 &next_release_version,
279 DependencyType::Normal,
280 link.to().name(),
281 )
282 .map_err(|err| err.into_error(path))?;
283 }
284 if link.build().is_present() {
285 info!(
286 "Upgrading build-dependency of {} to {}@{}",
287 link.to().name(),
288 package.name(),
289 &next_release_version
290 );
291 set_dependencies_version(
292 &mut cargo,
293 &next_release_version,
294 DependencyType::Build,
295 link.to().name(),
296 )
297 .map_err(|err| err.into_error(path))?;
298 }
299 if link.dev().is_present() {
300 info!(
301 "Upgrading dev-dependency of {} to {}@{}",
302 link.to().name(),
303 package.name(),
304 &next_release_version
305 );
306 set_dependencies_version(
307 &mut cargo,
308 &next_release_version,
309 DependencyType::Dev,
310 link.to().name(),
311 )
312 .map_err(|err| err.into_error(path))?;
313 }
314 }
315 }
316
317 debug!("writing {}", path.as_str());
318 write_cargo_toml(path.as_std_path(), cargo)?;
319
320 let lockfile_path = get_cargo_lock(path.as_std_path());
331 if lockfile_path.exists() {
332 debug!("reading {}", lockfile_path.to_string_lossy());
333 let mut lockfile = read_cargo_toml(&lockfile_path)?;
334
335 set_lockfile_self_describing_metadata(
336 &mut lockfile,
337 &next_release_version,
338 package.name(),
339 )?;
340
341 debug!("writing {}", lockfile_path.to_string_lossy());
342 write_cargo_toml(&lockfile_path, lockfile)?;
343 }
344 }
345
346 Ok(())
347}
348
349#[cfg_attr(feature = "napi-rs", napi(object))]
350#[derive(Debug, Default)]
351pub struct PublishArgs {
353 pub no_dirty: Option<bool>,
355
356 pub features: Option<HashMap<String, Vec<String>>>,
358
359 pub registry: Option<String>,
361}
362
363#[cfg(feature = "napi-rs")]
372#[napi]
373pub fn publish(opts: Option<PublishArgs>) -> Result<()> {
374 let manifest_path: Option<&Path> = None;
375 internal_publish(manifest_path, &opts.unwrap_or_default())
376}
377
378#[cfg(not(feature = "napi-rs"))]
387pub fn publish(manifest_path: Option<&Path>, opts: &PublishArgs) -> Result<()> {
388 internal_publish(manifest_path, opts)
389}
390
391fn internal_publish(manifest_path: Option<&Path>, opts: &PublishArgs) -> Result<()> {
392 debug!("Getting the package graph");
393 let graph = get_package_graph(manifest_path)?;
394 let optional_registry = opts.registry.as_deref();
395
396 let mut count = 0;
397 let mut last_id = None;
398
399 process_publishable_packages(&graph, optional_registry, |pkg| {
400 count += 1;
401 last_id = Some(pkg.id().clone());
402 publish_package(pkg, opts)
403 })?;
404
405 let main_crate = match graph.workspace().member_by_path("") {
406 Ok(pkg) if package_is_publishable(&pkg, optional_registry) => Some(pkg.name()),
407 _ => last_id.map(|id| {
408 graph
409 .metadata(&id)
410 .expect("id of a processed package not found in the package graph")
411 .name()
412 }),
413 };
414
415 if let Some(main_crate) = main_crate {
416 debug!("printing release record with main crate: {}", main_crate);
417 let name = format!(
418 "{} packages ({} packages published)",
419 optional_registry.unwrap_or("crates.io"),
420 count
421 );
422
423 let release_meta_json = if optional_registry.is_none() {
425 serde_json::to_string(&Release::new_crates_io_release(name, main_crate)?)
426 } else {
427 serde_json::to_string(&Release::new::<&str>(name, None, main_crate)?)
428 }
429 .map_err(|err| Error::write_release_error(err, main_crate))?;
430
431 info!("{:?}", release_meta_json);
432 } else {
433 debug!("no release record to print");
434 }
435
436 Ok(())
437}
438
439pub fn list_packages(manifest_path: Option<impl AsRef<Path>>) -> Result<()> {
449 internal_list_packages(None, manifest_path)
450}
451
452pub fn list_packages_with_arguments(
463 alternate_registry: Option<&str>,
464 manifest_path: Option<impl AsRef<Path>>,
465) -> Result<()> {
466 internal_list_packages(alternate_registry, manifest_path)
467}
468
469fn internal_list_packages(
470 alternate_registry: Option<&str>,
471 manifest_path: Option<impl AsRef<Path>>,
472) -> Result<()> {
473 info!("Building package graph");
474 let graph = get_package_graph(manifest_path)?;
475
476 process_publishable_packages(&graph, alternate_registry, |pkg| {
477 error!("{}({})", pkg.name(), pkg.version());
478 Ok(())
479 })
480}
481
482fn get_package_graph(manifest_path: Option<impl AsRef<Path>>) -> Result<PackageGraph> {
483 let manifest_path = manifest_path.as_ref().map(|path| path.as_ref());
484
485 let mut command = MetadataCommand::new();
486 if let Some(path) = manifest_path {
487 command.manifest_path(path);
488 }
489
490 debug!("manifest_path: {:?}", manifest_path);
491
492 command.build_graph().map_err(|err| {
493 let path = match manifest_path {
494 Some(path) => path.to_path_buf(),
495 None => env::current_dir()
496 .map(|path| path.join("Cargo.toml"))
497 .unwrap_or_else(|e| {
498 error!("Unable to get current directory: {}", e);
499 PathBuf::from("unknown manifest")
500 }),
501 };
502 Error::workspace_error(err, path).into()
503 })
504}
505
506fn target_source_is_publishable(source: PackageSource) -> bool {
514 source.is_workspace() || source.is_crates_io()
515}
516
517fn link_is_publishable(link: &PackageLink) -> bool {
523 let result = link.dev_only() || target_source_is_publishable(link.to().source());
524 if result {
525 trace!(
526 "Link from {} to {} is publishable.",
527 link.from().name(),
528 link.to().name()
529 );
530 }
531
532 result
533}
534
535fn package_is_publishable(pkg: &PackageMetadata, registry: Option<&str>) -> bool {
540 use guppy::graph::PackagePublish;
541 let registry_target = registry;
542
543 let result = match pkg.publish() {
544 guppy::graph::PackagePublish::Unrestricted => true,
545 guppy::graph::PackagePublish::Registries([registry]) => {
546 let registry_target = registry_target.unwrap_or(PackagePublish::CRATES_IO);
547 registry == registry_target
548 }
549 guppy::graph::PackagePublish::Registries([]) => false,
550 _ => todo!(),
551 };
552
553 if result {
554 trace!("package {} is publishable", pkg.name());
555 }
556
557 result
558}
559
560fn process_publishable_packages<F>(
561 graph: &PackageGraph,
562 alternate_registry: Option<&str>,
563 mut f: F,
564) -> Result<()>
565where
566 F: FnMut(&PackageMetadata) -> Result<()>,
567{
568 info!("iterating the workspace crates in dependency order");
569 for pkg in graph
570 .query_workspace()
571 .resolve_with_fn(|_, link| !link.dev_only())
572 .packages(DependencyDirection::Reverse)
573 .filter(|pkg| pkg.in_workspace() && package_is_publishable(pkg, alternate_registry))
574 {
575 f(&pkg)?;
576 }
577
578 Ok(())
579}
580
581fn get_crate_name<'a>(graph: &'a PackageGraph, id: &PackageId) -> &'a str {
583 graph
584 .metadata(id)
585 .unwrap_or_else(|_| panic!("id {} was not found in the graph {:?}", id, graph))
586 .name()
587}
588
589fn publish_package(pkg: &PackageMetadata, opts: &PublishArgs) -> Result<()> {
590 info!(
591 "Publishing version {} of {} to {} registry",
592 pkg.version(),
593 pkg.name(),
594 opts.registry.as_deref().unwrap_or("crates.io")
595 );
596
597 let cargo = env::var("CARGO")
598 .map(PathBuf::from)
599 .unwrap_or_else(|_| PathBuf::from("cargo"));
600
601 let mut command = Command::new(cargo);
602 command
603 .args(["publish", "--manifest-path"])
604 .arg(pkg.manifest_path());
605 if !opts.no_dirty.unwrap_or_default() {
606 command.arg("--allow-dirty");
607 }
608 if let Some(features) = opts.features.as_ref().and_then(|f| f.get(pkg.name())) {
609 command.arg("--features");
610 command.args(features);
611 }
612 if let Some(registry) = opts.registry.as_ref() {
613 command.arg("--registry");
614 command.arg(registry);
615 }
616
617 trace!("running: {:?}", command);
618
619 let output = command
620 .output()
621 .map_err(|err| Error::cargo_publish(err, pkg.manifest_path().as_std_path()))?;
622
623 let level = if output.status.success() {
624 Level::Trace
625 } else {
626 Level::Info
627 };
628
629 trace!("cargo publish stdout");
630 trace!("--------------------");
631 log_bytes(Level::Trace, &output.stdout);
632
633 log!(level, "cargo publish stderr");
634 log!(level, "--------------------");
635 log_bytes(level, &output.stderr);
636
637 if output.status.success() {
638 info!(
639 "Published {}@{} to {} registry",
640 pkg.name(),
641 pkg.version(),
642 opts.registry.as_deref().unwrap_or("crates.io")
643 );
644 Ok(())
645 } else {
646 error!(
647 "publishing package {} failed: {}\n{}",
648 pkg.name(),
649 output.status,
650 String::from_utf8_lossy(&output.stderr)
651 );
652 Err(Error::cargo_publish_status(
653 output.status,
654 pkg.manifest_path().as_std_path(),
655 &output.stderr,
656 )
657 .into())
658 }
659}
660
661fn log_bytes(level: Level, bytes: &[u8]) {
662 let mut buffer = Cursor::new(bytes);
663 let mut string = String::new();
664
665 while let Ok(size) = buffer.read_line(&mut string) {
666 if size == 0 {
667 return;
668 }
669 log!(level, "{}", string);
670 string.clear();
671 }
672}
673
674fn get_cargo_lock(path: &Path) -> PathBuf {
677 path.parent().unwrap().join("Cargo.lock")
678}
679
680fn read_cargo_toml(path: &Path) -> Result<DocumentMut> {
681 fs::read_to_string(path)
682 .map_err(|err| Error::file_read_error(err, path))?
683 .parse()
684 .map_err(|err| Error::toml_error(err, path).into())
685}
686
687fn write_cargo_toml(path: &Path, cargo: DocumentMut) -> Result<()> {
688 fs::write(path, cargo.to_string()).map_err(|err| Error::file_write_error(err, path).into())
689}
690
691fn get_top_table<'a>(doc: &'a DocumentMut, key: &str) -> Option<&'a Table> {
692 doc.as_table().get(key).and_then(Item::as_table)
693}
694
695fn get_top_table_mut<'a>(doc: &'a mut DocumentMut, key: &str) -> Option<&'a mut Table> {
696 doc.get_key_value_mut(key)
697 .and_then(|(_key, value)| value.as_table_mut())
698}
699
700fn table_add_or_update_value(table: &mut Table, key: &str, value: Value) -> Option<()> {
701 let entry = table.entry(key);
702
703 match entry {
704 toml_edit::Entry::Occupied(mut val) => {
705 val.insert(Item::Value(value));
706 Some(())
707 }
708 toml_edit::Entry::Vacant(val) => {
709 val.insert(Item::Value(value));
710 Some(())
711 }
712 }
713}
714
715fn inline_table_add_or_update_value(table: &mut InlineTable, key: &str, value: Value) {
716 match table.get_mut(key) {
717 Some(ver) => *ver = value,
718 None => {
719 table.get_or_insert(key, value);
720 }
721 }
722}
723
724fn dependency_has_version(
725 doc: &DocumentMut,
726 link: &PackageLink,
727 typ: DependencyType,
728) -> Result<()> {
729 let top_key = typ.key();
730
731 trace!(
732 "Checking for version key for {} in {} section of {}",
733 link.to().name(),
734 top_key,
735 link.from().name()
736 );
737 get_top_table(doc, top_key)
738 .and_then(|deps| deps.get(link.to().name()))
739 .and_then(Item::as_table_like)
740 .and_then(|dep| dep.get("version"))
741 .map(|_| ())
742 .ok_or_else(|| Error::bad_dependency(link, typ).into())
743}
744
745fn set_package_version(doc: &mut DocumentMut, version: &str) -> result::Result<(), CargoTomlError> {
746 let table =
747 get_top_table_mut(doc, "package").ok_or_else(|| CargoTomlError::no_table("package"))?;
748 table_add_or_update_value(table, "version", version.into())
749 .ok_or_else(|| CargoTomlError::no_value("version"))
750}
751
752fn find_matching_dependency_key<'table>(
754 table: &'table mut Table,
755 name: &'table str,
756) -> Option<String> {
757 for (key, dependency_item) in table.iter() {
758 if key == name {
760 return Some(name.to_string());
761 }
762
763 let Some(Item::Value(Value::String(package_ident))) = dependency_item.get("package") else {
767 continue;
768 };
769
770 let Some(package_ident) = package_ident.as_repr() else {
771 continue;
772 };
773
774 let maybe_package_ident_str_repr = package_ident
775 .as_raw()
776 .as_str()
777 .map(|repr| repr.trim_matches('"'));
780 if maybe_package_ident_str_repr == Some(name) {
781 return Some(key.to_string());
782 }
783 }
784
785 None
786}
787
788fn set_dependency_version(table: &mut Table, version: &str, name: &str) -> Option<()> {
789 let dependency_key = match find_matching_dependency_key(table, name) {
790 Some(key) => key,
791 None => return Some(()),
792 };
793
794 match table.entry(&dependency_key) {
795 toml_edit::Entry::Occupied(mut req) => {
796 let item = req.get_mut();
797
798 if let Some(item) = item.as_inline_table_mut() {
799 inline_table_add_or_update_value(item, "version", version.into());
800 return Some(());
801 }
802 if let Some(item) = item.as_table_mut() {
803 return table_add_or_update_value(item, "version", version.into());
804 }
805
806 None
807 }
808 toml_edit::Entry::Vacant(_) => Some(()),
809 }
810}
811
812fn set_dependencies_version(
813 doc: &mut DocumentMut,
814 version: &str,
815 typ: DependencyType,
816 name: &str,
817) -> result::Result<(), CargoTomlError> {
818 if let Some(table) = get_top_table_mut(doc, typ.key()) {
819 set_dependency_version(table, version, name)
820 .ok_or_else(|| CargoTomlError::set_version(name, version))?;
821 }
822
823 if let Some(table) = get_top_table_mut(doc, "target") {
824 let targets: Vec<_> = table.iter().map(|(key, _)| key.to_owned()).collect();
825
826 for target in targets {
827 let target_deps = table.entry(&target);
828 match target_deps {
829 toml_edit::Entry::Occupied(mut target_deps) => {
830 if let Some(target_deps) = target_deps
831 .get_mut()
832 .as_table_mut()
833 .and_then(|inner| inner[typ.key()].as_table_mut())
834 {
835 set_dependency_version(target_deps, version, name)
836 .ok_or_else(|| CargoTomlError::set_version(name, version))?;
837 }
838 }
839 toml_edit::Entry::Vacant(_) => {}
840 };
841 }
842 };
843
844 Ok(())
845}
846
847fn set_lockfile_self_describing_metadata(
848 doc: &mut DocumentMut,
849 next_release_version: &str,
850 package_name: &str,
851) -> result::Result<(), Error> {
852 let packages_entry = doc.as_table_mut().entry("package");
853
854 match packages_entry {
855 toml_edit::Entry::Occupied(mut entry) => {
856 let tables = entry
857 .get_mut()
858 .as_array_of_tables_mut()
859 .expect("Expected lockfile to contain an array of tables named 'packages'");
860
861 let matching_index = tables.iter().position(|table| {
862 table
863 .get("name")
864 .and_then(|item| item.as_str())
865 .map(|name| name == package_name)
866 .unwrap_or_default()
867 });
868
869 if let Some(matching_index) = matching_index {
870 let table = tables
871 .get_mut(matching_index)
872 .expect("Expected lockfile to contain reference to self");
873 table_add_or_update_value(table, "version", next_release_version.into());
874 } else {
875 return Err(Error::CargoLockfileUpdate {
876 reason: "Unable to locate self-referential metadata in lockfile".into(),
877 package_name: package_name.to_owned(),
878 });
879 }
880 }
881 _ => {
882 return Err(Error::CargoLockfileUpdate {
883 reason: "Cargo lockfile does not contain 'packages' array of tables".into(),
884 package_name: package_name.to_owned(),
885 })
886 }
887 };
888
889 Ok(())
890}
891
892#[derive(Debug)]
894pub enum DependencyType {
895 Normal,
897
898 Build,
900
901 Dev,
903}
904
905impl DependencyType {
906 fn key(&self) -> &str {
907 use DependencyType::*;
908
909 match self {
910 Normal => "dependencies",
911 Build => "build-dependencies",
912 Dev => "dev-dependencies",
913 }
914 }
915}
916
917impl fmt::Display for DependencyType {
918 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
919 use DependencyType::*;
920
921 match self {
922 Normal => write!(f, "Dependency"),
923 Build => write!(f, "Build dependency"),
924 Dev => write!(f, "Dev dependency"),
925 }
926 }
927}
928
929#[derive(Debug, Serialize)]
930struct Release {
931 name: String,
932 url: Option<Url>,
933}
934
935impl Release {
936 fn new<URL: AsRef<str>>(
937 name: impl AsRef<str>,
938 url: Option<URL>,
939 main_crate: impl AsRef<str>,
940 ) -> Result<Self> {
941 let url = if let Some(url) = url {
942 let base = Url::parse(url.as_ref()).map_err(Error::url_parse_error)?;
943 let url = base
944 .join(main_crate.as_ref())
945 .map_err(Error::url_parse_error)?;
946 Some(url)
947 } else {
948 None
949 };
950
951 Ok(Self {
952 name: name.as_ref().to_owned(),
953 url,
954 })
955 }
956
957 fn new_crates_io_release(name: impl AsRef<str>, main_crate: impl AsRef<str>) -> Result<Self> {
958 let base = Url::parse("https://crates.io/crates/").map_err(Error::url_parse_error)?;
959 let url = base
960 .join(main_crate.as_ref())
961 .map_err(Error::url_parse_error)?;
962
963 Ok(Self {
964 name: name.as_ref().to_owned(),
965 url: Some(url),
966 })
967 }
968}