Skip to content

Commit f193909

Browse files
committed
Build & upload rustdoc json output next to other docs
1 parent 7ba7591 commit f193909

File tree

4 files changed

+272
-12
lines changed

4 files changed

+272
-12
lines changed

src/db/delete.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use super::{CrateId, update_latest_version_id};
1111

1212
/// List of directories in docs.rs's underlying storage (either the database or S3) containing a
1313
/// subdirectory named after the crate. Those subdirectories will be deleted.
14-
static LIBRARY_STORAGE_PATHS_TO_DELETE: &[&str] = &["rustdoc", "sources"];
14+
static LIBRARY_STORAGE_PATHS_TO_DELETE: &[&str] = &["rustdoc", "rustdoc-json", "sources"];
1515
static OTHER_STORAGE_PATHS_TO_DELETE: &[&str] = &["sources"];
1616

1717
#[derive(Debug, thiserror::Error)]
@@ -222,6 +222,7 @@ mod tests {
222222
use super::*;
223223
use crate::db::ReleaseId;
224224
use crate::registry_api::{CrateOwner, OwnerKind};
225+
use crate::storage::rustdoc_json_path;
225226
use crate::test::{async_wrapper, fake_release_that_failed_before_build};
226227
use test_case::test_case;
227228

@@ -405,6 +406,17 @@ mod tests {
405406
.collect())
406407
}
407408

