Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d8350f1

Browse files
committedMar 15, 2025
Auto merge of #138521 - cuviper:stable-next, r=<try>
[stable] Release 1.85.1 - [Fix the doctest-merging feature of the 2024 Edition.](#137899) - [Relax some `target_feature` checks when generating docs.](#137632) - DRAFT NOTE: this is not formally `stable-accepted` yet. It also required some fixup to deal with differences in the `Stability` system, but I think it should have the same effect, limited to `rustdoc` execution. - [Fix errors in `fs::rename` on Windows 1607.](#137528) - [Downgrade bootstrap `cc` to fix custom targets.](#137460) - [Skip submodule updates when building Rust from a source tarball.](#137338) cc `@rust-lang/release` r? cuviper try-job: dist-x86_64-linux try-job: dist-i686-msvc try-job: i686-msvc try-job: x86_64-rust-for-linux
2 parents 4d91de4 + 0e8fd2d commit d8350f1

File tree

18 files changed

+387
-201
lines changed

18 files changed

+387
-201
lines changed
 

‎.github/workflows/ci.yml

+14
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,20 @@ jobs:
189189
- name: ensure the stable version number is correct
190190
run: src/ci/scripts/verify-stable-version-number.sh
191191

192+
# Temporary fix to unblock CI
193+
# Remove the latest Windows SDK for 32-bit Windows MSVC builds.
194+
# See issue https://github.com/rust-lang/rust/issues/137733 for more details.
195+
- name: Remove Windows SDK 10.0.26100.0
196+
shell: powershell
197+
if: ${{ matrix.image == 'i686-msvc' || matrix.image == 'dist-i686-msvc' }}
198+
run: |
199+
$kits = (Get-ItemProperty -path 'HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots').KitsRoot10
200+
$sdk_version = "10.0.26100.0"
201+
202+
foreach ($kind in 'Bin', 'Lib', 'Include') {
203+
Remove-Item -Force -Recurse $kits\$kind\$sdk_version -ErrorAction Continue
204+
}
205+
192206
- name: run the build
193207
# Redirect stderr to stdout to avoid reordering the two streams in the GHA logs.
194208
run: src/ci/scripts/run-build-from-ci.sh 2>&1

‎RELEASES.md

+11
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
Version 1.85.1 (2025-03-18)
2+
==========================
3+
4+
<a id="1.85.1"></a>
5+
6+
- [Fix the doctest-merging feature of the 2024 Edition.](https://github.com/rust-lang/rust/pull/137899/)
7+
- [Relax some `target_feature` checks when generating docs.](https://github.com/rust-lang/rust/pull/137632/)
8+
- [Fix errors in `fs::rename` on Windows 1607.](https://github.com/rust-lang/rust/pull/137528/)
9+
- [Downgrade bootstrap `cc` to fix custom targets.](https://github.com/rust-lang/rust/pull/137460/)
10+
- [Skip submodule updates when building Rust from a source tarball.](https://github.com/rust-lang/rust/pull/137338/)
11+
112
Version 1.85.0 (2025-02-20)
213
==========================
314

‎compiler/rustc_codegen_ssa/src/target_features.rs

+43-11
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use rustc_middle::query::Providers;
1010
use rustc_middle::ty::TyCtxt;
1111
use rustc_session::parse::feature_err;
1212
use rustc_span::{Span, Symbol, sym};
13-
use rustc_target::target_features;
13+
use rustc_target::target_features::{self, Stability};
1414

1515
use crate::errors;
1616

@@ -65,11 +65,16 @@ pub(crate) fn from_target_feature_attr(
6565
// Only allow target features whose feature gates have been enabled
6666
// and which are permitted to be toggled.
6767
if let Err(reason) = stability.toggle_allowed(/*enable*/ true) {
68-
tcx.dcx().emit_err(errors::ForbiddenTargetFeatureAttr {
69-
span: item.span(),
70-
feature,
71-
reason,
72-
});
68+
// We skip this error in rustdoc, where we want to allow all target features of
69+
// all targets, so we can't check their ABI compatibility and anyway we are not
70+
// generating code so "it's fine".
71+
if !tcx.sess.opts.actually_rustdoc {
72+
tcx.dcx().emit_err(errors::ForbiddenTargetFeatureAttr {
73+
span: item.span(),
74+
feature,
75+
reason,
76+
});
77+
}
7378
} else if let Some(nightly_feature) = stability.requires_nightly()
7479
&& !rust_features.enabled(nightly_feature)
7580
{
@@ -149,11 +154,38 @@ pub(crate) fn provide(providers: &mut Providers) {
149154
assert_eq!(cnum, LOCAL_CRATE);
150155
let target = &tcx.sess.target;
151156
if tcx.sess.opts.actually_rustdoc {
152-
// rustdoc needs to be able to document functions that use all the features, so
153-
// whitelist them all
154-
rustc_target::target_features::all_rust_features()
155-
.map(|(a, b)| (a.to_string(), b.compute_toggleability(target)))
156-
.collect()
157+
// HACK: rustdoc would like to pretend that we have all the target features, so we
158+
// have to merge all the lists into one. To ensure an unstable target never prevents
159+
// a stable one from working, we merge the stability info of all instances of the
160+
// same target feature name, with the "most stable" taking precedence. And then we
161+
// hope that this doesn't cause issues anywhere else in the compiler...
162+
let mut result: UnordMap<String, Stability<_>> = Default::default();
163+
for (name, stability) in rustc_target::target_features::all_rust_features() {
164+
use std::collections::hash_map::Entry;
165+
match result.entry(name.to_owned()) {
166+
Entry::Vacant(vacant_entry) => {
167+
vacant_entry.insert(stability.compute_toggleability(target));
168+
}
169+
Entry::Occupied(mut occupied_entry) => {
170+
// Merge the two stabilities, "more stable" taking precedence.
171+
match (occupied_entry.get(), &stability) {
172+
(Stability::Stable { .. }, _)
173+
| (
174+
Stability::Unstable { .. },
175+
Stability::Unstable { .. } | Stability::Forbidden { .. },
176+
)
177+
| (Stability::Forbidden { .. }, Stability::Forbidden { .. }) => {
178+
// The stability in the entry is at least as good as the new one, just keep it.
179+
}
180+
_ => {
181+
// Overwrite stabilite.
182+
occupied_entry.insert(stability.compute_toggleability(target));
183+
}
184+
}
185+
}
186+
}
187+
}
188+
result
157189
} else {
158190
tcx.sess
159191
.target

‎library/std/src/fs.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2402,7 +2402,7 @@ pub fn symlink_metadata<P: AsRef<Path>>(path: P) -> io::Result<Metadata> {
24022402
/// # Platform-specific behavior
24032403
///
24042404
/// This function currently corresponds to the `rename` function on Unix
2405-
/// and the `SetFileInformationByHandle` function on Windows.
2405+
/// and the `MoveFileExW` or `SetFileInformationByHandle` function on Windows.
24062406
///
24072407
/// Because of this, the behavior when both `from` and `to` exist differs. On
24082408
/// Unix, if `from` is a directory, `to` must also be an (empty) directory. If

‎library/std/src/sys/pal/windows/fs.rs

+60-125
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use super::api::{self, WinError};
22
use super::{IoResult, to_u16s};
3-
use crate::alloc::{alloc, handle_alloc_error};
3+
use crate::alloc::{Layout, alloc, dealloc};
44
use crate::borrow::Cow;
55
use crate::ffi::{OsStr, OsString, c_void};
66
use crate::io::{self, BorrowedCursor, Error, IoSlice, IoSliceMut, SeekFrom};
7-
use crate::mem::{self, MaybeUninit};
7+
use crate::mem::{self, MaybeUninit, offset_of};
88
use crate::os::windows::io::{AsHandle, BorrowedHandle};
99
use crate::os::windows::prelude::*;
1010
use crate::path::{Path, PathBuf};
@@ -296,6 +296,10 @@ impl OpenOptions {
296296
impl File {
297297
pub fn open(path: &Path, opts: &OpenOptions) -> io::Result<File> {
298298
let path = maybe_verbatim(path)?;
299+
Self::open_native(&path, opts)
300+
}
301+
302+
fn open_native(path: &[u16], opts: &OpenOptions) -> io::Result<File> {
299303
let creation = opts.get_creation_mode()?;
300304
let handle = unsafe {
301305
c::CreateFileW(
@@ -1234,141 +1238,72 @@ pub fn rename(old: &Path, new: &Path) -> io::Result<()> {
12341238
let old = maybe_verbatim(old)?;
12351239
let new = maybe_verbatim(new)?;
12361240

1237-
let new_len_without_nul_in_bytes = (new.len() - 1).try_into().unwrap();
1238-
1239-
// The last field of FILE_RENAME_INFO, the file name, is unsized,
1240-
// and FILE_RENAME_INFO has two padding bytes.
1241-
// Therefore we need to make sure to not allocate less than
1242-
// size_of::<c::FILE_RENAME_INFO>() bytes, which would be the case with
1243-
// 0 or 1 character paths + a null byte.
1244-
let struct_size = mem::size_of::<c::FILE_RENAME_INFO>()
1245-
.max(mem::offset_of!(c::FILE_RENAME_INFO, FileName) + new.len() * mem::size_of::<u16>());
1246-
1247-
let struct_size: u32 = struct_size.try_into().unwrap();
1248-
1249-
let create_file = |extra_access, extra_flags| {
1250-
let handle = unsafe {
1251-
HandleOrInvalid::from_raw_handle(c::CreateFileW(
1252-
old.as_ptr(),
1253-
c::SYNCHRONIZE | c::DELETE | extra_access,
1254-
c::FILE_SHARE_READ | c::FILE_SHARE_WRITE | c::FILE_SHARE_DELETE,
1255-
ptr::null(),
1256-
c::OPEN_EXISTING,
1257-
c::FILE_ATTRIBUTE_NORMAL | c::FILE_FLAG_BACKUP_SEMANTICS | extra_flags,
1258-
ptr::null_mut(),
1259-
))
1260-
};
1261-
1262-
OwnedHandle::try_from(handle).map_err(|_| io::Error::last_os_error())
1263-
};
1264-
1265-
// The following code replicates `MoveFileEx`'s behavior as reverse-engineered from its disassembly.
1266-
// If `old` refers to a mount point, we move it instead of the target.
1267-
let handle = match create_file(c::FILE_READ_ATTRIBUTES, c::FILE_FLAG_OPEN_REPARSE_POINT) {
1268-
Ok(handle) => {
1269-
let mut file_attribute_tag_info: MaybeUninit<c::FILE_ATTRIBUTE_TAG_INFO> =
1270-
MaybeUninit::uninit();
1271-
1272-
let result = unsafe {
1273-
cvt(c::GetFileInformationByHandleEx(
1274-
handle.as_raw_handle(),
1275-
c::FileAttributeTagInfo,
1276-
file_attribute_tag_info.as_mut_ptr().cast(),
1277-
mem::size_of::<c::FILE_ATTRIBUTE_TAG_INFO>().try_into().unwrap(),
1278-
))
1241+
if unsafe { c::MoveFileExW(old.as_ptr(), new.as_ptr(), c::MOVEFILE_REPLACE_EXISTING) } == 0 {
1242+
let err = api::get_last_error();
1243+
// if `MoveFileExW` fails with ERROR_ACCESS_DENIED then try to move
1244+
// the file while ignoring the readonly attribute.
1245+
// This is accomplished by calling `SetFileInformationByHandle` with `FileRenameInfoEx`.
1246+
if err == WinError::ACCESS_DENIED {
1247+
let mut opts = OpenOptions::new();
1248+
opts.access_mode(c::DELETE);
1249+
opts.custom_flags(c::FILE_FLAG_OPEN_REPARSE_POINT | c::FILE_FLAG_BACKUP_SEMANTICS);
1250+
let Ok(f) = File::open_native(&old, &opts) else { return Err(err).io_result() };
1251+
1252+
// Calculate the layout of the `FILE_RENAME_INFO` we pass to `SetFileInformation`
1253+
// This is a dynamically sized struct so we need to get the position of the last field to calculate the actual size.
1254+
let Ok(new_len_without_nul_in_bytes): Result<u32, _> = ((new.len() - 1) * 2).try_into()
1255+
else {
1256+
return Err(err).io_result();
12791257
};
1280-
1281-
if let Err(err) = result {
1282-
if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _)
1283-
|| err.raw_os_error() == Some(c::ERROR_INVALID_FUNCTION as _)
1284-
{
1285-
// `GetFileInformationByHandleEx` documents that not all underlying drivers support all file information classes.
1286-
// Since we know we passed the correct arguments, this means the underlying driver didn't understand our request;
1287-
// `MoveFileEx` proceeds by reopening the file without inhibiting reparse point behavior.
1288-
None
1289-
} else {
1290-
Some(Err(err))
1291-
}
1292-
} else {
1293-
// SAFETY: The struct has been initialized by GetFileInformationByHandleEx
1294-
let file_attribute_tag_info = unsafe { file_attribute_tag_info.assume_init() };
1295-
let file_type = FileType::new(
1296-
file_attribute_tag_info.FileAttributes,
1297-
file_attribute_tag_info.ReparseTag,
1298-
);
1299-
1300-
if file_type.is_symlink() {
1301-
// The file is a mount point, junction point or symlink so
1302-
// don't reopen the file so that the link gets renamed.
1303-
Some(Ok(handle))
1304-
} else {
1305-
// Otherwise reopen the file without inhibiting reparse point behavior.
1306-
None
1258+
let offset: u32 = offset_of!(c::FILE_RENAME_INFO, FileName).try_into().unwrap();
1259+
let struct_size = offset + new_len_without_nul_in_bytes + 2;
1260+
let layout =
1261+
Layout::from_size_align(struct_size as usize, align_of::<c::FILE_RENAME_INFO>())
1262+
.unwrap();
1263+
1264+
// SAFETY: We allocate enough memory for a full FILE_RENAME_INFO struct and a filename.
1265+
let file_rename_info;
1266+
unsafe {
1267+
file_rename_info = alloc(layout).cast::<c::FILE_RENAME_INFO>();
1268+
if file_rename_info.is_null() {
1269+
return Err(io::ErrorKind::OutOfMemory.into());
13071270
}
1308-
}
1309-
}
1310-
// The underlying driver may not support `FILE_FLAG_OPEN_REPARSE_POINT`: Retry without it.
1311-
Err(err) if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _) => None,
1312-
Err(err) => Some(Err(err)),
1313-
}
1314-
.unwrap_or_else(|| create_file(0, 0))?;
13151271

1316-
let layout = core::alloc::Layout::from_size_align(
1317-
struct_size as _,
1318-
mem::align_of::<c::FILE_RENAME_INFO>(),
1319-
)
1320-
.unwrap();
1321-
1322-
let file_rename_info = unsafe { alloc(layout) } as *mut c::FILE_RENAME_INFO;
1323-
1324-
if file_rename_info.is_null() {
1325-
handle_alloc_error(layout);
1326-
}
1327-
1328-
// SAFETY: file_rename_info is a non-null pointer pointing to memory allocated by the global allocator.
1329-
let mut file_rename_info = unsafe { Box::from_raw(file_rename_info) };
1330-
1331-
// SAFETY: We have allocated enough memory for a full FILE_RENAME_INFO struct and a filename.
1332-
unsafe {
1333-
(&raw mut (*file_rename_info).Anonymous).write(c::FILE_RENAME_INFO_0 {
1334-
Flags: c::FILE_RENAME_FLAG_REPLACE_IF_EXISTS | c::FILE_RENAME_FLAG_POSIX_SEMANTICS,
1335-
});
1336-
1337-
(&raw mut (*file_rename_info).RootDirectory).write(ptr::null_mut());
1338-
(&raw mut (*file_rename_info).FileNameLength).write(new_len_without_nul_in_bytes);
1339-
1340-
new.as_ptr()
1341-
.copy_to_nonoverlapping((&raw mut (*file_rename_info).FileName) as *mut u16, new.len());
1342-
}
1272+
(&raw mut (*file_rename_info).Anonymous).write(c::FILE_RENAME_INFO_0 {
1273+
Flags: c::FILE_RENAME_FLAG_REPLACE_IF_EXISTS
1274+
| c::FILE_RENAME_FLAG_POSIX_SEMANTICS,
1275+
});
13431276

1344-
// We don't use `set_file_information_by_handle` here as `FILE_RENAME_INFO` is used for both `FileRenameInfo` and `FileRenameInfoEx`.
1345-
let result = unsafe {
1346-
cvt(c::SetFileInformationByHandle(
1347-
handle.as_raw_handle(),
1348-
c::FileRenameInfoEx,
1349-
(&raw const *file_rename_info).cast::<c_void>(),
1350-
struct_size,
1351-
))
1352-
};
1277+
(&raw mut (*file_rename_info).RootDirectory).write(ptr::null_mut());
1278+
// Don't include the NULL in the size
1279+
(&raw mut (*file_rename_info).FileNameLength).write(new_len_without_nul_in_bytes);
13531280

1354-
if let Err(err) = result {
1355-
if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _) {
1356-
// FileRenameInfoEx and FILE_RENAME_FLAG_POSIX_SEMANTICS were added in Windows 10 1607; retry with FileRenameInfo.
1357-
file_rename_info.Anonymous.ReplaceIfExists = 1;
1281+
new.as_ptr().copy_to_nonoverlapping(
1282+
(&raw mut (*file_rename_info).FileName).cast::<u16>(),
1283+
new.len(),
1284+
);
1285+
}
13581286

1359-
cvt(unsafe {
1287+
let result = unsafe {
13601288
c::SetFileInformationByHandle(
1361-
handle.as_raw_handle(),
1362-
c::FileRenameInfo,
1363-
(&raw const *file_rename_info).cast::<c_void>(),
1289+
f.as_raw_handle(),
1290+
c::FileRenameInfoEx,
1291+
file_rename_info.cast::<c_void>(),
13641292
struct_size,
13651293
)
1366-
})?;
1294+
};
1295+
unsafe { dealloc(file_rename_info.cast::<u8>(), layout) };
1296+
if result == 0 {
1297+
if api::get_last_error() == WinError::DIR_NOT_EMPTY {
1298+
return Err(WinError::DIR_NOT_EMPTY).io_result();
1299+
} else {
1300+
return Err(err).io_result();
1301+
}
1302+
}
13671303
} else {
1368-
return Err(err);
1304+
return Err(err).io_result();
13691305
}
13701306
}
1371-
13721307
Ok(())
13731308
}
13741309

‎src/bootstrap/Cargo.lock

+2-2
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ dependencies = [
8484

8585
[[package]]
8686
name = "cc"
87-
version = "1.2.0"
87+
version = "1.1.22"
8888
source = "registry+https://github.com/rust-lang/crates.io-index"
89-
checksum = "1aeb932158bd710538c73702db6945cb68a8fb08c519e6e12706b94263b36db8"
89+
checksum = "9540e661f81799159abee814118cc139a2004b3a3aa3ea37724a1b66530b90e0"
9090
dependencies = [
9191
"shlex",
9292
]

‎src/bootstrap/Cargo.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ test = false
3636
# Most of the time updating these dependencies requires modifications to the
3737
# bootstrap codebase(e.g., https://github.com/rust-lang/rust/issues/124565);
3838
# otherwise, some targets will fail. That's why these dependencies are explicitly pinned.
39-
cc = "=1.2.0"
39+
#
40+
# Do not upgrade this crate unless https://github.com/rust-lang/cc-rs/issues/1317 is fixed.
41+
cc = "=1.1.22"
4042
cmake = "=0.1.48"
4143

4244
build_helper = { path = "../build_helper" }

‎src/bootstrap/src/core/config/config.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2668,7 +2668,7 @@ impl Config {
26682668
/// used instead to provide a nice error to the user if the submodule is
26692669
/// missing.
26702670
pub(crate) fn update_submodule(&self, relative_path: &str) {
2671-
if !self.submodules() {
2671+
if self.rust_info.is_from_tarball() || !self.submodules() {
26722672
return;
26732673
}
26742674

‎src/bootstrap/src/lib.rs

+4
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,10 @@ impl Build {
471471
/// The given `err_hint` will be shown to the user if the submodule is not
472472
/// checked out and submodule management is disabled.
473473
pub fn require_submodule(&self, submodule: &str, err_hint: Option<&str>) {
474+
if self.rust_info().is_from_tarball() {
475+
return;
476+
}
477+
474478
// When testing bootstrap itself, it is much faster to ignore
475479
// submodules. Almost all Steps work fine without their submodules.
476480
if cfg!(test) && !self.config.submodules() {

‎src/librustdoc/doctest.rs

+125-45
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ pub(crate) fn generate_args_file(file_path: &Path, options: &RustdocOptions) ->
9595
.map_err(|error| format!("failed to create args file: {error:?}"))?;
9696

9797
// We now put the common arguments into the file we created.
98-
let mut content = vec!["--crate-type=bin".to_string()];
98+
let mut content = vec![];
9999

100100
for cfg in &options.cfgs {
101101
content.push(format!("--cfg={cfg}"));
@@ -488,12 +488,18 @@ pub(crate) struct RunnableDocTest {
488488
line: usize,
489489
edition: Edition,
490490
no_run: bool,
491-
is_multiple_tests: bool,
491+
merged_test_code: Option<String>,
492492
}
493493

494494
impl RunnableDocTest {
495-
fn path_for_merged_doctest(&self) -> PathBuf {
496-
self.test_opts.outdir.path().join(format!("doctest_{}.rs", self.edition))
495+
fn path_for_merged_doctest_bundle(&self) -> PathBuf {
496+
self.test_opts.outdir.path().join(format!("doctest_bundle_{}.rs", self.edition))
497+
}
498+
fn path_for_merged_doctest_runner(&self) -> PathBuf {
499+
self.test_opts.outdir.path().join(format!("doctest_runner_{}.rs", self.edition))
500+
}
501+
fn is_multiple_tests(&self) -> bool {
502+
self.merged_test_code.is_some()
497503
}
498504
}
499505

@@ -512,91 +518,108 @@ fn run_test(
512518
let rust_out = add_exe_suffix("rust_out".to_owned(), &rustdoc_options.target);
513519
let output_file = doctest.test_opts.outdir.path().join(rust_out);
514520

515-
let rustc_binary = rustdoc_options
516-
.test_builder
517-
.as_deref()
518-
.unwrap_or_else(|| rustc_interface::util::rustc_path().expect("found rustc"));
519-
let mut compiler = wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
521+
// Common arguments used for compiling the doctest runner.
522+
// On merged doctests, the compiler is invoked twice: once for the test code itself,
523+
// and once for the runner wrapper (which needs to use `#![feature]` on stable).
524+
let mut compiler_args = vec![];
520525

521-
compiler.arg(format!("@{}", doctest.global_opts.args_file.display()));
526+
compiler_args.push(format!("@{}", doctest.global_opts.args_file.display()));
522527

523528
if let Some(sysroot) = &rustdoc_options.maybe_sysroot {
524-
compiler.arg(format!("--sysroot={}", sysroot.display()));
529+
compiler_args.push(format!("--sysroot={}", sysroot.display()));
525530
}
526531

527-
compiler.arg("--edition").arg(doctest.edition.to_string());
528-
if !doctest.is_multiple_tests {
529-
// Setting these environment variables is unneeded if this is a merged doctest.
530-
compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", &doctest.test_opts.path);
531-
compiler.env(
532-
"UNSTABLE_RUSTDOC_TEST_LINE",
533-
format!("{}", doctest.line as isize - doctest.full_test_line_offset as isize),
534-
);
535-
}
536-
compiler.arg("-o").arg(&output_file);
532+
compiler_args.extend_from_slice(&["--edition".to_owned(), doctest.edition.to_string()]);
537533
if langstr.test_harness {
538-
compiler.arg("--test");
534+
compiler_args.push("--test".to_owned());
539535
}
540536
if rustdoc_options.json_unused_externs.is_enabled() && !langstr.compile_fail {
541-
compiler.arg("--error-format=json");
542-
compiler.arg("--json").arg("unused-externs");
543-
compiler.arg("-W").arg("unused_crate_dependencies");
544-
compiler.arg("-Z").arg("unstable-options");
537+
compiler_args.push("--error-format=json".to_owned());
538+
compiler_args.extend_from_slice(&["--json".to_owned(), "unused-externs".to_owned()]);
539+
compiler_args.extend_from_slice(&["-W".to_owned(), "unused_crate_dependencies".to_owned()]);
540+
compiler_args.extend_from_slice(&["-Z".to_owned(), "unstable-options".to_owned()]);
545541
}
546542

547543
if doctest.no_run && !langstr.compile_fail && rustdoc_options.persist_doctests.is_none() {
548544
// FIXME: why does this code check if it *shouldn't* persist doctests
549545
// -- shouldn't it be the negation?
550-
compiler.arg("--emit=metadata");
546+
compiler_args.push("--emit=metadata".to_owned());
551547
}
552-
compiler.arg("--target").arg(match &rustdoc_options.target {
553-
TargetTuple::TargetTuple(s) => s,
554-
TargetTuple::TargetJson { path_for_rustdoc, .. } => {
555-
path_for_rustdoc.to_str().expect("target path must be valid unicode")
556-
}
557-
});
548+
compiler_args.extend_from_slice(&[
549+
"--target".to_owned(),
550+
match &rustdoc_options.target {
551+
TargetTuple::TargetTuple(s) => s.clone(),
552+
TargetTuple::TargetJson { path_for_rustdoc, .. } => {
553+
path_for_rustdoc.to_str().expect("target path must be valid unicode").to_owned()
554+
}
555+
},
556+
]);
558557
if let ErrorOutputType::HumanReadable(kind, color_config) = rustdoc_options.error_format {
559558
let short = kind.short();
560559
let unicode = kind == HumanReadableErrorType::Unicode;
561560

562561
if short {
563-
compiler.arg("--error-format").arg("short");
562+
compiler_args.extend_from_slice(&["--error-format".to_owned(), "short".to_owned()]);
564563
}
565564
if unicode {
566-
compiler.arg("--error-format").arg("human-unicode");
565+
compiler_args
566+
.extend_from_slice(&["--error-format".to_owned(), "human-unicode".to_owned()]);
567567
}
568568

569569
match color_config {
570570
ColorConfig::Never => {
571-
compiler.arg("--color").arg("never");
571+
compiler_args.extend_from_slice(&["--color".to_owned(), "never".to_owned()]);
572572
}
573573
ColorConfig::Always => {
574-
compiler.arg("--color").arg("always");
574+
compiler_args.extend_from_slice(&["--color".to_owned(), "always".to_owned()]);
575575
}
576576
ColorConfig::Auto => {
577-
compiler.arg("--color").arg(if supports_color { "always" } else { "never" });
577+
compiler_args.extend_from_slice(&[
578+
"--color".to_owned(),
579+
if supports_color { "always" } else { "never" }.to_owned(),
580+
]);
578581
}
579582
}
580583
}
581584

585+
let rustc_binary = rustdoc_options
586+
.test_builder
587+
.as_deref()
588+
.unwrap_or_else(|| rustc_interface::util::rustc_path().expect("found rustc"));
589+
let mut compiler = wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
590+
591+
compiler.args(&compiler_args);
592+
582593
// If this is a merged doctest, we need to write it into a file instead of using stdin
583594
// because if the size of the merged doctests is too big, it'll simply break stdin.
584-
if doctest.is_multiple_tests {
595+
if doctest.is_multiple_tests() {
585596
// It makes the compilation failure much faster if it is for a combined doctest.
586597
compiler.arg("--error-format=short");
587-
let input_file = doctest.path_for_merged_doctest();
598+
let input_file = doctest.path_for_merged_doctest_bundle();
588599
if std::fs::write(&input_file, &doctest.full_test_code).is_err() {
589600
// If we cannot write this file for any reason, we leave. All combined tests will be
590601
// tested as standalone tests.
591602
return Err(TestFailure::CompileError);
592603
}
593-
compiler.arg(input_file);
594604
if !rustdoc_options.nocapture {
595605
// If `nocapture` is disabled, then we don't display rustc's output when compiling
596606
// the merged doctests.
597607
compiler.stderr(Stdio::null());
598608
}
609+
// bundled tests are an rlib, loaded by a separate runner executable
610+
compiler
611+
.arg("--crate-type=lib")
612+
.arg("--out-dir")
613+
.arg(doctest.test_opts.outdir.path())
614+
.arg(input_file);
599615
} else {
616+
compiler.arg("--crate-type=bin").arg("-o").arg(&output_file);
617+
// Setting these environment variables is unneeded if this is a merged doctest.
618+
compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", &doctest.test_opts.path);
619+
compiler.env(
620+
"UNSTABLE_RUSTDOC_TEST_LINE",
621+
format!("{}", doctest.line as isize - doctest.full_test_line_offset as isize),
622+
);
600623
compiler.arg("-");
601624
compiler.stdin(Stdio::piped());
602625
compiler.stderr(Stdio::piped());
@@ -605,8 +628,65 @@ fn run_test(
605628
debug!("compiler invocation for doctest: {compiler:?}");
606629

607630
let mut child = compiler.spawn().expect("Failed to spawn rustc process");
608-
let output = if doctest.is_multiple_tests {
631+
let output = if let Some(merged_test_code) = &doctest.merged_test_code {
632+
// compile-fail tests never get merged, so this should always pass
609633
let status = child.wait().expect("Failed to wait");
634+
635+
// the actual test runner is a separate component, built with nightly-only features;
636+
// build it now
637+
let runner_input_file = doctest.path_for_merged_doctest_runner();
638+
639+
let mut runner_compiler =
640+
wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
641+
// the test runner does not contain any user-written code, so this doesn't allow
642+
// the user to exploit nightly-only features on stable
643+
runner_compiler.env("RUSTC_BOOTSTRAP", "1");
644+
runner_compiler.args(compiler_args);
645+
runner_compiler.args(&["--crate-type=bin", "-o"]).arg(&output_file);
646+
let mut extern_path = std::ffi::OsString::from(format!(
647+
"--extern=doctest_bundle_{edition}=",
648+
edition = doctest.edition
649+
));
650+
for extern_str in &rustdoc_options.extern_strs {
651+
if let Some((_cratename, path)) = extern_str.split_once('=') {
652+
// Direct dependencies of the tests themselves are
653+
// indirect dependencies of the test runner.
654+
// They need to be in the library search path.
655+
let dir = Path::new(path)
656+
.parent()
657+
.filter(|x| x.components().count() > 0)
658+
.unwrap_or(Path::new("."));
659+
runner_compiler.arg("-L").arg(dir);
660+
}
661+
}
662+
let output_bundle_file = doctest
663+
.test_opts
664+
.outdir
665+
.path()
666+
.join(format!("libdoctest_bundle_{edition}.rlib", edition = doctest.edition));
667+
extern_path.push(&output_bundle_file);
668+
runner_compiler.arg(extern_path);
669+
runner_compiler.arg(&runner_input_file);
670+
if std::fs::write(&runner_input_file, &merged_test_code).is_err() {
671+
// If we cannot write this file for any reason, we leave. All combined tests will be
672+
// tested as standalone tests.
673+
return Err(TestFailure::CompileError);
674+
}
675+
if !rustdoc_options.nocapture {
676+
// If `nocapture` is disabled, then we don't display rustc's output when compiling
677+
// the merged doctests.
678+
runner_compiler.stderr(Stdio::null());
679+
}
680+
runner_compiler.arg("--error-format=short");
681+
debug!("compiler invocation for doctest runner: {runner_compiler:?}");
682+
683+
let status = if !status.success() {
684+
status
685+
} else {
686+
let mut child_runner = runner_compiler.spawn().expect("Failed to spawn rustc process");
687+
child_runner.wait().expect("Failed to wait")
688+
};
689+
610690
process::Output { status, stdout: Vec::new(), stderr: Vec::new() }
611691
} else {
612692
let stdin = child.stdin.as_mut().expect("Failed to open stdin");
@@ -683,15 +763,15 @@ fn run_test(
683763
cmd.arg(&output_file);
684764
} else {
685765
cmd = Command::new(&output_file);
686-
if doctest.is_multiple_tests {
766+
if doctest.is_multiple_tests() {
687767
cmd.env("RUSTDOC_DOCTEST_BIN_PATH", &output_file);
688768
}
689769
}
690770
if let Some(run_directory) = &rustdoc_options.test_run_directory {
691771
cmd.current_dir(run_directory);
692772
}
693773

694-
let result = if doctest.is_multiple_tests || rustdoc_options.nocapture {
774+
let result = if doctest.is_multiple_tests() || rustdoc_options.nocapture {
695775
cmd.status().map(|status| process::Output {
696776
status,
697777
stdout: Vec::new(),
@@ -977,7 +1057,7 @@ fn doctest_run_fn(
9771057
line: scraped_test.line,
9781058
edition: scraped_test.edition(&rustdoc_options),
9791059
no_run: scraped_test.no_run(&rustdoc_options),
980-
is_multiple_tests: false,
1060+
merged_test_code: None,
9811061
};
9821062
let res =
9831063
run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs);

‎src/librustdoc/doctest/runner.rs

+25-12
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub(crate) struct DocTestRunner {
1414
crate_attrs: FxIndexSet<String>,
1515
ids: String,
1616
output: String,
17+
output_merged_tests: String,
1718
supports_color: bool,
1819
nb_tests: usize,
1920
}
@@ -24,6 +25,7 @@ impl DocTestRunner {
2425
crate_attrs: FxIndexSet::default(),
2526
ids: String::new(),
2627
output: String::new(),
28+
output_merged_tests: String::new(),
2729
supports_color: true,
2830
nb_tests: 0,
2931
}
@@ -55,7 +57,8 @@ impl DocTestRunner {
5557
scraped_test,
5658
ignore,
5759
self.nb_tests,
58-
&mut self.output
60+
&mut self.output,
61+
&mut self.output_merged_tests,
5962
),
6063
));
6164
self.supports_color &= doctest.supports_color;
@@ -78,25 +81,28 @@ impl DocTestRunner {
7881
"
7982
.to_string();
8083

84+
let mut code_prefix = String::new();
85+
8186
for crate_attr in &self.crate_attrs {
82-
code.push_str(crate_attr);
83-
code.push('\n');
87+
code_prefix.push_str(crate_attr);
88+
code_prefix.push('\n');
8489
}
8590

8691
if opts.attrs.is_empty() {
8792
// If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some
8893
// lints that are commonly triggered in doctests. The crate-level test attributes are
8994
// commonly used to make tests fail in case they trigger warnings, so having this there in
9095
// that case may cause some tests to pass when they shouldn't have.
91-
code.push_str("#![allow(unused)]\n");
96+
code_prefix.push_str("#![allow(unused)]\n");
9297
}
9398

9499
// Next, any attributes that came from the crate root via #![doc(test(attr(...)))].
95100
for attr in &opts.attrs {
96-
code.push_str(&format!("#![{attr}]\n"));
101+
code_prefix.push_str(&format!("#![{attr}]\n"));
97102
}
98103

99104
code.push_str("extern crate test;\n");
105+
writeln!(code, "extern crate doctest_bundle_{edition} as doctest_bundle;").unwrap();
100106

101107
let test_args = test_args.iter().fold(String::new(), |mut x, arg| {
102108
write!(x, "{arg:?}.to_string(),").unwrap();
@@ -161,20 +167,20 @@ the same process\");
161167
std::process::Termination::report(test::test_main(test_args, Vec::from(TESTS), None))
162168
}}",
163169
nb_tests = self.nb_tests,
164-
output = self.output,
170+
output = self.output_merged_tests,
165171
ids = self.ids,
166172
)
167173
.expect("failed to generate test code");
168174
let runnable_test = RunnableDocTest {
169-
full_test_code: code,
175+
full_test_code: format!("{code_prefix}{code}", code = self.output),
170176
full_test_line_offset: 0,
171177
test_opts: test_options,
172178
global_opts: opts.clone(),
173179
langstr: LangString::default(),
174180
line: 0,
175181
edition,
176182
no_run: false,
177-
is_multiple_tests: true,
183+
merged_test_code: Some(code),
178184
};
179185
let ret =
180186
run_test(runnable_test, rustdoc_options, self.supports_color, |_: UnusedExterns| {});
@@ -189,14 +195,15 @@ fn generate_mergeable_doctest(
189195
ignore: bool,
190196
id: usize,
191197
output: &mut String,
198+
output_merged_tests: &mut String,
192199
) -> String {
193200
let test_id = format!("__doctest_{id}");
194201

195202
if ignore {
196203
// We generate nothing else.
197-
writeln!(output, "mod {test_id} {{\n").unwrap();
204+
writeln!(output, "pub mod {test_id} {{}}\n").unwrap();
198205
} else {
199-
writeln!(output, "mod {test_id} {{\n{}{}", doctest.crates, doctest.maybe_crate_attrs)
206+
writeln!(output, "pub mod {test_id} {{\n{}{}", doctest.crates, doctest.maybe_crate_attrs)
200207
.unwrap();
201208
if doctest.has_main_fn {
202209
output.push_str(&doctest.everything_else);
@@ -216,11 +223,17 @@ fn main() {returns_result} {{
216223
)
217224
.unwrap();
218225
}
226+
writeln!(
227+
output,
228+
"\npub fn __main_fn() -> impl std::process::Termination {{ main() }} \n}}\n"
229+
)
230+
.unwrap();
219231
}
220232
let not_running = ignore || scraped_test.langstr.no_run;
221233
writeln!(
222-
output,
234+
output_merged_tests,
223235
"
236+
mod {test_id} {{
224237
pub const TEST: test::TestDescAndFn = test::TestDescAndFn::new_doctest(
225238
{test_name:?}, {ignore}, {file:?}, {line}, {no_run}, {should_panic},
226239
test::StaticTestFn(
@@ -242,7 +255,7 @@ test::StaticTestFn(
242255
if let Some(bin_path) = crate::__doctest_mod::doctest_path() {{
243256
test::assert_test_result(crate::__doctest_mod::doctest_runner(bin_path, {id}))
244257
}} else {{
245-
test::assert_test_result(self::main())
258+
test::assert_test_result(doctest_bundle::{test_id}::__main_fn())
246259
}}
247260
",
248261
)

‎src/version

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.85.0
1+
1.85.1

‎tests/run-make/doctests-merge/rmake.rs

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ fn test_and_compare(input_file: &str, stdout_file: &str, edition: &str, dep: &Pa
88
let output = cmd
99
.input(input_file)
1010
.arg("--test")
11-
.arg("-Zunstable-options")
1211
.edition(edition)
1312
.arg("--test-args=--test-threads=1")
1413
.extern_("foo", dep.display().to_string())

‎tests/rustdoc-ui/doctest/doctest-output.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//@[edition2015]edition:2015
33
//@[edition2015]aux-build:extern_macros.rs
44
//@[edition2015]compile-flags:--test --test-args=--test-threads=1
5-
//@[edition2024]edition:2015
5+
//@[edition2024]edition:2024
66
//@[edition2024]aux-build:extern_macros.rs
77
//@[edition2024]compile-flags:--test --test-args=--test-threads=1
88
//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
2+
running 1 test
3+
test $DIR/failed-doctest-test-crate.rs - m (line 14) ... FAILED
4+
5+
failures:
6+
7+
---- $DIR/failed-doctest-test-crate.rs - m (line 14) stdout ----
8+
error[E0432]: unresolved import `test`
9+
--> $DIR/failed-doctest-test-crate.rs:15:5
10+
|
11+
LL | use test::*;
12+
| ^^^^ you might be missing crate `test`
13+
|
14+
help: consider importing the `test` crate
15+
|
16+
LL + extern crate test;
17+
|
18+
19+
error: aborting due to 1 previous error
20+
21+
For more information about this error, try `rustc --explain E0432`.
22+
Couldn't compile the test.
23+
24+
failures:
25+
$DIR/failed-doctest-test-crate.rs - m (line 14)
26+
27+
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
28+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
2+
running 1 test
3+
test $DIR/failed-doctest-test-crate.rs - m (line 14) ... FAILED
4+
5+
failures:
6+
7+
---- $DIR/failed-doctest-test-crate.rs - m (line 14) stdout ----
8+
error[E0432]: unresolved import `test`
9+
--> $DIR/failed-doctest-test-crate.rs:15:5
10+
|
11+
LL | use test::*;
12+
| ^^^^ use of undeclared crate or module `test`
13+
14+
error: aborting due to 1 previous error
15+
16+
For more information about this error, try `rustc --explain E0432`.
17+
Couldn't compile the test.
18+
19+
failures:
20+
$DIR/failed-doctest-test-crate.rs - m (line 14)
21+
22+
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
23+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// FIXME: if/when the output of the test harness can be tested on its own, this test should be
2+
// adapted to use that, and that normalize line can go away
3+
4+
//@ revisions: edition2015 edition2024
5+
//@[edition2015]edition:2015
6+
//@[edition2024]edition:2024
7+
//@ compile-flags:--test
8+
//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"
9+
//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME"
10+
//@ failure-status: 101
11+
12+
/// <https://github.com/rust-lang/rust/pull/137899#discussion_r1976743383>
13+
///
14+
/// ```rust
15+
/// use test::*;
16+
/// ```
17+
pub mod m {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//! This is a regression test for <https://github.com/rust-lang/rust/issues/137366>, ensuring
2+
//! that we can use the `neon` target feature on ARM32 targets in rustdoc despite there
3+
//! being a "forbidden" feature of the same name for aarch64, and rustdoc merging the
4+
//! target features of all targets.
5+
//@ check-pass
6+
//@ revisions: arm aarch64
7+
//@[arm] compile-flags: --target armv7-unknown-linux-gnueabihf
8+
//@[arm] needs-llvm-components: arm
9+
//@[aarch64] compile-flags: --target aarch64-unknown-none-softfloat
10+
//@[aarch64] needs-llvm-components: aarch64
11+
12+
#![crate_type = "lib"]
13+
#![feature(no_core, lang_items)]
14+
#![feature(arm_target_feature)]
15+
#![no_core]
16+
17+
#[lang = "sized"]
18+
pub trait Sized {}
19+
20+
// `fp-armv8` is "forbidden" on aarch64 as we tie it to `neon`.
21+
#[target_feature(enable = "fp-armv8")]
22+
pub fn fun1() {}
23+
24+
// This would usually be rejected as it changes the ABI.
25+
// But we disable that check in rustdoc since we are building "for all targets" and the
26+
// check can't really handle that.
27+
#[target_feature(enable = "soft-float")]
28+
pub fn fun2() {}

0 commit comments

Comments
 (0)
Please sign in to comment.