netsuke-build 0.1.0-beta3

A YAML-powered Ninja/Jinja hybrid build system.
//! POSIX workspace traversal for the `which` fallback.

use camino::Utf8PathBuf;
use walkdir::WalkDir;

use super::super::is_executable;
use super::{
    WORKSPACE_MAX_DEPTH, WorkspaceSkipList, log_if_no_matches, should_visit_entry,
    unwrap_or_log_error,
};
use crate::stdlib::which::{env::EnvSnapshot, resolve_error::ResolveError};

/// Walk the working directory tree collecting executable matches, stopping
/// at the first match in first-match mode (`collect_all` is false) or
/// collecting every match in collect-all mode.
///
/// # Errors
///
/// Returns a [`ResolveError`] when directory traversal fails, a workspace path
/// is not valid UTF-8, or an executable probe fails.
pub(super) fn search_workspace(
    env: &EnvSnapshot,
    command: &str,
    collect_all: bool,
    skip_dirs: &WorkspaceSkipList,
) -> Result<Vec<Utf8PathBuf>, ResolveError> {
    let walker = WalkDir::new(&env.cwd)
        .follow_links(false)
        .max_depth(WORKSPACE_MAX_DEPTH)
        .sort_by_file_name()
        .into_iter()
        .filter_entry(|entry| should_visit_entry(entry, skip_dirs));

    let matches = collect_matching_executables(walker, command, collect_all, skip_dirs)?;
    log_if_no_matches(&matches, skip_dirs);
    Ok(matches)
}

/// Collect matching executables from a sequential walkdir iterator.
///
/// # Errors
///
/// Returns a [`ResolveError`] when a walkdir iteration fails, an entry path is
/// not valid UTF-8, or an entry cannot be probed for executability.
fn collect_matching_executables(
    walker: impl Iterator<Item = Result<walkdir::DirEntry, walkdir::Error>>,
    command: &str,
    collect_all: bool,
    skip_dirs: &WorkspaceSkipList,
) -> Result<Vec<Utf8PathBuf>, ResolveError> {
    let mut matches = Vec::new();

    for walk_entry in walker {
        let entry = unwrap_or_log_error(walk_entry)?;

        if let Some(path) = process_workspace_entry(entry, command, skip_dirs)? {
            matches.push(path);
            if !collect_all {
                break;
            }
        }
    }

    Ok(matches)
}

/// Inspect one walkdir entry, returning its path when it matches `command` and is executable.
///
/// # Errors
///
/// Returns a [`ResolveError`] when the entry path is not valid UTF-8 or the
/// executability probe fails.
fn process_workspace_entry(
    entry: walkdir::DirEntry,
    command: &str,
    _skip_dirs: &WorkspaceSkipList,
) -> Result<Option<Utf8PathBuf>, ResolveError> {
    if !entry.file_type().is_file() {
        return Ok(None);
    }

    let file_name = entry.file_name().to_string_lossy();
    if file_name != command {
        return Ok(None);
    }

    let path = entry.into_path();
    let utf8 = Utf8PathBuf::from_path_buf(path).map_err(|path_buf| {
        let lossy_path = path_buf.to_string_lossy();
        ResolveError::WorkspaceNonUtf8 {
            command: command.to_owned(),
            path: lossy_path.into_owned(),
        }
    })?;

    Ok(is_executable(&utf8)?.then_some(utf8))
}