@@ -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,92 @@ 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
+ #[ 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
+
784
911
#[ instrument( skip( self , build) ) ]
785
912
fn get_coverage (
786
913
& self ,
@@ -841,6 +968,9 @@ impl RustwideBuilder {
841
968
#[ allow( clippy:: too_many_arguments) ]
842
969
fn execute_build (
843
970
& self ,
971
+ build_id : BuildId ,
972
+ name : & str ,
973
+ version : & str ,
844
974
target : & str ,
845
975
is_default_target : bool ,
846
976
build : & Build ,
@@ -883,6 +1013,22 @@ impl RustwideBuilder {
883
1013
}
884
1014
} ;
885
1015
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
+
886
1032
let successful = {
887
1033
let _span = info_span ! ( "cargo_build" , target = %target, is_default_target) . entered ( ) ;
888
1034
logging:: capture ( & storage, || {
@@ -1114,13 +1260,12 @@ impl Default for BuildPackageSummary {
1114
1260
1115
1261
#[ cfg( test) ]
1116
1262
mod tests {
1117
- use std:: iter;
1118
-
1119
1263
use super :: * ;
1120
1264
use crate :: db:: types:: Feature ;
1121
1265
use crate :: registry_api:: ReleaseData ;
1122
1266
use crate :: storage:: CompressionAlgorithm ;
1123
1267
use crate :: test:: { AxumRouterTestExt , TestEnvironment , wrapper} ;
1268
+ use std:: { io, iter} ;
1124
1269
1125
1270
fn get_features (
1126
1271
env : & TestEnvironment ,
@@ -1305,6 +1450,31 @@ mod tests {
1305
1450
1306
1451
// other targets too
1307
1452
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
+
1308
1478
if target == & default_target {
1309
1479
continue ;
1310
1480
}
@@ -1876,4 +2046,19 @@ mod tests {
1876
2046
Ok ( ( ) )
1877
2047
} )
1878
2048
}
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
+ }
1879
2064
}
0 commit comments