diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8d58e888..3ea49a05 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -81,26 +81,6 @@ updates: - "automated" - "performance" - - package-ecosystem: "cargo" - directory: "/tools/file_operations_component" - schedule: - interval: "weekly" - day: "monday" - time: "06:00" - open-pull-requests-limit: 10 - reviewers: - - "avrabe" - assignees: - - "avrabe" - commit-message: - prefix: "rust/file-ops" - include: "scope" - labels: - - "dependencies" - - "rust" - - "automated" - - "wasm" - - package-ecosystem: "cargo" directory: "/tools-builder/toolchains" schedule: diff --git a/test/dual_implementation/BUILD.bazel b/test/dual_implementation/BUILD.bazel index 9e5c9c98..ec6ba3d3 100644 --- a/test/dual_implementation/BUILD.bazel +++ b/test/dual_implementation/BUILD.bazel @@ -6,28 +6,29 @@ load("//toolchains:dual_file_ops_toolchain.bzl", "dual_file_ops_toolchain") package(default_visibility = ["//visibility:public"]) # Test dual toolchain with actual components +# Uses Go-based file_ops for both implementations (production pattern) dual_file_ops_toolchain( name = "test_dual_toolchain", implementation_preference = "auto", - rust_component = "//tools/file_operations_component:file_ops", # Actual Rust implementation - tinygo_component = "//tools/file_operations_component:file_ops", # Current Rust implementation as mock TinyGo - wit_files = ["//tools/file_operations_component:wit_files"], + rust_component = "//tools/file_ops:file_ops", + tinygo_component = "//tools/file_ops:file_ops", + wit_files = ["//tools/file_ops:wit_files"], ) # Test security-focused configuration dual_file_ops_toolchain( name = "test_security_toolchain", implementation_preference = "tinygo", - tinygo_component = "//tools/file_operations_component:file_ops", - wit_files = ["//tools/file_operations_component:wit_files"], + tinygo_component = "//tools/file_ops:file_ops", + wit_files = ["//tools/file_ops:wit_files"], ) # Test performance-focused configuration dual_file_ops_toolchain( name = "test_performance_toolchain", implementation_preference = "rust", - rust_component = "//tools/file_operations_component:file_ops", - wit_files = ["//tools/file_operations_component:wit_files"], + rust_component = "//tools/file_ops:file_ops", + wit_files = ["//tools/file_ops:wit_files"], ) # Build tests to verify toolchain configurations diff --git a/toolchains/BUILD.bazel b/toolchains/BUILD.bazel index 3fdbcd5b..dddfa06e 100644 --- a/toolchains/BUILD.bazel +++ b/toolchains/BUILD.bazel @@ -199,7 +199,7 @@ bzl_library( file_ops_toolchain( name = "file_ops_toolchain_impl", file_ops_component = "//tools/file_ops:file_ops", - wit_files = ["//tools/file_operations_component:wit_files"], + wit_files = ["//tools/file_ops:wit_files"], ) toolchain( @@ -215,7 +215,7 @@ toolchain( file_ops_toolchain( name = "file_ops_toolchain_external_impl", file_ops_component = "//tools/file_ops_external:file_ops_external", - wit_files = ["//tools/file_operations_component:wit_files"], + wit_files = ["//tools/file_ops:wit_files"], ) toolchain( diff --git a/toolchains/file_ops_toolchain.bzl b/toolchains/file_ops_toolchain.bzl index 99379a3e..c64fb291 100644 --- a/toolchains/file_ops_toolchain.bzl +++ b/toolchains/file_ops_toolchain.bzl @@ -43,7 +43,7 @@ load("@rules_wasm_component//toolchains:file_ops_toolchain.bzl", "file_ops_toolc file_ops_toolchain( name = "file_ops_toolchain_impl", file_ops_component = "@rules_wasm_component//tools/file_ops:file_ops", - wit_files = ["@rules_wasm_component//tools/file_operations_component:wit_files"], + wit_files = ["@rules_wasm_component//tools/file_ops:wit_files"], visibility = ["//visibility:public"], ) diff --git a/tools/file_operations_component/BUILD.bazel b/tools/file_operations_component/BUILD.bazel deleted file mode 100644 index 37cd37cb..00000000 --- a/tools/file_operations_component/BUILD.bazel +++ /dev/null @@ -1,54 +0,0 @@ -"""File Operations Component for universal build system integration - -This component provides cross-platform file operations that replace -platform-specific shell scripts in Bazel rules for Go, C++, and JavaScript. -""" - -load("@rules_rust//rust:defs.bzl", "rust_binary") - -package(default_visibility = ["//visibility:public"]) - -# Build the file operations component as a CLI tool -# This allows it to be used directly in Bazel actions without WebAssembly runtime complexity -rust_binary( - name = "file_operations_component", - srcs = [ - "src/lib.rs", - "src/main.rs", - ], - deps = [ - "@crates//:anyhow", - "@crates//:serde", - "@crates//:serde_json", - ], - # Note: Currently building as CLI instead of WebAssembly component - # to avoid complex dependency resolution in Bazel crate_universe -) - -# Host binary for testing (optional) -rust_binary( - name = "file_ops_test", - srcs = ["src/main.rs"] if glob(["src/main.rs"]) else [], - tags = ["manual"], - deps = [ - "@crates//:anyhow", - "@crates//:filetime", - "@crates//:serde", - "@crates//:serde_json", - "@crates//:walkdir", - ], -) - -# Export WIT interface for external use -filegroup( - name = "wit_files", - srcs = ["wit/file-operations.wit"], - visibility = ["//visibility:public"], -) - -# Export for toolchain use -alias( - name = "file_ops", - actual = ":file_operations_component", - visibility = ["//visibility:public"], -) diff --git a/tools/file_operations_component/Cargo.toml b/tools/file_operations_component/Cargo.toml deleted file mode 100644 index a9ddca71..00000000 --- a/tools/file_operations_component/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "file_operations_component" -version = "1.0.0" -edition = "2021" -description = "Universal file operations component for WebAssembly build systems" -license = "Apache-2.0" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -wit-bindgen = "0.47.0" -anyhow = "1.0" -serde = "1.0" -serde_json = "1.0" -walkdir = "2.3" -filetime = "0.2" - -[dependencies.wit-bindgen-rt] -version = "0.44.0" -features = ["bitflags"] - -[package.metadata.component] -package = "build:file-ops" - -[package.metadata.component.dependencies] diff --git a/tools/file_operations_component/src/lib.rs b/tools/file_operations_component/src/lib.rs deleted file mode 100644 index 520907e7..00000000 --- a/tools/file_operations_component/src/lib.rs +++ /dev/null @@ -1,341 +0,0 @@ -//! Universal File Operations Component for WebAssembly Build Systems -//! -//! This component provides cross-platform file operations that replace -//! platform-specific shell scripts in build systems like Bazel. - -use anyhow::{Context, Result as AnyhowResult}; -use std::fs; -use std::path::Path; - -/// Copy a file from source to destination -pub fn copy_file(src: &str, dest: &str) -> AnyhowResult<()> { - let src_path = Path::new(src); - let dest_path = Path::new(dest); - - if !src_path.exists() { - return Err(anyhow::anyhow!("Source file does not exist: {}", src)); - } - - // Create parent directory if it doesn't exist - if let Some(parent) = dest_path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("Failed to create parent directory for: {}", dest))?; - } - - fs::copy(src_path, dest_path).with_context(|| format!("Failed to copy {} to {}", src, dest))?; - - Ok(()) -} - -/// Copy a directory recursively from source to destination -pub fn copy_directory(src: &str, dest: &str) -> AnyhowResult<()> { - let src_path = Path::new(src); - let dest_path = Path::new(dest); - - if !src_path.exists() { - return Err(anyhow::anyhow!("Source directory does not exist: {}", src)); - } - - if !src_path.is_dir() { - return Err(anyhow::anyhow!("Source is not a directory: {}", src)); - } - - // Create destination directory - fs::create_dir_all(dest_path) - .with_context(|| format!("Failed to create destination directory: {}", dest))?; - - copy_dir_recursive(src_path, dest_path)?; - - Ok(()) -} - -/// Create a directory (and all parent directories) -pub fn create_directory(path: &str) -> AnyhowResult<()> { - let dir_path = Path::new(path); - - fs::create_dir_all(dir_path) - .with_context(|| format!("Failed to create directory: {}", path))?; - - Ok(()) -} - -/// Recursively copy directory contents -fn copy_dir_recursive(src: &Path, dest: &Path) -> AnyhowResult<()> { - for entry in fs::read_dir(src)? { - let entry = entry?; - let src_path = entry.path(); - let dest_path = dest.join(entry.file_name()); - - if src_path.is_dir() { - fs::create_dir_all(&dest_path)?; - copy_dir_recursive(&src_path, &dest_path)?; - } else { - fs::copy(&src_path, &dest_path)?; - } - } - - Ok(()) -} - -#[derive(serde::Deserialize, serde::Serialize)] -pub struct WorkspaceConfig { - pub work_dir: String, - pub workspace_type: String, - pub sources: Vec, - pub headers: Vec, - pub dependencies: Vec, - pub bindings_dir: Option, -} - -#[derive(serde::Deserialize, serde::Serialize)] -pub struct FileSpec { - pub source: String, - pub destination: Option, - pub preserve_permissions: Option, -} - -#[derive(serde::Serialize)] -pub struct WorkspaceInfo { - pub workspace_path: String, - pub prepared_files: Vec, - pub preparation_time_ms: u64, - pub message: String, -} - -/// Prepare a complete workspace according to configuration -pub fn prepare_workspace(config: &WorkspaceConfig) -> AnyhowResult { - let start_time = std::time::Instant::now(); - let mut prepared_files = Vec::new(); - - // Create working directory - create_directory(&config.work_dir)?; - - // Copy source files - for source in &config.sources { - let dest_name = source - .destination - .as_ref() - .map(|s| s.as_str()) - .unwrap_or_else(|| { - Path::new(&source.source) - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("unknown") - }); - let dest_path = Path::new(&config.work_dir).join(dest_name); - - copy_file(&source.source, dest_path.to_str().unwrap())?; - prepared_files.push(dest_path.to_string_lossy().to_string()); - } - - // Copy header files - for header in &config.headers { - let dest_name = header - .destination - .as_ref() - .map(|s| s.as_str()) - .unwrap_or_else(|| { - Path::new(&header.source) - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("unknown") - }); - let dest_path = Path::new(&config.work_dir).join(dest_name); - - copy_file(&header.source, dest_path.to_str().unwrap())?; - prepared_files.push(dest_path.to_string_lossy().to_string()); - } - - // Copy dependencies - for dep in &config.dependencies { - let dest_name = dep - .destination - .as_ref() - .map(|s| s.as_str()) - .unwrap_or_else(|| { - Path::new(&dep.source) - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or("unknown") - }); - let dest_path = Path::new(&config.work_dir).join(dest_name); - - copy_file(&dep.source, dest_path.to_str().unwrap())?; - prepared_files.push(dest_path.to_string_lossy().to_string()); - } - - // Copy bindings directory if specified - if let Some(bindings_dir) = &config.bindings_dir { - if Path::new(bindings_dir).exists() { - copy_directory(bindings_dir, &config.work_dir)?; - prepared_files.push(format!("{}/* (bindings)", config.work_dir)); - } - } - - let duration = start_time.elapsed(); - let file_count = prepared_files.len(); - - Ok(WorkspaceInfo { - workspace_path: config.work_dir.clone(), - prepared_files, - preparation_time_ms: duration.as_millis() as u64, - message: format!( - "Successfully prepared {} workspace with {} files", - config.workspace_type, file_count - ), - }) -} - -// JSON Batch Operations Support - -/// JSON operation request -#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] -pub struct JsonOperation { - pub operation: String, - pub source: Option, - pub destination: Option, - pub content: Option, -} - -/// JSON batch request -#[derive(serde::Deserialize, Debug)] -pub struct JsonBatchRequest { - pub operations: Vec, -} - -/// JSON operation result -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct JsonOperationResult { - pub success: bool, - pub message: String, - pub output: Option, -} - -/// JSON batch response -#[derive(serde::Serialize, serde::Deserialize, Debug)] -pub struct JsonBatchResponse { - pub success: bool, - pub results: Vec, -} - -/// Process JSON batch operations -pub fn process_json_batch(request_json: &str) -> AnyhowResult { - let request: JsonBatchRequest = serde_json::from_str(request_json)?; - let mut results = Vec::new(); - let mut overall_success = true; - - for op in request.operations { - let result = execute_json_operation(&op); - if !result.success { - overall_success = false; - } - results.push(result); - } - - let response = JsonBatchResponse { - success: overall_success, - results, - }; - - Ok(serde_json::to_string(&response)?) -} - -/// List files in a directory -fn list_directory(path: &str) -> AnyhowResult> { - let mut entries = Vec::new(); - - for entry in fs::read_dir(path)? { - let entry = entry?; - if let Some(name) = entry.file_name().to_str() { - entries.push(name.to_string()); - } - } - - entries.sort(); - Ok(entries) -} - -/// Execute a single JSON operation -fn execute_json_operation(op: &JsonOperation) -> JsonOperationResult { - let result = match op.operation.as_str() { - "copy_file" => { - if let (Some(source), Some(dest)) = (&op.source, &op.destination) { - copy_file(source, dest).map(|_| None) - } else { - Err(anyhow::anyhow!("copy_file requires source and destination")) - } - } - "copy_directory" => { - if let (Some(source), Some(dest)) = (&op.source, &op.destination) { - copy_directory(source, dest).map(|_| None) - } else { - Err(anyhow::anyhow!("copy_directory requires source and destination")) - } - } - "create_directory" => { - if let Some(dest) = &op.destination { - create_directory(dest).map(|_| None) - } else { - Err(anyhow::anyhow!("create_directory requires destination")) - } - } - "copy_first_matching" => { - // Copy the first file matching a pattern (e.g., "*.rs") from source dir to destination - // source = directory to search - // content = glob pattern (e.g., "*.rs") - // destination = output file path - if let (Some(dir), Some(pattern), Some(dest)) = (&op.source, &op.content, &op.destination) { - match list_directory(dir) { - Ok(entries) => { - // Simple glob matching: *.ext means ends with .ext - let matching: Vec<_> = entries.iter() - .filter(|name| { - if pattern.starts_with("*.") { - let ext = &pattern[1..]; // Remove * - name.ends_with(ext) - } else { - name == &pattern - } - }) - .collect(); - - if matching.is_empty() { - Err(anyhow::anyhow!("No files matching '{}' found in {}", pattern, dir)) - } else { - // Copy the first match - let source_path = Path::new(dir).join(matching[0]); - match copy_file(source_path.to_str().unwrap(), dest) { - Ok(_) => { - let message = if matching.len() > 1 { - format!("Copied first match '{}' (found {} total)", matching[0], matching.len()) - } else { - format!("Copied '{}'", matching[0]) - }; - Ok(Some(message)) - } - Err(e) => Err(e) - } - } - } - Err(e) => Err(e) - } - } else { - Err(anyhow::anyhow!("copy_first_matching requires source (dir), content (pattern), and destination")) - } - } - _ => Err(anyhow::anyhow!("Unknown operation: {}", op.operation)), - }; - - match result { - Ok(output) => JsonOperationResult { - success: true, - message: format!("Operation '{}' completed successfully", op.operation), - output, - }, - Err(e) => JsonOperationResult { - success: false, - message: format!("Operation '{}' failed: {}", op.operation, e), - output: None, - }, - } -} diff --git a/tools/file_operations_component/src/main.rs b/tools/file_operations_component/src/main.rs deleted file mode 100644 index 0e60e31b..00000000 --- a/tools/file_operations_component/src/main.rs +++ /dev/null @@ -1,170 +0,0 @@ -//! Command-line interface for the File Operations Component -//! -//! This CLI allows the File Operations Component to be invoked from Bazel -//! rules as a standard executable, bridging the gap between Bazel's execution -//! model and cross-platform file operations. - -use std::env; -use std::fs; -use std::process; - -use anyhow::{Context, Result as AnyhowResult}; - -// Include the library functions directly -mod lib; - -fn main() { - if let Err(e) = run() { - eprintln!("Error: {}", e); - process::exit(1); - } -} - -fn run() -> AnyhowResult<()> { - let args: Vec = env::args().collect(); - - // Special case: If first arg is a file path ending in .json, treat it as JSON batch operations - if args.len() == 2 && args[1].ends_with(".json") { - process_json_batch_file(&args[1])?; - return Ok(()); - } - - if args.len() < 2 { - eprintln!("Usage: {} [args...]", args[0]); - eprintln!(" {} ", args[0]); - eprintln!("Operations:"); - eprintln!(" copy_file --src --dest "); - eprintln!(" copy_directory --src --dest "); - eprintln!(" create_directory --path "); - eprintln!(" prepare_workspace --config "); - eprintln!(" - Process JSON batch operations"); - process::exit(1); - } - - let operation = &args[1]; - - match operation.as_str() { - "copy_file" => { - let (src, dest) = parse_copy_args(&args[2..])?; - lib::copy_file(&src, &dest)?; - } - "copy_directory" => { - let (src, dest) = parse_copy_args(&args[2..])?; - lib::copy_directory(&src, &dest)?; - } - "create_directory" => { - let path = parse_path_arg(&args[2..])?; - lib::create_directory(&path)?; - } - "prepare_workspace" => { - let config_file = parse_config_arg(&args[2..])?; - prepare_workspace_from_file(&config_file)?; - } - _ => { - return Err(anyhow::anyhow!("Unknown operation: {}", operation)); - } - } - - Ok(()) -} - -fn parse_copy_args(args: &[String]) -> AnyhowResult<(String, String)> { - let mut src = None; - let mut dest = None; - - let mut i = 0; - while i < args.len() { - match args[i].as_str() { - "--src" => { - if i + 1 < args.len() { - src = Some(args[i + 1].clone()); - i += 2; - } else { - return Err(anyhow::anyhow!("--src requires a value")); - } - } - "--dest" => { - if i + 1 < args.len() { - dest = Some(args[i + 1].clone()); - i += 2; - } else { - return Err(anyhow::anyhow!("--dest requires a value")); - } - } - _ => { - return Err(anyhow::anyhow!("Unknown argument: {}", args[i])); - } - } - } - - let src = src.ok_or_else(|| anyhow::anyhow!("--src is required"))?; - let dest = dest.ok_or_else(|| anyhow::anyhow!("--dest is required"))?; - - Ok((src, dest)) -} - -fn parse_path_arg(args: &[String]) -> AnyhowResult { - if args.len() < 2 || args[0] != "--path" { - return Err(anyhow::anyhow!("Expected --path ")); - } - Ok(args[1].clone()) -} - -fn parse_config_arg(args: &[String]) -> AnyhowResult { - if args.len() < 2 || args[0] != "--config" { - return Err(anyhow::anyhow!("Expected --config ")); - } - Ok(args[1].clone()) -} - -fn prepare_workspace_from_file(config_file: &str) -> AnyhowResult<()> { - // Read configuration file - let config_content = fs::read_to_string(config_file) - .with_context(|| format!("Failed to read config file: {}", config_file))?; - - let config: lib::WorkspaceConfig = serde_json::from_str(&config_content) - .with_context(|| format!("Failed to parse config file: {}", config_file))?; - - // Call the library function - let result = lib::prepare_workspace(&config)?; - - println!("Workspace prepared successfully:"); - println!(" Path: {}", result.workspace_path); - println!(" Files: {}", result.prepared_files.len()); - println!(" Time: {}ms", result.preparation_time_ms); - println!(" Message: {}", result.message); - - Ok(()) -} - -fn process_json_batch_file(json_file: &str) -> AnyhowResult<()> { - // Read JSON batch operations file - let json_content = fs::read_to_string(json_file) - .with_context(|| format!("Failed to read JSON file: {}", json_file))?; - - // Process batch operations - let response_json = lib::process_json_batch(&json_content) - .with_context(|| format!("Failed to process JSON batch operations"))?; - - // Parse response - let response: lib::JsonBatchResponse = serde_json::from_str(&response_json) - .with_context(|| format!("Failed to parse JSON batch response"))?; - - // Print results - for (i, result) in response.results.iter().enumerate() { - if result.success { - println!("[{}] ✓ {}", i + 1, result.message); - if let Some(ref output) = result.output { - println!(" Output: {}", output); - } - } else { - eprintln!("[{}] ✗ {}", i + 1, result.message); - } - } - - if !response.success { - return Err(anyhow::anyhow!("Some operations failed")); - } - - Ok(()) -} diff --git a/tools/file_ops/BUILD.bazel b/tools/file_ops/BUILD.bazel index 1ab447ae..da062fec 100644 --- a/tools/file_ops/BUILD.bazel +++ b/tools/file_ops/BUILD.bazel @@ -20,3 +20,11 @@ alias( actual = ":file_ops", visibility = ["//visibility:public"], ) + +# Export WIT interface for toolchain use +# This defines the file operations interface contract +filegroup( + name = "wit_files", + srcs = ["wit/file-operations.wit"], + visibility = ["//visibility:public"], +) diff --git a/tools/file_operations_component/wit/file-operations.wit b/tools/file_ops/wit/file-operations.wit similarity index 100% rename from tools/file_operations_component/wit/file-operations.wit rename to tools/file_ops/wit/file-operations.wit