decuda 0.1.1

CUDA to HIP, SYCL, OpenCL, and Rust GPU migration tool — automatic source-code translator for porting CUDA C++ kernels to AMD ROCm HIP, Intel oneAPI SYCL, Khronos OpenCL, and Rust GPU (cust / rust-gpu)
Documentation
//! Directory walking: collect .cu / .cuh files.

use std::path::{Path, PathBuf};

use anyhow::Result;
use walkdir::WalkDir;

/// Walk the input path and collect CUDA source files. If `path` is a file, the
/// single file is returned (after extension validation). If it is a directory,
/// all `.cu` and `.cuh` files under it are returned.
pub fn collect_cuda_files(path: &Path, filter: Option<&str>) -> Result<Vec<PathBuf>> {
    if path.is_file() {
        return Ok(vec![path.to_path_buf()]);
    }
    if !path.is_dir() {
        anyhow::bail!("input path does not exist: {}", path.display());
    }
    let mut out = Vec::new();
    for entry in WalkDir::new(path)
        .follow_links(false)
        .into_iter()
        .filter_map(|e| e.ok())
    {
        let p = entry.path();
        if !p.is_file() {
            continue;
        }
        let Some(ext) = p.extension().and_then(|s| s.to_str()) else {
            continue;
        };
        if ext != "cu" && ext != "cuh" {
            continue;
        }
        if let Some(f) = filter {
            if !p.to_string_lossy().contains(f) {
                continue;
            }
        }
        out.push(p.to_path_buf());
    }
    out.sort();
    Ok(out)
}

/// Map a `.cu`/`.cuh` input path to its output for a given target.  Preserves
/// the relative directory layout under the target's subfolder. This is a pure
/// path-mapping function; it does NOT touch the filesystem. Use
/// [`ensure_parent_dir`] before writing to the returned path.
pub fn map_output_path(input_root: &Path, output_root: &Path, target: &str, file: &Path) -> PathBuf {
    let rel = file.strip_prefix(input_root).unwrap_or(file);
    let stem = rel.file_stem().and_then(|s| s.to_str()).unwrap_or("out");
    let parent = rel.parent().map(Path::to_path_buf).unwrap_or_default();
    let mut out = output_root.join(target).join(parent);
    let ext = target_extension(target);
    out.push(format!("{stem}.{ext}"));
    out
}

/// Create the parent directory of `p` if it doesn't already exist. Idempotent.
pub fn ensure_parent_dir(p: &Path) -> std::io::Result<()> {
    if let Some(parent) = p.parent() {
        std::fs::create_dir_all(parent)?;
    }
    Ok(())
}

fn target_extension(target: &str) -> &'static str {
    match target {
        "rust" => "rs",
        "opencl" => "cl",
        "sycl" => "sycl.cpp",
        "hip" => "hip.cpp",
        _ => "cpp",
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn collects_cu_files_in_directory() {
        let dir = tempdir().unwrap();
        fs::write(dir.path().join("a.cu"), "__global__ void k() {}").unwrap();
        fs::write(dir.path().join("b.cuh"), "// header").unwrap();
        fs::write(dir.path().join("c.cpp"), "// not cuda").unwrap();
        let got = collect_cuda_files(dir.path(), None).unwrap();
        let names: Vec<_> = got.iter().filter_map(|p| p.file_name()).collect();
        assert!(names.iter().any(|n| *n == "a.cu"));
        assert!(names.iter().any(|n| *n == "b.cuh"));
        assert!(!names.iter().any(|n| *n == "c.cpp"));
    }

    #[test]
    fn filter_substring_works() {
        let dir = tempdir().unwrap();
        fs::write(dir.path().join("foo_kernel.cu"), "").unwrap();
        fs::write(dir.path().join("bar_helper.cu"), "").unwrap();
        let got = collect_cuda_files(dir.path(), Some("foo")).unwrap();
        assert_eq!(got.len(), 1);
    }
}