diff --git a/src/docbuilder/metadata.rs b/src/docbuilder/metadata.rs index 97f0ea963..3fd1bd768 100644 --- a/src/docbuilder/metadata.rs +++ b/src/docbuilder/metadata.rs @@ -1,4 +1,4 @@ - +use std::collections::HashSet; use std::path::Path; use toml::Value; use error::Result; @@ -20,6 +20,7 @@ use failure::err_msg; /// all-features = true /// no-default-features = true /// default-target = "x86_64-unknown-linux-gnu" +/// targets = [ "x86_64-apple-darwin", "x86_64-pc-windows-msvc" ] /// rustc-args = [ "--example-rustc-arg" ] /// rustdoc-args = [ "--example-rustdoc-arg" ] /// ``` @@ -39,10 +40,23 @@ pub struct Metadata { /// Set `no-default-fatures` to `false` if you want to build only certain features. pub no_default_features: bool, - /// Docs.rs is running on `x86_64-unknown-linux-gnu` target system and default documentation - /// is always built on this target. You can change default target by setting this. + /// docs.rs runs on `x86_64-unknown-linux-gnu`, which is the default target for documentation by default. + /// + /// You can change the default target by setting this. + /// + /// If `default_target` is unset and `targets` is non-empty, + /// the first element of `targets` will be used as the `default_target`. pub default_target: Option, + /// If you want a crate to build only for specific targets, + /// set `targets` to the list of targets to build, in addition to `default-target`. + /// + /// If you do not set `targets`, all of the tier 1 supported targets will be built. + /// If you set `targets` to an empty array, only the default target will be built. + /// If you set `targets` to a non-empty array but do not set `default_target`, + /// the first element will be treated as the default. + pub targets: Option>, + /// List of command line arguments for `rustc`. pub rustc_args: Option>, @@ -50,7 +64,16 @@ pub struct Metadata { pub rustdoc_args: Option>, } - +/// The targets that should be built for a crate. +/// +/// The `default_target` is the target to be used as the home page for that crate. +/// +/// # See also +/// - [`Metadata::targets`](struct.Metadata.html#method.targets) +pub(super) struct BuildTargets<'a> { + pub(super) default_target: &'a str, + pub(super) other_targets: HashSet<&'a str>, +} impl Metadata { pub(crate) fn from_source_dir(source_dir: &Path) -> Result { @@ -87,6 +110,7 @@ impl Metadata { default_target: None, rustc_args: None, rustdoc_args: None, + targets: None, } } @@ -111,6 +135,8 @@ impl Metadata { .and_then(|v| v.as_bool()).unwrap_or(metadata.all_features); metadata.default_target = table.get("default-target") .and_then(|v| v.as_str()).map(|v| v.to_owned()); + metadata.targets = table.get("targets").and_then(|f| f.as_array()) + .and_then(|f| f.iter().map(|v| v.as_str().map(|v| v.to_owned())).collect()); metadata.rustc_args = table.get("rustc-args").and_then(|f| f.as_array()) .and_then(|f| f.iter().map(|v| v.as_str().map(|v| v.to_owned())).collect()); metadata.rustdoc_args = table.get("rustdoc-args").and_then(|f| f.as_array()) @@ -119,6 +145,22 @@ impl Metadata { metadata } + pub(super) fn targets(&self) -> BuildTargets<'_> { + use super::rustwide_builder::{HOST_TARGET, TARGETS}; + + let default_target = self.default_target.as_deref() + // Use the first element of `targets` if `default_target` is unset and `targets` is non-empty + .or_else(|| self.targets.as_ref().and_then(|targets| targets.iter().next().map(String::as_str))) + .unwrap_or(HOST_TARGET); + + // Let people opt-in to only having specific targets + let mut targets: HashSet<_> = self.targets.as_ref() + .map(|targets| targets.iter().map(String::as_str).collect()) + .unwrap_or_else(|| TARGETS.iter().copied().collect()); + + targets.remove(&default_target); + BuildTargets { default_target, other_targets: targets } + } } @@ -140,6 +182,7 @@ mod test { all-features = true no-default-features = true default-target = "x86_64-unknown-linux-gnu" + targets = [ "x86_64-apple-darwin", "x86_64-pc-windows-msvc" ] rustc-args = [ "--example-rustc-arg" ] rustdoc-args = [ "--example-rustdoc-arg" ] "#; @@ -159,6 +202,11 @@ mod test { assert_eq!(metadata.default_target.unwrap(), "x86_64-unknown-linux-gnu".to_owned()); + let targets = metadata.targets.expect("should have explicit target"); + assert_eq!(targets.len(), 2); + assert_eq!(targets[0], "x86_64-apple-darwin"); + assert_eq!(targets[1], "x86_64-pc-windows-msvc"); + let rustc_args = metadata.rustc_args.unwrap(); assert_eq!(rustc_args.len(), 1); assert_eq!(rustc_args[0], "--example-rustc-arg".to_owned()); @@ -167,4 +215,98 @@ mod test { assert_eq!(rustdoc_args.len(), 1); assert_eq!(rustdoc_args[0], "--example-rustdoc-arg".to_owned()); } + + #[test] + fn test_no_targets() { + // metadata section but no targets + let manifest = r#" + [package] + name = "test" + + [package.metadata.docs.rs] + features = [ "feature1", "feature2" ] + "#; + let metadata = Metadata::from_str(manifest); + assert!(metadata.targets.is_none()); + + // no package.metadata.docs.rs section + let metadata = Metadata::from_str(r#" + [package] + name = "test" + "#); + assert!(metadata.targets.is_none()); + + // targets explicitly set to empty array + let metadata = Metadata::from_str(r#" + [package.metadata.docs.rs] + targets = [] + "#); + assert!(metadata.targets.unwrap().is_empty()); + } + #[test] + fn test_select_targets() { + use crate::docbuilder::rustwide_builder::{HOST_TARGET, TARGETS}; + use super::BuildTargets; + + let mut metadata = Metadata::default(); + // unchanged default_target, targets not specified + let BuildTargets { default_target: default, other_targets: tier_one } = metadata.targets(); + assert_eq!(default, HOST_TARGET); + // should be equal to TARGETS \ {HOST_TARGET} + for actual in &tier_one { + assert!(TARGETS.contains(actual)); + } + for expected in TARGETS { + if *expected == HOST_TARGET { + assert!(!tier_one.contains(&HOST_TARGET)); + } else { + assert!(tier_one.contains(expected)); + } + } + + // unchanged default_target, targets specified to be empty + metadata.targets = Some(Vec::new()); + let BuildTargets { default_target: default, other_targets: others } = metadata.targets(); + assert_eq!(default, HOST_TARGET); + assert!(others.is_empty()); + + // unchanged default_target, targets non-empty + metadata.targets = Some(vec!["i686-pc-windows-msvc".into(), "i686-apple-darwin".into()]); + let BuildTargets { default_target: default, other_targets: others } = metadata.targets(); + assert_eq!(default, "i686-pc-windows-msvc"); + assert_eq!(others.len(), 1); + assert!(others.contains(&"i686-apple-darwin")); + + // make sure that default_target is not built twice + metadata.targets = Some(vec![HOST_TARGET.into()]); + let BuildTargets { default_target: default, other_targets: others } = metadata.targets(); + assert_eq!(default, HOST_TARGET); + assert!(others.is_empty()); + + // make sure that duplicates are removed + metadata.targets = Some(vec!["i686-pc-windows-msvc".into(), "i686-pc-windows-msvc".into()]); + let BuildTargets { default_target: default, other_targets: others } = metadata.targets(); + assert_eq!(default, "i686-pc-windows-msvc"); + assert!(others.is_empty()); + + // make sure that `default_target` always takes priority over `targets` + metadata.default_target = Some("i686-apple-darwin".into()); + let BuildTargets { default_target: default, other_targets: others } = metadata.targets(); + assert_eq!(default, "i686-apple-darwin"); + assert_eq!(others.len(), 1); + assert!(others.contains(&"i686-pc-windows-msvc")); + + // make sure that `default_target` takes priority over `HOST_TARGET` + metadata.targets = Some(vec![]); + let BuildTargets { default_target: default, other_targets: others } = metadata.targets(); + assert_eq!(default, "i686-apple-darwin"); + assert!(others.is_empty()); + + // and if `targets` is unset, it should still be set to `TARGETS` + metadata.targets = None; + let BuildTargets { default_target: default, other_targets: others } = metadata.targets(); + assert_eq!(default, "i686-apple-darwin"); + let tier_one_targets_no_default = TARGETS.iter().filter(|&&t| t != "i686-apple-darwin").copied().collect(); + assert_eq!(others, tier_one_targets_no_default); + } } diff --git a/src/docbuilder/rustwide_builder.rs b/src/docbuilder/rustwide_builder.rs index 64ab688b2..81298edcc 100644 --- a/src/docbuilder/rustwide_builder.rs +++ b/src/docbuilder/rustwide_builder.rs @@ -21,8 +21,11 @@ use super::Metadata; const USER_AGENT: &str = "docs.rs builder (https://github.com/rust-lang/docs.rs)"; const DEFAULT_RUSTWIDE_WORKSPACE: &str = ".rustwide"; -const DEFAULT_TARGET: &str = "x86_64-unknown-linux-gnu"; -const TARGETS: &[&str] = &[ +// It is crucial that this be the same as the host that `docs.rs` is being run on. +// Other values may cause strange and hard-to-debug errors. +// TODO: use `TARGET` instead? I think `TARGET` is only set for build scripts, though. +pub(super) const HOST_TARGET: &str = "x86_64-unknown-linux-gnu"; +pub(super) const TARGETS: &[&str] = &[ "i686-pc-windows-msvc", "i686-unknown-linux-gnu", "x86_64-apple-darwin", @@ -189,7 +192,9 @@ impl RustwideBuilder { build_dir .build(&self.toolchain, &krate, sandbox) .run(|build| { - let res = self.execute_build(None, build, &limits)?; + let metadata = Metadata::from_source_dir(&build.host_source_dir())?; + + let res = self.execute_build(HOST_TARGET, true, build, &limits, &metadata)?; if !res.result.successful { bail!("failed to build dummy crate for {}", self.rustc_version); } @@ -309,12 +314,16 @@ impl RustwideBuilder { let res = build_dir .build(&self.toolchain, &krate, sandbox) .run(|build| { + use docbuilder::metadata::BuildTargets; + let mut files_list = None; let mut has_docs = false; let mut successful_targets = Vec::new(); + let metadata = Metadata::from_source_dir(&build.host_source_dir())?; + let BuildTargets { default_target, other_targets } = metadata.targets(); // Do an initial build and then copy the sources in the database - let res = self.execute_build(None, &build, &limits)?; + let res = self.execute_build(default_target, true, &build, &limits, &metadata)?; if res.result.successful { debug!("adding sources into database"); let prefix = format!("sources/{}/{}", name, version); @@ -340,18 +349,17 @@ impl RustwideBuilder { )?; successful_targets.push(res.target.clone()); + // Then build the documentation for all the targets - for target in TARGETS { - if *target == res.target { - continue; - } - debug!("building package {} {} for {}", name, version, &target); + for target in other_targets { + debug!("building package {} {} for {}", name, version, target); self.build_target( - &target, + target, &build, &limits, &local_storage.path(), &mut successful_targets, + &metadata, )?; } self.upload_docs(&conn, name, version, local_storage.path())?; @@ -396,8 +404,9 @@ impl RustwideBuilder { limits: &Limits, local_storage: &Path, successful_targets: &mut Vec, + metadata: &Metadata, ) -> Result<()> { - let target_res = self.execute_build(Some(target), build, limits)?; + let target_res = self.execute_build(target, false, build, limits, metadata)?; if target_res.result.successful { // Cargo is not giving any error and not generating documentation of some crates // when we use a target compile options. Check documentation exists before @@ -413,17 +422,15 @@ impl RustwideBuilder { fn execute_build( &self, - target: Option<&str>, + target: &str, + is_default_target: bool, build: &Build, limits: &Limits, + metadata: &Metadata, ) -> Result { - let metadata = Metadata::from_source_dir(&build.host_source_dir())?; let cargo_metadata = CargoMetadata::load(&self.workspace, &self.toolchain, &build.host_source_dir())?; - let is_default_target = target.is_none(); - let target = target.or_else(|| metadata.default_target.as_ref().map(|s| s.as_str())); - let mut rustdoc_flags: Vec = vec![ "-Z".to_string(), "unstable-options".to_string(), @@ -445,9 +452,9 @@ impl RustwideBuilder { rustdoc_flags.append(&mut package_rustdoc_args.iter().map(|s| s.to_owned()).collect()); } let mut cargo_args = vec!["doc".to_owned(), "--lib".to_owned(), "--no-deps".to_owned()]; - if let Some(explicit_target) = target { + if target != HOST_TARGET { cargo_args.push("--target".to_owned()); - cargo_args.push(explicit_target.to_owned()); + cargo_args.push(target.to_owned()); }; if let Some(features) = &metadata.features { cargo_args.push("--features".to_owned()); @@ -485,15 +492,13 @@ impl RustwideBuilder { // cargo will put the output in `target//doc`. // However, if this is the default build, we don't want it there, // we want it in `target/doc`. - if let Some(explicit_target) = target { - if is_default_target { - // mv target/$explicit_target/doc target/doc - let target_dir = build.host_target_dir(); - let old_dir = target_dir.join(explicit_target).join("doc"); - let new_dir = target_dir.join("doc"); - debug!("rename {} to {}", old_dir.display(), new_dir.display()); - std::fs::rename(old_dir, new_dir)?; - } + if target != HOST_TARGET && is_default_target { + // mv target/$target/doc target/doc + let target_dir = build.host_target_dir(); + let old_dir = target_dir.join(target).join("doc"); + let new_dir = target_dir.join("doc"); + debug!("rename {} to {}", old_dir.display(), new_dir.display()); + std::fs::rename(old_dir, new_dir)?; } Ok(FullBuildResult { @@ -504,7 +509,7 @@ impl RustwideBuilder { successful, }, cargo_metadata, - target: target.unwrap_or(DEFAULT_TARGET).to_string(), + target: target.to_string(), }) } diff --git a/templates/about.hbs b/templates/about.hbs index 2002a52d4..a6404c0ee 100644 --- a/templates/about.hbs +++ b/templates/about.hbs @@ -162,6 +162,17 @@ no-default-features = true # - i686-pc-windows-msvc default-target = "x86_64-unknown-linux-gnu" +# Targets to build (default: all tier 1 targets) +# +# Same available targets as `default-target`. +# Set this to `[]` to only build the default target. +# +# If `default-target` is unset, the first element of `targets` is treated as the default target. +# Otherwise, these `targets` are built in addition to the default target. +# If both `default-target` and `targets` are unset, +# all tier-one targets will be built and `x86_64-unknown-linux-gnu` will be used as the default target. +targets = [ "x86_64-apple-darwin", "x86_64-pc-windows-msvc" ] + # Additional `RUSTFLAGS` to set (default: none) rustc-args = [ "--example-rustc-arg" ]