use std::cell::{Cell, RefCell};
use std::collections::BTreeSet;
use std::env;
-use std::ffi::OsStr;
+use std::ffi::{OsStr, OsString};
use std::fmt::{Debug, Write};
-use std::fs;
+use std::fs::{self, File};
use std::hash::Hash;
+use std::io::{BufRead, BufReader, ErrorKind};
use std::ops::Deref;
use std::path::{Component, Path, PathBuf};
-use std::process::Command;
+use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use crate::cache::{Cache, Interned, INTERNER};
pub use crate::Compiler;
// FIXME: replace with std::lazy after it gets stabilized and reaches beta
-use once_cell::sync::Lazy;
+use once_cell::sync::{Lazy, OnceCell};
+use xz2::bufread::XzDecoder;
pub struct Builder<'a> {
pub build: &'a Build,
pub struct RunConfig<'a> {
pub builder: &'a Builder<'a>,
pub target: TargetSelection,
- pub path: PathBuf,
+ pub paths: Vec<PathSet>,
}
impl RunConfig<'_> {
/// Collection of paths used to match a task rule.
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)]
pub enum PathSet {
- /// A collection of individual paths.
+ /// A collection of individual paths or aliases.
///
/// These are generally matched as a path suffix. For example, a
- /// command-line value of `libstd` will match if `src/libstd` is in the
+ /// command-line value of `std` will match if `library/std` is in the
/// set.
+ ///
+ /// NOTE: the paths within a set should always be aliases of one another.
+ /// For example, `src/librustdoc` and `src/tools/rustdoc` should be in the same set,
+ /// but `library/core` and `library/std` generally should not, unless there's no way (for that Step)
+ /// to build them separately.
Set(BTreeSet<TaskPath>),
/// A "suite" of paths.
///
}
fn has(&self, needle: &Path, module: Option<Kind>) -> bool {
- let check = |p: &TaskPath| {
- if let (Some(p_kind), Some(kind)) = (&p.kind, module) {
- p.path.ends_with(needle) && *p_kind == kind
- } else {
- p.path.ends_with(needle)
+ match self {
+ PathSet::Set(set) => set.iter().any(|p| Self::check(p, needle, module)),
+ PathSet::Suite(suite) => Self::check(suite, needle, module),
+ }
+ }
+
+ // internal use only
+ fn check(p: &TaskPath, needle: &Path, module: Option<Kind>) -> bool {
+ if let (Some(p_kind), Some(kind)) = (&p.kind, module) {
+ p.path.ends_with(needle) && *p_kind == kind
+ } else {
+ p.path.ends_with(needle)
+ }
+ }
+
+ /// Return all `TaskPath`s in `Self` that contain any of the `needles`, removing the
+ /// matched needles.
+ ///
+ /// This is used for `StepDescription::krate`, which passes all matching crates at once to
+ /// `Step::make_run`, rather than calling it many times with a single crate.
+ /// See `tests.rs` for examples.
+ fn intersection_removing_matches(
+ &self,
+ needles: &mut Vec<&Path>,
+ module: Option<Kind>,
+ ) -> PathSet {
+ let mut check = |p| {
+ for (i, n) in needles.iter().enumerate() {
+ let matched = Self::check(p, n, module);
+ if matched {
+ needles.remove(i);
+ return true;
+ }
}
+ false
};
-
match self {
- PathSet::Set(set) => set.iter().any(check),
- PathSet::Suite(suite) => check(suite),
+ PathSet::Set(set) => PathSet::Set(set.iter().filter(|&p| check(p)).cloned().collect()),
+ PathSet::Suite(suite) => {
+ if check(suite) {
+ self.clone()
+ } else {
+ PathSet::empty()
+ }
+ }
}
}
- fn path(&self, builder: &Builder<'_>) -> PathBuf {
+ /// A convenience wrapper for Steps which know they have no aliases and all their sets contain only a single path.
+ ///
+ /// This can be used with [`ShouldRun::krate`], [`ShouldRun::path`], or [`ShouldRun::alias`].
+ #[track_caller]
+ pub fn assert_single_path(&self) -> &TaskPath {
match self {
PathSet::Set(set) => {
- set.iter().next().map(|p| &p.path).unwrap_or(&builder.build.src).clone()
+ assert_eq!(set.len(), 1, "called assert_single_path on multiple paths");
+ set.iter().next().unwrap()
}
- PathSet::Suite(path) => path.path.clone(),
+ PathSet::Suite(_) => unreachable!("called assert_single_path on a Suite path"),
}
}
}
}
}
- fn maybe_run(&self, builder: &Builder<'_>, pathset: &PathSet) {
- if self.is_excluded(builder, pathset) {
+ fn maybe_run(&self, builder: &Builder<'_>, pathsets: Vec<PathSet>) {
+ if pathsets.iter().any(|set| self.is_excluded(builder, set)) {
return;
}
let targets = if self.only_hosts { &builder.hosts } else { &builder.targets };
for target in targets {
- let run = RunConfig { builder, path: pathset.path(builder), target: *target };
+ let run = RunConfig { builder, paths: pathsets.clone(), target: *target };
(self.make_run)(run);
}
}
fn is_excluded(&self, builder: &Builder<'_>, pathset: &PathSet) -> bool {
if builder.config.exclude.iter().any(|e| pathset.has(&e.path, e.kind)) {
- eprintln!("Skipping {:?} because it is excluded", pathset);
+ println!("Skipping {:?} because it is excluded", pathset);
return true;
}
for (desc, should_run) in v.iter().zip(&should_runs) {
if desc.default && should_run.is_really_default() {
for pathset in &should_run.paths {
- desc.maybe_run(builder, pathset);
+ desc.maybe_run(builder, vec![pathset.clone()]);
}
}
}
}
- for path in paths {
- // strip CurDir prefix if present
- let path = match path.strip_prefix(".") {
- Ok(p) => p,
- Err(_) => path,
- };
+ // strip CurDir prefix if present
+ let mut paths: Vec<_> =
+ paths.into_iter().map(|p| p.strip_prefix(".").unwrap_or(p)).collect();
- let mut attempted_run = false;
+ // Handle all test suite paths.
+ // (This is separate from the loop below to avoid having to handle multiple paths in `is_suite_path` somehow.)
+ paths.retain(|path| {
for (desc, should_run) in v.iter().zip(&should_runs) {
- if let Some(suite) = should_run.is_suite_path(path) {
- attempted_run = true;
- desc.maybe_run(builder, suite);
- } else if let Some(pathset) = should_run.pathset_for_path(path, desc.kind) {
- attempted_run = true;
- desc.maybe_run(builder, pathset);
+ if let Some(suite) = should_run.is_suite_path(&path) {
+ desc.maybe_run(builder, vec![suite.clone()]);
+ return false;
}
}
+ true
+ });
- if !attempted_run {
- eprintln!(
- "error: no `{}` rules matched '{}'",
- builder.kind.as_str(),
- path.display()
- );
- eprintln!(
- "help: run `x.py {} --help --verbose` to show a list of available paths",
- builder.kind.as_str()
- );
- eprintln!(
- "note: if you are adding a new Step to bootstrap itself, make sure you register it with `describe!`"
- );
- std::process::exit(1);
+ if paths.is_empty() {
+ return;
+ }
+
+ // Handle all PathSets.
+ for (desc, should_run) in v.iter().zip(&should_runs) {
+ let pathsets = should_run.pathset_for_paths_removing_matches(&mut paths, desc.kind);
+ if !pathsets.is_empty() {
+ desc.maybe_run(builder, pathsets);
}
}
+
+ if !paths.is_empty() {
+ eprintln!("error: no `{}` rules matched {:?}", builder.kind.as_str(), paths,);
+ eprintln!(
+ "help: run `x.py {} --help --verbose` to show a list of available paths",
+ builder.kind.as_str()
+ );
+ eprintln!(
+ "note: if you are adding a new Step to bootstrap itself, make sure you register it with `describe!`"
+ );
+ #[cfg(not(test))]
+ std::process::exit(1);
+ #[cfg(test)]
+ // so we can use #[should_panic]
+ panic!()
+ }
}
}
/// Indicates it should run if the command-line selects the given crate or
/// any of its (local) dependencies.
///
- /// `make_run` will be called separately for each matching command-line path.
+ /// `make_run` will be called a single time with all matching command-line paths.
pub fn krate(mut self, name: &str) -> Self {
for krate in self.builder.in_tree_crates(name, None) {
let path = krate.local_path(self.builder);
self
}
- pub fn is_suite_path(&self, path: &Path) -> Option<&PathSet> {
+ /// Handles individual files (not directories) within a test suite.
+ fn is_suite_path(&self, requested_path: &Path) -> Option<&PathSet> {
self.paths.iter().find(|pathset| match pathset {
- PathSet::Suite(p) => path.starts_with(&p.path),
+ PathSet::Suite(suite) => requested_path.starts_with(&suite.path),
PathSet::Set(_) => false,
})
}
self
}
- fn pathset_for_path(&self, path: &Path, kind: Kind) -> Option<&PathSet> {
- self.paths.iter().find(|pathset| pathset.has(path, Some(kind)))
+ /// Given a set of requested paths, return the subset which match the Step for this `ShouldRun`,
+ /// removing the matches from `paths`.
+ ///
+ /// NOTE: this returns multiple PathSets to allow for the possibility of multiple units of work
+ /// within the same step. For example, `test::Crate` allows testing multiple crates in the same
+ /// cargo invocation, which are put into separate sets because they aren't aliases.
+ ///
+ /// The reason we return PathSet instead of PathBuf is to allow for aliases that mean the same thing
+ /// (for now, just `all_krates` and `paths`, but we may want to add an `aliases` function in the future?)
+ fn pathset_for_paths_removing_matches(
+ &self,
+ paths: &mut Vec<&Path>,
+ kind: Kind,
+ ) -> Vec<PathSet> {
+ let mut sets = vec![];
+ for pathset in &self.paths {
+ let subset = pathset.intersection_removing_matches(paths, Some(kind));
+ if subset != PathSet::empty() {
+ sets.push(subset);
+ }
+ }
+ sets
}
}
Subcommand::Dist { ref paths } => (Kind::Dist, &paths[..]),
Subcommand::Install { ref paths } => (Kind::Install, &paths[..]),
Subcommand::Run { ref paths } => (Kind::Run, &paths[..]),
- Subcommand::Format { .. } | Subcommand::Clean { .. } | Subcommand::Setup { .. } => {
+ Subcommand::Format { .. } => (Kind::Format, &[][..]),
+ Subcommand::Clean { .. } | Subcommand::Setup { .. } => {
panic!()
}
};
StepDescription::run(v, self, paths);
}
+ /// Modifies the interpreter section of 'fname' to fix the dynamic linker,
+ /// or the RPATH section, to fix the dynamic library search path
+ ///
+ /// This is only required on NixOS and uses the PatchELF utility to
+ /// change the interpreter/RPATH of ELF executables.
+ ///
+ /// Please see https://nixos.org/patchelf.html for more information
+ pub(crate) fn fix_bin_or_dylib(&self, fname: &Path) {
+ // FIXME: cache NixOS detection?
+ match Command::new("uname").arg("-s").stderr(Stdio::inherit()).output() {
+ Err(_) => return,
+ Ok(output) if !output.status.success() => return,
+ Ok(output) => {
+ let mut s = output.stdout;
+ if s.last() == Some(&b'\n') {
+ s.pop();
+ }
+ if s != b"Linux" {
+ return;
+ }
+ }
+ }
+
+ // If the user has asked binaries to be patched for Nix, then
+ // don't check for NixOS or `/lib`, just continue to the patching.
+ // NOTE: this intentionally comes after the Linux check:
+ // - patchelf only works with ELF files, so no need to run it on Mac or Windows
+ // - On other Unix systems, there is no stable syscall interface, so Nix doesn't manage the global libc.
+ if !self.config.patch_binaries_for_nix {
+ // Use `/etc/os-release` instead of `/etc/NIXOS`.
+ // The latter one does not exist on NixOS when using tmpfs as root.
+ const NIX_IDS: &[&str] = &["ID=nixos", "ID='nixos'", "ID=\"nixos\""];
+ let os_release = match File::open("/etc/os-release") {
+ Err(e) if e.kind() == ErrorKind::NotFound => return,
+ Err(e) => panic!("failed to access /etc/os-release: {}", e),
+ Ok(f) => f,
+ };
+ if !BufReader::new(os_release).lines().any(|l| NIX_IDS.contains(&t!(l).trim())) {
+ return;
+ }
+ if Path::new("/lib").exists() {
+ return;
+ }
+ }
+
+ // At this point we're pretty sure the user is running NixOS or using Nix
+ println!("info: you seem to be using Nix. Attempting to patch {}", fname.display());
+
+ // Only build `.nix-deps` once.
+ static NIX_DEPS_DIR: OnceCell<PathBuf> = OnceCell::new();
+ let mut nix_build_succeeded = true;
+ let nix_deps_dir = NIX_DEPS_DIR.get_or_init(|| {
+ // Run `nix-build` to "build" each dependency (which will likely reuse
+ // the existing `/nix/store` copy, or at most download a pre-built copy).
+ //
+ // Importantly, we create a gc-root called `.nix-deps` in the `build/`
+ // directory, but still reference the actual `/nix/store` path in the rpath
+ // as it makes it significantly more robust against changes to the location of
+ // the `.nix-deps` location.
+ //
+ // bintools: Needed for the path of `ld-linux.so` (via `nix-support/dynamic-linker`).
+ // zlib: Needed as a system dependency of `libLLVM-*.so`.
+ // patchelf: Needed for patching ELF binaries (see doc comment above).
+ let nix_deps_dir = self.out.join(".nix-deps");
+ const NIX_EXPR: &str = "
+ with (import <nixpkgs> {});
+ symlinkJoin {
+ name = \"rust-stage0-dependencies\";
+ paths = [
+ zlib
+ patchelf
+ stdenv.cc.bintools
+ ];
+ }
+ ";
+ nix_build_succeeded = self.try_run(Command::new("nix-build").args(&[
+ Path::new("-E"),
+ Path::new(NIX_EXPR),
+ Path::new("-o"),
+ &nix_deps_dir,
+ ]));
+ nix_deps_dir
+ });
+ if !nix_build_succeeded {
+ return;
+ }
+
+ let mut patchelf = Command::new(nix_deps_dir.join("bin/patchelf"));
+ let rpath_entries = {
+ // ORIGIN is a relative default, all binary and dynamic libraries we ship
+ // appear to have this (even when `../lib` is redundant).
+ // NOTE: there are only two paths here, delimited by a `:`
+ let mut entries = OsString::from("$ORIGIN/../lib:");
+ entries.push(t!(fs::canonicalize(nix_deps_dir)));
+ entries.push("/lib");
+ entries
+ };
+ patchelf.args(&[OsString::from("--set-rpath"), rpath_entries]);
+ if !fname.extension().map_or(false, |ext| ext == "so") {
+ // Finally, set the corret .interp for binaries
+ let dynamic_linker_path = nix_deps_dir.join("nix-support/dynamic-linker");
+ // FIXME: can we support utf8 here? `args` doesn't accept Vec<u8>, only OsString ...
+ let dynamic_linker = t!(String::from_utf8(t!(fs::read(dynamic_linker_path))));
+ patchelf.args(&["--set-interpreter", dynamic_linker.trim_end()]);
+ }
+
+ self.try_run(patchelf.arg(fname));
+ }
+
+ pub(crate) fn download_component(&self, url: &str, dest_path: &Path, help_on_error: &str) {
+ // Use a temporary file in case we crash while downloading, to avoid a corrupt download in cache/.
+ let tempfile = self.tempdir().join(dest_path.file_name().unwrap());
+ // While bootstrap itself only supports http and https downloads, downstream forks might
+ // need to download components from other protocols. The match allows them adding more
+ // protocols without worrying about merge conficts if we change the HTTP implementation.
+ match url.split_once("://").map(|(proto, _)| proto) {
+ Some("http") | Some("https") => {
+ self.download_http_with_retries(&tempfile, url, help_on_error)
+ }
+ Some(other) => panic!("unsupported protocol {other} in {url}"),
+ None => panic!("no protocol in {url}"),
+ }
+ t!(std::fs::rename(&tempfile, dest_path));
+ }
+
+ fn download_http_with_retries(&self, tempfile: &Path, url: &str, help_on_error: &str) {
+ println!("downloading {}", url);
+ // Try curl. If that fails and we are on windows, fallback to PowerShell.
+ let mut curl = Command::new("curl");
+ curl.args(&[
+ "-#",
+ "-y",
+ "30",
+ "-Y",
+ "10", // timeout if speed is < 10 bytes/sec for > 30 seconds
+ "--connect-timeout",
+ "30", // timeout if cannot connect within 30 seconds
+ "--retry",
+ "3",
+ "-Sf",
+ "-o",
+ ]);
+ curl.arg(tempfile);
+ curl.arg(url);
+ if !self.check_run(&mut curl) {
+ if self.build.build.contains("windows-msvc") {
+ println!("Fallback to PowerShell");
+ for _ in 0..3 {
+ if self.try_run(Command::new("PowerShell.exe").args(&[
+ "/nologo",
+ "-Command",
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;",
+ &format!(
+ "(New-Object System.Net.WebClient).DownloadFile('{}', '{}')",
+ url, tempfile.to_str().expect("invalid UTF-8 not supported with powershell downloads"),
+ ),
+ ])) {
+ return;
+ }
+ println!("\nspurious failure, trying again");
+ }
+ }
+ if !help_on_error.is_empty() {
+ eprintln!("{}", help_on_error);
+ }
+ std::process::exit(1);
+ }
+ }
+
+ pub(crate) fn unpack(&self, tarball: &Path, dst: &Path, pattern: &str) {
+ println!("extracting {} to {}", tarball.display(), dst.display());
+ if !dst.exists() {
+ t!(fs::create_dir_all(dst));
+ }
+
+ // `tarball` ends with `.tar.xz`; strip that suffix
+ // example: `rust-dev-nightly-x86_64-unknown-linux-gnu`
+ let uncompressed_filename =
+ Path::new(tarball.file_name().expect("missing tarball filename")).file_stem().unwrap();
+ let directory_prefix = Path::new(Path::new(uncompressed_filename).file_stem().unwrap());
+
+ // decompress the file
+ let data = t!(File::open(tarball));
+ let decompressor = XzDecoder::new(BufReader::new(data));
+
+ let mut tar = tar::Archive::new(decompressor);
+ for member in t!(tar.entries()) {
+ let mut member = t!(member);
+ let original_path = t!(member.path()).into_owned();
+ // skip the top-level directory
+ if original_path == directory_prefix {
+ continue;
+ }
+ let mut short_path = t!(original_path.strip_prefix(directory_prefix));
+ if !short_path.starts_with(pattern) {
+ continue;
+ }
+ short_path = t!(short_path.strip_prefix(pattern));
+ let dst_path = dst.join(short_path);
+ self.verbose(&format!("extracting {} to {}", original_path.display(), dst.display()));
+ if !t!(member.unpack_in(dst)) {
+ panic!("path traversal attack ??");
+ }
+ let src_path = dst.join(original_path);
+ if src_path.is_dir() && dst_path.exists() {
+ continue;
+ }
+ t!(fs::rename(src_path, dst_path));
+ }
+ t!(fs::remove_dir_all(dst.join(directory_prefix)));
+ }
+
+ /// Returns whether the SHA256 checksum of `path` matches `expected`.
+ pub(crate) fn verify(&self, path: &Path, expected: &str) -> bool {
+ use sha2::Digest;
+
+ self.verbose(&format!("verifying {}", path.display()));
+ let mut hasher = sha2::Sha256::new();
+ // FIXME: this is ok for rustfmt (4.1 MB large at time of writing), but it seems memory-intensive for rustc and larger components.
+ // Consider using streaming IO instead?
+ let contents = if self.config.dry_run { vec![] } else { t!(fs::read(path)) };
+ hasher.update(&contents);
+ let found = hex::encode(hasher.finalize().as_slice());
+ let verified = found == expected;
+ if !verified && !self.config.dry_run {
+ println!(
+ "invalid checksum: \n\
+ found: {found}\n\
+ expected: {expected}",
+ );
+ }
+ return verified;
+ }
+
/// Obtain a compiler at a given stage and for a given host. Explicitly does
/// not take `Compiler` since all `Compiler` instances are meant to be
/// obtained through this function, since it ensures that they are valid
.join("lib");
// Avoid deleting the rustlib/ directory we just copied
// (in `impl Step for Sysroot`).
- if !builder.config.download_rustc {
+ if !builder.download_rustc() {
let _ = fs::remove_dir_all(&sysroot);
t!(fs::create_dir_all(&sysroot));
}
Config::llvm_link_shared(self)
}
+ pub(crate) fn download_rustc(&self) -> bool {
+ Config::download_rustc(self)
+ }
+
+ pub(crate) fn initial_rustfmt(&self) -> Option<PathBuf> {
+ Config::initial_rustfmt(self)
+ }
+
/// Prepares an invocation of `cargo` to be run.
///
/// This will create a `Command` that represents a pending execution of
// get some support for setting `--check-cfg` within build script, it's the least invasive
// hack that still let's us have cfg checking for the vast majority of the codebase.
if stage != 0 {
- // Enable cfg checking of cargo features for everything but std.
+ // Enable cfg checking of cargo features for everything but std and also enable cfg
+ // checking of names and values.
//
// Note: `std`, `alloc` and `core` imports some dependencies by #[path] (like
- // backtrace, core_simd, std_float, ...), those dependencies have their own features
- // but cargo isn't involved in the #[path] and so cannot pass the complete list of
- // features, so for that reason we don't enable checking of features for std.
+ // backtrace, core_simd, std_float, ...), those dependencies have their own
+ // features but cargo isn't involved in the #[path] process and so cannot pass the
+ // complete list of features, so for that reason we don't enable checking of
+ // features for std crates.
+ cargo.arg(if mode != Mode::Std {
+ "-Zcheck-cfg=names,values,features"
+ } else {
+ "-Zcheck-cfg=names,values"
+ });
+
+ // Add extra cfg not defined in/by rustc
//
- // FIXME: Re-enable this after the beta bump as apperently rustc-perf doesn't use the
- // beta cargo. See https://github.com/rust-lang/rust/pull/96984#issuecomment-1126678773
- // #[cfg(not(bootstrap))]
- // if mode != Mode::Std {
- // cargo.arg("-Zcheck-cfg-features"); // -Zcheck-cfg=features after bump
- // }
-
- // Enable cfg checking of well known names/values
- rustflags
- .arg("-Zunstable-options")
- // Enable checking of well known names
- .arg("--check-cfg=names()")
- // Enable checking of well known values
- .arg("--check-cfg=values()");
-
- // Add extra cfg not defined in rustc
+ // Note: Altrough it would seems that "-Zunstable-options" to `rustflags` is useless as
+ // cargo would implicitly add it, it was discover that sometimes bootstrap only use
+ // `rustflags` without `cargo` making it required.
+ rustflags.arg("-Zunstable-options");
for (restricted_mode, name, values) in EXTRA_CHECK_CFGS {
if *restricted_mode == None || *restricted_mode == Some(mode) {
// Creating a string of the values by concatenating each value:
// this), as well as #63012 which is the tracking issue for this
// feature on the rustc side.
cargo.arg("-Zbinary-dep-depinfo");
+ match mode {
+ Mode::ToolBootstrap => {
+ // Restrict the allowed features to those passed by rustbuild, so we don't depend on nightly accidentally.
+ // HACK: because anyhow does feature detection in build.rs, we need to allow the backtrace feature too.
+ rustflags.arg("-Zallow-features=binary-dep-depinfo,backtrace");
+ }
+ Mode::ToolStd => {
+ // Right now this is just compiletest and a few other tools that build on stable.
+ // Allow them to use `feature(test)`, but nothing else.
+ rustflags.arg("-Zallow-features=binary-dep-depinfo,test,backtrace");
+ }
+ Mode::Std | Mode::Rustc | Mode::Codegen | Mode::ToolRustc => {}
+ }
cargo.arg("-j").arg(self.jobs().to_string());
// Remove make-related flags to ensure Cargo can correctly set things up
},
);
- // FIXME(davidtwco): #[cfg(not(bootstrap))] - #95612 needs to be in the bootstrap compiler
- // for this conditional to be removed.
- if !target.contains("windows") || compiler.stage >= 1 {
+ if !target.contains("windows") {
let needs_unstable_opts = target.contains("linux")
|| target.contains("windows")
|| target.contains("bsd")
stack.push(Box::new(step.clone()));
}
+ #[cfg(feature = "build-metrics")]
+ self.metrics.enter_step(&step);
+
let (out, dur) = {
let start = Instant::now();
let zero = Duration::new(0, 0);
);
}
+ #[cfg(feature = "build-metrics")]
+ self.metrics.exit_step();
+
{
let mut stack = self.stack.borrow_mut();
let cur_step = stack.pop().expect("step stack empty");