diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 246bd37f145..d89a1efd717 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -798,7 +798,7 @@ pub struct TomlProject { description: Option, homepage: Option, documentation: Option, - readme: Option, + readme: Option, keywords: Option>, categories: Option>, license: Option, @@ -1198,11 +1198,19 @@ impl TomlManifest { project.links.as_deref(), project.namespaced_features.unwrap_or(false), )?; + + let readme = readme_for_project(package_root, project); + if let Some(ref r) = readme { + if !package_root.join(r).is_file() { + bail!("readme file with name '{}' was not found", r); + } + }; + let metadata = ManifestMetadata { description: project.description.clone(), homepage: project.homepage.clone(), documentation: project.documentation.clone(), - readme: project.readme.clone(), + readme, authors: project.authors.clone().unwrap_or_default(), license: project.license.clone(), license_file: project.license_file.clone(), @@ -1513,6 +1521,32 @@ impl TomlManifest { } } +/// Returns the name of the README file for a `TomlProject`. +fn readme_for_project(package_root: &Path, project: &TomlProject) -> Option { + match &project.readme { + None => default_readme_from_package_root(package_root), + Some(value) => match value { + StringOrBool::Bool(false) => None, + StringOrBool::Bool(true) => Some("README.md".to_string()), + StringOrBool::String(v) => Some(v.clone()), + }, + } +} + +const DEFAULT_README_FILES: [&str; 3] = ["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 `String` representing that name. +fn default_readme_from_package_root(package_root: &Path) -> Option { + for &readme_filename in DEFAULT_README_FILES.iter() { + if package_root.join(readme_filename).is_file() { + return Some(readme_filename.to_string()); + } + } + + None +} + /// Checks a list of build targets, and ensures the target names are unique within a vector. /// If not, the name of the offending build target is returned. fn unique_build_targets(targets: &[Target], package_root: &Path) -> Result<(), String> { diff --git a/src/doc/src/reference/manifest.md b/src/doc/src/reference/manifest.md index 25c6d6819c5..6b7b81bb007 100644 --- a/src/doc/src/reference/manifest.md +++ b/src/doc/src/reference/manifest.md @@ -165,6 +165,12 @@ will interpret it as Markdown and render it on the crate's page. 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. + #### The `homepage` field The `homepage` field should be a URL to a site that is the home page for your diff --git a/tests/testsuite/metadata.rs b/tests/testsuite/metadata.rs index 51e401e1d54..97c26635b00 100644 --- a/tests/testsuite/metadata.rs +++ b/tests/testsuite/metadata.rs @@ -1115,6 +1115,7 @@ fn package_metadata() { baz = "quux" "#, ) + .file("README.md", "") .file("src/lib.rs", "") .build(); @@ -1186,6 +1187,7 @@ fn package_publish() { publish = ["my-registry"] "#, ) + .file("README.md", "") .file("src/lib.rs", "") .build(); diff --git a/tests/testsuite/read_manifest.rs b/tests/testsuite/read_manifest.rs index 7ca0c6f9bd4..780ef710ccc 100644 --- a/tests/testsuite/read_manifest.rs +++ b/tests/testsuite/read_manifest.rs @@ -2,14 +2,16 @@ use cargo_test_support::{basic_bin_manifest, main_file, project}; -static MANIFEST_OUTPUT: &str = r#" -{ +fn manifest_output(readme_value: &str) -> String { + format!( + r#" +{{ "authors": [ "wycats@example.com" ], "categories": [], "name":"foo", - "readme": null, + "readme": {}, "repository": null, "version":"0.5.0", "id":"foo[..]0.5.0[..](path+file://[..]/foo)", @@ -21,19 +23,44 @@ static MANIFEST_OUTPUT: &str = r#" "edition": "2015", "source":null, "dependencies":[], - "targets":[{ + "targets":[{{ "kind":["bin"], "crate_types":["bin"], "doctest": false, "edition": "2015", "name":"foo", "src_path":"[..]/foo/src/foo.rs" - }], - "features":{}, + }}], + "features":{{}}, "manifest_path":"[..]Cargo.toml", "metadata": null, "publish": null -}"#; +}}"#, + readme_value + ) +} + +fn manifest_output_no_readme() -> String { + manifest_output("null") +} + +pub fn basic_bin_manifest_with_readme(name: &str, readme_filename: &str) -> String { + format!( + r#" + [package] + + name = "{}" + version = "0.5.0" + authors = ["wycats@example.com"] + readme = {} + + [[bin]] + + name = "{}" + "#, + name, readme_filename, name + ) +} #[cargo_test] fn cargo_read_manifest_path_to_cargo_toml_relative() { @@ -44,7 +71,7 @@ fn cargo_read_manifest_path_to_cargo_toml_relative() { p.cargo("read-manifest --manifest-path foo/Cargo.toml") .cwd(p.root().parent().unwrap()) - .with_json(MANIFEST_OUTPUT) + .with_json(&manifest_output_no_readme()) .run(); } @@ -58,7 +85,7 @@ fn cargo_read_manifest_path_to_cargo_toml_absolute() { p.cargo("read-manifest --manifest-path") .arg(p.root().join("Cargo.toml")) .cwd(p.root().parent().unwrap()) - .with_json(MANIFEST_OUTPUT) + .with_json(&manifest_output_no_readme()) .run(); } @@ -104,5 +131,83 @@ fn cargo_read_manifest_cwd() { .file("src/foo.rs", &main_file(r#""i am foo""#, &[])) .build(); - p.cargo("read-manifest").with_json(MANIFEST_OUTPUT).run(); + p.cargo("read-manifest") + .with_json(&manifest_output_no_readme()) + .run(); +} + +#[cargo_test] +fn cargo_read_manifest_with_specified_readme() { + let p = project() + .file( + "Cargo.toml", + &basic_bin_manifest_with_readme("foo", r#""SomeReadme.txt""#), + ) + .file("SomeReadme.txt", "Sample Project") + .file("src/foo.rs", &main_file(r#""i am foo""#, &[])) + .build(); + + p.cargo("read-manifest") + .with_json(&manifest_output(&format!(r#""{}""#, "SomeReadme.txt"))) + .run(); +} + +#[cargo_test] +fn cargo_read_manifest_default_readme() { + let readme_filenames = ["README.md", "README.txt", "README"]; + + for readme in readme_filenames.iter() { + let p = project() + .file("Cargo.toml", &basic_bin_manifest("foo")) + .file(readme, "Sample project") + .file("src/foo.rs", &main_file(r#""i am foo""#, &[])) + .build(); + + p.cargo("read-manifest") + .with_json(&manifest_output(&format!(r#""{}""#, readme))) + .run(); + } +} + +#[cargo_test] +fn cargo_read_manifest_suppress_default_readme() { + let p = project() + .file( + "Cargo.toml", + &basic_bin_manifest_with_readme("foo", "false"), + ) + .file("README.txt", "Sample project") + .file("src/foo.rs", &main_file(r#""i am foo""#, &[])) + .build(); + + p.cargo("read-manifest") + .with_json(&manifest_output_no_readme()) + .run(); +} + +// If a file named README.md exists, and `readme = true`, the value `README.md` should be defaulted in. +#[cargo_test] +fn cargo_read_manifest_defaults_readme_if_true() { + let p = project() + .file("Cargo.toml", &basic_bin_manifest_with_readme("foo", "true")) + .file("README.md", "Sample project") + .file("src/foo.rs", &main_file(r#""i am foo""#, &[])) + .build(); + + p.cargo("read-manifest") + .with_json(&manifest_output(&format!(r#""{}""#, "README.md"))) + .run(); +} + +// If a file named README.md does not exist, and `readme = true`, it should panic. +#[cargo_test] +#[should_panic] +fn cargo_read_manifest_panics_if_default_readme_not_found() { + let p = project() + .file("Cargo.toml", &basic_bin_manifest_with_readme("foo", "true")) + .file("README.txt", "Sample project") + .file("src/foo.rs", &main_file(r#""i am foo""#, &[])) + .build(); + + p.cargo("read-manifest").run(); }