@@ -12,7 +12,10 @@ use crate::db::{
12
12
use crate :: docbuilder:: Limits ;
13
13
use crate :: error:: Result ;
14
14
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
+ } ;
16
19
use crate :: utils:: {
17
20
CargoMetadata , ConfigName , copy_dir_all, get_config, parse_rustc_version, report_error,
18
21
set_config,
@@ -26,19 +29,39 @@ use rustwide::cmd::{Command, CommandError, SandboxBuilder, SandboxImage};
26
29
use rustwide:: logging:: { self , LogStorage } ;
27
30
use rustwide:: toolchain:: ToolchainError ;
28
31
use rustwide:: { AlternativeRegistry , Build , Crate , Toolchain , Workspace , WorkspaceBuilder } ;
32
+ use serde:: Deserialize ;
29
33
use std:: collections:: { HashMap , HashSet } ;
30
- use std:: fs;
34
+ use std:: fs:: { self , File } ;
35
+ use std:: io:: BufReader ;
31
36
use std:: path:: Path ;
32
37
use std:: sync:: Arc ;
33
38
use std:: time:: Instant ;
34
39
use tokio:: runtime:: Runtime ;
35
- use tracing:: { debug, info, info_span, instrument, warn} ;
40
+ use tracing:: { debug, error , info, info_span, instrument, warn} ;
36
41
37
42
const USER_AGENT : & str = "docs.rs builder (https://github.com/rust-lang/docs.rs)" ;
38
43
const COMPONENTS : & [ & str ] = & [ "llvm-tools-preview" , "rustc-dev" , "rustfmt" ] ;
39
44
const DUMMY_CRATE_NAME : & str = "empty-library" ;
40
45
const DUMMY_CRATE_VERSION : & str = "1.0.0" ;
41
46
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
+
42
65
async fn get_configured_toolchain ( conn : & mut sqlx:: PgConnection ) -> Result < Toolchain > {
43
66
let name: String = get_config ( conn, ConfigName :: Toolchain )
44
67
. await ?
@@ -303,8 +326,18 @@ impl RustwideBuilder {
303
326
. run ( |build| {
304
327
let metadata = Metadata :: from_crate_root ( build. host_source_dir ( ) ) ?;
305
328
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
+ ) ?;
308
341
if !res. result . successful {
309
342
bail ! ( "failed to build dummy crate for {}" , rustc_version) ;
310
343
}
@@ -518,12 +551,13 @@ impl RustwideBuilder {
518
551
build. fetch_build_std_dependencies ( & targets) ?;
519
552
}
520
553
554
+
521
555
let mut has_docs = false ;
522
556
let mut successful_targets = Vec :: new ( ) ;
523
557
524
558
// Perform an initial build
525
559
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) ?;
527
561
528
562
// If the build fails with the lockfile given, try using only the dependencies listed in Cargo.toml.
529
563
let cargo_lock = build. host_source_dir ( ) . join ( "Cargo.lock" ) ;
@@ -545,7 +579,7 @@ impl RustwideBuilder {
545
579
. run_capture ( ) ?;
546
580
}
547
581
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) ?;
549
583
}
550
584
551
585
if res. result . successful {
@@ -576,6 +610,7 @@ impl RustwideBuilder {
576
610
for target in other_targets. into_iter ( ) . take ( limits. targets ( ) ) {
577
611
debug ! ( "building package {} {} for {}" , name, version, target) ;
578
612
let target_res = self . build_target (
613
+ build_id, name, version,
579
614
target,
580
615
build,
581
616
& limits,
@@ -751,6 +786,9 @@ impl RustwideBuilder {
751
786
#[ allow( clippy:: too_many_arguments) ]
752
787
fn build_target (
753
788
& self ,
789
+ build_id : BuildId ,
790
+ name : & str ,
791
+ version : & str ,
754
792
target : & str ,
755
793
build : & Build ,
756
794
limits : & Limits ,
@@ -760,6 +798,9 @@ impl RustwideBuilder {
760
798
collect_metrics : bool ,
761
799
) -> Result < FullBuildResult > {
762
800
let target_res = self . execute_build (
801
+ build_id,
802
+ name,
803
+ version,
763
804
target,
764
805
false ,
765
806
build,
@@ -781,6 +822,102 @@ impl RustwideBuilder {
781
822
Ok ( target_res)
782
823
}
783
824
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
+
784
921
#[ instrument( skip( self , build) ) ]
785
922
fn get_coverage (
786
923
& self ,
@@ -841,6 +978,9 @@ impl RustwideBuilder {
841
978
#[ allow( clippy:: too_many_arguments) ]
842
979
fn execute_build (
843
980
& self ,
981
+ build_id : BuildId ,
982
+ name : & str ,
983
+ version : & str ,
844
984
target : & str ,
845
985
is_default_target : bool ,
846
986
build : & Build ,
@@ -883,6 +1023,22 @@ impl RustwideBuilder {
883
1023
}
884
1024
} ;
885
1025
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
+
886
1042
let successful = {
887
1043
let _span = info_span ! ( "cargo_build" , target = %target, is_default_target) . entered ( ) ;
888
1044
logging:: capture ( & storage, || {
@@ -1114,13 +1270,12 @@ impl Default for BuildPackageSummary {
1114
1270
1115
1271
#[ cfg( test) ]
1116
1272
mod tests {
1117
- use std:: iter;
1118
-
1119
1273
use super :: * ;
1120
1274
use crate :: db:: types:: Feature ;
1121
1275
use crate :: registry_api:: ReleaseData ;
1122
1276
use crate :: storage:: CompressionAlgorithm ;
1123
1277
use crate :: test:: { AxumRouterTestExt , TestEnvironment , wrapper} ;
1278
+ use std:: { io, iter} ;
1124
1279
1125
1280
fn get_features (
1126
1281
env : & TestEnvironment ,
@@ -1305,6 +1460,31 @@ mod tests {
1305
1460
1306
1461
// other targets too
1307
1462
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
+
1308
1488
if target == & default_target {
1309
1489
continue ;
1310
1490
}
@@ -1876,4 +2056,19 @@ mod tests {
1876
2056
Ok ( ( ) )
1877
2057
} )
1878
2058
}
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
+ }
1879
2074
}
0 commit comments