@@ -12,7 +12,10 @@ use crate::db::{
1212use crate :: docbuilder:: Limits ;
1313use crate :: error:: Result ;
1414use 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+ } ;
1619use 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};
2629use rustwide:: logging:: { self , LogStorage } ;
2730use rustwide:: toolchain:: ToolchainError ;
2831use rustwide:: { AlternativeRegistry , Build , Crate , Toolchain , Workspace , WorkspaceBuilder } ;
32+ use serde:: Deserialize ;
2933use std:: collections:: { HashMap , HashSet } ;
30- use std:: fs;
34+ use std:: fs:: { self , File } ;
35+ use std:: io:: BufReader ;
3136use std:: path:: Path ;
3237use std:: sync:: Arc ;
3338use std:: time:: Instant ;
3439use tokio:: runtime:: Runtime ;
35- use tracing:: { debug, info, info_span, instrument, warn} ;
40+ use tracing:: { debug, error , info, info_span, instrument, warn} ;
3641
3742const USER_AGENT : & str = "docs.rs builder (https://github.com/rust-lang/docs.rs)" ;
3843const COMPONENTS : & [ & str ] = & [ "llvm-tools-preview" , "rustc-dev" , "rustfmt" ] ;
3944const DUMMY_CRATE_NAME : & str = "empty-library" ;
4045const 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+
4265async 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) ]
11161272mod 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