diff --git a/Cargo.lock b/Cargo.lock index b637f5917..9ad64e304 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1204,6 +1204,39 @@ dependencies = [ "libbz2-rs-sys", ] +[[package]] +name = "camino" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122ec45a44b270afd1402f351b782c676b173e3c3fb28d86ff7ebfb4d86a4ee4" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "981a6f317983eec002839b90fae7411a85621410ae591a9cab2ecf5cb5744873" +dependencies = [ + "camino", + "cargo-platform", + "derive_builder", + "semver", + "serde", + "serde_json", + "thiserror 2.0.17", +] + [[package]] name = "caseless" version = "0.2.2" @@ -2025,6 +2058,7 @@ dependencies = [ "backtrace", "base64 0.22.1", "bzip2", + "cargo_metadata", "chrono", "clap", "comrak", diff --git a/Cargo.toml b/Cargo.toml index 67b3a9b21..92b069791 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ aws-sdk-cloudfront = "1.3.0" aws-smithy-types-convert = { version = "0.60.0", features = ["convert-chrono"] } http = "1.0.0" uuid = { version = "1.1.2", features = ["v4"]} +cargo_metadata = { version = "0.23.0", features = ["builder"] } # Data serialization and deserialization serde = { version = "1.0", features = ["derive"] } diff --git a/src/db/add_package.rs b/src/db/add_package.rs index 4f97c0e2b..ec0a3b154 100644 --- a/src/db/add_package.rs +++ b/src/db/add_package.rs @@ -7,7 +7,7 @@ use crate::{ error::Result, registry_api::{CrateData, CrateOwner, ReleaseData}, storage::CompressionAlgorithm, - utils::{MetadataPackage, rustc_version::parse_rustc_date}, + utils::{cargo_metadata::PackageExt, rustc_version::parse_rustc_date}, web::crate_details::{latest_release, releases_for_crate}, }; use anyhow::{Context, anyhow}; @@ -34,7 +34,7 @@ pub(crate) async fn finish_release( conn: &mut sqlx::PgConnection, crate_id: CrateId, release_id: ReleaseId, - metadata_pkg: &MetadataPackage, + metadata_pkg: &cargo_metadata::Package, source_dir: &Path, default_target: &str, source_files: Value, @@ -388,7 +388,7 @@ pub(crate) async fn initialize_build( } /// Reads features and converts them to Vec with default being first -fn get_features(pkg: &MetadataPackage) -> Vec { +fn get_features(pkg: &cargo_metadata::Package) -> Vec { let mut features = Vec::with_capacity(pkg.features.len()); if let Some(subfeatures) = pkg.features.get("default") { features.push(Feature::new("default".into(), subfeatures.clone())); @@ -403,8 +403,8 @@ fn get_features(pkg: &MetadataPackage) -> Vec { } /// Reads readme if there is any read defined in Cargo.toml of a Package -fn get_readme(pkg: &MetadataPackage, source_dir: &Path) -> Result> { - let readme_path = source_dir.join(pkg.readme.as_deref().unwrap_or("README.md")); +fn get_readme(pkg: &cargo_metadata::Package, source_dir: &Path) -> Result> { + let readme_path = source_dir.join(pkg.readme.as_deref().unwrap_or("README.md".into())); if !readme_path.exists() { return Ok(None); @@ -424,8 +424,8 @@ fn get_readme(pkg: &MetadataPackage, source_dir: &Path) -> Result } } -fn get_rustdoc(pkg: &MetadataPackage, source_dir: &Path) -> Result> { - if let Some(src_path) = &pkg.targets.first().and_then(|t| t.src_path.as_ref()) { +fn get_rustdoc(pkg: &cargo_metadata::Package, source_dir: &Path) -> Result> { + if let Some(src_path) = &pkg.targets.first().map(|t| &t.src_path) { let src_path = Path::new(src_path); if src_path.is_absolute() { read_rust_doc(src_path) @@ -473,7 +473,7 @@ fn read_rust_doc(file_path: &Path) -> Result> { /// Adds keywords into database async fn add_keywords_into_database( conn: &mut sqlx::PgConnection, - pkg: &MetadataPackage, + pkg: &cargo_metadata::Package, release_id: ReleaseId, ) -> Result<()> { let wanted_keywords: HashMap = pkg @@ -625,7 +625,7 @@ mod test { use super::*; use crate::registry_api::OwnerKind; use crate::test::*; - use crate::utils::CargoMetadata; + use crate::utils::cargo_metadata::{MetadataExt as _, load_cargo_metadata_from_host_path}; use chrono::NaiveDate; use std::slice; use test_case::test_case; @@ -1171,7 +1171,7 @@ mod test { "#; std::fs::write(dir.path().join("Cargo.toml"), [base, extra].concat())?; - let metadata = CargoMetadata::load_from_host_path(dir.path())?; + let metadata = load_cargo_metadata_from_host_path(dir.path())?; let features = super::get_features(metadata.root()); assert_eq!(features, expected.as_ref()); diff --git a/src/db/types/dependencies.rs b/src/db/types/dependencies.rs index c80c8d55f..7e86830bf 100644 --- a/src/db/types/dependencies.rs +++ b/src/db/types/dependencies.rs @@ -1,13 +1,16 @@ -use crate::utils::Dependency; -use derive_more::Deref; +use cargo_metadata::{Dependency, DependencyKind}; use semver::VersionReq; use serde::{Deserialize, Serialize}; -const DEFAULT_KIND: &str = "normal"; - -/// A crate dependency in our internal representation for releases.dependencies json. -#[derive(Debug, Clone, PartialEq, Deref)] -pub(crate) struct ReleaseDependency(Dependency); +/// A subset of `cargo_metadata::Dependency`. +/// Only the data we store in our `releases.dependencies` column. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct ReleaseDependency { + pub(crate) name: String, + pub(crate) req: VersionReq, + pub(crate) kind: DependencyKind, + pub(crate) optional: bool, +} impl<'de> Deserialize<'de> for ReleaseDependency { fn deserialize(deserializer: D) -> Result @@ -22,25 +25,24 @@ impl<'de> Deserialize<'de> for ReleaseDependency { /// just [name, version]`` Basic((String, VersionReq)), /// [name, version, kind] - WithKind((String, VersionReq, String)), + WithKind((String, VersionReq, DependencyKind)), /// [name, version, kind, optional] - Full((String, VersionReq, String, bool)), + Full((String, VersionReq, DependencyKind, bool)), } let src = Repr::deserialize(deserializer)?; let (name, req, kind, optional) = match src { - Repr::Basic((name, req)) => (name, req, DEFAULT_KIND.into(), false), + Repr::Basic((name, req)) => (name, req, DependencyKind::default(), false), Repr::WithKind((name, req, kind)) => (name, req, kind, false), Repr::Full((name, req, kind, optional)) => (name, req, kind, optional), }; - Ok(ReleaseDependency(Dependency { + Ok(ReleaseDependency { name, req, - kind: Some(kind), + kind, optional, - rename: None, - })) + }) } } @@ -49,21 +51,18 @@ impl Serialize for ReleaseDependency { where S: serde::Serializer, { - let dep = &self.0; - let kind = dep.kind.as_deref().unwrap_or(DEFAULT_KIND); - (dep.name.as_str(), &dep.req, kind, dep.optional).serialize(serializer) + (self.name.as_str(), &self.req, &self.kind, self.optional).serialize(serializer) } } impl From for ReleaseDependency { fn from(dep: Dependency) -> Self { - ReleaseDependency(dep) - } -} - -impl From for Dependency { - fn from(dep: ReleaseDependency) -> Self { - dep.0 + ReleaseDependency { + name: dep.name, + req: dep.req, + kind: dep.kind, + optional: dep.optional, + } } } @@ -103,12 +102,20 @@ mod tests { Ok(()) } - #[test_case(r#"[["vec_map", "^0.0.1"]]"#, "normal", false)] - #[test_case(r#"[["vec_map", "^0.0.1", "dev" ]]"#, "dev", false)] - #[test_case(r#"[["vec_map", "^0.0.1", "dev", true ]]"#, "dev", true)] + #[test_case(r#"[["vec_map", "^0.0.1"]]"#, DependencyKind::Normal, false)] + #[test_case( + r#"[["vec_map", "^0.0.1", "dev" ]]"#, + DependencyKind::Development, + false + )] + #[test_case( + r#"[["vec_map", "^0.0.1", "dev", true ]]"#, + DependencyKind::Development, + true + )] fn test_parse_dependency( input: &str, - expected_kind: &str, + expected_kind: DependencyKind, expected_optional: bool, ) -> Result<()> { let deps: ReleaseDependencyList = serde_json::from_str(input)?; @@ -118,7 +125,7 @@ mod tests { assert_eq!(dep.name, "vec_map"); assert_eq!(dep.req, VersionReq::parse("^0.0.1")?); - assert_eq!(dep.kind.as_deref(), Some(expected_kind)); + assert_eq!(dep.kind, expected_kind); assert_eq!(dep.optional, expected_optional); Ok(()) diff --git a/src/docbuilder/rustwide_builder.rs b/src/docbuilder/rustwide_builder.rs index d02880a96..6be0fde30 100644 --- a/src/docbuilder/rustwide_builder.rs +++ b/src/docbuilder/rustwide_builder.rs @@ -17,8 +17,9 @@ use crate::{ rustdoc_archive_path, rustdoc_json_path, source_archive_path, }, utils::{ - CargoMetadata, ConfigName, MetadataPackage, copy_dir_all, get_config, parse_rustc_version, - report_error, set_config, + ConfigName, + cargo_metadata::{MetadataExt as _, PackageExt as _, load_cargo_metadata_from_rustwide}, + copy_dir_all, get_config, parse_rustc_version, report_error, set_config, }, }; use anyhow::{Context as _, Error, anyhow, bail}; @@ -379,14 +380,14 @@ impl RustwideBuilder { } pub fn build_local_package(&mut self, path: &Path) -> Result { - let metadata = CargoMetadata::load_from_rustwide(&self.workspace, &self.toolchain, path) + let metadata = load_cargo_metadata_from_rustwide(&self.workspace, &self.toolchain, path) .map_err(|err| { err.context(format!("failed to load local package {}", path.display())) })?; let package = metadata.root(); self.build_package( &package.name, - &package.version, + &package.version(), PackageKind::Local(path), false, ) @@ -1001,7 +1002,7 @@ impl RustwideBuilder { create_essential_files: bool, collect_metrics: bool, ) -> Result { - let cargo_metadata = CargoMetadata::load_from_rustwide( + let cargo_metadata = load_cargo_metadata_from_rustwide( &self.workspace, &self.toolchain, &build.host_source_dir(), @@ -1232,7 +1233,7 @@ impl RustwideBuilder { copy_dir_all(source, dest).map_err(Into::into) } - fn get_repo(&self, metadata: &MetadataPackage) -> Result> { + fn get_repo(&self, metadata: &cargo_metadata::Package) -> Result> { self.runtime .block_on(self.repository_stats_updater.load_repository(metadata)) } @@ -1241,7 +1242,7 @@ impl RustwideBuilder { struct FullBuildResult { result: BuildResult, target: String, - cargo_metadata: CargoMetadata, + cargo_metadata: cargo_metadata::Metadata, doc_coverage: Option, build_log: String, } @@ -1286,10 +1287,12 @@ impl Default for BuildPackageSummary { #[cfg(test)] mod tests { use super::*; - use crate::db::types::Feature; - use crate::registry_api::ReleaseData; - use crate::storage::{CompressionAlgorithm, compression}; - use crate::test::{AxumRouterTestExt, TestEnvironment}; + use crate::{ + db::types::Feature, + registry_api::ReleaseData, + storage::{CompressionAlgorithm, compression}, + test::{AxumRouterTestExt, TestEnvironment, dummy_metadata_package}, + }; use pretty_assertions::assert_eq; use std::{io, iter}; use test_case::test_case; @@ -1658,21 +1661,7 @@ mod tests { &mut conn, crate_id, release_id, - &MetadataPackage { - name: crate_.into(), - version: version.clone(), - id: "".into(), - license: None, - repository: None, - homepage: None, - description: None, - documentation: None, - dependencies: vec![], - targets: vec![], - readme: None, - keywords: vec![], - features: HashMap::new(), - }, + &dummy_metadata_package().build().unwrap(), Path::new("/unknown/"), "x86_64-unknown-linux-gnu", serde_json::Value::Array(vec![]), diff --git a/src/repositories/updater.rs b/src/repositories/updater.rs index 43d29acd0..2bc3c5db6 100644 --- a/src/repositories/updater.rs +++ b/src/repositories/updater.rs @@ -1,6 +1,5 @@ use crate::error::Result; use crate::repositories::{GitHub, GitLab, RateLimitReached}; -use crate::utils::MetadataPackage; use crate::{Config, db::Pool}; use async_trait::async_trait; use chrono::{DateTime, Utc}; @@ -81,7 +80,10 @@ impl RepositoryStatsUpdater { Self { updaters, pool } } - pub(crate) async fn load_repository(&self, metadata: &MetadataPackage) -> Result> { + pub(crate) async fn load_repository( + &self, + metadata: &cargo_metadata::Package, + ) -> Result> { let url = match &metadata.repository { Some(url) => url, None => { diff --git a/src/test/fakes.rs b/src/test/fakes.rs index 4c4660089..387cf9e08 100644 --- a/src/test/fakes.rs +++ b/src/test/fakes.rs @@ -14,12 +14,18 @@ use crate::{ AsyncStorage, CompressionAlgorithm, RustdocJsonFormatVersion, compress, rustdoc_archive_path, rustdoc_json_path, source_archive_path, }, - utils::{Dependency, MetadataPackage, cargo_metadata::Target}, + utils::cargo_metadata::PackageExt as _, }; use anyhow::{Context, bail}; use base64::{Engine, engine::general_purpose::STANDARD as b64}; +use cargo_metadata::{PackageId, PackageName}; use chrono::{DateTime, Utc}; -use std::{collections::HashMap, fmt, iter, sync::Arc}; +use semver::VersionReq; +use std::{ + collections::{BTreeMap, HashMap}, + fmt, iter, + sync::Arc, +}; use tracing::debug; /// Create a fake release in the database that failed before the build. @@ -61,7 +67,7 @@ where pub(crate) struct FakeRelease<'a> { db: &'a TestDatabase, storage: Arc, - package: MetadataPackage, + package: cargo_metadata::Package, builds: Option>, /// name, content source_files: Vec<(&'a str, &'a [u8])>, @@ -98,35 +104,7 @@ impl<'a> FakeRelease<'a> { FakeRelease { db, storage, - package: MetadataPackage { - id: "fake-package-id".into(), - name: "fake-package".into(), - version: Version::new(1, 0, 0), - license: Some("MIT".into()), - repository: Some("https://git.example.com".into()), - homepage: Some("https://www.example.com".into()), - description: Some("Fake package".into()), - documentation: Some("https://docs.example.com".into()), - dependencies: vec![Dependency { - name: "fake-dependency".into(), - req: semver::VersionReq::parse("^1.0.0").unwrap(), - kind: None, - rename: None, - optional: false, - }], - targets: vec![Target::dummy_lib("fake_package".into(), None)], - readme: None, - keywords: vec!["fake".into(), "package".into()], - features: [ - ("default".into(), vec!["feature1".into(), "feature3".into()]), - ("feature1".into(), Vec::new()), - ("feature2".into(), vec!["feature1".into()]), - ("feature3".into(), Vec::new()), - ] - .iter() - .cloned() - .collect::>>(), - }, + package: dummy_metadata_package().build().unwrap(), builds: None, source_files: Vec::new(), rustdoc_files: Vec::new(), @@ -153,7 +131,7 @@ impl<'a> FakeRelease<'a> { self } - pub(crate) fn add_dependency(mut self, dependency: Dependency) -> Self { + pub(crate) fn add_dependency(mut self, dependency: cargo_metadata::Dependency) -> Self { self.package.dependencies.push(dependency); self } @@ -164,8 +142,10 @@ impl<'a> FakeRelease<'a> { } pub(crate) fn name(mut self, new: &str) -> Self { - self.package.name = new.into(); - self.package.id = format!("{new}-id"); + self.package.name = PackageName::new(new.into()); + self.package.id = PackageId { + repr: format!("{new}-id"), + }; self.package.targets[0].name = new.into(); self } @@ -175,7 +155,8 @@ impl<'a> FakeRelease<'a> { V: TryInto, V::Error: fmt::Debug, { - self.package.version = new.try_into().expect("invalid version"); + let version: Version = new.try_into().expect("invalid version"); + self.package.version = version.into(); self } @@ -244,7 +225,7 @@ impl<'a> FakeRelease<'a> { pub(crate) fn target_source(mut self, path: &'a str) -> Self { if let Some(target) = self.package.targets.first_mut() { - target.src_path = Some(path.into()); + target.src_path = path.into(); } self } @@ -283,7 +264,11 @@ impl<'a> FakeRelease<'a> { pub(crate) fn add_platform>(mut self, platform: S) -> Self { let platform = platform.into(); let name = self.package.targets[0].name.clone(); - let target = Target::dummy_lib(name, Some(platform.clone())); + let target = dummy_metadata_target() + .name(name) + .src_path(platform.clone()) + .build() + .unwrap(); self.package.targets.push(target); self.doc_targets.push(platform); self @@ -313,7 +298,7 @@ impl<'a> FakeRelease<'a> { } } - pub(crate) fn features(mut self, features: HashMap>) -> Self { + pub(crate) fn features(mut self, features: BTreeMap>) -> Self { self.package.features = features; self } @@ -402,7 +387,7 @@ impl<'a> FakeRelease<'a> { kind: FileKind, source_directory: &Path, archive_storage: bool, - package: &MetadataPackage, + package: &cargo_metadata::Package, storage: &AsyncStorage, ) -> Result<(Vec, CompressionAlgorithm)> { debug!( @@ -412,12 +397,14 @@ impl<'a> FakeRelease<'a> { ); if archive_storage { let (archive, public) = match kind { - FileKind::Rustdoc => { - (rustdoc_archive_path(&package.name, &package.version), true) - } - FileKind::Sources => { - (source_archive_path(&package.name, &package.version), false) - } + FileKind::Rustdoc => ( + rustdoc_archive_path(&package.name, &package.version()), + true, + ), + FileKind::Sources => ( + source_archive_path(&package.name, &package.version()), + false, + ), }; debug!("store in archive: {:?}", archive); let (files_list, new_alg) = crate::db::add_path_into_remote_archive( @@ -452,7 +439,7 @@ impl<'a> FakeRelease<'a> { .iter() .any(|&(path, _)| path == "Cargo.toml") { - let MetadataPackage { name, version, .. } = &package; + let cargo_metadata::Package { name, version, .. } = &package; let content = format!( r#" [package] @@ -490,8 +477,8 @@ impl<'a> FakeRelease<'a> { debug!("added rustdoc files"); for target in &package.targets[1..] { - let platform = target.src_path.as_ref().unwrap(); - let platform_dir = rustdoc_path.join(platform); + let platform = target.src_path.to_path_buf(); + let platform_dir = rustdoc_path.join(&platform); fs::create_dir(&platform_dir)?; store_files_into(&rustdoc_files, &platform_dir)?; @@ -544,7 +531,7 @@ impl<'a> FakeRelease<'a> { .store_one_uncompressed( &rustdoc_json_path( &package.name, - &package.version, + &package.version(), target, format_version, Some(*alg), @@ -561,8 +548,7 @@ impl<'a> FakeRelease<'a> { // non-linux platforms. let mut async_conn = db.async_conn().await; let crate_id = initialize_crate(&mut async_conn, &package.name).await?; - let release_id = initialize_release(&mut async_conn, crate_id, &package.version).await?; - + let release_id = initialize_release(&mut async_conn, crate_id, &package.version()).await?; crate::db::finish_release( &mut async_conn, crate_id, @@ -749,3 +735,76 @@ impl Default for FakeBuild { } } } + +pub(crate) fn dummy_metadata_dependency() -> cargo_metadata::DependencyBuilder { + cargo_metadata::DependencyBuilder::default() + .name("fake-dependency") + .source(None) + .req(VersionReq::parse("^1.0.0").unwrap()) + .kind(cargo_metadata::DependencyKind::Normal) + .optional(false) + .uses_default_features(true) + .features(vec![]) + .target(None) + .rename(None) + .registry(None) + .path(None) +} + +pub(crate) fn dummy_metadata_target() -> cargo_metadata::TargetBuilder { + cargo_metadata::TargetBuilder::default() + .name("fake_package") + .kind(vec![cargo_metadata::TargetKind::Lib]) + .crate_types(vec![cargo_metadata::CrateType::Lib]) + .required_features(vec![]) + .src_path("src/lib.rs") +} + +pub(crate) fn dummy_metadata_package() -> cargo_metadata::PackageBuilder { + // use ::new()? + cargo_metadata::PackageBuilder::default() + .id(cargo_metadata::PackageId { + repr: "fake-package-id".into(), + }) + .name( + "fake-package" + .parse::() + .unwrap(), + ) + .version(Version::new(1, 0, 0)) + .manifest_path("Cargo.toml") + .license(Some("MIT".to_string())) + .repository(Some("https://git.example.com".into())) + .homepage(Some("https://www.example.com".into())) + .description(Some("Fake package".into())) + .documentation(Some("https://docs.example.com".into())) + .dependencies(vec![dummy_metadata_dependency().build().unwrap()]) + .targets(vec![dummy_metadata_target().build().unwrap()]) + .keywords(vec!["fake".into(), "package".into()]) + .features( + [ + ("default".into(), vec!["feature1".into(), "feature3".into()]), + ("feature1".into(), Vec::new()), + ("feature2".into(), vec!["feature1".into()]), + ("feature3".into(), Vec::new()), + ] + .iter() + .cloned() + .collect::>>(), + ) +} + +#[test] +fn test_dummy_metadata_dependency_is_complete() { + dummy_metadata_dependency().build().unwrap(); +} + +#[test] +fn test_dummy_metadata_target_is_complete() { + dummy_metadata_target().build().unwrap(); +} + +#[test] +fn test_dummy_metadata_package_is_complete() { + dummy_metadata_package().build().unwrap(); +} diff --git a/src/test/mod.rs b/src/test/mod.rs index 3cde6e038..1e0dbc29d 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -1,6 +1,9 @@ mod fakes; -pub(crate) use self::fakes::{FakeBuild, fake_release_that_failed_before_build}; +pub(crate) use self::fakes::{ + FakeBuild, dummy_metadata_dependency, dummy_metadata_package, + fake_release_that_failed_before_build, +}; use crate::{ AsyncBuildQueue, BuildQueue, Config, Context, InstanceMetrics, cdn::CdnBackend, diff --git a/src/utils/cargo_metadata.rs b/src/utils/cargo_metadata.rs index f5327d686..58d98062f 100644 --- a/src/utils/cargo_metadata.rs +++ b/src/utils/cargo_metadata.rs @@ -1,88 +1,70 @@ use crate::{db::types::version::Version, error::Result}; -use anyhow::{Context, bail}; +use anyhow::bail; +use cargo_metadata::{CrateType, Metadata, Target}; use rustwide::{Toolchain, Workspace, cmd::Command}; -use semver::VersionReq; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; use std::path::Path; -pub(crate) struct CargoMetadata { - root: Package, +pub(crate) fn load_cargo_metadata_from_rustwide( + workspace: &Workspace, + toolchain: &Toolchain, + source_dir: &Path, +) -> Result { + let res = Command::new(workspace, toolchain.cargo()) + .args(&["metadata", "--format-version", "1"]) + .cd(source_dir) + .log_output(false) + .run_capture()?; + let [metadata] = res.stdout_lines() else { + bail!("invalid output returned by `cargo metadata`") + }; + + Ok(serde_json::from_str(metadata)?) } -impl CargoMetadata { - pub(crate) fn load_from_rustwide( - workspace: &Workspace, - toolchain: &Toolchain, - source_dir: &Path, - ) -> Result { - let res = Command::new(workspace, toolchain.cargo()) - .args(&["metadata", "--format-version", "1"]) - .cd(source_dir) - .log_output(false) - .run_capture()?; - let [metadata] = res.stdout_lines() else { - bail!("invalid output returned by `cargo metadata`") - }; - Self::load_from_metadata(metadata) - } - - #[cfg(test)] - pub(crate) fn load_from_host_path(source_dir: &Path) -> Result { - let res = std::process::Command::new("cargo") - .args(["metadata", "--format-version", "1", "--offline"]) - .current_dir(source_dir) - .output()?; - let status = res.status; - if !status.success() { - let stderr = std::str::from_utf8(&res.stderr).unwrap_or(""); - bail!("error returned by `cargo metadata`: {status}\n{stderr}") - } - Self::load_from_metadata(std::str::from_utf8(&res.stdout)?) +#[cfg(test)] +pub(crate) fn load_cargo_metadata_from_host_path(source_dir: &Path) -> Result { + let res = std::process::Command::new("cargo") + .args(["metadata", "--format-version", "1", "--offline"]) + .current_dir(source_dir) + .output()?; + let status = res.status; + if !status.success() { + let stderr = std::str::from_utf8(&res.stderr).unwrap_or(""); + bail!("error returned by `cargo metadata`: {status}\n{stderr}") } + Ok(serde_json::from_slice(&res.stdout)?) +} - pub(crate) fn load_from_metadata(metadata: &str) -> Result { - let metadata = serde_json::from_str::(metadata)?; - let root = metadata.resolve.root; - Ok(CargoMetadata { - root: metadata - .packages - .into_iter() - .find(|pkg| pkg.id == root) - .context("metadata.packages missing root package")?, - }) - } +pub(crate) trait MetadataExt { + fn root(&self) -> &cargo_metadata::Package; +} - pub(crate) fn root(&self) -> &Package { - &self.root +impl MetadataExt for cargo_metadata::Metadata { + fn root(&self) -> &cargo_metadata::Package { + self.root_package().as_ref().expect("missing root package") } } -#[derive(Debug, Deserialize, Serialize)] -pub(crate) struct Package { - pub(crate) id: String, - pub(crate) name: String, - pub(crate) version: Version, - pub(crate) license: Option, - pub(crate) repository: Option, - pub(crate) homepage: Option, - pub(crate) description: Option, - pub(crate) documentation: Option, - pub(crate) dependencies: Vec, - pub(crate) targets: Vec, - pub(crate) readme: Option, - pub(crate) keywords: Vec, - pub(crate) features: HashMap>, +pub(crate) trait PackageExt { + fn library_target(&self) -> Option<&Target>; + fn is_library(&self) -> bool; + fn normalize_package_name(&self, name: &str) -> String; + fn package_name(&self) -> String; + fn library_name(&self) -> Option; + fn version(&self) -> Version; } -impl Package { +impl PackageExt for cargo_metadata::Package { fn library_target(&self) -> Option<&Target> { - self.targets - .iter() - .find(|target| target.crate_types.iter().any(|kind| kind != "bin")) + self.targets.iter().find(|target| { + target + .crate_types + .iter() + .any(|kind| kind != &CrateType::Bin) + }) } - pub(crate) fn is_library(&self) -> bool { + fn is_library(&self) -> bool { self.library_target().is_some() } @@ -90,7 +72,7 @@ impl Package { name.replace('-', "_") } - pub(crate) fn package_name(&self) -> String { + fn package_name(&self) -> String { self.library_name().unwrap_or_else(|| { self.targets .first() @@ -99,80 +81,12 @@ impl Package { }) } - pub(crate) fn library_name(&self) -> Option { + fn library_name(&self) -> Option { self.library_target() .map(|target| self.normalize_package_name(&target.name)) } -} - -#[derive(Debug, Deserialize, Serialize)] -pub(crate) struct Target { - pub(crate) name: String, - #[cfg(not(test))] - crate_types: Vec, - #[cfg(test)] - pub(crate) crate_types: Vec, - pub(crate) src_path: Option, -} -impl Target { - #[cfg(test)] - pub(crate) fn dummy_lib(name: String, src_path: Option) -> Self { - Target { - name, - crate_types: vec!["lib".into()], - src_path, - } + fn version(&self) -> Version { + self.version.clone().into() } } - -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] -pub(crate) struct Dependency { - pub(crate) name: String, - pub(crate) req: VersionReq, - pub(crate) kind: Option, - pub(crate) rename: Option, - pub(crate) optional: bool, -} - -impl Dependency { - #[cfg(test)] - pub fn new(name: String, req: VersionReq) -> Dependency { - Dependency { - name, - req, - kind: None, - rename: None, - optional: false, - } - } - - #[cfg(test)] - pub fn set_optional(mut self, optional: bool) -> Self { - self.optional = optional; - self - } -} - -#[derive(Deserialize, Serialize)] -struct DeserializedMetadata { - packages: Vec, - resolve: DeserializedResolve, -} - -#[derive(Deserialize, Serialize)] -struct DeserializedResolve { - root: String, - nodes: Vec, -} - -#[derive(Deserialize, Serialize)] -struct DeserializedResolveNode { - id: String, - deps: Vec, -} - -#[derive(Deserialize, Serialize)] -struct DeserializedResolveDep { - pkg: String, -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 93b239ce7..1f697edcc 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,7 +1,6 @@ //! Various utilities for docs.rs pub(crate) use self::{ - cargo_metadata::{CargoMetadata, Dependency, Package as MetadataPackage}, copy::copy_dir_all, html::rewrite_rustdoc_html_stream, rustc_version::{get_correct_docsrs_style_file, parse_rustc_version}, diff --git a/src/web/build_details.rs b/src/web/build_details.rs index cc9a99b92..65dff64dd 100644 --- a/src/web/build_details.rs +++ b/src/web/build_details.rs @@ -168,7 +168,7 @@ pub(crate) async fn build_details_handler( #[cfg(test)] mod tests { use crate::test::{ - AxumResponseTestExt, AxumRouterTestExt, FakeBuild, async_wrapper, + AxumResponseTestExt, AxumRouterTestExt, FakeBuild, V1, async_wrapper, fake_release_that_failed_before_build, }; use kuchikiki::traits::TendrilSink; @@ -191,18 +191,14 @@ mod tests { fn test_partial_build_result() { async_wrapper(|env| async move { let mut conn = env.async_db().async_conn().await; - let (_, build_id) = fake_release_that_failed_before_build( - &mut conn, - "foo", - "0.1.0", - "some random error", - ) - .await?; + let (_, build_id) = + fake_release_that_failed_before_build(&mut conn, "foo", V1, "some random error") + .await?; let page = kuchikiki::parse_html().one( env.web_app() .await - .get(&format!("/crate/foo/0.1.0/builds/{build_id}")) + .get(&format!("/crate/foo/{V1}/builds/{build_id}")) .await? .error_for_status()? .text() @@ -222,13 +218,9 @@ mod tests { fn test_partial_build_result_plus_default_target_from_previous_build() { async_wrapper(|env| async move { let mut conn = env.async_db().async_conn().await; - let (release_id, build_id) = fake_release_that_failed_before_build( - &mut conn, - "foo", - "0.1.0", - "some random error", - ) - .await?; + let (release_id, build_id) = + fake_release_that_failed_before_build(&mut conn, "foo", V1, "some random error") + .await?; sqlx::query!( "UPDATE releases SET default_target = 'x86_64-unknown-linux-gnu' WHERE id = $1", @@ -240,7 +232,7 @@ mod tests { let page = kuchikiki::parse_html().one( env.web_app() .await - .get(&format!("/crate/foo/0.1.0/builds/{build_id}")) + .get(&format!("/crate/foo/{V1}/builds/{build_id}")) .await? .error_for_status()? .text() diff --git a/src/web/builds.rs b/src/web/builds.rs index 65660a2dc..41bf261c9 100644 --- a/src/web/builds.rs +++ b/src/web/builds.rs @@ -234,12 +234,12 @@ mod tests { fn build_list_empty_build() { async_wrapper(|env| async move { let mut conn = env.async_db().async_conn().await; - fake_release_that_failed_before_build(&mut conn, "foo", "0.1.0", "some errors").await?; + fake_release_that_failed_before_build(&mut conn, "foo", V1, "some errors").await?; let response = env .web_app() .await - .get("/crate/foo/0.1.0/builds") + .get(&format!("/crate/foo/{V1}/builds")) .await? .error_for_status()?; response.assert_cache_control(CachePolicy::NoCaching, env.config()); diff --git a/src/web/crate_details.rs b/src/web/crate_details.rs index 540a31d61..7481890be 100644 --- a/src/web/crate_details.rs +++ b/src/web/crate_details.rs @@ -7,7 +7,7 @@ use crate::{ impl_axum_webpage, registry_api::OwnerKind, storage::PathNotFoundError, - utils::{Dependency, get_correct_docsrs_style_file}, + utils::get_correct_docsrs_style_file, web::{ MatchedRelease, MetaData, ReqVersion, cache::CachePolicy, @@ -40,7 +40,7 @@ pub(crate) struct CrateDetails { pub(crate) version: Version, pub(crate) description: Option, pub(crate) owners: Vec<(String, String, OwnerKind)>, - pub(crate) dependencies: Vec, + pub(crate) dependencies: ReleaseDependencyList, readme: Option, rustdoc: Option, // this is description_long in database release_time: Option>, @@ -234,7 +234,7 @@ impl CrateDetails { let parsed_license = krate.license.as_deref().map(super::licenses::parse_license); - let dependencies: Vec = krate + let dependencies = krate .dependencies .map(serde_json::from_value::) .transpose() @@ -242,10 +242,7 @@ impl CrateDetails { // (at the time writing, 14 releases out of 2 million). // We silently ignore those here. .unwrap_or_default() - .unwrap_or_default() - .into_iter() - .map(Into::into) - .collect(); + .unwrap_or_default(); let mut crate_details = CrateDetails { name: krate.name, @@ -448,7 +445,7 @@ struct CrateDetailsPage { documentation_url: Option, repository_url: Option, repository_metadata: Option, - dependencies: Vec, + dependencies: ReleaseDependencyList, releases: Vec, readme: Option, build_status: BuildStatus, @@ -712,7 +709,7 @@ pub(crate) async fn get_all_platforms( mod tests { use super::*; use crate::test::{ - AxumResponseTestExt, AxumRouterTestExt, FakeBuild, TestDatabase, TestEnvironment, + AxumResponseTestExt, AxumRouterTestExt, FakeBuild, TestDatabase, TestEnvironment, V1, async_wrapper, fake_release_that_failed_before_build, }; use crate::{db::update_build_status, registry_api::CrateOwner}; @@ -720,7 +717,7 @@ mod tests { use kuchikiki::traits::TendrilSink; use pretty_assertions::assert_eq; use reqwest::StatusCode; - use std::collections::HashMap; + use std::collections::BTreeMap; async fn release_build_status( conn: &mut sqlx::PgConnection, @@ -1556,7 +1553,7 @@ mod tests { .await .name("library") .version("0.1.0") - .features(HashMap::new()) + .features(BTreeMap::new()) .create() .await?; @@ -1579,7 +1576,7 @@ mod tests { let features = [("_private".into(), Vec::new())] .iter() .cloned() - .collect::>>(); + .collect::>>(); env.fake_release() .await .name("library") @@ -1607,7 +1604,7 @@ mod tests { let features = [("feature1".into(), Vec::new())] .iter() .cloned() - .collect::>>(); + .collect::>>(); env.fake_release() .await .name("library") @@ -1643,7 +1640,7 @@ mod tests { ] .iter() .cloned() - .collect::>>(); + .collect::>>(); env.fake_release() .await .name("library") @@ -1743,12 +1740,12 @@ mod tests { fn test_minimal_failed_release_doesnt_error_features() { async_wrapper(|env| async move { let mut conn = env.async_db().async_conn().await; - fake_release_that_failed_before_build(&mut conn, "foo", "0.1.0", "some errors").await?; + fake_release_that_failed_before_build(&mut conn, "foo", V1, "some errors").await?; let text_content = env .web_app() .await - .get("/crate/foo/0.1.0/features") + .get(&format!("/crate/foo/{V1}/features")) .await? .error_for_status()? .text() @@ -1767,12 +1764,12 @@ mod tests { fn test_minimal_failed_release_doesnt_error() { async_wrapper(|env| async move { let mut conn = env.async_db().async_conn().await; - fake_release_that_failed_before_build(&mut conn, "foo", "0.1.0", "some errors").await?; + fake_release_that_failed_before_build(&mut conn, "foo", V1, "some errors").await?; let text_content = env .web_app() .await - .get("/crate/foo/0.1.0") + .get(&format!("/crate/foo/{V1}")) .await? .error_for_status()? .text() diff --git a/src/web/features.rs b/src/web/features.rs index 633d5d5b8..24f2b2330 100644 --- a/src/web/features.rs +++ b/src/web/features.rs @@ -395,7 +395,7 @@ mod tests { .await .name("foo") .version("0.2.1") - .features(HashMap::new()) + .features(BTreeMap::new()) .create() .await?; @@ -418,7 +418,7 @@ mod tests { .await .name("foo") .version("0.2.0") - .features(HashMap::new()) + .features(BTreeMap::new()) .create() .await?; @@ -437,7 +437,7 @@ mod tests { .await .name("foo") .version("0.1.0") - .features(HashMap::new()) + .features(BTreeMap::new()) .create() .await?; @@ -445,7 +445,7 @@ mod tests { .await .name("foo") .version("0.2.0") - .features(HashMap::new()) + .features(BTreeMap::new()) .create() .await?; @@ -468,7 +468,7 @@ mod tests { .await .name("foo") .version("0.1.0") - .features(HashMap::new()) + .features(BTreeMap::new()) .create() .await?; @@ -486,7 +486,7 @@ mod tests { .await .name("foo") .version("0.1.0") - .features(HashMap::new()) + .features(BTreeMap::new()) .create() .await?; @@ -518,7 +518,7 @@ mod tests { .await .name("foo") .version("0.1.0") - .features(features.into_iter().collect::>()) + .features(features.into_iter().collect::>()) .create() .await?; diff --git a/src/web/releases.rs b/src/web/releases.rs index cda9ee494..e4de59b05 100644 --- a/src/web/releases.rs +++ b/src/web/releases.rs @@ -1435,7 +1435,7 @@ mod tests { env.fake_release() .await .name("yet_another_crate") - .version("0.1.0") + .version(V1) .yanked(true) .create() .await?; @@ -1456,13 +1456,8 @@ mod tests { // release that failed in the fetch-step, will miss some details let mut conn = env.async_db().async_conn().await; - fake_release_that_failed_before_build( - &mut conn, - "failed_hard", - "0.1.0", - "some random error", - ) - .await?; + fake_release_that_failed_before_build(&mut conn, "failed_hard", V2, "some random error") + .await?; let _m = crates_io .mock("GET", "/api/v1/crates") @@ -1502,8 +1497,11 @@ mod tests { // * version used is the highest semver following our own "latest version" logic assert_eq!(links[0], "/some_random_crate/latest/some_random_crate/"); assert_eq!(links[1], "/and_another_one/latest/and_another_one/"); - assert_eq!(links[2], "/yet_another_crate/0.1.0/yet_another_crate/"); - assert_eq!(links[3], "/crate/failed_hard/0.1.0"); + assert_eq!( + links[2], + format!("/yet_another_crate/{V1}/yet_another_crate/") + ); + assert_eq!(links[3], format!("/crate/failed_hard/{V2}")); Ok(()) } diff --git a/src/web/rustdoc.rs b/src/web/rustdoc.rs index 361ef483f..afbecb1e7 100644 --- a/src/web/rustdoc.rs +++ b/src/web/rustdoc.rs @@ -898,7 +898,6 @@ mod test { registry_api::{CrateOwner, OwnerKind}, storage::compression::file_extension_for, test::*, - utils::Dependency, web::{cache::CachePolicy, encode_url_path}, }; use anyhow::{Context, Result}; @@ -906,6 +905,7 @@ mod test { use kuchikiki::traits::TendrilSink; use pretty_assertions::assert_eq; use reqwest::StatusCode; + use semver::VersionReq; use std::collections::BTreeMap; use test_case::test_case; use tracing::info; @@ -2663,8 +2663,12 @@ mod test { .version("0.1.0") .rustdoc_file("testing/index.html") .add_dependency( - Dependency::new("optional-dep".to_string(), "1.2.3".parse().unwrap()) - .set_optional(true), + dummy_metadata_dependency() + .name("optional-dep".to_string()) + .req(VersionReq::parse("1.2.3").unwrap()) + .optional(true) + .build() + .unwrap(), ) .create() .await?; diff --git a/templates/macros.html b/templates/macros.html index db8d0462c..d11b1c587 100644 --- a/templates/macros.html +++ b/templates/macros.html @@ -168,13 +168,9 @@ {%- endif -%} {{ dep.name|safe }} {{ dep.req|safe }} - {% if let Some(kind) = &dep.kind %} - {{ kind }} - {% if dep.optional %} - optional - {% endif %} - {% else %} - + {{ dep.kind }} + {% if dep.optional %} + optional {% endif %}