Skip to content

Commit eb1cb21

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

File tree

4 files changed

+262
-12
lines changed

4 files changed

+262
-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: 194 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,92 @@ 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+
#[allow(clippy::too_many_arguments)]
831+
fn execute_json_build(
832+
&self,
833+
build_id: BuildId,
834+
name: &str,
835+
version: &str,
836+
target: &str,
837+
is_default_target: bool,
838+
build: &Build,
839+
metadata: &Metadata,
840+
limits: &Limits,
841+
) -> Result<()> {
842+
let rustdoc_flags = vec!["--output-format".to_string(), "json".to_string()];
843+
844+
let mut storage = LogStorage::new(log::LevelFilter::Info);
845+
storage.set_max_size(limits.max_log_size());
846+
847+
let successful = logging::capture(&storage, || {
848+
let _span = info_span!("cargo_build_json", target = %target).entered();
849+
self.prepare_command(build, target, metadata, limits, rustdoc_flags, false)
850+
.and_then(|command| command.run().map_err(Error::from))
851+
.is_ok()
852+
});
853+
854+
{
855+
let _span = info_span!("store_json_build_logs").entered();
856+
let build_log_path = format!("build-logs/{build_id}/{target}_json.txt");
857+
self.storage
858+
.store_one(build_log_path, storage.to_string())
859+
.context("storing build log on S3")?;
860+
}
861+
862+
if !successful {
863+
// this is a normal build error and will be visible in the uploaded build logs.
864+
// We don't need the Err variant here.
865+
return Ok(());
866+
}
867+
868+
let json_dir = if metadata.proc_macro {
869+
assert!(
870+
is_default_target && target == HOST_TARGET,
871+
"can't handle cross-compiling macros"
872+
);
873+
build.host_target_dir().join("doc")
874+
} else {
875+
build.host_target_dir().join(target).join("doc")
876+
};
877+
878+
let json_filename = fs::read_dir(&json_dir)?
879+
.filter_map(|entry| {
880+
let entry = entry.ok()?;
881+
let path = entry.path();
882+
if path.is_file() && path.extension()? == "json" {
883+
Some(path)
884+
} else {
885+
None
886+
}
887+
})
888+
.next()
889+
.ok_or_else(|| {
890+
anyhow!("no JSON file found in target/doc after successful rustdoc json build")
891+
})?;
892+
893+
let format_version = read_format_version_from_rustdoc_json(&File::open(&json_filename)?)
894+
.context("couldn't parse rustdoc json to find format version")?;
895+
let compressed_json: Vec<u8> = compress(
896+
BufReader::new(File::open(&json_filename)?),
897+
CompressionAlgorithm::Zstd,
898+
)?;
899+
900+
for format_version in [format_version, RustdocJsonFormatVersion::Latest] {
901+
let _span = info_span!("store_json", %format_version).entered();
902+
self.storage.store_one(
903+
rustdoc_json_path(name, version, target, format_version),
904+
compressed_json.clone(),
905+
)?;
906+
}
907+
908+
Ok(())
909+
}
910+
784911
#[instrument(skip(self, build))]
785912
fn get_coverage(
786913
&self,
@@ -841,6 +968,9 @@ impl RustwideBuilder {
841968
#[allow(clippy::too_many_arguments)]
842969
fn execute_build(
843970
&self,
971+
build_id: BuildId,
972+
name: &str,
973+
version: &str,
844974
target: &str,
845975
is_default_target: bool,
846976
build: &Build,
@@ -883,6 +1013,22 @@ impl RustwideBuilder {
8831013
}
8841014
};
8851015

1016+
if let Err(err) = self.execute_json_build(
1017+
build_id,
1018+
name,
1019+
version,
1020+
target,
1021+
is_default_target,
1022+
build,
1023+
metadata,
1024+
limits,
1025+
) {
1026+
error!(
1027+
?err,
1028+
"internal error when trying to generate rustdoc JSON output"
1029+
);
1030+
}
1031+
8861032
let successful = {
8871033
let _span = info_span!("cargo_build", target = %target, is_default_target).entered();
8881034
logging::capture(&storage, || {
@@ -1114,13 +1260,12 @@ impl Default for BuildPackageSummary {
11141260

11151261
#[cfg(test)]
11161262
mod tests {
1117-
use std::iter;
1118-
11191263
use super::*;
11201264
use crate::db::types::Feature;
11211265
use crate::registry_api::ReleaseData;
11221266
use crate::storage::CompressionAlgorithm;
11231267
use crate::test::{AxumRouterTestExt, TestEnvironment, wrapper};
1268+
use std::{io, iter};
11241269

11251270
fn get_features(
11261271
env: &TestEnvironment,
@@ -1305,6 +1450,31 @@ mod tests {
13051450

13061451
// other targets too
13071452
for target in DEFAULT_TARGETS {
1453+
// check if rustdoc json files exist for all targets
1454+
assert!(storage.exists(&rustdoc_json_path(
1455+
crate_,
1456+
version,
1457+
target,
1458+
RustdocJsonFormatVersion::Latest
1459+
))?);
1460+
1461+
let json_prefix = format!("rustdoc-json/{crate_}/{version}/{target}/");
1462+
let mut json_files: Vec<_> = storage
1463+
.list_prefix(&json_prefix)
1464+
.filter_map(|res| res.ok())
1465+
.map(|f| f.strip_prefix(&json_prefix).unwrap().to_owned())
1466+
.collect();
1467+
json_files.sort();
1468+
dbg!(&json_prefix);
1469+
dbg!(&json_files);
1470+
assert_eq!(
1471+
json_files,
1472+
vec![
1473+
format!("empty-library_1.0.0_{target}_45.json.zst"),
1474+
format!("empty-library_1.0.0_{target}_latest.json.zst"),
1475+
]
1476+
);
1477+
13081478
if target == &default_target {
13091479
continue;
13101480
}
@@ -1876,4 +2046,19 @@ mod tests {
18762046
Ok(())
18772047
})
18782048
}
2049+
2050+
#[test]
2051+
fn test_read_format_version_from_rustdoc_json() -> Result<()> {
2052+
let buf = serde_json::to_vec(&serde_json::json!({
2053+
"something": "else",
2054+
"format_version": 42
2055+
}))?;
2056+
2057+
assert_eq!(
2058+
read_format_version_from_rustdoc_json(&mut io::Cursor::new(buf))?,
2059+
RustdocJsonFormatVersion::Version(42)
2060+
);
2061+
2062+
Ok(())
2063+
}
18792064
}

0 commit comments

Comments
 (0)