diff --git a/crates/term-transcript-cli/tests/e2e.rs b/crates/term-transcript-cli/tests/e2e.rs index 82d381a7..20988793 100644 --- a/crates/term-transcript-cli/tests/e2e.rs +++ b/crates/term-transcript-cli/tests/e2e.rs @@ -34,7 +34,7 @@ fn test_config() -> (TestConfig, TempDir) { // Switch off logging if `RUST_LOG` is set in the surrounding env .with_env("RUST_LOG", "off") .with_current_dir(temp_dir.path()) - .with_cargo_path() + .with_cargo_path_for("term-transcript") .with_additional_path(rainbow_dir) .with_io_timeout(Duration::from_secs(2)); let config = TestConfig::new(shell_options).with_match_kind(MatchKind::Precise); @@ -58,7 +58,8 @@ fn scrolled_template() -> Template { fn help_example() { use term_transcript::PtyCommand; - let shell_options = ShellOptions::new(PtyCommand::default()).with_cargo_path(); + let shell_options = + ShellOptions::new(PtyCommand::default()).with_cargo_path_for("term-transcript"); TestConfig::new(shell_options).test(svg_snapshot("help"), ["term-transcript --help"]); } diff --git a/crates/term-transcript/CHANGELOG.md b/crates/term-transcript/CHANGELOG.md index b8f55a9d..2d3e8625 100644 --- a/crates/term-transcript/CHANGELOG.md +++ b/crates/term-transcript/CHANGELOG.md @@ -9,6 +9,10 @@ The project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html) - Bump minimum supported Rust version to 1.86. +### Fixed + +- Rework `ShellOptions::with_cargo_path()` to work with custom target directories. + ## 0.5.0-beta.1 - 2026-02-04 ### Added @@ -104,7 +108,7 @@ The project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html) As an example, this can be used to import fonts using `@import` or `@font-face`. - Add a fallback error message to the default template if HTML-in-SVG embedding is not supported. -- Add [FAQ](../FAQ.md) with some tips and troubleshooting advice. +- Add a FAQ with some tips and troubleshooting advice. - Allow hiding `UserInput`s during transcript rendering by calling the `hide()` method. Hidden inputs are supported by the default and pure SVG templates. diff --git a/crates/term-transcript/src/shell/mod.rs b/crates/term-transcript/src/shell/mod.rs index 8fd06bc9..3fb8b280 100644 --- a/crates/term-transcript/src/shell/mod.rs +++ b/crates/term-transcript/src/shell/mod.rs @@ -263,25 +263,77 @@ impl ShellOptions { path } - /// Adds paths to cargo binaries (including examples) to the `PATH` env variable - /// for the shell described by these options. - /// This allows to call them by the corresponding filename, without specifying a path + #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", ret))] + fn legacy_cargo_path(binary_name: &str) -> Option { + let target_path = Self::target_path(); + let binary_path = target_path.join(format!("{binary_name}{}", env::consts::EXE_SUFFIX)); + let exists = binary_path.try_exists(); + + #[cfg(feature = "tracing")] + tracing::debug!(?binary_path, ?exists, "checked binary path"); + exists.ok()?.then_some(binary_path) + } + + fn panic_on_missing_cargo_path(binary_name: &str) -> ! { + let binaries: Vec<_> = env::vars_os() + .filter_map(|(name, _)| { + let name = name.into_string().ok()?; + Some(name.strip_prefix("CARGO_BIN_EXE_")?.to_owned()) + }) + .collect(); + if binaries.is_empty() { + panic!( + "`CARGO_BIN_EXE_{binary_name}` env variable is unset, and {binary_name} is not in the default cargo target dir.\n\ + help: If this is run in a unit test, move it to an integration test to gain access to `CARGO_BIN_EXE_` vars (requires Rust 1.94+)" + ); + } else { + panic!( + "`{binary_name}` does not look like a valid cargo binary in the workspace.\n\ + help: Available binaries: {binaries:?}" + ); + } + } + + /// Adds paths to a cargo binary to the `PATH` env variable for the shell described by these options. + /// This allows to call the binary by the corresponding filename, without specifying a path /// or doing complex preparations (e.g., calling `cargo install`). /// /// # Limitations /// /// - The caller must be a unit or integration test; the method will work improperly otherwise. + /// - Does not work in Rust 1.91, 1.92, 1.93 with a non-default `build.build-dir`. + #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self)))] #[must_use] - pub fn with_cargo_path(mut self) -> Self { - let target_path = Self::target_path(); - self.path_additions.push(target_path.join("examples")); - self.path_additions.push(target_path); + #[allow(clippy::missing_panics_doc)] // should never be triggered + pub fn with_cargo_path_for(mut self, binary_name: &str) -> Self { + let env_var_name = format!("CARGO_BIN_EXE_{binary_name}"); + let binary_path = env::var_os(&env_var_name).map(PathBuf::from); + + #[cfg(feature = "tracing")] + tracing::debug!(?binary_path, "got Rust 1.94+ path to binary"); + + let binary_path = binary_path + .or_else(|| Self::legacy_cargo_path(binary_name)) + .unwrap_or_else(|| Self::panic_on_missing_cargo_path(binary_name)); + + #[cfg(feature = "tracing")] + tracing::debug!(?binary_path, "got path to binary"); + + let parent_path = binary_path + .parent() + .expect("invalid binary path") + .to_owned(); + // The check is inefficient, but we shouldn't have many additional paths. + if !self.path_additions.contains(&parent_path) { + self.path_additions.push(parent_path); + } + self } /// Adds a specified path to the `PATH` env variable for the shell described by these options. /// This method can be called multiple times to add multiple paths and is composable - /// with [`Self::with_cargo_path()`]. + /// with [`Self::with_cargo_path_for()`]. #[must_use] pub fn with_additional_path(mut self, path: impl Into) -> Self { let path = path.into(); diff --git a/crates/term-transcript/src/shell/standard.rs b/crates/term-transcript/src/shell/standard.rs index 52006a20..42291947 100644 --- a/crates/term-transcript/src/shell/standard.rs +++ b/crates/term-transcript/src/shell/standard.rs @@ -94,7 +94,7 @@ impl ShellOptions { /// Creates an alias for the binary at `path_to_bin`, which should be an absolute path. /// This allows to call the binary using this alias without complex preparations (such as /// installing it globally via `cargo install`), and is more flexible than - /// [`Self::with_cargo_path()`]. + /// [`Self::with_cargo_path_for()`]. /// /// In integration tests, you may use [`env!("CARGO_BIN_EXE_")`] to get a path /// to binary targets. diff --git a/crates/term-transcript/src/test/mod.rs b/crates/term-transcript/src/test/mod.rs index 39c8d9ba..4b2bfd54 100644 --- a/crates/term-transcript/src/test/mod.rs +++ b/crates/term-transcript/src/test/mod.rs @@ -13,7 +13,8 @@ //! //! // Test configuration that can be shared across tests. //! fn config() -> TestConfig { -//! let shell_options = ShellOptions::default().with_cargo_path(); +//! let shell_options = ShellOptions::default() +//! .with_cargo_path_for("my-command"); //! TestConfig::new(shell_options) //! .with_match_kind(MatchKind::Precise) //! .with_output(TestOutputConfig::Verbose) diff --git a/crates/term-transcript/tests/integration.rs b/crates/term-transcript/tests/integration.rs index 9f61d5e0..4a19f2fd 100644 --- a/crates/term-transcript/tests/integration.rs +++ b/crates/term-transcript/tests/integration.rs @@ -162,9 +162,7 @@ fn transcript_with_empty_output(mute_outputs: &[bool], pure_svg: bool) -> anyhow } }); - let mut shell_options = ShellOptions::default() - .with_cargo_path() - .with_io_timeout(Duration::from_millis(200)); + let mut shell_options = ShellOptions::default().with_io_timeout(Duration::from_millis(200)); let transcript = Transcript::from_inputs(&mut shell_options, inputs)?; assert_tracing_for_transcript_from_inputs(&tracing_storage.lock());