Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,17 @@ doctor: _msvc-bootstrap
fi
echo ""

echo "Developer tooling:"
if RG_VER=$(rg --version 2>/dev/null | head -1); then
ok "ripgrep" "${RG_VER:-installed}"
else
fail "ripgrep" "not found (run: pecos install ripgrep)"
echo " Manual install options: cargo install ripgrep --locked, brew install ripgrep,"
echo " apt install ripgrep, or winget install BurntSushi.ripgrep"
echo " https://github.com/BurntSushi/ripgrep#installation"
fi
echo ""

if [ "$PROBLEMS" -eq 0 ]; then
echo "No problems found."
else
Expand Down
25 changes: 6 additions & 19 deletions crates/pecos-build/src/cmake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ pub fn find_system_cmake() -> Option<PathBuf> {
if !output.status.success() {
return None;
}
which_in_path("cmake")
let extensions: &[&str] = if cfg!(windows) {
&[".exe", ".bat", ""]
} else {
&[""]
};
crate::executable::which_in_path("cmake", extensions)
}

/// Directory containing the cmake binary for a given installation root.
Expand All @@ -64,24 +69,6 @@ pub fn cmake_binary_in(root: &Path) -> Option<PathBuf> {
candidate.is_file().then_some(candidate)
}

fn which_in_path(name: &str) -> Option<PathBuf> {
let path_var = std::env::var_os("PATH")?;
let exts: &[&str] = if cfg!(windows) {
&[".exe", ".bat", ""]
} else {
&[""]
};
for dir in std::env::split_paths(&path_var) {
for ext in exts {
let candidate = dir.join(format!("{name}{ext}"));
if candidate.is_file() {
return Some(candidate);
}
}
}
None
}

/// The docs URL we point users at for manual install instructions.
pub const DOCS_URL: &str =
"https://github.com/PECOS-packages/PECOS/blob/dev/docs/user-guide/cmake-setup.md";
32 changes: 32 additions & 0 deletions crates/pecos-build/src/executable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2026 The PECOS Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Executable lookup shared by external-tool detectors.

use std::path::PathBuf;

/// Resolve an executable via `PATH` using the caller's platform suffix policy.
#[must_use]
pub(crate) fn which_in_path(name: &str, extensions: &[&str]) -> Option<PathBuf> {
let path_var = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path_var) {
for ext in extensions {
let candidate = dir.join(format!("{name}{ext}"));
if candidate.is_file() {
return Some(candidate);
}
}
}
None
}
2 changes: 2 additions & 0 deletions crates/pecos-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,13 @@ pub mod cutensor;
pub mod deps;
pub mod download;
pub mod errors;
mod executable;
pub mod extract;
pub mod home;
pub mod llvm;
pub mod manifest;
pub mod prompt;
pub mod ripgrep;

