diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5c78ac2816cdb..88d2642fd8c3f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -189,6 +189,20 @@ jobs:
- name: ensure the stable version number is correct
run: src/ci/scripts/verify-stable-version-number.sh
+ # Temporary fix to unblock CI
+ # Remove the latest Windows SDK for 32-bit Windows MSVC builds.
+ # See issue https://github.com/rust-lang/rust/issues/137733 for more details.
+ - name: Remove Windows SDK 10.0.26100.0
+ shell: powershell
+ if: ${{ matrix.image == 'i686-msvc' || matrix.image == 'dist-i686-msvc' }}
+ run: |
+ $kits = (Get-ItemProperty -path 'HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots').KitsRoot10
+ $sdk_version = "10.0.26100.0"
+
+ foreach ($kind in 'Bin', 'Lib', 'Include') {
+ Remove-Item -Force -Recurse $kits\$kind\$sdk_version -ErrorAction Continue
+ }
+
- name: run the build
# Redirect stderr to stdout to avoid reordering the two streams in the GHA logs.
run: src/ci/scripts/run-build-from-ci.sh 2>&1
diff --git a/RELEASES.md b/RELEASES.md
index 038d7ca639f20..ecf76c63cd995 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -1,3 +1,14 @@
+Version 1.85.1 (2025-03-18)
+==========================
+
+
+
+- [Fix the doctest-merging feature of the 2024 Edition.](https://github.com/rust-lang/rust/pull/137899/)
+- [Relax some `target_feature` checks when generating docs.](https://github.com/rust-lang/rust/pull/137632/)
+- [Fix errors in `std::fs::rename` on Windows 1607.](https://github.com/rust-lang/rust/pull/137528/)
+- [Downgrade bootstrap `cc` to fix custom targets.](https://github.com/rust-lang/rust/pull/137460/)
+- [Skip submodule updates when building Rust from a source tarball.](https://github.com/rust-lang/rust/pull/137338/)
+
Version 1.85.0 (2025-02-20)
==========================
diff --git a/compiler/rustc_codegen_ssa/src/target_features.rs b/compiler/rustc_codegen_ssa/src/target_features.rs
index 7e80d014ea282..0c53a731221db 100644
--- a/compiler/rustc_codegen_ssa/src/target_features.rs
+++ b/compiler/rustc_codegen_ssa/src/target_features.rs
@@ -10,7 +10,7 @@ use rustc_middle::query::Providers;
use rustc_middle::ty::TyCtxt;
use rustc_session::parse::feature_err;
use rustc_span::{Span, Symbol, sym};
-use rustc_target::target_features;
+use rustc_target::target_features::{self, Stability};
use crate::errors;
@@ -65,11 +65,16 @@ pub(crate) fn from_target_feature_attr(
// Only allow target features whose feature gates have been enabled
// and which are permitted to be toggled.
if let Err(reason) = stability.toggle_allowed(/*enable*/ true) {
- tcx.dcx().emit_err(errors::ForbiddenTargetFeatureAttr {
- span: item.span(),
- feature,
- reason,
- });
+ // We skip this error in rustdoc, where we want to allow all target features of
+ // all targets, so we can't check their ABI compatibility and anyway we are not
+ // generating code so "it's fine".
+ if !tcx.sess.opts.actually_rustdoc {
+ tcx.dcx().emit_err(errors::ForbiddenTargetFeatureAttr {
+ span: item.span(),
+ feature,
+ reason,
+ });
+ }
} else if let Some(nightly_feature) = stability.requires_nightly()
&& !rust_features.enabled(nightly_feature)
{
@@ -149,11 +154,38 @@ pub(crate) fn provide(providers: &mut Providers) {
assert_eq!(cnum, LOCAL_CRATE);
let target = &tcx.sess.target;
if tcx.sess.opts.actually_rustdoc {
- // rustdoc needs to be able to document functions that use all the features, so
- // whitelist them all
- rustc_target::target_features::all_rust_features()
- .map(|(a, b)| (a.to_string(), b.compute_toggleability(target)))
- .collect()
+ // HACK: rustdoc would like to pretend that we have all the target features, so we
+ // have to merge all the lists into one. To ensure an unstable target never prevents
+ // a stable one from working, we merge the stability info of all instances of the
+ // same target feature name, with the "most stable" taking precedence. And then we
+ // hope that this doesn't cause issues anywhere else in the compiler...
+ let mut result: UnordMap> = Default::default();
+ for (name, stability) in rustc_target::target_features::all_rust_features() {
+ use std::collections::hash_map::Entry;
+ match result.entry(name.to_owned()) {
+ Entry::Vacant(vacant_entry) => {
+ vacant_entry.insert(stability.compute_toggleability(target));
+ }
+ Entry::Occupied(mut occupied_entry) => {
+ // Merge the two stabilities, "more stable" taking precedence.
+ match (occupied_entry.get(), &stability) {
+ (Stability::Stable { .. }, _)
+ | (
+ Stability::Unstable { .. },
+ Stability::Unstable { .. } | Stability::Forbidden { .. },
+ )
+ | (Stability::Forbidden { .. }, Stability::Forbidden { .. }) => {
+ // The stability in the entry is at least as good as the new one, just keep it.
+ }
+ _ => {
+ // Overwrite stabilite.
+ occupied_entry.insert(stability.compute_toggleability(target));
+ }
+ }
+ }
+ }
+ }
+ result
} else {
tcx.sess
.target
diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs
index 9b752ed14437c..f390fabad29dc 100644
--- a/library/std/src/fs.rs
+++ b/library/std/src/fs.rs
@@ -2402,7 +2402,7 @@ pub fn symlink_metadata>(path: P) -> io::Result {
/// # Platform-specific behavior
///
/// This function currently corresponds to the `rename` function on Unix
-/// and the `SetFileInformationByHandle` function on Windows.
+/// and the `MoveFileExW` or `SetFileInformationByHandle` function on Windows.
///
/// Because of this, the behavior when both `from` and `to` exist differs. On
/// Unix, if `from` is a directory, `to` must also be an (empty) directory. If
diff --git a/library/std/src/sys/pal/windows/fs.rs b/library/std/src/sys/pal/windows/fs.rs
index f8493c21ad444..7fedf15dad5c4 100644
--- a/library/std/src/sys/pal/windows/fs.rs
+++ b/library/std/src/sys/pal/windows/fs.rs
@@ -1,10 +1,10 @@
use super::api::{self, WinError};
use super::{IoResult, to_u16s};
-use crate::alloc::{alloc, handle_alloc_error};
+use crate::alloc::{Layout, alloc, dealloc};
use crate::borrow::Cow;
use crate::ffi::{OsStr, OsString, c_void};
use crate::io::{self, BorrowedCursor, Error, IoSlice, IoSliceMut, SeekFrom};
-use crate::mem::{self, MaybeUninit};
+use crate::mem::{self, MaybeUninit, offset_of};
use crate::os::windows::io::{AsHandle, BorrowedHandle};
use crate::os::windows::prelude::*;
use crate::path::{Path, PathBuf};
@@ -296,6 +296,10 @@ impl OpenOptions {
impl File {
pub fn open(path: &Path, opts: &OpenOptions) -> io::Result {
let path = maybe_verbatim(path)?;
+ Self::open_native(&path, opts)
+ }
+
+ fn open_native(path: &[u16], opts: &OpenOptions) -> io::Result {
let creation = opts.get_creation_mode()?;
let handle = unsafe {
c::CreateFileW(
@@ -1234,141 +1238,72 @@ pub fn rename(old: &Path, new: &Path) -> io::Result<()> {
let old = maybe_verbatim(old)?;
let new = maybe_verbatim(new)?;
- let new_len_without_nul_in_bytes = (new.len() - 1).try_into().unwrap();
-
- // The last field of FILE_RENAME_INFO, the file name, is unsized,
- // and FILE_RENAME_INFO has two padding bytes.
- // Therefore we need to make sure to not allocate less than
- // size_of::() bytes, which would be the case with
- // 0 or 1 character paths + a null byte.
- let struct_size = mem::size_of::()
- .max(mem::offset_of!(c::FILE_RENAME_INFO, FileName) + new.len() * mem::size_of::());
-
- let struct_size: u32 = struct_size.try_into().unwrap();
-
- let create_file = |extra_access, extra_flags| {
- let handle = unsafe {
- HandleOrInvalid::from_raw_handle(c::CreateFileW(
- old.as_ptr(),
- c::SYNCHRONIZE | c::DELETE | extra_access,
- c::FILE_SHARE_READ | c::FILE_SHARE_WRITE | c::FILE_SHARE_DELETE,
- ptr::null(),
- c::OPEN_EXISTING,
- c::FILE_ATTRIBUTE_NORMAL | c::FILE_FLAG_BACKUP_SEMANTICS | extra_flags,
- ptr::null_mut(),
- ))
- };
-
- OwnedHandle::try_from(handle).map_err(|_| io::Error::last_os_error())
- };
-
- // The following code replicates `MoveFileEx`'s behavior as reverse-engineered from its disassembly.
- // If `old` refers to a mount point, we move it instead of the target.
- let handle = match create_file(c::FILE_READ_ATTRIBUTES, c::FILE_FLAG_OPEN_REPARSE_POINT) {
- Ok(handle) => {
- let mut file_attribute_tag_info: MaybeUninit =
- MaybeUninit::uninit();
-
- let result = unsafe {
- cvt(c::GetFileInformationByHandleEx(
- handle.as_raw_handle(),
- c::FileAttributeTagInfo,
- file_attribute_tag_info.as_mut_ptr().cast(),
- mem::size_of::().try_into().unwrap(),
- ))
+ if unsafe { c::MoveFileExW(old.as_ptr(), new.as_ptr(), c::MOVEFILE_REPLACE_EXISTING) } == 0 {
+ let err = api::get_last_error();
+ // if `MoveFileExW` fails with ERROR_ACCESS_DENIED then try to move
+ // the file while ignoring the readonly attribute.
+ // This is accomplished by calling `SetFileInformationByHandle` with `FileRenameInfoEx`.
+ if err == WinError::ACCESS_DENIED {
+ let mut opts = OpenOptions::new();
+ opts.access_mode(c::DELETE);
+ opts.custom_flags(c::FILE_FLAG_OPEN_REPARSE_POINT | c::FILE_FLAG_BACKUP_SEMANTICS);
+ let Ok(f) = File::open_native(&old, &opts) else { return Err(err).io_result() };
+
+ // Calculate the layout of the `FILE_RENAME_INFO` we pass to `SetFileInformation`
+ // This is a dynamically sized struct so we need to get the position of the last field to calculate the actual size.
+ let Ok(new_len_without_nul_in_bytes): Result = ((new.len() - 1) * 2).try_into()
+ else {
+ return Err(err).io_result();
};
-
- if let Err(err) = result {
- if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _)
- || err.raw_os_error() == Some(c::ERROR_INVALID_FUNCTION as _)
- {
- // `GetFileInformationByHandleEx` documents that not all underlying drivers support all file information classes.
- // Since we know we passed the correct arguments, this means the underlying driver didn't understand our request;
- // `MoveFileEx` proceeds by reopening the file without inhibiting reparse point behavior.
- None
- } else {
- Some(Err(err))
- }
- } else {
- // SAFETY: The struct has been initialized by GetFileInformationByHandleEx
- let file_attribute_tag_info = unsafe { file_attribute_tag_info.assume_init() };
- let file_type = FileType::new(
- file_attribute_tag_info.FileAttributes,
- file_attribute_tag_info.ReparseTag,
- );
-
- if file_type.is_symlink() {
- // The file is a mount point, junction point or symlink so
- // don't reopen the file so that the link gets renamed.
- Some(Ok(handle))
- } else {
- // Otherwise reopen the file without inhibiting reparse point behavior.
- None
+ let offset: u32 = offset_of!(c::FILE_RENAME_INFO, FileName).try_into().unwrap();
+ let struct_size = offset + new_len_without_nul_in_bytes + 2;
+ let layout =
+ Layout::from_size_align(struct_size as usize, align_of::())
+ .unwrap();
+
+ // SAFETY: We allocate enough memory for a full FILE_RENAME_INFO struct and a filename.
+ let file_rename_info;
+ unsafe {
+ file_rename_info = alloc(layout).cast::();
+ if file_rename_info.is_null() {
+ return Err(io::ErrorKind::OutOfMemory.into());
}
- }
- }
- // The underlying driver may not support `FILE_FLAG_OPEN_REPARSE_POINT`: Retry without it.
- Err(err) if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _) => None,
- Err(err) => Some(Err(err)),
- }
- .unwrap_or_else(|| create_file(0, 0))?;
- let layout = core::alloc::Layout::from_size_align(
- struct_size as _,
- mem::align_of::(),
- )
- .unwrap();
-
- let file_rename_info = unsafe { alloc(layout) } as *mut c::FILE_RENAME_INFO;
-
- if file_rename_info.is_null() {
- handle_alloc_error(layout);
- }
-
- // SAFETY: file_rename_info is a non-null pointer pointing to memory allocated by the global allocator.
- let mut file_rename_info = unsafe { Box::from_raw(file_rename_info) };
-
- // SAFETY: We have allocated enough memory for a full FILE_RENAME_INFO struct and a filename.
- unsafe {
- (&raw mut (*file_rename_info).Anonymous).write(c::FILE_RENAME_INFO_0 {
- Flags: c::FILE_RENAME_FLAG_REPLACE_IF_EXISTS | c::FILE_RENAME_FLAG_POSIX_SEMANTICS,
- });
-
- (&raw mut (*file_rename_info).RootDirectory).write(ptr::null_mut());
- (&raw mut (*file_rename_info).FileNameLength).write(new_len_without_nul_in_bytes);
-
- new.as_ptr()
- .copy_to_nonoverlapping((&raw mut (*file_rename_info).FileName) as *mut u16, new.len());
- }
+ (&raw mut (*file_rename_info).Anonymous).write(c::FILE_RENAME_INFO_0 {
+ Flags: c::FILE_RENAME_FLAG_REPLACE_IF_EXISTS
+ | c::FILE_RENAME_FLAG_POSIX_SEMANTICS,
+ });
- // We don't use `set_file_information_by_handle` here as `FILE_RENAME_INFO` is used for both `FileRenameInfo` and `FileRenameInfoEx`.
- let result = unsafe {
- cvt(c::SetFileInformationByHandle(
- handle.as_raw_handle(),
- c::FileRenameInfoEx,
- (&raw const *file_rename_info).cast::(),
- struct_size,
- ))
- };
+ (&raw mut (*file_rename_info).RootDirectory).write(ptr::null_mut());
+ // Don't include the NULL in the size
+ (&raw mut (*file_rename_info).FileNameLength).write(new_len_without_nul_in_bytes);
- if let Err(err) = result {
- if err.raw_os_error() == Some(c::ERROR_INVALID_PARAMETER as _) {
- // FileRenameInfoEx and FILE_RENAME_FLAG_POSIX_SEMANTICS were added in Windows 10 1607; retry with FileRenameInfo.
- file_rename_info.Anonymous.ReplaceIfExists = 1;
+ new.as_ptr().copy_to_nonoverlapping(
+ (&raw mut (*file_rename_info).FileName).cast::(),
+ new.len(),
+ );
+ }
- cvt(unsafe {
+ let result = unsafe {
c::SetFileInformationByHandle(
- handle.as_raw_handle(),
- c::FileRenameInfo,
- (&raw const *file_rename_info).cast::(),
+ f.as_raw_handle(),
+ c::FileRenameInfoEx,
+ file_rename_info.cast::(),
struct_size,
)
- })?;
+ };
+ unsafe { dealloc(file_rename_info.cast::(), layout) };
+ if result == 0 {
+ if api::get_last_error() == WinError::DIR_NOT_EMPTY {
+ return Err(WinError::DIR_NOT_EMPTY).io_result();
+ } else {
+ return Err(err).io_result();
+ }
+ }
} else {
- return Err(err);
+ return Err(err).io_result();
}
}
-
Ok(())
}
diff --git a/src/bootstrap/Cargo.lock b/src/bootstrap/Cargo.lock
index c9697e670b777..b10ea346d6f9f 100644
--- a/src/bootstrap/Cargo.lock
+++ b/src/bootstrap/Cargo.lock
@@ -84,9 +84,9 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.2.0"
+version = "1.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1aeb932158bd710538c73702db6945cb68a8fb08c519e6e12706b94263b36db8"
+checksum = "9540e661f81799159abee814118cc139a2004b3a3aa3ea37724a1b66530b90e0"
dependencies = [
"shlex",
]
diff --git a/src/bootstrap/Cargo.toml b/src/bootstrap/Cargo.toml
index d8775a67e1939..68e0591c6b7b7 100644
--- a/src/bootstrap/Cargo.toml
+++ b/src/bootstrap/Cargo.toml
@@ -36,7 +36,9 @@ test = false
# Most of the time updating these dependencies requires modifications to the
# bootstrap codebase(e.g., https://github.com/rust-lang/rust/issues/124565);
# otherwise, some targets will fail. That's why these dependencies are explicitly pinned.
-cc = "=1.2.0"
+#
+# Do not upgrade this crate unless https://github.com/rust-lang/cc-rs/issues/1317 is fixed.
+cc = "=1.1.22"
cmake = "=0.1.48"
build_helper = { path = "../build_helper" }
diff --git a/src/bootstrap/src/core/config/config.rs b/src/bootstrap/src/core/config/config.rs
index 0587408e987ef..053330d7417c6 100644
--- a/src/bootstrap/src/core/config/config.rs
+++ b/src/bootstrap/src/core/config/config.rs
@@ -2668,7 +2668,7 @@ impl Config {
/// used instead to provide a nice error to the user if the submodule is
/// missing.
pub(crate) fn update_submodule(&self, relative_path: &str) {
- if !self.submodules() {
+ if self.rust_info.is_from_tarball() || !self.submodules() {
return;
}
diff --git a/src/bootstrap/src/lib.rs b/src/bootstrap/src/lib.rs
index ccc115a279fb5..e5d2fc790fbec 100644
--- a/src/bootstrap/src/lib.rs
+++ b/src/bootstrap/src/lib.rs
@@ -471,6 +471,10 @@ impl Build {
/// The given `err_hint` will be shown to the user if the submodule is not
/// checked out and submodule management is disabled.
pub fn require_submodule(&self, submodule: &str, err_hint: Option<&str>) {
+ if self.rust_info().is_from_tarball() {
+ return;
+ }
+
// When testing bootstrap itself, it is much faster to ignore
// submodules. Almost all Steps work fine without their submodules.
if cfg!(test) && !self.config.submodules() {
diff --git a/src/ci/docker/scripts/rfl-build.sh b/src/ci/docker/scripts/rfl-build.sh
index 8776e0f0be901..dbd0eff2079b6 100755
--- a/src/ci/docker/scripts/rfl-build.sh
+++ b/src/ci/docker/scripts/rfl-build.sh
@@ -8,16 +8,10 @@ LINUX_VERSION=v6.13-rc1
../x.py build --stage 2 library rustdoc clippy rustfmt
../x.py build --stage 0 cargo
-# Install rustup so that we can use the built toolchain easily, and also
-# install bindgen in an easy way.
-curl --proto '=https' --tlsv1.2 -sSf -o rustup.sh https://sh.rustup.rs
-sh rustup.sh -y --default-toolchain none
+BUILD_DIR=$(realpath ./build/x86_64-unknown-linux-gnu)
-source /cargo/env
-
-BUILD_DIR=$(realpath ./build)
-rustup toolchain link local "${BUILD_DIR}"/x86_64-unknown-linux-gnu/stage2
-rustup default local
+# Provide path to rustc, rustdoc, clippy-driver and rustfmt to RfL
+export PATH=${PATH}:${BUILD_DIR}/stage2/bin
mkdir -p rfl
cd rfl
@@ -33,10 +27,14 @@ git -C linux fetch --depth 1 origin ${LINUX_VERSION}
git -C linux checkout FETCH_HEAD
# Install bindgen
-"${BUILD_DIR}"/x86_64-unknown-linux-gnu/stage0/bin/cargo install \
+"${BUILD_DIR}"/stage0/bin/cargo install \
--version $(linux/scripts/min-tool-version.sh bindgen) \
+ --root ${BUILD_DIR}/bindgen \
bindgen-cli
+# Provide path to bindgen to RfL
+export PATH=${PATH}:${BUILD_DIR}/bindgen/bin
+
# Configure Rust for Linux
cat < linux/kernel/configs/rfl-for-rust-ci.config
# CONFIG_WERROR is not set
diff --git a/src/librustdoc/doctest.rs b/src/librustdoc/doctest.rs
index 009e9662933ae..881d0b6dcbe2a 100644
--- a/src/librustdoc/doctest.rs
+++ b/src/librustdoc/doctest.rs
@@ -95,7 +95,7 @@ pub(crate) fn generate_args_file(file_path: &Path, options: &RustdocOptions) ->
.map_err(|error| format!("failed to create args file: {error:?}"))?;
// We now put the common arguments into the file we created.
- let mut content = vec!["--crate-type=bin".to_string()];
+ let mut content = vec![];
for cfg in &options.cfgs {
content.push(format!("--cfg={cfg}"));
@@ -488,12 +488,18 @@ pub(crate) struct RunnableDocTest {
line: usize,
edition: Edition,
no_run: bool,
- is_multiple_tests: bool,
+ merged_test_code: Option,
}
impl RunnableDocTest {
- fn path_for_merged_doctest(&self) -> PathBuf {
- self.test_opts.outdir.path().join(format!("doctest_{}.rs", self.edition))
+ fn path_for_merged_doctest_bundle(&self) -> PathBuf {
+ self.test_opts.outdir.path().join(format!("doctest_bundle_{}.rs", self.edition))
+ }
+ fn path_for_merged_doctest_runner(&self) -> PathBuf {
+ self.test_opts.outdir.path().join(format!("doctest_runner_{}.rs", self.edition))
+ }
+ fn is_multiple_tests(&self) -> bool {
+ self.merged_test_code.is_some()
}
}
@@ -512,91 +518,108 @@ fn run_test(
let rust_out = add_exe_suffix("rust_out".to_owned(), &rustdoc_options.target);
let output_file = doctest.test_opts.outdir.path().join(rust_out);
- let rustc_binary = rustdoc_options
- .test_builder
- .as_deref()
- .unwrap_or_else(|| rustc_interface::util::rustc_path().expect("found rustc"));
- let mut compiler = wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
+ // Common arguments used for compiling the doctest runner.
+ // On merged doctests, the compiler is invoked twice: once for the test code itself,
+ // and once for the runner wrapper (which needs to use `#![feature]` on stable).
+ let mut compiler_args = vec![];
- compiler.arg(format!("@{}", doctest.global_opts.args_file.display()));
+ compiler_args.push(format!("@{}", doctest.global_opts.args_file.display()));
if let Some(sysroot) = &rustdoc_options.maybe_sysroot {
- compiler.arg(format!("--sysroot={}", sysroot.display()));
+ compiler_args.push(format!("--sysroot={}", sysroot.display()));
}
- compiler.arg("--edition").arg(doctest.edition.to_string());
- if !doctest.is_multiple_tests {
- // Setting these environment variables is unneeded if this is a merged doctest.
- compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", &doctest.test_opts.path);
- compiler.env(
- "UNSTABLE_RUSTDOC_TEST_LINE",
- format!("{}", doctest.line as isize - doctest.full_test_line_offset as isize),
- );
- }
- compiler.arg("-o").arg(&output_file);
+ compiler_args.extend_from_slice(&["--edition".to_owned(), doctest.edition.to_string()]);
if langstr.test_harness {
- compiler.arg("--test");
+ compiler_args.push("--test".to_owned());
}
if rustdoc_options.json_unused_externs.is_enabled() && !langstr.compile_fail {
- compiler.arg("--error-format=json");
- compiler.arg("--json").arg("unused-externs");
- compiler.arg("-W").arg("unused_crate_dependencies");
- compiler.arg("-Z").arg("unstable-options");
+ compiler_args.push("--error-format=json".to_owned());
+ compiler_args.extend_from_slice(&["--json".to_owned(), "unused-externs".to_owned()]);
+ compiler_args.extend_from_slice(&["-W".to_owned(), "unused_crate_dependencies".to_owned()]);
+ compiler_args.extend_from_slice(&["-Z".to_owned(), "unstable-options".to_owned()]);
}
if doctest.no_run && !langstr.compile_fail && rustdoc_options.persist_doctests.is_none() {
// FIXME: why does this code check if it *shouldn't* persist doctests
// -- shouldn't it be the negation?
- compiler.arg("--emit=metadata");
+ compiler_args.push("--emit=metadata".to_owned());
}
- compiler.arg("--target").arg(match &rustdoc_options.target {
- TargetTuple::TargetTuple(s) => s,
- TargetTuple::TargetJson { path_for_rustdoc, .. } => {
- path_for_rustdoc.to_str().expect("target path must be valid unicode")
- }
- });
+ compiler_args.extend_from_slice(&[
+ "--target".to_owned(),
+ match &rustdoc_options.target {
+ TargetTuple::TargetTuple(s) => s.clone(),
+ TargetTuple::TargetJson { path_for_rustdoc, .. } => {
+ path_for_rustdoc.to_str().expect("target path must be valid unicode").to_owned()
+ }
+ },
+ ]);
if let ErrorOutputType::HumanReadable(kind, color_config) = rustdoc_options.error_format {
let short = kind.short();
let unicode = kind == HumanReadableErrorType::Unicode;
if short {
- compiler.arg("--error-format").arg("short");
+ compiler_args.extend_from_slice(&["--error-format".to_owned(), "short".to_owned()]);
}
if unicode {
- compiler.arg("--error-format").arg("human-unicode");
+ compiler_args
+ .extend_from_slice(&["--error-format".to_owned(), "human-unicode".to_owned()]);
}
match color_config {
ColorConfig::Never => {
- compiler.arg("--color").arg("never");
+ compiler_args.extend_from_slice(&["--color".to_owned(), "never".to_owned()]);
}
ColorConfig::Always => {
- compiler.arg("--color").arg("always");
+ compiler_args.extend_from_slice(&["--color".to_owned(), "always".to_owned()]);
}
ColorConfig::Auto => {
- compiler.arg("--color").arg(if supports_color { "always" } else { "never" });
+ compiler_args.extend_from_slice(&[
+ "--color".to_owned(),
+ if supports_color { "always" } else { "never" }.to_owned(),
+ ]);
}
}
}
+ let rustc_binary = rustdoc_options
+ .test_builder
+ .as_deref()
+ .unwrap_or_else(|| rustc_interface::util::rustc_path().expect("found rustc"));
+ let mut compiler = wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
+
+ compiler.args(&compiler_args);
+
// If this is a merged doctest, we need to write it into a file instead of using stdin
// because if the size of the merged doctests is too big, it'll simply break stdin.
- if doctest.is_multiple_tests {
+ if doctest.is_multiple_tests() {
// It makes the compilation failure much faster if it is for a combined doctest.
compiler.arg("--error-format=short");
- let input_file = doctest.path_for_merged_doctest();
+ let input_file = doctest.path_for_merged_doctest_bundle();
if std::fs::write(&input_file, &doctest.full_test_code).is_err() {
// If we cannot write this file for any reason, we leave. All combined tests will be
// tested as standalone tests.
return Err(TestFailure::CompileError);
}
- compiler.arg(input_file);
if !rustdoc_options.nocapture {
// If `nocapture` is disabled, then we don't display rustc's output when compiling
// the merged doctests.
compiler.stderr(Stdio::null());
}
+ // bundled tests are an rlib, loaded by a separate runner executable
+ compiler
+ .arg("--crate-type=lib")
+ .arg("--out-dir")
+ .arg(doctest.test_opts.outdir.path())
+ .arg(input_file);
} else {
+ compiler.arg("--crate-type=bin").arg("-o").arg(&output_file);
+ // Setting these environment variables is unneeded if this is a merged doctest.
+ compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", &doctest.test_opts.path);
+ compiler.env(
+ "UNSTABLE_RUSTDOC_TEST_LINE",
+ format!("{}", doctest.line as isize - doctest.full_test_line_offset as isize),
+ );
compiler.arg("-");
compiler.stdin(Stdio::piped());
compiler.stderr(Stdio::piped());
@@ -605,8 +628,65 @@ fn run_test(
debug!("compiler invocation for doctest: {compiler:?}");
let mut child = compiler.spawn().expect("Failed to spawn rustc process");
- let output = if doctest.is_multiple_tests {
+ let output = if let Some(merged_test_code) = &doctest.merged_test_code {
+ // compile-fail tests never get merged, so this should always pass
let status = child.wait().expect("Failed to wait");
+
+ // the actual test runner is a separate component, built with nightly-only features;
+ // build it now
+ let runner_input_file = doctest.path_for_merged_doctest_runner();
+
+ let mut runner_compiler =
+ wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
+ // the test runner does not contain any user-written code, so this doesn't allow
+ // the user to exploit nightly-only features on stable
+ runner_compiler.env("RUSTC_BOOTSTRAP", "1");
+ runner_compiler.args(compiler_args);
+ runner_compiler.args(&["--crate-type=bin", "-o"]).arg(&output_file);
+ let mut extern_path = std::ffi::OsString::from(format!(
+ "--extern=doctest_bundle_{edition}=",
+ edition = doctest.edition
+ ));
+ for extern_str in &rustdoc_options.extern_strs {
+ if let Some((_cratename, path)) = extern_str.split_once('=') {
+ // Direct dependencies of the tests themselves are
+ // indirect dependencies of the test runner.
+ // They need to be in the library search path.
+ let dir = Path::new(path)
+ .parent()
+ .filter(|x| x.components().count() > 0)
+ .unwrap_or(Path::new("."));
+ runner_compiler.arg("-L").arg(dir);
+ }
+ }
+ let output_bundle_file = doctest
+ .test_opts
+ .outdir
+ .path()
+ .join(format!("libdoctest_bundle_{edition}.rlib", edition = doctest.edition));
+ extern_path.push(&output_bundle_file);
+ runner_compiler.arg(extern_path);
+ runner_compiler.arg(&runner_input_file);
+ if std::fs::write(&runner_input_file, &merged_test_code).is_err() {
+ // If we cannot write this file for any reason, we leave. All combined tests will be
+ // tested as standalone tests.
+ return Err(TestFailure::CompileError);
+ }
+ if !rustdoc_options.nocapture {
+ // If `nocapture` is disabled, then we don't display rustc's output when compiling
+ // the merged doctests.
+ runner_compiler.stderr(Stdio::null());
+ }
+ runner_compiler.arg("--error-format=short");
+ debug!("compiler invocation for doctest runner: {runner_compiler:?}");
+
+ let status = if !status.success() {
+ status
+ } else {
+ let mut child_runner = runner_compiler.spawn().expect("Failed to spawn rustc process");
+ child_runner.wait().expect("Failed to wait")
+ };
+
process::Output { status, stdout: Vec::new(), stderr: Vec::new() }
} else {
let stdin = child.stdin.as_mut().expect("Failed to open stdin");
@@ -683,7 +763,7 @@ fn run_test(
cmd.arg(&output_file);
} else {
cmd = Command::new(&output_file);
- if doctest.is_multiple_tests {
+ if doctest.is_multiple_tests() {
cmd.env("RUSTDOC_DOCTEST_BIN_PATH", &output_file);
}
}
@@ -691,7 +771,7 @@ fn run_test(
cmd.current_dir(run_directory);
}
- let result = if doctest.is_multiple_tests || rustdoc_options.nocapture {
+ let result = if doctest.is_multiple_tests() || rustdoc_options.nocapture {
cmd.status().map(|status| process::Output {
status,
stdout: Vec::new(),
@@ -977,7 +1057,7 @@ fn doctest_run_fn(
line: scraped_test.line,
edition: scraped_test.edition(&rustdoc_options),
no_run: scraped_test.no_run(&rustdoc_options),
- is_multiple_tests: false,
+ merged_test_code: None,
};
let res =
run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs);
diff --git a/src/librustdoc/doctest/runner.rs b/src/librustdoc/doctest/runner.rs
index 234f40c6c1ab2..58efa35711a0e 100644
--- a/src/librustdoc/doctest/runner.rs
+++ b/src/librustdoc/doctest/runner.rs
@@ -14,6 +14,7 @@ pub(crate) struct DocTestRunner {
crate_attrs: FxIndexSet,
ids: String,
output: String,
+ output_merged_tests: String,
supports_color: bool,
nb_tests: usize,
}
@@ -24,6 +25,7 @@ impl DocTestRunner {
crate_attrs: FxIndexSet::default(),
ids: String::new(),
output: String::new(),
+ output_merged_tests: String::new(),
supports_color: true,
nb_tests: 0,
}
@@ -55,7 +57,8 @@ impl DocTestRunner {
scraped_test,
ignore,
self.nb_tests,
- &mut self.output
+ &mut self.output,
+ &mut self.output_merged_tests,
),
));
self.supports_color &= doctest.supports_color;
@@ -78,9 +81,11 @@ impl DocTestRunner {
"
.to_string();
+ let mut code_prefix = String::new();
+
for crate_attr in &self.crate_attrs {
- code.push_str(crate_attr);
- code.push('\n');
+ code_prefix.push_str(crate_attr);
+ code_prefix.push('\n');
}
if opts.attrs.is_empty() {
@@ -88,15 +93,16 @@ impl DocTestRunner {
// lints that are commonly triggered in doctests. The crate-level test attributes are
// commonly used to make tests fail in case they trigger warnings, so having this there in
// that case may cause some tests to pass when they shouldn't have.
- code.push_str("#![allow(unused)]\n");
+ code_prefix.push_str("#![allow(unused)]\n");
}
// Next, any attributes that came from the crate root via #![doc(test(attr(...)))].
for attr in &opts.attrs {
- code.push_str(&format!("#![{attr}]\n"));
+ code_prefix.push_str(&format!("#![{attr}]\n"));
}
code.push_str("extern crate test;\n");
+ writeln!(code, "extern crate doctest_bundle_{edition} as doctest_bundle;").unwrap();
let test_args = test_args.iter().fold(String::new(), |mut x, arg| {
write!(x, "{arg:?}.to_string(),").unwrap();
@@ -161,12 +167,12 @@ the same process\");
std::process::Termination::report(test::test_main(test_args, Vec::from(TESTS), None))
}}",
nb_tests = self.nb_tests,
- output = self.output,
+ output = self.output_merged_tests,
ids = self.ids,
)
.expect("failed to generate test code");
let runnable_test = RunnableDocTest {
- full_test_code: code,
+ full_test_code: format!("{code_prefix}{code}", code = self.output),
full_test_line_offset: 0,
test_opts: test_options,
global_opts: opts.clone(),
@@ -174,7 +180,7 @@ std::process::Termination::report(test::test_main(test_args, Vec::from(TESTS), N
line: 0,
edition,
no_run: false,
- is_multiple_tests: true,
+ merged_test_code: Some(code),
};
let ret =
run_test(runnable_test, rustdoc_options, self.supports_color, |_: UnusedExterns| {});
@@ -189,14 +195,15 @@ fn generate_mergeable_doctest(
ignore: bool,
id: usize,
output: &mut String,
+ output_merged_tests: &mut String,
) -> String {
let test_id = format!("__doctest_{id}");
if ignore {
// We generate nothing else.
- writeln!(output, "mod {test_id} {{\n").unwrap();
+ writeln!(output, "pub mod {test_id} {{}}\n").unwrap();
} else {
- writeln!(output, "mod {test_id} {{\n{}{}", doctest.crates, doctest.maybe_crate_attrs)
+ writeln!(output, "pub mod {test_id} {{\n{}{}", doctest.crates, doctest.maybe_crate_attrs)
.unwrap();
if doctest.has_main_fn {
output.push_str(&doctest.everything_else);
@@ -216,11 +223,17 @@ fn main() {returns_result} {{
)
.unwrap();
}
+ writeln!(
+ output,
+ "\npub fn __main_fn() -> impl std::process::Termination {{ main() }} \n}}\n"
+ )
+ .unwrap();
}
let not_running = ignore || scraped_test.langstr.no_run;
writeln!(
- output,
+ output_merged_tests,
"
+mod {test_id} {{
pub const TEST: test::TestDescAndFn = test::TestDescAndFn::new_doctest(
{test_name:?}, {ignore}, {file:?}, {line}, {no_run}, {should_panic},
test::StaticTestFn(
@@ -242,7 +255,7 @@ test::StaticTestFn(
if let Some(bin_path) = crate::__doctest_mod::doctest_path() {{
test::assert_test_result(crate::__doctest_mod::doctest_runner(bin_path, {id}))
}} else {{
- test::assert_test_result(self::main())
+ test::assert_test_result(doctest_bundle::{test_id}::__main_fn())
}}
",
)
diff --git a/src/version b/src/version
index f288d11142d11..8510ffad03679 100644
--- a/src/version
+++ b/src/version
@@ -1 +1 @@
-1.85.0
+1.85.1
diff --git a/tests/run-make/doctests-merge/rmake.rs b/tests/run-make/doctests-merge/rmake.rs
index a25da7403e24b..a88b050c50fa8 100644
--- a/tests/run-make/doctests-merge/rmake.rs
+++ b/tests/run-make/doctests-merge/rmake.rs
@@ -8,7 +8,6 @@ fn test_and_compare(input_file: &str, stdout_file: &str, edition: &str, dep: &Pa
let output = cmd
.input(input_file)
.arg("--test")
- .arg("-Zunstable-options")
.edition(edition)
.arg("--test-args=--test-threads=1")
.extern_("foo", dep.display().to_string())
diff --git a/tests/rustdoc-ui/doctest/doctest-output.rs b/tests/rustdoc-ui/doctest/doctest-output.rs
index fb4ab06800019..04bd1813b4c81 100644
--- a/tests/rustdoc-ui/doctest/doctest-output.rs
+++ b/tests/rustdoc-ui/doctest/doctest-output.rs
@@ -2,7 +2,7 @@
//@[edition2015]edition:2015
//@[edition2015]aux-build:extern_macros.rs
//@[edition2015]compile-flags:--test --test-args=--test-threads=1
-//@[edition2024]edition:2015
+//@[edition2024]edition:2024
//@[edition2024]aux-build:extern_macros.rs
//@[edition2024]compile-flags:--test --test-args=--test-threads=1
//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"
diff --git a/tests/rustdoc-ui/doctest/failed-doctest-test-crate.edition2015.stdout b/tests/rustdoc-ui/doctest/failed-doctest-test-crate.edition2015.stdout
new file mode 100644
index 0000000000000..5bbf3ebfab6cc
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/failed-doctest-test-crate.edition2015.stdout
@@ -0,0 +1,28 @@
+
+running 1 test
+test $DIR/failed-doctest-test-crate.rs - m (line 14) ... FAILED
+
+failures:
+
+---- $DIR/failed-doctest-test-crate.rs - m (line 14) stdout ----
+error[E0432]: unresolved import `test`
+ --> $DIR/failed-doctest-test-crate.rs:15:5
+ |
+LL | use test::*;
+ | ^^^^ you might be missing crate `test`
+ |
+help: consider importing the `test` crate
+ |
+LL + extern crate test;
+ |
+
+error: aborting due to 1 previous error
+
+For more information about this error, try `rustc --explain E0432`.
+Couldn't compile the test.
+
+failures:
+ $DIR/failed-doctest-test-crate.rs - m (line 14)
+
+test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
+
diff --git a/tests/rustdoc-ui/doctest/failed-doctest-test-crate.edition2024.stdout b/tests/rustdoc-ui/doctest/failed-doctest-test-crate.edition2024.stdout
new file mode 100644
index 0000000000000..463b0fa95361b
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/failed-doctest-test-crate.edition2024.stdout
@@ -0,0 +1,23 @@
+
+running 1 test
+test $DIR/failed-doctest-test-crate.rs - m (line 14) ... FAILED
+
+failures:
+
+---- $DIR/failed-doctest-test-crate.rs - m (line 14) stdout ----
+error[E0432]: unresolved import `test`
+ --> $DIR/failed-doctest-test-crate.rs:15:5
+ |
+LL | use test::*;
+ | ^^^^ use of undeclared crate or module `test`
+
+error: aborting due to 1 previous error
+
+For more information about this error, try `rustc --explain E0432`.
+Couldn't compile the test.
+
+failures:
+ $DIR/failed-doctest-test-crate.rs - m (line 14)
+
+test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
+
diff --git a/tests/rustdoc-ui/doctest/failed-doctest-test-crate.rs b/tests/rustdoc-ui/doctest/failed-doctest-test-crate.rs
new file mode 100644
index 0000000000000..6966d3df11ce7
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/failed-doctest-test-crate.rs
@@ -0,0 +1,17 @@
+// FIXME: if/when the output of the test harness can be tested on its own, this test should be
+// adapted to use that, and that normalize line can go away
+
+//@ revisions: edition2015 edition2024
+//@[edition2015]edition:2015
+//@[edition2024]edition:2024
+//@ compile-flags:--test
+//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"
+//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME"
+//@ failure-status: 101
+
+///
+///
+/// ```rust
+/// use test::*;
+/// ```
+pub mod m {}
diff --git a/tests/rustdoc-ui/target-feature-stability.rs b/tests/rustdoc-ui/target-feature-stability.rs
new file mode 100644
index 0000000000000..17fa3ccfe3e89
--- /dev/null
+++ b/tests/rustdoc-ui/target-feature-stability.rs
@@ -0,0 +1,28 @@
+//! This is a regression test for , ensuring
+//! that we can use the `neon` target feature on ARM32 targets in rustdoc despite there
+//! being a "forbidden" feature of the same name for aarch64, and rustdoc merging the
+//! target features of all targets.
+//@ check-pass
+//@ revisions: arm aarch64
+//@[arm] compile-flags: --target armv7-unknown-linux-gnueabihf
+//@[arm] needs-llvm-components: arm
+//@[aarch64] compile-flags: --target aarch64-unknown-none-softfloat
+//@[aarch64] needs-llvm-components: aarch64
+
+#![crate_type = "lib"]
+#![feature(no_core, lang_items)]
+#![feature(arm_target_feature)]
+#![no_core]
+
+#[lang = "sized"]
+pub trait Sized {}
+
+// `fp-armv8` is "forbidden" on aarch64 as we tie it to `neon`.
+#[target_feature(enable = "fp-armv8")]
+pub fn fun1() {}
+
+// This would usually be rejected as it changes the ABI.
+// But we disable that check in rustdoc since we are building "for all targets" and the
+// check can't really handle that.
+#[target_feature(enable = "soft-float")]
+pub fn fun2() {}