409+
async fn json_exists(storage: &AsyncStorage, version: &str) -> Result<bool> {
410+
storage
411+
.exists(&rustdoc_json_path(
412+
"a",
413+
version,
414+
"x86_64-unknown-linux-gnu",
415+
crate::storage::RustdocJsonFormatVersion::Latest,
416+
))
417+
.await
418+
}
419+
408420
let mut conn = env.async_db().await.async_conn().await;
409421
let v1 = env
410422
.fake_release()
@@ -426,6 +438,7 @@ mod tests {
426438
.rustdoc_file_exists("a", "1.0.0", None, "a/index.html", archive_storage)
427439
.await?
428440
);
441+
assert!(json_exists(&*env.async_storage().await, "1.0.0").await?);
429442
let crate_id = sqlx::query_scalar!(
430443
r#"SELECT crate_id as "crate_id: CrateId" FROM releases WHERE id = $1"#,
431444
v1.0
@@ -457,6 +470,7 @@ mod tests {
457470
.rustdoc_file_exists("a", "2.0.0", None, "a/index.html", archive_storage)
458471
.await?
459472
);
473+
assert!(json_exists(&*env.async_storage().await, "2.0.0").await?);
460474
assert_eq!(
461475
owners(&mut conn, crate_id).await?,
462476
vec!["Peter Rabbit".to_string()]
@@ -494,13 +508,16 @@ mod tests {
494508
.await?
495509
);
496510
}
511+
assert!(!json_exists(&*env.async_storage().await, "1.0.0").await?);
512+
497513
assert!(release_exists(&mut conn, v2).await?);
498514
assert!(
499515
env.async_storage()
500516
.await
501517
.rustdoc_file_exists("a", "2.0.0", None, "a/index.html", archive_storage)
502518
.await?
503519
);
520+
assert!(json_exists(&*env.async_storage().await, "2.0.0").await?);
504521
assert_eq!(
505522
owners(&mut conn, crate_id).await?,
506523
vec!["Peter Rabbit".to_string()]

src/docbuilder/rustwide_builder.rs

Lines changed: 204 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ use crate::db::{
1212
use crate::docbuilder::Limits;
1313
use crate::error::Result;
1414
use crate::repositories::RepositoryStatsUpdater;
15-
use crate::storage::{rustdoc_archive_path, source_archive_path};
15+
use crate::storage::{
16+
CompressionAlgorithm, RustdocJsonFormatVersion, compress, rustdoc_archive_path,
17+
rustdoc_json_path, source_archive_path,
18+
};
1619
use crate::utils::{
1720
CargoMetadata, ConfigName, copy_dir_all, get_config, parse_rustc_version, report_error,
1821
set_config,
@@ -26,19 +29,39 @@ use rustwide::cmd::{Command, CommandError, SandboxBuilder, SandboxImage};
2629
use rustwide::logging::{self, LogStorage};
2730
use rustwide::toolchain::ToolchainError;
2831
use rustwide::{AlternativeRegistry, Build, Crate, Toolchain, Workspace, WorkspaceBuilder};
32+
use serde::Deserialize;
2933
use std::collections::{HashMap, HashSet};
30-
use std::fs;
34+
use std::fs::{self, File};
35+
use std::io::BufReader;
3136
use std::path::Path;
3237
use std::sync::Arc;
3338
use std::time::Instant;
3439
use tokio::runtime::Runtime;
35-
use tracing::{debug, info, info_span, instrument, warn};
40+
use tracing::{debug, error, info, info_span, instrument, warn};
3641

3742
const USER_AGENT: &str = "docs.rs builder (https://github.com/rust-lang/docs.rs)";
3843
const COMPONENTS: &[&str] = &["llvm-tools-preview", "rustc-dev", "rustfmt"];
3944
const DUMMY_CRATE_NAME: &str = "empty-library";
4045
const DUMMY_CRATE_VERSION: &str = "1.0.0";
4146

47+
/// read the format version from a rustdoc JSON file.
48+
fn read_format_version_from_rustdoc_json(
49+
reader: impl std::io::Read,
50+
) -> Result<RustdocJsonFormatVersion> {
51+
let reader = BufReader::new(reader);
52+
53+
#[derive(Deserialize)]
54+
struct RustdocJson {
55+
format_version: u16,
56+
}
57+
58+
let rustdoc_json: RustdocJson = serde_json::from_reader(reader)?;
59+
60+
Ok(RustdocJsonFormatVersion::Version(
61+
rustdoc_json.format_version,
62+
))
63+
}
64+
4265
async fn get_configured_toolchain(conn: &mut sqlx::PgConnection) -> Result<Toolchain> {
4366
let name: String = get_config(conn, ConfigName::Toolchain)
4467
.await?
@@ -303,8 +326,18 @@ impl RustwideBuilder {
303326
.run(|build| {
304327
let metadata = Metadata::from_crate_root(build.host_source_dir())?;
305328

306-
let res =
307-
self.execute_build(HOST_TARGET, true, build, &limits, &metadata, true, false)?;
329+
let res = self.execute_build(
330+
BuildId(0),
331+
DUMMY_CRATE_NAME,
332+
DUMMY_CRATE_VERSION,
333+
HOST_TARGET,
334+
true,
335+
build,
336+
&limits,
337+
&metadata,
338+
true,
339+
false,
340+
)?;
308341
if !res.result.successful {
309342
bail!("failed to build dummy crate for {}", rustc_version);
310343
}
@@ -518,12 +551,13 @@ impl RustwideBuilder {
518551
build.fetch_build_std_dependencies(&targets)?;
519552
}
520553

554+
521555
let mut has_docs = false;
522556
let mut successful_targets = Vec::new();
523557

524558
// Perform an initial build
525559
let mut res =
526-
self.execute_build(default_target, true, build, &limits, &metadata, false, collect_metrics)?;
560+
self.execute_build(build_id, name, version, default_target, true, build, &limits, &metadata, false, collect_metrics)?;
527561

528562
// If the build fails with the lockfile given, try using only the dependencies listed in Cargo.toml.
529563
let cargo_lock = build.host_source_dir().join("Cargo.lock");
@@ -545,7 +579,7 @@ impl RustwideBuilder {
545579
.run_capture()?;
546580
}
547581
res =
548-
self.execute_build(default_target, true, build, &limits, &metadata, false, collect_metrics)?;
582+
self.execute_build(build_id, name, version, default_target, true, build, &limits, &metadata, false, collect_metrics)?;
549583
}
550584

551585
if res.result.successful {
@@ -576,6 +610,7 @@ impl RustwideBuilder {
576610
for target in other_targets.into_iter().take(limits.targets()) {
577611
debug!("building package {} {} for {}", name, version, target);
578612
let target_res = self.build_target(
613+
build_id, name, version,
579614
target,
580615
build,
581616
&limits,
@@ -751,6 +786,9 @@ impl RustwideBuilder {
751786
#[allow(clippy::too_many_arguments)]
752787
fn build_target(
753788
&self,
789+
build_id: BuildId,
790+
name: &str,
791+
version: &str,
754792
target: &str,
755793
build: &Build,
756794
limits: &Limits,
@@ -760,6 +798,9 @@ impl RustwideBuilder {
760798
collect_metrics: bool,
761799
) -> Result<FullBuildResult> {
762800
let target_res = self.execute_build(
801+
build_id,
802+
name,
803+
version,
763804
target,
764805
false,
765806
build,
@@ -781,6 +822,102 @@ impl RustwideBuilder {
781822
Ok(target_res)
782823
}
783824

825+
/// run the build with rustdoc JSON output for a specific target and directly upload the
826+
/// build log & the JSON files.
827+
///
828+
/// The method only returns an `Err` for internal errors that should be retryable.
829+
/// For all build errors we would just upload the log file and still return Ok(())
830+
#[instrument(skip(self, build))]
831+
#[allow(clippy::too_many_arguments)]
832+
fn execute_json_build(
833+
&self,
834+
build_id: BuildId,
835+
name: &str,
836+
version: &str,
837+
target: &str,
838+
is_default_target: bool,
839+
build: &Build,
840+
metadata: &Metadata,
841+
limits: &Limits,
842+
) -> Result<()> {
843+
let rustdoc_flags = vec!["--output-format".to_string(), "json".to_string()];
844+
845+
let mut storage = LogStorage::new(log::LevelFilter::Info);
846+
storage.set_max_size(limits.max_log_size());
847+
848+
let successful = logging::capture(&storage, || {
849+
let _span = info_span!("cargo_build_json", target = %target).entered();
850+
self.prepare_command(build, target, metadata, limits, rustdoc_flags, false)
851+
.and_then(|command| command.run().map_err(Error::from))
852+
.is_ok()
853+
});
854+
855+
{
856+
let _span = info_span!("store_json_build_logs").entered();
857+
let build_log_path = format!("build-logs/{build_id}/{target}_json.txt");
858+
self.storage
859+
.store_one(build_log_path, storage.to_string())
860+
.context("storing build log on S3")?;
861+
}
862+
863+
if !successful {
864+
// this is a normal build error and will be visible in the uploaded build logs.
865+
// We don't need the Err variant here.
866+
return Ok(());
867+
}
868+
869+
let json_dir = if metadata.proc_macro {
870+
assert!(
871+
is_default_target && target == HOST_TARGET,
872+
"can't handle cross-compiling macros"
873+
);
874+
build.host_target_dir().join("doc")
875+
} else {
876+
build.host_target_dir().join(target).join("doc")
877+
};
878+
879+
let json_filename = fs::read_dir(&json_dir)?
880+
.filter_map(|entry| {
881+
let entry = entry.ok()?;
882+
let path = entry.path();
883+
if path.is_file() && path.extension()? == "json" {
884+
Some(path)
885+
} else {
886+
None
887+
}
888+
})
889+
.next()
890+
.ok_or_else(|| {
891+
anyhow!("no JSON file found in target/doc after successful rustdoc json build")
892+
})?;
893+
894+
let format_version = {
895+
let _span = info_span!("read_format_version").entered();
896+
read_format_version_from_rustdoc_json(&File::open(&json_filename)?)
897+
.context("couldn't parse rustdoc json to find format version")?
898+
};
899+
900+
let compressed_json: Vec<u8> = {
901+
let _span =
902+
info_span!("compress_json", file_size = json_filename.metadata()?.len()).entered();
903+
904+
compress(
905+
BufReader::new(File::open(&json_filename)?),
906+
CompressionAlgorithm::Zstd,
907+
)?
908+
};
909+
910+
for format_version in [format_version, RustdocJsonFormatVersion::Latest] {
911+
let _span = info_span!("store_json", %format_version).entered();
912+
self.storage.store_one(
913+
rustdoc_json_path(name, version, target, format_version),
914+
compressed_json.clone(),
915+
)?;
916+
}
917+
918+
Ok(())
919+
}
920+
784921
#[instrument(skip(self, build))]
785922
fn get_coverage(
786923
&self,
@@ -841,6 +978,9 @@ impl RustwideBuilder {
841978
#[allow(clippy::too_many_arguments)]
842979
fn execute_build(
843980
&self,
981+
build_id: BuildId,
982+
name: &str,
983+
version: &str,
844984
target: &str,
845985
is_default_target: bool,
846986
build: &Build,
@@ -883,6 +1023,22 @@ impl RustwideBuilder {
8831023
}
8841024
};
8851025

1026+
if let Err(err) = self.execute_json_build(
1027+
build_id,
1028+
name,
1029+
version,
1030+
target,
1031+
is_default_target,
1032+
build,
1033+
metadata,
1034+
limits,
1035+
) {
1036+
error!(
1037+
?err,
1038+
"internal error when trying to generate rustdoc JSON output"
1039+
);
1040+
}
1041+
8861042
let successful = {
8871043
let _span = info_span!("cargo_build", target = %target, is_default_target).entered();
8881044
logging::capture(&storage, || {
@@ -1114,13 +1270,12 @@ impl Default for BuildPackageSummary {
11141270

11151271
#[cfg(test)]
11161272
mod tests {
1117-
use std::iter;
1118-
11191273
use super::*;
11201274
use crate::db::types::Feature;
11211275
use crate::registry_api::ReleaseData;
11221276
use crate::storage::CompressionAlgorithm;
11231277
use crate::test::{AxumRouterTestExt, TestEnvironment, wrapper};
1278+
use std::{io, iter};
11241279

11251280
fn get_features(
11261281
env: &TestEnvironment,
@@ -1305,6 +1460,31 @@ mod tests {
13051460

13061461
// other targets too
13071462
for target in DEFAULT_TARGETS {
1463+
// check if rustdoc json files exist for all targets
1464+
assert!(storage.exists(&rustdoc_json_path(
1465+
crate_,
1466+
version,
1467+
target,
1468+
RustdocJsonFormatVersion::Latest
1469+
))?);
1470+
1471+
let json_prefix = format!("rustdoc-json/{crate_}/{version}/{target}/");
1472+
let mut json_files: Vec<_> = storage
1473+
.list_prefix(&json_prefix)
1474+
.filter_map(|res| res.ok())
1475+
.map(|f| f.strip_prefix(&json_prefix).unwrap().to_owned())
1476+
.collect();
1477+
json_files.sort();
1478+
dbg!(&json_prefix);
1479+
dbg!(&json_files);
1480+
assert_eq!(
1481+
json_files,
1482+
vec![
1483+
format!("empty-library_1.0.0_{target}_45.json.zst"),
1484+
format!("empty-library_1.0.0_{target}_latest.json.zst"),
1485+
]
1486+
);
1487+
13081488
if target == &default_target {
13091489
continue;
13101490
}
@@ -1876,4 +2056,19 @@ mod tests {
18762056
Ok(())
18772057
})
18782058
}
2059+
2060+
#[test]
2061+
fn test_read_format_version_from_rustdoc_json() -> Result<()> {
2062+
let buf = serde_json::to_vec(&serde_json::json!({
2063+
"something": "else",
2064+
"format_version": 42
2065+
}))?;
2066+
2067+
assert_eq!(
2068+
read_format_version_from_rustdoc_json(&mut io::Cursor::new(buf))?,
2069+
RustdocJsonFormatVersion::Version(42)
2070+
);
2071+
2072+
Ok(())
2073+
}
18792074
}

0 commit comments

Comments
 (0)