tracers_codegen/build_rs.rs
1//! This module contains the code that is used within `tracers`s build.rs` file to select
2//! the suitable tracing implementation at build time, and within a dependent crate's `build.rs`
3//! file to perform the build-time code generation to support the selected tracing implementation
4
5use crate::cargo;
6use crate::error::{TracersError, TracersResult};
7use crate::gen;
8use crate::gen::NativeLib;
9use crate::TracingImplementation;
10use failure::ResultExt;
11use serde::{Deserialize, Serialize};
12use std::env;
13use std::fs::File;
14use std::io::Write;
15use std::io::{BufReader, BufWriter};
16use std::path::{Path, PathBuf};
17
18/// Captures the features enabled for the build. There are various combinations of them which
19/// influence the logic related to what implementation is preferred
20#[derive(Debug, Clone)]
21struct FeatureFlags {
22 enable_dynamic_tracing: bool,
23 enable_static_tracing: bool,
24 force_dyn_stap: bool,
25 force_dyn_noop: bool,
26 force_static_stap: bool,
27 force_static_lttng: bool,
28 force_static_noop: bool,
29}
30
31impl FeatureFlags {
32 /// Read the feature flags from the environment variables set by Cargo at build time.
33 ///
34 /// Fails with an error if the combination of features is not valid
35 pub fn from_env() -> TracersResult<FeatureFlags> {
36 Self::new(
37 Self::is_feature_enabled("dynamic-tracing"),
38 Self::is_feature_enabled("static-tracing"),
39 Self::is_feature_enabled("force-dyn-stap"),
40 Self::is_feature_enabled("force-dyn-noop"),
41 Self::is_feature_enabled("force-static-stap"),
42 Self::is_feature_enabled("force-static-lttng"),
43 Self::is_feature_enabled("force-static-noop"),
44 )
45 }
46
47 /// Creates a feature flag structure from explicit arguments. Mostly used for testing
48 pub fn new(
49 enable_dynamic_tracing: bool,
50 enable_static_tracing: bool,
51 force_dyn_stap: bool,
52 force_dyn_noop: bool,
53 force_static_stap: bool,
54 force_static_lttng: bool,
55 force_static_noop: bool,
56 ) -> TracersResult<FeatureFlags> {
57 if enable_dynamic_tracing && enable_static_tracing {
58 return Err(TracersError::code_generation_error("The features `dynamic-tracing` and `static-tracing` are mutually exclusive; please choose one"));
59 }
60
61 if force_dyn_stap && force_dyn_noop {
62 return Err(TracersError::code_generation_error("The features `force-dyn-stap` and `force_dyn_noop` are mutually exclusive; please choose one"));
63 }
64
65 if force_static_stap && force_static_noop {
66 return Err(TracersError::code_generation_error("The features `force-static-stap` and `force_static_noop` are mutually exclusive; please choose one"));
67 }
68
69 if force_static_lttng && force_static_noop {
70 return Err(TracersError::code_generation_error("The features `force-static-lttng` and `force_static_noop` are mutually exclusive; please choose one"));
71 }
72
73 Ok(FeatureFlags {
74 enable_dynamic_tracing,
75 enable_static_tracing,
76 force_dyn_stap,
77 force_dyn_noop,
78 force_static_stap,
79 force_static_lttng,
80 force_static_noop,
81 })
82 }
83
84 pub fn enable_tracing(&self) -> bool {
85 self.enable_dynamic() || self.enable_static()
86 }
87
88 pub fn enable_dynamic(&self) -> bool {
89 self.enable_dynamic_tracing || self.force_dyn_noop || self.force_dyn_stap
90 }
91
92 pub fn enable_static(&self) -> bool {
93 self.enable_static_tracing
94 }
95
96 pub fn force_dyn_stap(&self) -> bool {
97 //Should the dynamic stap be required on pain of build failure?
98 self.force_dyn_stap
99 }
100
101 pub fn force_dyn_noop(&self) -> bool {
102 //Should the dynamic stap be required on pain of build failure?
103 self.force_dyn_noop
104 }
105
106 pub fn force_static_stap(&self) -> bool {
107 //Should the staticamic stap be required on pain of build failure?
108 self.force_static_stap
109 }
110
111 pub fn force_static_lttng(&self) -> bool {
112 //Should the static lttng be required on pain of build failure?
113 self.force_static_lttng
114 }
115
116 fn is_feature_enabled(name: &str) -> bool {
117 env::var(&format!(
118 "CARGO_FEATURE_{}",
119 name.to_uppercase().replace("-", "_")
120 ))
121 .is_ok()
122 }
123}
124
125/// Serializable struct which is populated in `build.rs` to indicate to the proc macros which
126/// tracing implementation they should use.
127#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
128pub(crate) struct BuildInfo {
129 pub package_name: String,
130 pub implementation: TracingImplementation,
131}
132
133impl BuildInfo {
134 pub fn new(package_name: String, implementation: TracingImplementation) -> BuildInfo {
135 BuildInfo {
136 package_name,
137 implementation,
138 }
139 }
140
141 pub fn load() -> TracersResult<BuildInfo> {
142 let path = Self::get_build_path()?;
143
144 let file = File::open(&path)
145 .map_err(|e| TracersError::build_info_read_error(path.clone(), e.into()))?;
146 let reader = BufReader::new(file);
147
148 serde_json::from_reader(reader)
149 .map_err(|e| TracersError::build_info_read_error(path.clone(), e.into()))
150 }
151
152 pub fn save(&self) -> TracersResult<PathBuf> {
153 let path = Self::get_build_path()?;
154
155 //Make sure the directory exists
156 path.parent()
157 .map(|p| {
158 std::fs::create_dir_all(p)
159 .map_err(|e| TracersError::build_info_write_error(path.clone(), e.into()))
160 })
161 .unwrap_or(Ok(()))?;
162
163 let file = File::create(&path)
164 .map_err(|e| TracersError::build_info_write_error(path.clone(), e.into()))?;
165 let writer = BufWriter::new(file);
166 serde_json::to_writer(writer, self)
167 .map_err(|e| TracersError::build_info_write_error(path.clone(), e.into()))?;
168
169 Ok(path)
170 }
171
172 fn get_build_path() -> TracersResult<PathBuf> {
173 //HACK: This is...not the most elegant solution. This code gets used in three contexts:
174 //
175 //1. When `tracers` itself is being built, its `build.rs` calls into `tracers-build` to
176 // decide which probing implementation to use based on the feature flags specified by the
177 // caller. In that case, `OUT_DIR` is set by `cargo` to the out directory for the
178 // `tracers` crate. In that situation the `BuildInfo` file should go somewhere in
179 // `$OUT_DIR`, in a subdirectory named with the `tracers` package name and version
180 //
181 //2. When some other crate is using `tracers`, its `build.rs` calls into `tracers-build` to
182 // perform the build-time code generation tasks, which first require knowing which
183 // implementation `tracers` is using, hence requires reading the `BuildInfo` file. In
184 // this case, `$OUT_DIR` is set to that dependent crate's output directory and is not
185 // what we want. We want to know the path to the `BuildInfo` file produced when
186 // `tracers` was built. Fortunately this information is passed on to the dependent crate
187 // by `tracers`s `build.rs` in the form of a cargo variable which cargo propagates to
188 // dependent crates as `DEP_TRACERS_BUILD_INFO_PATH`
189 //
190 //3. When one of the proc macros is invoked during compilation of some crate which is using
191 // `tracers`. In this case, the other crate's `build.rs` should have already been run,
192 // and it should have called into the `tracers_build::build()` (that call is what creates
193 // context #2 above). `tracers_build::build()` will instruct Cargo to set an environment
194 // variable `TRACERS_BUILD_INFO_PATH` during compilation, which this method will then
195 // example in order to get the path of the `BuidlInfo` build.
196 //
197 //I'm not proud of this code. But `tracers` pushes the Rust build system just about to the
198 //breaking point as it is. We're lucky it's possible at all, hacks notwithstanding
199 if "tracers" == env::var("CARGO_PKG_NAME").ok().unwrap_or_default() {
200 //This is context #1 in the comment above: we're being called from within the `tracers`
201 //build.rs
202 let rel_path = PathBuf::from(&format!(
203 "{}-{}/buildinfo.json",
204 env::var("CARGO_PKG_NAME").context("CARGO_PKG_NAME")?,
205 env::var("CARGO_PKG_VERSION").context("CARGO_PKG_VERSION")?
206 ));
207
208 Ok(PathBuf::from(env::var("OUT_DIR").context("OUT_DIR")?).join(rel_path))
209 } else if let Ok(build_info_path) = env::var("DEP_TRACERS_BUILD_INFO_PATH") {
210 //This is context #2 in the comment above
211 Ok(PathBuf::from(build_info_path))
212 } else if let Ok(build_info_path) = env::var("TRACERS_BUILD_INFO_PATH") {
213 //This is context #3 in the comment above
214 Ok(PathBuf::from(build_info_path))
215 } else {
216 //Since the first context happens in the `tracers` code itself and we know it's
217 //implemented correctly, it means that this is either context #2 or #3 and the caller
218 //did something wrong. Most likely they forgot to add the call to
219 //`tracers_build::build()` to their `build.rs`. Since this is an easy mistake to make
220 //we want an ergonomic error message here
221 Err(TracersError::missing_call_in_build_rs())
222 }
223 }
224}
225
226/// Called from the `build.rs` of all crates which have a direct dependency on `tracers` and
227/// `tracers_macros`. This determines the compile-time configuration of the `tracers` crate, and
228/// performs any build-time code generation necessary to support the code generated by the
229/// `tracers_macros` macros.
230///
231/// It should be the first line in the `main()` function, etc:
232///
233/// ```no_execute
234/// // build.rs
235/// use tracers_build::build;
236///
237/// fn main() {
238/// build();
239///
240/// //....
241/// }
242/// ```
243pub fn build() {
244 let mut stdout = std::io::stdout();
245 let mut stderr = std::io::stderr();
246
247 match build_internal(&mut stdout) {
248 Ok(_) => writeln!(stdout, "probes build succeeded").unwrap(),
249 Err(e) => {
250 //An error that propagates all the way up to here is serious enough that it means we
251 //cannot proceed. Fail the build by exiting the process forcefully
252 writeln!(stderr, "Error building probes: {}", e).unwrap();
253
254 std::process::exit(-1);
255 }
256 };
257}
258
259fn build_internal<OUT: Write>(out: &mut OUT) -> TracersResult<()> {
260 //First things first; get the BuildInfo from the `tracers` build, and tell Cargo to make that
261 //available to the proc macros at compile time via an environment variable
262 let build_info_path = BuildInfo::get_build_path()?;
263 writeln!(
264 out,
265 "cargo:rustc-env=TRACERS_BUILD_INFO_PATH={}",
266 build_info_path.display()
267 )
268 .unwrap();
269
270 generate_native_code(out)
271}
272
273/// This function is the counterpart to `build`, which is intended to be invoked in the `tracers`
274/// `build.rs` script. It reads the feature flags enabled on `tracers`, and from those flags and
275/// other information about the target sytem and the local build environment selects an
276/// implementation to use, or panics if no suitable implementation is possible
277pub fn tracers_build() {
278 let mut stdout = std::io::stdout();
279 let mut stderr = std::io::stderr();
280
281 let features = FeatureFlags::from_env().expect("Invalid feature flags");
282
283 match tracers_build_internal(&mut stdout, features) {
284 Ok(_) => {}
285 Err(e) => {
286 //failure here doesn't just mean one of the tracing impls failed to compile; when that
287 //happens we can always fall back to the no-op impl. This means something happened
288 //which prevents us from proceeding with the build
289 writeln!(stderr, "{}", e).unwrap();
290 panic!("tracers build failed: {}", e);
291 }
292 }
293}
294
295fn tracers_build_internal<OUT: Write>(out: &mut OUT, features: FeatureFlags) -> TracersResult<()> {
296 writeln!(out, "Detected features: \n{:?}", features).unwrap();
297
298 select_implementation(&features).map(|implementation| {
299 // Some implementation was selected, but it's possible that the selected
300 // "implementation" is to completely disable tracing. If that's not the case, set the
301 // appropriate features for the compiler to use when compiling the `tracers` code.
302 if implementation.is_enabled() {
303 writeln!(out, "cargo:rustc-cfg=enabled").unwrap();
304 writeln!(out,
305 "cargo:rustc-cfg={}_enabled",
306 if implementation.is_static() {
307 "static"
308 } else {
309 "dynamic"
310 }
311 ).unwrap(); //this category of tracing is enabled
312 writeln!(out, "cargo:rustc-cfg={}_enabled", implementation.as_ref()).unwrap(); //this specific impl is enabled
313 }
314
315 //All downstream creates from `tracers` will just call `tracers_build::build`, but this
316 //is a special case because we've already decided above which implementation to use.
317 //
318 //This decision needs to be saved to the OUT_DIR somewhere, so that all of our tests,
319 //examples, binaries, and benchmarks which use the proc macros will be able to generate
320 //the correct runtime tracing code to match the implementation we've chosen here
321 let build_info = BuildInfo::new(env::var("CARGO_PKG_NAME").expect("CARGO_PKG_NAME"), implementation);
322 match build_info.save() {
323 Ok(build_info_path) => {
324 //The above statements set compile-time features to the compiler knows which modules to
325 //include. The below will set environment variables DEP_TRACERS_(VARNAME) in dependent
326 //builds
327 //
328 //The codegen stuff in `tracers_build::build` will use this to determine what code
329 //generator to use
330 writeln!(out, "cargo:build-info-path={}", build_info_path.display()).unwrap();
331 }
332 Err(e) => {
333 writeln!(out, "cargo:warning=Error saving build info file; some targets may fail to build. Error details: {}", e).unwrap();
334 }
335 }
336 })?;
337
338 //Generate native code for the `tracers` crate. Nothing in the actual `tracers`
339 //library code contains any `#[tracer]` traits, but the tests and examples do, so if we
340 //want them to work propertly we need to run codegen for them just like on any other
341 //crate
342 generate_native_code(out)
343}
344
345/// Selects a `tracers` implementation given a set of feature flags specified by the user
346fn select_implementation(features: &FeatureFlags) -> TracersResult<TracingImplementation> {
347 if !features.enable_tracing() {
348 return Ok(TracingImplementation::Disabled);
349 }
350
351 //If any implementation is forced, then see if it's available and if so then accept it
352 if features.enable_dynamic() {
353 // Pick some dynamic tracing impl
354 if features.force_dyn_stap() {
355 if env::var("DEP_TRACERS_DYN_STAP_SUCCEEDED").is_err() {
356 return Err(TracersError::code_generation_error(
357 "force-dyn-stap is enabled but the dyn_stap library is not available",
358 ));
359 } else {
360 return Ok(TracingImplementation::DynamicStap);
361 }
362 } else if features.force_dyn_noop() {
363 //no-op is always available on all platforms
364 return Ok(TracingImplementation::DynamicNoOp);
365 }
366
367 //Else no tracing impl has been forced so we get to decide
368 if env::var("DEP_TRACERS_DYN_STAP_SUCCEEDED").is_ok() {
369 //use dyn_stap when it savailable
370 Ok(TracingImplementation::DynamicStap)
371 } else {
372 //else, fall back to noop
373 Ok(TracingImplementation::DynamicNoOp)
374 }
375 } else {
376 // Pick some static tracing impl
377 assert!(features.enable_static());
378
379 //TODO: Be a bit smarter about this
380 if features.force_static_stap() {
381 Ok(TracingImplementation::StaticStap)
382 } else if features.force_static_lttng() {
383 Ok(TracingImplementation::StaticLttng)
384 } else {
385 Ok(TracingImplementation::StaticNoOp)
386 }
387 }
388}
389
390fn generate_native_code(out: &mut dyn Write) -> TracersResult<()> {
391 let manifest_dir = env::var("CARGO_MANIFEST_DIR").context(
392 "CARGO_MANIFEST_DIR is not set; are you sure you're calling this from within build.rs?",
393 )?;
394
395 let manifest_path = PathBuf::from(manifest_dir).join("Cargo.toml");
396 let package_name = env::var("CARGO_PKG_NAME").unwrap();
397 let targets = cargo::get_targets(&manifest_path, &package_name).context("get_targets")?;
398 let out_path = &PathBuf::from(env::var("OUT_DIR").context("OUT_DIR")?);
399
400 let mut native_libs = gen::code_generator()?.generate_native_code(
401 out,
402 &Path::new(&manifest_path),
403 &out_path,
404 &package_name,
405 targets,
406 );
407
408 //There is usually some repetition when multiple providers are generated. Filter that out for
409 //better build performance
410 native_libs.sort_by(|a, b| a.partial_cmp(b).unwrap());
411 native_libs.dedup();
412
413 //Scan through all of the native libs output and send the info to cargo as applicable
414 for native_lib in native_libs.into_iter() {
415 match native_lib {
416 NativeLib::StaticWrapperLib(_) => {
417 //This is the name of a generated native wrapper. Ignore it here; the `tracers`
418 //attribute macro will use this to generate a `link` attribute at the actual call
419 //site
420 }
421 NativeLib::StaticWrapperLibPath(path) | NativeLib::SupportLibPath(path) => {
422 //This is the path to a directory where either the static wrapper or a support lib
423 //will be found. Make sure cargo adds that to the library path
424 println!("cargo:rustc-link-search=native={}", path.display());
425 }
426 NativeLib::DynamicSupportLib(lib) => {
427 //This is a dynamically-linked support library which should be linked exactly once
428 //for the crate, to support any number of generated native wrappers
429 println!("cargo:rustc-link-lib=dylib={}", lib);
430 }
431 NativeLib::StaticSupportLib(lib) => {
432 //This is a statically-linked support library which should be linked exactly once
433 //for the crate, to support any number of generated native wrappers
434 println!("cargo:rustc-link-lib=static={}", lib);
435 }
436 };
437 }
438
439 Ok(())
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445 use crate::testdata;
446 use crate::TracingType;
447
448 #[test]
449 #[should_panic]
450 fn tracers_build_panics_invalid_features() {
451 //These two feature flags are mutually exclusive
452 let guard = testdata::with_env_vars(vec![
453 ("CARGO_FEATURE_STATIC_TRACING", "1"),
454 ("CARGO_FEATURE_DYNAMIC_TRACING", "1"),
455 ]);
456
457 tracers_build();
458
459 drop(guard);
460 }
461
462 #[test]
463 fn build_rs_workflow_tests() {
464 // Simulates the entire process, starting with `tracers_build` choosing an implementation
465 // based on the selected feature flags, then the dependent crate calling `build` to query
466 // the build info generated by `tracers_build` and to perform perform
467 // pre-processing of its code, then the proc macros reading the build info persisted by
468 // `build` to generate the right implementation.
469 //
470 // This doesn't actually integrate all those systems in a test, but it simulates the
471 // relevant calls into the `build_rs` code
472 let test_cases = vec![
473 //features, expected_impl
474 (
475 // Tracing disabled entirely
476 FeatureFlags::new(false, false, false, false, false, false, false).unwrap(),
477 TracingImplementation::Disabled,
478 ),
479 (
480 // Tracing enabled, dynamic mode enabled with auto-detect, static disabled
481 FeatureFlags::new(true, false, false, false, false, false, false).unwrap(),
482 TracingImplementation::DynamicNoOp,
483 ),
484 (
485 // Tracing enabled, dynamic disabled, static enabled with auto-detect
486 FeatureFlags::new(false, true, false, false, false, false, false).unwrap(),
487 TracingImplementation::StaticNoOp,
488 ),
489 (
490 // Tracing enabled, dynamic disabled, static enabled with force-static-noop
491 FeatureFlags::new(false, true, false, false, false, false, true).unwrap(),
492 TracingImplementation::StaticNoOp,
493 ),
494 (
495 // Tracing enabled, dynamic disabled, static enabled with force-static-stap
496 FeatureFlags::new(false, true, false, false, true, false, false).unwrap(),
497 TracingImplementation::StaticStap,
498 ),
499 (
500 // Tracing enabled, dynamic disabled, static enabled with force-static-lttng
501 FeatureFlags::new(false, true, false, false, false, true, false).unwrap(),
502 TracingImplementation::StaticLttng,
503 ),
504 ];
505
506 let temp_dir = tempfile::tempdir().unwrap();
507 let manifest_dir = env!("CARGO_MANIFEST_DIR");
508 let out_dir = temp_dir.path().join("out");
509
510 for (features, expected_impl) in test_cases.into_iter() {
511 let context = format!(
512 "features: {:?}\nexpected_impl: {}",
513 features,
514 expected_impl.as_ref()
515 );
516
517 //First let's pretend we're in `tracers/build.rs`, and cargo has set the relevant env
518 //vars
519 let guard = testdata::with_env_vars(vec![
520 ("CARGO_PKG_NAME", "tracers"),
521 ("CARGO_PKG_VERSION", "1.2.3"),
522 ("CARGO_MANIFEST_DIR", manifest_dir),
523 ("OUT_DIR", out_dir.to_str().unwrap()),
524 ]);
525
526 let mut stdout = Vec::new();
527
528 tracers_build_internal(&mut stdout, features.clone())
529 .expect(&format!("Unexpected failure with features: {:?}", features));
530
531 //That worked. The resulting build info should have been written out
532 let build_info_path = BuildInfo::get_build_path().unwrap();
533
534 let step1_build_info = BuildInfo::load().expect(&format!(
535 "Failed to load build info for features: {:?}",
536 features
537 ));
538
539 assert_eq!(
540 expected_impl, step1_build_info.implementation,
541 "context: {}",
542 context
543 );
544
545 //And the path to this should have been written to stdout such that cargo will treat it
546 //as a variable that is passed to dependent crates' `build.rs`:
547 let output = String::from_utf8(stdout).unwrap();
548 assert!(
549 output.contains(&format!(
550 "cargo:build-info-path={}",
551 build_info_path.display()
552 )),
553 context
554 );
555
556 //and the features used to compile `tracers` should correspond to the implementation
557 match expected_impl.tracing_type() {
558 TracingType::Disabled => assert!(!output.contains("enabled")),
559 TracingType::Dynamic => assert!(output.contains("cargo:rustc-cfg=dynamic_enabled")),
560 TracingType::Static => assert!(output.contains("cargo:rustc-cfg=static_enabled")),
561 }
562
563 if expected_impl.is_enabled() {
564 assert!(
565 output.contains(&format!(
566 "cargo:rustc-cfg={}_enabled",
567 expected_impl.as_ref()
568 )),
569 context
570 );
571 }
572
573 //Next, the user crate's `build.rs` will want to know what the selected impl was
574 drop(guard);
575 let guard = testdata::with_env_vars(vec![
576 (
577 "DEP_TRACERS_BUILD_INFO_PATH",
578 build_info_path.to_str().unwrap(),
579 ),
580 ("OUT_DIR", out_dir.to_str().unwrap()),
581 ]);
582
583 let step2_build_info = BuildInfo::load().expect(&format!(
584 "Failed to load build info for features: {:?}",
585 features
586 ));
587
588 assert_eq!(step1_build_info, step2_build_info, "context: {}", context);
589 drop(guard);
590
591 //At this point in the process if this were a real build, the `build.rs` code would be
592 //generating code for a real crate. We're not going to simulate all of that here,
593 //however we can invoke the code gen for all of our test crates at this point, and the
594 //code gen should work using the currently selected implementatoin
595 for test_case in testdata::TEST_CRATES.iter() {
596 let context = format!(
597 "features: {:?} test_case: {}",
598 features,
599 test_case.root_directory.display()
600 );
601
602 let mut stdout = Vec::new();
603
604 let guard = testdata::with_env_vars(vec![
605 (
606 "DEP_TRACERS_BUILD_INFO_PATH",
607 build_info_path.to_str().unwrap(),
608 ),
609 ("OUT_DIR", out_dir.to_str().unwrap()),
610 ("CARGO_PKG_NAME", test_case.package_name),
611 (
612 "CARGO_MANIFEST_DIR",
613 test_case.root_directory.to_str().unwrap(),
614 ),
615 ("TARGET", "x86_64-linux-gnu"),
616 ("HOST", "x86_64-linux-gnu"),
617 ("OPT_LEVEL", "1"),
618 ]);
619
620 build_internal(&mut stdout).expect(&context);
621
622 //After the build, it should output something on stdout to tell Cargo to set a
623 //compiler-visible env var telling the proc macros where the `BuildInfo` file is
624 let output = String::from_utf8(stdout).unwrap();
625 assert!(output.contains(&format!(
626 "cargo:rustc-env=TRACERS_BUILD_INFO_PATH={}",
627 build_info_path.display()
628 )));
629
630 drop(guard);
631 }
632
633 //That worked, next the proc macros will be run by `rustc` while it builds the user
634 //crate.
635 let guard = testdata::with_env_vars(vec![(
636 "TRACERS_BUILD_INFO_PATH",
637 build_info_path.to_str().unwrap(),
638 )]);
639
640 let step3_build_info = BuildInfo::load().expect(&format!(
641 "Failed to load build info for features: {:?}",
642 features
643 ));
644
645 assert_eq!(step1_build_info, step3_build_info, "context: {}", context);
646
647 drop(guard);
648 }
649 }
650}