From 9d738e3f3c0d2c10726e1a6e4012f89e740f4553 Mon Sep 17 00:00:00 2001 From: MrDenkoV Date: Tue, 5 Sep 2023 11:10:25 +0200 Subject: [PATCH] Infer readme --- Cargo.lock | 20 ++ Cargo.toml | 1 + scarb-metadata/src/lib.rs | 2 +- scarb/Cargo.toml | 1 + scarb/src/core/manifest/mod.rs | 3 +- scarb/src/core/manifest/toml_manifest.rs | 110 +++++- scarb/tests/metadata.rs | 410 ++++++++++++++++++++++- website/docs/reference/manifest.md | 7 +- 8 files changed, 539 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04730e77d..6ee7e5bec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1580,6 +1580,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" +[[package]] +name = "erased-serde" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" +dependencies = [ + "serde", +] + [[package]] name = "errno" version = "0.3.1" @@ -3898,6 +3907,7 @@ dependencies = [ "scarb-ui", "semver", "serde", + "serde-untagged", "serde-value", "serde_json", "serde_test", @@ -4088,6 +4098,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ba3ac59c62f51b75a6bfad8840b2ede4a81ff5cc23c200221ef479ae75a4aa3" +dependencies = [ + "erased-serde", + "serde", +] + [[package]] name = "serde-value" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index 17d9b3350..d7ae71c4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ petgraph = "0.6.4" predicates = "3.0.3" semver = { version = "1.0.18", features = ["serde"] } serde = { version = "1.0.188", features = ["serde_derive"] } +serde-untagged = "0.1.1" serde-value = "0.7.0" serde_json = "1.0.105" serde_test = "1.0.176" diff --git a/scarb-metadata/src/lib.rs b/scarb-metadata/src/lib.rs index 6342f3ccd..d88e75b5d 100644 --- a/scarb-metadata/src/lib.rs +++ b/scarb-metadata/src/lib.rs @@ -374,7 +374,7 @@ pub struct ManifestMetadata { pub license_file: Option, /// A path to a file in the package root (relative to its `Scarb.toml`) that contains general /// information about the package. - pub readme: Option, + pub readme: Option, /// A URL to the source repository for this package. pub repository: Option, /// A map of additional internet links related to this package. diff --git a/scarb/Cargo.toml b/scarb/Cargo.toml index fb5d5d8ac..3642acdf5 100644 --- a/scarb/Cargo.toml +++ b/scarb/Cargo.toml @@ -50,6 +50,7 @@ scarb-build-metadata = { path = "../utils/scarb-build-metadata" } scarb-metadata = { path = "../scarb-metadata", default-features = false, features = ["builder"] } scarb-ui = { path = "../utils/scarb-ui" } semver.workspace = true +serde-untagged.workspace = true serde-value.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/scarb/src/core/manifest/mod.rs b/scarb/src/core/manifest/mod.rs index 68c77cd3a..ba8d819c2 100644 --- a/scarb/src/core/manifest/mod.rs +++ b/scarb/src/core/manifest/mod.rs @@ -1,6 +1,7 @@ use std::collections::{BTreeMap, HashSet}; use anyhow::{bail, ensure, Result}; +use camino::Utf8PathBuf; use derive_builder::Builder; use semver::VersionReq; use serde::{Deserialize, Serialize}; @@ -55,7 +56,7 @@ pub struct ManifestMetadata { pub keywords: Option>, pub license: Option, pub license_file: Option, - pub readme: Option, + pub readme: Option, pub repository: Option, #[serde(rename = "tool")] pub tool_metadata: Option>, diff --git a/scarb/src/core/manifest/toml_manifest.rs b/scarb/src/core/manifest/toml_manifest.rs index 993cb58bd..9defae78b 100644 --- a/scarb/src/core/manifest/toml_manifest.rs +++ b/scarb/src/core/manifest/toml_manifest.rs @@ -4,10 +4,12 @@ use std::fs; use std::path::PathBuf; use anyhow::{anyhow, bail, ensure, Context, Result}; -use camino::Utf8Path; +use camino::{Utf8Path, Utf8PathBuf}; use itertools::Itertools; +use pathdiff::diff_utf8_paths; use semver::{Version, VersionReq}; -use serde::{Deserialize, Serialize}; +use serde::{de, Deserialize, Serialize}; +use serde_untagged::UntaggedEnumVisitor; use smol_str::SmolStr; use tracing::trace; use url::Url; @@ -103,7 +105,7 @@ pub struct PackageInheritableFields { pub keywords: Option>, pub license: Option, pub license_file: Option, - pub readme: Option, + pub readme: Option, pub repository: Option, pub cairo_version: Option, } @@ -132,8 +134,19 @@ impl PackageInheritableFields { get_field!(homepage, String); get_field!(license, String); get_field!(license_file, String); - get_field!(readme, String); get_field!(repository, String); + + pub fn readme(&self, workspace_root: &Utf8Path, package_root: &Utf8Path) -> Result { + let Ok(Some(readme)) = readme_for_package(workspace_root, self.readme.as_ref()) else { + bail!("`workspace.package.readme` was not defined"); + }; + diff_utf8_paths( + workspace_root.parent().unwrap().join(readme), + package_root.parent().unwrap(), + ) + .map(PathOrBool::Path) + .context("failed to create relative path to workspace readme") + } } #[derive(Deserialize, Serialize, Clone, Debug)] @@ -167,13 +180,32 @@ pub struct TomlPackage { pub keywords: Option>>, pub license: Option>, pub license_file: Option>, - pub readme: Option>, + pub readme: Option>, pub repository: Option>, /// **UNSTABLE** This package does not depend on Cairo's `core`. pub no_core: Option, pub cairo_version: Option>, } +#[derive(Clone, Debug, Serialize, Eq, PartialEq)] +#[serde(untagged)] +pub enum PathOrBool { + Path(Utf8PathBuf), + Bool(bool), +} + +impl<'de> Deserialize<'de> for PathOrBool { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + UntaggedEnumVisitor::new() + .bool(|b| Ok(PathOrBool::Bool(b))) + .string(|s| Ok(PathOrBool::Path(s.into()))) + .deserialize(deserializer) + } +} + #[derive(Debug, Default, Clone, Deserialize, Serialize)] pub struct TomlWorkspaceDependency { pub workspace: bool, @@ -477,11 +509,19 @@ impl TomlManifest { .clone() .map(|mw| mw.resolve("license_file", || inheritable_package.license_file())) .transpose()?, - readme: package - .readme - .clone() - .map(|mw| mw.resolve("readme", || inheritable_package.readme())) - .transpose()?, + readme: readme_for_package( + manifest_path, + package + .readme + .clone() + .map(|mw| { + mw.resolve("readme", || { + inheritable_package.readme(workspace_manifest_path, manifest_path) + }) + }) + .transpose()? + .as_ref(), + )?, repository: package .repository .clone() @@ -704,6 +744,56 @@ impl TomlManifest { } } +/// Returns the absolute canonical path of the README file for a [`TomlPackage`]. +pub fn readme_for_package( + package_root: &Utf8Path, + readme: Option<&PathOrBool>, +) -> Result> { + let file_name = match readme { + None => default_readme_from_package_root(package_root.parent().unwrap()), + Some(PathOrBool::Path(p)) => Some(p.as_path()), + Some(PathOrBool::Bool(true)) => { + default_readme_from_package_root(package_root.parent().unwrap()) + .or_else(|| Some("README.md".into())) + } + Some(PathOrBool::Bool(false)) => None, + }; + + abs_canonical_path(package_root, file_name) +} + +/// Creates the absolute canonical path of the README file and checks if it exists +fn abs_canonical_path(prefix: &Utf8Path, readme: Option<&Utf8Path>) -> Result> { + match readme { + None => Ok(None), + Some(readme) => prefix + .parent() + .unwrap() + .join(readme) + .canonicalize_utf8() + .map(Some) + .with_context(|| { + format!( + "failed to find the readme at {}", + prefix.parent().unwrap().join(readme) + ) + }), + } +} + +const DEFAULT_README_FILES: &[&str] = &["README.md", "README.txt", "README"]; + +/// Checks if a file with any of the default README file names exists in the package root. +/// If so, returns a `Utf8Path` with that name. +fn default_readme_from_package_root(package_root: &Utf8Path) -> Option<&Utf8Path> { + for &readme_filename in DEFAULT_README_FILES { + if package_root.join(readme_filename).is_file() { + return Some(readme_filename.into()); + } + } + None +} + impl TomlDependency { fn to_dependency( &self, diff --git a/scarb/tests/metadata.rs b/scarb/tests/metadata.rs index a59af07e2..3fd7d6d11 100644 --- a/scarb/tests/metadata.rs +++ b/scarb/tests/metadata.rs @@ -1,6 +1,8 @@ use std::collections::BTreeMap; use assert_fs::prelude::*; +use camino::Utf8PathBuf; +use indoc::formatdoc; use serde_json::json; use scarb_metadata::{Cfg, ManifestMetadataBuilder, Metadata, PackageMetadata}; @@ -217,7 +219,7 @@ fn manifest_targets_and_metadata() { license = "MIT License" license-file = "./license.md" - readme = "./readme.md" + readme = "./README.md" [package.urls] hello = "https://world.com/" @@ -248,6 +250,7 @@ fn manifest_targets_and_metadata() { "#, ) .unwrap(); + t.child("README.md").touch().unwrap(); let meta = Scarb::quick_snapbox() .arg("--json") @@ -282,7 +285,7 @@ fn manifest_targets_and_metadata() { ])) .license(Some("MIT License".to_string())) .license_file(Some("./license.md".to_string())) - .readme(Some("./readme.md".to_string())) + .readme(Utf8PathBuf::from_path_buf(t.join("README.md").canonicalize().unwrap()).ok()) .repository(Some("https://github.com/johndoe/repo".to_string())) .tool(Some(BTreeMap::from_iter([ ("meta".to_string(), json!("data")), @@ -565,3 +568,406 @@ fn workspace_package_key_inheritance() { ]) ) } + +#[test] +fn infer_readme_simple() { + let t = assert_fs::TempDir::new().unwrap(); + ProjectBuilder::start() + .name("hello") + .version("0.1.0") + .build(&t); + + let meta = Scarb::quick_snapbox() + .arg("--json") + .arg("metadata") + .arg("--format-version") + .arg("1") + .current_dir(&t) + .stdout_json::(); + + assert_eq!( + packages_by_name(meta) + .get("hello") + .unwrap() + .manifest_metadata + .readme, + None + ); + + t.child("README").touch().unwrap(); + + let meta = Scarb::quick_snapbox() + .arg("--json") + .arg("metadata") + .arg("--format-version") + .arg("1") + .current_dir(&t) + .stdout_json::(); + + assert_eq!( + packages_by_name(meta) + .get("hello") + .unwrap() + .manifest_metadata + .readme, + Utf8PathBuf::from_path_buf(t.join("README").canonicalize().unwrap()).ok() + ); + + t.child("README.txt").touch().unwrap(); + + let meta = Scarb::quick_snapbox() + .arg("--json") + .arg("metadata") + .arg("--format-version") + .arg("1") + .current_dir(&t) + .stdout_json::(); + + assert_eq!( + packages_by_name(meta) + .get("hello") + .unwrap() + .manifest_metadata + .readme, + Utf8PathBuf::from_path_buf(t.join("README.txt").canonicalize().unwrap()).ok() + ); + + t.child("README.md").touch().unwrap(); + + let meta = Scarb::quick_snapbox() + .arg("--json") + .arg("metadata") + .arg("--format-version") + .arg("1") + .current_dir(&t) + .stdout_json::(); + + assert_eq!( + packages_by_name(meta) + .get("hello") + .unwrap() + .manifest_metadata + .readme, + Utf8PathBuf::from_path_buf(t.join("README.md").canonicalize().unwrap()).ok() + ); + + t.child("Scarb.toml") + .write_str( + r#" + [package] + name = "hello" + version = "1.0.0" + readme = "a/b/c/MEREAD.md" + "#, + ) + .unwrap(); + t.child("a").child("b").child("c").create_dir_all().unwrap(); + t.child("a") + .child("b") + .child("c") + .child("MEREAD.md") + .touch() + .unwrap(); + + let meta = Scarb::quick_snapbox() + .arg("--json") + .arg("metadata") + .arg("--format-version") + .arg("1") + .current_dir(&t) + .stdout_json::(); + + assert_eq!( + packages_by_name(meta) + .get("hello") + .unwrap() + .manifest_metadata + .readme, + Utf8PathBuf::from_path_buf(t.join("a/b/c/MEREAD.md").canonicalize().unwrap()).ok() + ); +} + +#[test] +fn infer_readme_simple_bool() { + let t = assert_fs::TempDir::new().unwrap(); + ProjectBuilder::start() + .name("hello") + .version("0.1.0") + .build(&t); + + t.child("Scarb.toml") + .write_str( + r#" + [package] + name = "hello" + version = "1.0.0" + readme = false + "#, + ) + .unwrap(); + + let meta = Scarb::quick_snapbox() + .arg("--json") + .arg("metadata") + .arg("--format-version") + .arg("1") + .current_dir(&t) + .stdout_json::(); + + assert_eq!( + packages_by_name(meta) + .get("hello") + .unwrap() + .manifest_metadata + .readme, + None + ); + + t.child("Scarb.toml") + .write_str( + r#" + [package] + name = "hello" + version = "1.0.0" + readme = true + "#, + ) + .unwrap(); + + Scarb::quick_snapbox() + .arg("--json") + .arg("metadata") + .arg("--format-version") + .arg("1") + .current_dir(&t) + .assert() + .failure() + .stdout_eq(formatdoc!( + r#" + {{"type":"error","message":"failed to parse manifest at: {path}/Scarb.toml\n\nCaused by:\n 0: failed to find the readme at {path}/README.md\n 1: No such file or directory (os error 2)"}} + "#, + path=t.path().canonicalize().unwrap().to_str().unwrap())); + + t.child("README.md").touch().unwrap(); + + let meta = Scarb::quick_snapbox() + .arg("--json") + .arg("metadata") + .arg("--format-version") + .arg("1") + .current_dir(&t) + .stdout_json::(); + + assert_eq!( + packages_by_name(meta) + .get("hello") + .unwrap() + .manifest_metadata + .readme, + Utf8PathBuf::from_path_buf(t.join("README.md").canonicalize().unwrap()).ok() + ); +} + +#[test] +fn infer_readme_workspace() { + let t = assert_fs::TempDir::new().unwrap(); + ProjectBuilder::start() + .name("hello") + .version("0.1.0") + .build(&t); + let ws = ["t1", "t2", "t3", "t4", "t5", "t6"].iter().zip( + [ + Some("MEREAD.md"), + Some("README.md"), + Some("README.txt"), + Some("TEST.txt"), + None, + None, + ] + .iter(), + ); + for (pack_name, readme_name) in ws { + Scarb::quick_snapbox() + .arg("new") + .arg(pack_name) + .current_dir(&t); + if let Some(name) = readme_name { + t.child(pack_name).child(name).touch().unwrap(); + } + } + t.child("MEREAD.md").touch().unwrap(); + t.child("tmp1").create_dir_all().unwrap(); + t.child("tmp1").child("tmp2").create_dir_all().unwrap(); + Scarb::quick_snapbox() + .arg("new") + .arg("t7") + .current_dir(t.child("tmp1").child("tmp2")); + t.child("tmp1") + .child("tmp2") + .child("t7") + .child("MEREAD.md") + .touch() + .unwrap(); + t.child("tmp1") + .child("tmp2") + .child("t7") + .child("Scarb.toml") + .write_str( + r#" + [package] + name = "t7" + version.workspace = true + edition = "2021" + readme.workspace = true + "#, + ) + .unwrap(); + + t.child("Scarb.toml") + .write_str( + r#" + [workspace] + members = [ + "t1", + "t2", + "t3", + "t4", + "t5", + "t6", + "tmp1/tmp2/t7", + ] + + [workspace.package] + version = "0.1.0" + edition = "2021" + readme = "MEREAD.md" + + [package] + name = "hello" + version.workspace = true + readme.workspace = true + "#, + ) + .unwrap(); + + t.child("t1") + .child("Scarb.toml") + .write_str( + r#" + [package] + name = "t1" + version.workspace = true + edition = "2021" + readme.workspace = true + "#, + ) + .unwrap(); + t.child("t2") + .child("Scarb.toml") + .write_str( + r#" + [package] + name = "t2" + version.workspace = true + edition = "2021" + readme = true + "#, + ) + .unwrap(); + t.child("t3") + .child("Scarb.toml") + .write_str( + r#" + [package] + name = "t3" + version.workspace = true + edition = "2021" + "#, + ) + .unwrap(); + t.child("t4") + .child("Scarb.toml") + .write_str( + r#" + [package] + name = "t4" + version.workspace = true + edition = "2021" + readme = "TEST.txt" + "#, + ) + .unwrap(); + t.child("t5") + .child("Scarb.toml") + .write_str( + r#" + [package] + name = "t5" + version.workspace = true + edition = "2021" + readme = false + "#, + ) + .unwrap(); + t.child("t6") + .child("Scarb.toml") + .write_str( + r#" + [package] + name = "t6" + version.workspace = true + edition = "2021" + "#, + ) + .unwrap(); + t.child("tmp1") + .child("tmp2") + .child("t7") + .child("Scarb.toml") + .write_str( + r#" + [package] + name = "t7" + version.workspace = true + edition = "2021" + readme.workspace = true + "#, + ) + .unwrap(); + + let meta = Scarb::quick_snapbox() + .arg("--json") + .arg("metadata") + .arg("--format-version") + .arg("1") + .current_dir(&t) + .stdout_json::(); + + let packages = packages_by_name(meta); + assert_eq!( + packages.get("hello").unwrap().manifest_metadata.readme, + Utf8PathBuf::from_path_buf(t.join("MEREAD.md").canonicalize().unwrap()).ok() + ); + assert_eq!( + packages.get("t7").unwrap().manifest_metadata.readme, + Utf8PathBuf::from_path_buf(t.join("MEREAD.md").canonicalize().unwrap()).ok() + ); + assert_eq!( + packages.get("t1").unwrap().manifest_metadata.readme, + Utf8PathBuf::from_path_buf(t.join("MEREAD.md").canonicalize().unwrap()).ok() + ); + assert_eq!( + packages.get("t2").unwrap().manifest_metadata.readme, + Utf8PathBuf::from_path_buf(t.child("t2").join("README.md").canonicalize().unwrap()).ok() + ); + assert_eq!( + packages.get("t3").unwrap().manifest_metadata.readme, + Utf8PathBuf::from_path_buf(t.child("t3").join("README.txt").canonicalize().unwrap()).ok() + ); + assert_eq!( + packages.get("t4").unwrap().manifest_metadata.readme, + Utf8PathBuf::from_path_buf(t.child("t4").join("TEST.txt").canonicalize().unwrap()).ok() + ); + assert_eq!(packages.get("t5").unwrap().manifest_metadata.readme, None); + assert_eq!(packages.get("t6").unwrap().manifest_metadata.readme, None); +} diff --git a/website/docs/reference/manifest.md b/website/docs/reference/manifest.md index d12072537..478537174 100644 --- a/website/docs/reference/manifest.md +++ b/website/docs/reference/manifest.md @@ -97,9 +97,14 @@ information about the package. ```toml [package] -readme = "README.md +readme = "README.md" ``` +If no value is specified for this field, and a file named `README.md`, `README.txt` or `README` exists in the package root, +then the name of that file will be used. +You can suppress this behavior by setting this field to false. +If the field is set to true, a default value of `README.md` will be assumed, unless file named `README.txt` or `README` exists in the package root, in which case it will be used instead. + ### `homepage` This field should be a URL to a site that is the home page for your package.