-
Notifications
You must be signed in to change notification settings - Fork 203
/
Copy pathrustwide_builder.rs
1397 lines (1234 loc) · 55 KB
/
rustwide_builder.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
use crate::db::file::add_path_into_database;
use crate::db::{
add_build_into_database, add_doc_coverage, add_package_into_database,
add_path_into_remote_archive, update_crate_data_in_database, Pool,
};
use crate::docbuilder::Limits;
use crate::error::Result;
use crate::repositories::RepositoryStatsUpdater;
use crate::storage::{rustdoc_archive_path, source_archive_path};
use crate::utils::{
copy_dir_all, get_config, parse_rustc_version, report_error, set_config, CargoMetadata,
ConfigName,
};
use crate::RUSTDOC_STATIC_STORAGE_PREFIX;
use crate::{db::blacklist::is_blacklisted, utils::MetadataPackage};
use crate::{AsyncStorage, Config, Context, InstanceMetrics, RegistryApi, Storage};
use anyhow::{anyhow, bail, Context as _, Error};
use docsrs_metadata::{BuildTargets, Metadata, DEFAULT_TARGETS, HOST_TARGET};
use failure::Error as FailureError;
use postgres::Client;
use regex::Regex;
use rustwide::cmd::{Command, CommandError, SandboxBuilder, SandboxImage};
use rustwide::logging::{self, LogStorage};
use rustwide::toolchain::ToolchainError;
use rustwide::{AlternativeRegistry, Build, Crate, Toolchain, Workspace, WorkspaceBuilder};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
use tokio::runtime::Runtime;
use tracing::{debug, info, warn};
const USER_AGENT: &str = "docs.rs builder (https://github.com/rust-lang/docs.rs)";
const COMPONENTS: &[&str] = &["llvm-tools-preview", "rustc-dev", "rustfmt"];
const DUMMY_CRATE_NAME: &str = "empty-library";
const DUMMY_CRATE_VERSION: &str = "1.0.0";
fn get_configured_toolchain(conn: &mut Client) -> Result<Toolchain> {
let name: String = get_config(conn, ConfigName::Toolchain)?.unwrap_or_else(|| "nightly".into());
// If the toolchain is all hex, assume it references an artifact from
// CI, for instance an `@bors try` build.
let re = Regex::new(r"^[a-fA-F0-9]+$").unwrap();
if re.is_match(&name) {
debug!("using CI build {}", &name);
Ok(Toolchain::ci(&name, false))
} else {
debug!("using toolchain {}", &name);
Ok(Toolchain::dist(&name))
}
}
fn build_workspace(context: &dyn Context) -> Result<Workspace> {
let config = context.config()?;
let mut builder = WorkspaceBuilder::new(&config.rustwide_workspace, USER_AGENT)
.running_inside_docker(config.inside_docker);
if let Some(custom_image) = &config.docker_image {
let image = match SandboxImage::local(custom_image) {
Ok(i) => i,
Err(CommandError::SandboxImageMissing(_)) => SandboxImage::remote(custom_image)?,
Err(err) => return Err(err.into()),
};
builder = builder.sandbox_image(image);
}
if cfg!(test) {
builder = builder.fast_init(true);
}
let workspace = builder.init().map_err(FailureError::compat)?;
workspace
.purge_all_build_dirs()
.map_err(FailureError::compat)?;
Ok(workspace)
}
pub enum PackageKind<'a> {
Local(&'a Path),
CratesIo,
Registry(&'a str),
}
pub struct RustwideBuilder {
workspace: Workspace,
toolchain: Toolchain,
runtime: Arc<Runtime>,
config: Arc<Config>,
db: Pool,
storage: Arc<Storage>,
async_storage: Arc<AsyncStorage>,
metrics: Arc<InstanceMetrics>,
registry_api: Arc<RegistryApi>,
repository_stats_updater: Arc<RepositoryStatsUpdater>,
workspace_initialize_time: Instant,
}
impl RustwideBuilder {
pub fn init(context: &dyn Context) -> Result<Self> {
let config = context.config()?;
let pool = context.pool()?;
let runtime = context.runtime()?;
Ok(RustwideBuilder {
workspace: build_workspace(context)?,
toolchain: get_configured_toolchain(&mut *pool.get()?)?,
config,
db: pool,
runtime: runtime.clone(),
storage: context.storage()?,
async_storage: runtime.block_on(context.async_storage())?,
metrics: context.instance_metrics()?,
registry_api: context.registry_api()?,
repository_stats_updater: context.repository_stats_updater()?,
workspace_initialize_time: Instant::now(),
})
}
pub fn reinitialize_workspace_if_interval_passed(
&mut self,
context: &dyn Context,
) -> Result<()> {
let interval = context.config()?.build_workspace_reinitialization_interval;
if self.workspace_initialize_time.elapsed() >= interval {
info!("start reinitialize workspace again");
self.workspace = build_workspace(context)?;
self.workspace_initialize_time = Instant::now();
}
Ok(())
}
fn prepare_sandbox(&self, limits: &Limits) -> SandboxBuilder {
SandboxBuilder::new()
.cpu_limit(self.config.build_cpu_limit.map(|limit| limit as f32))
.memory_limit(Some(limits.memory()))
.enable_networking(limits.networking())
}
pub fn purge_caches(&self) -> Result<()> {
self.workspace
.purge_all_caches()
.map_err(FailureError::compat)?;
Ok(())
}
pub fn update_toolchain(&mut self) -> Result<bool> {
self.toolchain = get_configured_toolchain(&mut *self.db.get()?)?;
// For CI builds, a lot of the normal update_toolchain things don't apply.
// CI builds are only for one platform (https://forge.rust-lang.org/infra/docs/rustc-ci.html#try-builds)
// so we only try installing for the current platform. If that's not a match,
// for instance if we're running on macOS or Windows, this will error.
// Also, detecting the rustc version relies on calling rustc through rustup with the
// +channel argument, but the +channel argument doesn't work for CI builds. So
// we fake the rustc version and install from scratch every time since we can't detect
// the already-installed rustc version.
if self.toolchain.as_ci().is_some() {
self.toolchain
.install(&self.workspace)
.map_err(FailureError::compat)?;
self.add_essential_files()?;
return Ok(true);
}
// Ignore errors if detection fails.
let old_version = self.detect_rustc_version().ok();
let mut targets_to_install = DEFAULT_TARGETS
.iter()
.map(|&t| t.to_string()) // &str has a specialized ToString impl, while &&str goes through Display
.collect::<HashSet<_>>();
let installed_targets = match self.toolchain.installed_targets(&self.workspace) {
Ok(targets) => targets,
Err(err) => {
if let Some(&ToolchainError::NotInstalled) = err.downcast_ref::<ToolchainError>() {
Vec::new()
} else {
return Err(err.compat().into());
}
}
};
// The extra targets are intentionally removed *before* trying to update.
//
// If a target is installed locally and it goes missing the next update, rustup will block
// the update to avoid leaving the system in a broken state. This is not a behavior we want
// though when we also remove the target from the list managed by docs.rs: we want that
// target gone, and we don't care if it's missing in the next update.
//
// Removing it beforehand works fine, and prevents rustup from blocking the update later in
// the method.
//
// Note that this means that non tier-one targets will be uninstalled on every update,
// and will not be reinstalled until explicitly requested by a crate.
for target in installed_targets {
if !targets_to_install.remove(&target) {
self.toolchain
.remove_target(&self.workspace, &target)
.map_err(FailureError::compat)?;
}
}
self.toolchain
.install(&self.workspace)
.map_err(FailureError::compat)?;
for target in &targets_to_install {
self.toolchain
.add_target(&self.workspace, target)
.map_err(FailureError::compat)?;
}
// NOTE: rustup will automatically refuse to update the toolchain
// if `rustfmt` is not available in the newer version
// NOTE: this ignores the error so that you can still run a build without rustfmt.
// This should only happen if you run a build for the first time when rustfmt isn't available.
for component in COMPONENTS {
if let Err(err) = self.toolchain.add_component(&self.workspace, component) {
warn!("failed to install {component}: {err}");
info!("continuing anyway, since this must be the first build");
}
}
let has_changed = old_version != Some(self.rustc_version()?);
Ok(has_changed)
}
fn rustc_version(&self) -> Result<String> {
let version = self
.toolchain
.as_ci()
.map(|ci| {
// Detecting the rustc version relies on calling rustc through rustup with the
// +channel argument, but the +channel argument doesn't work for CI builds. So
// we fake the rustc version.
Ok(format!("rustc 1.9999.0-nightly ({} 2999-12-29)", ci.sha()))
})
.unwrap_or_else(|| self.detect_rustc_version())?;
Ok(version)
}
/// Return a string containing the output of `rustc --version`. Only valid
/// for dist toolchains. Will error if run with a CI toolchain.
fn detect_rustc_version(&self) -> Result<String> {
info!("detecting rustc's version...");
let res = Command::new(&self.workspace, self.toolchain.rustc())
.args(&["--version"])
.log_output(false)
.run_capture()?;
let mut iter = res.stdout_lines().iter();
if let (Some(line), None) = (iter.next(), iter.next()) {
info!("found rustc {}", line);
Ok(line.clone())
} else {
Err(anyhow!("invalid output returned by `rustc --version`",))
}
}
fn get_limits(&self, krate: &str) -> Result<Limits> {
self.runtime.block_on({
let db = self.db.clone();
let config = self.config.clone();
async move {
let mut conn = db.get_async().await?;
Limits::for_crate(&config, &mut conn, krate).await
}
})
}
pub fn add_essential_files(&mut self) -> Result<()> {
let rustc_version = self.rustc_version()?;
let parsed_rustc_version = parse_rustc_version(&rustc_version)?;
info!("building a dummy crate to get essential files");
let mut conn = self.db.get()?;
let limits = self.get_limits(DUMMY_CRATE_NAME)?;
// FIXME: for now, purge all build dirs before each build.
// Currently we have some error situations where the build directory wouldn't be deleted
// after the build failed:
// https://github.com/rust-lang/docs.rs/issues/820
// This should be solved in a better way, likely refactoring the whole builder structure,
// but for now we chose this simple way to prevent that the build directory remains can
// fill up disk space.
// This also prevents having multiple builders using the same rustwide workspace,
// which we don't do. Currently our separate builders use a separate rustwide workspace.
self.workspace
.purge_all_build_dirs()
.map_err(FailureError::compat)?;
let mut build_dir = self
.workspace
.build_dir(&format!("essential-files-{parsed_rustc_version}"));
// This is an empty library crate that is supposed to always build.
let krate = Crate::crates_io(DUMMY_CRATE_NAME, DUMMY_CRATE_VERSION);
krate.fetch(&self.workspace).map_err(FailureError::compat)?;
build_dir
.build(&self.toolchain, &krate, self.prepare_sandbox(&limits))
.run(|build| {
(|| -> Result<()> {
let metadata = Metadata::from_crate_root(build.host_source_dir())?;
let res =
self.execute_build(HOST_TARGET, true, build, &limits, &metadata, true)?;
if !res.result.successful {
bail!("failed to build dummy crate for {}", rustc_version);
}
info!("copying essential files for {}", rustc_version);
assert!(!metadata.proc_macro);
let source = build.host_target_dir().join(HOST_TARGET).join("doc");
let dest = tempfile::Builder::new()
.prefix("essential-files")
.tempdir()?;
copy_dir_all(source, &dest)?;
// One https://github.com/rust-lang/rust/pull/101702 lands, static files will be
// put in their own directory, "static.files". To make sure those files are
// available at --static-root-path, we add files from that subdirectory, if present.
let static_files = dest.as_ref().join("static.files");
if static_files.try_exists()? {
self.runtime.block_on(add_path_into_database(
&self.async_storage,
RUSTDOC_STATIC_STORAGE_PREFIX,
&static_files,
))?;
} else {
self.runtime.block_on(add_path_into_database(
&self.async_storage,
RUSTDOC_STATIC_STORAGE_PREFIX,
&dest,
))?;
}
set_config(&mut conn, ConfigName::RustcVersion, rustc_version)?;
Ok(())
})()
.map_err(|e| failure::Error::from_boxed_compat(e.into()))
})
.map_err(|e| e.compat())?;
krate
.purge_from_cache(&self.workspace)
.map_err(FailureError::compat)?;
Ok(())
}
pub fn build_local_package(&mut self, path: &Path) -> Result<bool> {
let metadata = CargoMetadata::load_from_rustwide(&self.workspace, &self.toolchain, path)
.map_err(|err| {
err.context(format!("failed to load local package {}", path.display()))
})?;
let package = metadata.root();
self.build_package(&package.name, &package.version, PackageKind::Local(path))
}
pub fn build_package(
&mut self,
name: &str,
version: &str,
kind: PackageKind<'_>,
) -> Result<bool> {
let mut conn = self.db.get()?;
info!("building package {} {}", name, version);
if is_blacklisted(&mut conn, name)? {
info!("skipping build of {}, crate has been blacklisted", name);
return Ok(false);
}
let limits = self.get_limits(name)?;
#[cfg(target_os = "linux")]
if !self.config.disable_memory_limit {
use anyhow::Context;
let mem_info = procfs::Meminfo::new().context("failed to read /proc/meminfo")?;
let available = mem_info
.mem_available
.expect("kernel version too old for determining memory limit");
if limits.memory() as u64 > available {
bail!("not enough memory to build {} {}: needed {} MiB, have {} MiB\nhelp: set DOCSRS_DISABLE_MEMORY_LIMIT=true to force a build",
name, version, limits.memory() / 1024 / 1024, available / 1024 / 1024
);
} else {
debug!(
"had enough memory: {} MiB <= {} MiB",
limits.memory() / 1024 / 1024,
available / 1024 / 1024
);
}
}
// FIXME: for now, purge all build dirs before each build.
// Currently we have some error situations where the build directory wouldn't be deleted
// after the build failed:
// https://github.com/rust-lang/docs.rs/issues/820
// This should be solved in a better way, likely refactoring the whole builder structure,
// but for now we chose this simple way to prevent that the build directory remains can
// fill up disk space.
// This also prevents having multiple builders using the same rustwide workspace,
// which we don't do. Currently our separate builders use a separate rustwide workspace.
self.workspace
.purge_all_build_dirs()
.map_err(FailureError::compat)?;
let mut build_dir = self.workspace.build_dir(&format!("{name}-{version}"));
let is_local = matches!(kind, PackageKind::Local(_));
let krate = match kind {
PackageKind::Local(path) => Crate::local(path),
PackageKind::CratesIo => Crate::crates_io(name, version),
PackageKind::Registry(registry) => {
Crate::registry(AlternativeRegistry::new(registry), name, version)
}
};
krate.fetch(&self.workspace).map_err(FailureError::compat)?;
fs::create_dir_all(&self.config.temp_dir)?;
let local_storage = tempfile::tempdir_in(&self.config.temp_dir)?;
let successful = build_dir
.build(&self.toolchain, &krate, self.prepare_sandbox(&limits))
.run(|build| {
let metadata = Metadata::from_crate_root(build.host_source_dir())?;
let BuildTargets {
default_target,
other_targets,
} = metadata.targets(self.config.include_default_targets);
let mut targets = vec![default_target];
targets.extend(&other_targets);
// Fetch this before we enter the sandbox, so networking isn't blocked.
build.fetch_build_std_dependencies(&targets)?;
(|| -> Result<bool> {
let mut has_docs = false;
let mut successful_targets = Vec::new();
// Perform an initial build
let mut res =
self.execute_build(default_target, true, build, &limits, &metadata, false)?;
// If the build fails with the lockfile given, try using only the dependencies listed in Cargo.toml.
let cargo_lock = build.host_source_dir().join("Cargo.lock");
if !res.result.successful && cargo_lock.exists() {
info!("removing lockfile and reattempting build");
std::fs::remove_file(cargo_lock)?;
Command::new(&self.workspace, self.toolchain.cargo())
.cd(build.host_source_dir())
.args(&["generate-lockfile"])
.run()?;
Command::new(&self.workspace, self.toolchain.cargo())
.cd(build.host_source_dir())
.args(&["fetch", "--locked"])
.run()?;
res = self.execute_build(
default_target,
true,
build,
&limits,
&metadata,
false,
)?;
}
if res.result.successful {
if let Some(name) = res.cargo_metadata.root().library_name() {
let host_target = build.host_target_dir();
has_docs = host_target
.join(default_target)
.join("doc")
.join(name)
.is_dir();
}
}
let mut algs = HashSet::new();
if has_docs {
debug!("adding documentation for the default target to the database");
self.copy_docs(
&build.host_target_dir(),
local_storage.path(),
default_target,
true,
)?;
successful_targets.push(res.target.clone());
// Then build the documentation for all the targets
// Limit the number of targets so that no one can try to build all 200000 possible targets
for target in other_targets.into_iter().take(limits.targets()) {
debug!("building package {} {} for {}", name, version, target);
self.build_target(
target,
build,
&limits,
local_storage.path(),
&mut successful_targets,
&metadata,
)?;
}
let (_, new_alg) = self.runtime.block_on(add_path_into_remote_archive(
&self.async_storage,
&rustdoc_archive_path(name, version),
local_storage.path(),
true,
))?;
algs.insert(new_alg);
};
// Store the sources even if the build fails
debug!("adding sources into database");
let files_list = {
let (files_list, new_alg) =
self.runtime.block_on(add_path_into_remote_archive(
&self.async_storage,
&source_archive_path(name, version),
build.host_source_dir(),
false,
))?;
algs.insert(new_alg);
files_list
};
let has_examples = build.host_source_dir().join("examples").is_dir();
if res.result.successful {
self.metrics.successful_builds.inc();
} else if res.cargo_metadata.root().is_library() {
self.metrics.failed_builds.inc();
} else {
self.metrics.non_library_builds.inc();
}
let release_data = if !is_local {
match self
.runtime
.block_on(self.registry_api.get_release_data(name, version))
.with_context(|| {
format!("could not fetch releases-data for {name}-{version}")
}) {
Ok(data) => Some(data),
Err(err) => {
report_error(&err);
None
}
}
} else {
None
}
.unwrap_or_default();
let cargo_metadata = res.cargo_metadata.root();
let repository = self.get_repo(cargo_metadata)?;
let mut async_conn = self.runtime.block_on(self.db.get_async())?;
let release_id = self.runtime.block_on(add_package_into_database(
&mut async_conn,
cargo_metadata,
&build.host_source_dir(),
&res.result,
&res.target,
files_list,
successful_targets,
&release_data,
has_docs,
has_examples,
algs,
repository,
true,
))?;
if let Some(doc_coverage) = res.doc_coverage {
self.runtime.block_on(add_doc_coverage(
&mut async_conn,
release_id,
doc_coverage,
))?;
}
let build_id = self.runtime.block_on(add_build_into_database(
&mut async_conn,
release_id,
&res.result,
))?;
let build_log_path = format!("build-logs/{build_id}/{default_target}.txt");
self.storage.store_one(build_log_path, res.build_log)?;
// Some crates.io crate data is mutable, so we proactively update it during a release
if !is_local {
match self
.runtime
.block_on(self.registry_api.get_crate_data(name))
{
Ok(crate_data) => self.runtime.block_on(
update_crate_data_in_database(&mut async_conn, name, &crate_data),
)?,
Err(err) => warn!("{:#?}", err),
}
}
if res.result.successful {
// delete eventually existing files from pre-archive storage.
// we're doing this in the end so eventual problems in the build
// won't lead to non-existing docs.
for prefix in &["rustdoc", "sources"] {
let prefix = format!("{prefix}/{name}/{version}/");
debug!("cleaning old storage folder {}", prefix);
self.storage.delete_prefix(&prefix)?;
}
}
self.runtime.block_on(async move {
// we need to drop the async connection inside an async runtime context
// so sqlx can use a runtime to handle the pool.
drop(async_conn);
});
Ok(res.result.successful)
})()
.map_err(|e| failure::Error::from_boxed_compat(e.into()))
})
.map_err(|e| e.compat())?;
krate
.purge_from_cache(&self.workspace)
.map_err(FailureError::compat)?;
local_storage.close()?;
Ok(successful)
}
fn build_target(
&self,
target: &str,
build: &Build,
limits: &Limits,
local_storage: &Path,
successful_targets: &mut Vec<String>,
metadata: &Metadata,
) -> Result<()> {
let target_res = self.execute_build(target, false, build, limits, metadata, false)?;
if target_res.result.successful {
// Cargo is not giving any error and not generating documentation of some crates
// when we use a target compile options. Check documentation exists before
// adding target to successfully_targets.
if build.host_target_dir().join(target).join("doc").is_dir() {
debug!("adding documentation for target {} to the database", target,);
self.copy_docs(&build.host_target_dir(), local_storage, target, false)?;
successful_targets.push(target.to_string());
}
}
Ok(())
}
fn get_coverage(
&self,
target: &str,
build: &Build,
metadata: &Metadata,
limits: &Limits,
) -> Result<Option<DocCoverage>> {
let rustdoc_flags = vec![
"--output-format".to_string(),
"json".to_string(),
"--show-coverage".to_string(),
];
#[derive(serde::Deserialize)]
struct FileCoverage {
total: i32,
with_docs: i32,
total_examples: i32,
with_examples: i32,
}
let mut coverage = DocCoverage {
total_items: 0,
documented_items: 0,
total_items_needing_examples: 0,
items_with_examples: 0,
};
self.prepare_command(build, target, metadata, limits, rustdoc_flags)?
.process_lines(&mut |line, _| {
if line.starts_with('{') && line.ends_with('}') {
let parsed = match serde_json::from_str::<HashMap<String, FileCoverage>>(line) {
Ok(parsed) => parsed,
Err(_) => return,
};
for file in parsed.values() {
coverage.total_items += file.total;
coverage.documented_items += file.with_docs;
coverage.total_items_needing_examples += file.total_examples;
coverage.items_with_examples += file.with_examples;
}
}
})
.log_output(false)
.run()?;
Ok(
if coverage.total_items == 0 && coverage.documented_items == 0 {
None
} else {
Some(coverage)
},
)
}
fn execute_build(
&self,
target: &str,
is_default_target: bool,
build: &Build,
limits: &Limits,
metadata: &Metadata,
create_essential_files: bool,
) -> Result<FullBuildResult> {
let cargo_metadata = CargoMetadata::load_from_rustwide(
&self.workspace,
&self.toolchain,
&build.host_source_dir(),
)?;
let mut rustdoc_flags = vec![if create_essential_files {
"--emit=unversioned-shared-resources,toolchain-shared-resources"
} else {
"--emit=invocation-specific"
}
.to_string()];
rustdoc_flags.extend(vec![
"--resource-suffix".to_string(),
format!("-{}", parse_rustc_version(self.rustc_version()?)?),
]);
let mut storage = LogStorage::new(log::LevelFilter::Info);
storage.set_max_size(limits.max_log_size());
// we have to run coverage before the doc-build because currently it
// deletes the doc-target folder.
// https://github.com/rust-lang/cargo/issues/9447
let doc_coverage = match self.get_coverage(target, build, metadata, limits) {
Ok(cov) => cov,
Err(err) => {
info!("error when trying to get coverage: {}", err);
info!("continuing anyways.");
None
}
};
let successful = logging::capture(&storage, || {
self.prepare_command(build, target, metadata, limits, rustdoc_flags)
.and_then(|command| command.run().map_err(Error::from))
.is_ok()
});
// For proc-macros, cargo will put the output in `target/doc`.
// Move it to the target-specific directory for consistency with other builds.
// NOTE: don't rename this if the build failed, because `target/doc` won't exist.
if successful && metadata.proc_macro {
assert!(
is_default_target && target == HOST_TARGET,
"can't handle cross-compiling macros"
);
// mv target/doc target/$target/doc
let target_dir = build.host_target_dir();
let old_dir = target_dir.join("doc");
let new_dir = target_dir.join(target).join("doc");
debug!("rename {} to {}", old_dir.display(), new_dir.display());
std::fs::create_dir(target_dir.join(target))?;
std::fs::rename(old_dir, new_dir)?;
}
Ok(FullBuildResult {
result: BuildResult {
rustc_version: self.rustc_version()?,
docsrs_version: format!("docsrs {}", crate::BUILD_VERSION),
successful,
},
doc_coverage,
cargo_metadata,
build_log: storage.to_string(),
target: target.to_string(),
})
}
fn prepare_command<'ws, 'pl>(
&self,
build: &'ws Build,
target: &str,
metadata: &Metadata,
limits: &Limits,
mut rustdoc_flags_extras: Vec<String>,
) -> Result<Command<'ws, 'pl>> {
// Add docs.rs specific arguments
let mut cargo_args = vec![
"--offline".into(),
// We know that `metadata` unconditionally passes `-Z rustdoc-map`.
// Don't copy paste this, since that fact is not stable and may change in the future.
"-Zunstable-options".into(),
// Add `target` so that if a dependency has target-specific docs, this links to them properly.
//
// Note that this includes the target even if this is the default, since the dependency
// may have a different default (and the web backend will take care of redirecting if
// necessary).
//
// FIXME: host-only crates like proc-macros should probably not have this passed? but #1417 should make it OK
format!(
r#"--config=doc.extern-map.registries.crates-io="https://docs.rs/{{pkg_name}}/{{version}}/{target}""#
),
// Enables the unstable rustdoc-scrape-examples feature. We are "soft launching" this feature on
// docs.rs, but once it's stable we can remove this flag.
"-Zrustdoc-scrape-examples".into(),
];
if let Some(cpu_limit) = self.config.build_cpu_limit {
cargo_args.push(format!("-j{cpu_limit}"));
}
// Cargo has a series of frightening bugs around cross-compiling proc-macros:
// - Passing `--target` causes RUSTDOCFLAGS to fail to be passed 🤦
// - Passing `--target` will *create* `target/{target-name}/doc` but will put the docs in `target/doc` anyway
// As a result, it's not possible for us to support cross-compiling proc-macros.
// However, all these caveats unfortunately still apply when `{target-name}` is the host.
// So, only pass `--target` for crates that aren't proc-macros.
//
// Originally, this had a simpler check `target != HOST_TARGET`, but *that* was buggy when `HOST_TARGET` wasn't the same as the default target.
// Rather than trying to keep track of it all, only special case proc-macros, which are what we actually care about.
if !metadata.proc_macro {
cargo_args.push("--target".into());
cargo_args.push(target.into());
};
#[rustfmt::skip]
const UNCONDITIONAL_ARGS: &[&str] = &[
"--static-root-path", "/-/rustdoc.static/",
"--cap-lints", "warn",
"--extern-html-root-takes-precedence",
];
rustdoc_flags_extras.extend(UNCONDITIONAL_ARGS.iter().map(|&s| s.to_owned()));
let cargo_args = metadata.cargo_args(&cargo_args, &rustdoc_flags_extras);
// If the explicit target is not a tier one target, we need to install it.
let has_build_std = cargo_args.windows(2).any(|args| {
args[0].starts_with("-Zbuild-std")
|| (args[0] == "-Z" && args[1].starts_with("build-std"))
}) || cargo_args.last().unwrap().starts_with("-Zbuild-std");
if !docsrs_metadata::DEFAULT_TARGETS.contains(&target) && !has_build_std {
// This is a no-op if the target is already installed.
self.toolchain
.add_target(&self.workspace, target)
.map_err(FailureError::compat)?;
}
let mut command = build
.cargo()
.timeout(Some(limits.timeout()))
.no_output_timeout(None);
for (key, val) in metadata.environment_variables() {
command = command.env(key, val);
}
Ok(command.args(&cargo_args))
}
fn copy_docs(
&self,
target_dir: &Path,
local_storage: &Path,
target: &str,
is_default_target: bool,
) -> Result<()> {
let source = target_dir.join(target).join("doc");
let mut dest = local_storage.to_path_buf();
// only add target name to destination directory when we are copying a non-default target.
// this is allowing us to host documents in the root of the crate documentation directory.
// for example winapi will be available in docs.rs/winapi/$version/winapi/ for it's
// default target: x86_64-pc-windows-msvc. But since it will be built under
// target/x86_64-pc-windows-msvc we still need target in this function.
if !is_default_target {
dest = dest.join(target);
}
info!("copy {} to {}", source.display(), dest.display());
copy_dir_all(source, dest).map_err(Into::into)
}
fn get_repo(&self, metadata: &MetadataPackage) -> Result<Option<i32>> {
self.runtime
.block_on(self.repository_stats_updater.load_repository(metadata))
.map_err(Into::into)
}
}
struct FullBuildResult {
result: BuildResult,
target: String,
cargo_metadata: CargoMetadata,
doc_coverage: Option<DocCoverage>,
build_log: String,
}
#[derive(Clone, Copy)]
pub(crate) struct DocCoverage {
/// The total items that could be documented in the current crate, used to calculate
/// documentation coverage.
pub(crate) total_items: i32,
/// The items of the crate that are documented, used to calculate documentation coverage.
pub(crate) documented_items: i32,
/// The total items that could have code examples in the current crate, used to calculate
/// documentation coverage.
pub(crate) total_items_needing_examples: i32,
/// The items of the crate that have a code example, used to calculate documentation coverage.
pub(crate) items_with_examples: i32,
}
pub(crate) struct BuildResult {
pub(crate) rustc_version: String,
pub(crate) docsrs_version: String,
pub(crate) successful: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::{assert_redirect, assert_success, wrapper, TestEnvironment};
use serde_json::Value;
fn remove_cache_files(env: &TestEnvironment, crate_: &str, version: &str) -> Result<()> {
let paths = [
format!("cache/index.crates.io-6f17d22bba15001f/{crate_}-{version}.crate"),
format!("src/index.crates.io-6f17d22bba15001f/{crate_}-{version}"),
format!(
"index/index.crates.io-6f17d22bba15001f/.cache/{}/{}/{crate_}",
&crate_[0..2],
&crate_[2..4]
),
];
for path in paths {
let full_path = env
.config()
.rustwide_workspace
.join("cargo-home/registry")
.join(path);
if full_path.exists() {
info!("deleting {}", full_path.display());
if full_path.is_file() {
std::fs::remove_file(full_path)?;
} else {
std::fs::remove_dir_all(full_path)?;
}
}
}
Ok(())
}
#[test]
#[ignore]
fn test_build_crate() {
wrapper(|env| {
let crate_ = DUMMY_CRATE_NAME;
let crate_path = crate_.replace('-', "_");
let version = DUMMY_CRATE_VERSION;
let default_target = "x86_64-unknown-linux-gnu";
let storage = env.storage();
let old_rustdoc_file = format!("rustdoc/{crate_}/{version}/some_doc_file");
let old_source_file = format!("sources/{crate_}/{version}/some_source_file");
storage.store_one(&old_rustdoc_file, Vec::new())?;
storage.store_one(&old_source_file, Vec::new())?;
let mut builder = RustwideBuilder::init(env).unwrap();
builder.update_toolchain()?;
assert!(builder.build_package(crate_, version, PackageKind::CratesIo)?);
// check release record in the db (default and other targets)
let mut conn = env.db().conn();
let rows = conn
.query(
"SELECT
r.rustdoc_status,
r.default_target,
r.doc_targets,
r.archive_storage,
cov.total_items
FROM
crates as c
INNER JOIN releases AS r ON c.id = r.crate_id
LEFT OUTER JOIN doc_coverage AS cov ON r.id = cov.release_id
WHERE
c.name = $1 AND
r.version = $2",
&[&crate_, &version],
)