// Re-export main types for convenience
pub use deps::ensure_dep_ready;
Expand Down
11 changes: 2 additions & 9 deletions crates/pecos-build/src/llvm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,8 @@ pub struct PathToolReport {
/// (and external tools like Selene) find it.
#[must_use]
pub fn which_on_path(exe_name: &str) -> Option<PathBuf> {
let exe = if cfg!(windows) {
format!("{exe_name}.exe")
} else {
exe_name.to_string()
};
let paths = std::env::var_os("PATH")?;
std::env::split_paths(&paths)
.map(|dir| dir.join(&exe))
.find(|candidate| candidate.is_file())
let extensions: &[&str] = if cfg!(windows) { &[".exe"] } else { &[""] };
crate::executable::which_in_path(exe_name, extensions)
}

/// Parse an LLVM `--version` output into a bare `X.Y.Z` version string.
Expand Down
37 changes: 32 additions & 5 deletions crates/pecos-build/src/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::io::{self, BufRead, IsTerminal, Write};
/// How to resolve prompts: interactively, or with a forced answer.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PromptMode {
/// Ask the user interactively (falls back to default if not a TTY).
/// Ask the user interactively (declines if stdin is not a TTY).
Interactive,
/// Accept all prompts without asking.
AcceptAll,
Expand All @@ -22,18 +22,29 @@ pub enum PromptMode {
/// - `default_yes`: Whether the default answer is yes (`[Y/n]`) or no (`[y/N]`)
/// - `mode`: How to resolve the prompt
///
/// In `Interactive` mode, returns the default if stdin is not a TTY (e.g. piped input, CI).
/// In `Interactive` mode, declines if stdin is not a TTY (e.g. piped input, CI).
#[must_use]
pub fn confirm(message: &str, default_yes: bool, mode: PromptMode) -> bool {
confirm_with_terminal(message, default_yes, mode, io::stdin().is_terminal())
}

fn confirm_with_terminal(
message: &str,
default_yes: bool,
mode: PromptMode,
stdin_is_terminal: bool,
) -> bool {
match mode {
PromptMode::AcceptAll => return true,
PromptMode::DeclineAll => return false,
PromptMode::Interactive => {}
}

// Non-interactive environment -> use default silently
if !io::stdin().is_terminal() {
return default_yes;
if !stdin_is_terminal {
println!(
"Prompt auto-declined because stdin is non-interactive; pass `--yes` to accept prompts non-interactively."
);
return false;
}

let hint = if default_yes { "[Y/n]" } else { "[y/N]" };
Expand Down Expand Up @@ -67,4 +78,20 @@ mod tests {
assert!(!confirm("test?", false, PromptMode::DeclineAll));
assert!(!confirm("test?", true, PromptMode::DeclineAll));
}

#[test]
fn non_tty_interactive_declines_regardless_of_default() {
assert!(!confirm_with_terminal(
"test?",
false,
PromptMode::Interactive,
false
));
assert!(!confirm_with_terminal(
"test?",
true,
PromptMode::Interactive,
false
));
}
}
58 changes: 58 additions & 0 deletions crates/pecos-build/src/ripgrep.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2026 The PECOS Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! ripgrep detection and installation.

use crate::errors::{Error, Result};
use std::path::PathBuf;
use std::process::Command;

/// The docs URL we point users at for manual install instructions.
pub const DOCS_URL: &str = "https://github.com/BurntSushi/ripgrep#installation";

/// Find a usable `rg` on the system `PATH`.
#[must_use]
pub fn find_ripgrep() -> Option<PathBuf> {
let output = Command::new("rg").arg("--version").output().ok()?;
if !output.status.success() {
return None;
}
let extensions: &[&str] = if cfg!(windows) {
&[".exe", ".bat", ""]
} else {
&[""]
};
crate::executable::which_in_path("rg", extensions)
}

/// Install ripgrep with Cargo, streaming build output to the terminal.
///
/// # Errors
///
/// Returns an error if Cargo cannot be started or the installation fails.
pub fn install_ripgrep(force: bool) -> Result<()> {
let mut command = Command::new("cargo");
command.args(["install", "ripgrep", "--locked"]);
if force {
command.arg("--force");
}

let status = command.status()?;
if !status.success() {
return Err(Error::Config(format!(
"`cargo install ripgrep --locked` failed with {status}"
)));
}
Ok(())
}
17 changes: 15 additions & 2 deletions crates/pecos-cli/src/cli/install_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use pecos_build::errors::Error;
use pecos_build::prompt::{PromptMode, confirm};

/// Known installable targets
const KNOWN_TARGETS: &[&str] = &["cuda", "llvm", "cuquantum", "cmake"];
const KNOWN_TARGETS: &[&str] = &["cuda", "llvm", "cuquantum", "cmake", "ripgrep"];

/// Run the install command
pub fn run(
Expand Down Expand Up @@ -47,7 +47,7 @@ pub fn run(
.is_some_and(|p| p.to_string_lossy().contains(".pecos/deps/"));

if let Some(path) = existing.as_ref().filter(|_| !force) {
if is_local {
if is_local || !has_managed_install(target) {
println!(
"[{}/{}] {target}: already installed at {}",
i + 1,
Expand Down Expand Up @@ -89,13 +89,23 @@ pub fn run(
Ok(())
}

/// Whether PECOS can install a private copy under `~/.pecos/deps/`.
fn has_managed_install(target: &str) -> bool {
match target {
"cuda" | "llvm" | "cuquantum" | "cmake" => true,
"ripgrep" => false,
_ => unreachable!("target was validated above"),
}
}

/// Find where a target is currently installed (if at all)
fn find_existing(target: &str) -> Option<std::path::PathBuf> {
match target {
"cuda" => pecos_build::cuda::find_cuda(),
"llvm" => pecos_build::llvm::find_llvm(None),
"cuquantum" => pecos_build::cuquantum::find_cuquantum(),
"cmake" => pecos_build::cmake::find_cmake(),
"ripgrep" => pecos_build::ripgrep::find_ripgrep(),
_ => None,
}
}
Expand All @@ -116,6 +126,9 @@ fn install_target(target: &str, force: bool, no_configure: bool, yes: bool) -> R
"cmake" => {
pecos_build::cmake::installer::install_cmake(force)?;
}
"ripgrep" => {
pecos_build::ripgrep::install_ripgrep(force)?;
}
_ => unreachable!("target was validated above"),
}
Ok(())
Expand Down
18 changes: 18 additions & 0 deletions crates/pecos-cli/src/cli/rust_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ fn reject_static_llvm_workspace_test() -> Result<()> {
)))
}

fn require_cmake_for_mwpf(invocation: &str) -> Result<()> {
if pecos_build::cmake::find_cmake().is_some() {
return Ok(());
}

Err(Error::Config(format!(
"{invocation}, which enables the `mwpf` feature and requires cmake. \
Install cmake with `pecos install cmake`. See {}",
pecos_build::cmake::DOCS_URL
)))
}

/// Run the rust subcommand
pub fn run(command: &super::RustCommands) -> Result<()> {
match command {
Expand Down Expand Up @@ -305,6 +317,8 @@ fn run_cargo_command_with_rustflags(args: &[&str], rustflags: Option<&str>) -> b
/// Run cargo check with GPU-aware feature handling
#[allow(clippy::too_many_lines)]
fn run_check(include_ffi: bool) -> Result<()> {
require_cmake_for_mwpf("`pecos rust check` runs the workspace with `--all-features`")?;

let gpu_probe = probe_gpu_availability();
let include_gpu_sims = should_include_gpu_sims(&gpu_probe);

Expand Down Expand Up @@ -387,6 +401,8 @@ fn run_check(include_ffi: bool) -> Result<()> {
/// Run cargo clippy with GPU-aware feature handling
#[allow(clippy::too_many_lines)]
fn run_clippy(include_ffi: bool, fix: bool) -> Result<()> {
require_cmake_for_mwpf("`pecos rust clippy` runs the workspace with `--all-features`")?;

let gpu_probe = probe_gpu_availability();
let include_gpu_sims = should_include_gpu_sims(&gpu_probe);

Expand Down Expand Up @@ -486,6 +502,8 @@ fn run_clippy(include_ffi: bool, fix: bool) -> Result<()> {

/// Run cargo test with GPU-aware feature handling
fn run_test(profile: super::BuildProfile, include_ffi: bool) -> Result<()> {
require_cmake_for_mwpf("`pecos rust test` runs `cargo test -p pecos-decoders --all-features`")?;

// Warn about any C++ dependency version differences across crates
check_dep_consistency();

Expand Down
Loading
Loading