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 d5ea0b0ec..102c7be40 100644 --- a/src/db/add_package.rs +++ b/src/db/add_package.rs @@ -4,10 +4,11 @@ use crate::{ error::Result, registry_api::{CrateData, CrateOwner, ReleaseData}, storage::CompressionAlgorithm, - utils::{Dependency, MetadataPackage, rustc_version::parse_rustc_date}, + utils::{cargo_metadata::PackageExt as _, rustc_version::parse_rustc_date}, web::crate_details::{latest_release, releases_for_crate}, }; use anyhow::{Context, anyhow}; +use cargo_metadata::{Dependency, DependencyBuilder, DependencyKind}; use derive_more::{Deref, Display}; use futures_util::stream::TryStreamExt; use semver::VersionReq; @@ -34,12 +35,11 @@ pub struct ReleaseId(pub i32); #[sqlx(transparent)] pub struct BuildId(pub i32); -type DepOut = (String, String, String, bool); -type DepIn = (String, VersionReq, Option, Option); +type Dep = (String, VersionReq, Option, Option); /// A crate dependency in our internal representation for releases.dependencies json. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Deref)] -#[serde(from = "DepIn", into = "DepOut")] +#[serde(from = "Dep", into = "Dep")] pub(crate) struct ReleaseDependency(Dependency); impl ReleaseDependency { @@ -48,27 +48,31 @@ impl ReleaseDependency { } } -impl From for ReleaseDependency { - fn from((name, req, kind, optional): DepIn) -> Self { - ReleaseDependency(Dependency { - name, - req, - kind, - optional: optional.unwrap_or(false), - rename: None, - }) +impl From for ReleaseDependency { + fn from((name, req, kind, optional): Dep) -> Self { + ReleaseDependency( + DependencyBuilder::default() + .name(name) + .source(None) + .req(req) + .kind(kind.unwrap_or_default()) + .optional(optional.unwrap_or(false)) + .uses_default_features(false) + .features(vec![]) + .target(None) + .rename(None) + .registry(None) + .path(None) + .build() + .expect("we know the data is correct"), + ) } } -impl From for DepOut { +impl From for Dep { fn from(rd: ReleaseDependency) -> Self { let d = rd.0; - ( - d.name, - d.req.to_string(), - d.kind.unwrap_or_else(|| "normal".into()), - d.optional, - ) + (d.name, d.req, Some(d.kind), Some(d.optional)) } } @@ -84,7 +88,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, @@ -98,7 +102,7 @@ pub(crate) async fn finish_release( source_size: u64, ) -> Result<()> { debug!("updating release data"); - let dependencies = convert_dependencies(metadata_pkg)?; + let dependencies = convert_dependencies(metadata_pkg); let rustdoc = get_rustdoc(metadata_pkg, source_dir).unwrap_or(None); let readme = get_readme(metadata_pkg, source_dir).unwrap_or(None); let features = get_features(metadata_pkg); @@ -133,7 +137,7 @@ pub(crate) async fn finish_release( WHERE id = $1"#, release_id.0, registry_data.release_time, - dependencies, + serde_json::to_value(dependencies)?, metadata_pkg.package_name(), registry_data.yanked, has_docs, @@ -432,18 +436,24 @@ pub(crate) async fn initialize_build( Ok(build_id) } -/// Convert dependencies into our own internal JSON representation -fn convert_dependencies(pkg: &MetadataPackage) -> Result { - let dependencies: Vec<_> = pkg - .dependencies +fn convert_dependencies( + pkg: &cargo_metadata::Package, +) -> Vec<(String, VersionReq, DependencyKind, bool)> { + pkg.dependencies .iter() - .map(|dependency| ReleaseDependency(dependency.clone())) - .collect::>(); - Ok(serde_json::to_value(dependencies)?) + .map(|dependency| { + ( + dependency.name.clone(), + dependency.req.clone(), + dependency.kind, + dependency.optional, + ) + }) + .collect() } /// 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())); @@ -458,8 +468,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); @@ -479,8 +489,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) @@ -528,7 +538,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 @@ -680,11 +690,16 @@ 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; + #[test] + fn test_dependency_serialization_for_database() { + todo!(); + } + #[test] fn test_set_build_to_error() { async_wrapper(|env| async move { @@ -1226,7 +1241,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/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 df3fa4155..3760d4a19 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, @@ -27,6 +27,7 @@ use axum::{ extract::Extension, response::{IntoResponse, Response as AxumResponse}, }; +use cargo_metadata::Dependency; use chrono::{DateTime, Utc}; use futures_util::stream::TryStreamExt; use log::warn; @@ -707,7 +708,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}; @@ -715,7 +716,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, @@ -1551,7 +1552,7 @@ mod tests { .await .name("library") .version("0.1.0") - .features(HashMap::new()) + .features(BTreeMap::new()) .create() .await?; @@ -1574,7 +1575,7 @@ mod tests { let features = [("_private".into(), Vec::new())] .iter() .cloned() - .collect::>>(); + .collect::>>(); env.fake_release() .await .name("library") @@ -1602,7 +1603,7 @@ mod tests { let features = [("feature1".into(), Vec::new())] .iter() .cloned() - .collect::>>(); + .collect::>>(); env.fake_release() .await .name("library") @@ -1638,7 +1639,7 @@ mod tests { ] .iter() .cloned() - .collect::>>(); + .collect::>>(); env.fake_release() .await .name("library") @@ -1738,12 +1739,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() @@ -1762,12 +1763,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 1d7044dec..8da7d5bed 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 %}