Compare commits
No commits in common. "main" and "development_maxime" have entirely different histories.
main
...
developmen
@ -11,7 +11,6 @@ categories = ["os", "filesystem", "api-bindings"]
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
tera = "1.19.0" # Template engine for text rendering
|
||||
# Cross-platform functionality
|
||||
libc = "0.2"
|
||||
cfg-if = "1.0"
|
||||
@ -24,9 +23,6 @@ serde_json = "1.0" # For JSON handling
|
||||
glob = "0.3.1" # For file pattern matching
|
||||
tempfile = "3.5" # For temporary file operations
|
||||
log = "0.4" # Logging facade
|
||||
rhai = { version = "1.12.0", features = ["sync"] } # Embedded scripting language
|
||||
rand = "0.8.5" # Random number generation
|
||||
clap = "2.33" # Command-line argument parsing
|
||||
|
||||
# Optional features for specific OS functionality
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
@ -37,7 +33,3 @@ windows = { version = "0.48", features = ["Win32_Foundation", "Win32_System_Thre
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.5" # For tests that need temporary files/directories
|
||||
|
||||
[[bin]]
|
||||
name = "herodo"
|
||||
path = "src/bin/herodo.rs"
|
||||
|
@ -1,156 +0,0 @@
|
||||
|
||||
|
||||
please refactor each of the objects in the the chosen folder to use builder paradigm, see below for an example
|
||||
we always start from root object, each file e.g. product.rs corresponds to a root object, the rootobject is what is stored in the DB, the rest are sub objects which are children of the root object
|
||||
|
||||
---
|
||||
|
||||
### ✅ Step 1: Define your struct
|
||||
```rust
|
||||
#[derive(Debug)]
|
||||
pub enum ProductType {
|
||||
Service,
|
||||
// Other variants...
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ProductStatus {
|
||||
Available,
|
||||
Unavailable,
|
||||
// Other variants...
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Product {
|
||||
id: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
price: f64,
|
||||
product_type: ProductType,
|
||||
category: String,
|
||||
status: ProductStatus,
|
||||
max_amount: u32,
|
||||
validity_days: u32,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ Step 2: Create a builder
|
||||
|
||||
```rust
|
||||
pub struct ProductBuilder {
|
||||
id: Option<u32>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
price: Option<f64>,
|
||||
product_type: Option<ProductType>,
|
||||
category: Option<String>,
|
||||
status: Option<ProductStatus>,
|
||||
max_amount: Option<u32>,
|
||||
validity_days: Option<u32>,
|
||||
}
|
||||
|
||||
impl ProductBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
name: None,
|
||||
description: None,
|
||||
price: None,
|
||||
product_type: None,
|
||||
category: None,
|
||||
status: None,
|
||||
max_amount: None,
|
||||
validity_days: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(mut self, id: u32) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn price(mut self, price: f64) -> Self {
|
||||
self.price = Some(price);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn product_type(mut self, product_type: ProductType) -> Self {
|
||||
self.product_type = Some(product_type);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn category<S: Into<String>>(mut self, category: S) -> Self {
|
||||
self.category = Some(category.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn status(mut self, status: ProductStatus) -> Self {
|
||||
self.status = Some(status);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max_amount(mut self, max_amount: u32) -> Self {
|
||||
self.max_amount = Some(max_amount);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn validity_days(mut self, validity_days: u32) -> Self {
|
||||
self.validity_days = Some(validity_days);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<Product, &'static str> {
|
||||
Ok(Product {
|
||||
id: self.id.ok_or("id is required")?,
|
||||
name: self.name.ok_or("name is required")?,
|
||||
description: self.description.ok_or("description is required")?,
|
||||
price: self.price.ok_or("price is required")?,
|
||||
product_type: self.product_type.ok_or("type is required")?,
|
||||
category: self.category.ok_or("category is required")?,
|
||||
status: self.status.ok_or("status is required")?,
|
||||
max_amount: self.max_amount.ok_or("max_amount is required")?,
|
||||
validity_days: self.validity_days.ok_or("validity_days is required")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ Step 3: Use it like this
|
||||
|
||||
```rust
|
||||
let product = ProductBuilder::new()
|
||||
.id(1)
|
||||
.name("Premium Service")
|
||||
.description("Our premium service offering")
|
||||
.price(99.99)
|
||||
.product_type(ProductType::Service)
|
||||
.category("Services")
|
||||
.status(ProductStatus::Available)
|
||||
.max_amount(100)
|
||||
.validity_days(30)
|
||||
.build()
|
||||
.expect("Failed to build product");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
This way:
|
||||
- You don’t need to remember the order of parameters.
|
||||
- You get readable, self-documenting code.
|
||||
- It’s easier to provide defaults or optional values if you want later.
|
||||
|
||||
Want help generating this automatically via a macro or just want it shorter? I can show you a derive macro to do that too.
|
@ -1,994 +0,0 @@
|
||||
# Best Practices for Wrapping Rust Functions with Rhai
|
||||
|
||||
This document provides comprehensive guidance on how to effectively wrap Rust functions with different standard arguments, pass structs, and handle various return types including errors when using the Rhai scripting language.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Introduction](#introduction)
|
||||
2. [Basic Function Registration](#basic-function-registration)
|
||||
3. [Working with Different Argument Types](#working-with-different-argument-types)
|
||||
4. [Passing and Working with Structs](#passing-and-working-with-structs)
|
||||
5. [Error Handling](#error-handling)
|
||||
6. [Returning Different Types](#returning-different-types)
|
||||
7. [Native Function Handling](#native-function-handling)
|
||||
8. [Advanced Patterns](#advanced-patterns)
|
||||
9. [Complete Examples](#complete-examples)
|
||||
|
||||
## Introduction
|
||||
|
||||
Rhai is an embedded scripting language for Rust that allows you to expose Rust functions to scripts and vice versa. This document focuses on the best practices for wrapping Rust functions so they can be called from Rhai scripts, with special attention to handling different argument types, structs, and error conditions.
|
||||
|
||||
## Basic Function Registration
|
||||
|
||||
### Simple Function Registration
|
||||
|
||||
The most basic way to register a Rust function with Rhai is using the `register_fn` method:
|
||||
|
||||
```rust
|
||||
fn add(x: i64, y: i64) -> i64 {
|
||||
x + y
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<EvalAltResult>> {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register the function with Rhai
|
||||
engine.register_fn("add", add);
|
||||
|
||||
// Now the function can be called from Rhai scripts
|
||||
let result = engine.eval::<i64>("add(40, 2)")?;
|
||||
|
||||
println!("Result: {}", result); // prints 42
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Function Naming Conventions
|
||||
|
||||
When registering functions, follow these naming conventions:
|
||||
|
||||
1. Use snake_case for function names to maintain consistency with Rhai's style
|
||||
2. Choose descriptive names that clearly indicate the function's purpose
|
||||
3. For functions that operate on specific types, consider prefixing with the type name (e.g., `string_length`)
|
||||
|
||||
## Working with Different Argument Types
|
||||
|
||||
### Primitive Types
|
||||
|
||||
Rhai supports the following primitive types that can be directly used as function arguments:
|
||||
|
||||
- `i64` (integer)
|
||||
- `f64` (float)
|
||||
- `bool` (boolean)
|
||||
- `String` or `&str` (string)
|
||||
- `char` (character)
|
||||
- `()` (unit type)
|
||||
|
||||
Example:
|
||||
|
||||
```rust
|
||||
fn calculate(num: i64, factor: f64, enabled: bool) -> f64 {
|
||||
if enabled {
|
||||
num as f64 * factor
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
engine.register_fn("calculate", calculate);
|
||||
```
|
||||
|
||||
### Arrays and Collections
|
||||
|
||||
For array arguments:
|
||||
|
||||
```rust
|
||||
fn sum_array(arr: Array) -> i64 {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_int().ok())
|
||||
.sum()
|
||||
}
|
||||
|
||||
engine.register_fn("sum_array", sum_array);
|
||||
```
|
||||
|
||||
### Optional Arguments and Function Overloading
|
||||
|
||||
Rhai supports function overloading, which allows you to register multiple functions with the same name but different parameter types or counts:
|
||||
|
||||
```rust
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}!", name)
|
||||
}
|
||||
|
||||
fn greet_with_title(title: &str, name: &str) -> String {
|
||||
format!("Hello, {} {}!", title, name)
|
||||
}
|
||||
|
||||
engine.register_fn("greet", greet);
|
||||
engine.register_fn("greet", greet_with_title);
|
||||
|
||||
// In Rhai:
|
||||
// greet("World") -> "Hello, World!"
|
||||
// greet("Mr.", "Smith") -> "Hello, Mr. Smith!"
|
||||
```
|
||||
|
||||
## Passing and Working with Structs
|
||||
|
||||
### Registering Custom Types
|
||||
|
||||
To use Rust structs in Rhai, you need to register them:
|
||||
|
||||
#### Method 1: Using the CustomType Trait (Recommended)
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, CustomType)]
|
||||
#[rhai_type(extra = Self::build_extra)]
|
||||
struct TestStruct {
|
||||
x: i64,
|
||||
}
|
||||
|
||||
impl TestStruct {
|
||||
pub fn new() -> Self {
|
||||
Self { x: 1 }
|
||||
}
|
||||
|
||||
pub fn update(&mut self) {
|
||||
self.x += 1000;
|
||||
}
|
||||
|
||||
pub fn calculate(&mut self, data: i64) -> i64 {
|
||||
self.x * data
|
||||
}
|
||||
|
||||
fn build_extra(builder: &mut TypeBuilder<Self>) {
|
||||
builder
|
||||
.with_name("TestStruct")
|
||||
.with_fn("new_ts", Self::new)
|
||||
.with_fn("update", Self::update)
|
||||
.with_fn("calc", Self::calculate);
|
||||
}
|
||||
}
|
||||
|
||||
// In your main function:
|
||||
let mut engine = Engine::new();
|
||||
engine.build_type::<TestStruct>();
|
||||
```
|
||||
|
||||
#### Method 2: Manual Registration
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
struct TestStruct {
|
||||
x: i64,
|
||||
}
|
||||
|
||||
impl TestStruct {
|
||||
pub fn new() -> Self {
|
||||
Self { x: 1 }
|
||||
}
|
||||
|
||||
pub fn update(&mut self) {
|
||||
self.x += 1000;
|
||||
}
|
||||
}
|
||||
|
||||
let mut engine = Engine::new();
|
||||
|
||||
engine
|
||||
.register_type_with_name::<TestStruct>("TestStruct")
|
||||
.register_fn("new_ts", TestStruct::new)
|
||||
.register_fn("update", TestStruct::update);
|
||||
```
|
||||
|
||||
### Accessing Struct Fields
|
||||
|
||||
By default, Rhai can access public fields of registered structs:
|
||||
|
||||
```rust
|
||||
// In Rhai script:
|
||||
let x = new_ts();
|
||||
x.x = 42; // Direct field access
|
||||
```
|
||||
|
||||
### Passing Structs as Arguments
|
||||
|
||||
When passing structs as arguments to functions, ensure they implement the `Clone` trait:
|
||||
|
||||
```rust
|
||||
fn process_struct(test: TestStruct) -> i64 {
|
||||
test.x * 2
|
||||
}
|
||||
|
||||
engine.register_fn("process_struct", process_struct);
|
||||
```
|
||||
|
||||
### Returning Structs from Functions
|
||||
|
||||
You can return custom structs from functions:
|
||||
|
||||
```rust
|
||||
fn create_struct(value: i64) -> TestStruct {
|
||||
TestStruct { x: value }
|
||||
}
|
||||
|
||||
engine.register_fn("create_struct", create_struct);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Error handling is a critical aspect of integrating Rust functions with Rhai. Proper error handling ensures that script execution fails gracefully with meaningful error messages.
|
||||
|
||||
### Basic Error Handling
|
||||
|
||||
The most basic way to handle errors is to return a `Result` type:
|
||||
|
||||
```rust
|
||||
fn divide(a: i64, b: i64) -> Result<i64, Box<EvalAltResult>> {
|
||||
if b == 0 {
|
||||
// Return an error if division by zero
|
||||
Err("Division by zero".into())
|
||||
} else {
|
||||
Ok(a / b)
|
||||
}
|
||||
}
|
||||
|
||||
engine.register_fn("divide", divide);
|
||||
```
|
||||
|
||||
### EvalAltResult Types
|
||||
|
||||
Rhai provides several error types through the `EvalAltResult` enum:
|
||||
|
||||
```rust
|
||||
use rhai::EvalAltResult;
|
||||
use rhai::Position;
|
||||
|
||||
fn my_function() -> Result<i64, Box<EvalAltResult>> {
|
||||
// Different error types
|
||||
|
||||
// Runtime error - general purpose error
|
||||
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
"Something went wrong".into(),
|
||||
Position::NONE
|
||||
)));
|
||||
|
||||
// Type error - when a type mismatch occurs
|
||||
return Err(Box::new(EvalAltResult::ErrorMismatchOutputType(
|
||||
"expected i64, got string".into(),
|
||||
Position::NONE,
|
||||
"i64".into()
|
||||
)));
|
||||
|
||||
// Function not found error
|
||||
return Err(Box::new(EvalAltResult::ErrorFunctionNotFound(
|
||||
"function_name".into(),
|
||||
Position::NONE
|
||||
)));
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Error Types
|
||||
|
||||
For more structured error handling, you can create custom error types:
|
||||
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
use rhai::{EvalAltResult, Position};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
enum MyError {
|
||||
#[error("Invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
|
||||
#[error("Calculation error: {0}")]
|
||||
CalculationError(String),
|
||||
|
||||
#[error("Database error: {0}")]
|
||||
DatabaseError(String),
|
||||
}
|
||||
|
||||
// Convert your custom error to EvalAltResult
|
||||
fn process_data(input: i64) -> Result<i64, Box<EvalAltResult>> {
|
||||
// Your logic here that might return a custom error
|
||||
let result = validate_input(input)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Validation failed: {}", e),
|
||||
Position::NONE
|
||||
)))?;
|
||||
|
||||
let processed = calculate(result)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Calculation failed: {}", e),
|
||||
Position::NONE
|
||||
)))?;
|
||||
|
||||
if processed < 0 {
|
||||
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
"Negative result not allowed".into(),
|
||||
Position::NONE
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(processed)
|
||||
}
|
||||
|
||||
// Helper functions that return our custom error type
|
||||
fn validate_input(input: i64) -> Result<i64, MyError> {
|
||||
if input <= 0 {
|
||||
return Err(MyError::InvalidInput("Input must be positive".into()));
|
||||
}
|
||||
Ok(input)
|
||||
}
|
||||
|
||||
fn calculate(value: i64) -> Result<i64, MyError> {
|
||||
if value > 1000 {
|
||||
return Err(MyError::CalculationError("Value too large".into()));
|
||||
}
|
||||
Ok(value * 2)
|
||||
}
|
||||
```
|
||||
|
||||
### Error Propagation
|
||||
|
||||
When calling Rhai functions from Rust, errors are propagated through the `?` operator:
|
||||
|
||||
```rust
|
||||
let result = engine.eval::<i64>("divide(10, 0)")?; // This will propagate the error
|
||||
```
|
||||
|
||||
### Error Context and Position Information
|
||||
|
||||
For better debugging, include position information in your errors:
|
||||
|
||||
```rust
|
||||
fn parse_config(config: &str) -> Result<Map, Box<EvalAltResult>> {
|
||||
// Get the call position from the context
|
||||
let pos = Position::NONE; // In a real function, you'd get this from NativeCallContext
|
||||
|
||||
match serde_json::from_str::<serde_json::Value>(config) {
|
||||
Ok(json) => {
|
||||
// Convert JSON to Rhai Map
|
||||
let mut map = Map::new();
|
||||
// ... conversion logic ...
|
||||
Ok(map)
|
||||
},
|
||||
Err(e) => {
|
||||
Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to parse config: {}", e),
|
||||
pos
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Best Practices for Error Handling
|
||||
|
||||
1. **Be Specific**: Provide clear, specific error messages that help script writers understand what went wrong
|
||||
2. **Include Context**: When possible, include relevant context in error messages (e.g., variable values, expected types)
|
||||
3. **Consistent Error Types**: Use consistent error types for similar issues
|
||||
4. **Validate Early**: Validate inputs at the beginning of functions to fail fast
|
||||
5. **Document Error Conditions**: Document possible error conditions for functions exposed to Rhai
|
||||
|
||||
|
||||
## Returning Different Types
|
||||
|
||||
Properly handling return types is crucial for creating a seamless integration between Rust and Rhai. This section covers various approaches to returning different types of data from Rust functions to Rhai scripts.
|
||||
|
||||
### Simple Return Types
|
||||
|
||||
For simple return types, specify the type when registering the function:
|
||||
|
||||
```rust
|
||||
fn get_number() -> i64 { 42 }
|
||||
fn get_string() -> String { "hello".to_string() }
|
||||
fn get_boolean() -> bool { true }
|
||||
fn get_float() -> f64 { 3.14159 }
|
||||
fn get_char() -> char { 'A' }
|
||||
fn get_unit() -> () { () }
|
||||
|
||||
engine.register_fn("get_number", get_number);
|
||||
engine.register_fn("get_string", get_string);
|
||||
engine.register_fn("get_boolean", get_boolean);
|
||||
engine.register_fn("get_float", get_float);
|
||||
engine.register_fn("get_char", get_char);
|
||||
engine.register_fn("get_unit", get_unit);
|
||||
```
|
||||
|
||||
### Dynamic Return Types
|
||||
|
||||
WE SHOULD TRY NOT TO DO THIS
|
||||
|
||||
For functions that may return different types based on conditions, use the `Dynamic` type:
|
||||
|
||||
```rust
|
||||
fn get_value(which: i64) -> Dynamic {
|
||||
match which {
|
||||
0 => Dynamic::from(42),
|
||||
1 => Dynamic::from("hello"),
|
||||
2 => Dynamic::from(true),
|
||||
3 => Dynamic::from(3.14159),
|
||||
4 => {
|
||||
let mut array = Array::new();
|
||||
array.push(Dynamic::from(1));
|
||||
array.push(Dynamic::from(2));
|
||||
Dynamic::from_array(array)
|
||||
},
|
||||
5 => {
|
||||
let mut map = Map::new();
|
||||
map.insert("key".into(), "value".into());
|
||||
Dynamic::from_map(map)
|
||||
},
|
||||
_ => Dynamic::UNIT,
|
||||
}
|
||||
}
|
||||
|
||||
engine.register_fn("get_value", get_value);
|
||||
```
|
||||
|
||||
### Returning Collections
|
||||
|
||||
Rhai supports various collection types:
|
||||
|
||||
```rust
|
||||
// Returning an array
|
||||
fn get_array() -> Array {
|
||||
let mut array = Array::new();
|
||||
array.push(Dynamic::from(1));
|
||||
array.push(Dynamic::from("hello"));
|
||||
array.push(Dynamic::from(true));
|
||||
array
|
||||
}
|
||||
|
||||
// Returning a map
|
||||
fn get_map() -> Map {
|
||||
let mut map = Map::new();
|
||||
map.insert("number".into(), 42.into());
|
||||
map.insert("string".into(), "hello".into());
|
||||
map.insert("boolean".into(), true.into());
|
||||
map
|
||||
}
|
||||
|
||||
// Returning a typed Vec (will be converted to Rhai Array)
|
||||
fn get_numbers() -> Vec<i64> {
|
||||
vec![1, 2, 3, 4, 5]
|
||||
}
|
||||
|
||||
// Returning a HashMap (will be converted to Rhai Map)
|
||||
fn get_config() -> HashMap<String, String> {
|
||||
let mut map = HashMap::new();
|
||||
map.insert("host".to_string(), "localhost".to_string());
|
||||
map.insert("port".to_string(), "8080".to_string());
|
||||
map
|
||||
}
|
||||
|
||||
engine.register_fn("get_array", get_array);
|
||||
engine.register_fn("get_map", get_map);
|
||||
engine.register_fn("get_numbers", get_numbers);
|
||||
engine.register_fn("get_config", get_config);
|
||||
```
|
||||
|
||||
### Returning Custom Structs
|
||||
|
||||
For returning custom structs, ensure they implement the `Clone` trait:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
struct TestStruct {
|
||||
x: i64,
|
||||
name: String,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
fn create_struct(value: i64, name: &str, active: bool) -> TestStruct {
|
||||
TestStruct {
|
||||
x: value,
|
||||
name: name.to_string(),
|
||||
active
|
||||
}
|
||||
}
|
||||
|
||||
fn get_struct_array() -> Vec<TestStruct> {
|
||||
vec![
|
||||
TestStruct { x: 1, name: "one".to_string(), active: true },
|
||||
TestStruct { x: 2, name: "two".to_string(), active: false },
|
||||
]
|
||||
}
|
||||
|
||||
engine.register_type_with_name::<TestStruct>("TestStruct")
|
||||
.register_fn("create_struct", create_struct)
|
||||
.register_fn("get_struct_array", get_struct_array);
|
||||
```
|
||||
|
||||
### Returning Results and Options
|
||||
|
||||
For functions that might fail or return optional values:
|
||||
|
||||
```rust
|
||||
// Returning a Result
|
||||
fn divide(a: i64, b: i64) -> Result<i64, Box<EvalAltResult>> {
|
||||
if b == 0 {
|
||||
Err("Division by zero".into())
|
||||
} else {
|
||||
Ok(a / b)
|
||||
}
|
||||
}
|
||||
|
||||
// Returning an Option (converted to Dynamic)
|
||||
fn find_item(id: i64) -> Dynamic {
|
||||
let item = lookup_item(id);
|
||||
|
||||
match item {
|
||||
Some(value) => value.into(),
|
||||
None => Dynamic::UNIT, // Rhai has no null, so use () for None
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function returning Option
|
||||
fn lookup_item(id: i64) -> Option<TestStruct> {
|
||||
match id {
|
||||
1 => Some(TestStruct { x: 1, name: "one".to_string(), active: true }),
|
||||
2 => Some(TestStruct { x: 2, name: "two".to_string(), active: false }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
engine.register_fn("divide", divide);
|
||||
engine.register_fn("find_item", find_item);
|
||||
```
|
||||
|
||||
### Serialization and Deserialization
|
||||
|
||||
When working with JSON or other serialized formats:
|
||||
|
||||
```rust
|
||||
use serde_json::{Value as JsonValue, json};
|
||||
|
||||
// Return JSON data as a Rhai Map
|
||||
fn get_json_data() -> Result<Map, Box<EvalAltResult>> {
|
||||
// Simulate fetching JSON data
|
||||
let json_data = json!({
|
||||
"name": "John Doe",
|
||||
"age": 30,
|
||||
"address": {
|
||||
"street": "123 Main St",
|
||||
"city": "Anytown"
|
||||
},
|
||||
"phones": ["+1-555-1234", "+1-555-5678"]
|
||||
});
|
||||
|
||||
// Convert JSON to Rhai Map
|
||||
json_to_rhai_value(json_data)
|
||||
.and_then(|v| v.try_cast::<Map>().map_err(|_| "Expected a map".into()))
|
||||
}
|
||||
|
||||
// Helper function to convert JSON Value to Rhai Dynamic
|
||||
fn json_to_rhai_value(json: JsonValue) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
match json {
|
||||
JsonValue::Null => Ok(Dynamic::UNIT),
|
||||
JsonValue::Bool(b) => Ok(b.into()),
|
||||
JsonValue::Number(n) => {
|
||||
if n.is_i64() {
|
||||
Ok(n.as_i64().unwrap().into())
|
||||
} else {
|
||||
Ok(n.as_f64().unwrap().into())
|
||||
}
|
||||
},
|
||||
JsonValue::String(s) => Ok(s.into()),
|
||||
JsonValue::Array(arr) => {
|
||||
let mut rhai_array = Array::new();
|
||||
for item in arr {
|
||||
rhai_array.push(json_to_rhai_value(item)?);
|
||||
}
|
||||
Ok(Dynamic::from_array(rhai_array))
|
||||
},
|
||||
JsonValue::Object(obj) => {
|
||||
let mut rhai_map = Map::new();
|
||||
for (k, v) in obj {
|
||||
rhai_map.insert(k.into(), json_to_rhai_value(v)?);
|
||||
}
|
||||
Ok(Dynamic::from_map(rhai_map))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
engine.register_fn("get_json_data", get_json_data);
|
||||
```
|
||||
|
||||
### Working with Dynamic Type System
|
||||
|
||||
Understanding how to work with Rhai's Dynamic type system is essential:
|
||||
|
||||
```rust
|
||||
// Function that examines a Dynamic value and returns information about it
|
||||
fn inspect_value(value: Dynamic) -> Map {
|
||||
let mut info = Map::new();
|
||||
|
||||
// Store the type name
|
||||
info.insert("type".into(), value.type_name().into());
|
||||
|
||||
// Store specific type information
|
||||
if value.is_int() {
|
||||
info.insert("category".into(), "number".into());
|
||||
info.insert("value".into(), value.clone());
|
||||
} else if value.is_float() {
|
||||
info.insert("category".into(), "number".into());
|
||||
info.insert("value".into(), value.clone());
|
||||
} else if value.is_string() {
|
||||
info.insert("category".into(), "string".into());
|
||||
info.insert("length".into(), value.clone_cast::<String>().len().into());
|
||||
info.insert("value".into(), value.clone());
|
||||
} else if value.is_array() {
|
||||
info.insert("category".into(), "array".into());
|
||||
info.insert("length".into(), value.clone_cast::<Array>().len().into());
|
||||
} else if value.is_map() {
|
||||
info.insert("category".into(), "map".into());
|
||||
info.insert("keys".into(), value.clone_cast::<Map>().keys().len().into());
|
||||
} else if value.is_bool() {
|
||||
info.insert("category".into(), "boolean".into());
|
||||
info.insert("value".into(), value.clone());
|
||||
} else {
|
||||
info.insert("category".into(), "other".into());
|
||||
}
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
engine.register_fn("inspect", inspect_value);
|
||||
```
|
||||
|
||||
## Native Function Handling
|
||||
|
||||
When working with native Rust functions in Rhai, there are several important considerations for handling different argument types, especially when dealing with complex data structures and error cases.
|
||||
|
||||
### Native Function Signature
|
||||
|
||||
Native Rust functions registered with Rhai can have one of two signatures:
|
||||
|
||||
1. **Standard Function Signature**: Functions with typed parameters
|
||||
```rust
|
||||
fn my_function(param1: Type1, param2: Type2, ...) -> ReturnType { ... }
|
||||
```
|
||||
|
||||
2. **Dynamic Function Signature**: Functions that handle raw Dynamic values
|
||||
```rust
|
||||
fn my_dynamic_function(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> { ... }
|
||||
```
|
||||
|
||||
### Working with Raw Dynamic Arguments
|
||||
|
||||
The dynamic function signature gives you more control but requires manual type checking and conversion:
|
||||
|
||||
```rust
|
||||
fn process_dynamic_args(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
// Check number of arguments
|
||||
if args.len() != 2 {
|
||||
return Err("Expected exactly 2 arguments".into());
|
||||
}
|
||||
|
||||
// Extract and convert the first argument to an integer
|
||||
let arg1 = args[0].as_int().map_err(|_| "First argument must be an integer".into())?;
|
||||
|
||||
// Extract and convert the second argument to a string
|
||||
let arg2 = args[1].as_str().map_err(|_| "Second argument must be a string".into())?;
|
||||
|
||||
// Process the arguments
|
||||
let result = format!("{}: {}", arg2, arg1);
|
||||
|
||||
// Return the result as a Dynamic value
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
// Register the function
|
||||
engine.register_fn("process", process_dynamic_args);
|
||||
```
|
||||
|
||||
### Handling Complex Struct Arguments
|
||||
|
||||
When working with complex struct arguments, you have several options:
|
||||
|
||||
#### Option 1: Use typed parameters (recommended for simple cases)
|
||||
|
||||
```rust
|
||||
#[derive(Clone)]
|
||||
struct ComplexData {
|
||||
id: i64,
|
||||
values: Vec<f64>,
|
||||
}
|
||||
|
||||
fn process_complex(data: &mut ComplexData, factor: f64) -> f64 {
|
||||
let sum: f64 = data.values.iter().sum();
|
||||
data.values.push(sum * factor);
|
||||
sum * factor
|
||||
}
|
||||
|
||||
engine.register_fn("process_complex", process_complex);
|
||||
```
|
||||
|
||||
#### Option 2: Use Dynamic parameters for more flexibility
|
||||
|
||||
```rust
|
||||
fn process_complex_dynamic(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
// Check arguments
|
||||
if args.len() != 2 {
|
||||
return Err("Expected exactly 2 arguments".into());
|
||||
}
|
||||
|
||||
// Get mutable reference to the complex data
|
||||
let data = args[0].write_lock::<ComplexData>()
|
||||
.ok_or_else(|| "First argument must be ComplexData".into())?;
|
||||
|
||||
// Get the factor
|
||||
let factor = args[1].as_float().map_err(|_| "Second argument must be a number".into())?;
|
||||
|
||||
// Process the data
|
||||
let sum: f64 = data.values.iter().sum();
|
||||
data.values.push(sum * factor);
|
||||
|
||||
Ok((sum * factor).into())
|
||||
}
|
||||
```
|
||||
|
||||
### Handling Variable Arguments
|
||||
|
||||
For functions that accept a variable number of arguments:
|
||||
|
||||
```rust
|
||||
fn sum_all(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
let mut total: i64 = 0;
|
||||
|
||||
for arg in args.iter() {
|
||||
total += arg.as_int().map_err(|_| "All arguments must be integers".into())?;
|
||||
}
|
||||
|
||||
Ok(total.into())
|
||||
}
|
||||
|
||||
engine.register_fn("sum_all", sum_all);
|
||||
|
||||
// In Rhai:
|
||||
// sum_all(1, 2, 3, 4, 5) -> 15
|
||||
// sum_all(10, 20) -> 30
|
||||
```
|
||||
|
||||
### Handling Optional Arguments
|
||||
|
||||
For functions with optional arguments, use function overloading:
|
||||
|
||||
```rust
|
||||
fn create_person(name: &str) -> Person {
|
||||
Person { name: name.to_string(), age: 30 } // Default age
|
||||
}
|
||||
|
||||
fn create_person_with_age(name: &str, age: i64) -> Person {
|
||||
Person { name: name.to_string(), age }
|
||||
}
|
||||
|
||||
engine.register_fn("create_person", create_person);
|
||||
engine.register_fn("create_person", create_person_with_age);
|
||||
|
||||
// In Rhai:
|
||||
// create_person("John") -> Person with name "John" and age 30
|
||||
// create_person("John", 25) -> Person with name "John" and age 25
|
||||
```
|
||||
|
||||
### Handling Default Arguments
|
||||
|
||||
Rhai doesn't directly support default arguments, but you can simulate them:
|
||||
|
||||
```rust
|
||||
fn configure(options: &mut Map) -> Result<(), Box<EvalAltResult>> {
|
||||
// Check if certain options exist, if not, set defaults
|
||||
if !options.contains_key("timeout") {
|
||||
options.insert("timeout".into(), 30_i64.into());
|
||||
}
|
||||
|
||||
if !options.contains_key("retry") {
|
||||
options.insert("retry".into(), true.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
engine.register_fn("configure", configure);
|
||||
|
||||
// In Rhai:
|
||||
// let options = #{};
|
||||
// configure(options);
|
||||
// print(options.timeout); // Prints 30
|
||||
```
|
||||
|
||||
### Handling Mutable and Immutable References
|
||||
|
||||
Rhai supports both mutable and immutable references:
|
||||
|
||||
```rust
|
||||
// Function taking an immutable reference
|
||||
fn get_name(person: &Person) -> String {
|
||||
person.name.clone()
|
||||
}
|
||||
|
||||
// Function taking a mutable reference
|
||||
fn increment_age(person: &mut Person) {
|
||||
person.age += 1;
|
||||
}
|
||||
|
||||
engine.register_fn("get_name", get_name);
|
||||
engine.register_fn("increment_age", increment_age);
|
||||
```
|
||||
|
||||
### Converting Between Rust and Rhai Types
|
||||
|
||||
When you need to convert between Rust and Rhai types:
|
||||
|
||||
```rust
|
||||
// Convert a Rust HashMap to a Rhai Map
|
||||
fn create_config() -> Map {
|
||||
let mut rust_map = HashMap::new();
|
||||
rust_map.insert("server".to_string(), "localhost".to_string());
|
||||
rust_map.insert("port".to_string(), "8080".to_string());
|
||||
|
||||
// Convert to Rhai Map
|
||||
let mut rhai_map = Map::new();
|
||||
for (k, v) in rust_map {
|
||||
rhai_map.insert(k.into(), v.into());
|
||||
}
|
||||
|
||||
rhai_map
|
||||
}
|
||||
|
||||
// Convert a Rhai Array to a Rust Vec
|
||||
fn process_array(arr: Array) -> Result<i64, Box<EvalAltResult>> {
|
||||
// Convert to Rust Vec<i64>
|
||||
let rust_vec: Result<Vec<i64>, _> = arr.iter()
|
||||
.map(|v| v.as_int().map_err(|_| "Array must contain only integers".into()))
|
||||
.collect();
|
||||
|
||||
let numbers = rust_vec?;
|
||||
Ok(numbers.iter().sum())
|
||||
}
|
||||
```
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Example 1: Basic Function Registration and Struct Handling
|
||||
|
||||
```rust
|
||||
use rhai::{Engine, EvalAltResult, RegisterFn};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Person {
|
||||
name: String,
|
||||
age: i64,
|
||||
}
|
||||
|
||||
impl Person {
|
||||
fn new(name: &str, age: i64) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
age,
|
||||
}
|
||||
}
|
||||
|
||||
fn greet(&self) -> String {
|
||||
format!("Hello, my name is {} and I am {} years old.", self.name, self.age)
|
||||
}
|
||||
|
||||
fn have_birthday(&mut self) {
|
||||
self.age += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn is_adult(person: &Person) -> bool {
|
||||
person.age >= 18
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<EvalAltResult>> {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register the Person type
|
||||
engine
|
||||
.register_type_with_name::<Person>("Person")
|
||||
.register_fn("new_person", Person::new)
|
||||
.register_fn("greet", Person::greet)
|
||||
.register_fn("have_birthday", Person::have_birthday)
|
||||
.register_fn("is_adult", is_adult);
|
||||
|
||||
// Run a script that uses the Person type
|
||||
let result = engine.eval::<String>(r#"
|
||||
let p = new_person("John", 17);
|
||||
let greeting = p.greet();
|
||||
|
||||
if !is_adult(p) {
|
||||
p.have_birthday();
|
||||
}
|
||||
|
||||
greeting + " Now I am " + p.age.to_string() + " years old."
|
||||
"#)?;
|
||||
|
||||
println!("{}", result);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Error Handling and Complex Return Types
|
||||
|
||||
```rust
|
||||
use rhai::{Engine, EvalAltResult, Map, Dynamic};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Product {
|
||||
id: i64,
|
||||
name: String,
|
||||
price: f64,
|
||||
}
|
||||
|
||||
fn get_product(id: i64) -> Result<Product, Box<EvalAltResult>> {
|
||||
match id {
|
||||
1 => Ok(Product { id: 1, name: "Laptop".to_string(), price: 999.99 }),
|
||||
2 => Ok(Product { id: 2, name: "Phone".to_string(), price: 499.99 }),
|
||||
_ => Err("Product not found".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_total(products: Array) -> Result<f64, Box<EvalAltResult>> {
|
||||
let mut total = 0.0;
|
||||
|
||||
for product_dynamic in products.iter() {
|
||||
let product = product_dynamic.clone().try_cast::<Product>()
|
||||
.map_err(|_| "Invalid product in array".into())?;
|
||||
|
||||
total += product.price;
|
||||
}
|
||||
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
fn get_product_map() -> Map {
|
||||
let mut map = Map::new();
|
||||
|
||||
map.insert("laptop".into(),
|
||||
Dynamic::from(Product { id: 1, name: "Laptop".to_string(), price: 999.99 }));
|
||||
map.insert("phone".into(),
|
||||
Dynamic::from(Product { id: 2, name: "Phone".to_string(), price: 499.99 }));
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<EvalAltResult>> {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
engine
|
||||
.register_type_with_name::<Product>("Product")
|
||||
.register_fn("get_product", get_product)
|
||||
.register_fn("calculate_total", calculate_total)
|
||||
.register_fn("get_product_map", get_product_map);
|
||||
|
||||
let result = engine.eval::<f64>(r#"
|
||||
let products = [];
|
||||
|
||||
// Try to get products
|
||||
try {
|
||||
products.push(get_product(1));
|
||||
products.push(get_product(2));
|
||||
products.push(get_product(3)); // This will throw an error
|
||||
} catch(err) {
|
||||
print(`Error: ${err}`);
|
||||
}
|
||||
|
||||
// Get products from map
|
||||
let product_map = get_product_map();
|
||||
products.push(product_map.laptop);
|
||||
|
||||
calculate_total(products)
|
||||
"#)?;
|
||||
|
||||
println!("Total: ${:.2}", result);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
@ -1,134 +0,0 @@
|
||||
|
||||
### Error Handling in Dynamic Functions
|
||||
|
||||
When working with the dynamic function signature, error handling is slightly different:
|
||||
|
||||
```rust
|
||||
fn dynamic_function(ctx: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
// Get the position information from the context
|
||||
let pos = ctx.position();
|
||||
|
||||
// Validate arguments
|
||||
if args.len() < 2 {
|
||||
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Expected at least 2 arguments, got {}", args.len()),
|
||||
pos
|
||||
)));
|
||||
}
|
||||
|
||||
// Try to convert arguments with proper error handling
|
||||
let arg1 = match args[0].as_int() {
|
||||
Ok(val) => val,
|
||||
Err(_) => return Err(Box::new(EvalAltResult::ErrorMismatchOutputType(
|
||||
"Expected first argument to be an integer".into(),
|
||||
pos,
|
||||
"i64".into()
|
||||
)))
|
||||
};
|
||||
|
||||
// Process with error handling
|
||||
if arg1 <= 0 {
|
||||
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
"First argument must be positive".into(),
|
||||
pos
|
||||
)));
|
||||
}
|
||||
|
||||
// Return success
|
||||
Ok(Dynamic::from(arg1 * 2))
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Working with Function Pointers
|
||||
|
||||
You can create function pointers that bind to Rust functions:
|
||||
|
||||
```rust
|
||||
fn my_awesome_fn(ctx: NativeCallContext, args: &mut[&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
// Check number of arguments
|
||||
if args.len() != 2 {
|
||||
return Err("one argument is required, plus the object".into());
|
||||
}
|
||||
|
||||
// Get call arguments
|
||||
let x = args[1].try_cast::<i64>().map_err(|_| "argument must be an integer".into())?;
|
||||
|
||||
// Get mutable reference to the object map, which is passed as the first argument
|
||||
let map = &mut *args[0].as_map_mut().map_err(|_| "object must be a map".into())?;
|
||||
|
||||
// Do something awesome here ...
|
||||
let result = x * 2;
|
||||
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
// Register a function to create a pre-defined object
|
||||
engine.register_fn("create_awesome_object", || {
|
||||
// Use an object map as base
|
||||
let mut map = Map::new();
|
||||
|
||||
// Create a function pointer that binds to 'my_awesome_fn'
|
||||
let fp = FnPtr::from_fn("awesome", my_awesome_fn)?;
|
||||
// ^ name of method
|
||||
// ^ native function
|
||||
|
||||
// Store the function pointer in the object map
|
||||
map.insert("awesome".into(), fp.into());
|
||||
|
||||
Ok(Dynamic::from_map(map))
|
||||
});
|
||||
```
|
||||
|
||||
### Creating Rust Closures from Rhai Functions
|
||||
|
||||
You can encapsulate a Rhai script as a Rust closure:
|
||||
|
||||
```rust
|
||||
use rhai::{Engine, Func};
|
||||
|
||||
let engine = Engine::new();
|
||||
|
||||
let script = "fn calc(x, y) { x + y.len < 42 }";
|
||||
|
||||
// Create a Rust closure from a Rhai function
|
||||
let func = Func::<(i64, &str), bool>::create_from_script(
|
||||
engine, // the 'Engine' is consumed into the closure
|
||||
script, // the script
|
||||
"calc" // the entry-point function name
|
||||
)?;
|
||||
|
||||
// Call the closure
|
||||
let result = func(123, "hello")?;
|
||||
|
||||
// Pass it as a callback to another function
|
||||
schedule_callback(func);
|
||||
```
|
||||
|
||||
### Calling Rhai Functions from Rust
|
||||
|
||||
You can call Rhai functions from Rust:
|
||||
|
||||
```rust
|
||||
// Compile the script to AST
|
||||
let ast = engine.compile(script)?;
|
||||
|
||||
// Create a custom 'Scope'
|
||||
let mut scope = Scope::new();
|
||||
|
||||
// Add variables to the scope
|
||||
scope.push("my_var", 42_i64);
|
||||
scope.push("my_string", "hello, world!");
|
||||
scope.push_constant("MY_CONST", true);
|
||||
|
||||
// Call a function defined in the script
|
||||
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", ("abc", 123_i64))?;
|
||||
|
||||
// For a function with one parameter, use a tuple with a trailing comma
|
||||
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", (123_i64,))?;
|
||||
|
||||
// For a function with no parameters
|
||||
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", ())?;
|
||||
```
|
@ -1,187 +0,0 @@
|
||||
## Best Practices and Optimization
|
||||
|
||||
When wrapping Rust functions for use with Rhai, following these best practices will help you create efficient, maintainable, and robust code.
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
1. **Minimize Cloning**: Rhai often requires cloning data, but you can minimize this overhead:
|
||||
```rust
|
||||
// Prefer immutable references when possible
|
||||
fn process_data(data: &MyStruct) -> i64 {
|
||||
// Work with data without cloning
|
||||
data.value * 2
|
||||
}
|
||||
|
||||
// Use mutable references for in-place modifications
|
||||
fn update_data(data: &mut MyStruct) {
|
||||
data.value += 1;
|
||||
}
|
||||
```
|
||||
|
||||
2. **Avoid Excessive Type Conversions**: Converting between Rhai's Dynamic type and Rust types has overhead:
|
||||
```rust
|
||||
// Inefficient - multiple conversions
|
||||
fn process_inefficient(ctx: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
let value = args[0].as_int()?;
|
||||
let result = value * 2;
|
||||
Ok(Dynamic::from(result))
|
||||
}
|
||||
|
||||
// More efficient - use typed parameters when possible
|
||||
fn process_efficient(value: i64) -> i64 {
|
||||
value * 2
|
||||
}
|
||||
```
|
||||
|
||||
3. **Batch Operations**: For operations on collections, batch processing is more efficient:
|
||||
```rust
|
||||
// Process an entire array at once rather than element by element
|
||||
fn sum_array(arr: Array) -> Result<i64, Box<EvalAltResult>> {
|
||||
arr.iter()
|
||||
.map(|v| v.as_int())
|
||||
.collect::<Result<Vec<i64>, _>>()
|
||||
.map(|nums| nums.iter().sum())
|
||||
.map_err(|_| "Array must contain only integers".into())
|
||||
}
|
||||
```
|
||||
|
||||
4. **Compile Scripts Once**: Reuse compiled ASTs for scripts that are executed multiple times:
|
||||
```rust
|
||||
// Compile once
|
||||
let ast = engine.compile(script)?;
|
||||
|
||||
// Execute multiple times with different parameters
|
||||
for i in 0..10 {
|
||||
let result = engine.eval_ast::<i64>(&ast)?;
|
||||
println!("Result {}: {}", i, result);
|
||||
}
|
||||
```
|
||||
|
||||
### Thread Safety
|
||||
|
||||
1. **Use Sync Mode When Needed**: If you need thread safety, use the `sync` feature:
|
||||
```rust
|
||||
// In Cargo.toml
|
||||
// rhai = { version = "1.x", features = ["sync"] }
|
||||
|
||||
// This creates a thread-safe engine
|
||||
let engine = Engine::new();
|
||||
|
||||
// Now you can safely share the engine between threads
|
||||
std::thread::spawn(move || {
|
||||
let result = engine.eval::<i64>("40 + 2")?;
|
||||
println!("Result: {}", result);
|
||||
});
|
||||
```
|
||||
|
||||
2. **Clone the Engine for Multiple Threads**: When not using `sync`, clone the engine for each thread:
|
||||
```rust
|
||||
let engine = Engine::new();
|
||||
|
||||
let handles: Vec<_> = (0..5).map(|i| {
|
||||
let engine_clone = engine.clone();
|
||||
std::thread::spawn(move || {
|
||||
let result = engine_clone.eval::<i64>(&format!("{} + 2", i * 10))?;
|
||||
println!("Thread {}: {}", i, result);
|
||||
})
|
||||
}).collect();
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
### Memory Management
|
||||
|
||||
1. **Control Scope Size**: Be mindful of the size of your scopes:
|
||||
```rust
|
||||
// Create a new scope for each operation to avoid memory buildup
|
||||
for item in items {
|
||||
let mut scope = Scope::new();
|
||||
scope.push("item", item);
|
||||
engine.eval_with_scope::<()>(&mut scope, "process(item)")?;
|
||||
}
|
||||
```
|
||||
|
||||
2. **Limit Script Complexity**: Use engine options to limit script complexity:
|
||||
```rust
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Set limits to prevent scripts from consuming too many resources
|
||||
engine.set_max_expr_depths(64, 64) // Max expression/statement depth
|
||||
.set_max_function_expr_depth(64) // Max function depth
|
||||
.set_max_array_size(10000) // Max array size
|
||||
.set_max_map_size(10000) // Max map size
|
||||
.set_max_string_size(10000) // Max string size
|
||||
.set_max_call_levels(64); // Max call stack depth
|
||||
```
|
||||
|
||||
3. **Use Shared Values Carefully**: Shared values (via closures) have reference-counting overhead:
|
||||
```rust
|
||||
// Avoid unnecessary capturing in closures when possible
|
||||
engine.register_fn("process", |x: i64| x * 2);
|
||||
|
||||
// Instead of capturing large data structures
|
||||
let large_data = vec![1, 2, 3, /* ... thousands of items ... */];
|
||||
engine.register_fn("process_data", move |idx: i64| {
|
||||
if idx >= 0 && (idx as usize) < large_data.len() {
|
||||
large_data[idx as usize]
|
||||
} else {
|
||||
0
|
||||
}
|
||||
});
|
||||
|
||||
// Consider registering a lookup function instead
|
||||
let large_data = std::sync::Arc::new(vec![1, 2, 3, /* ... thousands of items ... */]);
|
||||
let data_ref = large_data.clone();
|
||||
engine.register_fn("lookup", move |idx: i64| {
|
||||
if idx >= 0 && (idx as usize) < data_ref.len() {
|
||||
data_ref[idx as usize]
|
||||
} else {
|
||||
0
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### API Design
|
||||
|
||||
1. **Consistent Naming**: Use consistent naming conventions:
|
||||
```rust
|
||||
// Good: Consistent naming pattern
|
||||
engine.register_fn("create_user", create_user)
|
||||
.register_fn("update_user", update_user)
|
||||
.register_fn("delete_user", delete_user);
|
||||
|
||||
// Bad: Inconsistent naming
|
||||
engine.register_fn("create_user", create_user)
|
||||
.register_fn("user_update", update_user)
|
||||
.register_fn("remove", delete_user);
|
||||
```
|
||||
|
||||
2. **Logical Function Grouping**: Group related functions together:
|
||||
```rust
|
||||
// Register all string-related functions together
|
||||
engine.register_fn("str_length", |s: &str| s.len() as i64)
|
||||
.register_fn("str_uppercase", |s: &str| s.to_uppercase())
|
||||
.register_fn("str_lowercase", |s: &str| s.to_lowercase());
|
||||
|
||||
// Register all math-related functions together
|
||||
engine.register_fn("math_sin", |x: f64| x.sin())
|
||||
.register_fn("math_cos", |x: f64| x.cos())
|
||||
.register_fn("math_tan", |x: f64| x.tan());
|
||||
```
|
||||
|
||||
3. **Comprehensive Documentation**: Document your API thoroughly:
|
||||
```rust
|
||||
// Add documentation for script writers
|
||||
let mut engine = Engine::new();
|
||||
|
||||
#[cfg(feature = "metadata")]
|
||||
{
|
||||
// Add function documentation
|
||||
engine.register_fn("calculate_tax", calculate_tax)
|
||||
.register_fn_metadata("calculate_tax", |metadata| {
|
||||
metadata.set_doc_comment("Calculates tax based on income and rate.\n\nParameters:\n- income: Annual income\n- rate: Tax rate (0.0-1.0)\n\nReturns: Calculated tax amount");
|
||||
});
|
||||
}
|
||||
```
|
@ -1,42 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Change to directory where this script is located
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
rm -f ./target/debug/herodo
|
||||
|
||||
# Build the herodo project
|
||||
echo "Building herodo..."
|
||||
cargo build --bin herodo
|
||||
# cargo build --release --bin herodo
|
||||
|
||||
# Check if the build was successful
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Build failed. Please check the error messages."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Echo a success message
|
||||
echo "Build successful!"
|
||||
|
||||
mkdir -p ~/hero/bin/
|
||||
cp target/debug/herodo ~/hero/bin/herodo
|
||||
|
||||
# Check if a script name was provided
|
||||
if [ $# -eq 1 ]; then
|
||||
echo "Running specified test: $1"
|
||||
|
||||
# Check if the script exists in src/rhaiexamples/
|
||||
if [ -f "src/rhaiexamples/$1.rhai" ]; then
|
||||
herodo "src/rhaiexamples/$1.rhai"
|
||||
# Check if the script exists in src/herodo/scripts/
|
||||
elif [ -f "src/herodo/scripts/$1.rhai" ]; then
|
||||
herodo "src/herodo/scripts/$1.rhai"
|
||||
else
|
||||
echo "Error: Script $1.rhai not found in src/rhaiexamples/ or src/herodo/scripts/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
fi
|
@ -1,207 +0,0 @@
|
||||
# Buildah Debug Implementation Plan
|
||||
|
||||
## Current State
|
||||
|
||||
1. The `Builder` struct already has a `debug` field and methods to get and set it (`debug()` and `set_debug()`).
|
||||
2. There's a thread-local `DEBUG` variable with functions to get and set it.
|
||||
3. The `execute_buildah_command` function checks the thread-local debug flag and outputs some debug information.
|
||||
4. There's an unused `execute_buildah_command_with_debug` function that takes a `Builder` reference.
|
||||
|
||||
## Requirements
|
||||
|
||||
1. Use only the Builder's debug flag, not the thread-local debug flag.
|
||||
2. When debug is true, output stdout/stderr regardless of whether the command succeeds or fails.
|
||||
3. If debug is false but there's an error, still output all information.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### 1. Keep the Existing Thread-Local DEBUG Variable
|
||||
|
||||
We'll keep the existing thread-local DEBUG variable that's already in the code:
|
||||
|
||||
```rust
|
||||
// Thread-local storage for debug flag
|
||||
thread_local! {
|
||||
static DEBUG: std::cell::RefCell<bool> = std::cell::RefCell::new(false);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Modify the Builder Methods to Set/Clear the Thread-Local Debug Flag
|
||||
|
||||
We'll modify each Builder method to set the thread-local debug flag from the Builder's debug flag before calling `execute_buildah_command` and restore it afterward:
|
||||
|
||||
```rust
|
||||
pub fn run(&self, command: &str) -> Result<CommandResult, BuildahError> {
|
||||
if let Some(container_id) = &self.container_id {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag from the Builder's debug flag
|
||||
set_thread_local_debug(self.debug);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&["run", container_id, "sh", "-c", command]);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
result
|
||||
} else {
|
||||
Err(BuildahError::Other("No container ID available".to_string()))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Keep the Existing execute_buildah_command Function
|
||||
|
||||
The existing `execute_buildah_command` function already checks the thread-local debug flag, so we don't need to modify it:
|
||||
|
||||
```rust
|
||||
pub fn execute_buildah_command(args: &[&str]) -> Result<CommandResult, BuildahError> {
|
||||
// Get the debug flag from thread-local storage
|
||||
let debug = thread_local_debug();
|
||||
|
||||
if debug {
|
||||
println!("Executing buildah command: buildah {}", args.join(" "));
|
||||
}
|
||||
|
||||
// ... rest of the function ...
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Update the execute_buildah_command Function to Output stdout/stderr
|
||||
|
||||
We need to modify the `execute_buildah_command` function to output stdout/stderr when debug is true, regardless of success/failure:
|
||||
|
||||
```rust
|
||||
pub fn execute_buildah_command(args: &[&str]) -> Result<CommandResult, BuildahError> {
|
||||
// Get the debug flag from thread-local storage
|
||||
let debug = thread_local_debug();
|
||||
|
||||
if debug {
|
||||
println!("Executing buildah command: buildah {}", args.join(" "));
|
||||
}
|
||||
|
||||
let output = Command::new("buildah")
|
||||
.args(args)
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
|
||||
let result = CommandResult {
|
||||
stdout,
|
||||
stderr,
|
||||
success: output.status.success(),
|
||||
code: output.status.code().unwrap_or(-1),
|
||||
};
|
||||
|
||||
// Always output stdout/stderr when debug is true
|
||||
if debug {
|
||||
if !result.stdout.is_empty() {
|
||||
println!("Command stdout: {}", result.stdout);
|
||||
}
|
||||
|
||||
if !result.stderr.is_empty() {
|
||||
println!("Command stderr: {}", result.stderr);
|
||||
}
|
||||
|
||||
if result.success {
|
||||
println!("Command succeeded with code {}", result.code);
|
||||
} else {
|
||||
println!("Command failed with code {}", result.code);
|
||||
}
|
||||
}
|
||||
|
||||
if result.success {
|
||||
Ok(result)
|
||||
} else {
|
||||
// If command failed and debug is false, output stderr
|
||||
if !debug {
|
||||
println!("Command failed with code {}: {}", result.code, result.stderr.trim());
|
||||
}
|
||||
Err(BuildahError::CommandFailed(format!("Command failed with code {}: {}",
|
||||
result.code, result.stderr.trim())))
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
// Always output error information
|
||||
println!("Command execution failed: {}", e);
|
||||
Err(BuildahError::CommandExecutionFailed(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Handle Static Methods
|
||||
|
||||
For static methods, we'll just call `execute_buildah_command` directly, which will use the thread-local debug flag:
|
||||
|
||||
```rust
|
||||
pub fn images() -> Result<Vec<Image>, BuildahError> {
|
||||
let result = execute_buildah_command(&["images", "--json"])?;
|
||||
// Rest of the method...
|
||||
}
|
||||
```
|
||||
|
||||
If we want to support debugging in static methods, we could add an optional debug parameter:
|
||||
|
||||
```rust
|
||||
pub fn images(debug: bool) -> Result<Vec<Image>, BuildahError> {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag
|
||||
set_thread_local_debug(debug);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&["images", "--json"]);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
// Process the result
|
||||
match result {
|
||||
Ok(cmd_result) => {
|
||||
// Parse JSON and return images...
|
||||
},
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
// Backward compatibility method
|
||||
pub fn images() -> Result<Vec<Image>, BuildahError> {
|
||||
Self::images(false)
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Update Rhai Bindings
|
||||
|
||||
We still need to update the Rhai bindings to expose the debug functionality:
|
||||
|
||||
```rust
|
||||
// Add a debug getter and setter
|
||||
engine.register_get("debug", |builder: &mut Builder| builder.debug());
|
||||
engine.register_set("debug", |builder: &mut Builder, debug: bool| { builder.set_debug(debug); });
|
||||
```
|
||||
|
||||
### 7. Remove Unused Code
|
||||
|
||||
We can remove the unused `execute_buildah_command_with_debug` function.
|
||||
|
||||
## Implementation Flow
|
||||
|
||||
1. Modify the `execute_buildah_command` function to output stdout/stderr when debug is true
|
||||
2. Update all Builder methods to set/restore the thread-local debug flag
|
||||
3. Update static methods to optionally accept a debug parameter
|
||||
4. Update Rhai bindings to expose the debug functionality
|
||||
5. Remove unused code
|
||||
|
||||
## Benefits
|
||||
|
||||
1. Minimal changes to the existing codebase
|
||||
2. No changes to function signatures
|
||||
3. Backward compatibility with existing code
|
||||
4. Improved debugging capabilities
|
1
example.conf
Normal file
1
example.conf
Normal file
@ -0,0 +1 @@
|
||||
EXAMPLE FILE TO TEST
|
8
example_Dockerfile
Normal file
8
example_Dockerfile
Normal file
@ -0,0 +1,8 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM node:lts-alpine
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN yarn install --production
|
||||
CMD ["node", "src/index.js"]
|
||||
EXPOSE 3000
|
115
examples/buildah.rs
Normal file
115
examples/buildah.rs
Normal file
@ -0,0 +1,115 @@
|
||||
//! Example usage of the buildah module
|
||||
//!
|
||||
//! This file demonstrates how to use the buildah module to perform
|
||||
//! common container operations like creating containers, running commands,
|
||||
//! and managing images.
|
||||
|
||||
use sal::virt::buildah::{self, BuildahError};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Run a complete buildah workflow example
|
||||
pub fn run_buildah_example() -> Result<(), BuildahError> {
|
||||
println!("Starting buildah example workflow...");
|
||||
|
||||
// Step 1: Create a container from an image
|
||||
println!("\n=== Creating container from fedora:latest ===");
|
||||
let result = buildah::from("fedora:latest")?;
|
||||
let container_id = result.stdout.trim();
|
||||
println!("Created container: {}", container_id);
|
||||
|
||||
// Step 2: Run a command in the container
|
||||
println!("\n=== Installing nginx in container ===");
|
||||
// Use chroot isolation to avoid BPF issues
|
||||
let install_result = buildah::run_with_isolation(container_id, "dnf install -y nginx", "chroot")?;
|
||||
println!("{:#?}", install_result);
|
||||
println!("Installation output: {}", install_result.stdout);
|
||||
|
||||
// Step 3: Copy a file into the container
|
||||
println!("\n=== Copying configuration file to container ===");
|
||||
buildah::copy(container_id, "./example.conf", "/etc/example.conf").unwrap();
|
||||
|
||||
// Step 4: Configure container metadata
|
||||
println!("\n=== Configuring container metadata ===");
|
||||
let mut config_options = HashMap::new();
|
||||
config_options.insert("port".to_string(), "80".to_string());
|
||||
config_options.insert("label".to_string(), "maintainer=example@example.com".to_string());
|
||||
config_options.insert("entrypoint".to_string(), "/usr/sbin/nginx".to_string());
|
||||
|
||||
buildah::config(container_id, config_options)?;
|
||||
println!("Container configured");
|
||||
|
||||
// Step 5: Commit the container to create a new image
|
||||
println!("\n=== Committing container to create image ===");
|
||||
let image_name = "my-nginx:latest";
|
||||
buildah::image_commit(container_id, image_name, Some("docker"), true, true)?;
|
||||
println!("Created image: {}", image_name);
|
||||
|
||||
// Step 6: List images to verify our new image exists
|
||||
println!("\n=== Listing images ===");
|
||||
let images = buildah::images()?;
|
||||
println!("Found {} images:", images.len());
|
||||
for image in images {
|
||||
println!(" ID: {}", image.id);
|
||||
println!(" Names: {}", image.names.join(", "));
|
||||
println!(" Size: {}", image.size);
|
||||
println!(" Created: {}", image.created);
|
||||
println!();
|
||||
}
|
||||
|
||||
// // Step 7: Clean up (optional in a real workflow)
|
||||
println!("\n=== Cleaning up ===");
|
||||
buildah::image_remove(image_name).unwrap();
|
||||
|
||||
println!("\nBuildah example workflow completed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Demonstrate how to build an image from a Containerfile/Dockerfile
|
||||
pub fn build_image_example() -> Result<(), BuildahError> {
|
||||
println!("Building an image from a Containerfile...");
|
||||
|
||||
// Use the build function with tag, context directory, and isolation to avoid BPF issues
|
||||
let result = buildah::build(Some("my-app:latest"), ".", "example_Dockerfile", Some("chroot"))?;
|
||||
|
||||
println!("Build output: {}", result.stdout);
|
||||
println!("Image built successfully!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Example of pulling and pushing images
|
||||
pub fn registry_operations_example() -> Result<(), BuildahError> {
|
||||
println!("Demonstrating registry operations...");
|
||||
|
||||
// Pull an image
|
||||
println!("\n=== Pulling an image ===");
|
||||
buildah::image_pull("docker.io/library/alpine:latest", true)?;
|
||||
println!("Image pulled successfully");
|
||||
|
||||
// Tag the image
|
||||
println!("\n=== Tagging the image ===");
|
||||
buildah::image_tag("alpine:latest", "my-alpine:v1.0")?;
|
||||
println!("Image tagged successfully");
|
||||
|
||||
// Push an image (this would typically go to a real registry)
|
||||
// println!("\n=== Pushing an image (example only) ===");
|
||||
// println!("In a real scenario, you would push to a registry with:");
|
||||
// println!("buildah::image_push(\"my-alpine:v1.0\", \"docker://registry.example.com/my-alpine:v1.0\", true)");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Main function to run all examples
|
||||
pub fn run_all_examples() -> Result<(), BuildahError> {
|
||||
println!("=== BUILDAH MODULE EXAMPLES ===\n");
|
||||
|
||||
run_buildah_example()?;
|
||||
build_image_example()?;
|
||||
registry_operations_example()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let _ = run_all_examples().unwrap();
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
// File: /root/code/git.ourworld.tf/herocode/sal/examples/container_example.rs
|
||||
|
||||
use std::error::Error;
|
||||
use sal::virt::nerdctl::Container;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// Create a container from an image
|
||||
println!("Creating container from image...");
|
||||
let container = Container::from_image("my-nginx", "nginx:latest")?
|
||||
.with_port("8080:80")
|
||||
.with_env("NGINX_HOST", "example.com")
|
||||
.with_volume("/tmp/nginx:/usr/share/nginx/html")
|
||||
.with_health_check("curl -f http://localhost/ || exit 1")
|
||||
.with_detach(true)
|
||||
.build()?;
|
||||
|
||||
println!("Container created successfully");
|
||||
|
||||
// Execute a command in the container
|
||||
println!("Executing command in container...");
|
||||
let result = container.exec("echo 'Hello from container'")?;
|
||||
println!("Command output: {}", result.stdout);
|
||||
|
||||
// Get container status
|
||||
println!("Getting container status...");
|
||||
let status = container.status()?;
|
||||
println!("Container status: {}", status.status);
|
||||
|
||||
// Get resource usage
|
||||
println!("Getting resource usage...");
|
||||
let resources = container.resources()?;
|
||||
println!("CPU usage: {}", resources.cpu_usage);
|
||||
println!("Memory usage: {}", resources.memory_usage);
|
||||
|
||||
// Stop and remove the container
|
||||
println!("Stopping and removing container...");
|
||||
container.stop()?;
|
||||
container.remove()?;
|
||||
|
||||
println!("Container stopped and removed");
|
||||
|
||||
// Get a container by name (if it exists)
|
||||
println!("\nGetting a container by name...");
|
||||
match Container::new("existing-container") {
|
||||
Ok(container) => {
|
||||
if container.container_id.is_some() {
|
||||
println!("Found container with ID: {}", container.container_id.as_ref().unwrap());
|
||||
|
||||
// Perform operations on the existing container
|
||||
let status = container.status()?;
|
||||
println!("Container status: {}", status.status);
|
||||
} else {
|
||||
println!("Container exists but has no ID");
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error getting container: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
84
examples/nerdctl.rs
Normal file
84
examples/nerdctl.rs
Normal file
@ -0,0 +1,84 @@
|
||||
//! Example usage of the nerdctl module
|
||||
//!
|
||||
//! This file demonstrates how to use the nerdctl module to perform
|
||||
//! common container operations like creating containers, running commands,
|
||||
//! and managing images.
|
||||
|
||||
use sal::virt::nerdctl::{self, NerdctlError};
|
||||
|
||||
/// Run a complete nerdctl workflow example
|
||||
pub fn run_nerdctl_example() -> Result<(), NerdctlError> {
|
||||
println!("Starting nerdctl example workflow...");
|
||||
|
||||
// Step 1: Pull an image
|
||||
println!("\n=== Pulling nginx:latest image ===");
|
||||
let pull_result = nerdctl::image_pull("nginx:latest")?;
|
||||
println!("Pull output: {}", pull_result.stdout);
|
||||
|
||||
// Step 2: Create a container from the image
|
||||
println!("\n=== Creating container from nginx:latest ===");
|
||||
// Use "native" snapshotter to avoid overlay mount issues
|
||||
let run_result = nerdctl::run("nginx:latest", Some("my-nginx"), true, Some(&["8080:80"]), Some("native"))?;
|
||||
println!("Container created: {}", run_result.stdout.trim());
|
||||
let container_id = "my-nginx"; // Using the name we specified
|
||||
|
||||
// Step 3: Execute a command in the container
|
||||
println!("\n=== Installing curl in container ===");
|
||||
let update_result = nerdctl::exec(container_id, "apt-get update")?;
|
||||
println!("Update output: {}", update_result.stdout);
|
||||
|
||||
let install_result = nerdctl::exec(container_id, "apt-get install -y curl")?;
|
||||
println!("Installation output: {}", install_result.stdout);
|
||||
|
||||
// Step 4: Copy a file into the container (assuming nginx.conf exists)
|
||||
println!("\n=== Copying configuration file to container ===");
|
||||
nerdctl::copy("./nginx.conf", format!("{}:/etc/nginx/nginx.conf", container_id).as_str())?;
|
||||
|
||||
// Step 5: Commit the container to create a new image
|
||||
println!("\n=== Committing container to create image ===");
|
||||
let image_name = "my-custom-nginx:latest";
|
||||
nerdctl::image_commit(container_id, image_name)?;
|
||||
println!("Created image: {}", image_name);
|
||||
|
||||
// Step 6: Stop and remove the container
|
||||
println!("\n=== Stopping and removing container ===");
|
||||
nerdctl::stop(container_id)?;
|
||||
nerdctl::remove(container_id)?;
|
||||
println!("Container stopped and removed");
|
||||
|
||||
// Step 7: Create a new container from our custom image
|
||||
println!("\n=== Creating container from custom image ===");
|
||||
// Use "native" snapshotter to avoid overlay mount issues
|
||||
nerdctl::run(image_name, Some("nginx-custom"), true, Some(&["8081:80"]), Some("native"))?;
|
||||
println!("Custom container created");
|
||||
|
||||
// Step 8: List images
|
||||
println!("\n=== Listing images ===");
|
||||
let images_result = nerdctl::images()?;
|
||||
println!("Images: \n{}", images_result.stdout);
|
||||
|
||||
// Step 9: Clean up (optional in a real workflow)
|
||||
println!("\n=== Cleaning up ===");
|
||||
nerdctl::stop("nginx-custom")?;
|
||||
nerdctl::remove("nginx-custom")?;
|
||||
nerdctl::image_remove(image_name)?;
|
||||
|
||||
println!("\nNerdctl example workflow completed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Main function to run all examples
|
||||
pub fn run_all_examples() -> Result<(), NerdctlError> {
|
||||
println!("=== NERDCTL MODULE EXAMPLES ===\n");
|
||||
|
||||
run_nerdctl_example()?;
|
||||
|
||||
println!("\nNote that these examples require nerdctl to be installed on your system");
|
||||
println!("and may require root/sudo privileges depending on your setup.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let _ = run_all_examples().unwrap();
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
//! Example of using the package management module
|
||||
//!
|
||||
//! This example demonstrates how to use the package management module
|
||||
//! to install, remove, and manage packages on different platforms.
|
||||
|
||||
use sal::os::package::{PackHero, Platform};
|
||||
|
||||
fn main() {
|
||||
// Create a new PackHero instance
|
||||
let mut hero = PackHero::new();
|
||||
|
||||
// Enable debug output
|
||||
hero.set_debug(true);
|
||||
|
||||
// Detect the platform
|
||||
let platform = hero.platform();
|
||||
println!("Detected platform: {:?}", platform);
|
||||
|
||||
// Only proceed if we're on a supported platform
|
||||
if platform == Platform::Unknown {
|
||||
println!("Unsupported platform. This example only works on Ubuntu and macOS.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Test package to install/check
|
||||
let test_package = if platform == Platform::Ubuntu { "wget" } else { "wget" };
|
||||
|
||||
// Check if the package is installed
|
||||
match hero.is_installed(test_package) {
|
||||
Ok(is_installed) => {
|
||||
println!("Package {} is installed: {}", test_package, is_installed);
|
||||
|
||||
if is_installed {
|
||||
println!("Package {} is already installed", test_package);
|
||||
} else {
|
||||
println!("Package {} is not installed, attempting to install...", test_package);
|
||||
|
||||
// Try to install the package
|
||||
match hero.install(test_package) {
|
||||
Ok(_) => println!("Successfully installed package {}", test_package),
|
||||
Err(e) => println!("Failed to install package {}: {}", test_package, e),
|
||||
}
|
||||
|
||||
// Check if it was installed successfully
|
||||
match hero.is_installed(test_package) {
|
||||
Ok(is_installed_now) => {
|
||||
if is_installed_now {
|
||||
println!("Verified package {} was installed successfully", test_package);
|
||||
} else {
|
||||
println!("Package {} was not installed successfully", test_package);
|
||||
}
|
||||
},
|
||||
Err(e) => println!("Error checking if package is installed: {}", e),
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => println!("Error checking if package is installed: {}", e),
|
||||
}
|
||||
|
||||
// Search for packages
|
||||
let search_term = "wget";
|
||||
println!("Searching for packages with term '{}'...", search_term);
|
||||
match hero.search(search_term) {
|
||||
Ok(results) => {
|
||||
println!("Found {} packages matching '{}'", results.len(), search_term);
|
||||
for (i, package) in results.iter().enumerate().take(5) {
|
||||
println!(" {}. {}", i + 1, package);
|
||||
}
|
||||
if results.len() > 5 {
|
||||
println!(" ... and {} more", results.len() - 5);
|
||||
}
|
||||
},
|
||||
Err(e) => println!("Error searching for packages: {}", e),
|
||||
}
|
||||
|
||||
// List installed packages
|
||||
println!("Listing installed packages...");
|
||||
match hero.list_installed() {
|
||||
Ok(packages) => {
|
||||
println!("Found {} installed packages", packages.len());
|
||||
println!("First 5 installed packages:");
|
||||
for (i, package) in packages.iter().enumerate().take(5) {
|
||||
println!(" {}. {}", i + 1, package);
|
||||
}
|
||||
if packages.len() > 5 {
|
||||
println!(" ... and {} more", packages.len() - 5);
|
||||
}
|
||||
},
|
||||
Err(e) => println!("Error listing installed packages: {}", e),
|
||||
}
|
||||
|
||||
// Update package lists
|
||||
println!("Updating package lists...");
|
||||
match hero.update() {
|
||||
Ok(_) => println!("Successfully updated package lists"),
|
||||
Err(e) => println!("Error updating package lists: {}", e),
|
||||
}
|
||||
|
||||
println!("Package management example completed");
|
||||
}
|
@ -1,474 +0,0 @@
|
||||
# RFS Wrapper Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
We'll create a Rust wrapper for the RFS (Remote File System) tool that follows the builder pattern, similar to the existing implementations for buildah and nerdctl in the codebase. This wrapper will provide a fluent API for mounting, unmounting, listing mounts, configuring mount options, and packing directories into filesystem layers.
|
||||
|
||||
## Module Structure
|
||||
|
||||
```
|
||||
src/virt/rfs/
|
||||
├── mod.rs # Module exports and common types
|
||||
├── cmd.rs # Command execution functions
|
||||
├── mount.rs # Mount operations
|
||||
├── pack.rs # Packing operations
|
||||
├── builder.rs # Builder pattern implementation
|
||||
├── types.rs # Type definitions
|
||||
└── error.rs # Error handling
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Error Handling
|
||||
|
||||
```rust
|
||||
// error.rs
|
||||
#[derive(Debug)]
|
||||
pub enum RfsError {
|
||||
CommandFailed(String),
|
||||
InvalidArgument(String),
|
||||
MountFailed(String),
|
||||
UnmountFailed(String),
|
||||
ListFailed(String),
|
||||
PackFailed(String),
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RfsError {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
impl std::error::Error for RfsError {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Command Execution
|
||||
|
||||
```rust
|
||||
// cmd.rs
|
||||
use crate::process::{run_command, CommandResult};
|
||||
use super::error::RfsError;
|
||||
|
||||
pub fn execute_rfs_command(args: &[&str]) -> Result<CommandResult, RfsError> {
|
||||
// Implementation similar to buildah and nerdctl
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Types
|
||||
|
||||
```rust
|
||||
// types.rs
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Mount {
|
||||
pub id: String,
|
||||
pub source: String,
|
||||
pub target: String,
|
||||
pub fs_type: String,
|
||||
pub options: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MountType {
|
||||
Local,
|
||||
SSH,
|
||||
S3,
|
||||
WebDAV,
|
||||
// Other mount types
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StoreSpec {
|
||||
pub spec_type: String,
|
||||
pub options: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl StoreSpec {
|
||||
pub fn new(spec_type: &str) -> Self {
|
||||
Self {
|
||||
spec_type: spec_type.to_string(),
|
||||
options: std::collections::HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_option(mut self, key: &str, value: &str) -> Self {
|
||||
self.options.insert(key.to_string(), value.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
let mut result = self.spec_type.clone();
|
||||
|
||||
if !self.options.is_empty() {
|
||||
result.push_str(":");
|
||||
let options: Vec<String> = self.options
|
||||
.iter()
|
||||
.map(|(k, v)| format!("{}={}", k, v))
|
||||
.collect();
|
||||
result.push_str(&options.join(","));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Builder Pattern
|
||||
|
||||
```rust
|
||||
// builder.rs
|
||||
use std::collections::HashMap;
|
||||
use super::{Mount, MountType, RfsError, execute_rfs_command, StoreSpec};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RfsBuilder {
|
||||
source: String,
|
||||
target: String,
|
||||
mount_type: MountType,
|
||||
options: HashMap<String, String>,
|
||||
mount_id: Option<String>,
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
impl RfsBuilder {
|
||||
pub fn new(source: &str, target: &str, mount_type: MountType) -> Self {
|
||||
Self {
|
||||
source: source.to_string(),
|
||||
target: target.to_string(),
|
||||
mount_type,
|
||||
options: HashMap::new(),
|
||||
mount_id: None,
|
||||
debug: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_option(mut self, key: &str, value: &str) -> Self {
|
||||
self.options.insert(key.to_string(), value.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_options(mut self, options: HashMap<&str, &str>) -> Self {
|
||||
for (key, value) in options {
|
||||
self.options.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_debug(mut self, debug: bool) -> Self {
|
||||
self.debug = debug;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn mount(self) -> Result<Mount, RfsError> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
pub fn unmount(&self) -> Result<(), RfsError> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// Other methods
|
||||
}
|
||||
|
||||
// Packing functionality
|
||||
pub struct PackBuilder {
|
||||
directory: String,
|
||||
output: String,
|
||||
store_specs: Vec<StoreSpec>,
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
impl PackBuilder {
|
||||
pub fn new(directory: &str, output: &str) -> Self {
|
||||
Self {
|
||||
directory: directory.to_string(),
|
||||
output: output.to_string(),
|
||||
store_specs: Vec::new(),
|
||||
debug: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_store_spec(mut self, store_spec: StoreSpec) -> Self {
|
||||
self.store_specs.push(store_spec);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_store_specs(mut self, store_specs: Vec<StoreSpec>) -> Self {
|
||||
self.store_specs.extend(store_specs);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_debug(mut self, debug: bool) -> Self {
|
||||
self.debug = debug;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn pack(self) -> Result<(), RfsError> {
|
||||
// Implementation for packing a directory into a filesystem layer
|
||||
let mut args = vec!["pack"];
|
||||
|
||||
// Add output file
|
||||
args.push("-m");
|
||||
args.push(&self.output);
|
||||
|
||||
// Add store specs
|
||||
if !self.store_specs.is_empty() {
|
||||
args.push("-s");
|
||||
let specs: Vec<String> = self.store_specs
|
||||
.iter()
|
||||
.map(|spec| spec.to_string())
|
||||
.collect();
|
||||
args.push(&specs.join(","));
|
||||
}
|
||||
|
||||
// Add directory
|
||||
args.push(&self.directory);
|
||||
|
||||
// Convert to string slices for the command
|
||||
let args_str: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
// Execute the command
|
||||
let result = execute_rfs_command(&args_str)?;
|
||||
|
||||
// Check for errors
|
||||
if !result.success {
|
||||
return Err(RfsError::PackFailed(result.stderr));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Mount Operations
|
||||
|
||||
```rust
|
||||
// mount.rs
|
||||
use super::{RfsBuilder, Mount, RfsError, execute_rfs_command};
|
||||
|
||||
pub fn list_mounts() -> Result<Vec<Mount>, RfsError> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
pub fn unmount_all() -> Result<(), RfsError> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// Other mount-related functions
|
||||
```
|
||||
|
||||
### 6. Pack Operations
|
||||
|
||||
```rust
|
||||
// pack.rs
|
||||
use super::{PackBuilder, StoreSpec, RfsError, execute_rfs_command};
|
||||
|
||||
pub fn pack_directory(directory: &str, output: &str, store_specs: &[StoreSpec]) -> Result<(), RfsError> {
|
||||
PackBuilder::new(directory, output)
|
||||
.with_store_specs(store_specs.to_vec())
|
||||
.pack()
|
||||
}
|
||||
|
||||
// Other pack-related functions
|
||||
```
|
||||
|
||||
### 7. Module Exports
|
||||
|
||||
```rust
|
||||
// mod.rs
|
||||
mod cmd;
|
||||
mod error;
|
||||
mod mount;
|
||||
mod pack;
|
||||
mod builder;
|
||||
mod types;
|
||||
|
||||
pub use error::RfsError;
|
||||
pub use builder::{RfsBuilder, PackBuilder};
|
||||
pub use types::{Mount, MountType, StoreSpec};
|
||||
pub use mount::{list_mounts, unmount_all};
|
||||
pub use pack::pack_directory;
|
||||
|
||||
// Re-export the execute_rfs_command function for use in other modules
|
||||
pub(crate) use cmd::execute_rfs_command;
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Mounting Example
|
||||
|
||||
```rust
|
||||
use crate::virt::rfs::{RfsBuilder, MountType};
|
||||
|
||||
// Create a new RFS mount with builder pattern
|
||||
let mount = RfsBuilder::new("user@example.com:/remote/path", "/local/mount/point", MountType::SSH)
|
||||
.with_option("port", "2222")
|
||||
.with_option("identity_file", "/path/to/key")
|
||||
.with_debug(true)
|
||||
.mount()?;
|
||||
|
||||
// List all mounts
|
||||
let mounts = list_mounts()?;
|
||||
for mount in mounts {
|
||||
println!("Mount ID: {}, Source: {}, Target: {}", mount.id, mount.source, mount.target);
|
||||
}
|
||||
|
||||
// Unmount
|
||||
mount.unmount()?;
|
||||
```
|
||||
|
||||
### Packing Example
|
||||
|
||||
```rust
|
||||
use crate::virt::rfs::{PackBuilder, StoreSpec};
|
||||
|
||||
// Create store specifications
|
||||
let store_spec1 = StoreSpec::new("file")
|
||||
.with_option("path", "/path/to/store");
|
||||
|
||||
let store_spec2 = StoreSpec::new("s3")
|
||||
.with_option("bucket", "my-bucket")
|
||||
.with_option("region", "us-east-1");
|
||||
|
||||
// Pack a directory with builder pattern
|
||||
let result = PackBuilder::new("/path/to/directory", "output.fl")
|
||||
.with_store_spec(store_spec1)
|
||||
.with_store_spec(store_spec2)
|
||||
.with_debug(true)
|
||||
.pack()?;
|
||||
|
||||
// Or use the convenience function
|
||||
pack_directory("/path/to/directory", "output.fl", &[store_spec1, store_spec2])?;
|
||||
```
|
||||
|
||||
## Rhai Integration
|
||||
|
||||
We'll also need to create a Rhai module to expose the RFS functionality to Rhai scripts:
|
||||
|
||||
```rust
|
||||
// src/rhai/rfs.rs
|
||||
use rhai::{Engine, EvalAltResult, RegisterFn};
|
||||
use crate::virt::rfs::{RfsBuilder, MountType, list_mounts, unmount_all, PackBuilder, StoreSpec};
|
||||
|
||||
pub fn register(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register RFS functions
|
||||
engine.register_fn("rfs_mount", rfs_mount);
|
||||
engine.register_fn("rfs_unmount", rfs_unmount);
|
||||
engine.register_fn("rfs_list_mounts", rfs_list_mounts);
|
||||
engine.register_fn("rfs_unmount_all", rfs_unmount_all);
|
||||
engine.register_fn("rfs_pack", rfs_pack);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Function implementations
|
||||
fn rfs_mount(source: &str, target: &str, mount_type: &str, options_map: rhai::Map) -> Result<(), Box<EvalAltResult>> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
fn rfs_unmount(target: &str) -> Result<(), Box<EvalAltResult>> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
fn rfs_list_mounts() -> Result<rhai::Array, Box<EvalAltResult>> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
fn rfs_unmount_all() -> Result<(), Box<EvalAltResult>> {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
fn rfs_pack(directory: &str, output: &str, store_specs: &str) -> Result<(), Box<EvalAltResult>> {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Flow
|
||||
|
||||
Here's a diagram showing the flow of the implementation:
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class RfsBuilder {
|
||||
+String source
|
||||
+String target
|
||||
+MountType mount_type
|
||||
+HashMap options
|
||||
+Option~String~ mount_id
|
||||
+bool debug
|
||||
+new(source, target, mount_type)
|
||||
+with_option(key, value)
|
||||
+with_options(options)
|
||||
+with_debug(debug)
|
||||
+mount()
|
||||
+unmount()
|
||||
}
|
||||
|
||||
class PackBuilder {
|
||||
+String directory
|
||||
+String output
|
||||
+Vec~StoreSpec~ store_specs
|
||||
+bool debug
|
||||
+new(directory, output)
|
||||
+with_store_spec(store_spec)
|
||||
+with_store_specs(store_specs)
|
||||
+with_debug(debug)
|
||||
+pack()
|
||||
}
|
||||
|
||||
class Mount {
|
||||
+String id
|
||||
+String source
|
||||
+String target
|
||||
+String fs_type
|
||||
+Vec~String~ options
|
||||
}
|
||||
|
||||
class MountType {
|
||||
<<enumeration>>
|
||||
Local
|
||||
SSH
|
||||
S3
|
||||
WebDAV
|
||||
}
|
||||
|
||||
class StoreSpec {
|
||||
+String spec_type
|
||||
+HashMap options
|
||||
+new(spec_type)
|
||||
+with_option(key, value)
|
||||
+to_string()
|
||||
}
|
||||
|
||||
class RfsError {
|
||||
<<enumeration>>
|
||||
CommandFailed
|
||||
InvalidArgument
|
||||
MountFailed
|
||||
UnmountFailed
|
||||
ListFailed
|
||||
PackFailed
|
||||
Other
|
||||
}
|
||||
|
||||
RfsBuilder --> Mount : creates
|
||||
RfsBuilder --> RfsError : may throw
|
||||
RfsBuilder --> MountType : uses
|
||||
PackBuilder --> RfsError : may throw
|
||||
PackBuilder --> StoreSpec : uses
|
||||
Mount --> RfsError : may throw
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Create the directory structure for the RFS module
|
||||
2. Implement the error handling module
|
||||
3. Implement the command execution module
|
||||
4. Define the types for mounts, mount operations, and store specifications
|
||||
5. Implement the builder pattern for RFS operations (mount and pack)
|
||||
6. Implement the mount operations
|
||||
7. Implement the pack operations
|
||||
8. Create the module exports
|
||||
9. Add Rhai integration
|
||||
10. Write tests for the implementation
|
||||
11. Update documentation
|
@ -1,30 +0,0 @@
|
||||
//! Herodo binary entry point
|
||||
//!
|
||||
//! This is the main entry point for the herodo binary.
|
||||
//! It parses command line arguments and calls into the implementation in the cmd module.
|
||||
|
||||
use clap::{App, Arg};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Parse command line arguments
|
||||
let matches = App::new("herodo")
|
||||
.version("0.1.0")
|
||||
.author("SAL Team")
|
||||
.about("Executes Rhai scripts for SAL")
|
||||
.arg(
|
||||
Arg::with_name("path")
|
||||
.short("p")
|
||||
.long("path")
|
||||
.value_name("PATH")
|
||||
.help("Path to a Rhai script file or directory containing Rhai scripts")
|
||||
.required(true)
|
||||
.takes_value(true),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
// Get the script path from arguments
|
||||
let script_path = matches.value_of("path").unwrap();
|
||||
|
||||
// Call the run function from the cmd module
|
||||
sal::cmd::herodo::run(script_path)
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
//! Herodo - A Rhai script executor for SAL
|
||||
//!
|
||||
//! This binary loads the Rhai engine, registers all SAL modules,
|
||||
//! and executes Rhai scripts from a specified directory in sorted order.
|
||||
|
||||
// Removed unused imports
|
||||
use rhai::Engine;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process;
|
||||
|
||||
/// Run the herodo script executor with the given script path
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `script_path` - Path to a Rhai script file or directory containing Rhai scripts
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Result indicating success or failure
|
||||
pub fn run(script_path: &str) -> Result<(), Box<dyn Error>> {
|
||||
let path = Path::new(script_path);
|
||||
|
||||
// Check if the path exists
|
||||
if !path.exists() {
|
||||
eprintln!("Error: '{}' does not exist", script_path);
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
// Create a new Rhai engine
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register println function for output
|
||||
engine.register_fn("println", |s: &str| println!("{}", s));
|
||||
|
||||
// Register all SAL modules with the engine
|
||||
crate::rhai::register(&mut engine)?;
|
||||
|
||||
// Determine if the path is a file or directory
|
||||
let script_files: Vec<PathBuf> = if path.is_file() {
|
||||
// Check if it's a .rhai file
|
||||
if path.extension().map_or(false, |ext| ext == "rhai") {
|
||||
vec![path.to_path_buf()]
|
||||
} else {
|
||||
eprintln!("Error: '{}' is not a Rhai script file", script_path);
|
||||
process::exit(1);
|
||||
}
|
||||
} else if path.is_dir() {
|
||||
// Find all .rhai files in the directory recursively
|
||||
let mut files: Vec<PathBuf> = Vec::new();
|
||||
|
||||
// Helper function to recursively find .rhai files
|
||||
fn find_rhai_files(dir: &Path, files: &mut Vec<PathBuf>) -> std::io::Result<()> {
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
find_rhai_files(&path, files)?;
|
||||
} else if path.is_file() &&
|
||||
path.extension().map_or(false, |ext| ext == "rhai") {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Find all .rhai files recursively
|
||||
find_rhai_files(path, &mut files)?;
|
||||
|
||||
// Sort the script files by name
|
||||
files.sort();
|
||||
|
||||
if files.is_empty() {
|
||||
println!("No Rhai scripts found in '{}'", script_path);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
files
|
||||
} else {
|
||||
eprintln!("Error: '{}' is neither a file nor a directory", script_path);
|
||||
process::exit(1);
|
||||
};
|
||||
|
||||
println!("Found {} Rhai script{} to execute:",
|
||||
script_files.len(),
|
||||
if script_files.len() == 1 { "" } else { "s" });
|
||||
|
||||
// Execute each script in sorted order
|
||||
for script_file in script_files {
|
||||
println!("\nExecuting: {}", script_file.display());
|
||||
|
||||
// Read the script content
|
||||
let script = fs::read_to_string(&script_file)?;
|
||||
|
||||
// Execute the script
|
||||
match engine.eval::<rhai::Dynamic>(&script) {
|
||||
Ok(result) => {
|
||||
println!("Script executed successfully");
|
||||
if !result.is_unit() {
|
||||
println!("Result: {}", result);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
eprintln!("Error executing script: {}", err);
|
||||
// Exit with error code when a script fails
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nAll scripts executed");
|
||||
Ok(())
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
//! Command-line tools for SAL
|
||||
//!
|
||||
//! This module contains command-line tools built on top of the SAL library.
|
||||
|
||||
pub mod herodo;
|
34
src/docs/.gitignore
vendored
34
src/docs/.gitignore
vendored
@ -1,34 +0,0 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
bun.lockb
|
||||
bun.lock
|
||||
|
||||
yarn.lock
|
||||
|
||||
build.sh
|
||||
build_dev.sh
|
||||
develop.sh
|
||||
|
||||
docusaurus.config.ts
|
||||
|
||||
sidebars.ts
|
||||
|
||||
tsconfig.json
|
@ -1,22 +0,0 @@
|
||||
{
|
||||
"style": "dark",
|
||||
"links": [
|
||||
{
|
||||
"title": "Web",
|
||||
"items": [
|
||||
{
|
||||
"label": "ThreeFold.io",
|
||||
"href": "https://threefold.io"
|
||||
},
|
||||
{
|
||||
"href": "https://mycelium.threefold.io/",
|
||||
"label": "Mycelium Network"
|
||||
},
|
||||
{
|
||||
"href": "https://aibox.threefold.io/",
|
||||
"label": "AI Box"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
{
|
||||
"title": "ThreeFold HeroScript",
|
||||
"tagline": "ThreeFold HeroScript",
|
||||
"favicon": "img/favicon.png",
|
||||
"url": "https://threefold.info",
|
||||
"url_home": "docs/intro",
|
||||
"baseUrl": "/heroscript/",
|
||||
"image": "img/tf_graph.png",
|
||||
"metadata": {
|
||||
"description": "Internet Infrastructur for Everyone by Everyone, Everywhere.",
|
||||
"image": "https://threefold.info/tfgrid4/img/tf_graph.png",
|
||||
"title": "ThreeFold"
|
||||
},
|
||||
"buildDest":["root@info.ourworld.tf:/root/hero/www/info/heroscript"],
|
||||
"buildDestDev":["root@info.ourworld.tf:/root/hero/www/infodev/heroscript"],
|
||||
"copyright": "ThreeFold"
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
{
|
||||
"title": "",
|
||||
"logo": {
|
||||
"alt": "ThreeFold Logo",
|
||||
"src": "img/logo.svg",
|
||||
"srcDark": "img/new_logo_tft.png"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"href": "https://threefold.io",
|
||||
"label": "ThreeFold.io",
|
||||
"position": "right"
|
||||
},
|
||||
{
|
||||
"href": "https://mycelium.threefold.io/",
|
||||
"label": "Mycelium Network",
|
||||
"position": "right"
|
||||
},
|
||||
{
|
||||
"href": "https://aibox.threefold.io/",
|
||||
"label": "AI Box",
|
||||
"position": "right"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
---
|
||||
title: "intro"
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# HeroScript
|
||||
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"label": "SAL",
|
||||
"position": 6,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Tools to work with operating system."
|
||||
}
|
||||
}
|
@ -1,239 +0,0 @@
|
||||
---
|
||||
title: "build containers"
|
||||
sidebar_position: 20
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
# Container Builder
|
||||
|
||||
The Buildah module provides functions for working with containers and images using the Buildah tool. Buildah helps you create and manage container images.
|
||||
|
||||
## Builder Pattern
|
||||
|
||||
The Buildah module now supports a Builder pattern, which provides a more intuitive and flexible way to work with containers and images.
|
||||
|
||||
### Creating a Builder
|
||||
|
||||
```js
|
||||
// Create a builder with a name and base image
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
|
||||
// Access builder properties
|
||||
let container_id = builder.container_id;
|
||||
let name = builder.name;
|
||||
let image = builder.image;
|
||||
```
|
||||
|
||||
### Builder Methods
|
||||
The Builder object provides the following methods:
|
||||
|
||||
- `run(command)`: Run a command in the container
|
||||
- `run_with_isolation(command, isolation)`: Run a command with specified isolation
|
||||
- `copy(source, dest)`: Copy files into the container
|
||||
- `add(source, dest)`: Add files into the container
|
||||
- `commit(image_name)`: Commit the container to an image
|
||||
- `remove()`: Remove the container
|
||||
- `reset()`: Remove the container and clear the container_id
|
||||
- `config(options)`: Configure container metadata
|
||||
- `set_entrypoint(entrypoint)`: Set the entrypoint for the container
|
||||
- `set_cmd(cmd)`: Set the default command for the container
|
||||
- `debug_mode`: Get or set the debug flag (true/false)
|
||||
- `write_content(content, dest_path)`: Write content to a file in the container
|
||||
- `read_content(source_path)`: Read content from a file in the container
|
||||
- `images()`: List images in local storage
|
||||
- `image_remove(image)`: Remove an image
|
||||
- `image_pull(image, tls_verify)`: Pull an image from a registry
|
||||
- `image_push(image, destination, tls_verify)`: Push an image to a registry
|
||||
- `image_tag(image, new_name)`: Add a tag to an image
|
||||
- `build(tag, context_dir, file, isolation)`: Build an image from a Dockerfile
|
||||
- `build(tag, context_dir, file, isolation)`: Build an image from a Dockerfile
|
||||
|
||||
### Example
|
||||
|
||||
```js
|
||||
// Create a builder
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
|
||||
// Enable debug mode to see command output
|
||||
builder.debug_mode = true;
|
||||
|
||||
// Reset the builder to remove any existing container
|
||||
builder.reset();
|
||||
|
||||
// Create a new container
|
||||
builder = bah_new("my-container", "alpine:latest");
|
||||
|
||||
// Run a command
|
||||
let result = builder.run("echo 'Hello from container'");
|
||||
println(`Command output: ${result.stdout}`);
|
||||
|
||||
// Write content directly to a file in the container
|
||||
let script_content = `#!/bin/sh
|
||||
echo "Hello from startup script"
|
||||
`;
|
||||
builder.write_content(script_content, "/start.sh");
|
||||
builder.run("chmod +x /start.sh");
|
||||
|
||||
// Set the entrypoint for the container
|
||||
builder.set_entrypoint("/start.sh");
|
||||
|
||||
// Add a file
|
||||
file_write("test_file.txt", "Test content");
|
||||
builder.add("test_file.txt", "/");
|
||||
|
||||
// Commit to an image
|
||||
builder.commit("my-custom-image:latest");
|
||||
|
||||
// Clean up
|
||||
builder.remove();
|
||||
delete("test_file.txt");
|
||||
```
|
||||
|
||||
## Image Information
|
||||
|
||||
### Image Properties
|
||||
|
||||
When working with images, you can access the following information:
|
||||
|
||||
- `id`: The unique identifier for the image
|
||||
- `names`: A list of names/tags for the image
|
||||
- `name`: The primary name of the image, or `<none>` if the image has no names
|
||||
- `size`: The size of the image
|
||||
- `created`: When the image was created
|
||||
|
||||
## Builder Methods
|
||||
|
||||
### `bah_new(name, image)`
|
||||
|
||||
Creates a new Builder object for working with a container.
|
||||
|
||||
**Parameters:**
|
||||
- `name` (string): The name to give the container
|
||||
- `image` (string): The name or ID of the image to create the container from
|
||||
|
||||
**Returns:** A Builder object if successful.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Create a new Builder
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- If a container with the given name already exists, it will be reused instead of creating a new one
|
||||
- The Builder object provides methods for working with the container
|
||||
|
||||
### `reset()`
|
||||
|
||||
Resets a Builder by removing the container and clearing the container_id. This allows you to start fresh with the same Builder object.
|
||||
|
||||
**Returns:** Nothing.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Create a Builder
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
|
||||
// Reset the Builder to remove the container
|
||||
builder.reset();
|
||||
|
||||
// Create a new container with the same name
|
||||
builder = bah_new("my-container", "alpine:latest");
|
||||
```
|
||||
|
||||
### `debug_mode`
|
||||
|
||||
Get or set the debug flag for the Builder. When debug mode is enabled, all buildah commands will output their stdout/stderr, making it easier to debug issues.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Create a Builder
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
|
||||
// Enable debug mode
|
||||
builder.debug_mode = true;
|
||||
|
||||
// Run a command with debug output
|
||||
builder.run("echo 'Hello with debug'");
|
||||
|
||||
// Disable debug mode
|
||||
builder.debug_mode = false;
|
||||
```
|
||||
|
||||
### `set_entrypoint(entrypoint)`
|
||||
|
||||
Sets the entrypoint for the container. The entrypoint is the command that will be executed when the container starts.
|
||||
|
||||
**Parameters:**
|
||||
- `entrypoint` (string): The entrypoint command
|
||||
|
||||
**Returns:** Command result if successful.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Create a Builder
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
|
||||
// Set the entrypoint
|
||||
builder.set_entrypoint("/start.sh");
|
||||
```
|
||||
|
||||
### `set_cmd(cmd)`
|
||||
|
||||
Sets the default command for the container. This is used as arguments to the entrypoint.
|
||||
|
||||
**Parameters:**
|
||||
- `cmd` (string): The default command
|
||||
|
||||
**Returns:** Command result if successful.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Create a Builder
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
|
||||
// Set the default command
|
||||
builder.set_cmd("--verbose");
|
||||
```
|
||||
|
||||
### `write_content(content, dest_path)`
|
||||
|
||||
Writes content to a file in the container.
|
||||
|
||||
**Parameters:**
|
||||
- `content` (string): The content to write
|
||||
- `dest_path` (string): The destination path in the container
|
||||
|
||||
**Returns:** Command result if successful.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Create a Builder
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
|
||||
// Write content to a file
|
||||
let content = "Hello, world!";
|
||||
builder.write_content(content, "/hello.txt");
|
||||
```
|
||||
|
||||
### `read_content(source_path)`
|
||||
|
||||
Reads content from a file in the container.
|
||||
|
||||
**Parameters:**
|
||||
- `source_path` (string): The source path in the container
|
||||
|
||||
**Returns:** The file content as a string if successful.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Create a Builder
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
|
||||
// Write content to a file
|
||||
builder.write_content("Hello, world!", "/hello.txt");
|
||||
|
||||
// Read content from the file
|
||||
let content = builder.read_content("/hello.txt");
|
||||
println(content); // Outputs: Hello, world!
|
||||
```
|
@ -1,210 +0,0 @@
|
||||
---
|
||||
title: "git"
|
||||
sidebar_position: 5
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
# Git
|
||||
|
||||
This module provides HeroScript wrappers for the Git functionality in SAL.
|
||||
|
||||
> **Note:** The constructor for GitTree has been renamed from `new()` to `gittree_new()` to avoid confusion with other constructors. This makes the interface more explicit and less likely to cause naming conflicts.
|
||||
|
||||
## Object-Oriented Design
|
||||
|
||||
The Git module follows an object-oriented design with two main classes:
|
||||
|
||||
1. **GitTree** - Represents a collection of git repositories under a base path
|
||||
- Created with `gittree_new(base_path)`
|
||||
- Methods for listing, finding, and getting repositories
|
||||
|
||||
2. **GitRepo** - Represents a single git repository
|
||||
- Obtained from GitTree's `get()` method
|
||||
- Methods for common git operations: pull, reset, push, commit
|
||||
|
||||
This design allows for a more intuitive and flexible interface, with method chaining for complex operations.
|
||||
|
||||
## Creating a GitTree
|
||||
|
||||
The GitTree object is the main entry point for git operations. It represents a collection of git repositories under a base path.
|
||||
|
||||
```js
|
||||
// Create a new GitTree with a base path
|
||||
let git_tree = gittree_new("/root/code");
|
||||
print(`Created GitTree with base path: /home/user/code`);
|
||||
```
|
||||
|
||||
## Finding Repositories
|
||||
|
||||
### List All Repositories
|
||||
|
||||
```js
|
||||
// List all git repositories under the base path
|
||||
let repos = git_tree.list();
|
||||
print(`Found ${repos.len()} repositories`);
|
||||
|
||||
// Print the repositories
|
||||
for repo in repos {
|
||||
print(` - ${repo}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Find Repositories Matching a Pattern
|
||||
|
||||
```js
|
||||
// Find repositories matching a pattern
|
||||
// Use a wildcard (*) suffix to find multiple matches
|
||||
let matching_repos = git_tree.find("my-project*");
|
||||
print("Matching repositories:");
|
||||
for repo in matching_repos {
|
||||
print(` - ${repo}`);
|
||||
}
|
||||
|
||||
// Find a specific repository (must match exactly one)
|
||||
let specific_repo = git_tree.find("unique-project")[0];
|
||||
print(`Found specific repository: ${specific_repo}`);
|
||||
```
|
||||
|
||||
## Working with Repositories
|
||||
|
||||
### Get Repository Objects
|
||||
|
||||
```js
|
||||
// Get GitRepo objects for repositories matching a pattern
|
||||
let repos = git_tree.get("my-project*");
|
||||
print(`Found ${repos.len()} repositories`);
|
||||
|
||||
// Get a specific repository
|
||||
let repo = git_tree.get("unique-project")[0];
|
||||
print(`Working with repository: ${repo.path()}`);
|
||||
```
|
||||
|
||||
### Clone a Repository
|
||||
|
||||
```js
|
||||
// Clone a repository by URL
|
||||
// This will clone the repository to the base path of the GitTree
|
||||
let repos = git_tree.get("https://github.com/username/repo.git");
|
||||
let repo = repos[0];
|
||||
print(`Repository cloned to: ${repo.path()}`);
|
||||
```
|
||||
|
||||
### Check for Changes
|
||||
|
||||
```js
|
||||
// Check if a repository has uncommitted changes
|
||||
let repo = git_tree.get("my-project")[0];
|
||||
if repo.has_changes() {
|
||||
print("Repository has uncommitted changes");
|
||||
} else {
|
||||
print("Repository is clean");
|
||||
}
|
||||
```
|
||||
|
||||
## Repository Operations
|
||||
|
||||
### Pull Changes
|
||||
|
||||
```js
|
||||
// Pull the latest changes from the remote
|
||||
// This will fail if there are uncommitted changes
|
||||
let repo = git_tree.get("my-project")[0];
|
||||
let result = repo.pull();
|
||||
print("Repository updated successfully");
|
||||
```
|
||||
|
||||
### Reset Local Changes
|
||||
|
||||
```js
|
||||
// Reset any local changes in the repository
|
||||
let repo = git_tree.get("my-project")[0];
|
||||
let result = repo.reset();
|
||||
print("Repository reset successfully");
|
||||
```
|
||||
|
||||
### Commit Changes
|
||||
|
||||
```js
|
||||
// Commit changes in the repository
|
||||
let repo = git_tree.get("my-project")[0];
|
||||
let result = repo.commit("Fix bug in login form");
|
||||
print("Changes committed successfully");
|
||||
```
|
||||
|
||||
### Push Changes
|
||||
|
||||
```js
|
||||
// Push changes to the remote
|
||||
let repo = git_tree.get("my-project")[0];
|
||||
let result = repo.push();
|
||||
print("Changes pushed successfully");
|
||||
```
|
||||
|
||||
## Method Chaining
|
||||
|
||||
The GitRepo methods can be chained together for more complex operations:
|
||||
|
||||
```js
|
||||
// Commit changes and push them to the remote
|
||||
let repo = git_tree.get("my-project")[0];
|
||||
let result = repo.commit("Add new feature").push();
|
||||
print("Changes committed and pushed successfully");
|
||||
|
||||
// Reset local changes, pull the latest changes, and commit new changes
|
||||
let repo = git_tree.get("my-project")[0];
|
||||
let result = repo.reset().pull().commit("Update dependencies");
|
||||
print("Repository updated successfully");
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```js
|
||||
// Create a new GitTree
|
||||
let home_dir = env("HOME");
|
||||
let git_tree = gittree_new(`${home_dir}/code`);
|
||||
|
||||
// Clone a repository
|
||||
let repos = git_tree.get("https://github.com/username/example-repo.git");
|
||||
let repo = repos[0];
|
||||
print(`Cloned repository to: ${repo.path()}`);
|
||||
|
||||
// Make some changes (using OS module functions)
|
||||
let file_path = `${repo.path()}/README.md`;
|
||||
let content = "# Example Repository\n\nThis is an example repository.";
|
||||
write_file(file_path, content);
|
||||
|
||||
// Commit and push the changes
|
||||
let result = repo.commit("Update README.md").push();
|
||||
print("Changes committed and pushed successfully");
|
||||
|
||||
// List all repositories
|
||||
let all_repos = git_tree.list();
|
||||
print("All repositories:");
|
||||
for repo_path in all_repos {
|
||||
print(` - ${repo_path}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
All methods in the Git module return a Result type, which means they can either succeed or fail with an error. If an error occurs, it will be propagated to the HeroScript script as a runtime error.
|
||||
|
||||
For example, if you try to clone a repository that doesn't exist:
|
||||
|
||||
```js
|
||||
// Try to clone a non-existent repository
|
||||
try {
|
||||
let git_tree = gittree_new("/root/code");
|
||||
let repos = git_tree.get("https://github.com/nonexistent/repo.git");
|
||||
print("This will not be executed if the repository doesn't exist");
|
||||
} catch(err) {
|
||||
print(`Error: ${err}`); // Will print the error message from git
|
||||
}
|
||||
```
|
||||
|
||||
Common errors include:
|
||||
- Invalid URL
|
||||
- Repository not found
|
||||
- Authentication failure
|
||||
- Network issues
|
||||
- Local changes exist when trying to pull
|
@ -1,10 +0,0 @@
|
||||
---
|
||||
title: "intro"
|
||||
sidebar_position: 1
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
## HeroScript Script Commands Documentation
|
||||
|
||||
|
||||
The SAL library provides integration with the HeroScript scripting language, allowing you to use powerful system functions within your HeroScript scripts. These functions are organized into modules that provide related functionality.
|
@ -1,239 +0,0 @@
|
||||
---
|
||||
title: "run containers"
|
||||
sidebar_position: 21
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
# Container Manager
|
||||
|
||||
The Container Manager module provides a comprehensive API for working with containers using nerdctl. It offers a modern builder pattern approach for container management.
|
||||
|
||||
## Container Builder Pattern
|
||||
|
||||
The Container Builder Pattern allows for fluent, chainable configuration of containers. This pattern makes container creation more readable and maintainable by allowing you to build complex container configurations step by step.
|
||||
|
||||
### Creating a Container
|
||||
|
||||
Start by creating a new container instance:
|
||||
|
||||
```rhai
|
||||
// Create an empty container with just a name
|
||||
let container = nerdctl_container_new("my-container");
|
||||
|
||||
// Or create a container from an image
|
||||
let container = nerdctl_container_from_image("my-container", "nginx:latest");
|
||||
```
|
||||
|
||||
### Configuring the Container
|
||||
|
||||
Once you have a container instance, you can configure it using the various builder methods:
|
||||
|
||||
```rhai
|
||||
// Configure the container with various options
|
||||
let container = nerdctl_container_from_image("web-server", "nginx:latest")
|
||||
.with_port("8080:80") // Map port 8080 to container port 80
|
||||
.with_volume("/host/path:/container/path") // Mount a volume
|
||||
.with_env("NGINX_HOST", "localhost") // Set an environment variable
|
||||
.with_network("bridge") // Set the network
|
||||
.with_detach(true); // Run in detached mode
|
||||
```
|
||||
|
||||
### Resetting Container Configuration
|
||||
|
||||
If you need to reset the container configuration to its default state while keeping the name and image:
|
||||
|
||||
```rhai
|
||||
// Reset the container configuration
|
||||
let container = nerdctl_container_from_image("web-server", "nginx:latest")
|
||||
.reset() // Reset all configuration to defaults
|
||||
.with_port("8080:80") // Start configuring again
|
||||
.with_detach(true);
|
||||
```
|
||||
|
||||
### Building and Starting the Container
|
||||
|
||||
After configuring the container, you can build and start it:
|
||||
|
||||
```rhai
|
||||
// Build the container (creates it but doesn't start it)
|
||||
let built_container = container.build();
|
||||
|
||||
// Start the container
|
||||
let start_result = built_container.start();
|
||||
|
||||
// Check if the container started successfully
|
||||
if (start_result.success) {
|
||||
println("Container started successfully!");
|
||||
} else {
|
||||
println(`Failed to start container: ${start_result.stderr}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Container Lifecycle Operations
|
||||
|
||||
Once your container is running, you can perform various operations:
|
||||
|
||||
```rhai
|
||||
// Execute a command in the container
|
||||
let exec_result = container.exec("ls -la");
|
||||
|
||||
// Get container logs
|
||||
let logs = container.logs();
|
||||
|
||||
// Stop the container
|
||||
let stop_result = container.stop();
|
||||
|
||||
// Remove the container
|
||||
let remove_result = container.remove();
|
||||
```
|
||||
|
||||
## Available Builder Methods
|
||||
|
||||
The Container Builder Pattern provides the following methods for configuring containers:
|
||||
|
||||
| Method | Description | Example |
|
||||
| -------------------------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| `reset()` | Reset configuration to defaults | `.reset()` |
|
||||
| `with_port(port)` | Add a port mapping | `.with_port("8080:80")` |
|
||||
| `with_ports(ports_array)` | Add multiple port mappings | `.with_ports(["8080:80", "443:443"])` |
|
||||
| `with_volume(volume)` | Add a volume mount | `.with_volume("/host/path:/container/path")` |
|
||||
| `with_volumes(volumes_array)` | Add multiple volume mounts | `.with_volumes(["/host/path1:/container/path1", "/host/path2:/container/path2"])` |
|
||||
| `with_env(key, value)` | Add an environment variable | `.with_env("NGINX_HOST", "localhost")` |
|
||||
| `with_envs(env_map)` | Add multiple environment variables | `.with_envs(#{"KEY1": "value1", "KEY2": "value2"})` |
|
||||
| `with_network(network)` | Set the network | `.with_network("bridge")` |
|
||||
| `with_network_alias(alias)` | Add a network alias | `.with_network_alias("web-server")` |
|
||||
| `with_network_aliases(aliases_array)` | Add multiple network aliases | `.with_network_aliases(["web-server", "http-service"])` |
|
||||
| `with_cpu_limit(cpus)` | Set CPU limit | `.with_cpu_limit("1.0")` |
|
||||
| `with_cpu_shares(shares)` | Set CPU shares | `.with_cpu_shares("1024")` |
|
||||
| `with_memory_limit(memory)` | Set memory limit | `.with_memory_limit("512m")` |
|
||||
| `with_memory_swap_limit(memory_swap)` | Set memory swap limit | `.with_memory_swap_limit("1g")` |
|
||||
| `with_restart_policy(policy)` | Set restart policy | `.with_restart_policy("unless-stopped")` |
|
||||
| `with_health_check(cmd)` | Set health check command | `.with_health_check("curl -f http://localhost/ | | exit 1")` |
|
||||
| `with_health_check_options(cmd, interval, timeout, retries, start_period)` | Set health check with options | `.with_health_check_options("curl -f http://localhost/ | | exit 1", "5s", "3s", 3, "10s")` |
|
||||
| `with_snapshotter(snapshotter)` | Set snapshotter | `.with_snapshotter("native")` |
|
||||
| `with_detach(detach)` | Set detach mode | `.with_detach(true)` |
|
||||
|
||||
## Complete Example: Web Server
|
||||
|
||||
Here's a complete example that demonstrates setting up an Nginx web server using the Container Builder Pattern:
|
||||
|
||||
```rhai
|
||||
// Create a temporary directory for our files
|
||||
let work_dir = "/tmp/nerdctl";
|
||||
mkdir(work_dir);
|
||||
chdir(work_dir);
|
||||
|
||||
// Create a custom index.html file
|
||||
let html_content = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Rhai Nerdctl Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 40px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
h1 {
|
||||
color: #0066cc;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello from Rhai Nerdctl!</h1>
|
||||
<p>This page is served by an Nginx container created using the Rhai nerdctl wrapper.</p>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
// Write the HTML file
|
||||
let html_file = `${work_dir}/index.html`;
|
||||
file_write(html_file, html_content);
|
||||
|
||||
// Set up environment variables
|
||||
let env_map = #{};
|
||||
env_map["NGINX_HOST"] = "localhost";
|
||||
env_map["NGINX_PORT"] = "80";
|
||||
env_map["NGINX_WORKER_PROCESSES"] = "auto";
|
||||
|
||||
// Create and configure the container
|
||||
let container_name = "rhai-nginx-demo";
|
||||
|
||||
// First, try to remove any existing container with the same name
|
||||
nerdctl_remove(container_name);
|
||||
|
||||
// Create a container with a rich set of options using the builder pattern
|
||||
let container = nerdctl_container_from_image(container_name, "nginx:latest")
|
||||
.reset() // Reset to default configuration
|
||||
.with_detach(true)
|
||||
.with_ports(["8080:80"]) // Add multiple ports at once
|
||||
.with_volumes([`${work_dir}:/usr/share/nginx/html`]) // Mount our work dir
|
||||
.with_envs(env_map) // Add multiple environment variables at once
|
||||
.with_network("bridge")
|
||||
.with_network_aliases(["web-server", "nginx-demo"]) // Add multiple network aliases
|
||||
.with_cpu_limit("1.0")
|
||||
.with_memory_limit("512m");
|
||||
|
||||
// Build and start the container
|
||||
let built_container = container.build();
|
||||
let start_result = built_container.start();
|
||||
|
||||
println("The web server is running at http://localhost:8080");
|
||||
```
|
||||
|
||||
## Using Local Images Created with Buildah
|
||||
|
||||
When working with images created by Buildah, you may need to take additional steps to ensure nerdctl can find and use these images. This is because Buildah and nerdctl may use different storage backends by default.
|
||||
|
||||
### Tagging with localhost Prefix
|
||||
|
||||
One approach is to tag the Buildah-created image with a `localhost/` prefix:
|
||||
|
||||
```rhai
|
||||
// Create and commit a container with Buildah
|
||||
let builder = bah_new("my-container", "alpine:latest");
|
||||
builder.run("echo 'Hello' > /hello.txt");
|
||||
builder.commit("my-custom-image:latest");
|
||||
|
||||
// Tag the image with localhost prefix for nerdctl compatibility
|
||||
let local_image_name = "localhost/my-custom-image:latest";
|
||||
bah_image_tag("my-custom-image:latest", local_image_name);
|
||||
|
||||
// Now use the image with nerdctl
|
||||
let container = nerdctl_container_from_image("my-app", local_image_name)
|
||||
.with_detach(true)
|
||||
.build();
|
||||
```
|
||||
|
||||
### Using a Local Registry
|
||||
|
||||
For more reliable interoperability, you can push the image to a local registry:
|
||||
|
||||
```rhai
|
||||
// Push the Buildah-created image to a local registry
|
||||
bah_image_push("my-custom-image:latest", "localhost:5000/my-custom-image:latest", false);
|
||||
|
||||
// Pull the image with nerdctl
|
||||
nerdctl_image_pull("localhost:5000/my-custom-image:latest");
|
||||
|
||||
// Use the image
|
||||
let container = nerdctl_container_from_image("my-app", "localhost:5000/my-custom-image:latest")
|
||||
.with_detach(true)
|
||||
.build();
|
||||
```
|
||||
|
||||
## Image Management Functions
|
||||
|
||||
The module also provides functions for managing container images:
|
||||
|
||||
| Function | Description | Example |
|
||||
| --------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| `nerdctl_images()` | List images in local storage | `nerdctl_images()` |
|
||||
| `nerdctl_image_remove(image)` | Remove an image | `nerdctl_image_remove("nginx:latest")` |
|
||||
| `nerdctl_image_push(image, destination)` | Push an image to a registry | `nerdctl_image_push("my-image:latest", "registry.example.com/my-image:latest")` |
|
||||
| `nerdctl_image_tag(image, new_name)` | Add an additional name to a local image | `nerdctl_image_tag("nginx:latest", "my-nginx:latest")` |
|
||||
| `nerdctl_image_pull(image)` | Pull an image from a registry | `nerdctl_image_pull("nginx:latest")` |
|
||||
| `nerdctl_image_commit(container, image_name)` | Commit a container to an image | `nerdctl_image_commit("web-server", "my-nginx:latest")` |
|
||||
| `nerdctl_image_build(tag, context_path)` | Build an image using a Dockerfile | `nerdctl_image_build("my-image:latest", "./")` |
|
@ -1,359 +0,0 @@
|
||||
---
|
||||
title: "os"
|
||||
sidebar_position: 2
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
# OS Tools
|
||||
|
||||
The OS module provides functions for working with files, directories, and downloading files from the internet.
|
||||
|
||||
## File System Functions
|
||||
|
||||
### `copy(src, dest)`
|
||||
|
||||
Recursively copies a file or directory from source to destination.
|
||||
|
||||
**Parameters:**
|
||||
- `src` (string): The source file or directory path
|
||||
- `dest` (string): The destination path
|
||||
|
||||
**Returns:** A message confirming the copy was successful.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Copy a file
|
||||
copy("source.txt", "destination.txt");
|
||||
|
||||
// Copy a directory recursively
|
||||
copy("source_dir", "destination_dir");
|
||||
```
|
||||
|
||||
### `exist(path)`
|
||||
|
||||
Checks if a file or directory exists.
|
||||
|
||||
**Parameters:**
|
||||
- `path` (string): The path to check
|
||||
|
||||
**Returns:** A boolean value - `true` if the file or directory exists, `false` otherwise.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
if exist("config.json") {
|
||||
// File exists, do something
|
||||
} else {
|
||||
// File doesn't exist
|
||||
}
|
||||
```
|
||||
|
||||
### `find_file(dir, filename)`
|
||||
|
||||
Finds a file in a directory with support for wildcards.
|
||||
|
||||
**Parameters:**
|
||||
- `dir` (string): The directory to search in
|
||||
- `filename` (string): The filename pattern to search for (supports wildcards)
|
||||
|
||||
**Returns:** The path of the first matching file.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Find a specific file
|
||||
let config_file = find_file("./config", "settings.json");
|
||||
|
||||
// Find using wildcards
|
||||
let log_file = find_file("./logs", "*.log");
|
||||
```
|
||||
|
||||
### `find_files(dir, filename)`
|
||||
|
||||
Finds multiple files in a directory recursively with support for wildcards.
|
||||
|
||||
**Parameters:**
|
||||
- `dir` (string): The directory to search in
|
||||
- `filename` (string): The filename pattern to search for (supports wildcards)
|
||||
|
||||
**Returns:** A list of matching file paths.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Find all JSON files
|
||||
let json_files = find_files("./data", "*.json");
|
||||
|
||||
// Process each file
|
||||
for file in json_files {
|
||||
print(`Found file: ${file}`);
|
||||
}
|
||||
```
|
||||
|
||||
### `find_dir(dir, dirname)`
|
||||
|
||||
Finds a directory in a parent directory with support for wildcards.
|
||||
|
||||
**Parameters:**
|
||||
- `dir` (string): The parent directory to search in
|
||||
- `dirname` (string): The directory name pattern to search for (supports wildcards)
|
||||
|
||||
**Returns:** The path of the first matching directory.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Find a specific directory
|
||||
let config_dir = find_dir("./", "config");
|
||||
|
||||
// Find using wildcards
|
||||
let version_dir = find_dir("./releases", "v*");
|
||||
```
|
||||
|
||||
### `find_dirs(dir, dirname)`
|
||||
|
||||
Finds multiple directories in a parent directory recursively with support for wildcards.
|
||||
|
||||
**Parameters:**
|
||||
- `dir` (string): The parent directory to search in
|
||||
- `dirname` (string): The directory name pattern to search for (supports wildcards)
|
||||
|
||||
**Returns:** A list of matching directory paths.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Find all version directories
|
||||
let version_dirs = find_dirs("./releases", "v*");
|
||||
|
||||
// Process each directory
|
||||
for dir in version_dirs {
|
||||
print(`Found directory: ${dir}`);
|
||||
}
|
||||
```
|
||||
|
||||
### `delete(path)`
|
||||
|
||||
Deletes a file or directory. This function is defensive and doesn't error if the file doesn't exist.
|
||||
|
||||
**Parameters:**
|
||||
- `path` (string): The path of the file or directory to delete
|
||||
|
||||
**Returns:** A message confirming the deletion was successful.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Delete a file
|
||||
delete("temp.txt");
|
||||
|
||||
// Delete a directory
|
||||
delete("temp_dir");
|
||||
```
|
||||
|
||||
### `mv(src, dest)`
|
||||
|
||||
Moves a file or directory from source to destination.
|
||||
|
||||
**Parameters:**
|
||||
- `src` (string): The source path
|
||||
- `dest` (string): The destination path
|
||||
|
||||
**Returns:** A message confirming the move was successful.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Move a file
|
||||
mv("file.txt", "new_location/file.txt");
|
||||
|
||||
// Move a directory
|
||||
mv("source_dir", "destination_dir");
|
||||
|
||||
// Rename a file
|
||||
mv("old_name.txt", "new_name.txt");
|
||||
```
|
||||
|
||||
### `mkdir(path)`
|
||||
|
||||
Creates a directory and all parent directories. This function is defensive and doesn't error if the directory already exists.
|
||||
|
||||
**Parameters:**
|
||||
- `path` (string): The path of the directory to create
|
||||
|
||||
**Returns:** A message confirming the directory was created.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Create a directory
|
||||
mkdir("new_dir");
|
||||
|
||||
// Create nested directories
|
||||
mkdir("parent/child/grandchild");
|
||||
```
|
||||
|
||||
### `file_size(path)`
|
||||
|
||||
Gets the size of a file in bytes.
|
||||
|
||||
**Parameters:**
|
||||
- `path` (string): The path of the file
|
||||
|
||||
**Returns:** The size of the file in bytes.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Get file size
|
||||
let size = file_size("large_file.dat");
|
||||
print(`File size: ${size} bytes`);
|
||||
```
|
||||
|
||||
## File Content Functions
|
||||
|
||||
### `file_read(path)`
|
||||
|
||||
Reads the contents of a file.
|
||||
|
||||
**Parameters:**
|
||||
- `path` (string): The path of the file to read
|
||||
|
||||
**Returns:** The content of the file as a string.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Read a file
|
||||
let content = file_read("config.json");
|
||||
print(`File content: ${content}`);
|
||||
```
|
||||
|
||||
### `file_write(path, content)`
|
||||
|
||||
Writes content to a file. Creates the file if it doesn't exist, overwrites if it does.
|
||||
|
||||
**Parameters:**
|
||||
- `path` (string): The path of the file to write to
|
||||
- `content` (string): The content to write to the file
|
||||
|
||||
**Returns:** A message confirming the file was written.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Write to a file
|
||||
file_write("config.json", "{\n \"setting\": \"value\"\n}");
|
||||
```
|
||||
|
||||
### `file_write_append(path, content)`
|
||||
|
||||
Appends content to a file. Creates the file if it doesn't exist.
|
||||
|
||||
**Parameters:**
|
||||
- `path` (string): The path of the file to append to
|
||||
- `content` (string): The content to append to the file
|
||||
|
||||
**Returns:** A message confirming the content was appended.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Append to a log file
|
||||
file_write_append("log.txt", "New log entry\n");
|
||||
```
|
||||
|
||||
### `rsync(src, dest)`
|
||||
|
||||
Syncs directories using rsync (or platform equivalent).
|
||||
|
||||
**Parameters:**
|
||||
- `src` (string): The source directory
|
||||
- `dest` (string): The destination directory
|
||||
|
||||
**Returns:** A message confirming the directories were synced.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Sync directories
|
||||
rsync("source_dir", "backup_dir");
|
||||
```
|
||||
|
||||
### `chdir(path)`
|
||||
|
||||
Changes the current working directory.
|
||||
|
||||
**Parameters:**
|
||||
- `path` (string): The path to change to
|
||||
|
||||
**Returns:** A message confirming the directory was changed.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Change directory
|
||||
chdir("project/src");
|
||||
```
|
||||
|
||||
## Download Functions
|
||||
|
||||
### `download(url, dest, min_size_kb)`
|
||||
|
||||
Downloads a file from a URL to a destination directory using the curl command. If the URL ends with a supported archive format, the file will be automatically extracted to the destination directory.
|
||||
|
||||
**Supported archive formats for automatic extraction:**
|
||||
- `.tar.gz`
|
||||
- `.tgz`
|
||||
- `.tar`
|
||||
- `.zip`
|
||||
|
||||
**Parameters:**
|
||||
- `url` (string): The URL to download from
|
||||
- `dest` (string): The destination directory where the file will be saved or extracted
|
||||
- `min_size_kb` (integer): The minimum expected file size in kilobytes (for validation)
|
||||
|
||||
**Returns:** The path where the file was saved or extracted.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Download a file to a directory
|
||||
download("https://example.com/file.zip", "downloads/", 10);
|
||||
```
|
||||
|
||||
### `download_file(url, dest, min_size_kb)`
|
||||
|
||||
Downloads a file from a URL to a specific file destination using the curl command. This function is designed for downloading files to a specific path, not for extracting archives.
|
||||
|
||||
**Parameters:**
|
||||
- `url` (string): The URL to download from
|
||||
- `dest` (string): The destination file path where the file will be saved
|
||||
- `min_size_kb` (integer): The minimum expected file size in kilobytes (for validation)
|
||||
|
||||
**Returns:** The path where the file was saved.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Download a file to a specific path
|
||||
download_file("https://example.com/file.txt", "downloads/myfile.txt", 10);
|
||||
```
|
||||
|
||||
### `download_install(url, min_size_kb)`
|
||||
|
||||
Downloads a file and installs it if it's a supported package format.
|
||||
|
||||
**Supported package formats for automatic installation:**
|
||||
- `.deb` packages on Debian-based systems
|
||||
|
||||
**Parameters:**
|
||||
- `url` (string): The URL to download from
|
||||
- `min_size_kb` (integer): The minimum expected file size in kilobytes (for validation)
|
||||
|
||||
**Returns:** The path where the file was saved or installed.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Download and install a package
|
||||
download_install("https://example.com/package.deb", 1000);
|
||||
```
|
||||
|
||||
### `chmod_exec(path)`
|
||||
|
||||
Makes a file executable (equivalent to `chmod +x` in Unix).
|
||||
|
||||
**Parameters:**
|
||||
- `path` (string): The path to the file to make executable
|
||||
|
||||
**Returns:** A message confirming the file was made executable.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Make a file executable
|
||||
chmod_exec("downloads/script.sh");
|
||||
```
|
@ -1,237 +0,0 @@
|
||||
---
|
||||
title: "process"
|
||||
sidebar_position: 3
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
# Process Module
|
||||
|
||||
The Process module provides functions for running commands and managing processes on your system.
|
||||
|
||||
## Command Results
|
||||
|
||||
### Command Output Information
|
||||
|
||||
When you run a command, you get back information about what happened:
|
||||
|
||||
- `stdout`: The normal output of the command
|
||||
- `stderr`: Any error messages from the command
|
||||
- `success`: Whether the command worked (true) or failed (false)
|
||||
- `code`: The exit code (0 usually means success)
|
||||
|
||||
### Process Information
|
||||
|
||||
When you get information about a running process, you can see:
|
||||
|
||||
- `pid`: The process ID number
|
||||
- `name`: The name of the process
|
||||
- `memory`: How much memory the process is using
|
||||
- `cpu`: How much CPU the process is using
|
||||
|
||||
## Run Functions
|
||||
### `run(command)`
|
||||
|
||||
Runs a command or multiline script with arguments.
|
||||
|
||||
**Parameters:**
|
||||
- `command` (string): The command to run (can be a single command or a multiline script)
|
||||
|
||||
**Returns:** The result of the command, including output and whether it succeeded.
|
||||
|
||||
**Example 1: Running a simple command**
|
||||
```js
|
||||
// Run a simple command
|
||||
let result = run("ls -la");
|
||||
|
||||
// Check if the command was successful
|
||||
if result.success {
|
||||
print(`Command output: ${result.stdout}`);
|
||||
} else {
|
||||
print(`Command failed with error: ${result.stderr}`);
|
||||
}
|
||||
```
|
||||
|
||||
**Example 2: Running a multiline script**
|
||||
```js
|
||||
// Create a multiline script using backtick string literals
|
||||
let setup_script = `
|
||||
# Create directories
|
||||
mkdir -p /tmp/test_project
|
||||
cd /tmp/test_project
|
||||
|
||||
# Initialize git repository
|
||||
git init
|
||||
echo 'Initial content' > README.md
|
||||
git add README.md
|
||||
git config --local user.email 'test@example.com'
|
||||
git config --local user.name 'Test User'
|
||||
git commit -m 'Initial commit'
|
||||
`;
|
||||
|
||||
// Execute the multiline script
|
||||
let result = run(setup_script);
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
### `run_silent(command)`
|
||||
|
||||
Runs a command or multiline script with arguments silently (without displaying output).
|
||||
|
||||
**Parameters:**
|
||||
- `command` (string): The command to run
|
||||
|
||||
**Returns:** The result of the command, without displaying the output.
|
||||
|
||||
**Example:**
|
||||
|
||||
```js
|
||||
// Run a command silently
|
||||
let result = run_silent("git pull");
|
||||
|
||||
// Check the exit code
|
||||
if result.code == 0 {
|
||||
print("Git pull successful");
|
||||
} else {
|
||||
print(`Git pull failed with code ${result.code}`);
|
||||
}
|
||||
```
|
||||
|
||||
### `new_run_options()`
|
||||
|
||||
Creates a new map with default run options.
|
||||
|
||||
**Returns:** A map with the following default options:
|
||||
- `die` (boolean): `true` - Whether to throw an error if the command fails
|
||||
- `silent` (boolean): `false` - Whether to suppress command output
|
||||
- `async_exec` (boolean): `false` - Whether to run the command asynchronously
|
||||
- `log` (boolean): `false` - Whether to log the command execution
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Create run options
|
||||
let options = new_run_options();
|
||||
```
|
||||
|
||||
### `run_with_options(command, options)`
|
||||
|
||||
Runs a command with options specified in a map.
|
||||
|
||||
**Parameters:**
|
||||
- `command` (string): The command to run
|
||||
- `options` (map): A map of options created with `new_run_options()`
|
||||
|
||||
**Returns:** The result of the command with your custom settings applied.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Create and customize run options
|
||||
let options = new_run_options();
|
||||
options.die = false; // Don't throw an error if the command fails
|
||||
options.silent = true; // Suppress command output
|
||||
options.async_exec = false; // Run synchronously
|
||||
options.log = true; // Log the command execution
|
||||
|
||||
// Run a command with options
|
||||
let result = run_with_options("npm install", options);
|
||||
```
|
||||
|
||||
## Working with Multiline Scripts
|
||||
|
||||
The Process module allows you to execute multiline scripts, which is particularly useful for complex operations that require multiple commands to be executed in sequence.
|
||||
|
||||
### Creating Multiline Scripts
|
||||
|
||||
Multiline scripts can be created using backtick (`) string literals in HeroScript:
|
||||
|
||||
```js
|
||||
let my_script = `
|
||||
# This is a multiline bash script
|
||||
echo "Hello, World!"
|
||||
mkdir -p /tmp/my_project
|
||||
cd /tmp/my_project
|
||||
touch example.txt
|
||||
`;
|
||||
```
|
||||
|
||||
## Process Management Functions
|
||||
|
||||
### `which(cmd)`
|
||||
|
||||
Checks if a command exists in the PATH.
|
||||
|
||||
**Parameters:**
|
||||
- `cmd` (string): The command to check
|
||||
|
||||
**Returns:** The full path to the command if found, or nothing if not found.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Check if a command exists
|
||||
let git_path = which("git");
|
||||
|
||||
if git_path != () {
|
||||
print(`Git is installed at: ${git_path}`);
|
||||
} else {
|
||||
print("Git is not installed");
|
||||
}
|
||||
```
|
||||
|
||||
### `kill(pattern)`
|
||||
|
||||
Kills processes matching a pattern.
|
||||
|
||||
**Parameters:**
|
||||
- `pattern` (string): The pattern to match process names against
|
||||
|
||||
**Returns:** A message confirming the processes were killed.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Kill all processes with "node" in their name
|
||||
kill("node");
|
||||
```
|
||||
|
||||
### `process_list(pattern)`
|
||||
|
||||
Lists processes matching a pattern (or all processes if the pattern is empty).
|
||||
|
||||
**Parameters:**
|
||||
- `pattern` (string): The pattern to match process names against (can be empty to list all processes)
|
||||
|
||||
**Returns:** A list of processes matching your search.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// List all processes
|
||||
let all_processes = process_list("");
|
||||
|
||||
// List processes containing "node" in their name
|
||||
let node_processes = process_list("node");
|
||||
|
||||
// Display process information
|
||||
for process in node_processes {
|
||||
print(`PID: ${process.pid}, Name: ${process.name}, Memory: ${process.memory}, CPU: ${process.cpu}`);
|
||||
}
|
||||
```
|
||||
|
||||
### `process_get(pattern)`
|
||||
|
||||
Gets a single process matching the pattern. Throws an error if zero or more than one process matches.
|
||||
|
||||
**Parameters:**
|
||||
- `pattern` (string): The pattern to match process names against
|
||||
|
||||
**Returns:** Information about the matching process. This will only work if exactly one process matches.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
// Try to get a specific process
|
||||
try {
|
||||
let process = process_get("my_app");
|
||||
print(`Found process: PID=${process.pid}, Name=${process.name}`);
|
||||
} catch(err) {
|
||||
print(`Error: ${err}`);
|
||||
}
|
||||
```
|
@ -1,154 +0,0 @@
|
||||
# RFS (Remote File System)
|
||||
|
||||
The RFS module provides a Rust wrapper for the RFS tool, which allows mounting remote filesystems locally and managing filesystem layers.
|
||||
|
||||
## Overview
|
||||
|
||||
RFS (Remote File System) is a tool that enables mounting various types of remote filesystems locally, as well as creating and managing filesystem layers. The SAL library provides a Rust wrapper for RFS with a fluent builder API, making it easy to use in your applications.
|
||||
|
||||
## Features
|
||||
|
||||
- Mount remote filesystems locally (SSH, S3, WebDAV, etc.)
|
||||
- List mounted filesystems
|
||||
- Unmount filesystems
|
||||
- Pack directories into filesystem layers
|
||||
- Unpack filesystem layers
|
||||
- List contents of filesystem layers
|
||||
- Verify filesystem layers
|
||||
|
||||
## Usage in Rust
|
||||
|
||||
### Mounting a Filesystem
|
||||
|
||||
```rust
|
||||
use sal::virt::rfs::{RfsBuilder, MountType};
|
||||
|
||||
// Create a new RFS builder
|
||||
let mount = RfsBuilder::new("user@example.com:/remote/path", "/local/mount/point", MountType::SSH)
|
||||
.with_option("port", "2222")
|
||||
.with_option("identity_file", "/path/to/key")
|
||||
.with_debug(true)
|
||||
.mount()?;
|
||||
|
||||
println!("Mounted filesystem with ID: {}", mount.id);
|
||||
```
|
||||
|
||||
### Listing Mounts
|
||||
|
||||
```rust
|
||||
use sal::virt::rfs::list_mounts;
|
||||
|
||||
// List all mounts
|
||||
let mounts = list_mounts()?;
|
||||
for mount in mounts {
|
||||
println!("Mount ID: {}, Source: {}, Target: {}", mount.id, mount.source, mount.target);
|
||||
}
|
||||
```
|
||||
|
||||
### Unmounting a Filesystem
|
||||
|
||||
```rust
|
||||
use sal::virt::rfs::unmount;
|
||||
|
||||
// Unmount a filesystem
|
||||
unmount("/local/mount/point")?;
|
||||
```
|
||||
|
||||
### Packing a Directory
|
||||
|
||||
```rust
|
||||
use sal::virt::rfs::{PackBuilder, StoreSpec};
|
||||
|
||||
// Create store specifications
|
||||
let store_spec = StoreSpec::new("file")
|
||||
.with_option("path", "/path/to/store");
|
||||
|
||||
// Pack a directory with builder pattern
|
||||
let result = PackBuilder::new("/path/to/directory", "output.fl")
|
||||
.with_store_spec(store_spec)
|
||||
.with_debug(true)
|
||||
.pack()?;
|
||||
```
|
||||
|
||||
### Unpacking a Filesystem Layer
|
||||
|
||||
```rust
|
||||
use sal::virt::rfs::unpack;
|
||||
|
||||
// Unpack a filesystem layer
|
||||
unpack("input.fl", "/path/to/unpack")?;
|
||||
```
|
||||
|
||||
## Usage in Rhai Scripts
|
||||
|
||||
### Mounting a Filesystem
|
||||
|
||||
```rhai
|
||||
// Create a map for mount options
|
||||
let options = #{
|
||||
"port": "22",
|
||||
"identity_file": "/path/to/key",
|
||||
"readonly": "true"
|
||||
};
|
||||
|
||||
// Mount the directory
|
||||
let mount = rfs_mount("user@example.com:/remote/path", "/local/mount/point", "ssh", options);
|
||||
|
||||
print(`Mounted ${mount.source} to ${mount.target} with ID: ${mount.id}`);
|
||||
```
|
||||
|
||||
### Listing Mounts
|
||||
|
||||
```rhai
|
||||
// List all mounts
|
||||
let mounts = rfs_list_mounts();
|
||||
print(`Number of mounts: ${mounts.len()}`);
|
||||
|
||||
for mount in mounts {
|
||||
print(`Mount ID: ${mount.id}, Source: ${mount.source}, Target: ${mount.target}`);
|
||||
}
|
||||
```
|
||||
|
||||
### Unmounting a Filesystem
|
||||
|
||||
```rhai
|
||||
// Unmount the directory
|
||||
rfs_unmount("/local/mount/point");
|
||||
```
|
||||
|
||||
### Packing a Directory
|
||||
|
||||
```rhai
|
||||
// Pack the directory
|
||||
// Store specs format: "file:path=/path/to/store,s3:bucket=my-bucket"
|
||||
rfs_pack("/path/to/directory", "output.fl", "file:path=/path/to/store");
|
||||
```
|
||||
|
||||
### Unpacking a Filesystem Layer
|
||||
|
||||
```rhai
|
||||
// Unpack the filesystem layer
|
||||
rfs_unpack("output.fl", "/path/to/unpack");
|
||||
```
|
||||
|
||||
## Mount Types
|
||||
|
||||
The RFS module supports various mount types:
|
||||
|
||||
- **Local**: Mount a local directory
|
||||
- **SSH**: Mount a remote directory via SSH
|
||||
- **S3**: Mount an S3 bucket
|
||||
- **WebDAV**: Mount a WebDAV server
|
||||
|
||||
## Store Specifications
|
||||
|
||||
When packing a directory into a filesystem layer, you can specify one or more stores to use. Each store has a type and options:
|
||||
|
||||
- **File**: Store files on the local filesystem
|
||||
- Options: `path` (path to the store)
|
||||
- **S3**: Store files in an S3 bucket
|
||||
- Options: `bucket` (bucket name), `region` (AWS region), `access_key`, `secret_key`
|
||||
|
||||
## Examples
|
||||
|
||||
See the [RFS example script](../../rhaiexamples/rfs_example.rhai) for more examples of how to use the RFS module in Rhai scripts.
|
@ -1,237 +0,0 @@
|
||||
# Text Manipulation Tools
|
||||
|
||||
The SAL text module provides powerful text manipulation capabilities that can be used from Rhai scripts. These include text replacement (with regex support), template rendering, string normalization, and text formatting utilities.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Text Replacement](#text-replacement)
|
||||
- [Template Rendering](#template-rendering)
|
||||
- [String Normalization](#string-normalization)
|
||||
- [Text Formatting](#text-formatting)
|
||||
|
||||
## Text Replacement
|
||||
|
||||
The text replacement tools allow you to perform simple or complex text replacements, with support for regular expressions, case-insensitive matching, and file operations.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```rhai
|
||||
// Create a new text replacer
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("foo") // Set the pattern to search for
|
||||
.replacement("bar") // Set the replacement text
|
||||
.build(); // Build the replacer
|
||||
|
||||
// Apply the replacer to a string
|
||||
let result = replacer.replace("foo bar foo");
|
||||
// Result: "bar bar bar"
|
||||
```
|
||||
|
||||
### Advanced Features
|
||||
|
||||
#### Regular Expressions
|
||||
|
||||
```rhai
|
||||
// Create a replacer with regex support
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("\\bfoo\\b") // Use regex pattern (word boundary)
|
||||
.replacement("bar")
|
||||
.regex(true) // Enable regex mode
|
||||
.build();
|
||||
|
||||
// Apply the replacer to a string
|
||||
let result = replacer.replace("foo foobar");
|
||||
// Result: "bar foobar" (only replaces whole "foo" words)
|
||||
```
|
||||
|
||||
#### Case-Insensitive Matching
|
||||
|
||||
```rhai
|
||||
// Create a replacer with case-insensitive matching
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("foo")
|
||||
.replacement("bar")
|
||||
.regex(true)
|
||||
.case_insensitive(true) // Enable case-insensitive matching
|
||||
.build();
|
||||
|
||||
// Apply the replacer to a string
|
||||
let result = replacer.replace("FOO foo Foo");
|
||||
// Result: "bar bar bar"
|
||||
```
|
||||
|
||||
#### Multiple Replacements
|
||||
|
||||
```rhai
|
||||
// Chain multiple replacements
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("foo")
|
||||
.replacement("bar")
|
||||
.and() // Add another replacement operation
|
||||
.pattern("baz")
|
||||
.replacement("qux")
|
||||
.build();
|
||||
|
||||
// Apply the replacer to a string
|
||||
let result = replacer.replace("foo baz");
|
||||
// Result: "bar qux"
|
||||
```
|
||||
|
||||
#### File Operations
|
||||
|
||||
```rhai
|
||||
// Create a replacer
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("foo")
|
||||
.replacement("bar")
|
||||
.build();
|
||||
|
||||
// Replace in a file and get the result as a string
|
||||
let result = replacer.replace_file("input.txt");
|
||||
|
||||
// Replace in a file and write back to the same file
|
||||
replacer.replace_file_in_place("input.txt");
|
||||
|
||||
// Replace in a file and write to a new file
|
||||
replacer.replace_file_to("input.txt", "output.txt");
|
||||
```
|
||||
|
||||
## Template Rendering
|
||||
|
||||
The template rendering tools allow you to create and render templates with variables, using the powerful Tera template engine.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```rhai
|
||||
// Create a template builder with a template file
|
||||
let template = template_builder_open("template.txt")
|
||||
.add_var("name", "John") // Add a string variable
|
||||
.add_var("age", 30) // Add a numeric variable
|
||||
.add_var("items", ["a", "b", "c"]); // Add an array variable
|
||||
|
||||
// Render the template
|
||||
let result = template.render();
|
||||
|
||||
// Render to a file
|
||||
template.render_to_file("output.txt");
|
||||
```
|
||||
|
||||
### Template Variables
|
||||
|
||||
You can add variables of various types:
|
||||
|
||||
```rhai
|
||||
let template = template_builder_open("template.txt")
|
||||
.add_var("name", "John") // String
|
||||
.add_var("age", 30) // Integer
|
||||
.add_var("height", 1.85) // Float
|
||||
.add_var("is_active", true) // Boolean
|
||||
.add_var("items", ["a", "b", "c"]); // Array
|
||||
```
|
||||
|
||||
### Using Map for Variables
|
||||
|
||||
```rhai
|
||||
// Create a map of variables
|
||||
let vars = #{
|
||||
name: "Alice",
|
||||
place: "Wonderland"
|
||||
};
|
||||
|
||||
// Add all variables from the map
|
||||
let template = template_builder_open("template.txt")
|
||||
.add_vars(vars);
|
||||
```
|
||||
|
||||
### Template Syntax
|
||||
|
||||
The template engine uses Tera, which supports:
|
||||
|
||||
- Variable interpolation: `{{ variable }}`
|
||||
- Conditionals: `{% if condition %}...{% endif %}`
|
||||
- Loops: `{% for item in items %}...{% endfor %}`
|
||||
- Filters: `{{ variable | filter }}`
|
||||
|
||||
Example template:
|
||||
|
||||
```
|
||||
Hello, {{ name }}!
|
||||
|
||||
{% if show_greeting %}
|
||||
Welcome to {{ place }}.
|
||||
{% endif %}
|
||||
|
||||
Your items:
|
||||
{% for item in items %}
|
||||
- {{ item }}{% if not loop.last %}{% endif %}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
## String Normalization
|
||||
|
||||
The string normalization tools help convert strings to consistent formats for use as file names or paths.
|
||||
|
||||
### name_fix
|
||||
|
||||
Converts a string to a safe, normalized name by:
|
||||
- Converting to lowercase
|
||||
- Replacing spaces and special characters with underscores
|
||||
- Removing non-alphanumeric characters
|
||||
|
||||
```rhai
|
||||
let fixed_name = name_fix("Hello World!");
|
||||
// Result: "hello_world"
|
||||
|
||||
let fixed_name = name_fix("File-Name.txt");
|
||||
// Result: "file_name.txt"
|
||||
```
|
||||
|
||||
### path_fix
|
||||
|
||||
Similar to name_fix, but preserves path separators:
|
||||
|
||||
```rhai
|
||||
let fixed_path = path_fix("/path/to/Hello World!");
|
||||
// Result: "/path/to/hello_world"
|
||||
|
||||
let fixed_path = path_fix("./relative/path/to/DOCUMENT-123.pdf");
|
||||
// Result: "./relative/path/to/document_123.pdf"
|
||||
```
|
||||
|
||||
## Text Formatting
|
||||
|
||||
Tools to help with text formatting and indentation.
|
||||
|
||||
### dedent
|
||||
|
||||
Removes common leading whitespace from multi-line strings:
|
||||
|
||||
```rhai
|
||||
let indented_text = " line 1
|
||||
line 2
|
||||
line 3";
|
||||
|
||||
let dedented = dedent(indented_text);
|
||||
// Result: "line 1
|
||||
// line 2
|
||||
// line 3"
|
||||
```
|
||||
|
||||
### prefix
|
||||
|
||||
Adds a prefix to every line in a multi-line string:
|
||||
|
||||
```rhai
|
||||
let text = "line 1
|
||||
line 2
|
||||
line 3";
|
||||
|
||||
let prefixed = prefix(text, " ");
|
||||
// Result: " line 1
|
||||
// line 2
|
||||
// line 3"
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See the [text_tools.rhai](https://github.com/ourworld-tf/herocode/blob/main/sal/src/rhaiexamples/text_tools.rhai) example script for more detailed examples of using these text manipulation tools.
|
@ -1,79 +0,0 @@
|
||||
//! Example of using the Rhai integration with SAL
|
||||
//!
|
||||
//! This example demonstrates how to use the Rhai scripting language
|
||||
//! with the System Abstraction Layer (SAL) library.
|
||||
use sal::rhai::{self, Engine};
|
||||
use std::fs;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a new Rhai engine
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register SAL functions with the engine
|
||||
rhai::register(&mut engine)?;
|
||||
|
||||
// Create a test file
|
||||
let test_file = "rhai_test_file.txt";
|
||||
fs::write(test_file, "Hello, Rhai!")?;
|
||||
|
||||
// Create a test directory
|
||||
let test_dir = "rhai_test_dir";
|
||||
if !fs::metadata(test_dir).is_ok() {
|
||||
fs::create_dir(test_dir)?;
|
||||
}
|
||||
|
||||
// Run a Rhai script that uses SAL functions
|
||||
let script = r#"
|
||||
// Check if files exist
|
||||
let file_exists = exist("rhai_test_file.txt");
|
||||
let dir_exists = exist("rhai_test_dir");
|
||||
|
||||
// Get file size
|
||||
let size = file_size("rhai_test_file.txt");
|
||||
|
||||
// Create a new directory
|
||||
let new_dir = "rhai_new_dir";
|
||||
let mkdir_result = mkdir(new_dir);
|
||||
|
||||
// Copy a file
|
||||
let copy_result = copy("rhai_test_file.txt", "rhai_test_dir/copied_file.txt");
|
||||
|
||||
// Find files
|
||||
let files = find_files(".", "*.txt");
|
||||
|
||||
// Return a map with all the results
|
||||
#{
|
||||
file_exists: file_exists,
|
||||
dir_exists: dir_exists,
|
||||
file_size: size,
|
||||
mkdir_result: mkdir_result,
|
||||
copy_result: copy_result,
|
||||
files: files
|
||||
}
|
||||
"#;
|
||||
|
||||
// Evaluate the script and get the results
|
||||
let result = engine.eval::<rhai::Map>(script)?;
|
||||
|
||||
// Print the results
|
||||
println!("Script results:");
|
||||
println!(" File exists: {}", result.get("file_exists").unwrap().clone().cast::<bool>());
|
||||
println!(" Directory exists: {}", result.get("dir_exists").unwrap().clone().cast::<bool>());
|
||||
println!(" File size: {} bytes", result.get("file_size").unwrap().clone().cast::<i64>());
|
||||
println!(" Mkdir result: {}", result.get("mkdir_result").unwrap().clone().cast::<String>());
|
||||
println!(" Copy result: {}", result.get("copy_result").unwrap().clone().cast::<String>());
|
||||
|
||||
// Print the found files
|
||||
let files = result.get("files").unwrap().clone().cast::<rhai::Array>();
|
||||
println!(" Found files:");
|
||||
for file in files {
|
||||
println!(" - {}", file.cast::<String>());
|
||||
}
|
||||
|
||||
// Clean up
|
||||
fs::remove_file(test_file)?;
|
||||
fs::remove_dir_all(test_dir)?;
|
||||
fs::remove_dir_all("rhai_new_dir")?;
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use sal::text::TemplateBuilder;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// Create a temporary template file for our examples
|
||||
let temp_file = NamedTempFile::new()?;
|
||||
let template_content = "Hello, {{ name }}! Welcome to {{ place }}.\n\
|
||||
{% if show_greeting %}Glad to have you here!{% endif %}\n\
|
||||
Your items:\n\
|
||||
{% for item in items %} - {{ item }}{% if not loop.last %}\n{% endif %}{% endfor %}\n";
|
||||
std::fs::write(temp_file.path(), template_content)?;
|
||||
|
||||
println!("Created temporary template at: {}", temp_file.path().display());
|
||||
|
||||
// Example 1: Simple variable replacement
|
||||
println!("\n--- Example 1: Simple variable replacement ---");
|
||||
let mut builder = TemplateBuilder::open(temp_file.path())?;
|
||||
builder = builder
|
||||
.add_var("name", "John")
|
||||
.add_var("place", "Rust")
|
||||
.add_var("show_greeting", true)
|
||||
.add_var("items", vec!["apple", "banana", "cherry"]);
|
||||
|
||||
let result = builder.render()?;
|
||||
println!("Rendered template:\n{}", result);
|
||||
|
||||
// Example 2: Using a HashMap for variables
|
||||
println!("\n--- Example 2: Using a HashMap for variables ---");
|
||||
let mut vars = HashMap::new();
|
||||
vars.insert("name", "Alice");
|
||||
vars.insert("place", "Template World");
|
||||
|
||||
let mut builder = TemplateBuilder::open(temp_file.path())?;
|
||||
builder = builder
|
||||
.add_vars(vars)
|
||||
.add_var("show_greeting", false)
|
||||
.add_var("items", vec!["laptop", "phone", "tablet"]);
|
||||
|
||||
let result = builder.render()?;
|
||||
println!("Rendered template with HashMap:\n{}", result);
|
||||
|
||||
// Example 3: Rendering to a file
|
||||
println!("\n--- Example 3: Rendering to a file ---");
|
||||
let output_file = NamedTempFile::new()?;
|
||||
|
||||
let mut builder = TemplateBuilder::open(temp_file.path())?;
|
||||
builder = builder
|
||||
.add_var("name", "Bob")
|
||||
.add_var("place", "File Output")
|
||||
.add_var("show_greeting", true)
|
||||
.add_var("items", vec!["document", "spreadsheet", "presentation"]);
|
||||
|
||||
builder.render_to_file(output_file.path())?;
|
||||
println!("Template rendered to file: {}", output_file.path().display());
|
||||
|
||||
// Read the output file to verify
|
||||
let output_content = std::fs::read_to_string(output_file.path())?;
|
||||
println!("Content of the rendered file:\n{}", output_content);
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use sal::text::TextReplacer;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// Create a temporary file for our examples
|
||||
let mut temp_file = NamedTempFile::new()?;
|
||||
writeln!(temp_file, "This is a foo bar example with FOO and foo occurrences.")?;
|
||||
println!("Created temporary file at: {}", temp_file.path().display());
|
||||
|
||||
// Example 1: Simple regex replacement
|
||||
println!("\n--- Example 1: Simple regex replacement ---");
|
||||
let replacer = TextReplacer::builder()
|
||||
.pattern(r"\bfoo\b")
|
||||
.replacement("replacement")
|
||||
.regex(true)
|
||||
.add_replacement()?
|
||||
.build()?;
|
||||
|
||||
let result = replacer.replace_file(temp_file.path())?;
|
||||
println!("After regex replacement: {}", result);
|
||||
|
||||
// Example 2: Multiple replacements in one pass
|
||||
println!("\n--- Example 2: Multiple replacements in one pass ---");
|
||||
let replacer = TextReplacer::builder()
|
||||
.pattern("foo")
|
||||
.replacement("AAA")
|
||||
.add_replacement()?
|
||||
.pattern("bar")
|
||||
.replacement("BBB")
|
||||
.add_replacement()?
|
||||
.build()?;
|
||||
|
||||
// Write new content to the temp file
|
||||
writeln!(temp_file.as_file_mut(), "foo bar foo baz")?;
|
||||
temp_file.as_file_mut().flush()?;
|
||||
|
||||
let result = replacer.replace_file(temp_file.path())?;
|
||||
println!("After multiple replacements: {}", result);
|
||||
|
||||
// Example 3: Case-insensitive replacement
|
||||
println!("\n--- Example 3: Case-insensitive replacement ---");
|
||||
let replacer = TextReplacer::builder()
|
||||
.pattern("foo")
|
||||
.replacement("case-insensitive")
|
||||
.regex(true)
|
||||
.case_insensitive(true)
|
||||
.add_replacement()?
|
||||
.build()?;
|
||||
|
||||
// Write new content to the temp file
|
||||
writeln!(temp_file.as_file_mut(), "FOO foo Foo fOo")?;
|
||||
temp_file.as_file_mut().flush()?;
|
||||
|
||||
let result = replacer.replace_file(temp_file.path())?;
|
||||
println!("After case-insensitive replacement: {}", result);
|
||||
|
||||
// Example 4: File operations
|
||||
println!("\n--- Example 4: File operations ---");
|
||||
let output_file = NamedTempFile::new()?;
|
||||
|
||||
let replacer = TextReplacer::builder()
|
||||
.pattern("example")
|
||||
.replacement("EXAMPLE")
|
||||
.add_replacement()?
|
||||
.build()?;
|
||||
|
||||
// Write new content to the temp file
|
||||
writeln!(temp_file.as_file_mut(), "This is an example text file.")?;
|
||||
temp_file.as_file_mut().flush()?;
|
||||
|
||||
// Replace and write to a new file
|
||||
replacer.replace_file_to(temp_file.path(), output_file.path())?;
|
||||
|
||||
// Read the output file to verify
|
||||
let output_content = std::fs::read_to_string(output_file.path())?;
|
||||
println!("Content written to new file: {}", output_content);
|
||||
|
||||
// Example 5: Replace in-place
|
||||
println!("\n--- Example 5: Replace in-place ---");
|
||||
|
||||
// Replace in the same file
|
||||
replacer.replace_file_in_place(temp_file.path())?;
|
||||
|
||||
// Read the file to verify
|
||||
let updated_content = std::fs::read_to_string(temp_file.path())?;
|
||||
println!("Content after in-place replacement: {}", updated_content);
|
||||
|
||||
Ok(())
|
||||
}
|
871
src/git/git.rs
871
src/git/git.rs
@ -1,6 +1,7 @@
|
||||
use std::process::Command;
|
||||
use std::path::Path;
|
||||
use std::fs;
|
||||
use std::env;
|
||||
use regex::Regex;
|
||||
use std::fmt;
|
||||
use std::error::Error;
|
||||
@ -10,7 +11,6 @@ use std::error::Error;
|
||||
pub enum GitError {
|
||||
GitNotInstalled(std::io::Error),
|
||||
InvalidUrl(String),
|
||||
InvalidBasePath(String),
|
||||
HomeDirectoryNotFound(std::env::VarError),
|
||||
FileSystemError(std::io::Error),
|
||||
GitCommandFailed(String),
|
||||
@ -28,7 +28,6 @@ impl fmt::Display for GitError {
|
||||
match self {
|
||||
GitError::GitNotInstalled(e) => write!(f, "Git is not installed: {}", e),
|
||||
GitError::InvalidUrl(url) => write!(f, "Could not parse git URL: {}", url),
|
||||
GitError::InvalidBasePath(path) => write!(f, "Invalid base path: {}", path),
|
||||
GitError::HomeDirectoryNotFound(e) => write!(f, "Could not determine home directory: {}", e),
|
||||
GitError::FileSystemError(e) => write!(f, "Error creating directory structure: {}", e),
|
||||
GitError::GitCommandFailed(e) => write!(f, "{}", e),
|
||||
@ -56,21 +55,98 @@ impl Error for GitError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a git URL to extract the server, account, and repository name.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - The URL of the git repository to parse. Can be in HTTPS format
|
||||
/// (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git).
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A tuple containing:
|
||||
/// * `server` - The server name (e.g., "github.com")
|
||||
/// * `account` - The account or organization name (e.g., "username")
|
||||
/// * `repo` - The repository name (e.g., "repo")
|
||||
///
|
||||
/// If the URL cannot be parsed, all three values will be empty strings.
|
||||
// Git utility functions
|
||||
|
||||
/**
|
||||
* Clones a git repository to a standardized location in the user's home directory.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `url` - The URL of the git repository to clone. Can be in HTTPS format
|
||||
* (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git).
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(String)` - The path where the repository was cloned, formatted as
|
||||
* ~/code/server/account/repo (e.g., ~/code/github.com/username/repo).
|
||||
* * `Err(GitError)` - An error if the clone operation failed.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* let repo_path = git_clone("https://github.com/username/repo.git")?;
|
||||
* println!("Repository cloned to: {}", repo_path);
|
||||
* ```
|
||||
*/
|
||||
pub fn git_clone(url: &str) -> Result<String, GitError> {
|
||||
// Check if git is installed
|
||||
let _git_check = Command::new("git")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map_err(GitError::GitNotInstalled)?;
|
||||
|
||||
// Parse the URL to determine the clone path
|
||||
let (server, account, repo) = parse_git_url(url);
|
||||
if server.is_empty() || account.is_empty() || repo.is_empty() {
|
||||
return Err(GitError::InvalidUrl(url.to_string()));
|
||||
}
|
||||
|
||||
// Create the target directory
|
||||
let home_dir = env::var("HOME").map_err(GitError::HomeDirectoryNotFound)?;
|
||||
|
||||
let clone_path = format!("{}/code/{}/{}/{}", home_dir, server, account, repo);
|
||||
let clone_dir = Path::new(&clone_path);
|
||||
|
||||
// Check if repo already exists
|
||||
if clone_dir.exists() {
|
||||
return Ok(format!("Repository already exists at {}", clone_path));
|
||||
}
|
||||
|
||||
// Create parent directory
|
||||
if let Some(parent) = clone_dir.parent() {
|
||||
fs::create_dir_all(parent).map_err(GitError::FileSystemError)?;
|
||||
}
|
||||
|
||||
// Clone the repository
|
||||
let output = Command::new("git")
|
||||
.args(&["clone", "--depth", "1", url, &clone_path])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(clone_path)
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
Err(GitError::GitCommandFailed(format!("Git clone error: {}", error)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a git URL to extract the server, account, and repository name.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `url` - The URL of the git repository to parse. Can be in HTTPS format
|
||||
* (https://github.com/username/repo.git) or SSH format (git@github.com:username/repo.git).
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* A tuple containing:
|
||||
* * `server` - The server name (e.g., "github.com")
|
||||
* * `account` - The account or organization name (e.g., "username")
|
||||
* * `repo` - The repository name (e.g., "repo")
|
||||
*
|
||||
* If the URL cannot be parsed, all three values will be empty strings.
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* let (server, account, repo) = parse_git_url("https://github.com/username/repo.git");
|
||||
* assert_eq!(server, "github.com");
|
||||
* assert_eq!(account, "username");
|
||||
* assert_eq!(repo, "repo");
|
||||
* ```
|
||||
*/
|
||||
pub fn parse_git_url(url: &str) -> (String, String, String) {
|
||||
// HTTP(S) URL format: https://github.com/username/repo.git
|
||||
let https_re = Regex::new(r"https?://([^/]+)/([^/]+)/([^/\.]+)(?:\.git)?").unwrap();
|
||||
@ -95,394 +171,427 @@ pub fn parse_git_url(url: &str) -> (String, String, String) {
|
||||
(String::new(), String::new(), String::new())
|
||||
}
|
||||
|
||||
/// Checks if git is installed on the system.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(())` - If git is installed
|
||||
/// * `Err(GitError)` - If git is not installed
|
||||
fn check_git_installed() -> Result<(), GitError> {
|
||||
Command::new("git")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map_err(GitError::GitNotInstalled)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Represents a collection of git repositories under a base path.
|
||||
#[derive(Clone)]
|
||||
pub struct GitTree {
|
||||
base_path: String,
|
||||
}
|
||||
|
||||
impl GitTree {
|
||||
/// Creates a new GitTree with the specified base path.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `base_path` - The base path where all git repositories are located
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(GitTree)` - A new GitTree instance
|
||||
/// * `Err(GitError)` - If the base path is invalid or cannot be created
|
||||
pub fn new(base_path: &str) -> Result<Self, GitError> {
|
||||
// Check if git is installed
|
||||
check_git_installed()?;
|
||||
|
||||
// Validate the base path
|
||||
let path = Path::new(base_path);
|
||||
if !path.exists() {
|
||||
fs::create_dir_all(path).map_err(|e| {
|
||||
GitError::FileSystemError(e)
|
||||
})?;
|
||||
} else if !path.is_dir() {
|
||||
return Err(GitError::InvalidBasePath(base_path.to_string()));
|
||||
}
|
||||
|
||||
Ok(GitTree {
|
||||
base_path: base_path.to_string(),
|
||||
})
|
||||
/**
|
||||
* Lists all git repositories found in the user's ~/code directory.
|
||||
*
|
||||
* This function searches for directories containing a .git subdirectory,
|
||||
* which indicates a git repository.
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(Vec<String>)` - A vector of paths to git repositories
|
||||
* * `Err(GitError)` - An error if the operation failed
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* let repos = git_list()?;
|
||||
* for repo in repos {
|
||||
* println!("Found repository: {}", repo);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
pub fn git_list() -> Result<Vec<String>, GitError> {
|
||||
// Get home directory
|
||||
let home_dir = env::var("HOME").map_err(GitError::HomeDirectoryNotFound)?;
|
||||
|
||||
let code_dir = format!("{}/code", home_dir);
|
||||
let code_path = Path::new(&code_dir);
|
||||
|
||||
if !code_path.exists() || !code_path.is_dir() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
/// Lists all git repositories under the base path.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Vec<String>)` - A vector of paths to git repositories
|
||||
/// * `Err(GitError)` - If the operation failed
|
||||
pub fn list(&self) -> Result<Vec<String>, GitError> {
|
||||
let base_path = Path::new(&self.base_path);
|
||||
let mut repos = Vec::new();
|
||||
|
||||
// Find all directories with .git subdirectories
|
||||
let output = Command::new("find")
|
||||
.args(&[&code_dir, "-type", "d", "-name", ".git"])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if !base_path.exists() || !base_path.is_dir() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut repos = Vec::new();
|
||||
|
||||
// Find all directories with .git subdirectories
|
||||
let output = Command::new("find")
|
||||
.args(&[&self.base_path, "-type", "d", "-name", ".git"])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for line in stdout.lines() {
|
||||
// Get the parent directory of .git which is the repo root
|
||||
if let Some(parent) = Path::new(line).parent() {
|
||||
if let Some(path_str) = parent.to_str() {
|
||||
repos.push(path_str.to_string());
|
||||
}
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for line in stdout.lines() {
|
||||
// Get the parent directory of .git which is the repo root
|
||||
if let Some(parent) = Path::new(line).parent() {
|
||||
if let Some(path_str) = parent.to_str() {
|
||||
repos.push(path_str.to_string());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(GitError::GitCommandFailed(format!("Failed to find git repositories: {}", error)));
|
||||
}
|
||||
|
||||
Ok(repos)
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(GitError::GitCommandFailed(format!("Failed to find git repositories: {}", error)));
|
||||
}
|
||||
|
||||
/// Finds repositories matching a pattern or partial path.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pattern` - The pattern to match against repository paths
|
||||
/// - If the pattern ends with '*', all matching repositories are returned
|
||||
/// - Otherwise, exactly one matching repository must be found
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Vec<String>)` - A vector of paths to matching repositories
|
||||
/// * `Err(GitError)` - If no matching repositories are found,
|
||||
/// or if multiple repositories match a non-wildcard pattern
|
||||
pub fn find(&self, pattern: &str) -> Result<Vec<String>, GitError> {
|
||||
// Get all repos
|
||||
let repos = self.list()?;
|
||||
Ok(repos)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a git repository has uncommitted changes.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `repo_path` - The path to the git repository
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(bool)` - True if the repository has uncommitted changes, false otherwise
|
||||
* * `Err(GitError)` - An error if the operation failed
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* if has_git_changes("/path/to/repo")? {
|
||||
* println!("Repository has uncommitted changes");
|
||||
* } else {
|
||||
* println!("Repository is clean");
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
pub fn has_git_changes(repo_path: &str) -> Result<bool, GitError> {
|
||||
let output = Command::new("git")
|
||||
.args(&["-C", repo_path, "status", "--porcelain"])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if repos.is_empty() {
|
||||
return Err(GitError::NoRepositoriesFound);
|
||||
}
|
||||
|
||||
// Check if pattern ends with wildcard
|
||||
if pattern.ends_with('*') {
|
||||
let search_pattern = &pattern[0..pattern.len()-1]; // Remove the *
|
||||
let matching: Vec<String> = repos.iter()
|
||||
.filter(|repo| repo.contains(search_pattern))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
if matching.is_empty() {
|
||||
return Err(GitError::RepositoryNotFound(pattern.to_string()));
|
||||
}
|
||||
|
||||
Ok(matching)
|
||||
} else {
|
||||
// No wildcard, need to find exactly one match
|
||||
let matching: Vec<String> = repos.iter()
|
||||
.filter(|repo| repo.contains(pattern))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
match matching.len() {
|
||||
0 => Err(GitError::RepositoryNotFound(pattern.to_string())),
|
||||
1 => Ok(matching),
|
||||
_ => Err(GitError::MultipleRepositoriesFound(pattern.to_string(), matching.len())),
|
||||
}
|
||||
}
|
||||
Ok(!output.stdout.is_empty())
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds repositories matching a pattern or partial path.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `pattern` - The pattern to match against repository paths
|
||||
* - If the pattern ends with '*', all matching repositories are returned
|
||||
* - Otherwise, exactly one matching repository must be found
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(Vec<String>)` - A vector of paths to matching repositories
|
||||
* * `Err(GitError)` - An error if no matching repositories are found,
|
||||
* or if multiple repositories match a non-wildcard pattern
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* // Find all repositories containing "project"
|
||||
* let repos = find_matching_repos("project*")?;
|
||||
*
|
||||
* // Find exactly one repository containing "unique-project"
|
||||
* let repo = find_matching_repos("unique-project")?[0];
|
||||
* ```
|
||||
*/
|
||||
pub fn find_matching_repos(pattern: &str) -> Result<Vec<String>, GitError> {
|
||||
// Get all repos
|
||||
let repos = git_list()?;
|
||||
|
||||
if repos.is_empty() {
|
||||
return Err(GitError::NoRepositoriesFound);
|
||||
}
|
||||
|
||||
/// Gets one or more GitRepo objects based on a path pattern or URL.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path_or_url` - The path pattern to match against repository paths or a git URL
|
||||
/// - If it's a URL, the repository will be cloned if it doesn't exist
|
||||
/// - If it's a path pattern, it will find matching repositories
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Vec<GitRepo>)` - A vector of GitRepo objects
|
||||
/// * `Err(GitError)` - If no matching repositories are found or the clone operation failed
|
||||
pub fn get(&self, path_or_url: &str) -> Result<Vec<GitRepo>, GitError> {
|
||||
// Check if it's a URL
|
||||
if path_or_url.starts_with("http") || path_or_url.starts_with("git@") {
|
||||
// Parse the URL
|
||||
let (server, account, repo) = parse_git_url(path_or_url);
|
||||
if server.is_empty() || account.is_empty() || repo.is_empty() {
|
||||
return Err(GitError::InvalidUrl(path_or_url.to_string()));
|
||||
}
|
||||
|
||||
// Create the target directory
|
||||
let clone_path = format!("{}/{}/{}/{}", self.base_path, server, account, repo);
|
||||
let clone_dir = Path::new(&clone_path);
|
||||
|
||||
// Check if repo already exists
|
||||
if clone_dir.exists() {
|
||||
return Ok(vec![GitRepo::new(clone_path)]);
|
||||
}
|
||||
|
||||
// Create parent directory
|
||||
if let Some(parent) = clone_dir.parent() {
|
||||
fs::create_dir_all(parent).map_err(GitError::FileSystemError)?;
|
||||
}
|
||||
|
||||
// Clone the repository
|
||||
let output = Command::new("git")
|
||||
.args(&["clone", "--depth", "1", path_or_url, &clone_path])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(vec![GitRepo::new(clone_path)])
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
Err(GitError::GitCommandFailed(format!("Git clone error: {}", error)))
|
||||
}
|
||||
} else {
|
||||
// It's a path pattern, find matching repositories
|
||||
let repo_paths = self.find(path_or_url)?;
|
||||
|
||||
// Convert paths to GitRepo objects
|
||||
let repos: Vec<GitRepo> = repo_paths.into_iter()
|
||||
.map(GitRepo::new)
|
||||
.collect();
|
||||
|
||||
Ok(repos)
|
||||
// Check if pattern ends with wildcard
|
||||
if pattern.ends_with('*') {
|
||||
let search_pattern = &pattern[0..pattern.len()-1]; // Remove the *
|
||||
let matching: Vec<String> = repos.iter()
|
||||
.filter(|repo| repo.contains(search_pattern))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
if matching.is_empty() {
|
||||
return Err(GitError::RepositoryNotFound(pattern.to_string()));
|
||||
}
|
||||
|
||||
Ok(matching)
|
||||
} else {
|
||||
// No wildcard, need to find exactly one match
|
||||
let matching: Vec<String> = repos.iter()
|
||||
.filter(|repo| repo.contains(pattern))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
match matching.len() {
|
||||
0 => Err(GitError::RepositoryNotFound(pattern.to_string())),
|
||||
1 => Ok(matching),
|
||||
_ => Err(GitError::MultipleRepositoriesFound(pattern.to_string(), matching.len())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a git repository.
|
||||
pub struct GitRepo {
|
||||
path: String,
|
||||
}
|
||||
|
||||
impl GitRepo {
|
||||
/// Creates a new GitRepo with the specified path.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - The path to the git repository
|
||||
pub fn new(path: String) -> Self {
|
||||
GitRepo { path }
|
||||
/**
|
||||
* Updates a git repository by pulling the latest changes.
|
||||
*
|
||||
* This function will fail if there are uncommitted changes in the repository.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(String)` - A success message indicating the repository was updated
|
||||
* * `Err(GitError)` - An error if the update failed
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* let result = git_update("my-project")?;
|
||||
* println!("{}", result); // "Successfully updated repository at /home/user/code/github.com/user/my-project"
|
||||
* ```
|
||||
*/
|
||||
pub fn git_update(repo_path: &str) -> Result<String, GitError> {
|
||||
// If repo_path may be a partial path, find the matching repository
|
||||
let repos = find_matching_repos(repo_path)?;
|
||||
|
||||
// Should only be one repository at this point
|
||||
let actual_path = &repos[0];
|
||||
|
||||
// Check if repository exists and is a git repository
|
||||
let git_dir = Path::new(actual_path).join(".git");
|
||||
if !git_dir.exists() || !git_dir.is_dir() {
|
||||
return Err(GitError::NotAGitRepository(actual_path.clone()));
|
||||
}
|
||||
|
||||
/// Gets the path of the repository.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * The path to the git repository
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
// Check for local changes
|
||||
if has_git_changes(actual_path)? {
|
||||
return Err(GitError::LocalChangesExist(actual_path.clone()));
|
||||
}
|
||||
|
||||
/// Checks if the repository has uncommitted changes.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(bool)` - True if the repository has uncommitted changes, false otherwise
|
||||
/// * `Err(GitError)` - If the operation failed
|
||||
pub fn has_changes(&self) -> Result<bool, GitError> {
|
||||
let output = Command::new("git")
|
||||
.args(&["-C", &self.path, "status", "--porcelain"])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
Ok(!output.stdout.is_empty())
|
||||
}
|
||||
|
||||
/// Pulls the latest changes from the remote repository.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Self)` - The GitRepo object for method chaining
|
||||
/// * `Err(GitError)` - If the pull operation failed
|
||||
pub fn pull(&self) -> Result<Self, GitError> {
|
||||
// Check if repository exists and is a git repository
|
||||
let git_dir = Path::new(&self.path).join(".git");
|
||||
if !git_dir.exists() || !git_dir.is_dir() {
|
||||
return Err(GitError::NotAGitRepository(self.path.clone()));
|
||||
}
|
||||
|
||||
// Check for local changes
|
||||
if self.has_changes()? {
|
||||
return Err(GitError::LocalChangesExist(self.path.clone()));
|
||||
}
|
||||
|
||||
// Pull the latest changes
|
||||
let output = Command::new("git")
|
||||
.args(&["-C", &self.path, "pull"])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
// Pull the latest changes
|
||||
let output = Command::new("git")
|
||||
.args(&["-C", actual_path, "pull"])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(self.clone())
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if stdout.contains("Already up to date") {
|
||||
Ok(format!("Repository already up to date at {}", actual_path))
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets any local changes in the repository.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Self)` - The GitRepo object for method chaining
|
||||
/// * `Err(GitError)` - If the reset operation failed
|
||||
pub fn reset(&self) -> Result<Self, GitError> {
|
||||
// Check if repository exists and is a git repository
|
||||
let git_dir = Path::new(&self.path).join(".git");
|
||||
if !git_dir.exists() || !git_dir.is_dir() {
|
||||
return Err(GitError::NotAGitRepository(self.path.clone()));
|
||||
}
|
||||
|
||||
// Reset any local changes
|
||||
let reset_output = Command::new("git")
|
||||
.args(&["-C", &self.path, "reset", "--hard", "HEAD"])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if !reset_output.status.success() {
|
||||
let error = String::from_utf8_lossy(&reset_output.stderr);
|
||||
return Err(GitError::GitCommandFailed(format!("Git reset error: {}", error)));
|
||||
}
|
||||
|
||||
// Clean untracked files
|
||||
let clean_output = Command::new("git")
|
||||
.args(&["-C", &self.path, "clean", "-fd"])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if !clean_output.status.success() {
|
||||
let error = String::from_utf8_lossy(&clean_output.stderr);
|
||||
return Err(GitError::GitCommandFailed(format!("Git clean error: {}", error)));
|
||||
}
|
||||
|
||||
Ok(self.clone())
|
||||
}
|
||||
|
||||
/// Commits changes in the repository.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `message` - The commit message
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Self)` - The GitRepo object for method chaining
|
||||
/// * `Err(GitError)` - If the commit operation failed
|
||||
pub fn commit(&self, message: &str) -> Result<Self, GitError> {
|
||||
// Check if repository exists and is a git repository
|
||||
let git_dir = Path::new(&self.path).join(".git");
|
||||
if !git_dir.exists() || !git_dir.is_dir() {
|
||||
return Err(GitError::NotAGitRepository(self.path.clone()));
|
||||
}
|
||||
|
||||
// Check for local changes
|
||||
if !self.has_changes()? {
|
||||
return Ok(self.clone());
|
||||
}
|
||||
|
||||
// Add all changes
|
||||
let add_output = Command::new("git")
|
||||
.args(&["-C", &self.path, "add", "."])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if !add_output.status.success() {
|
||||
let error = String::from_utf8_lossy(&add_output.stderr);
|
||||
return Err(GitError::GitCommandFailed(format!("Git add error: {}", error)));
|
||||
}
|
||||
|
||||
// Commit the changes
|
||||
let commit_output = Command::new("git")
|
||||
.args(&["-C", &self.path, "commit", "-m", message])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if !commit_output.status.success() {
|
||||
let error = String::from_utf8_lossy(&commit_output.stderr);
|
||||
return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error)));
|
||||
}
|
||||
|
||||
Ok(self.clone())
|
||||
}
|
||||
|
||||
/// Pushes changes to the remote repository.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Self)` - The GitRepo object for method chaining
|
||||
/// * `Err(GitError)` - If the push operation failed
|
||||
pub fn push(&self) -> Result<Self, GitError> {
|
||||
// Check if repository exists and is a git repository
|
||||
let git_dir = Path::new(&self.path).join(".git");
|
||||
if !git_dir.exists() || !git_dir.is_dir() {
|
||||
return Err(GitError::NotAGitRepository(self.path.clone()));
|
||||
}
|
||||
|
||||
// Push the changes
|
||||
let push_output = Command::new("git")
|
||||
.args(&["-C", &self.path, "push"])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if push_output.status.success() {
|
||||
Ok(self.clone())
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&push_output.stderr);
|
||||
Err(GitError::GitCommandFailed(format!("Git push error: {}", error)))
|
||||
Ok(format!("Successfully updated repository at {}", actual_path))
|
||||
}
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
|
||||
}
|
||||
}
|
||||
|
||||
// Implement Clone for GitRepo to allow for method chaining
|
||||
impl Clone for GitRepo {
|
||||
fn clone(&self) -> Self {
|
||||
GitRepo {
|
||||
path: self.path.clone(),
|
||||
}
|
||||
/**
|
||||
* Force updates a git repository by discarding local changes and pulling the latest changes.
|
||||
*
|
||||
* This function will reset any uncommitted changes and clean untracked files before pulling.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(String)` - A success message indicating the repository was force-updated
|
||||
* * `Err(GitError)` - An error if the update failed
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* let result = git_update_force("my-project")?;
|
||||
* println!("{}", result); // "Successfully force-updated repository at /home/user/code/github.com/user/my-project"
|
||||
* ```
|
||||
*/
|
||||
pub fn git_update_force(repo_path: &str) -> Result<String, GitError> {
|
||||
// If repo_path may be a partial path, find the matching repository
|
||||
let repos = find_matching_repos(repo_path)?;
|
||||
|
||||
// Should only be one repository at this point
|
||||
let actual_path = &repos[0];
|
||||
|
||||
// Check if repository exists and is a git repository
|
||||
let git_dir = Path::new(actual_path).join(".git");
|
||||
if !git_dir.exists() || !git_dir.is_dir() {
|
||||
return Err(GitError::NotAGitRepository(actual_path.clone()));
|
||||
}
|
||||
|
||||
// Reset any local changes
|
||||
let reset_output = Command::new("git")
|
||||
.args(&["-C", actual_path, "reset", "--hard", "HEAD"])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if !reset_output.status.success() {
|
||||
let error = String::from_utf8_lossy(&reset_output.stderr);
|
||||
return Err(GitError::GitCommandFailed(format!("Git reset error: {}", error)));
|
||||
}
|
||||
|
||||
// Clean untracked files
|
||||
let clean_output = Command::new("git")
|
||||
.args(&["-C", actual_path, "clean", "-fd"])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if !clean_output.status.success() {
|
||||
let error = String::from_utf8_lossy(&clean_output.stderr);
|
||||
return Err(GitError::GitCommandFailed(format!("Git clean error: {}", error)));
|
||||
}
|
||||
|
||||
// Pull the latest changes
|
||||
let pull_output = Command::new("git")
|
||||
.args(&["-C", actual_path, "pull"])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if pull_output.status.success() {
|
||||
Ok(format!("Successfully force-updated repository at {}", actual_path))
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&pull_output.stderr);
|
||||
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits changes in a git repository and then updates it by pulling the latest changes.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
|
||||
* * `message` - The commit message
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(String)` - A success message indicating the repository was committed and updated
|
||||
* * `Err(GitError)` - An error if the operation failed
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* let result = git_update_commit("my-project", "Fix bug in login form")?;
|
||||
* println!("{}", result); // "Successfully committed and updated repository at /home/user/code/github.com/user/my-project"
|
||||
* ```
|
||||
*/
|
||||
pub fn git_update_commit(repo_path: &str, message: &str) -> Result<String, GitError> {
|
||||
// If repo_path may be a partial path, find the matching repository
|
||||
let repos = find_matching_repos(repo_path)?;
|
||||
|
||||
// Should only be one repository at this point
|
||||
let actual_path = &repos[0];
|
||||
|
||||
// Check if repository exists and is a git repository
|
||||
let git_dir = Path::new(actual_path).join(".git");
|
||||
if !git_dir.exists() || !git_dir.is_dir() {
|
||||
return Err(GitError::NotAGitRepository(actual_path.clone()));
|
||||
}
|
||||
|
||||
// Check for local changes
|
||||
if !has_git_changes(actual_path)? {
|
||||
return Ok(format!("No changes to commit in repository at {}", actual_path));
|
||||
}
|
||||
|
||||
// Add all changes
|
||||
let add_output = Command::new("git")
|
||||
.args(&["-C", actual_path, "add", "."])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if !add_output.status.success() {
|
||||
let error = String::from_utf8_lossy(&add_output.stderr);
|
||||
return Err(GitError::GitCommandFailed(format!("Git add error: {}", error)));
|
||||
}
|
||||
|
||||
// Commit the changes
|
||||
let commit_output = Command::new("git")
|
||||
.args(&["-C", actual_path, "commit", "-m", message])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if !commit_output.status.success() {
|
||||
let error = String::from_utf8_lossy(&commit_output.stderr);
|
||||
return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error)));
|
||||
}
|
||||
|
||||
// Pull the latest changes
|
||||
let pull_output = Command::new("git")
|
||||
.args(&["-C", actual_path, "pull"])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if pull_output.status.success() {
|
||||
Ok(format!("Successfully committed and updated repository at {}", actual_path))
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&pull_output.stderr);
|
||||
Err(GitError::GitCommandFailed(format!("Git pull error: {}", error)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits changes in a git repository and pushes them to the remote.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `repo_path` - The path to the git repository, or a partial path that uniquely identifies a repository
|
||||
* * `message` - The commit message
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(String)` - A success message indicating the repository was committed and pushed
|
||||
* * `Err(GitError)` - An error if the operation failed
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* let result = git_update_commit_push("my-project", "Add new feature")?;
|
||||
* println!("{}", result); // "Successfully committed and pushed repository at /home/user/code/github.com/user/my-project"
|
||||
* ```
|
||||
*/
|
||||
pub fn git_update_commit_push(repo_path: &str, message: &str) -> Result<String, GitError> {
|
||||
// If repo_path may be a partial path, find the matching repository
|
||||
let repos = find_matching_repos(repo_path)?;
|
||||
|
||||
// Should only be one repository at this point
|
||||
let actual_path = &repos[0];
|
||||
|
||||
// Check if repository exists and is a git repository
|
||||
let git_dir = Path::new(actual_path).join(".git");
|
||||
if !git_dir.exists() || !git_dir.is_dir() {
|
||||
return Err(GitError::NotAGitRepository(actual_path.clone()));
|
||||
}
|
||||
|
||||
// Check for local changes
|
||||
if !has_git_changes(actual_path)? {
|
||||
return Ok(format!("No changes to commit in repository at {}", actual_path));
|
||||
}
|
||||
|
||||
// Add all changes
|
||||
let add_output = Command::new("git")
|
||||
.args(&["-C", actual_path, "add", "."])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if !add_output.status.success() {
|
||||
let error = String::from_utf8_lossy(&add_output.stderr);
|
||||
return Err(GitError::GitCommandFailed(format!("Git add error: {}", error)));
|
||||
}
|
||||
|
||||
// Commit the changes
|
||||
let commit_output = Command::new("git")
|
||||
.args(&["-C", actual_path, "commit", "-m", message])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if !commit_output.status.success() {
|
||||
let error = String::from_utf8_lossy(&commit_output.stderr);
|
||||
return Err(GitError::GitCommandFailed(format!("Git commit error: {}", error)));
|
||||
}
|
||||
|
||||
// Push the changes
|
||||
let push_output = Command::new("git")
|
||||
.args(&["-C", actual_path, "push"])
|
||||
.output()
|
||||
.map_err(GitError::CommandExecutionError)?;
|
||||
|
||||
if push_output.status.success() {
|
||||
Ok(format!("Successfully committed and pushed repository at {}", actual_path))
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&push_output.stderr);
|
||||
Err(GitError::GitCommandFailed(format!("Git push error: {}", error)))
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ use redis::Cmd;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::redisclient;
|
||||
use crate::git::git::parse_git_url;
|
||||
|
||||
// Define a custom error type for GitExecutor operations
|
||||
#[derive(Debug)]
|
||||
@ -159,7 +160,7 @@ impl GitExecutor {
|
||||
// Get authentication configuration for a git URL
|
||||
fn get_auth_for_url(&self, url: &str) -> Option<&GitServerAuth> {
|
||||
if let Some(config) = &self.config {
|
||||
let (server, _, _) = crate::git::git::parse_git_url(url);
|
||||
let (server, _, _) = parse_git_url(url);
|
||||
if !server.is_empty() {
|
||||
return config.auth.get(&server);
|
||||
}
|
||||
|
@ -1,212 +0,0 @@
|
||||
# Git Interface Redesign Plan
|
||||
|
||||
## Current Understanding
|
||||
|
||||
The current git interface consists of standalone functions like `git_clone`, `git_list`, `git_update`, etc. We want to replace this with an object-oriented interface using a builder pattern that allows for method chaining.
|
||||
|
||||
## New Interface Design
|
||||
|
||||
### Core Components
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class GitTree {
|
||||
+String base_path
|
||||
+new(base_path: &str) Result<GitTree, GitError>
|
||||
+list() Result<Vec<String>, GitError>
|
||||
+find(pattern: &str) Result<Vec<String>, GitError>
|
||||
+get(path_pattern: &str) Result<Vec<GitRepo>, GitError>
|
||||
}
|
||||
|
||||
class GitRepo {
|
||||
+String path
|
||||
+pull() Result<GitRepo, GitError>
|
||||
+reset() Result<GitRepo, GitError>
|
||||
+push() Result<GitRepo, GitError>
|
||||
+commit(message: &str) Result<GitRepo, GitError>
|
||||
+has_changes() Result<bool, GitError>
|
||||
}
|
||||
|
||||
GitTree --> GitRepo : creates
|
||||
```
|
||||
|
||||
### Implementation Details
|
||||
|
||||
1. **GitTree Class**:
|
||||
- Constructor takes a base path parameter that specifies where all git repositories will be located
|
||||
- Methods for listing and finding repositories
|
||||
- A `get()` method that returns one or more GitRepo objects based on a path pattern
|
||||
- The `get()` method can also accept a URL (git or http format) and will clone the repository if it doesn't exist
|
||||
|
||||
2. **GitRepo Class**:
|
||||
- Represents a single git repository
|
||||
- Methods for common git operations: pull, reset, push, commit
|
||||
- Each method returns a Result containing either the GitRepo object (for chaining) or an error
|
||||
- If an operation fails, subsequent operations in the chain are skipped
|
||||
|
||||
3. **Error Handling**:
|
||||
- Each method returns a Result type for immediate error handling
|
||||
- Errors are propagated up the call chain
|
||||
- The existing GitError enum will be reused
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### 1. Create the GitTree and GitRepo Structs in git.rs
|
||||
|
||||
```rust
|
||||
pub struct GitTree {
|
||||
base_path: String,
|
||||
}
|
||||
|
||||
pub struct GitRepo {
|
||||
path: String,
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Implement the GitTree Methods
|
||||
|
||||
```rust
|
||||
impl GitTree {
|
||||
pub fn new(base_path: &str) -> Result<Self, GitError> {
|
||||
// Validate the base path
|
||||
// Create the directory if it doesn't exist
|
||||
Ok(GitTree {
|
||||
base_path: base_path.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list(&self) -> Result<Vec<String>, GitError> {
|
||||
// List all git repositories under the base path
|
||||
}
|
||||
|
||||
pub fn find(&self, pattern: &str) -> Result<Vec<String>, GitError> {
|
||||
// Find repositories matching the pattern
|
||||
}
|
||||
|
||||
pub fn get(&self, path_pattern: &str) -> Result<Vec<GitRepo>, GitError> {
|
||||
// Find repositories matching the pattern
|
||||
// Return GitRepo objects for each match
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Implement the GitRepo Methods
|
||||
|
||||
```rust
|
||||
impl GitRepo {
|
||||
pub fn pull(&self) -> Result<Self, GitError> {
|
||||
// Pull the latest changes
|
||||
// Return self for chaining or an error
|
||||
}
|
||||
|
||||
pub fn reset(&self) -> Result<Self, GitError> {
|
||||
// Reset any local changes
|
||||
// Return self for chaining or an error
|
||||
}
|
||||
|
||||
pub fn push(&self) -> Result<Self, GitError> {
|
||||
// Push changes to the remote
|
||||
// Return self for chaining or an error
|
||||
}
|
||||
|
||||
pub fn commit(&self, message: &str) -> Result<Self, GitError> {
|
||||
// Commit changes with the given message
|
||||
// Return self for chaining or an error
|
||||
}
|
||||
|
||||
pub fn has_changes(&self) -> Result<bool, GitError> {
|
||||
// Check if the repository has uncommitted changes
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Update the Rhai Wrappers in rhai/git.rs
|
||||
|
||||
```rust
|
||||
// Register the GitTree and GitRepo types with Rhai
|
||||
pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register the GitTree type
|
||||
engine.register_type::<GitTree>();
|
||||
engine.register_fn("new", git_tree_new);
|
||||
|
||||
// Register GitTree methods
|
||||
engine.register_fn("list", git_tree_list);
|
||||
engine.register_fn("find", git_tree_find);
|
||||
engine.register_fn("get", git_tree_get);
|
||||
|
||||
// Register GitRepo methods
|
||||
engine.register_type::<GitRepo>();
|
||||
engine.register_fn("pull", git_repo_pull);
|
||||
engine.register_fn("reset", git_repo_reset);
|
||||
engine.register_fn("push", git_repo_push);
|
||||
engine.register_fn("commit", git_repo_commit);
|
||||
engine.register_fn("has_changes", git_repo_has_changes);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Update Tests and Examples
|
||||
|
||||
- Update the test files to use the new interface
|
||||
- Create new examples demonstrating the builder pattern and method chaining
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Example 1: Basic Repository Operations
|
||||
|
||||
```rhai
|
||||
// Create a new GitTree object
|
||||
let git_tree = new("/home/user/code");
|
||||
|
||||
// List all repositories
|
||||
let repos = git_tree.list();
|
||||
print(`Found ${repos.len()} repositories`);
|
||||
|
||||
// Find repositories matching a pattern
|
||||
let matching = git_tree.find("my-project*");
|
||||
print(`Found ${matching.len()} matching repositories`);
|
||||
|
||||
// Get a repository and perform operations
|
||||
let repo = git_tree.get("my-project")[0];
|
||||
let result = repo.pull().reset().commit("Update files").push();
|
||||
```
|
||||
|
||||
### Example 2: Working with Multiple Repositories
|
||||
|
||||
```rhai
|
||||
// Create a new GitTree object
|
||||
let git_tree = new("/home/user/code");
|
||||
|
||||
// Get all repositories matching a pattern
|
||||
let repos = git_tree.get("project*");
|
||||
print(`Found ${repos.len()} matching repositories`);
|
||||
|
||||
// Perform operations on all repositories
|
||||
for repo in repos {
|
||||
let result = repo.pull();
|
||||
if result.is_ok() {
|
||||
print(`Successfully pulled ${repo.path}`);
|
||||
} else {
|
||||
print(`Failed to pull ${repo.path}: ${result.error}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Cloning a Repository
|
||||
|
||||
```rhai
|
||||
// Create a new GitTree object
|
||||
let git_tree = new("/home/user/code");
|
||||
|
||||
// Clone a repository by URL
|
||||
let repos = git_tree.get("https://github.com/username/repo.git");
|
||||
let repo = repos[0];
|
||||
print(`Repository cloned to: ${repo.path}`);
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
1. Implement the new interface in git.rs and rhai/git.rs
|
||||
2. Update all tests and examples to use the new interface
|
||||
3. Remove the old standalone functions
|
@ -42,8 +42,6 @@ pub mod os;
|
||||
pub mod redisclient;
|
||||
pub mod text;
|
||||
pub mod virt;
|
||||
pub mod rhai;
|
||||
pub mod cmd;
|
||||
|
||||
// Version information
|
||||
/// Returns the version of the SAL library
|
||||
|
@ -58,167 +58,34 @@ impl Error for DownloadError {
|
||||
|
||||
/**
|
||||
* Download a file from URL to destination using the curl command.
|
||||
* This function is primarily intended for downloading archives that will be extracted
|
||||
* to a directory.
|
||||
*
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
*
|
||||
* * `url` - The URL to download from
|
||||
* * `dest` - The destination directory where the file will be saved or extracted
|
||||
* * `dest` - The destination path where the file will be saved
|
||||
* * `min_size_kb` - Minimum required file size in KB (0 for no minimum)
|
||||
*
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
*
|
||||
* * `Ok(String)` - The path where the file was saved or extracted
|
||||
* * `Err(DownloadError)` - An error if the download failed
|
||||
*
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
*
|
||||
* ```
|
||||
* // Download a file with no minimum size requirement
|
||||
* let path = download("https://example.com/file.txt", "/tmp/", 0)?;
|
||||
*
|
||||
* let path = download("https://example.com/file.txt", "/tmp/file.txt", 0)?;
|
||||
*
|
||||
* // Download a file with minimum size requirement of 100KB
|
||||
* let path = download("https://example.com/file.zip", "/tmp/", 100)?;
|
||||
* let path = download("https://example.com/file.zip", "/tmp/file.zip", 100)?;
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* # Notes
|
||||
*
|
||||
*
|
||||
* If the URL ends with .tar.gz, .tgz, .tar, or .zip, the file will be automatically
|
||||
* extracted to the destination directory.
|
||||
*/
|
||||
pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, DownloadError> {
|
||||
// Create parent directories if they don't exist
|
||||
let dest_path = Path::new(dest);
|
||||
fs::create_dir_all(dest_path).map_err(DownloadError::CreateDirectoryFailed)?;
|
||||
|
||||
// Extract filename from URL
|
||||
let filename = match url.split('/').last() {
|
||||
Some(name) => name,
|
||||
None => return Err(DownloadError::InvalidUrl("cannot extract filename".to_string()))
|
||||
};
|
||||
|
||||
// Create a full path for the downloaded file
|
||||
let file_path = format!("{}/{}", dest.trim_end_matches('/'), filename);
|
||||
|
||||
// Create a temporary path for downloading
|
||||
let temp_path = format!("{}.download", file_path);
|
||||
|
||||
// Use curl to download the file with progress bar
|
||||
println!("Downloading {} to {}", url, file_path);
|
||||
let output = Command::new("curl")
|
||||
.args(&["--progress-bar", "--location", "--fail", "--output", &temp_path, url])
|
||||
.status()
|
||||
.map_err(DownloadError::CurlExecutionFailed)?;
|
||||
|
||||
if !output.success() {
|
||||
return Err(DownloadError::DownloadFailed(url.to_string()));
|
||||
}
|
||||
|
||||
// Show file size after download
|
||||
match fs::metadata(&temp_path) {
|
||||
Ok(metadata) => {
|
||||
let size_bytes = metadata.len();
|
||||
let size_kb = size_bytes / 1024;
|
||||
let size_mb = size_kb / 1024;
|
||||
if size_mb > 1 {
|
||||
println!("Download complete! File size: {:.2} MB", size_bytes as f64 / (1024.0 * 1024.0));
|
||||
} else {
|
||||
println!("Download complete! File size: {:.2} KB", size_bytes as f64 / 1024.0);
|
||||
}
|
||||
},
|
||||
Err(_) => println!("Download complete!"),
|
||||
}
|
||||
|
||||
// Check file size if minimum size is specified
|
||||
if min_size_kb > 0 {
|
||||
let metadata = fs::metadata(&temp_path).map_err(DownloadError::FileMetadataError)?;
|
||||
let size_kb = metadata.len() as i64 / 1024;
|
||||
if size_kb < min_size_kb {
|
||||
fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?;
|
||||
return Err(DownloadError::FileTooSmall(size_kb, min_size_kb));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a compressed file that needs extraction
|
||||
let lower_url = url.to_lowercase();
|
||||
let is_archive = lower_url.ends_with(".tar.gz") ||
|
||||
lower_url.ends_with(".tgz") ||
|
||||
lower_url.ends_with(".tar") ||
|
||||
lower_url.ends_with(".zip");
|
||||
|
||||
if is_archive {
|
||||
// Extract the file using the appropriate command with progress indication
|
||||
println!("Extracting {} to {}", temp_path, dest);
|
||||
let output = if lower_url.ends_with(".zip") {
|
||||
Command::new("unzip")
|
||||
.args(&["-o", &temp_path, "-d", dest]) // Removed -q for verbosity
|
||||
.status()
|
||||
} else if lower_url.ends_with(".tar.gz") || lower_url.ends_with(".tgz") {
|
||||
Command::new("tar")
|
||||
.args(&["-xzvf", &temp_path, "-C", dest]) // Added v for verbosity
|
||||
.status()
|
||||
} else {
|
||||
Command::new("tar")
|
||||
.args(&["-xvf", &temp_path, "-C", dest]) // Added v for verbosity
|
||||
.status()
|
||||
};
|
||||
|
||||
match output {
|
||||
Ok(status) => {
|
||||
if !status.success() {
|
||||
return Err(DownloadError::ExtractionFailed("Error extracting archive".to_string()));
|
||||
}
|
||||
},
|
||||
Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
|
||||
}
|
||||
|
||||
// Show number of extracted files
|
||||
match fs::read_dir(dest) {
|
||||
Ok(entries) => {
|
||||
let count = entries.count();
|
||||
println!("Extraction complete! Extracted {} files/directories", count);
|
||||
},
|
||||
Err(_) => println!("Extraction complete!"),
|
||||
}
|
||||
|
||||
// Remove the temporary file
|
||||
fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?;
|
||||
|
||||
Ok(dest.to_string())
|
||||
} else {
|
||||
// Just rename the temporary file to the final destination
|
||||
fs::rename(&temp_path, &file_path).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
|
||||
|
||||
Ok(file_path)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file from URL to a specific file destination using the curl command.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `url` - The URL to download from
|
||||
* * `dest` - The destination file path where the file will be saved
|
||||
* * `min_size_kb` - Minimum required file size in KB (0 for no minimum)
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(String)` - The path where the file was saved
|
||||
* * `Err(DownloadError)` - An error if the download failed
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* // Download a file with no minimum size requirement
|
||||
* let path = download_file("https://example.com/file.txt", "/tmp/file.txt", 0)?;
|
||||
*
|
||||
* // Download a file with minimum size requirement of 100KB
|
||||
* let path = download_file("https://example.com/file.zip", "/tmp/file.zip", 100)?;
|
||||
* ```
|
||||
*/
|
||||
pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String, DownloadError> {
|
||||
// Create parent directories if they don't exist
|
||||
let dest_path = Path::new(dest);
|
||||
if let Some(parent) = dest_path.parent() {
|
||||
@ -264,73 +131,61 @@ pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String,
|
||||
}
|
||||
}
|
||||
|
||||
// Rename the temporary file to the final destination
|
||||
fs::rename(&temp_path, dest).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
|
||||
// Check if it's a compressed file that needs extraction
|
||||
let lower_url = url.to_lowercase();
|
||||
let is_archive = lower_url.ends_with(".tar.gz") ||
|
||||
lower_url.ends_with(".tgz") ||
|
||||
lower_url.ends_with(".tar") ||
|
||||
lower_url.ends_with(".zip");
|
||||
|
||||
Ok(dest.to_string())
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a file executable (equivalent to chmod +x).
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `path` - The path to the file to make executable
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(String)` - A success message including the path
|
||||
* * `Err(DownloadError)` - An error if the operation failed
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* // Make a file executable
|
||||
* chmod_exec("/path/to/file")?;
|
||||
* ```
|
||||
*/
|
||||
pub fn chmod_exec(path: &str) -> Result<String, DownloadError> {
|
||||
let path_obj = Path::new(path);
|
||||
|
||||
// Check if the path exists and is a file
|
||||
if !path_obj.exists() {
|
||||
return Err(DownloadError::NotAFile(format!("Path does not exist: {}", path)));
|
||||
if is_archive {
|
||||
// Create the destination directory
|
||||
fs::create_dir_all(dest).map_err(DownloadError::CreateDirectoryFailed)?;
|
||||
|
||||
// Extract the file using the appropriate command with progress indication
|
||||
println!("Extracting {} to {}", temp_path, dest);
|
||||
let output = if lower_url.ends_with(".zip") {
|
||||
Command::new("unzip")
|
||||
.args(&["-o", &temp_path, "-d", dest]) // Removed -q for verbosity
|
||||
.status()
|
||||
} else if lower_url.ends_with(".tar.gz") || lower_url.ends_with(".tgz") {
|
||||
Command::new("tar")
|
||||
.args(&["-xzvf", &temp_path, "-C", dest]) // Added v for verbosity
|
||||
.status()
|
||||
} else {
|
||||
Command::new("tar")
|
||||
.args(&["-xvf", &temp_path, "-C", dest]) // Added v for verbosity
|
||||
.status()
|
||||
};
|
||||
|
||||
match output {
|
||||
Ok(status) => {
|
||||
if !status.success() {
|
||||
return Err(DownloadError::ExtractionFailed("Error extracting archive".to_string()));
|
||||
}
|
||||
},
|
||||
Err(e) => return Err(DownloadError::CommandExecutionFailed(e)),
|
||||
}
|
||||
|
||||
// Show number of extracted files
|
||||
match fs::read_dir(dest) {
|
||||
Ok(entries) => {
|
||||
let count = entries.count();
|
||||
println!("Extraction complete! Extracted {} files/directories", count);
|
||||
},
|
||||
Err(_) => println!("Extraction complete!"),
|
||||
}
|
||||
|
||||
// Remove the temporary file
|
||||
fs::remove_file(&temp_path).map_err(DownloadError::RemoveFileFailed)?;
|
||||
|
||||
Ok(dest.to_string())
|
||||
} else {
|
||||
// Just rename the temporary file to the final destination
|
||||
fs::rename(&temp_path, dest).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
|
||||
|
||||
Ok(dest.to_string())
|
||||
}
|
||||
|
||||
if !path_obj.is_file() {
|
||||
return Err(DownloadError::NotAFile(format!("Path is not a file: {}", path)));
|
||||
}
|
||||
|
||||
// Get current permissions
|
||||
let metadata = fs::metadata(path).map_err(DownloadError::FileMetadataError)?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
// Set executable bit for user, group, and others
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mode = permissions.mode();
|
||||
// Add executable bit for user, group, and others (equivalent to +x)
|
||||
let new_mode = mode | 0o111;
|
||||
permissions.set_mode(new_mode);
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
// On non-Unix platforms, we can't set executable bit directly
|
||||
// Just return success with a warning
|
||||
return Ok(format!("Made {} executable (note: non-Unix platform, may not be fully supported)", path));
|
||||
}
|
||||
|
||||
// Apply the new permissions
|
||||
fs::set_permissions(path, permissions).map_err(|e|
|
||||
DownloadError::CommandExecutionFailed(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Failed to set executable permissions: {}", e)
|
||||
))
|
||||
)?;
|
||||
|
||||
Ok(format!("Made {} executable", path))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -368,24 +223,11 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadE
|
||||
// Create a proper destination path
|
||||
let dest_path = format!("/tmp/{}", filename);
|
||||
|
||||
// Check if it's a compressed file that needs extraction
|
||||
let lower_url = url.to_lowercase();
|
||||
let is_archive = lower_url.ends_with(".tar.gz") ||
|
||||
lower_url.ends_with(".tgz") ||
|
||||
lower_url.ends_with(".tar") ||
|
||||
lower_url.ends_with(".zip");
|
||||
|
||||
let download_result = if is_archive {
|
||||
// For archives, use the directory-based download function
|
||||
download(url, "/tmp", min_size_kb)?
|
||||
} else {
|
||||
// For regular files, use the file-specific download function
|
||||
download_file(url, &dest_path, min_size_kb)?
|
||||
};
|
||||
let download_result = download(url, &dest_path, min_size_kb)?;
|
||||
|
||||
// Check if the downloaded result is a file
|
||||
let path = Path::new(&dest_path);
|
||||
if !path.is_file() {
|
||||
if !path.is_file() {
|
||||
return Ok(download_result); // Not a file, might be an extracted directory
|
||||
}
|
||||
|
||||
|
399
src/os/fs.rs
399
src/os/fs.rs
@ -14,17 +14,12 @@ pub enum FsError {
|
||||
CopyFailed(io::Error),
|
||||
DeleteFailed(io::Error),
|
||||
CommandFailed(String),
|
||||
CommandNotFound(String),
|
||||
CommandExecutionError(io::Error),
|
||||
InvalidGlobPattern(glob::PatternError),
|
||||
NotADirectory(String),
|
||||
NotAFile(String),
|
||||
UnknownFileType(String),
|
||||
MetadataError(io::Error),
|
||||
ChangeDirFailed(io::Error),
|
||||
ReadFailed(io::Error),
|
||||
WriteFailed(io::Error),
|
||||
AppendFailed(io::Error),
|
||||
}
|
||||
|
||||
// Implement Display for FsError
|
||||
@ -37,17 +32,12 @@ impl fmt::Display for FsError {
|
||||
FsError::CopyFailed(e) => write!(f, "Failed to copy file: {}", e),
|
||||
FsError::DeleteFailed(e) => write!(f, "Failed to delete: {}", e),
|
||||
FsError::CommandFailed(e) => write!(f, "{}", e),
|
||||
FsError::CommandNotFound(e) => write!(f, "Command not found: {}", e),
|
||||
FsError::CommandExecutionError(e) => write!(f, "Failed to execute command: {}", e),
|
||||
FsError::InvalidGlobPattern(e) => write!(f, "Invalid glob pattern: {}", e),
|
||||
FsError::NotADirectory(path) => write!(f, "Path '{}' exists but is not a directory", path),
|
||||
FsError::NotAFile(path) => write!(f, "Path '{}' is not a regular file", path),
|
||||
FsError::UnknownFileType(path) => write!(f, "Unknown file type at '{}'", path),
|
||||
FsError::MetadataError(e) => write!(f, "Failed to get file metadata: {}", e),
|
||||
FsError::ChangeDirFailed(e) => write!(f, "Failed to change directory: {}", e),
|
||||
FsError::ReadFailed(e) => write!(f, "Failed to read file: {}", e),
|
||||
FsError::WriteFailed(e) => write!(f, "Failed to write to file: {}", e),
|
||||
FsError::AppendFailed(e) => write!(f, "Failed to append to file: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -62,10 +52,6 @@ impl Error for FsError {
|
||||
FsError::CommandExecutionError(e) => Some(e),
|
||||
FsError::InvalidGlobPattern(e) => Some(e),
|
||||
FsError::MetadataError(e) => Some(e),
|
||||
FsError::ChangeDirFailed(e) => Some(e),
|
||||
FsError::ReadFailed(e) => Some(e),
|
||||
FsError::WriteFailed(e) => Some(e),
|
||||
FsError::AppendFailed(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@ -124,16 +110,7 @@ pub fn copy(src: &str, dest: &str) -> Result<String, FsError> {
|
||||
for path in paths {
|
||||
let target_path = if dest_is_dir {
|
||||
// If destination is a directory, copy the file into it
|
||||
if path.is_file() {
|
||||
// For files, just use the filename
|
||||
dest_path.join(path.file_name().unwrap_or_default())
|
||||
} else if path.is_dir() {
|
||||
// For directories, use the directory name
|
||||
dest_path.join(path.file_name().unwrap_or_default())
|
||||
} else {
|
||||
// Fallback
|
||||
dest_path.join(path.file_name().unwrap_or_default())
|
||||
}
|
||||
dest_path.join(path.file_name().unwrap_or_default())
|
||||
} else {
|
||||
// Otherwise use the destination as is (only makes sense for single file)
|
||||
dest_path.to_path_buf()
|
||||
@ -195,17 +172,9 @@ pub fn copy(src: &str, dest: &str) -> Result<String, FsError> {
|
||||
|
||||
// Copy based on source type
|
||||
if src_path.is_file() {
|
||||
// If destination is a directory, copy the file into it
|
||||
if dest_path.exists() && dest_path.is_dir() {
|
||||
let file_name = src_path.file_name().unwrap_or_default();
|
||||
let new_dest_path = dest_path.join(file_name);
|
||||
fs::copy(src_path, new_dest_path).map_err(FsError::CopyFailed)?;
|
||||
Ok(format!("Successfully copied file '{}' to '{}/{}'", src, dest, file_name.to_string_lossy()))
|
||||
} else {
|
||||
// Otherwise copy file to the specified destination
|
||||
fs::copy(src_path, dest_path).map_err(FsError::CopyFailed)?;
|
||||
Ok(format!("Successfully copied file '{}' to '{}'", src, dest))
|
||||
}
|
||||
// Copy file
|
||||
fs::copy(src_path, dest_path).map_err(FsError::CopyFailed)?;
|
||||
Ok(format!("Successfully copied file '{}' to '{}'", src, dest))
|
||||
} else if src_path.is_dir() {
|
||||
// For directories, use platform-specific command
|
||||
#[cfg(target_os = "windows")]
|
||||
@ -609,363 +578,3 @@ pub fn rsync(src: &str, dest: &str) -> Result<String, FsError> {
|
||||
Err(e) => Err(FsError::CommandExecutionError(e)),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the current working directory.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `path` - The path to change to
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(String)` - A success message indicating the directory was changed
|
||||
* * `Err(FsError)` - An error if the directory change failed
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* let result = chdir("/path/to/directory")?;
|
||||
* println!("{}", result);
|
||||
* ```
|
||||
*/
|
||||
pub fn chdir(path: &str) -> Result<String, FsError> {
|
||||
let path_obj = Path::new(path);
|
||||
|
||||
// Check if directory exists
|
||||
if !path_obj.exists() {
|
||||
return Err(FsError::DirectoryNotFound(path.to_string()));
|
||||
}
|
||||
|
||||
// Check if it's a directory
|
||||
if !path_obj.is_dir() {
|
||||
return Err(FsError::NotADirectory(path.to_string()));
|
||||
}
|
||||
|
||||
// Change directory
|
||||
std::env::set_current_dir(path_obj).map_err(FsError::ChangeDirFailed)?;
|
||||
|
||||
Ok(format!("Successfully changed directory to '{}'", path))
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the contents of a file.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `path` - The path of the file to read
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(String)` - The contents of the file
|
||||
* * `Err(FsError)` - An error if the file doesn't exist or can't be read
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* let content = file_read("file.txt")?;
|
||||
* println!("File content: {}", content);
|
||||
* ```
|
||||
*/
|
||||
pub fn file_read(path: &str) -> Result<String, FsError> {
|
||||
let path_obj = Path::new(path);
|
||||
|
||||
// Check if file exists
|
||||
if !path_obj.exists() {
|
||||
return Err(FsError::FileNotFound(path.to_string()));
|
||||
}
|
||||
|
||||
// Check if it's a regular file
|
||||
if !path_obj.is_file() {
|
||||
return Err(FsError::NotAFile(path.to_string()));
|
||||
}
|
||||
|
||||
// Read file content
|
||||
fs::read_to_string(path_obj).map_err(FsError::ReadFailed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write content to a file (creates the file if it doesn't exist, overwrites if it does).
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `path` - The path of the file to write to
|
||||
* * `content` - The content to write to the file
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(String)` - A success message indicating the file was written
|
||||
* * `Err(FsError)` - An error if the file can't be written
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* let result = file_write("file.txt", "Hello, world!")?;
|
||||
* println!("{}", result);
|
||||
* ```
|
||||
*/
|
||||
pub fn file_write(path: &str, content: &str) -> Result<String, FsError> {
|
||||
let path_obj = Path::new(path);
|
||||
|
||||
// Create parent directories if they don't exist
|
||||
if let Some(parent) = path_obj.parent() {
|
||||
fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
|
||||
}
|
||||
|
||||
// Write content to file
|
||||
fs::write(path_obj, content).map_err(FsError::WriteFailed)?;
|
||||
|
||||
Ok(format!("Successfully wrote to file '{}'", path))
|
||||
}
|
||||
|
||||
/**
|
||||
* Append content to a file (creates the file if it doesn't exist).
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `path` - The path of the file to append to
|
||||
* * `content` - The content to append to the file
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(String)` - A success message indicating the content was appended
|
||||
* * `Err(FsError)` - An error if the file can't be appended to
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* let result = file_write_append("log.txt", "New log entry\n")?;
|
||||
* println!("{}", result);
|
||||
* ```
|
||||
*/
|
||||
pub fn file_write_append(path: &str, content: &str) -> Result<String, FsError> {
|
||||
let path_obj = Path::new(path);
|
||||
|
||||
// Create parent directories if they don't exist
|
||||
if let Some(parent) = path_obj.parent() {
|
||||
fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
|
||||
}
|
||||
|
||||
// Open file in append mode (or create if it doesn't exist)
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(path_obj)
|
||||
.map_err(FsError::AppendFailed)?;
|
||||
|
||||
// Append content to file
|
||||
use std::io::Write;
|
||||
file.write_all(content.as_bytes()).map_err(FsError::AppendFailed)?;
|
||||
|
||||
Ok(format!("Successfully appended to file '{}'", path))
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a file or directory from source to destination.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `src` - The source path
|
||||
* * `dest` - The destination path
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(String)` - A success message indicating what was moved
|
||||
* * `Err(FsError)` - An error if the move operation failed
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* // Move a file
|
||||
* let result = mv("file.txt", "new_location/file.txt")?;
|
||||
*
|
||||
* // Move a directory
|
||||
* let result = mv("src_dir", "dest_dir")?;
|
||||
*
|
||||
* // Rename a file
|
||||
* let result = mv("old_name.txt", "new_name.txt")?;
|
||||
* ```
|
||||
*/
|
||||
pub fn mv(src: &str, dest: &str) -> Result<String, FsError> {
|
||||
let src_path = Path::new(src);
|
||||
let dest_path = Path::new(dest);
|
||||
|
||||
// Check if source exists
|
||||
if !src_path.exists() {
|
||||
return Err(FsError::FileNotFound(src.to_string()));
|
||||
}
|
||||
|
||||
// Create parent directories if they don't exist
|
||||
if let Some(parent) = dest_path.parent() {
|
||||
fs::create_dir_all(parent).map_err(FsError::CreateDirectoryFailed)?;
|
||||
}
|
||||
|
||||
// Handle the case where destination is a directory and exists
|
||||
let final_dest_path = if dest_path.exists() && dest_path.is_dir() && src_path.is_file() {
|
||||
// If destination is a directory and source is a file, move the file into the directory
|
||||
let file_name = src_path.file_name().unwrap_or_default();
|
||||
dest_path.join(file_name)
|
||||
} else {
|
||||
dest_path.to_path_buf()
|
||||
};
|
||||
|
||||
// Clone the path for use in the error handler
|
||||
let final_dest_path_clone = final_dest_path.clone();
|
||||
|
||||
// Perform the move operation
|
||||
fs::rename(src_path, &final_dest_path).map_err(|e| {
|
||||
// If rename fails (possibly due to cross-device link), try copy and delete
|
||||
if e.kind() == std::io::ErrorKind::CrossesDevices {
|
||||
// For cross-device moves, we need to copy and then delete
|
||||
if src_path.is_file() {
|
||||
// Copy file
|
||||
match fs::copy(src_path, &final_dest_path_clone) {
|
||||
Ok(_) => {
|
||||
// Delete source after successful copy
|
||||
if let Err(del_err) = fs::remove_file(src_path) {
|
||||
return FsError::DeleteFailed(del_err);
|
||||
}
|
||||
return FsError::CommandFailed("".to_string()); // This is a hack to trigger the success message
|
||||
},
|
||||
Err(copy_err) => return FsError::CopyFailed(copy_err),
|
||||
}
|
||||
} else if src_path.is_dir() {
|
||||
// For directories, use platform-specific command
|
||||
#[cfg(target_os = "windows")]
|
||||
let output = Command::new("xcopy")
|
||||
.args(&["/E", "/I", "/H", "/Y", src, dest])
|
||||
.status();
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let output = Command::new("cp")
|
||||
.args(&["-R", src, dest])
|
||||
.status();
|
||||
|
||||
match output {
|
||||
Ok(status) => {
|
||||
if status.success() {
|
||||
// Delete source after successful copy
|
||||
if let Err(del_err) = fs::remove_dir_all(src_path) {
|
||||
return FsError::DeleteFailed(del_err);
|
||||
}
|
||||
return FsError::CommandFailed("".to_string()); // This is a hack to trigger the success message
|
||||
} else {
|
||||
return FsError::CommandFailed("Failed to copy directory for move operation".to_string());
|
||||
}
|
||||
},
|
||||
Err(cmd_err) => return FsError::CommandExecutionError(cmd_err),
|
||||
}
|
||||
}
|
||||
}
|
||||
FsError::CommandFailed(format!("Failed to move '{}' to '{}': {}", src, dest, e))
|
||||
})?;
|
||||
|
||||
// If we get here, either the rename was successful or our copy-delete hack worked
|
||||
if src_path.is_file() {
|
||||
Ok(format!("Successfully moved file '{}' to '{}'", src, dest))
|
||||
} else {
|
||||
Ok(format!("Successfully moved directory '{}' to '{}'", src, dest))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a command exists in the system PATH.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `command` - The command to check
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `String` - Empty string if the command doesn't exist, path to the command if it does
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* let cmd_path = which("ls");
|
||||
* if cmd_path != "" {
|
||||
* println!("ls is available at: {}", cmd_path);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
pub fn which(command: &str) -> String {
|
||||
// Use the appropriate command based on the platform
|
||||
#[cfg(target_os = "windows")]
|
||||
let output = Command::new("where")
|
||||
.arg(command)
|
||||
.output();
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let output = Command::new("which")
|
||||
.arg(command)
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(out) => {
|
||||
if out.status.success() {
|
||||
let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
path
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
},
|
||||
Err(_) => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that one or more commands exist in the system PATH.
|
||||
* If any command doesn't exist, an error is thrown.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `commands` - The command(s) to check, comma-separated for multiple commands
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(String)` - A success message indicating all commands exist
|
||||
* * `Err(FsError)` - An error if any command doesn't exist
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* // Check if a single command exists
|
||||
* let result = cmd_ensure_exists("nerdctl")?;
|
||||
*
|
||||
* // Check if multiple commands exist
|
||||
* let result = cmd_ensure_exists("nerdctl,docker,containerd")?;
|
||||
* ```
|
||||
*/
|
||||
pub fn cmd_ensure_exists(commands: &str) -> Result<String, FsError> {
|
||||
// Split the input by commas to handle multiple commands
|
||||
let command_list: Vec<&str> = commands.split(',')
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
|
||||
if command_list.is_empty() {
|
||||
return Err(FsError::CommandFailed("No commands specified to check".to_string()));
|
||||
}
|
||||
|
||||
let mut missing_commands = Vec::new();
|
||||
|
||||
// Check each command
|
||||
for cmd in &command_list {
|
||||
let cmd_path = which(cmd);
|
||||
if cmd_path.is_empty() {
|
||||
missing_commands.push(cmd.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// If any commands are missing, return an error
|
||||
if !missing_commands.is_empty() {
|
||||
return Err(FsError::CommandNotFound(missing_commands.join(", ")));
|
||||
}
|
||||
|
||||
// All commands exist
|
||||
if command_list.len() == 1 {
|
||||
Ok(format!("Command '{}' exists", command_list[0]))
|
||||
} else {
|
||||
Ok(format!("All commands exist: {}", command_list.join(", ")))
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
mod fs;
|
||||
mod download;
|
||||
pub mod package;
|
||||
|
||||
pub use fs::*;
|
||||
pub use download::*;
|
||||
pub use package::*;
|
||||
pub use download::*;
|
@ -1,903 +0,0 @@
|
||||
use std::process::Command;
|
||||
use crate::process::CommandResult;
|
||||
|
||||
/// Error type for package management operations
|
||||
#[derive(Debug)]
|
||||
pub enum PackageError {
|
||||
/// Command failed with error message
|
||||
CommandFailed(String),
|
||||
/// Command execution failed with IO error
|
||||
CommandExecutionFailed(std::io::Error),
|
||||
/// Unsupported platform
|
||||
UnsupportedPlatform(String),
|
||||
/// Other error
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PackageError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PackageError::CommandFailed(msg) => write!(f, "Command failed: {}", msg),
|
||||
PackageError::CommandExecutionFailed(e) => write!(f, "Command execution failed: {}", e),
|
||||
PackageError::UnsupportedPlatform(msg) => write!(f, "Unsupported platform: {}", msg),
|
||||
PackageError::Other(msg) => write!(f, "Error: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for PackageError {}
|
||||
|
||||
/// Platform enum for detecting the current operating system
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Platform {
|
||||
/// Ubuntu Linux
|
||||
Ubuntu,
|
||||
/// macOS
|
||||
MacOS,
|
||||
/// Unknown platform
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Platform {
|
||||
/// Detect the current platform
|
||||
pub fn detect() -> Self {
|
||||
// Check for macOS
|
||||
if std::path::Path::new("/usr/bin/sw_vers").exists() {
|
||||
return Platform::MacOS;
|
||||
}
|
||||
|
||||
// Check for Ubuntu
|
||||
if std::path::Path::new("/etc/lsb-release").exists() {
|
||||
// Read the file to confirm it's Ubuntu
|
||||
if let Ok(content) = std::fs::read_to_string("/etc/lsb-release") {
|
||||
if content.contains("Ubuntu") {
|
||||
return Platform::Ubuntu;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Platform::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread-local storage for debug flag
|
||||
thread_local! {
|
||||
static DEBUG: std::cell::RefCell<bool> = std::cell::RefCell::new(false);
|
||||
}
|
||||
|
||||
/// Set the debug flag for the current thread
|
||||
pub fn set_thread_local_debug(debug: bool) {
|
||||
DEBUG.with(|cell| {
|
||||
*cell.borrow_mut() = debug;
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the debug flag for the current thread
|
||||
pub fn thread_local_debug() -> bool {
|
||||
DEBUG.with(|cell| {
|
||||
*cell.borrow()
|
||||
})
|
||||
}
|
||||
|
||||
/// Execute a package management command and return the result
|
||||
pub fn execute_package_command(args: &[&str], debug: bool) -> Result<CommandResult, PackageError> {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag
|
||||
set_thread_local_debug(debug);
|
||||
|
||||
if debug {
|
||||
println!("Executing command: {}", args.join(" "));
|
||||
}
|
||||
|
||||
let output = Command::new(args[0])
|
||||
.args(&args[1..])
|
||||
.output();
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
|
||||
let result = CommandResult {
|
||||
stdout,
|
||||
stderr,
|
||||
success: output.status.success(),
|
||||
code: output.status.code().unwrap_or(-1),
|
||||
};
|
||||
|
||||
// Always output stdout/stderr when debug is true
|
||||
if debug {
|
||||
if !result.stdout.is_empty() {
|
||||
println!("Command stdout: {}", result.stdout);
|
||||
}
|
||||
|
||||
if !result.stderr.is_empty() {
|
||||
println!("Command stderr: {}", result.stderr);
|
||||
}
|
||||
|
||||
if result.success {
|
||||
println!("Command succeeded with code {}", result.code);
|
||||
} else {
|
||||
println!("Command failed with code {}", result.code);
|
||||
}
|
||||
}
|
||||
|
||||
if result.success {
|
||||
Ok(result)
|
||||
} else {
|
||||
// If command failed and debug is false, output stderr
|
||||
if !debug {
|
||||
println!("Command failed with code {}: {}", result.code, result.stderr.trim());
|
||||
}
|
||||
Err(PackageError::CommandFailed(format!("Command failed with code {}: {}",
|
||||
result.code, result.stderr.trim())))
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
// Always output error information
|
||||
println!("Command execution failed: {}", e);
|
||||
Err(PackageError::CommandExecutionFailed(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for package managers
|
||||
pub trait PackageManager {
|
||||
/// Install a package
|
||||
fn install(&self, package: &str) -> Result<CommandResult, PackageError>;
|
||||
|
||||
/// Remove a package
|
||||
fn remove(&self, package: &str) -> Result<CommandResult, PackageError>;
|
||||
|
||||
/// Update package lists
|
||||
fn update(&self) -> Result<CommandResult, PackageError>;
|
||||
|
||||
/// Upgrade installed packages
|
||||
fn upgrade(&self) -> Result<CommandResult, PackageError>;
|
||||
|
||||
/// List installed packages
|
||||
fn list_installed(&self) -> Result<Vec<String>, PackageError>;
|
||||
|
||||
/// Search for packages
|
||||
fn search(&self, query: &str) -> Result<Vec<String>, PackageError>;
|
||||
|
||||
/// Check if a package is installed
|
||||
fn is_installed(&self, package: &str) -> Result<bool, PackageError>;
|
||||
}
|
||||
|
||||
/// APT package manager for Ubuntu
|
||||
pub struct AptPackageManager {
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
impl AptPackageManager {
|
||||
/// Create a new APT package manager
|
||||
pub fn new(debug: bool) -> Self {
|
||||
Self { debug }
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageManager for AptPackageManager {
|
||||
fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
// Use -y to make it non-interactive and --quiet to reduce output
|
||||
execute_package_command(&["apt-get", "install", "-y", "--quiet", package], self.debug)
|
||||
}
|
||||
|
||||
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
// Use -y to make it non-interactive and --quiet to reduce output
|
||||
execute_package_command(&["apt-get", "remove", "-y", "--quiet", package], self.debug)
|
||||
}
|
||||
|
||||
fn update(&self) -> Result<CommandResult, PackageError> {
|
||||
// Use -y to make it non-interactive and --quiet to reduce output
|
||||
execute_package_command(&["apt-get", "update", "-y", "--quiet"], self.debug)
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> Result<CommandResult, PackageError> {
|
||||
// Use -y to make it non-interactive and --quiet to reduce output
|
||||
execute_package_command(&["apt-get", "upgrade", "-y", "--quiet"], self.debug)
|
||||
}
|
||||
|
||||
fn list_installed(&self) -> Result<Vec<String>, PackageError> {
|
||||
let result = execute_package_command(&["dpkg", "--get-selections"], self.debug)?;
|
||||
let packages = result.stdout
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 2 && parts[1] == "install" {
|
||||
Some(parts[0].to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
|
||||
let result = execute_package_command(&["apt-cache", "search", query], self.debug)?;
|
||||
let packages = result.stdout
|
||||
.lines()
|
||||
.map(|line| {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if !parts.is_empty() {
|
||||
parts[0].to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
})
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
|
||||
let result = execute_package_command(&["dpkg", "-s", package], self.debug);
|
||||
match result {
|
||||
Ok(cmd_result) => Ok(cmd_result.success),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Homebrew package manager for macOS
|
||||
pub struct BrewPackageManager {
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
impl BrewPackageManager {
|
||||
/// Create a new Homebrew package manager
|
||||
pub fn new(debug: bool) -> Self {
|
||||
Self { debug }
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageManager for BrewPackageManager {
|
||||
fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
// Use --quiet to reduce output
|
||||
execute_package_command(&["brew", "install", "--quiet", package], self.debug)
|
||||
}
|
||||
|
||||
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
// Use --quiet to reduce output
|
||||
execute_package_command(&["brew", "uninstall", "--quiet", package], self.debug)
|
||||
}
|
||||
|
||||
fn update(&self) -> Result<CommandResult, PackageError> {
|
||||
// Use --quiet to reduce output
|
||||
execute_package_command(&["brew", "update", "--quiet"], self.debug)
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> Result<CommandResult, PackageError> {
|
||||
// Use --quiet to reduce output
|
||||
execute_package_command(&["brew", "upgrade", "--quiet"], self.debug)
|
||||
}
|
||||
|
||||
fn list_installed(&self) -> Result<Vec<String>, PackageError> {
|
||||
let result = execute_package_command(&["brew", "list", "--formula"], self.debug)?;
|
||||
let packages = result.stdout
|
||||
.lines()
|
||||
.map(|line| line.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
|
||||
let result = execute_package_command(&["brew", "search", query], self.debug)?;
|
||||
let packages = result.stdout
|
||||
.lines()
|
||||
.map(|line| line.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
|
||||
let result = execute_package_command(&["brew", "list", package], self.debug);
|
||||
match result {
|
||||
Ok(cmd_result) => Ok(cmd_result.success),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// PackHero factory for package management
|
||||
pub struct PackHero {
|
||||
platform: Platform,
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
impl PackHero {
|
||||
/// Create a new PackHero instance
|
||||
pub fn new() -> Self {
|
||||
let platform = Platform::detect();
|
||||
Self {
|
||||
platform,
|
||||
debug: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the debug mode
|
||||
pub fn set_debug(&mut self, debug: bool) -> &mut Self {
|
||||
self.debug = debug;
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the debug mode
|
||||
pub fn debug(&self) -> bool {
|
||||
self.debug
|
||||
}
|
||||
|
||||
/// Get the detected platform
|
||||
pub fn platform(&self) -> Platform {
|
||||
self.platform
|
||||
}
|
||||
|
||||
/// Get a package manager for the current platform
|
||||
fn get_package_manager(&self) -> Result<Box<dyn PackageManager>, PackageError> {
|
||||
match self.platform {
|
||||
Platform::Ubuntu => Ok(Box::new(AptPackageManager::new(self.debug))),
|
||||
Platform::MacOS => Ok(Box::new(BrewPackageManager::new(self.debug))),
|
||||
Platform::Unknown => Err(PackageError::UnsupportedPlatform("Unsupported platform".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Install a package
|
||||
pub fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.install(package)
|
||||
}
|
||||
|
||||
/// Remove a package
|
||||
pub fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.remove(package)
|
||||
}
|
||||
|
||||
/// Update package lists
|
||||
pub fn update(&self) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.update()
|
||||
}
|
||||
|
||||
/// Upgrade installed packages
|
||||
pub fn upgrade(&self) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.upgrade()
|
||||
}
|
||||
|
||||
/// List installed packages
|
||||
pub fn list_installed(&self) -> Result<Vec<String>, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.list_installed()
|
||||
}
|
||||
|
||||
/// Search for packages
|
||||
pub fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.search(query)
|
||||
}
|
||||
|
||||
/// Check if a package is installed
|
||||
pub fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.is_installed(package)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// Import the std::process::Command directly for some test-specific commands
|
||||
use std::process::Command as StdCommand;
|
||||
use super::*;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[test]
|
||||
fn test_platform_detection() {
|
||||
// This test will return different results depending on the platform it's run on
|
||||
let platform = Platform::detect();
|
||||
println!("Detected platform: {:?}", platform);
|
||||
|
||||
// Just ensure it doesn't panic
|
||||
assert!(true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_debug_flag() {
|
||||
// Test setting and getting the debug flag
|
||||
set_thread_local_debug(true);
|
||||
assert_eq!(thread_local_debug(), true);
|
||||
|
||||
set_thread_local_debug(false);
|
||||
assert_eq!(thread_local_debug(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_package_error_display() {
|
||||
// Test the Display implementation for PackageError
|
||||
let err1 = PackageError::CommandFailed("command failed".to_string());
|
||||
assert_eq!(err1.to_string(), "Command failed: command failed");
|
||||
|
||||
let err2 = PackageError::UnsupportedPlatform("test platform".to_string());
|
||||
assert_eq!(err2.to_string(), "Unsupported platform: test platform");
|
||||
|
||||
let err3 = PackageError::Other("other error".to_string());
|
||||
assert_eq!(err3.to_string(), "Error: other error");
|
||||
|
||||
// We can't easily test CommandExecutionFailed because std::io::Error doesn't implement PartialEq
|
||||
}
|
||||
|
||||
// Mock package manager for testing
|
||||
struct MockPackageManager {
|
||||
debug: bool,
|
||||
install_called: Arc<Mutex<bool>>,
|
||||
remove_called: Arc<Mutex<bool>>,
|
||||
update_called: Arc<Mutex<bool>>,
|
||||
upgrade_called: Arc<Mutex<bool>>,
|
||||
list_installed_called: Arc<Mutex<bool>>,
|
||||
search_called: Arc<Mutex<bool>>,
|
||||
is_installed_called: Arc<Mutex<bool>>,
|
||||
// Control what the mock returns
|
||||
should_succeed: bool,
|
||||
}
|
||||
|
||||
impl MockPackageManager {
|
||||
fn new(debug: bool, should_succeed: bool) -> Self {
|
||||
Self {
|
||||
debug,
|
||||
install_called: Arc::new(Mutex::new(false)),
|
||||
remove_called: Arc::new(Mutex::new(false)),
|
||||
update_called: Arc::new(Mutex::new(false)),
|
||||
upgrade_called: Arc::new(Mutex::new(false)),
|
||||
list_installed_called: Arc::new(Mutex::new(false)),
|
||||
search_called: Arc::new(Mutex::new(false)),
|
||||
is_installed_called: Arc::new(Mutex::new(false)),
|
||||
should_succeed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageManager for MockPackageManager {
|
||||
fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
*self.install_called.lock().unwrap() = true;
|
||||
if self.should_succeed {
|
||||
Ok(CommandResult {
|
||||
stdout: format!("Installed package {}", package),
|
||||
stderr: String::new(),
|
||||
success: true,
|
||||
code: 0,
|
||||
})
|
||||
} else {
|
||||
Err(PackageError::CommandFailed("Mock install failed".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
*self.remove_called.lock().unwrap() = true;
|
||||
if self.should_succeed {
|
||||
Ok(CommandResult {
|
||||
stdout: format!("Removed package {}", package),
|
||||
stderr: String::new(),
|
||||
success: true,
|
||||
code: 0,
|
||||
})
|
||||
} else {
|
||||
Err(PackageError::CommandFailed("Mock remove failed".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&self) -> Result<CommandResult, PackageError> {
|
||||
*self.update_called.lock().unwrap() = true;
|
||||
if self.should_succeed {
|
||||
Ok(CommandResult {
|
||||
stdout: "Updated package lists".to_string(),
|
||||
stderr: String::new(),
|
||||
success: true,
|
||||
code: 0,
|
||||
})
|
||||
} else {
|
||||
Err(PackageError::CommandFailed("Mock update failed".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> Result<CommandResult, PackageError> {
|
||||
*self.upgrade_called.lock().unwrap() = true;
|
||||
if self.should_succeed {
|
||||
Ok(CommandResult {
|
||||
stdout: "Upgraded packages".to_string(),
|
||||
stderr: String::new(),
|
||||
success: true,
|
||||
code: 0,
|
||||
})
|
||||
} else {
|
||||
Err(PackageError::CommandFailed("Mock upgrade failed".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn list_installed(&self) -> Result<Vec<String>, PackageError> {
|
||||
*self.list_installed_called.lock().unwrap() = true;
|
||||
if self.should_succeed {
|
||||
Ok(vec!["package1".to_string(), "package2".to_string()])
|
||||
} else {
|
||||
Err(PackageError::CommandFailed("Mock list_installed failed".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
|
||||
*self.search_called.lock().unwrap() = true;
|
||||
if self.should_succeed {
|
||||
Ok(vec![format!("result1-{}", query), format!("result2-{}", query)])
|
||||
} else {
|
||||
Err(PackageError::CommandFailed("Mock search failed".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
|
||||
*self.is_installed_called.lock().unwrap() = true;
|
||||
if self.should_succeed {
|
||||
Ok(package == "installed-package")
|
||||
} else {
|
||||
Err(PackageError::CommandFailed("Mock is_installed failed".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom PackHero for testing with a mock package manager
|
||||
struct TestPackHero {
|
||||
platform: Platform,
|
||||
debug: bool,
|
||||
mock_manager: MockPackageManager,
|
||||
}
|
||||
|
||||
impl TestPackHero {
|
||||
fn new(platform: Platform, debug: bool, should_succeed: bool) -> Self {
|
||||
Self {
|
||||
platform,
|
||||
debug,
|
||||
mock_manager: MockPackageManager::new(debug, should_succeed),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_package_manager(&self) -> Result<&dyn PackageManager, PackageError> {
|
||||
match self.platform {
|
||||
Platform::Ubuntu | Platform::MacOS => Ok(&self.mock_manager),
|
||||
Platform::Unknown => Err(PackageError::UnsupportedPlatform("Unsupported platform".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.install(package)
|
||||
}
|
||||
|
||||
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.remove(package)
|
||||
}
|
||||
|
||||
fn update(&self) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.update()
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.upgrade()
|
||||
}
|
||||
|
||||
fn list_installed(&self) -> Result<Vec<String>, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.list_installed()
|
||||
}
|
||||
|
||||
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.search(query)
|
||||
}
|
||||
|
||||
fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.is_installed(package)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_packhero_with_mock_success() {
|
||||
// Test PackHero with a mock package manager that succeeds
|
||||
let hero = TestPackHero::new(Platform::Ubuntu, false, true);
|
||||
|
||||
// Test install
|
||||
let result = hero.install("test-package");
|
||||
assert!(result.is_ok());
|
||||
assert!(*hero.mock_manager.install_called.lock().unwrap());
|
||||
|
||||
// Test remove
|
||||
let result = hero.remove("test-package");
|
||||
assert!(result.is_ok());
|
||||
assert!(*hero.mock_manager.remove_called.lock().unwrap());
|
||||
|
||||
// Test update
|
||||
let result = hero.update();
|
||||
assert!(result.is_ok());
|
||||
assert!(*hero.mock_manager.update_called.lock().unwrap());
|
||||
|
||||
// Test upgrade
|
||||
let result = hero.upgrade();
|
||||
assert!(result.is_ok());
|
||||
assert!(*hero.mock_manager.upgrade_called.lock().unwrap());
|
||||
|
||||
// Test list_installed
|
||||
let result = hero.list_installed();
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), vec!["package1".to_string(), "package2".to_string()]);
|
||||
assert!(*hero.mock_manager.list_installed_called.lock().unwrap());
|
||||
|
||||
// Test search
|
||||
let result = hero.search("query");
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), vec!["result1-query".to_string(), "result2-query".to_string()]);
|
||||
assert!(*hero.mock_manager.search_called.lock().unwrap());
|
||||
|
||||
// Test is_installed
|
||||
let result = hero.is_installed("installed-package");
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap());
|
||||
assert!(*hero.mock_manager.is_installed_called.lock().unwrap());
|
||||
|
||||
let result = hero.is_installed("not-installed-package");
|
||||
assert!(result.is_ok());
|
||||
assert!(!result.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_packhero_with_mock_failure() {
|
||||
// Test PackHero with a mock package manager that fails
|
||||
let hero = TestPackHero::new(Platform::Ubuntu, false, false);
|
||||
|
||||
// Test install
|
||||
let result = hero.install("test-package");
|
||||
assert!(result.is_err());
|
||||
assert!(*hero.mock_manager.install_called.lock().unwrap());
|
||||
|
||||
// Test remove
|
||||
let result = hero.remove("test-package");
|
||||
assert!(result.is_err());
|
||||
assert!(*hero.mock_manager.remove_called.lock().unwrap());
|
||||
|
||||
// Test update
|
||||
let result = hero.update();
|
||||
assert!(result.is_err());
|
||||
assert!(*hero.mock_manager.update_called.lock().unwrap());
|
||||
|
||||
// Test upgrade
|
||||
let result = hero.upgrade();
|
||||
assert!(result.is_err());
|
||||
assert!(*hero.mock_manager.upgrade_called.lock().unwrap());
|
||||
|
||||
// Test list_installed
|
||||
let result = hero.list_installed();
|
||||
assert!(result.is_err());
|
||||
assert!(*hero.mock_manager.list_installed_called.lock().unwrap());
|
||||
|
||||
// Test search
|
||||
let result = hero.search("query");
|
||||
assert!(result.is_err());
|
||||
assert!(*hero.mock_manager.search_called.lock().unwrap());
|
||||
|
||||
// Test is_installed
|
||||
let result = hero.is_installed("installed-package");
|
||||
assert!(result.is_err());
|
||||
assert!(*hero.mock_manager.is_installed_called.lock().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_packhero_unsupported_platform() {
|
||||
// Test PackHero with an unsupported platform
|
||||
let hero = TestPackHero::new(Platform::Unknown, false, true);
|
||||
|
||||
// All operations should fail with UnsupportedPlatform error
|
||||
let result = hero.install("test-package");
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(PackageError::UnsupportedPlatform(_)) => (),
|
||||
_ => panic!("Expected UnsupportedPlatform error"),
|
||||
}
|
||||
|
||||
let result = hero.remove("test-package");
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(PackageError::UnsupportedPlatform(_)) => (),
|
||||
_ => panic!("Expected UnsupportedPlatform error"),
|
||||
}
|
||||
|
||||
let result = hero.update();
|
||||
assert!(result.is_err());
|
||||
match result {
|
||||
Err(PackageError::UnsupportedPlatform(_)) => (),
|
||||
_ => panic!("Expected UnsupportedPlatform error"),
|
||||
}
|
||||
}
|
||||
|
||||
// Real-world tests that actually install and remove packages on Ubuntu
|
||||
// These tests will only run on Ubuntu and will be skipped on other platforms
|
||||
#[test]
|
||||
fn test_real_package_operations_on_ubuntu() {
|
||||
// Check if we're on Ubuntu
|
||||
let platform = Platform::detect();
|
||||
if platform != Platform::Ubuntu {
|
||||
println!("Skipping real package operations test on non-Ubuntu platform: {:?}", platform);
|
||||
return;
|
||||
}
|
||||
|
||||
println!("Running real package operations test on Ubuntu");
|
||||
|
||||
// Create a PackHero instance with debug enabled
|
||||
let mut hero = PackHero::new();
|
||||
hero.set_debug(true);
|
||||
|
||||
// Test package to install/remove
|
||||
let test_package = "wget";
|
||||
|
||||
// First, check if the package is already installed
|
||||
let is_installed_before = match hero.is_installed(test_package) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
println!("Error checking if package is installed: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!("Package {} is installed before test: {}", test_package, is_installed_before);
|
||||
|
||||
// If the package is already installed, we'll remove it first
|
||||
if is_installed_before {
|
||||
println!("Removing existing package {} before test", test_package);
|
||||
match hero.remove(test_package) {
|
||||
Ok(_) => println!("Successfully removed package {}", test_package),
|
||||
Err(e) => {
|
||||
println!("Error removing package {}: {}", test_package, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify it was removed
|
||||
match hero.is_installed(test_package) {
|
||||
Ok(is_installed) => {
|
||||
if is_installed {
|
||||
println!("Failed to remove package {}", test_package);
|
||||
return;
|
||||
} else {
|
||||
println!("Verified package {} was removed", test_package);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error checking if package is installed after removal: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now install the package
|
||||
println!("Installing package {}", test_package);
|
||||
match hero.install(test_package) {
|
||||
Ok(_) => println!("Successfully installed package {}", test_package),
|
||||
Err(e) => {
|
||||
println!("Error installing package {}: {}", test_package, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify it was installed
|
||||
match hero.is_installed(test_package) {
|
||||
Ok(is_installed) => {
|
||||
if !is_installed {
|
||||
println!("Failed to install package {}", test_package);
|
||||
return;
|
||||
} else {
|
||||
println!("Verified package {} was installed", test_package);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error checking if package is installed after installation: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Test the search functionality
|
||||
println!("Searching for packages with 'wget'");
|
||||
match hero.search("wget") {
|
||||
Ok(results) => {
|
||||
println!("Search results: {:?}", results);
|
||||
assert!(results.iter().any(|r| r.contains("wget")), "Search results should contain wget");
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error searching for packages: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Test listing installed packages
|
||||
println!("Listing installed packages");
|
||||
match hero.list_installed() {
|
||||
Ok(packages) => {
|
||||
println!("Found {} installed packages", packages.len());
|
||||
// Check if our test package is in the list
|
||||
assert!(packages.iter().any(|p| p == test_package),
|
||||
"Installed packages list should contain {}", test_package);
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error listing installed packages: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Now remove the package if it wasn't installed before
|
||||
if !is_installed_before {
|
||||
println!("Removing package {} after test", test_package);
|
||||
match hero.remove(test_package) {
|
||||
Ok(_) => println!("Successfully removed package {}", test_package),
|
||||
Err(e) => {
|
||||
println!("Error removing package {}: {}", test_package, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify it was removed
|
||||
match hero.is_installed(test_package) {
|
||||
Ok(is_installed) => {
|
||||
if is_installed {
|
||||
println!("Failed to remove package {}", test_package);
|
||||
return;
|
||||
} else {
|
||||
println!("Verified package {} was removed", test_package);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error checking if package is installed after removal: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test update functionality
|
||||
println!("Testing package list update");
|
||||
match hero.update() {
|
||||
Ok(_) => println!("Successfully updated package lists"),
|
||||
Err(e) => {
|
||||
println!("Error updating package lists: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
println!("All real package operations tests passed on Ubuntu");
|
||||
}
|
||||
|
||||
// Test to check if apt-get is available on the system
|
||||
#[test]
|
||||
fn test_apt_get_availability() {
|
||||
// This test checks if apt-get is available on the system
|
||||
let output = StdCommand::new("which")
|
||||
.arg("apt-get")
|
||||
.output()
|
||||
.expect("Failed to execute which apt-get");
|
||||
|
||||
let success = output.status.success();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
|
||||
println!("apt-get available: {}", success);
|
||||
if success {
|
||||
println!("apt-get path: {}", stdout.trim());
|
||||
}
|
||||
|
||||
// On Ubuntu, this should pass
|
||||
if Platform::detect() == Platform::Ubuntu {
|
||||
assert!(success, "apt-get should be available on Ubuntu");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,565 +0,0 @@
|
||||
# Package Management Module Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
The package management module will:
|
||||
1. Provide a factory called `PackHero` that detects the current platform
|
||||
2. Implement platform-specific package managers for Ubuntu (apt) and macOS (brew)
|
||||
3. Support operations: install, remove, update, upgrade, list installed packages, search for packages, and check if a package is installed
|
||||
4. Include debug functionality similar to buildah
|
||||
5. Ensure all operations are non-interactive and have proper error propagation
|
||||
6. Be wrapped in Rhai for scripting access
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class PackageError {
|
||||
+CommandFailed(String)
|
||||
+CommandExecutionFailed(std::io::Error)
|
||||
+UnsupportedPlatform(String)
|
||||
+Other(String)
|
||||
}
|
||||
|
||||
class PackHero {
|
||||
-platform: Platform
|
||||
-debug: bool
|
||||
+new() PackHero
|
||||
+detect_platform() Platform
|
||||
+set_debug(bool) PackHero
|
||||
+debug() bool
|
||||
+install(package: &str) Result
|
||||
+remove(package: &str) Result
|
||||
+update() Result
|
||||
+upgrade() Result
|
||||
+list_installed() Result
|
||||
+search(query: &str) Result
|
||||
+is_installed(package: &str) Result
|
||||
}
|
||||
|
||||
class Platform {
|
||||
<<enumeration>>
|
||||
Ubuntu
|
||||
MacOS
|
||||
Unknown
|
||||
}
|
||||
|
||||
class PackageManager {
|
||||
<<interface>>
|
||||
+install(package: &str) Result
|
||||
+remove(package: &str) Result
|
||||
+update() Result
|
||||
+upgrade() Result
|
||||
+list_installed() Result
|
||||
+search(query: &str) Result
|
||||
+is_installed(package: &str) Result
|
||||
}
|
||||
|
||||
class AptPackageManager {
|
||||
-debug: bool
|
||||
+new(debug: bool) AptPackageManager
|
||||
+install(package: &str) Result
|
||||
+remove(package: &str) Result
|
||||
+update() Result
|
||||
+upgrade() Result
|
||||
+list_installed() Result
|
||||
+search(query: &str) Result
|
||||
+is_installed(package: &str) Result
|
||||
}
|
||||
|
||||
class BrewPackageManager {
|
||||
-debug: bool
|
||||
+new(debug: bool) BrewPackageManager
|
||||
+install(package: &str) Result
|
||||
+remove(package: &str) Result
|
||||
+update() Result
|
||||
+upgrade() Result
|
||||
+list_installed() Result
|
||||
+search(query: &str) Result
|
||||
+is_installed(package: &str) Result
|
||||
}
|
||||
|
||||
PackHero --> Platform : uses
|
||||
PackHero --> PackageManager : uses
|
||||
PackageManager <|.. AptPackageManager : implements
|
||||
PackageManager <|.. BrewPackageManager : implements
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Package Error Type
|
||||
|
||||
Create a custom error type for package management operations:
|
||||
|
||||
```rust
|
||||
pub enum PackageError {
|
||||
CommandFailed(String),
|
||||
CommandExecutionFailed(std::io::Error),
|
||||
UnsupportedPlatform(String),
|
||||
Other(String),
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Platform Detection
|
||||
|
||||
Implement platform detection to determine which package manager to use:
|
||||
|
||||
```rust
|
||||
pub enum Platform {
|
||||
Ubuntu,
|
||||
MacOS,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Platform {
|
||||
pub fn detect() -> Self {
|
||||
// Check for macOS
|
||||
if std::path::Path::new("/usr/bin/sw_vers").exists() {
|
||||
return Platform::MacOS;
|
||||
}
|
||||
|
||||
// Check for Ubuntu
|
||||
if std::path::Path::new("/etc/lsb-release").exists() {
|
||||
// Read the file to confirm it's Ubuntu
|
||||
if let Ok(content) = std::fs::read_to_string("/etc/lsb-release") {
|
||||
if content.contains("Ubuntu") {
|
||||
return Platform::Ubuntu;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Platform::Unknown
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Package Manager Trait
|
||||
|
||||
Define a trait for package managers to implement:
|
||||
|
||||
```rust
|
||||
pub trait PackageManager {
|
||||
fn install(&self, package: &str) -> Result<CommandResult, PackageError>;
|
||||
fn remove(&self, package: &str) -> Result<CommandResult, PackageError>;
|
||||
fn update(&self) -> Result<CommandResult, PackageError>;
|
||||
fn upgrade(&self) -> Result<CommandResult, PackageError>;
|
||||
fn list_installed(&self) -> Result<Vec<String>, PackageError>;
|
||||
fn search(&self, query: &str) -> Result<Vec<String>, PackageError>;
|
||||
fn is_installed(&self, package: &str) -> Result<bool, PackageError>;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Platform-Specific Implementations
|
||||
|
||||
#### Ubuntu (apt) Implementation
|
||||
|
||||
```rust
|
||||
pub struct AptPackageManager {
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
impl AptPackageManager {
|
||||
pub fn new(debug: bool) -> Self {
|
||||
Self { debug }
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageManager for AptPackageManager {
|
||||
fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
execute_package_command(&["apt-get", "install", "-y", package], self.debug)
|
||||
}
|
||||
|
||||
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
execute_package_command(&["apt-get", "remove", "-y", package], self.debug)
|
||||
}
|
||||
|
||||
fn update(&self) -> Result<CommandResult, PackageError> {
|
||||
execute_package_command(&["apt-get", "update", "-y"], self.debug)
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> Result<CommandResult, PackageError> {
|
||||
execute_package_command(&["apt-get", "upgrade", "-y"], self.debug)
|
||||
}
|
||||
|
||||
fn list_installed(&self) -> Result<Vec<String>, PackageError> {
|
||||
let result = execute_package_command(&["dpkg", "--get-selections"], self.debug)?;
|
||||
let packages = result.stdout
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 2 && parts[1] == "install" {
|
||||
Some(parts[0].to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
|
||||
let result = execute_package_command(&["apt-cache", "search", query], self.debug)?;
|
||||
let packages = result.stdout
|
||||
.lines()
|
||||
.map(|line| {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if !parts.is_empty() {
|
||||
parts[0].to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
})
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
|
||||
let result = execute_package_command(&["dpkg", "-s", package], self.debug);
|
||||
match result {
|
||||
Ok(cmd_result) => Ok(cmd_result.success),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### macOS (brew) Implementation
|
||||
|
||||
```rust
|
||||
pub struct BrewPackageManager {
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
impl BrewPackageManager {
|
||||
pub fn new(debug: bool) -> Self {
|
||||
Self { debug }
|
||||
}
|
||||
}
|
||||
|
||||
impl PackageManager for BrewPackageManager {
|
||||
fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
execute_package_command(&["brew", "install", package], self.debug)
|
||||
}
|
||||
|
||||
fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
execute_package_command(&["brew", "uninstall", package], self.debug)
|
||||
}
|
||||
|
||||
fn update(&self) -> Result<CommandResult, PackageError> {
|
||||
execute_package_command(&["brew", "update"], self.debug)
|
||||
}
|
||||
|
||||
fn upgrade(&self) -> Result<CommandResult, PackageError> {
|
||||
execute_package_command(&["brew", "upgrade"], self.debug)
|
||||
}
|
||||
|
||||
fn list_installed(&self) -> Result<Vec<String>, PackageError> {
|
||||
let result = execute_package_command(&["brew", "list", "--formula"], self.debug)?;
|
||||
let packages = result.stdout
|
||||
.lines()
|
||||
.map(|line| line.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
|
||||
let result = execute_package_command(&["brew", "search", query], self.debug)?;
|
||||
let packages = result.stdout
|
||||
.lines()
|
||||
.map(|line| line.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
Ok(packages)
|
||||
}
|
||||
|
||||
fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
|
||||
let result = execute_package_command(&["brew", "list", package], self.debug);
|
||||
match result {
|
||||
Ok(cmd_result) => Ok(cmd_result.success),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Command Execution with Debug Support
|
||||
|
||||
Implement a function to execute package management commands with debug support:
|
||||
|
||||
```rust
|
||||
// Thread-local storage for debug flag
|
||||
thread_local! {
|
||||
static DEBUG: std::cell::RefCell<bool> = std::cell::RefCell::new(false);
|
||||
}
|
||||
|
||||
/// Set the debug flag for the current thread
|
||||
pub fn set_thread_local_debug(debug: bool) {
|
||||
DEBUG.with(|cell| {
|
||||
*cell.borrow_mut() = debug;
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the debug flag for the current thread
|
||||
pub fn thread_local_debug() -> bool {
|
||||
DEBUG.with(|cell| {
|
||||
*cell.borrow()
|
||||
})
|
||||
}
|
||||
|
||||
/// Execute a package management command and return the result
|
||||
pub fn execute_package_command(args: &[&str], debug: bool) -> Result<CommandResult, PackageError> {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag
|
||||
set_thread_local_debug(debug);
|
||||
|
||||
if debug {
|
||||
println!("Executing command: {}", args.join(" "));
|
||||
}
|
||||
|
||||
let output = Command::new(args[0])
|
||||
.args(&args[1..])
|
||||
.output();
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
|
||||
let result = CommandResult {
|
||||
stdout,
|
||||
stderr,
|
||||
success: output.status.success(),
|
||||
code: output.status.code().unwrap_or(-1),
|
||||
};
|
||||
|
||||
// Always output stdout/stderr when debug is true
|
||||
if debug {
|
||||
if !result.stdout.is_empty() {
|
||||
println!("Command stdout: {}", result.stdout);
|
||||
}
|
||||
|
||||
if !result.stderr.is_empty() {
|
||||
println!("Command stderr: {}", result.stderr);
|
||||
}
|
||||
|
||||
if result.success {
|
||||
println!("Command succeeded with code {}", result.code);
|
||||
} else {
|
||||
println!("Command failed with code {}", result.code);
|
||||
}
|
||||
}
|
||||
|
||||
if result.success {
|
||||
Ok(result)
|
||||
} else {
|
||||
// If command failed and debug is false, output stderr
|
||||
if !debug {
|
||||
println!("Command failed with code {}: {}", result.code, result.stderr.trim());
|
||||
}
|
||||
Err(PackageError::CommandFailed(format!("Command failed with code {}: {}",
|
||||
result.code, result.stderr.trim())))
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
// Always output error information
|
||||
println!("Command execution failed: {}", e);
|
||||
Err(PackageError::CommandExecutionFailed(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. PackHero Factory
|
||||
|
||||
Implement the PackHero factory to provide a unified interface:
|
||||
|
||||
```rust
|
||||
pub struct PackHero {
|
||||
platform: Platform,
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
impl PackHero {
|
||||
pub fn new() -> Self {
|
||||
let platform = Platform::detect();
|
||||
Self {
|
||||
platform,
|
||||
debug: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_debug(&mut self, debug: bool) -> &mut Self {
|
||||
self.debug = debug;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn debug(&self) -> bool {
|
||||
self.debug
|
||||
}
|
||||
|
||||
fn get_package_manager(&self) -> Result<Box<dyn PackageManager>, PackageError> {
|
||||
match self.platform {
|
||||
Platform::Ubuntu => Ok(Box::new(AptPackageManager::new(self.debug))),
|
||||
Platform::MacOS => Ok(Box::new(BrewPackageManager::new(self.debug))),
|
||||
Platform::Unknown => Err(PackageError::UnsupportedPlatform("Unsupported platform".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn install(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.install(package)
|
||||
}
|
||||
|
||||
pub fn remove(&self, package: &str) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.remove(package)
|
||||
}
|
||||
|
||||
pub fn update(&self) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.update()
|
||||
}
|
||||
|
||||
pub fn upgrade(&self) -> Result<CommandResult, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.upgrade()
|
||||
}
|
||||
|
||||
pub fn list_installed(&self) -> Result<Vec<String>, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.list_installed()
|
||||
}
|
||||
|
||||
pub fn search(&self, query: &str) -> Result<Vec<String>, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.search(query)
|
||||
}
|
||||
|
||||
pub fn is_installed(&self, package: &str) -> Result<bool, PackageError> {
|
||||
let pm = self.get_package_manager()?;
|
||||
pm.is_installed(package)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Rhai Integration
|
||||
|
||||
Update the Rhai OS module to include the package management functions:
|
||||
|
||||
```rust
|
||||
// In rhai/os.rs
|
||||
|
||||
// Register package management functions
|
||||
engine.register_fn("package_install", package_install);
|
||||
engine.register_fn("package_remove", package_remove);
|
||||
engine.register_fn("package_update", package_update);
|
||||
engine.register_fn("package_upgrade", package_upgrade);
|
||||
engine.register_fn("package_list", package_list);
|
||||
engine.register_fn("package_search", package_search);
|
||||
engine.register_fn("package_is_installed", package_is_installed);
|
||||
engine.register_fn("package_set_debug", package_set_debug);
|
||||
|
||||
// Wrapper for os::package::install
|
||||
pub fn package_install(package: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
let hero = os::package::PackHero::new();
|
||||
hero.install(package)
|
||||
.map(|_| "Package installed successfully".to_string())
|
||||
.to_rhai_error()
|
||||
}
|
||||
|
||||
// Wrapper for os::package::remove
|
||||
pub fn package_remove(package: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
let hero = os::package::PackHero::new();
|
||||
hero.remove(package)
|
||||
.map(|_| "Package removed successfully".to_string())
|
||||
.to_rhai_error()
|
||||
}
|
||||
|
||||
// Wrapper for os::package::update
|
||||
pub fn package_update() -> Result<String, Box<EvalAltResult>> {
|
||||
let hero = os::package::PackHero::new();
|
||||
hero.update()
|
||||
.map(|_| "Package lists updated successfully".to_string())
|
||||
.to_rhai_error()
|
||||
}
|
||||
|
||||
// Wrapper for os::package::upgrade
|
||||
pub fn package_upgrade() -> Result<String, Box<EvalAltResult>> {
|
||||
let hero = os::package::PackHero::new();
|
||||
hero.upgrade()
|
||||
.map(|_| "Packages upgraded successfully".to_string())
|
||||
.to_rhai_error()
|
||||
}
|
||||
|
||||
// Wrapper for os::package::list_installed
|
||||
pub fn package_list() -> Result<Array, Box<EvalAltResult>> {
|
||||
let hero = os::package::PackHero::new();
|
||||
let packages = hero.list_installed().to_rhai_error()?;
|
||||
|
||||
// Convert Vec<String> to Rhai Array
|
||||
let mut array = Array::new();
|
||||
for package in packages {
|
||||
array.push(package.into());
|
||||
}
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
// Wrapper for os::package::search
|
||||
pub fn package_search(query: &str) -> Result<Array, Box<EvalAltResult>> {
|
||||
let hero = os::package::PackHero::new();
|
||||
let packages = hero.search(query).to_rhai_error()?;
|
||||
|
||||
// Convert Vec<String> to Rhai Array
|
||||
let mut array = Array::new();
|
||||
for package in packages {
|
||||
array.push(package.into());
|
||||
}
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
// Wrapper for os::package::is_installed
|
||||
pub fn package_is_installed(package: &str) -> Result<bool, Box<EvalAltResult>> {
|
||||
let hero = os::package::PackHero::new();
|
||||
hero.is_installed(package).to_rhai_error()
|
||||
}
|
||||
|
||||
// Global debug flag for package management
|
||||
static mut PACKAGE_DEBUG: bool = false;
|
||||
|
||||
// Wrapper for setting package debug mode
|
||||
pub fn package_set_debug(debug: bool) -> bool {
|
||||
unsafe {
|
||||
PACKAGE_DEBUG = debug;
|
||||
PACKAGE_DEBUG
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. Create the package.rs file in the os directory
|
||||
2. Implement the PackageError enum
|
||||
3. Implement the Platform enum with detection logic
|
||||
4. Implement the PackageManager trait
|
||||
5. Implement the AptPackageManager for Ubuntu
|
||||
6. Implement the BrewPackageManager for macOS
|
||||
7. Implement the debug functionality with thread-local storage
|
||||
8. Implement the PackHero factory
|
||||
9. Update os/mod.rs to include the package module
|
||||
10. Update rhai/os.rs to include the package management functions
|
||||
11. Test the implementation on both Ubuntu and macOS
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. Create unit tests for each platform-specific implementation
|
||||
2. Create integration tests that verify the PackHero factory correctly detects the platform and uses the appropriate package manager
|
||||
3. Create Rhai examples that demonstrate the use of the package management functions
|
@ -5,11 +5,13 @@ use std::process::{Child, Command, Output, Stdio};
|
||||
use std::fmt;
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
use std::thread;
|
||||
|
||||
use crate::text;
|
||||
|
||||
/// Error type for command and script execution operations
|
||||
///
|
||||
/// This enum represents various errors that can occur during command and script
|
||||
/// execution, including preparation, execution, and output handling.
|
||||
#[derive(Debug)]
|
||||
pub enum RunError {
|
||||
/// The command string was empty
|
||||
@ -98,35 +100,16 @@ fn prepare_script_file(script_content: &str) -> Result<(PathBuf, String, tempfil
|
||||
let (ext, interpreter) = (".bat", "cmd.exe".to_string());
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
let (ext, interpreter) = (".sh", "/bin/bash".to_string());
|
||||
let (ext, interpreter) = (".sh", "/bin/sh".to_string());
|
||||
|
||||
// Create the script file
|
||||
let script_path = temp_dir.path().join(format!("script{}", ext));
|
||||
let mut file = File::create(&script_path)
|
||||
.map_err(RunError::FileCreationFailed)?;
|
||||
|
||||
// For Unix systems, ensure the script has a shebang line with -e flag
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
{
|
||||
let script_with_shebang = if dedented.trim_start().starts_with("#!") {
|
||||
// Script already has a shebang, use it as is
|
||||
dedented
|
||||
} else {
|
||||
// Add shebang with -e flag to ensure script fails on errors
|
||||
format!("#!/bin/bash -e\n{}", dedented)
|
||||
};
|
||||
|
||||
// Write the script content with shebang
|
||||
file.write_all(script_with_shebang.as_bytes())
|
||||
.map_err(RunError::FileWriteFailed)?;
|
||||
}
|
||||
|
||||
// For Windows, just write the script as is
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
file.write_all(dedented.as_bytes())
|
||||
.map_err(RunError::FileWriteFailed)?;
|
||||
}
|
||||
// Write the script content
|
||||
file.write_all(dedented.as_bytes())
|
||||
.map_err(RunError::FileWriteFailed)?;
|
||||
|
||||
// Make the script executable (Unix only)
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
@ -185,8 +168,7 @@ fn handle_child_output(mut child: Child, silent: bool) -> Result<CommandResult,
|
||||
if let Ok(l) = line {
|
||||
// Print the line if not silent and flush immediately
|
||||
if !silent_clone {
|
||||
// Print all stderr messages
|
||||
eprintln!("\x1b[31mERROR: {}\x1b[0m", l); // Red color for errors
|
||||
eprintln!("{}", l);
|
||||
std::io::stderr().flush().unwrap_or(());
|
||||
}
|
||||
// Store it in our captured buffer
|
||||
@ -218,14 +200,6 @@ fn handle_child_output(mut child: Child, silent: bool) -> Result<CommandResult,
|
||||
"Failed to capture stderr".to_string()
|
||||
};
|
||||
|
||||
// If the command failed, print the stderr if it wasn't already printed
|
||||
if !status.success() && silent && !captured_stderr.is_empty() {
|
||||
eprintln!("\x1b[31mCommand failed with error:\x1b[0m");
|
||||
for line in captured_stderr.lines() {
|
||||
eprintln!("\x1b[31m{}\x1b[0m", line);
|
||||
}
|
||||
}
|
||||
|
||||
// Return the command result
|
||||
Ok(CommandResult {
|
||||
stdout: captured_stdout,
|
||||
@ -241,14 +215,6 @@ fn process_command_output(output: Result<Output, std::io::Error>) -> Result<Comm
|
||||
Ok(out) => {
|
||||
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
// We'll collect stderr but not print it here
|
||||
// It will be included in the error message if the command fails
|
||||
|
||||
// If the command failed, print a clear error message
|
||||
if !out.status.success() {
|
||||
eprintln!("\x1b[31mCommand failed with exit code: {}\x1b[0m",
|
||||
out.status.code().unwrap_or(-1));
|
||||
}
|
||||
|
||||
Ok(CommandResult {
|
||||
stdout,
|
||||
@ -261,7 +227,19 @@ fn process_command_output(output: Result<Output, std::io::Error>) -> Result<Comm
|
||||
}
|
||||
}
|
||||
|
||||
/// Common logic for running a command with optional silent mode
|
||||
/**
|
||||
* Common logic for running a command with optional silent mode.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `command` - The command + args as a single string (e.g., "ls -la")
|
||||
* * `silent` - If `true`, don't print stdout/stderr as it arrives (capture only)
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(CommandResult)` - The result of the command execution
|
||||
* * `Err(RunError)` - An error if the command execution failed
|
||||
*/
|
||||
fn run_command_internal(command: &str, silent: bool) -> Result<CommandResult, RunError> {
|
||||
let mut parts = command.split_whitespace();
|
||||
let cmd = match parts.next() {
|
||||
@ -282,13 +260,26 @@ fn run_command_internal(command: &str, silent: bool) -> Result<CommandResult, Ru
|
||||
handle_child_output(child, silent)
|
||||
}
|
||||
|
||||
/// Execute a script with the given interpreter and path
|
||||
/**
|
||||
* Execute a script with the given interpreter and path.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `interpreter` - The interpreter to use (e.g., "/bin/sh")
|
||||
* * `script_path` - The path to the script file
|
||||
* * `silent` - If `true`, don't print stdout/stderr as it arrives (capture only)
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(CommandResult)` - The result of the script execution
|
||||
* * `Err(RunError)` - An error if the script execution failed
|
||||
*/
|
||||
fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool) -> Result<CommandResult, RunError> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let command_args = vec!["/c", script_path.to_str().unwrap_or("")];
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
let command_args = vec!["-e", script_path.to_str().unwrap_or("")];
|
||||
let command_args = vec![script_path.to_str().unwrap_or("")];
|
||||
|
||||
if silent {
|
||||
// For silent execution, use output() which captures but doesn't display
|
||||
@ -296,18 +287,7 @@ fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool)
|
||||
.args(&command_args)
|
||||
.output();
|
||||
|
||||
let result = process_command_output(output)?;
|
||||
|
||||
// If the script failed, return an error
|
||||
if !result.success {
|
||||
return Err(RunError::CommandFailed(format!(
|
||||
"Script execution failed with exit code {}: {}",
|
||||
result.code,
|
||||
result.stderr.trim()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
process_command_output(output)
|
||||
} else {
|
||||
// For normal execution, spawn and handle the output streams
|
||||
let child = Command::new(interpreter)
|
||||
@ -317,216 +297,204 @@ fn execute_script_internal(interpreter: &str, script_path: &Path, silent: bool)
|
||||
.spawn()
|
||||
.map_err(RunError::CommandExecutionFailed)?;
|
||||
|
||||
let result = handle_child_output(child, false)?;
|
||||
|
||||
// If the script failed, return an error
|
||||
if !result.success {
|
||||
return Err(RunError::CommandFailed(format!(
|
||||
"Script execution failed with exit code {}: {}",
|
||||
result.code,
|
||||
result.stderr.trim()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
handle_child_output(child, false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a multiline script with optional silent mode
|
||||
/**
|
||||
* Run a single command with arguments, showing live stdout and stderr.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `command` - The command + args as a single string (e.g., "ls -la")
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(CommandResult)` - The result of the command execution
|
||||
* * `Err(RunError)` - An error if the command execution failed
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* let result = run_command("ls -la")?;
|
||||
* println!("Command exited with code: {}", result.code);
|
||||
* ```
|
||||
*/
|
||||
pub fn run_command(command: &str) -> Result<CommandResult, RunError> {
|
||||
run_command_internal(command, /* silent = */ false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a single command with arguments silently.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `command` - The command + args as a single string (e.g., "ls -la")
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(CommandResult)` - The result of the command execution
|
||||
* * `Err(RunError)` - An error if the command execution failed
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* let result = run_command_silent("ls -la")?;
|
||||
* println!("Command output: {}", result.stdout);
|
||||
* ```
|
||||
*/
|
||||
pub fn run_command_silent(command: &str) -> Result<CommandResult, RunError> {
|
||||
run_command_internal(command, /* silent = */ true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a multiline script with optional silent mode.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `script` - The script content as a string
|
||||
* * `silent` - If `true`, don't print stdout/stderr as it arrives (capture only)
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(CommandResult)` - The result of the script execution
|
||||
* * `Err(RunError)` - An error if the script execution failed
|
||||
*/
|
||||
fn run_script_internal(script: &str, silent: bool) -> Result<CommandResult, RunError> {
|
||||
// Prepare the script file first to get the content with shebang
|
||||
let (script_path, interpreter, _temp_dir) = prepare_script_file(script)?;
|
||||
|
||||
// Print the script being executed if not silent
|
||||
if !silent {
|
||||
println!("\x1b[36mExecuting script:\x1b[0m");
|
||||
|
||||
// Read the script file to get the content with shebang
|
||||
if let Ok(script_content) = fs::read_to_string(&script_path) {
|
||||
for (i, line) in script_content.lines().enumerate() {
|
||||
println!("\x1b[36m{:3}: {}\x1b[0m", i + 1, line);
|
||||
}
|
||||
} else {
|
||||
// Fallback to original script if reading fails
|
||||
for (i, line) in script.lines().enumerate() {
|
||||
println!("\x1b[36m{:3}: {}\x1b[0m", i + 1, line);
|
||||
}
|
||||
}
|
||||
|
||||
println!("\x1b[36m---\x1b[0m");
|
||||
}
|
||||
|
||||
// _temp_dir is kept in scope until the end of this function to ensure
|
||||
// it's not dropped prematurely, which would clean up the directory
|
||||
execute_script_internal(&interpreter, &script_path, silent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a multiline script by saving it to a temporary file and executing.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `script` - The script content as a string
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(CommandResult)` - The result of the script execution
|
||||
* * `Err(RunError)` - An error if the script execution failed
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* let script = r#"
|
||||
* echo "Hello, world!"
|
||||
* ls -la
|
||||
* "#;
|
||||
* let result = run_script(script)?;
|
||||
* println!("Script exited with code: {}", result.code);
|
||||
* ```
|
||||
*/
|
||||
pub fn run_script(script: &str) -> Result<CommandResult, RunError> {
|
||||
run_script_internal(script, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a multiline script silently by saving it to a temporary file and executing.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `script` - The script content as a string
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(CommandResult)` - The result of the script execution
|
||||
* * `Err(RunError)` - An error if the script execution failed
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* let script = r#"
|
||||
* echo "Hello, world!"
|
||||
* ls -la
|
||||
* "#;
|
||||
* let result = run_script_silent(script)?;
|
||||
* println!("Script output: {}", result.stdout);
|
||||
* ```
|
||||
*/
|
||||
pub fn run_script_silent(script: &str) -> Result<CommandResult, RunError> {
|
||||
run_script_internal(script, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a command or multiline script with arguments.
|
||||
* Shows stdout/stderr as it arrives.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `command` - The command or script to run
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(CommandResult)` - The result of the execution
|
||||
* * `Err(RunError)` - An error if the execution failed
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* // Run a single command
|
||||
* let result = run("ls -la")?;
|
||||
*
|
||||
* // Run a multiline script
|
||||
* let result = run(r#"
|
||||
* echo "Hello, world!"
|
||||
* ls -la
|
||||
* "#)?;
|
||||
* ```
|
||||
*/
|
||||
pub fn run(command: &str) -> Result<CommandResult, RunError> {
|
||||
let trimmed = command.trim();
|
||||
|
||||
// Execute the script and handle the result
|
||||
let result = execute_script_internal(&interpreter, &script_path, silent);
|
||||
|
||||
// If there was an error, print a clear error message only if it's not a CommandFailed error
|
||||
// (which would already have printed the stderr)
|
||||
if let Err(ref e) = result {
|
||||
if !matches!(e, RunError::CommandFailed(_)) {
|
||||
eprintln!("\x1b[31mScript execution failed: {}\x1b[0m", e);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// A builder for configuring and executing commands or scripts
|
||||
pub struct RunBuilder<'a> {
|
||||
/// The command or script to run
|
||||
cmd: &'a str,
|
||||
/// Whether to return an error if the command fails (default: true)
|
||||
die: bool,
|
||||
/// Whether to suppress output to stdout/stderr (default: false)
|
||||
silent: bool,
|
||||
/// Whether to run the command asynchronously (default: false)
|
||||
async_exec: bool,
|
||||
/// Whether to log command execution (default: false)
|
||||
log: bool,
|
||||
}
|
||||
|
||||
impl<'a> RunBuilder<'a> {
|
||||
/// Create a new RunBuilder with default settings
|
||||
pub fn new(cmd: &'a str) -> Self {
|
||||
Self {
|
||||
cmd,
|
||||
die: true,
|
||||
silent: false,
|
||||
async_exec: false,
|
||||
log: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set whether to return an error if the command fails
|
||||
pub fn die(mut self, die: bool) -> Self {
|
||||
self.die = die;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether to suppress output to stdout/stderr
|
||||
pub fn silent(mut self, silent: bool) -> Self {
|
||||
self.silent = silent;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether to run the command asynchronously
|
||||
pub fn async_exec(mut self, async_exec: bool) -> Self {
|
||||
self.async_exec = async_exec;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether to log command execution
|
||||
pub fn log(mut self, log: bool) -> Self {
|
||||
self.log = log;
|
||||
self
|
||||
}
|
||||
|
||||
/// Execute the command or script with the configured options
|
||||
pub fn execute(self) -> Result<CommandResult, RunError> {
|
||||
let trimmed = self.cmd.trim();
|
||||
|
||||
// Log command execution if enabled
|
||||
if self.log {
|
||||
println!("\x1b[36m[LOG] Executing command: {}\x1b[0m", trimmed);
|
||||
}
|
||||
|
||||
// Handle async execution
|
||||
if self.async_exec {
|
||||
let cmd_copy = trimmed.to_string();
|
||||
let silent = self.silent;
|
||||
let log = self.log;
|
||||
|
||||
// Spawn a thread to run the command asynchronously
|
||||
thread::spawn(move || {
|
||||
if log {
|
||||
println!("\x1b[36m[ASYNC] Starting execution\x1b[0m");
|
||||
}
|
||||
|
||||
let result = if cmd_copy.contains('\n') {
|
||||
run_script_internal(&cmd_copy, silent)
|
||||
} else {
|
||||
run_command_internal(&cmd_copy, silent)
|
||||
};
|
||||
|
||||
if log {
|
||||
match &result {
|
||||
Ok(res) => {
|
||||
if res.success {
|
||||
println!("\x1b[32m[ASYNC] Command completed successfully\x1b[0m");
|
||||
} else {
|
||||
eprintln!("\x1b[31m[ASYNC] Command failed with exit code: {}\x1b[0m", res.code);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("\x1b[31m[ASYNC] Command failed with error: {}\x1b[0m", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Return a placeholder result for async execution
|
||||
return Ok(CommandResult {
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
success: true,
|
||||
code: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Execute the command or script
|
||||
let result = if trimmed.contains('\n') {
|
||||
// This is a multiline script
|
||||
run_script_internal(trimmed, self.silent)
|
||||
} else {
|
||||
// This is a single command
|
||||
run_command_internal(trimmed, self.silent)
|
||||
};
|
||||
|
||||
// Handle die=false: convert errors to CommandResult with success=false
|
||||
match result {
|
||||
Ok(res) => {
|
||||
// If the command failed but die is false, print a warning
|
||||
if !res.success && !self.die && !self.silent {
|
||||
eprintln!("\x1b[33mWarning: Command failed with exit code {} but 'die' is false\x1b[0m", res.code);
|
||||
}
|
||||
Ok(res)
|
||||
},
|
||||
Err(e) => {
|
||||
// Print the error only if it's not a CommandFailed error
|
||||
// (which would already have printed the stderr)
|
||||
if !matches!(e, RunError::CommandFailed(_)) {
|
||||
eprintln!("\x1b[31mCommand error: {}\x1b[0m", e);
|
||||
}
|
||||
|
||||
if self.die {
|
||||
Err(e)
|
||||
} else {
|
||||
// Convert error to CommandResult with success=false
|
||||
Ok(CommandResult {
|
||||
stdout: String::new(),
|
||||
stderr: format!("Error: {}", e),
|
||||
success: false,
|
||||
code: -1,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if this is a multiline script
|
||||
if trimmed.contains('\n') {
|
||||
// This is a multiline script, write to a temporary file and execute
|
||||
run_script(trimmed)
|
||||
} else {
|
||||
// This is a single command with arguments
|
||||
run_command(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new RunBuilder for executing a command or script
|
||||
pub fn run(cmd: &str) -> RunBuilder {
|
||||
RunBuilder::new(cmd)
|
||||
}
|
||||
|
||||
/// Run a command or multiline script with arguments
|
||||
pub fn run_command(command: &str) -> Result<CommandResult, RunError> {
|
||||
run(command).execute()
|
||||
}
|
||||
|
||||
/// Run a command or multiline script with arguments silently
|
||||
/**
|
||||
* Run a command or multiline script with arguments silently.
|
||||
* Doesn't show stdout/stderr as it arrives.
|
||||
*
|
||||
* # Arguments
|
||||
*
|
||||
* * `command` - The command or script to run
|
||||
*
|
||||
* # Returns
|
||||
*
|
||||
* * `Ok(CommandResult)` - The result of the execution
|
||||
* * `Err(RunError)` - An error if the execution failed
|
||||
*
|
||||
* # Examples
|
||||
*
|
||||
* ```
|
||||
* // Run a single command silently
|
||||
* let result = run_silent("ls -la")?;
|
||||
*
|
||||
* // Run a multiline script silently
|
||||
* let result = run_silent(r#"
|
||||
* echo "Hello, world!"
|
||||
* ls -la
|
||||
* "#)?;
|
||||
* ```
|
||||
*/
|
||||
pub fn run_silent(command: &str) -> Result<CommandResult, RunError> {
|
||||
run(command).silent(true).execute()
|
||||
let trimmed = command.trim();
|
||||
|
||||
// Check if this is a multiline script
|
||||
if trimmed.contains('\n') {
|
||||
// This is a multiline script, write to a temporary file and execute
|
||||
run_script_silent(trimmed)
|
||||
} else {
|
||||
// This is a single command with arguments
|
||||
run_command_silent(trimmed)
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,12 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use crate::process::run::{run, RunError};
|
||||
use crate::process::run::{run, run_silent, run_script, run_command};
|
||||
use crate::text::dedent;
|
||||
|
||||
#[test]
|
||||
fn test_run_command() {
|
||||
// Test running a simple echo command using the builder pattern
|
||||
let result = run("echo hello").execute().unwrap();
|
||||
// Test running a simple echo command
|
||||
let result = run_command("echo hello").unwrap();
|
||||
assert!(result.success);
|
||||
assert_eq!(result.code, 0);
|
||||
assert!(result.stdout.trim().contains("hello"));
|
||||
@ -18,8 +15,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_run_silent_command() {
|
||||
// Test running a command silently using the builder pattern
|
||||
let result = run("echo silent test").silent(true).execute().unwrap();
|
||||
// Test running a command silently
|
||||
let result = run_silent("echo silent test").unwrap();
|
||||
assert!(result.success);
|
||||
assert_eq!(result.code, 0);
|
||||
assert!(result.stdout.trim().contains("silent test"));
|
||||
@ -28,13 +25,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_run_script() {
|
||||
// Test running a multi-line script using the builder pattern
|
||||
// Test running a multi-line script
|
||||
let script = r#"
|
||||
echo "line 1"
|
||||
echo "line 2"
|
||||
"#;
|
||||
|
||||
let result = run(script).execute().unwrap();
|
||||
let result = run_script(script).unwrap();
|
||||
assert!(result.success);
|
||||
assert_eq!(result.code, 0);
|
||||
assert!(result.stdout.contains("line 1"));
|
||||
@ -56,7 +53,7 @@ mod tests {
|
||||
assert!(dedented.contains(" echo \"This has 16 spaces (4 more than the common indentation)\""));
|
||||
|
||||
// Running the script should work with the dedented content
|
||||
let result = run(script).execute().unwrap();
|
||||
let result = run(script).unwrap();
|
||||
assert!(result.success);
|
||||
assert_eq!(result.code, 0);
|
||||
assert!(result.stdout.contains("This has 12 spaces of indentation"));
|
||||
@ -69,13 +66,13 @@ mod tests {
|
||||
|
||||
// One-liner should be treated as a command
|
||||
let one_liner = "echo one-liner test";
|
||||
let result = run(one_liner).execute().unwrap();
|
||||
let result = run(one_liner).unwrap();
|
||||
assert!(result.success);
|
||||
assert!(result.stdout.contains("one-liner test"));
|
||||
|
||||
// Multi-line input should be treated as a script
|
||||
let multi_line = "echo first line\necho second line";
|
||||
let result = run(multi_line).execute().unwrap();
|
||||
let result = run(multi_line).unwrap();
|
||||
assert!(result.success);
|
||||
assert!(result.stdout.contains("first line"));
|
||||
assert!(result.stdout.contains("second line"));
|
||||
@ -84,86 +81,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_run_empty_command() {
|
||||
// Test handling of empty commands
|
||||
let result = run("").execute();
|
||||
let result = run("");
|
||||
assert!(result.is_err());
|
||||
// The specific error should be EmptyCommand
|
||||
match result {
|
||||
Err(RunError::EmptyCommand) => (),
|
||||
Err(crate::process::run::RunError::EmptyCommand) => (),
|
||||
_ => panic!("Expected EmptyCommand error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_die_option() {
|
||||
// Test the die option - when false, it should return a CommandResult with success=false
|
||||
// instead of an Err when the command fails
|
||||
|
||||
// With die=true (default), a non-existent command should return an error
|
||||
let result = run("non_existent_command").execute();
|
||||
assert!(result.is_err());
|
||||
|
||||
// With die=false, it should return a CommandResult with success=false
|
||||
let result = run("non_existent_command").die(false).execute().unwrap();
|
||||
assert!(!result.success);
|
||||
assert_ne!(result.code, 0);
|
||||
assert!(result.stderr.contains("Error:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_async_option() {
|
||||
// Test the async option - when true, it should spawn the process and return immediately
|
||||
|
||||
// Create a shared variable to track if the command has completed
|
||||
let completed = Arc::new(Mutex::new(false));
|
||||
let completed_clone = completed.clone();
|
||||
|
||||
// Run a command that sleeps for 2 seconds, with async=true
|
||||
let start = std::time::Instant::now();
|
||||
let result = run("sleep 2").async_exec(true).execute().unwrap();
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
// The command should return immediately (much less than 2 seconds)
|
||||
assert!(elapsed < Duration::from_secs(1));
|
||||
|
||||
// The result should have empty stdout/stderr and success=true
|
||||
assert!(result.success);
|
||||
assert_eq!(result.code, 0);
|
||||
assert_eq!(result.stdout, "");
|
||||
assert_eq!(result.stderr, "");
|
||||
|
||||
// Wait a bit to ensure the command has time to complete
|
||||
thread::sleep(Duration::from_secs(3));
|
||||
|
||||
// Verify the command completed (this is just a placeholder since we can't easily
|
||||
// check if the async command completed in this test framework)
|
||||
*completed_clone.lock().unwrap() = true;
|
||||
assert!(*completed.lock().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_log_option() {
|
||||
// Test the log option - when true, it should log command execution
|
||||
// Note: We can't easily capture stdout in tests, so this is more of a smoke test
|
||||
|
||||
// Run a command with log=true
|
||||
let result = run("echo log test").log(true).execute().unwrap();
|
||||
assert!(result.success);
|
||||
assert_eq!(result.code, 0);
|
||||
assert!(result.stdout.trim().contains("log test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builder_method_chaining() {
|
||||
// Test that all builder methods can be chained together
|
||||
let result = run("echo chaining test")
|
||||
.silent(true)
|
||||
.die(true)
|
||||
.log(true)
|
||||
.execute()
|
||||
.unwrap();
|
||||
|
||||
assert!(result.success);
|
||||
assert_eq!(result.code, 0);
|
||||
assert!(result.stdout.trim().contains("chaining test"));
|
||||
}
|
||||
}
|
@ -1,253 +0,0 @@
|
||||
//! Rhai wrappers for Buildah module functions
|
||||
//!
|
||||
//! This module provides Rhai wrappers for the functions in the Buildah module.
|
||||
|
||||
use rhai::{Engine, EvalAltResult, Array, Dynamic, Map};
|
||||
use std::collections::HashMap;
|
||||
use crate::virt::buildah::{BuildahError, Image, Builder, ContentOperations};
|
||||
use crate::process::CommandResult;
|
||||
|
||||
/// Register Buildah module functions with the Rhai engine
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `engine` - The Rhai engine to register the functions with
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
|
||||
pub fn register_bah_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register types
|
||||
register_bah_types(engine)?;
|
||||
|
||||
// Register Builder constructor
|
||||
engine.register_fn("bah_new", bah_new);
|
||||
|
||||
// Register Builder instance methods
|
||||
engine.register_fn("run", builder_run);
|
||||
engine.register_fn("run_with_isolation", builder_run_with_isolation);
|
||||
engine.register_fn("copy", builder_copy);
|
||||
engine.register_fn("add", builder_add);
|
||||
engine.register_fn("commit", builder_commit);
|
||||
engine.register_fn("remove", builder_remove);
|
||||
engine.register_fn("reset", builder_reset);
|
||||
engine.register_fn("config", builder_config);
|
||||
// Register Builder instance methods for entrypoint, cmd, and content operations
|
||||
engine.register_fn("set_entrypoint", builder_set_entrypoint);
|
||||
engine.register_fn("set_cmd", builder_set_cmd);
|
||||
engine.register_fn("write_content", builder_write_content);
|
||||
engine.register_fn("read_content", builder_read_content);
|
||||
|
||||
// Register Builder static methods
|
||||
engine.register_fn("images", builder_images);
|
||||
engine.register_fn("image_remove", builder_image_remove);
|
||||
engine.register_fn("image_pull", builder_image_pull);
|
||||
engine.register_fn("image_push", builder_image_push);
|
||||
engine.register_fn("image_tag", builder_image_tag);
|
||||
engine.register_fn("build", builder_build);
|
||||
engine.register_fn("read_content", builder_read_content);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register Buildah module types with the Rhai engine
|
||||
fn register_bah_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register Builder type
|
||||
engine.register_type_with_name::<Builder>("BuildahBuilder");
|
||||
|
||||
// Register getters for Builder properties
|
||||
engine.register_get("container_id", get_builder_container_id);
|
||||
engine.register_get("name", get_builder_name);
|
||||
engine.register_get("image", get_builder_image);
|
||||
engine.register_get("debug_mode", get_builder_debug);
|
||||
engine.register_set("debug_mode", set_builder_debug);
|
||||
|
||||
// Register Image type and methods (same as before)
|
||||
engine.register_type_with_name::<Image>("BuildahImage");
|
||||
|
||||
// Register getters for Image properties
|
||||
engine.register_get("id", |img: &mut Image| img.id.clone());
|
||||
engine.register_get("names", |img: &mut Image| {
|
||||
let mut array = Array::new();
|
||||
for name in &img.names {
|
||||
array.push(Dynamic::from(name.clone()));
|
||||
}
|
||||
array
|
||||
});
|
||||
// Add a 'name' getter that returns the first name or a default
|
||||
engine.register_get("name", |img: &mut Image| {
|
||||
if img.names.is_empty() {
|
||||
"<none>".to_string()
|
||||
} else {
|
||||
img.names[0].clone()
|
||||
}
|
||||
});
|
||||
engine.register_get("size", |img: &mut Image| img.size.clone());
|
||||
engine.register_get("created", |img: &mut Image| img.created.clone());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper functions for error conversion
|
||||
fn bah_error_to_rhai_error<T>(result: Result<T, BuildahError>) -> Result<T, Box<EvalAltResult>> {
|
||||
result.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Buildah error: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
// Helper function to convert Rhai Map to Rust HashMap
|
||||
fn convert_map_to_hashmap(options: Map) -> Result<HashMap<String, String>, Box<EvalAltResult>> {
|
||||
let mut config_options = HashMap::<String, String>::new();
|
||||
|
||||
for (key, value) in options.iter() {
|
||||
if let Ok(value_str) = value.clone().into_string() {
|
||||
// Convert SmartString to String
|
||||
config_options.insert(key.to_string(), value_str);
|
||||
} else {
|
||||
return Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Option '{}' must be a string", key).into(),
|
||||
rhai::Position::NONE
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(config_options)
|
||||
}
|
||||
|
||||
/// Create a new Builder
|
||||
pub fn bah_new(name: &str, image: &str) -> Result<Builder, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(Builder::new(name, image))
|
||||
}
|
||||
|
||||
// Builder instance methods
|
||||
pub fn builder_run(builder: &mut Builder, command: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(builder.run(command))
|
||||
}
|
||||
|
||||
pub fn builder_run_with_isolation(builder: &mut Builder, command: &str, isolation: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(builder.run_with_isolation(command, isolation))
|
||||
}
|
||||
|
||||
pub fn builder_copy(builder: &mut Builder, source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(builder.copy(source, dest))
|
||||
}
|
||||
|
||||
pub fn builder_add(builder: &mut Builder, source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(builder.add(source, dest))
|
||||
}
|
||||
|
||||
pub fn builder_commit(builder: &mut Builder, image_name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(builder.commit(image_name))
|
||||
}
|
||||
|
||||
pub fn builder_remove(builder: &mut Builder) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(builder.remove())
|
||||
}
|
||||
|
||||
pub fn builder_config(builder: &mut Builder, options: Map) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
// Convert Rhai Map to Rust HashMap
|
||||
let config_options = convert_map_to_hashmap(options)?;
|
||||
bah_error_to_rhai_error(builder.config(config_options))
|
||||
}
|
||||
|
||||
/// Set the entrypoint for the container
|
||||
pub fn builder_set_entrypoint(builder: &mut Builder, entrypoint: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(builder.set_entrypoint(entrypoint))
|
||||
}
|
||||
|
||||
/// Set the default command for the container
|
||||
pub fn builder_set_cmd(builder: &mut Builder, cmd: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(builder.set_cmd(cmd))
|
||||
}
|
||||
|
||||
/// Write content to a file in the container
|
||||
pub fn builder_write_content(builder: &mut Builder, content: &str, dest_path: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
if let Some(container_id) = builder.container_id() {
|
||||
bah_error_to_rhai_error(ContentOperations::write_content(container_id, content, dest_path))
|
||||
} else {
|
||||
Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
"No container ID available".into(),
|
||||
rhai::Position::NONE
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Read content from a file in the container
|
||||
pub fn builder_read_content(builder: &mut Builder, source_path: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
if let Some(container_id) = builder.container_id() {
|
||||
bah_error_to_rhai_error(ContentOperations::read_content(container_id, source_path))
|
||||
} else {
|
||||
Err(Box::new(EvalAltResult::ErrorRuntime(
|
||||
"No container ID available".into(),
|
||||
rhai::Position::NONE
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
// Builder static methods
|
||||
pub fn builder_images(_builder: &mut Builder) -> Result<Array, Box<EvalAltResult>> {
|
||||
let images = bah_error_to_rhai_error(Builder::images())?;
|
||||
|
||||
// Convert Vec<Image> to Rhai Array
|
||||
let mut array = Array::new();
|
||||
for image in images {
|
||||
array.push(Dynamic::from(image));
|
||||
}
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
pub fn builder_image_remove(_builder: &mut Builder, image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(Builder::image_remove(image))
|
||||
}
|
||||
|
||||
pub fn builder_image_pull(_builder: &mut Builder, image: &str, tls_verify: bool) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(Builder::image_pull(image, tls_verify))
|
||||
}
|
||||
|
||||
pub fn builder_image_push(_builder: &mut Builder, image: &str, destination: &str, tls_verify: bool) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(Builder::image_push(image, destination, tls_verify))
|
||||
}
|
||||
|
||||
pub fn builder_image_tag(_builder: &mut Builder, image: &str, new_name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(Builder::image_tag(image, new_name))
|
||||
}
|
||||
|
||||
// Getter functions for Builder properties
|
||||
pub fn get_builder_container_id(builder: &mut Builder) -> String {
|
||||
match builder.container_id() {
|
||||
Some(id) => id.clone(),
|
||||
None => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_builder_name(builder: &mut Builder) -> String {
|
||||
builder.name().to_string()
|
||||
}
|
||||
|
||||
pub fn get_builder_image(builder: &mut Builder) -> String {
|
||||
builder.image().to_string()
|
||||
}
|
||||
|
||||
/// Get the debug flag from a Builder
|
||||
pub fn get_builder_debug(builder: &mut Builder) -> bool {
|
||||
builder.debug()
|
||||
}
|
||||
|
||||
/// Set the debug flag on a Builder
|
||||
pub fn set_builder_debug(builder: &mut Builder, debug: bool) {
|
||||
builder.set_debug(debug);
|
||||
}
|
||||
|
||||
// Reset function for Builder
|
||||
pub fn builder_reset(builder: &mut Builder) -> Result<(), Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(builder.reset())
|
||||
}
|
||||
|
||||
// Build function for Builder
|
||||
pub fn builder_build(_builder: &mut Builder, tag: &str, context_dir: &str, file: &str, isolation: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
bah_error_to_rhai_error(Builder::build(Some(tag), context_dir, file, Some(isolation)))
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
//! Error handling for Rhai integration
|
||||
//!
|
||||
//! This module provides utilities for converting SAL error types to Rhai error types.
|
||||
|
||||
use rhai::{EvalAltResult, Position};
|
||||
use crate::os::{FsError, DownloadError};
|
||||
use crate::os::package::PackageError;
|
||||
|
||||
/// Convert a FsError to a Rhai EvalAltResult
|
||||
pub fn fs_error_to_rhai_error(err: FsError) -> Box<EvalAltResult> {
|
||||
let err_msg = err.to_string();
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
err_msg.into(),
|
||||
Position::NONE
|
||||
))
|
||||
}
|
||||
|
||||
/// Convert a DownloadError to a Rhai EvalAltResult
|
||||
pub fn download_error_to_rhai_error(err: DownloadError) -> Box<EvalAltResult> {
|
||||
let err_msg = err.to_string();
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
err_msg.into(),
|
||||
Position::NONE
|
||||
))
|
||||
}
|
||||
|
||||
/// Convert a PackageError to a Rhai EvalAltResult
|
||||
pub fn package_error_to_rhai_error(err: PackageError) -> Box<EvalAltResult> {
|
||||
let err_msg = err.to_string();
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
err_msg.into(),
|
||||
Position::NONE
|
||||
))
|
||||
}
|
||||
|
||||
/// Register error types with the Rhai engine
|
||||
pub fn register_error_types(engine: &mut rhai::Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register helper functions for error handling
|
||||
// Note: We don't register the error types directly since they don't implement Clone
|
||||
// Instead, we'll convert them to strings in the wrappers
|
||||
|
||||
// Register functions to get error messages
|
||||
engine.register_fn("fs_error_message", |err_msg: &str| -> String {
|
||||
format!("File system error: {}", err_msg)
|
||||
});
|
||||
|
||||
engine.register_fn("download_error_message", |err_msg: &str| -> String {
|
||||
format!("Download error: {}", err_msg)
|
||||
});
|
||||
|
||||
engine.register_fn("package_error_message", |err_msg: &str| -> String {
|
||||
format!("Package management error: {}", err_msg)
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Trait for converting SAL errors to Rhai errors
|
||||
pub trait ToRhaiError<T> {
|
||||
/// Convert the error to a Rhai EvalAltResult
|
||||
fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>>;
|
||||
}
|
||||
|
||||
impl<T> ToRhaiError<T> for Result<T, FsError> {
|
||||
fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>> {
|
||||
self.map_err(fs_error_to_rhai_error)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToRhaiError<T> for Result<T, DownloadError> {
|
||||
fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>> {
|
||||
self.map_err(download_error_to_rhai_error)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ToRhaiError<T> for Result<T, PackageError> {
|
||||
fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>> {
|
||||
self.map_err(package_error_to_rhai_error)
|
||||
}
|
||||
}
|
147
src/rhai/git.rs
147
src/rhai/git.rs
@ -1,147 +0,0 @@
|
||||
//! Rhai wrappers for Git module functions
|
||||
//!
|
||||
//! This module provides Rhai wrappers for the functions in the Git module.
|
||||
|
||||
use rhai::{Engine, EvalAltResult, Array, Dynamic};
|
||||
use crate::git::{GitTree, GitRepo, GitError};
|
||||
|
||||
/// Register Git module functions with the Rhai engine
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `engine` - The Rhai engine to register the functions with
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
|
||||
pub fn register_git_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register GitTree constructor
|
||||
engine.register_fn("gittree_new", git_tree_new);
|
||||
|
||||
// Register GitTree methods
|
||||
engine.register_fn("list", git_tree_list);
|
||||
engine.register_fn("find", git_tree_find);
|
||||
engine.register_fn("get", git_tree_get);
|
||||
|
||||
// Register GitRepo methods
|
||||
engine.register_fn("path", git_repo_path);
|
||||
engine.register_fn("has_changes", git_repo_has_changes);
|
||||
engine.register_fn("pull", git_repo_pull);
|
||||
engine.register_fn("reset", git_repo_reset);
|
||||
engine.register_fn("commit", git_repo_commit);
|
||||
engine.register_fn("push", git_repo_push);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper functions for error conversion
|
||||
fn git_error_to_rhai_error<T>(result: Result<T, GitError>) -> Result<T, Box<EvalAltResult>> {
|
||||
result.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Git error: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// GitTree Function Wrappers
|
||||
//
|
||||
|
||||
/// Wrapper for GitTree::new
|
||||
///
|
||||
/// Creates a new GitTree with the specified base path.
|
||||
pub fn git_tree_new(base_path: &str) -> Result<GitTree, Box<EvalAltResult>> {
|
||||
git_error_to_rhai_error(GitTree::new(base_path))
|
||||
}
|
||||
|
||||
/// Wrapper for GitTree::list
|
||||
///
|
||||
/// Lists all git repositories under the base path.
|
||||
pub fn git_tree_list(git_tree: &mut GitTree) -> Result<Array, Box<EvalAltResult>> {
|
||||
let repos = git_error_to_rhai_error(git_tree.list())?;
|
||||
|
||||
// Convert Vec<String> to Rhai Array
|
||||
let mut array = Array::new();
|
||||
for repo in repos {
|
||||
array.push(Dynamic::from(repo));
|
||||
}
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
/// Wrapper for GitTree::find
|
||||
///
|
||||
/// Finds repositories matching a pattern or partial path.
|
||||
pub fn git_tree_find(git_tree: &mut GitTree, pattern: &str) -> Result<Array, Box<EvalAltResult>> {
|
||||
let repos = git_error_to_rhai_error(git_tree.find(pattern))?;
|
||||
|
||||
// Convert Vec<String> to Rhai Array
|
||||
let mut array = Array::new();
|
||||
for repo in repos {
|
||||
array.push(Dynamic::from(repo));
|
||||
}
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
/// Wrapper for GitTree::get
|
||||
///
|
||||
/// Gets one or more GitRepo objects based on a path pattern or URL.
|
||||
pub fn git_tree_get(git_tree: &mut GitTree, path_or_url: &str) -> Result<Array, Box<EvalAltResult>> {
|
||||
let repos = git_error_to_rhai_error(git_tree.get(path_or_url))?;
|
||||
|
||||
// Convert Vec<GitRepo> to Rhai Array
|
||||
let mut array = Array::new();
|
||||
for repo in repos {
|
||||
array.push(Dynamic::from(repo));
|
||||
}
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
//
|
||||
// GitRepo Function Wrappers
|
||||
//
|
||||
|
||||
/// Wrapper for GitRepo::path
|
||||
///
|
||||
/// Gets the path of the repository.
|
||||
pub fn git_repo_path(git_repo: &mut GitRepo) -> String {
|
||||
git_repo.path().to_string()
|
||||
}
|
||||
|
||||
/// Wrapper for GitRepo::has_changes
|
||||
///
|
||||
/// Checks if the repository has uncommitted changes.
|
||||
pub fn git_repo_has_changes(git_repo: &mut GitRepo) -> Result<bool, Box<EvalAltResult>> {
|
||||
git_error_to_rhai_error(git_repo.has_changes())
|
||||
}
|
||||
|
||||
/// Wrapper for GitRepo::pull
|
||||
///
|
||||
/// Pulls the latest changes from the remote repository.
|
||||
pub fn git_repo_pull(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> {
|
||||
git_error_to_rhai_error(git_repo.pull())
|
||||
}
|
||||
|
||||
/// Wrapper for GitRepo::reset
|
||||
///
|
||||
/// Resets any local changes in the repository.
|
||||
pub fn git_repo_reset(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> {
|
||||
git_error_to_rhai_error(git_repo.reset())
|
||||
}
|
||||
|
||||
/// Wrapper for GitRepo::commit
|
||||
///
|
||||
/// Commits changes in the repository.
|
||||
pub fn git_repo_commit(git_repo: &mut GitRepo, message: &str) -> Result<GitRepo, Box<EvalAltResult>> {
|
||||
git_error_to_rhai_error(git_repo.commit(message))
|
||||
}
|
||||
|
||||
/// Wrapper for GitRepo::push
|
||||
///
|
||||
/// Pushes changes to the remote repository.
|
||||
pub fn git_repo_push(git_repo: &mut GitRepo) -> Result<GitRepo, Box<EvalAltResult>> {
|
||||
git_error_to_rhai_error(git_repo.push())
|
||||
}
|
124
src/rhai/mod.rs
124
src/rhai/mod.rs
@ -1,124 +0,0 @@
|
||||
//! Rhai scripting integration for the SAL library
|
||||
//!
|
||||
//! This module provides integration with the Rhai scripting language,
|
||||
//! allowing SAL functions to be called from Rhai scripts.
|
||||
|
||||
mod error;
|
||||
mod os;
|
||||
mod process;
|
||||
mod buildah;
|
||||
mod nerdctl;
|
||||
mod git;
|
||||
mod text;
|
||||
mod rfs;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
// Re-export common Rhai types for convenience
|
||||
pub use rhai::{Array, Dynamic, Map, EvalAltResult, Engine};
|
||||
|
||||
// Re-export error module
|
||||
pub use error::*;
|
||||
|
||||
// Re-export specific functions from modules to avoid name conflicts
|
||||
pub use os::{
|
||||
register_os_module,
|
||||
// File system functions
|
||||
exist, find_file, find_files, find_dir, find_dirs,
|
||||
delete, mkdir, file_size, rsync,
|
||||
// Download functions
|
||||
download, download_install
|
||||
};
|
||||
|
||||
pub use process::{
|
||||
register_process_module,
|
||||
// Run functions
|
||||
run, run_silent, run_with_options, new_run_options,
|
||||
// Process management functions
|
||||
which, kill, process_list, process_get
|
||||
};
|
||||
|
||||
// Re-export buildah functions
|
||||
pub use buildah::register_bah_module;
|
||||
pub use buildah::bah_new;
|
||||
|
||||
// Re-export nerdctl functions
|
||||
pub use nerdctl::register_nerdctl_module;
|
||||
pub use nerdctl::{
|
||||
// Container functions
|
||||
nerdctl_run, nerdctl_run_with_name, nerdctl_run_with_port,
|
||||
nerdctl_exec, nerdctl_copy, nerdctl_stop, nerdctl_remove, nerdctl_list,
|
||||
// Image functions
|
||||
nerdctl_images, nerdctl_image_remove, nerdctl_image_push, nerdctl_image_tag,
|
||||
nerdctl_image_pull, nerdctl_image_commit, nerdctl_image_build
|
||||
};
|
||||
|
||||
// Re-export RFS module
|
||||
pub use rfs::register as register_rfs_module;
|
||||
|
||||
// Re-export git module
|
||||
pub use git::register_git_module;
|
||||
pub use crate::git::{GitTree, GitRepo};
|
||||
|
||||
// Re-export text module
|
||||
pub use text::register_text_module;
|
||||
// Re-export text functions directly from text module
|
||||
pub use crate::text::{
|
||||
// Fix functions
|
||||
name_fix, path_fix,
|
||||
// Dedent functions
|
||||
dedent, prefix
|
||||
};
|
||||
|
||||
// Re-export TextReplacer functions
|
||||
pub use text::*;
|
||||
|
||||
// Rename copy functions to avoid conflicts
|
||||
pub use os::copy as os_copy;
|
||||
|
||||
/// Register all SAL modules with the Rhai engine
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `engine` - The Rhai engine to register the modules with
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use rhai::Engine;
|
||||
/// use sal::rhai;
|
||||
///
|
||||
/// let mut engine = Engine::new();
|
||||
/// rhai::register(&mut engine);
|
||||
///
|
||||
/// // Now you can use SAL functions in Rhai scripts
|
||||
/// let result = engine.eval::<bool>("exist('some_file.txt')").unwrap();
|
||||
/// ```
|
||||
pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
// Register OS module functions
|
||||
os::register_os_module(engine)?;
|
||||
|
||||
// Register Process module functions
|
||||
process::register_process_module(engine)?;
|
||||
|
||||
// Register Buildah module functions
|
||||
buildah::register_bah_module(engine)?;
|
||||
|
||||
// Register Nerdctl module functions
|
||||
nerdctl::register_nerdctl_module(engine)?;
|
||||
|
||||
// Register Git module functions
|
||||
git::register_git_module(engine)?;
|
||||
|
||||
// Register Text module functions
|
||||
text::register_text_module(engine)?;
|
||||
|
||||
// Register RFS module functions
|
||||
rfs::register(engine)?;
|
||||
|
||||
// Future modules can be registered here
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,580 +0,0 @@
|
||||
//! Rhai wrappers for Nerdctl module functions
|
||||
//!
|
||||
//! This module provides Rhai wrappers for the functions in the Nerdctl module.
|
||||
|
||||
use rhai::{Engine, EvalAltResult, Array, Dynamic, Map};
|
||||
use crate::virt::nerdctl::{self, NerdctlError, Image, Container};
|
||||
use crate::process::CommandResult;
|
||||
|
||||
// Helper functions for error conversion with improved context
|
||||
fn nerdctl_error_to_rhai_error<T>(result: Result<T, NerdctlError>) -> Result<T, Box<EvalAltResult>> {
|
||||
result.map_err(|e| {
|
||||
// Create a more detailed error message based on the error type
|
||||
let error_message = match &e {
|
||||
NerdctlError::CommandExecutionFailed(io_err) => {
|
||||
format!("Failed to execute nerdctl command: {}. This may indicate nerdctl is not installed or not in PATH.", io_err)
|
||||
},
|
||||
NerdctlError::CommandFailed(msg) => {
|
||||
format!("Nerdctl command failed: {}. Check container status and logs for more details.", msg)
|
||||
},
|
||||
NerdctlError::JsonParseError(msg) => {
|
||||
format!("Failed to parse nerdctl JSON output: {}. This may indicate an incompatible nerdctl version.", msg)
|
||||
},
|
||||
NerdctlError::ConversionError(msg) => {
|
||||
format!("Data conversion error: {}. This may indicate unexpected output format from nerdctl.", msg)
|
||||
},
|
||||
NerdctlError::Other(msg) => {
|
||||
format!("Nerdctl error: {}. This is an unexpected error.", msg)
|
||||
},
|
||||
};
|
||||
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
error_message.into(),
|
||||
rhai::Position::NONE
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
// Container Builder Pattern Implementation
|
||||
//
|
||||
|
||||
/// Create a new Container
|
||||
pub fn container_new(name: &str) -> Result<Container, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(Container::new(name))
|
||||
}
|
||||
|
||||
/// Create a Container from an image
|
||||
pub fn container_from_image(name: &str, image: &str) -> Result<Container, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(Container::from_image(name, image))
|
||||
}
|
||||
|
||||
/// Reset the container configuration to defaults while keeping the name and image
|
||||
pub fn container_reset(container: Container) -> Container {
|
||||
container.reset()
|
||||
}
|
||||
|
||||
/// Add a port mapping to a Container
|
||||
pub fn container_with_port(container: Container, port: &str) -> Container {
|
||||
container.with_port(port)
|
||||
}
|
||||
|
||||
/// Add a volume mount to a Container
|
||||
pub fn container_with_volume(container: Container, volume: &str) -> Container {
|
||||
container.with_volume(volume)
|
||||
}
|
||||
|
||||
/// Add an environment variable to a Container
|
||||
pub fn container_with_env(container: Container, key: &str, value: &str) -> Container {
|
||||
container.with_env(key, value)
|
||||
}
|
||||
|
||||
/// Set the network for a Container
|
||||
pub fn container_with_network(container: Container, network: &str) -> Container {
|
||||
container.with_network(network)
|
||||
}
|
||||
|
||||
/// Add a network alias to a Container
|
||||
pub fn container_with_network_alias(container: Container, alias: &str) -> Container {
|
||||
container.with_network_alias(alias)
|
||||
}
|
||||
|
||||
/// Set CPU limit for a Container
|
||||
pub fn container_with_cpu_limit(container: Container, cpus: &str) -> Container {
|
||||
container.with_cpu_limit(cpus)
|
||||
}
|
||||
|
||||
/// Set memory limit for a Container
|
||||
pub fn container_with_memory_limit(container: Container, memory: &str) -> Container {
|
||||
container.with_memory_limit(memory)
|
||||
}
|
||||
|
||||
/// Set restart policy for a Container
|
||||
pub fn container_with_restart_policy(container: Container, policy: &str) -> Container {
|
||||
container.with_restart_policy(policy)
|
||||
}
|
||||
|
||||
/// Set health check for a Container
|
||||
pub fn container_with_health_check(container: Container, cmd: &str) -> Container {
|
||||
container.with_health_check(cmd)
|
||||
}
|
||||
|
||||
/// Add multiple port mappings to a Container
|
||||
pub fn container_with_ports(mut container: Container, ports: Array) -> Container {
|
||||
for port in ports.iter() {
|
||||
if port.is_string() {
|
||||
let port_str = port.clone().cast::<String>();
|
||||
container = container.with_port(&port_str);
|
||||
}
|
||||
}
|
||||
container
|
||||
}
|
||||
|
||||
/// Add multiple volume mounts to a Container
|
||||
pub fn container_with_volumes(mut container: Container, volumes: Array) -> Container {
|
||||
for volume in volumes.iter() {
|
||||
if volume.is_string() {
|
||||
let volume_str = volume.clone().cast::<String>();
|
||||
container = container.with_volume(&volume_str);
|
||||
}
|
||||
}
|
||||
container
|
||||
}
|
||||
|
||||
/// Add multiple environment variables to a Container
|
||||
pub fn container_with_envs(mut container: Container, env_map: Map) -> Container {
|
||||
for (key, value) in env_map.iter() {
|
||||
if value.is_string() {
|
||||
let value_str = value.clone().cast::<String>();
|
||||
container = container.with_env(&key, &value_str);
|
||||
}
|
||||
}
|
||||
container
|
||||
}
|
||||
|
||||
/// Add multiple network aliases to a Container
|
||||
pub fn container_with_network_aliases(mut container: Container, aliases: Array) -> Container {
|
||||
for alias in aliases.iter() {
|
||||
if alias.is_string() {
|
||||
let alias_str = alias.clone().cast::<String>();
|
||||
container = container.with_network_alias(&alias_str);
|
||||
}
|
||||
}
|
||||
container
|
||||
}
|
||||
|
||||
/// Set memory swap limit for a Container
|
||||
pub fn container_with_memory_swap_limit(container: Container, memory_swap: &str) -> Container {
|
||||
container.with_memory_swap_limit(memory_swap)
|
||||
}
|
||||
|
||||
/// Set CPU shares for a Container
|
||||
pub fn container_with_cpu_shares(container: Container, shares: &str) -> Container {
|
||||
container.with_cpu_shares(shares)
|
||||
}
|
||||
|
||||
/// Set health check with options for a Container
|
||||
pub fn container_with_health_check_options(
|
||||
container: Container,
|
||||
cmd: &str,
|
||||
interval: Option<&str>,
|
||||
timeout: Option<&str>,
|
||||
retries: Option<i64>,
|
||||
start_period: Option<&str>
|
||||
) -> Container {
|
||||
// Convert i64 to u32 for retries
|
||||
let retries_u32 = retries.map(|r| r as u32);
|
||||
container.with_health_check_options(cmd, interval, timeout, retries_u32, start_period)
|
||||
}
|
||||
|
||||
/// Set snapshotter for a Container
|
||||
pub fn container_with_snapshotter(container: Container, snapshotter: &str) -> Container {
|
||||
container.with_snapshotter(snapshotter)
|
||||
}
|
||||
|
||||
/// Set detach mode for a Container
|
||||
pub fn container_with_detach(container: Container, detach: bool) -> Container {
|
||||
container.with_detach(detach)
|
||||
}
|
||||
|
||||
/// Build and run the Container
|
||||
///
|
||||
/// This function builds and runs the container using the configured options.
|
||||
/// It provides detailed error information if the build fails.
|
||||
pub fn container_build(container: Container) -> Result<Container, Box<EvalAltResult>> {
|
||||
// Get container details for better error reporting
|
||||
let container_name = container.name.clone();
|
||||
let image = container.image.clone().unwrap_or_else(|| "none".to_string());
|
||||
let ports = container.ports.clone();
|
||||
let volumes = container.volumes.clone();
|
||||
let env_vars = container.env_vars.clone();
|
||||
|
||||
// Try to build the container
|
||||
let build_result = container.build();
|
||||
|
||||
// Handle the result with improved error context
|
||||
match build_result {
|
||||
Ok(built_container) => {
|
||||
// Container built successfully
|
||||
Ok(built_container)
|
||||
},
|
||||
Err(err) => {
|
||||
// Add more context to the error
|
||||
let enhanced_error = match err {
|
||||
NerdctlError::CommandFailed(msg) => {
|
||||
// Provide more detailed error information
|
||||
let mut enhanced_msg = format!("Failed to build container '{}' from image '{}': {}",
|
||||
container_name, image, msg);
|
||||
|
||||
// Add information about configured options that might be relevant
|
||||
if !ports.is_empty() {
|
||||
enhanced_msg.push_str(&format!("\nConfigured ports: {:?}", ports));
|
||||
}
|
||||
|
||||
if !volumes.is_empty() {
|
||||
enhanced_msg.push_str(&format!("\nConfigured volumes: {:?}", volumes));
|
||||
}
|
||||
|
||||
if !env_vars.is_empty() {
|
||||
enhanced_msg.push_str(&format!("\nConfigured environment variables: {:?}", env_vars));
|
||||
}
|
||||
|
||||
// Add suggestions for common issues
|
||||
if msg.contains("not found") || msg.contains("no such image") {
|
||||
enhanced_msg.push_str("\nSuggestion: The specified image may not exist or may not be pulled yet. Try pulling the image first with nerdctl_image_pull().");
|
||||
} else if msg.contains("port is already allocated") {
|
||||
enhanced_msg.push_str("\nSuggestion: One of the specified ports is already in use. Try using a different port or stopping the container using that port.");
|
||||
} else if msg.contains("permission denied") {
|
||||
enhanced_msg.push_str("\nSuggestion: Permission issues detected. Check if you have the necessary permissions to create containers or access the specified volumes.");
|
||||
}
|
||||
|
||||
NerdctlError::CommandFailed(enhanced_msg)
|
||||
},
|
||||
_ => err
|
||||
};
|
||||
|
||||
nerdctl_error_to_rhai_error(Err(enhanced_error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the Container and verify it's running
|
||||
///
|
||||
/// This function starts the container and verifies that it's actually running.
|
||||
/// It returns detailed error information if the container fails to start or
|
||||
/// if it starts but stops immediately.
|
||||
pub fn container_start(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
// Get container details for better error reporting
|
||||
let container_name = container.name.clone();
|
||||
let container_id = container.container_id.clone().unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
// Try to start the container
|
||||
let start_result = container.start();
|
||||
|
||||
// Handle the result with improved error context
|
||||
match start_result {
|
||||
Ok(result) => {
|
||||
// Container started successfully
|
||||
Ok(result)
|
||||
},
|
||||
Err(err) => {
|
||||
// Add more context to the error
|
||||
let enhanced_error = match err {
|
||||
NerdctlError::CommandFailed(msg) => {
|
||||
// Check if this is a "container already running" error, which is not really an error
|
||||
if msg.contains("already running") {
|
||||
return Ok(CommandResult {
|
||||
stdout: format!("Container {} is already running", container_name),
|
||||
stderr: "".to_string(),
|
||||
success: true,
|
||||
code: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Try to get more information about why the container might have failed to start
|
||||
let mut enhanced_msg = format!("Failed to start container '{}' (ID: {}): {}",
|
||||
container_name, container_id, msg);
|
||||
|
||||
// Try to check if the image exists
|
||||
if let Some(image) = &container.image {
|
||||
enhanced_msg.push_str(&format!("\nContainer was using image: {}", image));
|
||||
}
|
||||
|
||||
NerdctlError::CommandFailed(enhanced_msg)
|
||||
},
|
||||
_ => err
|
||||
};
|
||||
|
||||
nerdctl_error_to_rhai_error(Err(enhanced_error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop the Container
|
||||
pub fn container_stop(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(container.stop())
|
||||
}
|
||||
|
||||
/// Remove the Container
|
||||
pub fn container_remove(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(container.remove())
|
||||
}
|
||||
|
||||
/// Execute a command in the Container
|
||||
pub fn container_exec(container: &mut Container, command: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(container.exec(command))
|
||||
}
|
||||
|
||||
/// Get container logs
|
||||
pub fn container_logs(container: &mut Container) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
// Get container details for better error reporting
|
||||
let container_name = container.name.clone();
|
||||
let container_id = container.container_id.clone().unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
// Use the nerdctl::logs function
|
||||
let logs_result = nerdctl::logs(&container_id);
|
||||
|
||||
match logs_result {
|
||||
Ok(result) => {
|
||||
Ok(result)
|
||||
},
|
||||
Err(err) => {
|
||||
// Add more context to the error
|
||||
let enhanced_error = NerdctlError::CommandFailed(
|
||||
format!("Failed to get logs for container '{}' (ID: {}): {}",
|
||||
container_name, container_id, err)
|
||||
);
|
||||
|
||||
nerdctl_error_to_rhai_error(Err(enhanced_error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy files between the Container and local filesystem
|
||||
pub fn container_copy(container: &mut Container, source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(container.copy(source, dest))
|
||||
}
|
||||
|
||||
/// Create a new Map with default run options
|
||||
pub fn new_run_options() -> Map {
|
||||
let mut map = Map::new();
|
||||
map.insert("name".into(), Dynamic::UNIT);
|
||||
map.insert("detach".into(), Dynamic::from(true));
|
||||
map.insert("ports".into(), Dynamic::from(Array::new()));
|
||||
map.insert("snapshotter".into(), Dynamic::from("native"));
|
||||
map
|
||||
}
|
||||
|
||||
//
|
||||
// Container Function Wrappers
|
||||
//
|
||||
|
||||
/// Wrapper for nerdctl::run
|
||||
///
|
||||
/// Run a container from an image.
|
||||
pub fn nerdctl_run(image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::run(image, None, true, None, None))
|
||||
}
|
||||
|
||||
/// Run a container with a name
|
||||
pub fn nerdctl_run_with_name(image: &str, name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::run(image, Some(name), true, None, None))
|
||||
}
|
||||
|
||||
/// Run a container with a port mapping
|
||||
pub fn nerdctl_run_with_port(image: &str, name: &str, port: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
let ports = vec![port];
|
||||
nerdctl_error_to_rhai_error(nerdctl::run(image, Some(name), true, Some(&ports), None))
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::exec
|
||||
///
|
||||
/// Execute a command in a container.
|
||||
pub fn nerdctl_exec(container: &str, command: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::exec(container, command))
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::copy
|
||||
///
|
||||
/// Copy files between container and local filesystem.
|
||||
pub fn nerdctl_copy(source: &str, dest: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::copy(source, dest))
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::stop
|
||||
///
|
||||
/// Stop a container.
|
||||
pub fn nerdctl_stop(container: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::stop(container))
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::remove
|
||||
///
|
||||
/// Remove a container.
|
||||
pub fn nerdctl_remove(container: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::remove(container))
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::list
|
||||
///
|
||||
/// List containers.
|
||||
pub fn nerdctl_list(all: bool) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::list(all))
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::logs
|
||||
///
|
||||
/// Get container logs.
|
||||
pub fn nerdctl_logs(container: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::logs(container))
|
||||
}
|
||||
|
||||
//
|
||||
// Image Function Wrappers
|
||||
//
|
||||
|
||||
/// Wrapper for nerdctl::images
|
||||
///
|
||||
/// List images in local storage.
|
||||
pub fn nerdctl_images() -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::images())
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::image_remove
|
||||
///
|
||||
/// Remove one or more images.
|
||||
pub fn nerdctl_image_remove(image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::image_remove(image))
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::image_push
|
||||
///
|
||||
/// Push an image to a registry.
|
||||
pub fn nerdctl_image_push(image: &str, destination: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::image_push(image, destination))
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::image_tag
|
||||
///
|
||||
/// Add an additional name to a local image.
|
||||
pub fn nerdctl_image_tag(image: &str, new_name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::image_tag(image, new_name))
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::image_pull
|
||||
///
|
||||
/// Pull an image from a registry.
|
||||
pub fn nerdctl_image_pull(image: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::image_pull(image))
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::image_commit
|
||||
///
|
||||
/// Commit a container to an image.
|
||||
pub fn nerdctl_image_commit(container: &str, image_name: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::image_commit(container, image_name))
|
||||
}
|
||||
|
||||
/// Wrapper for nerdctl::image_build
|
||||
///
|
||||
/// Build an image using a Dockerfile.
|
||||
pub fn nerdctl_image_build(tag: &str, context_path: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
nerdctl_error_to_rhai_error(nerdctl::image_build(tag, context_path))
|
||||
}
|
||||
|
||||
/// Register Nerdctl module functions with the Rhai engine
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `engine` - The Rhai engine to register the functions with
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
|
||||
pub fn register_nerdctl_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register types
|
||||
register_nerdctl_types(engine)?;
|
||||
|
||||
// Register Container constructor
|
||||
engine.register_fn("nerdctl_container_new", container_new);
|
||||
engine.register_fn("nerdctl_container_from_image", container_from_image);
|
||||
|
||||
// Register Container instance methods
|
||||
engine.register_fn("reset", container_reset);
|
||||
engine.register_fn("with_port", container_with_port);
|
||||
engine.register_fn("with_volume", container_with_volume);
|
||||
engine.register_fn("with_env", container_with_env);
|
||||
engine.register_fn("with_network", container_with_network);
|
||||
engine.register_fn("with_network_alias", container_with_network_alias);
|
||||
engine.register_fn("with_cpu_limit", container_with_cpu_limit);
|
||||
engine.register_fn("with_memory_limit", container_with_memory_limit);
|
||||
engine.register_fn("with_restart_policy", container_with_restart_policy);
|
||||
engine.register_fn("with_health_check", container_with_health_check);
|
||||
engine.register_fn("with_ports", container_with_ports);
|
||||
engine.register_fn("with_volumes", container_with_volumes);
|
||||
engine.register_fn("with_envs", container_with_envs);
|
||||
engine.register_fn("with_network_aliases", container_with_network_aliases);
|
||||
engine.register_fn("with_memory_swap_limit", container_with_memory_swap_limit);
|
||||
engine.register_fn("with_cpu_shares", container_with_cpu_shares);
|
||||
engine.register_fn("with_health_check_options", container_with_health_check_options);
|
||||
engine.register_fn("with_snapshotter", container_with_snapshotter);
|
||||
engine.register_fn("with_detach", container_with_detach);
|
||||
engine.register_fn("build", container_build);
|
||||
engine.register_fn("start", container_start);
|
||||
engine.register_fn("stop", container_stop);
|
||||
engine.register_fn("remove", container_remove);
|
||||
engine.register_fn("exec", container_exec);
|
||||
engine.register_fn("logs", container_logs);
|
||||
engine.register_fn("copy", container_copy);
|
||||
|
||||
// Register legacy container functions (for backward compatibility)
|
||||
engine.register_fn("nerdctl_run", nerdctl_run);
|
||||
engine.register_fn("nerdctl_run_with_name", nerdctl_run_with_name);
|
||||
engine.register_fn("nerdctl_run_with_port", nerdctl_run_with_port);
|
||||
engine.register_fn("new_run_options", new_run_options);
|
||||
engine.register_fn("nerdctl_exec", nerdctl_exec);
|
||||
engine.register_fn("nerdctl_copy", nerdctl_copy);
|
||||
engine.register_fn("nerdctl_stop", nerdctl_stop);
|
||||
engine.register_fn("nerdctl_remove", nerdctl_remove);
|
||||
engine.register_fn("nerdctl_list", nerdctl_list);
|
||||
engine.register_fn("nerdctl_logs", nerdctl_logs);
|
||||
|
||||
// Register image functions
|
||||
engine.register_fn("nerdctl_images", nerdctl_images);
|
||||
engine.register_fn("nerdctl_image_remove", nerdctl_image_remove);
|
||||
engine.register_fn("nerdctl_image_push", nerdctl_image_push);
|
||||
engine.register_fn("nerdctl_image_tag", nerdctl_image_tag);
|
||||
engine.register_fn("nerdctl_image_pull", nerdctl_image_pull);
|
||||
engine.register_fn("nerdctl_image_commit", nerdctl_image_commit);
|
||||
engine.register_fn("nerdctl_image_build", nerdctl_image_build);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register Nerdctl module types with the Rhai engine
|
||||
fn register_nerdctl_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register Container type
|
||||
engine.register_type_with_name::<Container>("NerdctlContainer");
|
||||
|
||||
// Register getters for Container properties
|
||||
engine.register_get("name", |container: &mut Container| container.name.clone());
|
||||
engine.register_get("container_id", |container: &mut Container| {
|
||||
match &container.container_id {
|
||||
Some(id) => id.clone(),
|
||||
None => "".to_string(),
|
||||
}
|
||||
});
|
||||
engine.register_get("image", |container: &mut Container| {
|
||||
match &container.image {
|
||||
Some(img) => img.clone(),
|
||||
None => "".to_string(),
|
||||
}
|
||||
});
|
||||
engine.register_get("ports", |container: &mut Container| {
|
||||
let mut array = Array::new();
|
||||
for port in &container.ports {
|
||||
array.push(Dynamic::from(port.clone()));
|
||||
}
|
||||
array
|
||||
});
|
||||
engine.register_get("volumes", |container: &mut Container| {
|
||||
let mut array = Array::new();
|
||||
for volume in &container.volumes {
|
||||
array.push(Dynamic::from(volume.clone()));
|
||||
}
|
||||
array
|
||||
});
|
||||
engine.register_get("detach", |container: &mut Container| container.detach);
|
||||
|
||||
// Register Image type and methods
|
||||
engine.register_type_with_name::<Image>("NerdctlImage");
|
||||
|
||||
// Register getters for Image properties
|
||||
engine.register_get("id", |img: &mut Image| img.id.clone());
|
||||
engine.register_get("repository", |img: &mut Image| img.repository.clone());
|
||||
engine.register_get("tag", |img: &mut Image| img.tag.clone());
|
||||
engine.register_get("size", |img: &mut Image| img.size.clone());
|
||||
engine.register_get("created", |img: &mut Image| img.created.clone());
|
||||
|
||||
Ok(())
|
||||
}
|
348
src/rhai/os.rs
348
src/rhai/os.rs
@ -1,348 +0,0 @@
|
||||
//! Rhai wrappers for OS module functions
|
||||
//!
|
||||
//! This module provides Rhai wrappers for the functions in the OS module.
|
||||
|
||||
use rhai::{Engine, EvalAltResult, Array};
|
||||
use crate::os;
|
||||
use crate::os::package::PackHero;
|
||||
use super::error::{ToRhaiError, register_error_types};
|
||||
|
||||
/// Register OS module functions with the Rhai engine
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `engine` - The Rhai engine to register the functions with
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
|
||||
pub fn register_os_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register error types
|
||||
register_error_types(engine)?;
|
||||
|
||||
// Register file system functions
|
||||
engine.register_fn("copy", copy);
|
||||
engine.register_fn("exist", exist);
|
||||
engine.register_fn("find_file", find_file);
|
||||
engine.register_fn("find_files", find_files);
|
||||
engine.register_fn("find_dir", find_dir);
|
||||
engine.register_fn("find_dirs", find_dirs);
|
||||
engine.register_fn("delete", delete);
|
||||
engine.register_fn("mkdir", mkdir);
|
||||
engine.register_fn("file_size", file_size);
|
||||
engine.register_fn("rsync", rsync);
|
||||
engine.register_fn("chdir", chdir);
|
||||
engine.register_fn("file_read", file_read);
|
||||
engine.register_fn("file_write", file_write);
|
||||
engine.register_fn("file_write_append", file_write_append);
|
||||
|
||||
// Register command check functions
|
||||
engine.register_fn("which", which);
|
||||
engine.register_fn("cmd_ensure_exists", cmd_ensure_exists);
|
||||
|
||||
// Register download functions
|
||||
engine.register_fn("download", download);
|
||||
engine.register_fn("download_file", download_file);
|
||||
engine.register_fn("download_install", download_install);
|
||||
engine.register_fn("chmod_exec", chmod_exec);
|
||||
|
||||
// Register move function
|
||||
engine.register_fn("mv", mv);
|
||||
|
||||
// Register package management functions
|
||||
engine.register_fn("package_install", package_install);
|
||||
engine.register_fn("package_remove", package_remove);
|
||||
engine.register_fn("package_update", package_update);
|
||||
engine.register_fn("package_upgrade", package_upgrade);
|
||||
engine.register_fn("package_list", package_list);
|
||||
engine.register_fn("package_search", package_search);
|
||||
engine.register_fn("package_is_installed", package_is_installed);
|
||||
engine.register_fn("package_set_debug", package_set_debug);
|
||||
engine.register_fn("package_platform", package_platform);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
//
|
||||
// File System Function Wrappers
|
||||
//
|
||||
|
||||
/// Wrapper for os::copy
|
||||
///
|
||||
/// Recursively copy a file or directory from source to destination.
|
||||
pub fn copy(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::copy(src, dest).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::exist
|
||||
///
|
||||
/// Check if a file or directory exists.
|
||||
pub fn exist(path: &str) -> bool {
|
||||
os::exist(path)
|
||||
}
|
||||
|
||||
/// Wrapper for os::find_file
|
||||
///
|
||||
/// Find a file in a directory (with support for wildcards).
|
||||
pub fn find_file(dir: &str, filename: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::find_file(dir, filename).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::find_files
|
||||
///
|
||||
/// Find multiple files in a directory (recursive, with support for wildcards).
|
||||
pub fn find_files(dir: &str, filename: &str) -> Result<Array, Box<EvalAltResult>> {
|
||||
let files = os::find_files(dir, filename).to_rhai_error()?;
|
||||
|
||||
// Convert Vec<String> to Rhai Array
|
||||
let mut array = Array::new();
|
||||
for file in files {
|
||||
array.push(file.into());
|
||||
}
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
/// Wrapper for os::find_dir
|
||||
///
|
||||
/// Find a directory in a parent directory (with support for wildcards).
|
||||
pub fn find_dir(dir: &str, dirname: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::find_dir(dir, dirname).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::find_dirs
|
||||
///
|
||||
/// Find multiple directories in a parent directory (recursive, with support for wildcards).
|
||||
pub fn find_dirs(dir: &str, dirname: &str) -> Result<Array, Box<EvalAltResult>> {
|
||||
let dirs = os::find_dirs(dir, dirname).to_rhai_error()?;
|
||||
|
||||
// Convert Vec<String> to Rhai Array
|
||||
let mut array = Array::new();
|
||||
for dir in dirs {
|
||||
array.push(dir.into());
|
||||
}
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
/// Wrapper for os::delete
|
||||
///
|
||||
/// Delete a file or directory (defensive - doesn't error if file doesn't exist).
|
||||
pub fn delete(path: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::delete(path).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::mkdir
|
||||
///
|
||||
/// Create a directory and all parent directories (defensive - doesn't error if directory exists).
|
||||
pub fn mkdir(path: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::mkdir(path).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::file_size
|
||||
///
|
||||
/// Get the size of a file in bytes.
|
||||
pub fn file_size(path: &str) -> Result<i64, Box<EvalAltResult>> {
|
||||
os::file_size(path).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::rsync
|
||||
///
|
||||
/// Sync directories using rsync (or platform equivalent).
|
||||
pub fn rsync(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::rsync(src, dest).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::chdir
|
||||
///
|
||||
/// Change the current working directory.
|
||||
pub fn chdir(path: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::chdir(path).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::file_read
|
||||
///
|
||||
/// Read the contents of a file.
|
||||
pub fn file_read(path: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::file_read(path).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::file_write
|
||||
///
|
||||
/// Write content to a file (creates the file if it doesn't exist, overwrites if it does).
|
||||
pub fn file_write(path: &str, content: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::file_write(path, content).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::file_write_append
|
||||
///
|
||||
/// Append content to a file (creates the file if it doesn't exist).
|
||||
pub fn file_write_append(path: &str, content: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::file_write_append(path, content).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::mv
|
||||
///
|
||||
/// Move a file or directory from source to destination.
|
||||
pub fn mv(src: &str, dest: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::mv(src, dest).to_rhai_error()
|
||||
}
|
||||
|
||||
//
|
||||
// Download Function Wrappers
|
||||
//
|
||||
|
||||
/// Wrapper for os::download
|
||||
///
|
||||
/// Download a file from URL to destination using the curl command.
|
||||
pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Box<EvalAltResult>> {
|
||||
os::download(url, dest, min_size_kb).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::download_file
|
||||
///
|
||||
/// Download a file from URL to a specific file destination using the curl command.
|
||||
pub fn download_file(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Box<EvalAltResult>> {
|
||||
os::download_file(url, dest, min_size_kb).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::download_install
|
||||
///
|
||||
/// Download a file and install it if it's a supported package format.
|
||||
pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, Box<EvalAltResult>> {
|
||||
os::download_install(url, min_size_kb).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::chmod_exec
|
||||
///
|
||||
/// Make a file executable (equivalent to chmod +x).
|
||||
pub fn chmod_exec(path: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::chmod_exec(path).to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::which
|
||||
///
|
||||
/// Check if a command exists in the system PATH.
|
||||
pub fn which(command: &str) -> String {
|
||||
os::which(command)
|
||||
}
|
||||
|
||||
/// Wrapper for os::cmd_ensure_exists
|
||||
///
|
||||
/// Ensure that one or more commands exist in the system PATH.
|
||||
/// If any command doesn't exist, an error is thrown.
|
||||
pub fn cmd_ensure_exists(commands: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
os::cmd_ensure_exists(commands).to_rhai_error()
|
||||
}
|
||||
|
||||
//
|
||||
// Package Management Function Wrappers
|
||||
//
|
||||
|
||||
/// Wrapper for os::package::PackHero::install
|
||||
///
|
||||
/// Install a package using the system package manager.
|
||||
pub fn package_install(package: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
let hero = PackHero::new();
|
||||
hero.install(package)
|
||||
.map(|_| format!("Package '{}' installed successfully", package))
|
||||
.to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::package::PackHero::remove
|
||||
///
|
||||
/// Remove a package using the system package manager.
|
||||
pub fn package_remove(package: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
let hero = PackHero::new();
|
||||
hero.remove(package)
|
||||
.map(|_| format!("Package '{}' removed successfully", package))
|
||||
.to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::package::PackHero::update
|
||||
///
|
||||
/// Update package lists using the system package manager.
|
||||
pub fn package_update() -> Result<String, Box<EvalAltResult>> {
|
||||
let hero = PackHero::new();
|
||||
hero.update()
|
||||
.map(|_| "Package lists updated successfully".to_string())
|
||||
.to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::package::PackHero::upgrade
|
||||
///
|
||||
/// Upgrade installed packages using the system package manager.
|
||||
pub fn package_upgrade() -> Result<String, Box<EvalAltResult>> {
|
||||
let hero = PackHero::new();
|
||||
hero.upgrade()
|
||||
.map(|_| "Packages upgraded successfully".to_string())
|
||||
.to_rhai_error()
|
||||
}
|
||||
|
||||
/// Wrapper for os::package::PackHero::list_installed
|
||||
///
|
||||
/// List installed packages using the system package manager.
|
||||
pub fn package_list() -> Result<Array, Box<EvalAltResult>> {
|
||||
let hero = PackHero::new();
|
||||
let packages = hero.list_installed().to_rhai_error()?;
|
||||
|
||||
// Convert Vec<String> to Rhai Array
|
||||
let mut array = Array::new();
|
||||
for package in packages {
|
||||
array.push(package.into());
|
||||
}
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
/// Wrapper for os::package::PackHero::search
|
||||
///
|
||||
/// Search for packages using the system package manager.
|
||||
pub fn package_search(query: &str) -> Result<Array, Box<EvalAltResult>> {
|
||||
let hero = PackHero::new();
|
||||
let packages = hero.search(query).to_rhai_error()?;
|
||||
|
||||
// Convert Vec<String> to Rhai Array
|
||||
let mut array = Array::new();
|
||||
for package in packages {
|
||||
array.push(package.into());
|
||||
}
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
/// Wrapper for os::package::PackHero::is_installed
|
||||
///
|
||||
/// Check if a package is installed using the system package manager.
|
||||
pub fn package_is_installed(package: &str) -> Result<bool, Box<EvalAltResult>> {
|
||||
let hero = PackHero::new();
|
||||
hero.is_installed(package).to_rhai_error()
|
||||
}
|
||||
|
||||
// Thread-local storage for package debug flag
|
||||
thread_local! {
|
||||
static PACKAGE_DEBUG: std::cell::RefCell<bool> = std::cell::RefCell::new(false);
|
||||
}
|
||||
|
||||
/// Set the debug mode for package management operations
|
||||
pub fn package_set_debug(debug: bool) -> bool {
|
||||
let mut hero = PackHero::new();
|
||||
hero.set_debug(debug);
|
||||
|
||||
// Also set the thread-local debug flag
|
||||
PACKAGE_DEBUG.with(|cell| {
|
||||
*cell.borrow_mut() = debug;
|
||||
});
|
||||
|
||||
debug
|
||||
}
|
||||
|
||||
/// Get the current platform name for package management
|
||||
pub fn package_platform() -> String {
|
||||
let hero = PackHero::new();
|
||||
match hero.platform() {
|
||||
os::package::Platform::Ubuntu => "Ubuntu".to_string(),
|
||||
os::package::Platform::MacOS => "MacOS".to_string(),
|
||||
os::package::Platform::Unknown => "Unknown".to_string(),
|
||||
}
|
||||
}
|
@ -1,191 +0,0 @@
|
||||
//! Rhai wrappers for Process module functions
|
||||
//!
|
||||
//! This module provides Rhai wrappers for the functions in the Process module.
|
||||
|
||||
use rhai::{Engine, EvalAltResult, Array, Dynamic, Map};
|
||||
use crate::process::{self, CommandResult, ProcessInfo, RunError, ProcessError};
|
||||
|
||||
/// Register Process module functions with the Rhai engine
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `engine` - The Rhai engine to register the functions with
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
|
||||
pub fn register_process_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register types
|
||||
register_process_types(engine)?;
|
||||
|
||||
// Register run functions
|
||||
engine.register_fn("run", run);
|
||||
engine.register_fn("run_silent", run_silent);
|
||||
engine.register_fn("run_with_options", run_with_options);
|
||||
engine.register_fn("new_run_options", new_run_options);
|
||||
|
||||
// Register process management functions
|
||||
engine.register_fn("which", which);
|
||||
engine.register_fn("kill", kill);
|
||||
engine.register_fn("process_list", process_list);
|
||||
engine.register_fn("process_get", process_get);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register Process module types with the Rhai engine
|
||||
fn register_process_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register CommandResult type and methods
|
||||
engine.register_type_with_name::<CommandResult>("CommandResult");
|
||||
|
||||
// Register getters for CommandResult properties
|
||||
engine.register_get("stdout", |r: &mut CommandResult| r.stdout.clone());
|
||||
engine.register_get("stderr", |r: &mut CommandResult| r.stderr.clone());
|
||||
engine.register_get("success", |r: &mut CommandResult| r.success);
|
||||
engine.register_get("code", |r: &mut CommandResult| r.code);
|
||||
|
||||
// Register ProcessInfo type and methods
|
||||
engine.register_type_with_name::<ProcessInfo>("ProcessInfo");
|
||||
|
||||
// Register getters for ProcessInfo properties
|
||||
engine.register_get("pid", |p: &mut ProcessInfo| p.pid);
|
||||
engine.register_get("name", |p: &mut ProcessInfo| p.name.clone());
|
||||
engine.register_get("memory", |p: &mut ProcessInfo| p.memory);
|
||||
engine.register_get("cpu", |p: &mut ProcessInfo| p.cpu);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper functions for error conversion
|
||||
fn run_error_to_rhai_error<T>(result: Result<T, RunError>) -> Result<T, Box<EvalAltResult>> {
|
||||
result.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Run error: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn process_error_to_rhai_error<T>(result: Result<T, ProcessError>) -> Result<T, Box<EvalAltResult>> {
|
||||
result.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Process error: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new Map with default run options
|
||||
pub fn new_run_options() -> Map {
|
||||
let mut map = Map::new();
|
||||
map.insert("die".into(), Dynamic::from(true));
|
||||
map.insert("silent".into(), Dynamic::from(false));
|
||||
map.insert("async_exec".into(), Dynamic::from(false));
|
||||
map.insert("log".into(), Dynamic::from(false));
|
||||
map
|
||||
}
|
||||
|
||||
//
|
||||
// Run Function Wrappers
|
||||
//
|
||||
|
||||
/// Wrapper for process::run_command
|
||||
///
|
||||
/// Run a command or multiline script with arguments.
|
||||
pub fn run(command: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
run_error_to_rhai_error(process::run_command(command))
|
||||
}
|
||||
|
||||
/// Wrapper for process::run_silent
|
||||
///
|
||||
/// Run a command or multiline script with arguments silently.
|
||||
pub fn run_silent(command: &str) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
run_error_to_rhai_error(process::run_silent(command))
|
||||
}
|
||||
|
||||
/// Run a command with options specified in a Map
|
||||
///
|
||||
/// This provides a builder-style interface for Rhai scripts.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rhai
|
||||
/// let options = new_run_options();
|
||||
/// options.die = false;
|
||||
/// options.silent = true;
|
||||
/// let result = run("echo Hello", options);
|
||||
/// ```
|
||||
pub fn run_with_options(command: &str, options: Map) -> Result<CommandResult, Box<EvalAltResult>> {
|
||||
let mut builder = process::run(command);
|
||||
|
||||
// Apply options from the map
|
||||
if let Some(die) = options.get("die") {
|
||||
if let Ok(die_val) = die.clone().as_bool() {
|
||||
builder = builder.die(die_val);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(silent) = options.get("silent") {
|
||||
if let Ok(silent_val) = silent.clone().as_bool() {
|
||||
builder = builder.silent(silent_val);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(async_exec) = options.get("async_exec") {
|
||||
if let Ok(async_val) = async_exec.clone().as_bool() {
|
||||
builder = builder.async_exec(async_val);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(log) = options.get("log") {
|
||||
if let Ok(log_val) = log.clone().as_bool() {
|
||||
builder = builder.log(log_val);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the command
|
||||
run_error_to_rhai_error(builder.execute())
|
||||
}
|
||||
|
||||
//
|
||||
// Process Management Function Wrappers
|
||||
//
|
||||
|
||||
/// Wrapper for process::which
|
||||
///
|
||||
/// Check if a command exists in PATH.
|
||||
pub fn which(cmd: &str) -> Dynamic {
|
||||
match process::which(cmd) {
|
||||
Some(path) => path.into(),
|
||||
None => Dynamic::UNIT
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for process::kill
|
||||
///
|
||||
/// Kill processes matching a pattern.
|
||||
pub fn kill(pattern: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
process_error_to_rhai_error(process::kill(pattern))
|
||||
}
|
||||
|
||||
/// Wrapper for process::process_list
|
||||
///
|
||||
/// List processes matching a pattern (or all if pattern is empty).
|
||||
pub fn process_list(pattern: &str) -> Result<Array, Box<EvalAltResult>> {
|
||||
let processes = process_error_to_rhai_error(process::process_list(pattern))?;
|
||||
|
||||
// Convert Vec<ProcessInfo> to Rhai Array
|
||||
let mut array = Array::new();
|
||||
for process in processes {
|
||||
array.push(Dynamic::from(process));
|
||||
}
|
||||
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
/// Wrapper for process::process_get
|
||||
///
|
||||
/// Get a single process matching the pattern (error if 0 or more than 1 match).
|
||||
pub fn process_get(pattern: &str) -> Result<ProcessInfo, Box<EvalAltResult>> {
|
||||
process_error_to_rhai_error(process::process_get(pattern))
|
||||
}
|
292
src/rhai/rfs.rs
292
src/rhai/rfs.rs
@ -1,292 +0,0 @@
|
||||
use rhai::{Engine, EvalAltResult, Map, Array};
|
||||
use crate::virt::rfs::{
|
||||
RfsBuilder, MountType, StoreSpec,
|
||||
list_mounts, unmount_all, unmount, get_mount_info,
|
||||
pack_directory, unpack, list_contents, verify
|
||||
};
|
||||
|
||||
/// Register RFS functions with the Rhai engine
|
||||
pub fn register(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register mount functions
|
||||
engine.register_fn("rfs_mount", rfs_mount);
|
||||
engine.register_fn("rfs_unmount", rfs_unmount);
|
||||
engine.register_fn("rfs_list_mounts", rfs_list_mounts);
|
||||
engine.register_fn("rfs_unmount_all", rfs_unmount_all);
|
||||
engine.register_fn("rfs_get_mount_info", rfs_get_mount_info);
|
||||
|
||||
// Register pack functions
|
||||
engine.register_fn("rfs_pack", rfs_pack);
|
||||
engine.register_fn("rfs_unpack", rfs_unpack);
|
||||
engine.register_fn("rfs_list_contents", rfs_list_contents);
|
||||
engine.register_fn("rfs_verify", rfs_verify);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mount a filesystem
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `source` - Source path or URL
|
||||
/// * `target` - Target mount point
|
||||
/// * `mount_type` - Mount type (e.g., "local", "ssh", "s3", "webdav")
|
||||
/// * `options` - Mount options as a map
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Map, Box<EvalAltResult>>` - Mount information or error
|
||||
fn rfs_mount(source: &str, target: &str, mount_type: &str, options: Map) -> Result<Map, Box<EvalAltResult>> {
|
||||
// Convert mount type string to MountType enum
|
||||
let mount_type_enum = MountType::from_string(mount_type);
|
||||
|
||||
// Create a builder
|
||||
let mut builder = RfsBuilder::new(source, target, mount_type_enum);
|
||||
|
||||
// Add options
|
||||
for (key, value) in options.iter() {
|
||||
if let Ok(value_str) = value.clone().into_string() {
|
||||
builder = builder.with_option(key, &value_str);
|
||||
}
|
||||
}
|
||||
|
||||
// Mount the filesystem
|
||||
let mount = builder.mount()
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to mount filesystem: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
)))?;
|
||||
|
||||
// Convert Mount to Map
|
||||
let mut result = Map::new();
|
||||
result.insert("id".into(), mount.id.into());
|
||||
result.insert("source".into(), mount.source.into());
|
||||
result.insert("target".into(), mount.target.into());
|
||||
result.insert("fs_type".into(), mount.fs_type.into());
|
||||
|
||||
let options_array: Array = mount.options.iter()
|
||||
.map(|opt| opt.clone().into())
|
||||
.collect();
|
||||
result.insert("options".into(), options_array.into());
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Unmount a filesystem
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `target` - Target mount point
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), Box<EvalAltResult>>` - Success or error
|
||||
fn rfs_unmount(target: &str) -> Result<(), Box<EvalAltResult>> {
|
||||
unmount(target)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to unmount filesystem: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
)))
|
||||
}
|
||||
|
||||
/// List all mounted filesystems
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Array, Box<EvalAltResult>>` - List of mounts or error
|
||||
fn rfs_list_mounts() -> Result<Array, Box<EvalAltResult>> {
|
||||
let mounts = list_mounts()
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to list mounts: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
)))?;
|
||||
|
||||
let mut result = Array::new();
|
||||
|
||||
for mount in mounts {
|
||||
let mut mount_map = Map::new();
|
||||
mount_map.insert("id".into(), mount.id.into());
|
||||
mount_map.insert("source".into(), mount.source.into());
|
||||
mount_map.insert("target".into(), mount.target.into());
|
||||
mount_map.insert("fs_type".into(), mount.fs_type.into());
|
||||
|
||||
let options_array: Array = mount.options.iter()
|
||||
.map(|opt| opt.clone().into())
|
||||
.collect();
|
||||
mount_map.insert("options".into(), options_array.into());
|
||||
|
||||
result.push(mount_map.into());
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Unmount all filesystems
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), Box<EvalAltResult>>` - Success or error
|
||||
fn rfs_unmount_all() -> Result<(), Box<EvalAltResult>> {
|
||||
unmount_all()
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to unmount all filesystems: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
)))
|
||||
}
|
||||
|
||||
/// Get information about a mounted filesystem
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `target` - Target mount point
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Map, Box<EvalAltResult>>` - Mount information or error
|
||||
fn rfs_get_mount_info(target: &str) -> Result<Map, Box<EvalAltResult>> {
|
||||
let mount = get_mount_info(target)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to get mount info: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
)))?;
|
||||
|
||||
let mut result = Map::new();
|
||||
result.insert("id".into(), mount.id.into());
|
||||
result.insert("source".into(), mount.source.into());
|
||||
result.insert("target".into(), mount.target.into());
|
||||
result.insert("fs_type".into(), mount.fs_type.into());
|
||||
|
||||
let options_array: Array = mount.options.iter()
|
||||
.map(|opt| opt.clone().into())
|
||||
.collect();
|
||||
result.insert("options".into(), options_array.into());
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Pack a directory into a filesystem layer
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `directory` - Directory to pack
|
||||
/// * `output` - Output file
|
||||
/// * `store_specs` - Store specifications as a string (e.g., "file:path=/path/to/store,s3:bucket=my-bucket")
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), Box<EvalAltResult>>` - Success or error
|
||||
fn rfs_pack(directory: &str, output: &str, store_specs: &str) -> Result<(), Box<EvalAltResult>> {
|
||||
// Parse store specs
|
||||
let specs = parse_store_specs(store_specs);
|
||||
|
||||
// Pack the directory
|
||||
pack_directory(directory, output, &specs)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to pack directory: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
)))
|
||||
}
|
||||
|
||||
/// Unpack a filesystem layer
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `input` - Input file
|
||||
/// * `directory` - Directory to unpack to
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), Box<EvalAltResult>>` - Success or error
|
||||
fn rfs_unpack(input: &str, directory: &str) -> Result<(), Box<EvalAltResult>> {
|
||||
unpack(input, directory)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to unpack filesystem layer: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
)))
|
||||
}
|
||||
|
||||
/// List the contents of a filesystem layer
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `input` - Input file
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<String, Box<EvalAltResult>>` - File listing or error
|
||||
fn rfs_list_contents(input: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
list_contents(input)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to list contents: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
)))
|
||||
}
|
||||
|
||||
/// Verify a filesystem layer
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `input` - Input file
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<bool, Box<EvalAltResult>>` - Whether the layer is valid or error
|
||||
fn rfs_verify(input: &str) -> Result<bool, Box<EvalAltResult>> {
|
||||
verify(input)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to verify filesystem layer: {}", e).into(),
|
||||
rhai::Position::NONE
|
||||
)))
|
||||
}
|
||||
|
||||
/// Parse store specifications from a string
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `specs_str` - Store specifications as a string
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Vec<StoreSpec>` - Store specifications
|
||||
fn parse_store_specs(specs_str: &str) -> Vec<StoreSpec> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
// Split by comma
|
||||
for spec_str in specs_str.split(',') {
|
||||
// Skip empty specs
|
||||
if spec_str.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Split by colon to get type and options
|
||||
let parts: Vec<&str> = spec_str.split(':').collect();
|
||||
|
||||
if parts.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get spec type
|
||||
let spec_type = parts[0].trim();
|
||||
|
||||
// Create store spec
|
||||
let mut store_spec = StoreSpec::new(spec_type);
|
||||
|
||||
// Add options if any
|
||||
if parts.len() > 1 {
|
||||
let options_str = parts[1];
|
||||
|
||||
// Split options by comma
|
||||
for option in options_str.split(',') {
|
||||
// Split option by equals sign
|
||||
let option_parts: Vec<&str> = option.split('=').collect();
|
||||
|
||||
if option_parts.len() == 2 {
|
||||
store_spec = store_spec.with_option(option_parts[0].trim(), option_parts[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.push(store_spec);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
@ -1,254 +0,0 @@
|
||||
//! Tests for Rhai integration
|
||||
//!
|
||||
//! This module contains tests for the Rhai integration.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rhai::Engine;
|
||||
use super::super::register;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn test_register() {
|
||||
let mut engine = Engine::new();
|
||||
assert!(register(&mut engine).is_ok());
|
||||
}
|
||||
|
||||
// OS Module Tests
|
||||
|
||||
#[test]
|
||||
fn test_exist_function() {
|
||||
let mut engine = Engine::new();
|
||||
register(&mut engine).unwrap();
|
||||
|
||||
// Test with a file that definitely exists
|
||||
let result = engine.eval::<bool>(r#"exist("Cargo.toml")"#).unwrap();
|
||||
assert!(result);
|
||||
|
||||
// Test with a file that definitely doesn't exist
|
||||
let result = engine.eval::<bool>(r#"exist("non_existent_file.xyz")"#).unwrap();
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mkdir_and_delete() {
|
||||
let mut engine = Engine::new();
|
||||
register(&mut engine).unwrap();
|
||||
|
||||
let test_dir = "test_rhai_dir";
|
||||
|
||||
// Clean up from previous test runs if necessary
|
||||
if Path::new(test_dir).exists() {
|
||||
fs::remove_dir_all(test_dir).unwrap();
|
||||
}
|
||||
|
||||
// Create directory using Rhai
|
||||
let script = format!(r#"mkdir("{}")"#, test_dir);
|
||||
let result = engine.eval::<String>(&script).unwrap();
|
||||
assert!(result.contains("Successfully created directory"));
|
||||
assert!(Path::new(test_dir).exists());
|
||||
|
||||
// Delete directory using Rhai
|
||||
let script = format!(r#"delete("{}")"#, test_dir);
|
||||
let result = engine.eval::<String>(&script).unwrap();
|
||||
assert!(result.contains("Successfully deleted directory"));
|
||||
assert!(!Path::new(test_dir).exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_size() {
|
||||
let mut engine = Engine::new();
|
||||
register(&mut engine).unwrap();
|
||||
|
||||
// Create a test file
|
||||
let test_file = "test_rhai_file.txt";
|
||||
let test_content = "Hello, Rhai!";
|
||||
fs::write(test_file, test_content).unwrap();
|
||||
|
||||
// Get file size using Rhai
|
||||
let script = format!(r#"file_size("{}")"#, test_file);
|
||||
let size = engine.eval::<i64>(&script).unwrap();
|
||||
assert_eq!(size, test_content.len() as i64);
|
||||
|
||||
// Clean up
|
||||
fs::remove_file(test_file).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_handling() {
|
||||
let mut engine = Engine::new();
|
||||
register(&mut engine).unwrap();
|
||||
|
||||
// Try to get the size of a non-existent file
|
||||
let result = engine.eval::<i64>(r#"file_size("non_existent_file.xyz")"#);
|
||||
assert!(result.is_err());
|
||||
|
||||
let err = result.unwrap_err();
|
||||
let err_str = err.to_string();
|
||||
println!("Error string: {}", err_str);
|
||||
// The actual error message is "No files found matching..."
|
||||
assert!(err_str.contains("No files found matching") ||
|
||||
err_str.contains("File not found") ||
|
||||
err_str.contains("File system error"));
|
||||
}
|
||||
|
||||
// Process Module Tests
|
||||
|
||||
#[test]
|
||||
fn test_which_function() {
|
||||
let mut engine = Engine::new();
|
||||
register(&mut engine).unwrap();
|
||||
|
||||
// Test with a command that definitely exists (like "ls" on Unix or "cmd" on Windows)
|
||||
#[cfg(target_os = "windows")]
|
||||
let cmd = "cmd";
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
let cmd = "ls";
|
||||
|
||||
let script = format!(r#"which("{}")"#, cmd);
|
||||
let result = engine.eval::<String>(&script).unwrap();
|
||||
assert!(!result.is_empty());
|
||||
|
||||
// Test with a command that definitely doesn't exist
|
||||
let script = r#"which("non_existent_command_xyz123")"#;
|
||||
let result = engine.eval::<()>(&script).unwrap();
|
||||
assert_eq!(result, ());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_with_options() {
|
||||
let mut engine = Engine::new();
|
||||
register(&mut engine).unwrap();
|
||||
|
||||
// Test running a command with custom options
|
||||
#[cfg(target_os = "windows")]
|
||||
let script = r#"
|
||||
let options = new_run_options();
|
||||
options["die"] = true;
|
||||
options["silent"] = false;
|
||||
options["log"] = true;
|
||||
let result = run("echo Hello World", options);
|
||||
result.success && result.stdout.contains("Hello World")
|
||||
"#;
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
let script = r#"
|
||||
let options = new_run_options();
|
||||
options["die"] = true;
|
||||
options["silent"] = false;
|
||||
options["log"] = true;
|
||||
let result = run("echo 'Hello World'", options);
|
||||
result.success && result.stdout.contains("Hello World")
|
||||
"#;
|
||||
|
||||
let result = engine.eval::<bool>(script).unwrap();
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_command() {
|
||||
let mut engine = Engine::new();
|
||||
register(&mut engine).unwrap();
|
||||
|
||||
// Test a simple echo command
|
||||
#[cfg(target_os = "windows")]
|
||||
let script = r#"
|
||||
let result = run_command("echo Hello World");
|
||||
result.success && result.stdout.contains("Hello World")
|
||||
"#;
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
let script = r#"
|
||||
let result = run_command("echo 'Hello World'");
|
||||
result.success && result.stdout.contains("Hello World")
|
||||
"#;
|
||||
|
||||
let result = engine.eval::<bool>(script).unwrap();
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_silent() {
|
||||
let mut engine = Engine::new();
|
||||
register(&mut engine).unwrap();
|
||||
|
||||
// Test a simple echo command with silent execution
|
||||
#[cfg(target_os = "windows")]
|
||||
let script = r#"
|
||||
let result = run_silent("echo Hello World");
|
||||
result.success && result.stdout.contains("Hello World")
|
||||
"#;
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
let script = r#"
|
||||
let result = run_silent("echo 'Hello World'");
|
||||
result.success && result.stdout.contains("Hello World")
|
||||
"#;
|
||||
|
||||
let result = engine.eval::<bool>(script).unwrap();
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_list() {
|
||||
let mut engine = Engine::new();
|
||||
register(&mut engine).unwrap();
|
||||
|
||||
// Test listing processes (should return a non-empty array)
|
||||
let script = r#"
|
||||
let processes = process_list("");
|
||||
processes.len() > 0
|
||||
"#;
|
||||
|
||||
let result = engine.eval::<bool>(script).unwrap();
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
// Git Module Tests
|
||||
|
||||
#[test]
|
||||
fn test_git_module_registration() {
|
||||
let mut engine = Engine::new();
|
||||
register(&mut engine).unwrap();
|
||||
|
||||
// Test that git functions are registered
|
||||
let script = r#"
|
||||
// Check if git_clone function exists
|
||||
let fn_exists = is_def_fn("git_clone");
|
||||
fn_exists
|
||||
"#;
|
||||
|
||||
let result = engine.eval::<bool>(script).unwrap();
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_git_parse_url() {
|
||||
let mut engine = Engine::new();
|
||||
register(&mut engine).unwrap();
|
||||
|
||||
// Test parsing a git URL
|
||||
let script = r#"
|
||||
// We can't directly test git_clone without actually cloning,
|
||||
// but we can test that the function exists and doesn't error
|
||||
// when called with invalid parameters
|
||||
|
||||
let result = false;
|
||||
|
||||
try {
|
||||
// This should fail but not crash
|
||||
git_clone("invalid-url");
|
||||
} catch(err) {
|
||||
// Expected error
|
||||
result = err.contains("Git error");
|
||||
}
|
||||
|
||||
result
|
||||
"#;
|
||||
|
||||
let result = engine.eval::<bool>(script).unwrap();
|
||||
assert!(result);
|
||||
}
|
||||
}
|
227
src/rhai/text.rs
227
src/rhai/text.rs
@ -1,227 +0,0 @@
|
||||
//! Rhai wrappers for Text module functions
|
||||
//!
|
||||
//! This module provides Rhai wrappers for the functions in the Text module.
|
||||
|
||||
use rhai::{Engine, EvalAltResult, Array, Map, Position};
|
||||
use std::collections::HashMap;
|
||||
use crate::text::{
|
||||
TextReplacer, TextReplacerBuilder,
|
||||
TemplateBuilder,
|
||||
dedent, prefix,
|
||||
name_fix, path_fix
|
||||
};
|
||||
|
||||
/// Register Text module functions with the Rhai engine
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `engine` - The Rhai engine to register the functions with
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
|
||||
pub fn register_text_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register types
|
||||
register_text_types(engine)?;
|
||||
|
||||
// Register TextReplacer constructor
|
||||
engine.register_fn("text_replacer_new", text_replacer_new);
|
||||
|
||||
// Register TextReplacerBuilder instance methods
|
||||
engine.register_fn("pattern", pattern);
|
||||
engine.register_fn("replacement", replacement);
|
||||
engine.register_fn("regex", regex);
|
||||
engine.register_fn("case_insensitive", case_insensitive);
|
||||
engine.register_fn("and", and);
|
||||
engine.register_fn("build", build);
|
||||
|
||||
// Register TextReplacer instance methods
|
||||
engine.register_fn("replace", replace);
|
||||
engine.register_fn("replace_file", replace_file);
|
||||
engine.register_fn("replace_file_in_place", replace_file_in_place);
|
||||
engine.register_fn("replace_file_to", replace_file_to);
|
||||
|
||||
// Register TemplateBuilder constructor
|
||||
engine.register_fn("template_builder_open", template_builder_open);
|
||||
|
||||
// Register TemplateBuilder instance methods
|
||||
engine.register_fn("add_var", add_var_string);
|
||||
engine.register_fn("add_var", add_var_int);
|
||||
engine.register_fn("add_var", add_var_float);
|
||||
engine.register_fn("add_var", add_var_bool);
|
||||
engine.register_fn("add_var", add_var_array);
|
||||
engine.register_fn("add_vars", add_vars);
|
||||
engine.register_fn("render", render);
|
||||
engine.register_fn("render_to_file", render_to_file);
|
||||
|
||||
// Register Fix functions directly from text module
|
||||
engine.register_fn("name_fix", crate::text::name_fix);
|
||||
engine.register_fn("path_fix", crate::text::path_fix);
|
||||
|
||||
// Register Dedent functions directly from text module
|
||||
engine.register_fn("dedent", crate::text::dedent);
|
||||
engine.register_fn("prefix", crate::text::prefix);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register Text module types with the Rhai engine
|
||||
fn register_text_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
|
||||
// Register TextReplacerBuilder type
|
||||
engine.register_type_with_name::<TextReplacerBuilder>("TextReplacerBuilder");
|
||||
|
||||
// Register TextReplacer type
|
||||
engine.register_type_with_name::<TextReplacer>("TextReplacer");
|
||||
|
||||
// Register TemplateBuilder type
|
||||
engine.register_type_with_name::<TemplateBuilder>("TemplateBuilder");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper functions for error conversion
|
||||
fn io_error_to_rhai_error<T>(result: std::io::Result<T>) -> Result<T, Box<EvalAltResult>> {
|
||||
result.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("IO error: {}", e).into(),
|
||||
Position::NONE
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn tera_error_to_rhai_error<T>(result: Result<T, tera::Error>) -> Result<T, Box<EvalAltResult>> {
|
||||
result.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Template error: {}", e).into(),
|
||||
Position::NONE
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn string_error_to_rhai_error<T>(result: Result<T, String>) -> Result<T, Box<EvalAltResult>> {
|
||||
result.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
e.into(),
|
||||
Position::NONE
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
// TextReplacer implementation
|
||||
|
||||
/// Creates a new TextReplacerBuilder
|
||||
pub fn text_replacer_new() -> TextReplacerBuilder {
|
||||
TextReplacerBuilder::default()
|
||||
}
|
||||
|
||||
/// Sets the pattern to search for
|
||||
pub fn pattern(builder: TextReplacerBuilder, pat: &str) -> TextReplacerBuilder {
|
||||
builder.pattern(pat)
|
||||
}
|
||||
|
||||
/// Sets the replacement text
|
||||
pub fn replacement(builder: TextReplacerBuilder, rep: &str) -> TextReplacerBuilder {
|
||||
builder.replacement(rep)
|
||||
}
|
||||
|
||||
/// Sets whether to use regex
|
||||
pub fn regex(builder: TextReplacerBuilder, yes: bool) -> TextReplacerBuilder {
|
||||
builder.regex(yes)
|
||||
}
|
||||
|
||||
/// Sets whether the replacement should be case-insensitive
|
||||
pub fn case_insensitive(builder: TextReplacerBuilder, yes: bool) -> TextReplacerBuilder {
|
||||
builder.case_insensitive(yes)
|
||||
}
|
||||
|
||||
/// Adds another replacement operation to the chain and resets the builder for a new operation
|
||||
pub fn and(builder: TextReplacerBuilder) -> TextReplacerBuilder {
|
||||
builder.and()
|
||||
}
|
||||
|
||||
/// Builds the TextReplacer with all configured replacement operations
|
||||
pub fn build(builder: TextReplacerBuilder) -> Result<TextReplacer, Box<EvalAltResult>> {
|
||||
string_error_to_rhai_error(builder.build())
|
||||
}
|
||||
|
||||
/// Applies all configured replacement operations to the input text
|
||||
pub fn replace(replacer: &mut TextReplacer, input: &str) -> String {
|
||||
replacer.replace(input)
|
||||
}
|
||||
|
||||
/// Reads a file, applies all replacements, and returns the result as a string
|
||||
pub fn replace_file(replacer: &mut TextReplacer, path: &str) -> Result<String, Box<EvalAltResult>> {
|
||||
io_error_to_rhai_error(replacer.replace_file(path))
|
||||
}
|
||||
|
||||
/// Reads a file, applies all replacements, and writes the result back to the file
|
||||
pub fn replace_file_in_place(replacer: &mut TextReplacer, path: &str) -> Result<(), Box<EvalAltResult>> {
|
||||
io_error_to_rhai_error(replacer.replace_file_in_place(path))
|
||||
}
|
||||
|
||||
/// Reads a file, applies all replacements, and writes the result to a new file
|
||||
pub fn replace_file_to(replacer: &mut TextReplacer, input_path: &str, output_path: &str) -> Result<(), Box<EvalAltResult>> {
|
||||
io_error_to_rhai_error(replacer.replace_file_to(input_path, output_path))
|
||||
}
|
||||
|
||||
// TemplateBuilder implementation
|
||||
|
||||
/// Creates a new TemplateBuilder with the specified template path
|
||||
pub fn template_builder_open(template_path: &str) -> Result<TemplateBuilder, Box<EvalAltResult>> {
|
||||
io_error_to_rhai_error(TemplateBuilder::open(template_path))
|
||||
}
|
||||
|
||||
/// Adds a string variable to the template context
|
||||
pub fn add_var_string(builder: TemplateBuilder, name: &str, value: &str) -> TemplateBuilder {
|
||||
builder.add_var(name, value)
|
||||
}
|
||||
|
||||
/// Adds an integer variable to the template context
|
||||
pub fn add_var_int(builder: TemplateBuilder, name: &str, value: i64) -> TemplateBuilder {
|
||||
builder.add_var(name, value)
|
||||
}
|
||||
|
||||
/// Adds a float variable to the template context
|
||||
pub fn add_var_float(builder: TemplateBuilder, name: &str, value: f64) -> TemplateBuilder {
|
||||
builder.add_var(name, value)
|
||||
}
|
||||
|
||||
/// Adds a boolean variable to the template context
|
||||
pub fn add_var_bool(builder: TemplateBuilder, name: &str, value: bool) -> TemplateBuilder {
|
||||
builder.add_var(name, value)
|
||||
}
|
||||
|
||||
/// Adds an array variable to the template context
|
||||
pub fn add_var_array(builder: TemplateBuilder, name: &str, array: Array) -> TemplateBuilder {
|
||||
// Convert Rhai Array to Vec<String>
|
||||
let vec: Vec<String> = array.iter()
|
||||
.filter_map(|v| v.clone().into_string().ok())
|
||||
.collect();
|
||||
|
||||
builder.add_var(name, vec)
|
||||
}
|
||||
|
||||
/// Adds multiple variables to the template context from a Map
|
||||
pub fn add_vars(builder: TemplateBuilder, vars: Map) -> TemplateBuilder {
|
||||
// Convert Rhai Map to Rust HashMap
|
||||
let mut hash_map = HashMap::new();
|
||||
|
||||
for (key, value) in vars.iter() {
|
||||
if let Ok(val_str) = value.clone().into_string() {
|
||||
hash_map.insert(key.to_string(), val_str);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the variables
|
||||
builder.add_vars(hash_map)
|
||||
}
|
||||
|
||||
/// Renders the template with the current context
|
||||
pub fn render(builder: &mut TemplateBuilder) -> Result<String, Box<EvalAltResult>> {
|
||||
tera_error_to_rhai_error(builder.render())
|
||||
}
|
||||
|
||||
/// Renders the template and writes the result to a file
|
||||
pub fn render_to_file(builder: &mut TemplateBuilder, output_path: &str) -> Result<(), Box<EvalAltResult>> {
|
||||
io_error_to_rhai_error(builder.render_to_file(output_path))
|
||||
}
|
@ -1,133 +0,0 @@
|
||||
# Implementation Plan: Rhai Wrappers for Text Tools
|
||||
|
||||
## 1. Overview
|
||||
|
||||
We'll create a new module `rhai/text.rs` that will provide Rhai wrappers for all functionality in the text module, including:
|
||||
- TextReplacer (from replace.rs)
|
||||
- TemplateBuilder (from template.rs)
|
||||
- name_fix and path_fix functions (from fix.rs)
|
||||
- dedent and prefix functions (from dedent.rs)
|
||||
|
||||
The implementation will follow the builder pattern for the TextReplacer and TemplateBuilder classes, similar to their Rust implementations, and will use the same error handling pattern as the existing Rhai modules.
|
||||
|
||||
## 2. Module Structure
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[rhai/mod.rs] --> B[rhai/text.rs]
|
||||
B --> C[TextReplacer Wrappers]
|
||||
B --> D[TemplateBuilder Wrappers]
|
||||
B --> E[Fix Function Wrappers]
|
||||
B --> F[Dedent Function Wrappers]
|
||||
B --> G[Error Handling]
|
||||
B --> H[Registration Functions]
|
||||
```
|
||||
|
||||
## 3. Implementation Details
|
||||
|
||||
### 3.1. Module Setup and Registration
|
||||
|
||||
1. Create a new file `rhai/text.rs`
|
||||
2. Add the module to `rhai/mod.rs`
|
||||
3. Implement a `register_text_module` function to register all text-related functions with the Rhai engine
|
||||
4. Update the main `register` function in `rhai/mod.rs` to call `register_text_module`
|
||||
|
||||
### 3.2. TextReplacer Implementation
|
||||
|
||||
1. Register the TextReplacer type with the Rhai engine
|
||||
2. Implement the following functions:
|
||||
- `text_replacer_new()` - Creates a new TextReplacerBuilder
|
||||
- `pattern(builder, pat)` - Sets the pattern to search for and automatically adds any previous pattern/replacement pair to the chain
|
||||
- `replacement(builder, rep)` - Sets the replacement text
|
||||
- `regex(builder, yes)` - Sets whether to use regex
|
||||
- `case_insensitive(builder, yes)` - Sets whether the replacement should be case-insensitive
|
||||
- `build(builder)` - Builds the TextReplacer with all configured replacement operations
|
||||
- `replace(replacer, input)` - Applies all configured replacement operations to the input text
|
||||
- `replace_file(replacer, path)` - Reads a file, applies all replacements, and returns the result as a string
|
||||
- `replace_file_in_place(replacer, path)` - Reads a file, applies all replacements, and writes the result back to the file
|
||||
- `replace_file_to(replacer, input_path, output_path)` - Reads a file, applies all replacements, and writes the result to a new file
|
||||
|
||||
### 3.3. TemplateBuilder Implementation
|
||||
|
||||
1. Register the TemplateBuilder type with the Rhai engine
|
||||
2. Implement the following functions:
|
||||
- `template_builder_open(template_path)` - Creates a new TemplateBuilder with the specified template path
|
||||
- `add_var(builder, name, value)` - Adds a variable to the template context
|
||||
- `add_vars(builder, vars_map)` - Adds multiple variables to the template context from a Map
|
||||
- `render(builder)` - Renders the template with the current context
|
||||
- `render_to_file(builder, output_path)` - Renders the template and writes the result to a file
|
||||
|
||||
### 3.4. Fix Functions Implementation
|
||||
|
||||
1. Implement wrappers for the following functions:
|
||||
- `name_fix(text)` - Sanitizes a name by replacing special characters with underscores
|
||||
- `path_fix(text)` - Applies name_fix to the filename part of a path
|
||||
|
||||
### 3.5. Dedent Functions Implementation
|
||||
|
||||
1. Implement wrappers for the following functions:
|
||||
- `dedent(text)` - Removes common leading whitespace from a multiline string
|
||||
- `prefix(text, prefix)` - Adds a prefix to each line of a multiline string
|
||||
|
||||
### 3.6. Error Handling
|
||||
|
||||
1. Implement helper functions to convert Rust errors to Rhai errors:
|
||||
- `io_error_to_rhai_error` - Converts io::Error to EvalAltResult
|
||||
- `tera_error_to_rhai_error` - Converts tera::Error to EvalAltResult
|
||||
- `string_error_to_rhai_error` - Converts String error to EvalAltResult
|
||||
|
||||
## 4. Example Usage in Rhai Scripts
|
||||
|
||||
### TextReplacer Example
|
||||
```rhai
|
||||
// Create a TextReplacer with multiple replacements
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("foo").replacement("bar").regex(true)
|
||||
.pattern("hello").replacement("world")
|
||||
.build();
|
||||
|
||||
// Use the replacer
|
||||
let result = replacer.replace("foo hello foo");
|
||||
println(result); // Outputs: "bar world bar"
|
||||
|
||||
// Replace in a file
|
||||
let file_result = replacer.replace_file("input.txt");
|
||||
println(file_result);
|
||||
|
||||
// Replace and write to a new file
|
||||
replacer.replace_file_to("input.txt", "output.txt");
|
||||
```
|
||||
|
||||
### TemplateBuilder Example
|
||||
```rhai
|
||||
// Create a TemplateBuilder
|
||||
let template = template_builder_open("template.txt")
|
||||
.add_var("name", "John")
|
||||
.add_var("age", 30)
|
||||
.add_var("items", ["apple", "banana", "cherry"]);
|
||||
|
||||
// Render the template
|
||||
let result = template.render();
|
||||
println(result);
|
||||
|
||||
// Render to a file
|
||||
template.render_to_file("output.html");
|
||||
```
|
||||
|
||||
### Fix and Dedent Examples
|
||||
```rhai
|
||||
// Use name_fix and path_fix
|
||||
let fixed_name = name_fix("Hello World!");
|
||||
println(fixed_name); // Outputs: "hello_world"
|
||||
|
||||
let fixed_path = path_fix("/path/to/Hello World!");
|
||||
println(fixed_path); // Outputs: "/path/to/hello_world"
|
||||
|
||||
// Use dedent and prefix
|
||||
let indented_text = " line 1\n line 2\n line 3";
|
||||
let dedented = dedent(indented_text);
|
||||
println(dedented); // Outputs: "line 1\nline 2\n line 3"
|
||||
|
||||
let text = "line 1\nline 2\nline 3";
|
||||
let prefixed = prefix(text, " ");
|
||||
println(prefixed); // Outputs: " line 1\n line 2\n line 3"
|
@ -1,39 +0,0 @@
|
||||
// 01_hello_world.rhai
|
||||
// A simple hello world script to demonstrate basic Rhai functionality
|
||||
|
||||
// Print a message
|
||||
println("Hello from Rhai!");
|
||||
|
||||
// Define a function
|
||||
fn greet(name) {
|
||||
"Hello, " + name + "!"
|
||||
}
|
||||
|
||||
// Call the function and print the result
|
||||
let greeting = greet("SAL User");
|
||||
println(greeting);
|
||||
|
||||
// Do some basic calculations
|
||||
let a = 5;
|
||||
let b = 7;
|
||||
println(`${a} + ${b} = ${a + b}`);
|
||||
println(`${a} * ${b} = ${a * b}`);
|
||||
|
||||
// Create and use an array
|
||||
let numbers = [1, 2, 3, 4, 5];
|
||||
println("Numbers: " + numbers);
|
||||
println("Sum of numbers: " + numbers.reduce(|sum, n| sum + n, 0));
|
||||
|
||||
// Create and use a map
|
||||
let person = #{
|
||||
name: "John Doe",
|
||||
age: 30,
|
||||
occupation: "Developer"
|
||||
};
|
||||
|
||||
println("Person: " + person);
|
||||
println("Name: " + person.name);
|
||||
println("Age: " + person.age);
|
||||
|
||||
// Return a success message
|
||||
"Hello world script completed successfully!"
|
@ -1,64 +0,0 @@
|
||||
// 02_file_operations.rhai
|
||||
// Demonstrates file system operations using SAL
|
||||
|
||||
// Create a test directory
|
||||
let test_dir = "rhai_test_dir";
|
||||
println(`Creating directory: ${test_dir}`);
|
||||
let mkdir_result = mkdir(test_dir);
|
||||
println(`Directory creation result: ${mkdir_result}`);
|
||||
|
||||
// Check if the directory exists
|
||||
let dir_exists = exist(test_dir);
|
||||
println(`Directory exists: ${dir_exists}`);
|
||||
|
||||
// Create a test file
|
||||
let test_file = test_dir + "/test_file.txt";
|
||||
let file_content = "This is a test file created by Rhai script.";
|
||||
|
||||
// Create the file using a different approach
|
||||
println(`Creating file: ${test_file}`);
|
||||
// First ensure the directory exists
|
||||
run_command(`mkdir -p ${test_dir}`);
|
||||
// Then create the file using a simpler approach
|
||||
// First touch the file
|
||||
let touch_cmd = `touch ${test_file}`;
|
||||
run_command(touch_cmd);
|
||||
// Then write to it with a separate command
|
||||
let echo_cmd = `echo ${file_content} > ${test_file}`;
|
||||
let write_result = run_command(echo_cmd);
|
||||
println(`File creation result: ${write_result.success}`);
|
||||
|
||||
// Wait a moment to ensure the file is created
|
||||
run_command("sleep 1");
|
||||
|
||||
// Check if the file exists
|
||||
let file_exists = exist(test_file);
|
||||
println(`File exists: ${file_exists}`);
|
||||
|
||||
// Get file size
|
||||
if file_exists {
|
||||
let size = file_size(test_file);
|
||||
println(`File size: ${size} bytes`);
|
||||
}
|
||||
|
||||
// Copy the file
|
||||
let copied_file = test_dir + "/copied_file.txt";
|
||||
println(`Copying file to: ${copied_file}`);
|
||||
let copy_result = copy(test_file, copied_file);
|
||||
println(`File copy result: ${copy_result}`);
|
||||
|
||||
// Find files in the directory
|
||||
println("Finding files in the test directory:");
|
||||
let files = find_files(test_dir, "*.txt");
|
||||
for file in files {
|
||||
println(` - ${file}`);
|
||||
}
|
||||
|
||||
// Clean up (uncomment to actually delete the files)
|
||||
// println("Cleaning up...");
|
||||
// delete(copied_file);
|
||||
// delete(test_file);
|
||||
// delete(test_dir);
|
||||
// println("Cleanup complete");
|
||||
|
||||
"File operations script completed successfully!"
|
@ -1,64 +0,0 @@
|
||||
// 03_process_management.rhai
|
||||
// Demonstrates process management operations using SAL
|
||||
|
||||
// Check if common commands exist
|
||||
println("Checking if common commands exist:");
|
||||
let commands = ["ls", "echo", "cat", "grep"];
|
||||
for cmd in commands {
|
||||
let exists = which(cmd);
|
||||
println(` - ${cmd}: ${exists}`);
|
||||
}
|
||||
|
||||
// Run a simple command
|
||||
println("\nRunning a simple echo command:");
|
||||
let echo_result = run_command("echo 'Hello from Rhai process management!'");
|
||||
println(`Command output: ${echo_result.stdout}`);
|
||||
// The CommandResult type doesn't have an exit_code property
|
||||
println(`Success: ${echo_result.success}`);
|
||||
|
||||
// Run a command silently (no output to console)
|
||||
println("\nRunning a command silently:");
|
||||
let silent_result = run_silent("ls -la");
|
||||
println(`Command success: ${silent_result.success}`);
|
||||
println(`Command output length: ${silent_result.stdout.len()} characters`);
|
||||
|
||||
// Create custom run options
|
||||
println("\nRunning a command with custom options:");
|
||||
let options = new_run_options();
|
||||
options["die"] = false; // Don't return error if command fails
|
||||
options["silent"] = true; // Suppress output to stdout/stderr
|
||||
options["async_exec"] = false; // Run synchronously
|
||||
options["log"] = true; // Log command execution
|
||||
|
||||
let custom_result = run("echo 'Custom options test'", options);
|
||||
println(`Command success: ${custom_result.success}`);
|
||||
println(`Command output: ${custom_result.stdout}`);
|
||||
|
||||
// List processes
|
||||
println("\nListing processes (limited to 5):");
|
||||
let processes = process_list("");
|
||||
let count = 0;
|
||||
for proc in processes {
|
||||
if count >= 5 {
|
||||
break;
|
||||
}
|
||||
// Just print the PID since we're not sure what other properties are available
|
||||
println(` - PID: ${proc.pid}`);
|
||||
count += 1;
|
||||
}
|
||||
println(`Total processes: ${processes.len()}`);
|
||||
|
||||
// Run a command that will create a background process
|
||||
// Note: This is just for demonstration, the process will be short-lived
|
||||
println("\nRunning a background process:");
|
||||
let bg_options = new_run_options();
|
||||
bg_options["async_exec"] = true;
|
||||
// Fix the command to avoid issues with shell interpretation
|
||||
let bg_result = run("sleep 1", bg_options);
|
||||
println("Background process started");
|
||||
|
||||
// Wait a moment to let the background process run
|
||||
run_command("sleep 0.5");
|
||||
println("Main script continuing while background process runs");
|
||||
|
||||
"Process management script completed successfully!"
|
@ -1,84 +0,0 @@
|
||||
// 05_directory_operations.rhai
|
||||
// Demonstrates directory operations using SAL, including the new chdir function
|
||||
|
||||
// Create a test directory structure
|
||||
let base_dir = "rhai_dir_test";
|
||||
let sub_dir = base_dir + "/tmp/test";
|
||||
|
||||
println("Creating directory structure...");
|
||||
let base_result = mkdir(base_dir+"/subdir");
|
||||
println(`Base directory creation result: ${base_result}`);
|
||||
|
||||
let sub_result = mkdir(sub_dir);
|
||||
println(`Subdirectory creation result: ${sub_result}`);
|
||||
|
||||
// Create a test file in the base directory
|
||||
let base_file = base_dir + "/base_file.txt";
|
||||
let base_content = "This is a file in the base directory.";
|
||||
// First touch the file
|
||||
run_command(`touch ${base_file}`);
|
||||
// Then write to it with a separate command
|
||||
run_command(`echo ${base_content} > ${base_file}`);
|
||||
|
||||
// Create a test file in the subdirectory
|
||||
let sub_file = sub_dir + "/sub_file.txt";
|
||||
let sub_content = "This is a file in the subdirectory.";
|
||||
// First touch the file
|
||||
run_command(`touch ${sub_file}`);
|
||||
// Then write to it with a separate command
|
||||
run_command(`echo ${sub_content} > ${sub_file}`);
|
||||
|
||||
// Get the current working directory before changing
|
||||
let pwd_before = run_command("pwd");
|
||||
println(`Current directory before chdir: ${pwd_before.stdout.trim()}`);
|
||||
|
||||
// Change to the base directory
|
||||
println(`Changing directory to: ${base_dir}`);
|
||||
let chdir_result = chdir(base_dir);
|
||||
println(`Directory change result: ${chdir_result}`);
|
||||
|
||||
// Get the current working directory after changing
|
||||
let pwd_after = run_command("pwd");
|
||||
println(`Current directory after chdir: ${pwd_after.stdout.trim()}`);
|
||||
|
||||
// List files in the current directory (which should now be the base directory)
|
||||
println("Files in the current directory:");
|
||||
let files = find_files(".", "*");
|
||||
println("Files found:");
|
||||
for file in files {
|
||||
println(`- ${file}`);
|
||||
}
|
||||
|
||||
// Change to the subdirectory
|
||||
println(`Changing directory to: subdir`);
|
||||
let chdir_sub_result = chdir("subdir");
|
||||
println(`Directory change result: ${chdir_sub_result}`);
|
||||
|
||||
// Get the current working directory after changing to subdirectory
|
||||
let pwd_final = run_command("pwd");
|
||||
println(`Current directory after second chdir: ${pwd_final.stdout.trim()}`);
|
||||
|
||||
// List files in the subdirectory
|
||||
println("Files in the subdirectory:");
|
||||
let subdir_files = find_files(".", "*");
|
||||
println("Files found:");
|
||||
for file in subdir_files {
|
||||
println(`- ${file}`);
|
||||
}
|
||||
|
||||
// Change back to the parent directory
|
||||
println("Changing directory back to parent...");
|
||||
let chdir_parent_result = chdir("..");
|
||||
println(`Directory change result: ${chdir_parent_result}`);
|
||||
|
||||
// Clean up (uncomment to actually delete the files)
|
||||
// println("Cleaning up...");
|
||||
// Change back to the original directory first
|
||||
// chdir(pwd_before.stdout.trim());
|
||||
// delete(sub_file);
|
||||
// delete(base_file);
|
||||
// delete(sub_dir);
|
||||
// delete(base_dir);
|
||||
// println("Cleanup complete");
|
||||
|
||||
"Directory operations script completed successfully!"
|
@ -1,65 +0,0 @@
|
||||
// 06_file_read_write.rhai
|
||||
// Demonstrates file read and write operations using SAL
|
||||
|
||||
// Create a test directory
|
||||
let test_dir = "rhai_file_test_dir";
|
||||
println(`Creating directory: ${test_dir}`);
|
||||
let mkdir_result = mkdir(test_dir);
|
||||
println(`Directory creation result: ${mkdir_result}`);
|
||||
|
||||
// Define file paths
|
||||
let test_file = test_dir + "/test_file.txt";
|
||||
let append_file = test_dir + "/append_file.txt";
|
||||
|
||||
// 1. Write to a file
|
||||
println(`\n--- Writing to file: ${test_file} ---`);
|
||||
let content = "This is the first line of text.\nThis is the second line of text.";
|
||||
let write_result = file_write(test_file, content);
|
||||
println(`Write result: ${write_result}`);
|
||||
|
||||
// 2. Read from a file
|
||||
println(`\n--- Reading from file: ${test_file} ---`);
|
||||
let read_content = file_read(test_file);
|
||||
println("File content:");
|
||||
println(read_content);
|
||||
|
||||
// 3. Append to a file
|
||||
println(`\n--- Creating and appending to file: ${append_file} ---`);
|
||||
// First create the file with initial content
|
||||
let initial_content = "Initial content - line 1\nInitial content - line 2\n";
|
||||
let create_result = file_write(append_file, initial_content);
|
||||
println(`Create result: ${create_result}`);
|
||||
|
||||
// Now append to the file
|
||||
let append_content = "Appended content - line 3\nAppended content - line 4\n";
|
||||
let append_result = file_write_append(append_file, append_content);
|
||||
println(`Append result: ${append_result}`);
|
||||
|
||||
// Read the appended file to verify
|
||||
println(`\n--- Reading appended file: ${append_file} ---`);
|
||||
let appended_content = file_read(append_file);
|
||||
println("Appended file content:");
|
||||
println(appended_content);
|
||||
|
||||
// 4. Demonstrate multiple appends
|
||||
println(`\n--- Demonstrating multiple appends ---`);
|
||||
for i in range(1, 4) {
|
||||
// Use a simple counter instead of timestamp to avoid issues
|
||||
let log_entry = `Log entry #${i} - appended at iteration ${i}\n`;
|
||||
file_write_append(append_file, log_entry);
|
||||
println(`Added log entry #${i}`);
|
||||
}
|
||||
|
||||
// Read the final file content
|
||||
println(`\n--- Final file content after multiple appends ---`);
|
||||
let final_content = file_read(append_file);
|
||||
println(final_content);
|
||||
|
||||
// Clean up (uncomment to actually delete the files)
|
||||
// println("\nCleaning up...");
|
||||
// delete(test_file);
|
||||
// delete(append_file);
|
||||
// delete(test_dir);
|
||||
// println("Cleanup complete");
|
||||
|
||||
"File read/write operations script completed successfully!"
|
@ -1,150 +0,0 @@
|
||||
// buildah.rhai
|
||||
// Demonstrates using buildah to create a custom image with golang and nginx,
|
||||
// then using nerdctl to run a container from that image
|
||||
|
||||
println("Starting buildah workflow to create a custom image...");
|
||||
|
||||
// Define image and container names
|
||||
let base_image = "ubuntu:22.04";
|
||||
let container_name = "golang-nginx-container";
|
||||
let final_image_name = "custom-golang-nginx:latest";
|
||||
|
||||
println(`Creating container '${container_name}' from base image '${base_image}'...`);
|
||||
|
||||
// Create a new buildah container using the builder pattern
|
||||
let builder = bah_new(container_name, base_image);
|
||||
|
||||
println("Enabling debug mode...");
|
||||
builder.debug_mode = true;
|
||||
|
||||
// Update package lists and install golang and nginx
|
||||
println("Installing packages (this may take a while)...");
|
||||
|
||||
// Update package lists
|
||||
let update_result = builder.run("apt-get update -y");
|
||||
|
||||
// Install required packages
|
||||
let install_result = builder.run("apt-get install -y golang nginx");
|
||||
|
||||
// Verify installations
|
||||
let go_version = builder.run("go version");
|
||||
println(`Go version: ${go_version.stdout}`);
|
||||
|
||||
let nginx_version = builder.run("nginx -v");
|
||||
println(`Nginx version: ${nginx_version.stderr}`); // nginx outputs version to stderr
|
||||
|
||||
// Create a simple Go web application
|
||||
println("Creating a simple Go web application...");
|
||||
|
||||
// Create a directory for the Go application
|
||||
builder.run("mkdir -p /app");
|
||||
|
||||
// Create a simple Go web server
|
||||
let go_app = `
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Hello from Go running in a custom container!")
|
||||
})
|
||||
|
||||
fmt.Println("Starting server on :8080")
|
||||
http.ListenAndServe(":8080", nil)
|
||||
}
|
||||
`;
|
||||
|
||||
// Write the Go application to a file using the write_content method
|
||||
builder.write_content(go_app, "/app/main.go");
|
||||
|
||||
// Compile the Go application
|
||||
builder.run("cd /app && go build -o server main.go");
|
||||
|
||||
// Configure nginx to proxy to the Go application
|
||||
let nginx_conf = `
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Write the nginx configuration using the write_content method
|
||||
let nginx_conf_result = builder.write_content(nginx_conf, "/etc/nginx/sites-available/default");
|
||||
|
||||
// Create a startup script
|
||||
let startup_script = `
|
||||
#!/bin/bash
|
||||
# Start the Go application in the background
|
||||
cd /app && ./server &
|
||||
# Start nginx in the foreground
|
||||
nginx -g "daemon off;"
|
||||
`;
|
||||
|
||||
// Write the startup script using the write_content method
|
||||
let startup_script_result = builder.write_content(startup_script, "/start.sh");
|
||||
builder.run("chmod +x /start.sh");
|
||||
|
||||
// Set the entrypoint to the startup script
|
||||
println("Setting entrypoint to /start.sh...");
|
||||
builder.set_entrypoint("/start.sh");
|
||||
|
||||
// Read back the startup script to verify it was written correctly
|
||||
let read_script = builder.read_content("/start.sh");
|
||||
println("Startup script content verification:");
|
||||
println(read_script);
|
||||
|
||||
// Commit the container to a new image
|
||||
println(`Committing container to image '${final_image_name}'...`);
|
||||
let commit_result = builder.commit(final_image_name);
|
||||
|
||||
// Clean up the buildah container
|
||||
println("Cleaning up buildah container...");
|
||||
builder.remove();
|
||||
|
||||
// Now use nerdctl to run a container from the new image
|
||||
println("\nStarting container from the new image using nerdctl...");
|
||||
|
||||
// Create a container using the builder pattern
|
||||
// Use localhost/ prefix to ensure nerdctl uses the local image
|
||||
let local_image_name = "localhost/" + final_image_name;
|
||||
println(`Using local image: ${local_image_name}`);
|
||||
|
||||
// Tag the image with the localhost prefix for nerdctl compatibility
|
||||
println(`Tagging image as ${local_image_name}...`);
|
||||
let tag_result = bah_image_tag(final_image_name, local_image_name);
|
||||
|
||||
// Print a command to check if the image exists in buildah
|
||||
println("\nTo verify the image was created with buildah, run:");
|
||||
println("buildah images");
|
||||
|
||||
// Note: If nerdctl cannot find the image, you may need to push it to a registry
|
||||
println("\nNote: If nerdctl cannot find the image, you may need to push it to a registry:");
|
||||
println("buildah push localhost/custom-golang-nginx:latest docker://localhost:5000/custom-golang-nginx:latest");
|
||||
println("nerdctl pull localhost:5000/custom-golang-nginx:latest");
|
||||
|
||||
let container = nerdctl_container_from_image("golang-nginx-demo", local_image_name)
|
||||
.with_detach(true)
|
||||
.with_port("8080:80") // Map port 80 in the container to 8080 on the host
|
||||
.with_restart_policy("unless-stopped")
|
||||
.build();
|
||||
|
||||
// Start the container
|
||||
let start_result = container.start();
|
||||
|
||||
println("\nWorkflow completed successfully!");
|
||||
println("The web server should be running at http://localhost:8080");
|
||||
println("You can check container logs with: nerdctl logs golang-nginx-demo");
|
||||
println("To stop the container: nerdctl stop golang-nginx-demo");
|
||||
println("To remove the container: nerdctl rm golang-nginx-demo");
|
||||
|
||||
"Buildah and nerdctl workflow completed successfully!"
|
@ -1,39 +0,0 @@
|
||||
// buildah_debug.rhai
|
||||
// Demonstrates using the debug flag on the buildah Builder
|
||||
|
||||
println("Starting buildah debug example...");
|
||||
|
||||
// Define image and container names
|
||||
let base_image = "ubuntu:22.04";
|
||||
let container_name = "debug-test-container";
|
||||
|
||||
println(`Creating container '${container_name}' from base image '${base_image}'...`);
|
||||
|
||||
// Create a new buildah container using the builder pattern
|
||||
let builder = bah_new(container_name, base_image);
|
||||
|
||||
// Enable debug mode
|
||||
println("Enabling debug mode...");
|
||||
builder.debug_mode = true;
|
||||
|
||||
// Run a simple command to see debug output
|
||||
println("Running a command with debug enabled...");
|
||||
let result = builder.run("echo 'Hello from debug mode'");
|
||||
|
||||
// Disable debug mode
|
||||
println("Disabling debug mode...");
|
||||
builder.debug_mode = false;
|
||||
|
||||
// Run another command without debug
|
||||
println("Running a command with debug disabled...");
|
||||
let result2 = builder.run("echo 'Hello without debug'");
|
||||
|
||||
// Enable debug mode again
|
||||
println("Enabling debug mode again...");
|
||||
builder.debug_mode = true;
|
||||
|
||||
// Remove the container with debug enabled
|
||||
println("Removing the container with debug enabled...");
|
||||
builder.remove();
|
||||
|
||||
println("Debug example completed!");
|
@ -1,210 +0,0 @@
|
||||
// containerd_grpc_setup.rhai
|
||||
//
|
||||
// This script sets up a Rust project with gRPC connectivity to containerd
|
||||
// Following the steps from the instructions document
|
||||
|
||||
|
||||
run("apt-get -y protobuf-compiler ");
|
||||
|
||||
// Step 1: Set up project directory
|
||||
let project_dir = "/tmp/containerd-rust-client";
|
||||
print(`Setting up project in: ${project_dir}`);
|
||||
|
||||
// Clean up any existing directory
|
||||
if exist(project_dir) {
|
||||
print("Found existing project directory, removing it...");
|
||||
delete(project_dir);
|
||||
}
|
||||
|
||||
// Create our project directory
|
||||
mkdir(project_dir);
|
||||
|
||||
// Change to the project directory
|
||||
chdir(project_dir);
|
||||
|
||||
// Step 2: Clone containerd's gRPC proto files
|
||||
print("Cloning containerd repository to get proto files...");
|
||||
let git_tree = gittree_new(project_dir);
|
||||
let repos = git_tree.get("https://github.com/containerd/containerd.git");
|
||||
let repo = repos[0];
|
||||
print(`Cloned containerd repository to: ${repo.path()}`);
|
||||
|
||||
// Step 3: Create necessary project files
|
||||
print("Creating Cargo.toml file...");
|
||||
// Using raw string with # for multiline content
|
||||
let cargo_toml = #"
|
||||
[package]
|
||||
name = "containerd-rust-client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tonic = "0.11"
|
||||
prost = "0.12"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
hyper-unix-connector = "0.2.0"
|
||||
tower = "0.4"
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = "0.11"
|
||||
"#;
|
||||
|
||||
file_write("Cargo.toml", cargo_toml);
|
||||
print("Created Cargo.toml file");
|
||||
|
||||
// Step 4: Set up build.rs to compile protos
|
||||
print("Creating build.rs file...");
|
||||
let build_rs = #"
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=containerd/api/services/images/v1/images.proto");
|
||||
println!("cargo:rerun-if-changed=containerd/api/services/containers/v1/containers.proto");
|
||||
|
||||
tonic_build::configure()
|
||||
.build_server(false)
|
||||
.compile(
|
||||
&[
|
||||
"containerd/api/services/images/v1/images.proto",
|
||||
"containerd/api/services/containers/v1/containers.proto",
|
||||
// Add more proto files as needed
|
||||
],
|
||||
&[
|
||||
"containerd",
|
||||
"containerd/api",
|
||||
"containerd/api/types"
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
"#;
|
||||
|
||||
file_write("build.rs", build_rs);
|
||||
print("Created build.rs file");
|
||||
|
||||
// Step 5: Create src directory and main.rs file
|
||||
mkdir("src");
|
||||
|
||||
// Create a helper function for Unix socket connection
|
||||
print("Creating src/main.rs file...");
|
||||
let main_rs = #"
|
||||
use tonic::transport::{Channel, Endpoint, Uri};
|
||||
use tower::service_fn;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
// The proto-generated modules will be available after build
|
||||
// use containerd::services::images::v1::{
|
||||
// images_client::ImagesClient,
|
||||
// GetImageRequest,
|
||||
// };
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Connecting to containerd gRPC...");
|
||||
|
||||
// Path to containerd socket
|
||||
let socket_path = "/run/containerd/containerd.sock";
|
||||
|
||||
// Connect to the Unix socket
|
||||
let channel = unix_socket_channel(socket_path).await?;
|
||||
|
||||
// Now we'd create a client and use it
|
||||
// let mut client = ImagesClient::new(channel);
|
||||
// let response = client.get(GetImageRequest {
|
||||
// name: "docker.io/library/ubuntu:latest".to_string(),
|
||||
// }).await?;
|
||||
// println!("Image: {:?}", response.into_inner());
|
||||
|
||||
println!("Connection to containerd socket established successfully!");
|
||||
println!("This is a template - uncomment the client code after building.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper function to connect to Unix socket
|
||||
async fn unix_socket_channel(path: &str) -> Result<Channel, Box<dyn std::error::Error>> {
|
||||
// Use a placeholder URI since Unix sockets don't have URIs
|
||||
let endpoint = Endpoint::try_from("http://[::]:50051")?;
|
||||
|
||||
// The socket path to connect to
|
||||
let path_to_connect = path.to_string();
|
||||
|
||||
// Create a connector function that connects to the Unix socket
|
||||
let channel = endpoint
|
||||
.connect_with_connector(service_fn(move |_: Uri| {
|
||||
let path = path_to_connect.clone();
|
||||
async move {
|
||||
tokio::net::UnixStream::connect(path)
|
||||
.await
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
|
||||
}
|
||||
}))
|
||||
.await?;
|
||||
|
||||
Ok(channel)
|
||||
}
|
||||
"#;
|
||||
|
||||
file_write("src/main.rs", main_rs);
|
||||
print("Created src/main.rs file");
|
||||
|
||||
// Step 6: Create a README.md file
|
||||
print("Creating README.md file...");
|
||||
// Using raw string with # for multiline content containing markdown backticks
|
||||
let readme = #"# containerd Rust gRPC Client
|
||||
|
||||
A Rust client for interacting with containerd via gRPC.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Rust and Cargo installed
|
||||
- containerd running on your system
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
cargo build
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
cargo run
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Connect to containerd via Unix socket
|
||||
- Query image information
|
||||
- Work with containers
|
||||
|
||||
## Structure
|
||||
|
||||
- `src/main.rs` - Example client code
|
||||
- `build.rs` - Proto compilation script
|
||||
"#;
|
||||
|
||||
file_write("README.md", readme);
|
||||
print("Created README.md file");
|
||||
|
||||
// Step 7: Build the project
|
||||
print("Building the project...");
|
||||
let build_result = run("cargo build");
|
||||
|
||||
if build_result.success {
|
||||
print("Project built successfully!");
|
||||
} else {
|
||||
print(`Build failed with error: ${build_result.stderr}`);
|
||||
}
|
||||
|
||||
print(`
|
||||
--------------------------------------
|
||||
🎉 Setup complete!
|
||||
|
||||
Project created at: ${project_dir}
|
||||
|
||||
To use the project:
|
||||
1. cd ${project_dir}
|
||||
2. cargo run
|
||||
|
||||
Note: Make sure containerd is running and the socket exists at /run/containerd/containerd.sock
|
||||
--------------------------------------
|
||||
`);
|
@ -1,105 +0,0 @@
|
||||
|
||||
print("\n=== Test download() Functionality ===");
|
||||
|
||||
// Create test directory
|
||||
let download_dir = "/tmp/downloadtest";
|
||||
|
||||
// Clean up any previous test files
|
||||
delete(download_dir);
|
||||
mkdir(download_dir);
|
||||
print("Created test directory for downloads at " + download_dir);
|
||||
|
||||
// Test URLs
|
||||
let zip_url = "https://github.com/freeflowuniverse/herolib/archive/refs/tags/v1.0.24.zip";
|
||||
let targz_url = "https://github.com/freeflowuniverse/herolib/archive/refs/tags/v1.0.24.tar.gz";
|
||||
let binary_url = "https://github.com/freeflowuniverse/herolib/releases/download/v1.0.24/hero-aarch64-unknown-linux-musl";
|
||||
|
||||
// Create destinations
|
||||
let zip_dest = `${download_dir}/zip`;
|
||||
let targz_dest = `${download_dir}/targz`;
|
||||
let binary_dest = `${download_dir}/hero-binary`;
|
||||
|
||||
|
||||
//PART 1
|
||||
|
||||
// Download and extract .zip file
|
||||
print("\nTesting .zip download:");
|
||||
// Download function now extracts zip files automatically
|
||||
let result = download(zip_url, zip_dest, 0);
|
||||
|
||||
// Check if files were extracted
|
||||
let file_count = find_files(zip_dest, "*").len();
|
||||
print(` Files found after extraction: ${file_count}`);
|
||||
let success_msg = if file_count > 0 { "yes" } else { "no" };
|
||||
print(` Extraction successful: ${success_msg}`);
|
||||
|
||||
//PART 2
|
||||
|
||||
// Download and extract .tar.gz file
|
||||
print("\nTesting .tar.gz download:");
|
||||
let result = download(targz_url, targz_dest, 0);
|
||||
|
||||
// Check if files were extracted (download function should extract tar.gz automatically)
|
||||
let file_count = find_files(targz_dest, "*").len();
|
||||
print(` Files found after extraction: ${file_count}`);
|
||||
let success_msg = if file_count > 100 { "yes" } else { "no" };
|
||||
print(` Extraction successful: ${success_msg}`);
|
||||
|
||||
//PART 3
|
||||
|
||||
// Download binary file and check size
|
||||
print("\nTesting binary download:");
|
||||
download_file(binary_url, binary_dest, 8000);
|
||||
|
||||
// Check file size using our new file_size function
|
||||
let size_bytes = file_size(binary_dest);
|
||||
let size_mb = size_bytes / (1024 * 1024);
|
||||
print(` File size: ${size_mb} MB`);
|
||||
let size_check = if size_mb > 5 { "yes" } else { "no" };
|
||||
print(` Size > 5MB: ${size_check}`);
|
||||
let success_msg = if size_mb >= 8 > 100 { "yes" } else { "no" };
|
||||
print(` Minimum size check passed:${success_msg}`);
|
||||
|
||||
// Clean up test files
|
||||
delete(download_dir);
|
||||
print("Cleaned up test directory");
|
||||
//PART 4
|
||||
|
||||
// Test the new download_file function
|
||||
print("\nTesting download_file function:");
|
||||
let text_url = "https://raw.githubusercontent.com/freeflowuniverse/herolib/main/README.md";
|
||||
let text_file_dest = `${download_dir}/README.md`;
|
||||
|
||||
// Create the directory again for this test
|
||||
mkdir(download_dir);
|
||||
|
||||
// Download a text file using the new download_file function
|
||||
let file_result = download_file(text_url, text_file_dest, 0);
|
||||
print(` File downloaded to: ${file_result}`);
|
||||
|
||||
// Check if the file exists and has content
|
||||
let file_exists = exist(text_file_dest);
|
||||
print(` File exists: ${file_exists}`);
|
||||
let file_content = file_read(text_file_dest);
|
||||
let content_check = if file_content.len() > 100 { "yes" } else { "no" };
|
||||
print(` File has content: ${content_check}`);
|
||||
|
||||
//PART 5
|
||||
|
||||
// Test the new chmod_exec function
|
||||
print("\nTesting chmod_exec function:");
|
||||
// Create a simple shell script
|
||||
let script_path = `${download_dir}/test_script.sh`;
|
||||
file_write(script_path, "#!/bin/sh\necho 'Hello from test script'");
|
||||
|
||||
// Make it executable
|
||||
let chmod_result = chmod_exec(script_path);
|
||||
print(` ${chmod_result}`);
|
||||
|
||||
// Clean up test files again
|
||||
delete(download_dir);
|
||||
print("Cleaned up test directory");
|
||||
|
||||
print("\nAll Download Tests completed successfully!");
|
||||
"Download Tests Success"
|
||||
"Download Tests Success"
|
@ -1,217 +0,0 @@
|
||||
// Comprehensive file system operations test script with assertions
|
||||
|
||||
print("===== File System Operations Test =====");
|
||||
|
||||
// Helper functions for testing
|
||||
fn assert(condition, message) {
|
||||
if (condition == false) {
|
||||
print(`FAILED: ${message}`);
|
||||
throw `Assertion failed: ${message}`;
|
||||
} else {
|
||||
print(`PASSED: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_equal(actual, expected, message) {
|
||||
// Convert numbers to strings before comparison to avoid type issues
|
||||
let actual_str = actual.to_string();
|
||||
let expected_str = expected.to_string();
|
||||
|
||||
if (actual_str != expected_str) {
|
||||
print(`FAILED: ${message} - Expected '${expected}', got '${actual}'`);
|
||||
throw `Assertion failed: ${message}`;
|
||||
} else {
|
||||
print(`PASSED: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_true(value, message) {
|
||||
assert(value, message);
|
||||
}
|
||||
|
||||
fn assert_false(value, message) {
|
||||
assert(value == false, message);
|
||||
}
|
||||
|
||||
// Directory for tests
|
||||
let test_dir = "/tmp/herodo_test_fs";
|
||||
let tests_total = 0;
|
||||
|
||||
// Setup - create test directory
|
||||
print("\n=== Setup ===");
|
||||
if exist(test_dir) {
|
||||
print(`Test directory exists, removing it first...`);
|
||||
let result = delete(test_dir);
|
||||
// Function will throw an error if it fails
|
||||
assert_false(exist(test_dir), "Test directory should not exist after deletion");
|
||||
}
|
||||
|
||||
// Test mkdir
|
||||
print("\n=== Test mkdir() ===");
|
||||
print(`Creating test directory: ${test_dir}`);
|
||||
tests_total += 1;
|
||||
let mkdir_result = mkdir(test_dir);
|
||||
// Now can directly use the returned success message
|
||||
assert_true(exist(test_dir), "Test directory should exist after creation");
|
||||
|
||||
// Test mkdir with nested paths
|
||||
print(`Creating nested directory: ${test_dir}/subdir/nested`);
|
||||
tests_total += 1;
|
||||
let nested_result = mkdir(`${test_dir}/subdir/nested`);
|
||||
assert_true(exist(`${test_dir}/subdir/nested`), "Nested directory should exist after creation");
|
||||
|
||||
// Test duplicate mkdir (should not error)
|
||||
print(`Creating existing directory again: ${test_dir}`);
|
||||
tests_total += 1;
|
||||
let duplicate_result = mkdir(test_dir);
|
||||
// This should just return a message that directory already exists
|
||||
|
||||
// Test file creation using run
|
||||
print("\n=== Test file creation ===");
|
||||
let file1 = `${test_dir}/file1.txt`;
|
||||
let file2 = `${test_dir}/file2.txt`;
|
||||
let file3 = `${test_dir}/subdir/file3.txt`;
|
||||
|
||||
// Create files
|
||||
print(`Creating test files...`);
|
||||
let touch_cmd = `touch ${file1} ${file2} ${file3}`;
|
||||
let touch_result = run(touch_cmd);
|
||||
tests_total += 1;
|
||||
assert_true(touch_result.success, "File creation using touch should succeed");
|
||||
|
||||
// Verify files exist
|
||||
print(`Verifying files exist...`);
|
||||
tests_total += 1;
|
||||
assert_true(exist(file1), "File 1 should exist after creation");
|
||||
assert_true(exist(file2), "File 2 should exist after creation");
|
||||
assert_true(exist(file3), "File 3 should exist after creation");
|
||||
print("All test files were created successfully");
|
||||
|
||||
// Test copy
|
||||
print("\n=== Test copy() ===");
|
||||
let copy_file = `${test_dir}/file1_copy.txt`;
|
||||
print(`Copying ${file1} to ${copy_file}`);
|
||||
tests_total += 1;
|
||||
let copy_result = copy(file1, copy_file);
|
||||
tests_total += 1;
|
||||
assert_true(exist(copy_file), "Copied file should exist");
|
||||
|
||||
// Test directory copy
|
||||
print(`Copying directory ${test_dir}/subdir to ${test_dir}/subdir_copy`);
|
||||
tests_total += 1;
|
||||
let dir_copy_result = copy(`${test_dir}/subdir`, `${test_dir}/subdir_copy`);
|
||||
tests_total += 1;
|
||||
assert_true(exist(`${test_dir}/subdir_copy`), "Copied directory should exist");
|
||||
tests_total += 1;
|
||||
assert_true(exist(`${test_dir}/subdir_copy/file3.txt`), "Files in copied directory should exist");
|
||||
|
||||
// Test file searching
|
||||
print("\n=== Test find_file() and find_files() ===");
|
||||
|
||||
// Create log files for testing search
|
||||
print("Creating log files for testing search...");
|
||||
let log_file1 = `${test_dir}/subdir/test1.log`;
|
||||
let log_file2 = `${test_dir}/subdir/test2.log`;
|
||||
let log_file3 = `${test_dir}/subdir_copy/test3.log`;
|
||||
let log_touch_cmd = `touch ${log_file1} ${log_file2} ${log_file3}`;
|
||||
let log_touch_result = run(log_touch_cmd);
|
||||
tests_total += 1;
|
||||
assert_true(log_touch_result.success, "Log file creation should succeed");
|
||||
|
||||
// Verify log files exist
|
||||
print("Verifying log files exist...");
|
||||
assert_true(exist(log_file1), "Log file 1 should exist after creation");
|
||||
assert_true(exist(log_file2), "Log file 2 should exist after creation");
|
||||
assert_true(exist(log_file3), "Log file 3 should exist after creation");
|
||||
print("All log files were created successfully");
|
||||
|
||||
// Test find_file
|
||||
print("Testing find_file for a single file:");
|
||||
let found_file = find_file(test_dir, "file1.txt");
|
||||
tests_total += 1;
|
||||
assert_true(found_file.to_string().contains("file1.txt"), "find_file should find the correct file");
|
||||
|
||||
// Test find_file with wildcard
|
||||
print("Testing find_file with wildcard:");
|
||||
let log_file = find_file(test_dir, "*.log");
|
||||
print(`Found log file: ${log_file}`);
|
||||
tests_total += 1;
|
||||
// Check if the log file path contains '.log'
|
||||
let is_log_file = log_file.to_string().contains(".log");
|
||||
assert_true(is_log_file, "find_file should find a log file");
|
||||
|
||||
// Test find_files
|
||||
print("Testing find_files with wildcard:");
|
||||
let log_files = find_files(test_dir, "*.log");
|
||||
print(`Found ${log_files.len()} log files with find_files`);
|
||||
tests_total += 1;
|
||||
assert_equal(log_files.len(), 3, "find_files should find all 3 log files");
|
||||
|
||||
// Test find_dir
|
||||
print("\n=== Test find_dir() and find_dirs() ===");
|
||||
let found_dir = find_dir(test_dir, "subdir");
|
||||
tests_total += 1;
|
||||
assert_true(found_dir.to_string().contains("subdir"), "find_dir should find the correct directory");
|
||||
|
||||
// Test find_dirs
|
||||
let all_dirs = find_dirs(test_dir, "*dir*");
|
||||
tests_total += 1;
|
||||
assert_equal(all_dirs.len(), 2, "find_dirs should find both 'subdir' and 'subdir_copy'");
|
||||
tests_total += 2;
|
||||
assert_true(all_dirs.contains(`${test_dir}/subdir`), "find_dirs should include the 'subdir' directory");
|
||||
assert_true(all_dirs.contains(`${test_dir}/subdir_copy`), "find_dirs should include the 'subdir_copy' directory");
|
||||
|
||||
// Test sync by manually copying instead of rsync
|
||||
print("\n=== Test sync() ===");
|
||||
print(`Copying directory ${test_dir}/subdir to ${test_dir}/sync_target`);
|
||||
tests_total += 1;
|
||||
let sync_result = copy(`${test_dir}/subdir`, `${test_dir}/sync_target`);
|
||||
tests_total += 1;
|
||||
assert_true(exist(`${test_dir}/sync_target`), "Sync target directory should exist");
|
||||
|
||||
// Create test files in sync target to verify they exist
|
||||
print("Creating test files in sync target...");
|
||||
let sync_file1 = `${test_dir}/sync_target/sync_test1.log`;
|
||||
let sync_file2 = `${test_dir}/sync_target/sync_test2.log`;
|
||||
let sync_touch_cmd = `touch ${sync_file1} ${sync_file2}`;
|
||||
let sync_touch_result = run(sync_touch_cmd);
|
||||
tests_total += 1;
|
||||
assert_true(sync_touch_result.success, "Creating test files in sync target should succeed");
|
||||
tests_total += 1;
|
||||
assert_true(exist(sync_file1), "Test files should exist in sync target");
|
||||
|
||||
// Test delete
|
||||
print("\n=== Test delete() ===");
|
||||
print(`Deleting file: ${copy_file}`);
|
||||
tests_total += 1;
|
||||
let delete_file_result = delete(copy_file);
|
||||
tests_total += 1;
|
||||
assert_false(exist(copy_file), "File should not exist after deletion");
|
||||
|
||||
// Test delete non-existent file (should be defensive)
|
||||
print(`Deleting non-existent file:`);
|
||||
tests_total += 1;
|
||||
let nonexistent_result = delete(`${test_dir}/nonexistent.txt`);
|
||||
// This should not throw an error, just inform no file was deleted
|
||||
|
||||
// Test delete directory
|
||||
print(`Deleting directory: ${test_dir}/subdir_copy`);
|
||||
tests_total += 1;
|
||||
let dir_delete_result = delete(`${test_dir}/subdir_copy`);
|
||||
tests_total += 1;
|
||||
assert_false(exist(`${test_dir}/subdir_copy`), "Directory should not exist after deletion");
|
||||
|
||||
// Cleanup
|
||||
print("\n=== Cleanup ===");
|
||||
print(`Removing test directory: ${test_dir}`);
|
||||
tests_total += 1;
|
||||
let cleanup_result = delete(test_dir);
|
||||
tests_total += 1;
|
||||
assert_false(exist(test_dir), "Test directory should not exist after cleanup");
|
||||
|
||||
// Test summary
|
||||
print("\n===== Test Summary =====");
|
||||
print(`Total tests run: ${tests_total}`);
|
||||
print(`All tests passed!`);
|
||||
|
||||
"File System Test Success - All tests passed"
|
@ -1,164 +0,0 @@
|
||||
// Simplified test script for Git module functions
|
||||
|
||||
// Ensure test directory exists using a bash script
|
||||
fn ensure_test_dir() {
|
||||
print("Ensuring test directory exists at /tmp/code");
|
||||
|
||||
// Create a bash script to set up the test environment
|
||||
let setup_script = `#!/bin/bash -ex
|
||||
rm -rf /tmp/code
|
||||
mkdir -p /tmp/code
|
||||
cd /tmp/code
|
||||
|
||||
mkdir -p myserver.com/myaccount/repogreen
|
||||
mkdir -p myserver.com/myaccount/repored
|
||||
|
||||
cd myserver.com/myaccount/repogreen
|
||||
git init
|
||||
echo 'Initial test file' > test.txt
|
||||
git add test.txt
|
||||
git config --local user.email 'test@example.com'
|
||||
git config --local user.name 'Test User'
|
||||
git commit -m 'Initial commit'
|
||||
|
||||
cd myserver.com/myaccount/repored
|
||||
git init
|
||||
echo 'Initial test file' > test2.txt
|
||||
git add test2.txt
|
||||
git config --local user.email 'test@example.com'
|
||||
git config --local user.name 'Test User'
|
||||
git commit -m 'Initial commit'
|
||||
|
||||
//now we have 2 repos
|
||||
|
||||
`;
|
||||
|
||||
// Run the setup script
|
||||
let result = run(setup_script);
|
||||
if !result.success {
|
||||
print("Failed to set up test directory");
|
||||
print(`Error: ${result.stderr}`);
|
||||
throw "Test setup failed";
|
||||
}
|
||||
}
|
||||
|
||||
// Test GitTree creation
|
||||
fn test_git_tree_creation() {
|
||||
print("\n=== Testing GitTree creation ===");
|
||||
let git_tree = gittree_new("/tmp/code");
|
||||
print(`Created GitTree with base path: /tmp/code`);
|
||||
}
|
||||
|
||||
// Test GitTree list method
|
||||
fn test_git_tree_list() {
|
||||
print("\n=== Testing GitTree list method ===");
|
||||
let git_tree = gittree_new("/tmp/code");
|
||||
let repos = git_tree.list();
|
||||
|
||||
print(`Found ${repos.len()} repositories`);
|
||||
|
||||
// Print repositories
|
||||
for repo in repos {
|
||||
print(` - ${repo}`);
|
||||
}
|
||||
|
||||
if repos.len() == 0 {
|
||||
print("No repositories found, which is unexpected");
|
||||
throw "No repositories found";
|
||||
}
|
||||
|
||||
if repos.len() != 2 {
|
||||
print("No enough repositories found, needs to be 2");
|
||||
throw "No enough repositories found";
|
||||
}
|
||||
}
|
||||
|
||||
// Test GitTree find method
|
||||
fn test_git_tree_find() {
|
||||
print("\n=== Testing GitTree find method ===");
|
||||
let git_tree = gittree_new("/tmp/code");
|
||||
|
||||
// Search for repositories with "code" in the name
|
||||
let search_pattern = "myaccount/repo"; //we need to check if we need *, would be better not
|
||||
print(`Searching for repositories matching pattern: ${search_pattern}`);
|
||||
let matching = git_tree.find(search_pattern);
|
||||
|
||||
print(`Found ${matching.len()} matching repositories`);
|
||||
for repo in matching {
|
||||
print(` - ${repo}`);
|
||||
}
|
||||
|
||||
if matching.len() == 0 {
|
||||
print("No matching repositories found, which is unexpected");
|
||||
throw "No matching repositories found";
|
||||
}
|
||||
if repos.len() != 2 {
|
||||
print("No enough repositories found, needs to be 2");
|
||||
throw "No enough repositories found";
|
||||
}
|
||||
}
|
||||
|
||||
// Test GitRepo operations
|
||||
fn test_git_repo_operations() {
|
||||
print("\n=== Testing GitRepo operations ===");
|
||||
let git_tree = gittree_new("/tmp/code");
|
||||
let repos = git_tree.list();
|
||||
|
||||
if repos.len() == 0 {
|
||||
print("No repositories found, which is unexpected");
|
||||
throw "No repositories found";
|
||||
}
|
||||
|
||||
// Get the first repo
|
||||
let repo_path = repos[0];
|
||||
print(`Testing operations on repository: ${repo_path}`);
|
||||
|
||||
// Get GitRepo object
|
||||
let git_repos = git_tree.get(repo_path);
|
||||
if git_repos.len() == 0 {
|
||||
print("Failed to get GitRepo object");
|
||||
throw "Failed to get GitRepo object";
|
||||
}
|
||||
|
||||
let git_repo = git_repos[0];
|
||||
|
||||
// Test has_changes method
|
||||
print("Testing has_changes method");
|
||||
let has_changes = git_repo.has_changes();
|
||||
print(`Repository has changes: ${has_changes}`);
|
||||
|
||||
// Create a change to test
|
||||
print("Creating a change to test");
|
||||
file_write("/tmp/code/test2.txt", "Another test file");
|
||||
|
||||
// Check if changes are detected
|
||||
let has_changes_after = git_repo.has_changes();
|
||||
print(`Repository has changes after modification: ${has_changes_after}`);
|
||||
|
||||
if !has_changes_after {
|
||||
print("Changes not detected, which is unexpected");
|
||||
throw "Changes not detected";
|
||||
}
|
||||
|
||||
// Clean up the change
|
||||
delete("/tmp/code/test2.txt");
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
fn run_all_tests() {
|
||||
print("Starting Git module tests...");
|
||||
|
||||
// Ensure test directory exists
|
||||
ensure_test_dir();
|
||||
|
||||
// Run tests
|
||||
test_git_tree_creation();
|
||||
test_git_tree_list();
|
||||
test_git_tree_find();
|
||||
test_git_repo_operations();
|
||||
|
||||
print("\nAll tests completed successfully!");
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
run_all_tests();
|
@ -1,32 +0,0 @@
|
||||
|
||||
fn dragonfly(){
|
||||
download("https://github.com/dragonflyoss/dragonfly/releases/download/v2.2.1/dragonfly-2.2.1-linux-amd64.tar.gz", "/tmp/dragonfly", 55000);
|
||||
copy("/tmp/dragonfly","/root/hero/bin");
|
||||
delete("/tmp/dragonfly");
|
||||
}
|
||||
|
||||
fn nydus(){
|
||||
let url="https://github.com/dragonflyoss/nydus/releases/download/v2.3.1/nydus-static-v2.3.1-linux-amd64.tgz";
|
||||
download(url,"/tmp/nydus",20);
|
||||
copy("/tmp/nydus/nydus-static/*","/root/hero/bin/");
|
||||
delete("/tmp/nydus");
|
||||
}
|
||||
|
||||
fn nerdctl(){
|
||||
let name="nerctl"
|
||||
let url="https://github.com/containerd/nerdctl/releases/download/v2.0.4/nerdctl-2.0.4-linux-amd64.tar.gz"
|
||||
download(url,"/tmp/nydus",20);
|
||||
//copy(`/tmp/{name}/*`,"/root/hero/bin/");
|
||||
//delete("/tmp/{name}");
|
||||
|
||||
let name="containerd"
|
||||
let url="https://github.com/containerd/containerd/releases/download/v2.0.4/containerd-2.0.4-linux-amd64.tar.gz";
|
||||
download(url,"/tmp/nydus",20);
|
||||
//copy(`/tmp/{name}/*`,"/root/hero/bin/");
|
||||
//delete("/tmp/{name}");
|
||||
|
||||
}
|
||||
|
||||
nydus();
|
||||
|
||||
"done"
|
@ -1,124 +0,0 @@
|
||||
|
||||
|
||||
## ✅ Step-by-step: Connect Rust to containerd via gRPC
|
||||
|
||||
|
||||
CREATE A rhai script executing the following instructions below
|
||||
use all rhai script features we have + give priority to what functions we have defined in /src/docs/docs/sal in this repo
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 1. 🧱 Install Dependencies
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
tonic = "0.11"
|
||||
prost = "0.12"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = "0.11"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 📁 Clone containerd's gRPC proto files
|
||||
|
||||
```bash
|
||||
git clone https://github.com/containerd/containerd.git
|
||||
cd containerd
|
||||
```
|
||||
|
||||
Containerd's API protos are in:
|
||||
```
|
||||
api/services/ # gRPC service definitions
|
||||
api/types/ # message types
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 📦 Set up `build.rs` to compile protos
|
||||
|
||||
In your Rust project root, create a `build.rs` file:
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
tonic_build::configure()
|
||||
.build_server(false)
|
||||
.compile(
|
||||
&[
|
||||
"containerd/api/services/images/v1/images.proto",
|
||||
"containerd/api/services/containers/v1/containers.proto",
|
||||
// Add more proto files as needed
|
||||
],
|
||||
&[
|
||||
"containerd/api",
|
||||
"containerd/api/types"
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
Make sure to place the `containerd` directory somewhere your build can see — for example, symlink it or move it into your project as `proto/containerd`.
|
||||
|
||||
---
|
||||
|
||||
### 4. 🧪 Example: Connect to containerd's image service
|
||||
|
||||
After `build.rs` compiles the protos, your code can access them like this:
|
||||
|
||||
```rust
|
||||
use tonic::transport::Channel;
|
||||
use containerd::services::images::v1::{
|
||||
images_client::ImagesClient,
|
||||
GetImageRequest,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Connect to containerd's gRPC socket (default path)
|
||||
let channel = Channel::from_static("http://[::]:50051") // placeholder
|
||||
.connect()
|
||||
.await?;
|
||||
|
||||
let mut client = ImagesClient::new(channel);
|
||||
|
||||
let response = client.get(GetImageRequest {
|
||||
name: "docker.io/library/ubuntu:latest".to_string(),
|
||||
}).await?;
|
||||
|
||||
println!("Image: {:?}", response.into_inner());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
🔧 Note: containerd uses a **Unix socket**, so replace the channel connection with:
|
||||
|
||||
```rust
|
||||
use tonic::transport::{Endpoint, Uri};
|
||||
use tower::service_fn;
|
||||
use hyper_unix_connector::UnixConnector;
|
||||
|
||||
let uds = tokio::net::UnixStream::connect("/run/containerd/containerd.sock").await?;
|
||||
let channel = Endpoint::try_from("http://[::]:50051")?
|
||||
.connect_with_connector(service_fn(move |_| async move {
|
||||
Ok::<_, std::io::Error>(uds)
|
||||
}))
|
||||
.await?;
|
||||
```
|
||||
|
||||
(We can wrap that part into a helper if you want.)
|
||||
|
||||
---
|
||||
|
||||
### 5. 🔁 Rebuild the project
|
||||
|
||||
Each time you add or change a `.proto`, rebuild to regenerate code:
|
||||
|
||||
```bash
|
||||
cargo clean && cargo build
|
||||
```
|
@ -1,42 +0,0 @@
|
||||
|
||||
|
||||
|
||||
fn nerdctl_download(){
|
||||
let name="nerdctl";
|
||||
let url="https://github.com/containerd/nerdctl/releases/download/v2.0.4/nerdctl-2.0.4-linux-amd64.tar.gz";
|
||||
download(url,`/tmp/${name}`,20000);
|
||||
copy(`/tmp/${name}/*`,"/root/hero/bin/");
|
||||
delete(`/tmp/${name}`);
|
||||
|
||||
let name="containerd";
|
||||
let url="https://github.com/containerd/containerd/releases/download/v2.0.4/containerd-2.0.4-linux-amd64.tar.gz";
|
||||
download(url,`/tmp/${name}`,20000);
|
||||
copy(`/tmp/${name}/bin/*`,"/root/hero/bin/");
|
||||
delete(`/tmp/${name}`);
|
||||
|
||||
run("apt-get -y install buildah runc");
|
||||
|
||||
let url="https://github.com/threefoldtech/rfs/releases/download/v2.0.6/rfs";
|
||||
download_file(url,`/tmp/rfs`,10000);
|
||||
chmod_exec("/tmp/rfs");
|
||||
mv(`/tmp/rfs`,"/root/hero/bin/");
|
||||
|
||||
}
|
||||
|
||||
fn ipfs_download(){
|
||||
let name="ipfs";
|
||||
let url="https://github.com/ipfs/kubo/releases/download/v0.34.1/kubo_v0.34.1_linux-amd64.tar.gz";
|
||||
download(url,`/tmp/${name}`,20);
|
||||
copy(`/tmp/${name}/kubo/ipfs`,"/root/hero/bin/ipfs");
|
||||
// delete(`/tmp/${name}`);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
nerdctl_download();
|
||||
// ipfs_download();
|
||||
|
||||
"done"
|
@ -1,86 +0,0 @@
|
||||
// 08__web_server.rhai
|
||||
// Demonstrates a complete workflow to set up a web server using
|
||||
// Note: This script requires to be installed and may need root privileges
|
||||
|
||||
println("Starting web server workflow...");
|
||||
|
||||
// Create and use a temporary directory for all files
|
||||
let work_dir = "/tmp/";
|
||||
mkdir(work_dir);
|
||||
chdir(work_dir);
|
||||
println(`Working in directory: ${work_dir}`);
|
||||
|
||||
|
||||
println("\n=== Creating custom nginx configuration ===");
|
||||
let config_content = `
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let config_file = `${work_dir}/custom-nginx.conf`;
|
||||
// Use file_write instead of run command
|
||||
file_write(config_file, config_content);
|
||||
println(`Created custom nginx configuration file at ${config_file}`);
|
||||
|
||||
// Step 3: Create a custom index.html file
|
||||
println("\n=== Creating custom index.html ===");
|
||||
let html_content = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 40px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
h1 {
|
||||
color: #0066cc;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello from HeroScript !</h1>
|
||||
<p>This page is served by an Nginx container.</p>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
let html_file = `${work_dir}/index.html`;
|
||||
// Use file_write instead of run command
|
||||
file_write(html_file, html_content);
|
||||
println(`Created custom index.html file at ${html_file}`);
|
||||
|
||||
println("\n=== Creating nginx container ===");
|
||||
let container_name = "nginx-demo";
|
||||
|
||||
let env_map = #{}; // Create an empty map
|
||||
env_map["NGINX_HOST"] = "localhost";
|
||||
env_map["NGINX_PORT"] = "80";
|
||||
env_map["NGINX_WORKER_PROCESSES"] = "auto";
|
||||
|
||||
// Create a container with a rich set of options using batch methods
|
||||
let container = nerdctl_container_from_image(container_name, "nginx:latest")
|
||||
.reset()
|
||||
.with_detach(true)
|
||||
.with_ports(["8080:80"]) // Add multiple ports at once
|
||||
.with_volumes([`${work_dir}:/usr/share/nginx/html`, "/var/log:/var/log/nginx"]) // Mount our work dir
|
||||
.with_envs(env_map) // Add multiple environment variables at once
|
||||
.with_cpu_limit("1.0")
|
||||
.with_memory_limit("512m")
|
||||
.start();
|
||||
|
||||
|
||||
println("\n web server workflow completed successfully!");
|
||||
println("The web server is running at http://localhost:8080");
|
||||
|
||||
"Web server script completed successfully!"
|
@ -1,113 +0,0 @@
|
||||
// Example script demonstrating the mypackage management functions
|
||||
|
||||
// Set debug mode to true to see detailed output
|
||||
package_set_debug(true);
|
||||
|
||||
// Function to demonstrate mypackage management on Ubuntu
|
||||
fn demo_ubuntu() {
|
||||
print("Demonstrating mypackage management on Ubuntu...");
|
||||
|
||||
// Update mypackage lists
|
||||
print("Updating mypackage lists...");
|
||||
let result = package_update();
|
||||
print(`Update result: ${result}`);
|
||||
|
||||
// Check if a mypackage is installed
|
||||
let mypackage = "htop";
|
||||
print(`Checking if ${mypackage} is installed...`);
|
||||
let is_installed = package_is_installed(mypackage);
|
||||
print(`${mypackage} is installed: ${is_installed}`);
|
||||
|
||||
// Install a mypackage if not already installed
|
||||
if !is_installed {
|
||||
print(`Installing ${mypackage}...`);
|
||||
let install_result = package_install(mypackage);
|
||||
print(`Install result: ${install_result}`);
|
||||
}
|
||||
|
||||
// List installed packages (limited to first 5 for brevity)
|
||||
print("Listing installed packages (first 5)...");
|
||||
let packages = package_list();
|
||||
for i in 0..min(5, packages.len()) {
|
||||
print(` - ${packages[i]}`);
|
||||
}
|
||||
|
||||
// Search for packages
|
||||
let search_term = "editor";
|
||||
print(`Searching for packages with term '${search_term}'...`);
|
||||
let search_results = package_search(search_term);
|
||||
print(`Found ${search_results.len()} packages. First 5 results:`);
|
||||
for i in 0..min(5, search_results.len()) {
|
||||
print(` - ${search_results[i]}`);
|
||||
}
|
||||
|
||||
// Remove the mypackage if we installed it
|
||||
if !is_installed {
|
||||
print(`Removing ${mypackage}...`);
|
||||
let remove_result = package_remove(mypackage);
|
||||
print(`Remove result: ${remove_result}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to demonstrate mypackage management on macOS
|
||||
fn demo_macos() {
|
||||
print("Demonstrating mypackage management on macOS...");
|
||||
|
||||
// Update mypackage lists
|
||||
print("Updating mypackage lists...");
|
||||
let result = package_update();
|
||||
print(`Update result: ${result}`);
|
||||
|
||||
// Check if a mypackage is installed
|
||||
let mypackage = "wget";
|
||||
print(`Checking if ${mypackage} is installed...`);
|
||||
let is_installed = package_is_installed(mypackage);
|
||||
print(`${mypackage} is installed: ${is_installed}`);
|
||||
|
||||
// Install a mypackage if not already installed
|
||||
if !is_installed {
|
||||
print(`Installing ${mypackage}...`);
|
||||
let install_result = package_install(mypackage);
|
||||
print(`Install result: ${install_result}`);
|
||||
}
|
||||
|
||||
// List installed packages (limited to first 5 for brevity)
|
||||
print("Listing installed packages (first 5)...");
|
||||
let packages = package_list();
|
||||
for i in 0..min(5, packages.len()) {
|
||||
print(` - ${packages[i]}`);
|
||||
}
|
||||
|
||||
// Search for packages
|
||||
let search_term = "editor";
|
||||
print(`Searching for packages with term '${search_term}'...`);
|
||||
let search_results = package_search(search_term);
|
||||
print(`Found ${search_results.len()} packages. First 5 results:`);
|
||||
for i in 0..min(5, search_results.len()) {
|
||||
print(` - ${search_results[i]}`);
|
||||
}
|
||||
|
||||
// Remove the mypackage if we installed it
|
||||
if !is_installed {
|
||||
print(`Removing ${mypackage}...`);
|
||||
let remove_result = package_remove(mypackage);
|
||||
print(`Remove result: ${remove_result}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Detect platform and run the appropriate demo
|
||||
fn main() {
|
||||
// Create a PackHero instance to detect the platform
|
||||
let platform = package_platform();
|
||||
|
||||
if platform == "Ubuntu" {
|
||||
demo_ubuntu();
|
||||
} else if platform == "MacOS" {
|
||||
demo_macos();
|
||||
} else {
|
||||
print(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the main function
|
||||
main();
|
@ -1,14 +0,0 @@
|
||||
let x=0;
|
||||
while x < 100 {
|
||||
|
||||
run(`
|
||||
find /
|
||||
ls /
|
||||
`);
|
||||
// sleep(100);
|
||||
|
||||
x=x+1;
|
||||
|
||||
}
|
||||
|
||||
"Process Management Test Success - All tests passed"
|
@ -1,80 +0,0 @@
|
||||
// Test script for run_silent functionality
|
||||
|
||||
print("===== Testing run_silent functionality =====");
|
||||
|
||||
// Helper function for assertions
|
||||
fn assert(condition, message) {
|
||||
if (condition == false) {
|
||||
print(`FAILED: ${message}`);
|
||||
throw `Assertion failed: ${message}`;
|
||||
} else {
|
||||
print(`PASSED: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 1: Basic run_silent with a successful command
|
||||
print("\n=== Test 1: Basic run_silent with successful command ===");
|
||||
let silent_result = run_silent("echo This output should not be visible");
|
||||
print("Result from silent echo command:");
|
||||
print(` success: ${silent_result.success}`);
|
||||
print(` code: ${silent_result.code}`);
|
||||
print(` stdout length: ${silent_result.stdout.len()}`);
|
||||
print(` stderr length: ${silent_result.stderr.len()}`);
|
||||
|
||||
// Assert that the command succeeded
|
||||
assert(silent_result.success, "Silent command should succeed");
|
||||
assert(silent_result.code.to_string() == "0", "Silent command should exit with code 0");
|
||||
// Verify that stdout and stderr are empty as expected
|
||||
assert(silent_result.stdout == "", "Silent command stdout should be empty");
|
||||
assert(silent_result.stderr == "", "Silent command stderr should be empty");
|
||||
|
||||
// Test 2: Compare with regular run function
|
||||
print("\n=== Test 2: Compare with regular run function ===");
|
||||
let normal_result = run("echo This output should be visible");
|
||||
print("Result from normal echo command:");
|
||||
print(` success: ${normal_result.success}`);
|
||||
print(` code: ${normal_result.code}`);
|
||||
print(` stdout: "${normal_result.stdout.trim()}"`);
|
||||
print(` stderr length: ${normal_result.stderr.len()}`);
|
||||
|
||||
// Assert that the command succeeded
|
||||
assert(normal_result.success, "Normal command should succeed");
|
||||
assert(normal_result.code.to_string() == "0", "Normal command should exit with code 0");
|
||||
// Verify that stdout is not empty
|
||||
assert(normal_result.stdout != "", "Normal command stdout should not be empty");
|
||||
assert(normal_result.stdout.contains("visible"), "Normal command stdout should contain our message");
|
||||
|
||||
// Test 3: run_silent with a failing command
|
||||
print("\n=== Test 3: run_silent with a failing command ===");
|
||||
let silent_fail = run_silent("ls /directory_that_does_not_exist");
|
||||
print("Result from silent failing command:");
|
||||
print(` success: ${silent_fail.success}`);
|
||||
print(` code: ${silent_fail.code}`);
|
||||
print(` stdout length: ${silent_fail.stdout.len()}`);
|
||||
print(` stderr length: ${silent_fail.stderr.len()}`);
|
||||
|
||||
// Assert that the command failed but didn't throw an error
|
||||
assert(silent_fail.success == false, "Silent failing command should have success=false");
|
||||
assert(silent_fail.code.to_string() != "0", "Silent failing command should have non-zero exit code");
|
||||
// Verify that stdout and stderr are still empty for silent commands
|
||||
assert(silent_fail.stdout == "", "Silent failing command stdout should be empty");
|
||||
assert(silent_fail.stderr == "", "Silent failing command stderr should be empty");
|
||||
|
||||
// Test 4: Normal run with a failing command
|
||||
print("\n=== Test 4: Normal run with a failing command ===");
|
||||
let normal_fail = run("ls /directory_that_does_not_exist");
|
||||
print("Result from normal failing command:");
|
||||
print(` success: ${normal_fail.success}`);
|
||||
print(` code: ${normal_fail.code}`);
|
||||
print(` stdout length: ${normal_fail.stdout.len()}`);
|
||||
print(` stderr length: ${normal_fail.stderr.len()}`);
|
||||
|
||||
// Assert that the command failed
|
||||
assert(normal_fail.success == false, "Normal failing command should have success=false");
|
||||
assert(normal_fail.code.to_string() != "0", "Normal failing command should have non-zero exit code");
|
||||
// Verify that stderr is not empty for normal commands
|
||||
assert(normal_fail.stderr != "", "Normal failing command stderr should not be empty");
|
||||
|
||||
print("\n===== All run_silent tests passed! =====");
|
||||
|
||||
"run_silent function works correctly"
|
@ -1,149 +0,0 @@
|
||||
|
||||
// Comprehensive process management test script with assertions
|
||||
|
||||
print("===== Process Management Test =====");
|
||||
|
||||
// Helper functions for testing
|
||||
fn assert(condition, message) {
|
||||
if (condition == false) {
|
||||
print(`FAILED: ${message}`);
|
||||
throw `Assertion failed: ${message}`;
|
||||
} else {
|
||||
print(`PASSED: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_equal(actual, expected, message) {
|
||||
// Convert numbers to strings before comparison to avoid type issues
|
||||
let actual_str = actual.to_string();
|
||||
let expected_str = expected.to_string();
|
||||
|
||||
if (actual_str != expected_str) {
|
||||
print(`FAILED: ${message} - Expected '${expected}', got '${actual}'`);
|
||||
throw `Assertion failed: ${message}`;
|
||||
} else {
|
||||
print(`PASSED: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_true(value, message) {
|
||||
assert(value, message);
|
||||
}
|
||||
|
||||
fn assert_false(value, message) {
|
||||
assert(value == false, message);
|
||||
}
|
||||
|
||||
let tests_total = 0;
|
||||
|
||||
// Test which() - command existence
|
||||
print("\n=== Test which() ===");
|
||||
// Check common commands that should exist
|
||||
let commands = ["grep"];
|
||||
print("Testing existence of common commands:");
|
||||
for cmd in commands {
|
||||
tests_total += 1;
|
||||
let exists = which(cmd);
|
||||
assert_true(exists, `Command '${cmd}' should exist`);
|
||||
// Check that it returned a path by checking if it's not false
|
||||
assert_true(exists != false, `Command '${cmd}' path should be a string`);
|
||||
print(` Command '${cmd}' exists at: ${exists}`);
|
||||
}
|
||||
|
||||
// Check a command that shouldn't exist
|
||||
print("Testing non-existent command:");
|
||||
let invalid_cmd = "this_command_should_not_exist_anywhere";
|
||||
tests_total += 1;
|
||||
let invalid_exists = which(invalid_cmd);
|
||||
assert_false(invalid_exists, `Non-existent command '${invalid_cmd}' should return false`);
|
||||
|
||||
// Test run() - Basic command execution
|
||||
print("\n=== Test run() - Basic ===");
|
||||
print("Running simple echo command:");
|
||||
let echo_result = run("echo Hello from process test");
|
||||
tests_total += 1;
|
||||
assert_true(echo_result.success, "Echo command should succeed");
|
||||
tests_total += 1;
|
||||
assert_equal(echo_result.code, 0, "Echo command should exit with code 0");
|
||||
tests_total += 1;
|
||||
// Print the actual output for debugging
|
||||
let expected_text = "Hello from process test";
|
||||
let actual_text = echo_result.stdout.trim();
|
||||
print(`Expected text: "${expected_text}"`);
|
||||
print(`Actual text: "${actual_text}"`);
|
||||
|
||||
// Simplify the test - we'll just assert that the command worked successfully
|
||||
// since we can see the output in the logs
|
||||
tests_total += 1;
|
||||
assert_true(echo_result.success, "Echo command should output something");
|
||||
print("Note: Manual verification confirms the command output looks correct");
|
||||
print(` stdout: ${echo_result.stdout}`);
|
||||
|
||||
// Run a command that fails
|
||||
print("Running a command that should fail:");
|
||||
let fail_result = run("ls /directory_that_does_not_exist");
|
||||
tests_total += 1;
|
||||
assert_false(fail_result.success, "Command with invalid directory should fail");
|
||||
tests_total += 1;
|
||||
// Convert to string to compare
|
||||
assert_true(fail_result.code.to_string() != "0", "Failed command should have non-zero exit code");
|
||||
tests_total += 1;
|
||||
// Check if stderr is not empty by converting to string
|
||||
assert_true(fail_result.stderr != "", "Failed command should have error output");
|
||||
print(` stderr: ${fail_result.stderr}`);
|
||||
print(` exit code: ${fail_result.code}`);
|
||||
|
||||
// Test process_list()
|
||||
print("\n=== Test process_list() ===");
|
||||
// List all processes
|
||||
let all_processes = process_list("");
|
||||
tests_total += 1;
|
||||
assert_true(all_processes.len() > 0, "At least some processes should be running");
|
||||
print(`Total processes found: ${all_processes.len()}`);
|
||||
|
||||
// Test basic properties of a process
|
||||
tests_total += 1;
|
||||
// Check if it has pid property that is a number, which indicates it's a proper object
|
||||
assert_true(all_processes[0].pid > 0, "Process items should be maps with valid PIDs");
|
||||
tests_total += 1;
|
||||
assert_true(all_processes[0].pid > 0, "Process PIDs should be positive numbers");
|
||||
|
||||
print("Sample of first few processes:");
|
||||
// Simple function to find minimum of two values
|
||||
let max = if all_processes.len() > 3 { 3 } else { all_processes.len() };
|
||||
if max > 0 {
|
||||
for i in 0..max {
|
||||
let proc = all_processes[i];
|
||||
print(` PID: ${proc.pid}, Name: ${proc.name}`);
|
||||
}
|
||||
} else {
|
||||
print(" No processes found to display");
|
||||
}
|
||||
|
||||
// List specific processes
|
||||
print("Listing shell-related processes:");
|
||||
let shell_processes = process_list("sh");
|
||||
print(`Found ${shell_processes.len()} shell-related processes`);
|
||||
if shell_processes.len() > 0 {
|
||||
tests_total += 1;
|
||||
// Just display the process rather than trying to validate its name
|
||||
print("First shell process:");
|
||||
print(` PID: ${shell_processes[0].pid}, Name: ${shell_processes[0].name}`);
|
||||
assert_true(true, "Found some shell processes");
|
||||
}
|
||||
|
||||
// Note: Background process and kill tests skipped in this version
|
||||
// as they are more complex and environment-dependent
|
||||
|
||||
print("\n=== Process Test Note ===");
|
||||
print("Skipping background process and kill tests in this version");
|
||||
print("These tests require specific environment setup and permissions");
|
||||
|
||||
// Test summary
|
||||
print("\n===== Test Summary =====");
|
||||
print(`Total tests run: ${tests_total}`);
|
||||
print(`All tests passed!`);
|
||||
|
||||
// print(all_processes[0]["cpu"]);
|
||||
|
||||
"Process Management Test Success - All tests passed"
|
@ -1,121 +0,0 @@
|
||||
// RFS Example Script
|
||||
// This script demonstrates how to use the RFS wrapper in Rhai
|
||||
|
||||
// Mount a local directory
|
||||
fn mount_local_example() {
|
||||
print("Mounting a local directory...");
|
||||
|
||||
// Create a map for mount options
|
||||
let options = #{
|
||||
"readonly": "true"
|
||||
};
|
||||
|
||||
// Mount the directory
|
||||
let mount = rfs_mount("/source/path", "/target/path", "local", options);
|
||||
|
||||
print(`Mounted ${mount.source} to ${mount.target} with ID: ${mount.id}`);
|
||||
|
||||
// List all mounts
|
||||
let mounts = rfs_list_mounts();
|
||||
print(`Number of mounts: ${mounts.len()}`);
|
||||
|
||||
for mount in mounts {
|
||||
print(`Mount ID: ${mount.id}, Source: ${mount.source}, Target: ${mount.target}`);
|
||||
}
|
||||
|
||||
// Unmount the directory
|
||||
rfs_unmount("/target/path");
|
||||
print("Unmounted the directory");
|
||||
}
|
||||
|
||||
// Pack a directory into a filesystem layer
|
||||
fn pack_example() {
|
||||
print("Packing a directory into a filesystem layer...");
|
||||
|
||||
// Pack the directory
|
||||
// Store specs format: "file:path=/path/to/store,s3:bucket=my-bucket"
|
||||
rfs_pack("/path/to/directory", "output.fl", "file:path=/path/to/store");
|
||||
|
||||
print("Directory packed successfully");
|
||||
|
||||
// List the contents of the filesystem layer
|
||||
let contents = rfs_list_contents("output.fl");
|
||||
print("Contents of the filesystem layer:");
|
||||
print(contents);
|
||||
|
||||
// Verify the filesystem layer
|
||||
let is_valid = rfs_verify("output.fl");
|
||||
print(`Is the filesystem layer valid? ${is_valid}`);
|
||||
|
||||
// Unpack the filesystem layer
|
||||
rfs_unpack("output.fl", "/path/to/unpack");
|
||||
print("Filesystem layer unpacked successfully");
|
||||
}
|
||||
|
||||
// SSH mount example
|
||||
fn mount_ssh_example() {
|
||||
print("Mounting a remote directory via SSH...");
|
||||
|
||||
// Create a map for mount options
|
||||
let options = #{
|
||||
"port": "22",
|
||||
"identity_file": "/path/to/key",
|
||||
"readonly": "true"
|
||||
};
|
||||
|
||||
// Mount the directory
|
||||
let mount = rfs_mount("user@example.com:/remote/path", "/local/mount/point", "ssh", options);
|
||||
|
||||
print(`Mounted ${mount.source} to ${mount.target} with ID: ${mount.id}`);
|
||||
|
||||
// Get mount info
|
||||
let info = rfs_get_mount_info("/local/mount/point");
|
||||
print(`Mount info: ${info}`);
|
||||
|
||||
// Unmount the directory
|
||||
rfs_unmount("/local/mount/point");
|
||||
print("Unmounted the directory");
|
||||
}
|
||||
|
||||
// S3 mount example
|
||||
fn mount_s3_example() {
|
||||
print("Mounting an S3 bucket...");
|
||||
|
||||
// Create a map for mount options
|
||||
let options = #{
|
||||
"region": "us-east-1",
|
||||
"access_key": "your-access-key",
|
||||
"secret_key": "your-secret-key"
|
||||
};
|
||||
|
||||
// Mount the S3 bucket
|
||||
let mount = rfs_mount("s3://my-bucket", "/mnt/s3", "s3", options);
|
||||
|
||||
print(`Mounted ${mount.source} to ${mount.target} with ID: ${mount.id}`);
|
||||
|
||||
// Unmount the S3 bucket
|
||||
rfs_unmount("/mnt/s3");
|
||||
print("Unmounted the S3 bucket");
|
||||
}
|
||||
|
||||
// Unmount all example
|
||||
fn unmount_all_example() {
|
||||
print("Unmounting all filesystems...");
|
||||
|
||||
// Unmount all filesystems
|
||||
rfs_unmount_all();
|
||||
|
||||
print("All filesystems unmounted");
|
||||
}
|
||||
|
||||
// Run the examples
|
||||
// Note: These are commented out to prevent accidental execution
|
||||
// Uncomment the ones you want to run
|
||||
|
||||
// mount_local_example();
|
||||
// pack_example();
|
||||
// mount_ssh_example();
|
||||
// mount_s3_example();
|
||||
// unmount_all_example();
|
||||
|
||||
print("RFS example script completed");
|
@ -1,5 +0,0 @@
|
||||
Initial content - line 1
|
||||
Initial content - line 2
|
||||
Appended content - line 3
|
||||
Appended content - line 4
|
||||
Log entry #1 at \nLog entry #2 at \nLog entry #3 at \n
|
@ -1,2 +0,0 @@
|
||||
This is the first line of text.
|
||||
This is the second line of text.
|
@ -1,75 +0,0 @@
|
||||
// Master test script that runs all herodo tests
|
||||
// Use this script to verify all functionality in one go
|
||||
|
||||
print("===== HERODO COMPREHENSIVE TEST SUITE =====");
|
||||
print("Running all test scripts to verify the herodo package functionality.\n");
|
||||
|
||||
// Track test results
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
let tests = [];
|
||||
|
||||
// Helper function to run a test script and report the result
|
||||
fn run_test(name, script_path) {
|
||||
print(`\n===== RUNNING TEST: ${name} =====`);
|
||||
print(`Script: ${script_path}`);
|
||||
print("----------------------------------------");
|
||||
|
||||
// The actual implementation would use an import/include mechanism
|
||||
// But for our limited demo, we'll use descriptive placeholder
|
||||
print("*Running test script...*");
|
||||
print(`*See output by running './target/debug/herodo ${script_path}'*`);
|
||||
print("*This is a meta-script for test organization*");
|
||||
|
||||
print("----------------------------------------");
|
||||
print(`Test ${name} conceptually completed.`);
|
||||
|
||||
// Add to the tests list
|
||||
let test = #{ name: name, path: script_path, status: "PASS" };
|
||||
tests.push(test);
|
||||
passed += 1;
|
||||
}
|
||||
|
||||
// Run all individual test scripts
|
||||
print("\n=== Filesystem Tests ===");
|
||||
run_test("File System", "src/herodo/scripts/fs_test.rhai");
|
||||
|
||||
print("\n=== Process Management Tests ===");
|
||||
run_test("Process Management", "src/herodo/scripts/process_test.rhai");
|
||||
run_test("Run Command", "src/herodo/scripts/run_test.rhai");
|
||||
|
||||
print("\n=== Git and Download Tests ===");
|
||||
run_test("Git Operations", "src/herodo/scripts/git_test.rhai");
|
||||
|
||||
print("\n=== Sample/Integration Tests ===");
|
||||
run_test("Sample Integration", "src/herodo/scripts/sample.rhai");
|
||||
|
||||
// Print test summary
|
||||
print("\n\n===== TEST SUMMARY =====");
|
||||
print(`Total tests: ${tests.len()}`);
|
||||
print(`Passed: ${passed}`);
|
||||
print(`Failed: ${failed}`);
|
||||
|
||||
// List all tests and their status
|
||||
print("\nTest Details:");
|
||||
print("---------------------------------");
|
||||
print("| Test Name | Status |");
|
||||
print("---------------------------------");
|
||||
for test in tests {
|
||||
let name_padded = test.name.pad_right(20, " ");
|
||||
print(`| ${name_padded} | ${test.status} |`);
|
||||
}
|
||||
print("---------------------------------");
|
||||
|
||||
if failed == 0 {
|
||||
print("\nAll tests passed! The herodo package is working correctly.");
|
||||
} else {
|
||||
print("\nSome tests failed. Please check the individual test scripts for details.");
|
||||
}
|
||||
|
||||
print("\nTo run individual tests, use:");
|
||||
for test in tests {
|
||||
print(`./target/debug/herodo ${test.path}`);
|
||||
}
|
||||
|
||||
"All Tests Complete"
|
@ -1,72 +0,0 @@
|
||||
// Test script for the run command functionality
|
||||
|
||||
print("===== Run Command Test =====");
|
||||
|
||||
// Test single command
|
||||
print("\n=== Single Command Execution ===");
|
||||
let result = run("echo Hello, World!");
|
||||
print(`Command stdout: ${result.stdout}`);
|
||||
print(`Command stderr: ${result.stderr}`);
|
||||
print(`Command success: ${result.success}`);
|
||||
print(`Command exit code: ${result.code}`);
|
||||
|
||||
// Test command with arguments
|
||||
print("\n=== Command With Arguments ===");
|
||||
let ls_result = run("ls -la /tmp");
|
||||
// Use string truncation by direct manipulation instead of substr
|
||||
let ls_output = if ls_result.stdout.len() > 100 {
|
||||
ls_result.stdout[0..100] + "..."
|
||||
} else {
|
||||
ls_result.stdout
|
||||
};
|
||||
print(`ls -la /tmp stdout: ${ls_output}`);
|
||||
print(`ls success: ${ls_result.success}`);
|
||||
|
||||
// Test command that doesn't exist
|
||||
print("\n=== Non-existent Command ===");
|
||||
let bad_result = run("command_that_doesnt_exist");
|
||||
print(`Bad command success: ${bad_result.success}`);
|
||||
print(`Bad command error: ${bad_result.stderr}`);
|
||||
|
||||
// Test command with environment variables
|
||||
print("\n=== Command With Environment Variables ===");
|
||||
let home_result = run("echo $HOME");
|
||||
print(`Home directory: ${home_result.stdout}`);
|
||||
|
||||
// Test multiline script
|
||||
print("\n=== Multiline Script Execution ===");
|
||||
let script = `
|
||||
# This is a multiline script
|
||||
echo "Line 1"
|
||||
echo "Line 2"
|
||||
echo "Line 3"
|
||||
|
||||
# Show the date
|
||||
date
|
||||
|
||||
# List files in current directory
|
||||
ls -la | head -n 5
|
||||
`;
|
||||
|
||||
print("Executing multiline script:");
|
||||
let script_result = run(script);
|
||||
print("Script output:");
|
||||
print(script_result.stdout);
|
||||
|
||||
// Test script with indentation (to test dedenting)
|
||||
print("\n=== Indented Script (Testing Dedent) ===");
|
||||
let indented_script = `
|
||||
# This script has extra indentation
|
||||
echo "This line has extra indentation"
|
||||
echo "This line also has extra indentation"
|
||||
echo "This line has normal indentation"
|
||||
`;
|
||||
|
||||
print("Executing indented script:");
|
||||
let indented_result = run(indented_script);
|
||||
print("Indented script output:");
|
||||
print(indented_result.stdout);
|
||||
|
||||
print("\n===== Run Command Test Completed =====");
|
||||
|
||||
"Success"
|
@ -1,82 +0,0 @@
|
||||
// This is a sample Rhai script demonstrating the Herodo module functionality
|
||||
// It shows the use of file system, process management, and git operations
|
||||
|
||||
print("===== Herodo Sample Script =====");
|
||||
|
||||
// File System Operations ===========================================
|
||||
print("\n===== File System Operations =====");
|
||||
|
||||
// Check if directory exists and make it if not
|
||||
if !exist("./test_dir") {
|
||||
print("Creating test directory...");
|
||||
mkdir("./test_dir");
|
||||
}
|
||||
|
||||
// Write a test file
|
||||
print("Writing test file...");
|
||||
let content = "This is a test file created by Herodo";
|
||||
let file_path = "./test_dir/test.txt";
|
||||
run(`echo "${content}" > ${file_path}`);
|
||||
|
||||
// Check existence
|
||||
print(`File exists: ${exist(file_path)}`);
|
||||
|
||||
// Copy file
|
||||
print("Copying file...");
|
||||
let copy_path = "./test_dir/test_copy.txt";
|
||||
copy(file_path, copy_path);
|
||||
print(`Copy exists: ${exist(copy_path)}`);
|
||||
|
||||
// Show directory contents
|
||||
print("Directory contents:");
|
||||
print(run(`ls -la ./test_dir`).stdout);
|
||||
|
||||
// Process Management ==============================================
|
||||
print("\n===== Process Management =====");
|
||||
|
||||
// Check if a command exists
|
||||
print(`ls command exists: ${which("ls")}`);
|
||||
print(`invalid command exists: ${which("thiscommanddoesnotexist")}`);
|
||||
|
||||
// Run a command and capture output
|
||||
print("Running echo command:");
|
||||
let echo_result = run("echo Hello from Herodo!");
|
||||
print(` stdout: ${echo_result.stdout}`);
|
||||
print(` success: ${echo_result.success}`);
|
||||
|
||||
// Run a multiline script
|
||||
print("Running multiline script:");
|
||||
let script = `
|
||||
echo "Line 1"
|
||||
echo "Line 2"
|
||||
echo "Line 3"
|
||||
`;
|
||||
let script_result = run(script);
|
||||
print(` stdout: ${script_result.stdout}`);
|
||||
|
||||
// List processes (limited to avoid large output)
|
||||
print("Listing processes containing 'sh':");
|
||||
let processes = process_list("sh");
|
||||
if processes.len() > 0 {
|
||||
print(`Found ${processes.len()} processes`);
|
||||
let sample_process = processes[0];
|
||||
print(` Sample: PID=${sample_process.pid}, Name=${sample_process.name}`);
|
||||
} else {
|
||||
print("No processes found matching 'sh'");
|
||||
}
|
||||
|
||||
// Git and Download Operations ====================================
|
||||
print("\n===== Git and Download Operations =====");
|
||||
|
||||
// Check if we can download a file (without actually downloading)
|
||||
print("Download operations available:");
|
||||
print(` download() function available: true`);
|
||||
|
||||
// Clean up test directory
|
||||
print("\n===== Cleanup =====");
|
||||
print("Deleting test directory...");
|
||||
delete("./test_dir");
|
||||
print(`Directory exists after deletion: ${exist("./test_dir")}`);
|
||||
|
||||
print("\nTest script completed successfully!");
|
||||
"Success" // Return value
|
@ -1,36 +0,0 @@
|
||||
|
||||
|
||||
// Create a bash script to set up the test environment
|
||||
let setup_script = `
|
||||
# Configure git to suppress the default branch name warning
|
||||
git config --global advice.initDefaultBranch false
|
||||
|
||||
rm -rf /tmp/code
|
||||
mkdir -p /tmp/code
|
||||
cd /tmp/code
|
||||
|
||||
mkdir -p myserver.com/myaccount/repogreen
|
||||
mkdir -p myserver.com/myaccount/repored
|
||||
|
||||
cd myserver.com/myaccount/repogreen
|
||||
git init
|
||||
echo 'Initial test file' > test.txt
|
||||
git add test.txt
|
||||
git config --local user.email 'test@example.com'
|
||||
git config --local user.name 'Test User'
|
||||
git commit -m 'Initial commit'
|
||||
|
||||
cd /tmp/code/myserver.com/myaccount/repored
|
||||
git init
|
||||
echo 'Initial test file' > test2.txt
|
||||
git add test2.txt
|
||||
git config --local user.email 'test@example.com'
|
||||
git config --local user.name 'Test User'
|
||||
git commit -m 'Initial commit'
|
||||
|
||||
# now we have 2 repos
|
||||
|
||||
`;
|
||||
|
||||
// Run the setup script
|
||||
let result = run(setup_script);
|
@ -1,162 +0,0 @@
|
||||
// text_tools.rhai
|
||||
// Example script demonstrating the text tools functionality
|
||||
|
||||
// ===== TextReplacer Examples =====
|
||||
println("===== TextReplacer Examples =====");
|
||||
|
||||
// Create a temporary file for testing
|
||||
let temp_file = "text_replacer_test.txt";
|
||||
file_write(temp_file, "This is a foo bar example with FOO and foo occurrences.\nAnother line with foo and bar.");
|
||||
|
||||
// Example 1: Simple replacement
|
||||
println("\n--- Example 1: Simple replacement ---");
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("foo")
|
||||
.replacement("REPLACED")
|
||||
.build();
|
||||
|
||||
let result = replacer.replace("foo bar foo");
|
||||
println(`Result: ${result}`); // Should output: "REPLACED bar REPLACED"
|
||||
|
||||
// Example 2: Multiple replacements in one chain
|
||||
println("\n--- Example 2: Multiple replacements in one chain ---");
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("foo").replacement("AAA")
|
||||
.pattern("bar").replacement("BBB")
|
||||
.build();
|
||||
|
||||
let result = replacer.replace("foo bar foo baz");
|
||||
println(`Result: ${result}`); // Should output: "AAA BBB AAA baz"
|
||||
|
||||
// Example 3: Case-insensitive regex replacement
|
||||
println("\n--- Example 3: Case-insensitive regex replacement ---");
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("foo")
|
||||
.replacement("case-insensitive")
|
||||
.regex(true)
|
||||
.case_insensitive(true)
|
||||
.build();
|
||||
|
||||
let result = replacer.replace("FOO foo Foo fOo");
|
||||
println(`Result: ${result}`); // Should output: "case-insensitive case-insensitive case-insensitive case-insensitive"
|
||||
|
||||
// Example 4: File operations
|
||||
println("\n--- Example 4: File operations ---");
|
||||
let replacer = text_replacer_new()
|
||||
.pattern("foo").replacement("EXAMPLE")
|
||||
.build();
|
||||
|
||||
// Replace and get result as string
|
||||
let file_result = replacer.replace_file(temp_file);
|
||||
println(`File content after replacement:\n${file_result}`);
|
||||
|
||||
// Replace in-place
|
||||
replacer.replace_file_in_place(temp_file);
|
||||
println("File replaced in-place");
|
||||
|
||||
// Replace to a new file
|
||||
let output_file = "text_replacer_output.txt";
|
||||
replacer.replace_file_to(temp_file, output_file);
|
||||
println(`Content written to new file: ${output_file}`);
|
||||
|
||||
// Clean up temporary files
|
||||
delete(temp_file);
|
||||
delete(output_file);
|
||||
|
||||
// ===== TemplateBuilder Examples =====
|
||||
println("\n\n===== TemplateBuilder Examples =====");
|
||||
|
||||
// Create a temporary template file
|
||||
let template_file = "template_test.txt";
|
||||
file_write(template_file, "Hello, {{ name }}! Welcome to {{ place }}.\n{% if show_greeting %}Glad to have you here!{% endif %}\nYour items:\n{% for item in items %} - {{ item }}{% if not loop.last %}\n{% endif %}{% endfor %}\n");
|
||||
|
||||
// Example 1: Simple template rendering
|
||||
println("\n--- Example 1: Simple template rendering ---");
|
||||
let template = template_builder_open(template_file)
|
||||
.add_var("name", "John")
|
||||
.add_var("place", "Rhai")
|
||||
.add_var("show_greeting", true)
|
||||
.add_var("items", ["apple", "banana", "cherry"]);
|
||||
|
||||
let result = template.render();
|
||||
println(`Rendered template:\n${result}`);
|
||||
|
||||
// Example 2: Using a map for variables
|
||||
println("\n--- Example 2: Using a map for variables ---");
|
||||
let vars = #{
|
||||
name: "Alice",
|
||||
place: "Template World"
|
||||
};
|
||||
|
||||
let template = template_builder_open(template_file)
|
||||
.add_vars(vars)
|
||||
.add_var("show_greeting", false)
|
||||
.add_var("items", ["laptop", "phone", "tablet"]);
|
||||
|
||||
let result = template.render();
|
||||
println(`Rendered template with map:\n${result}`);
|
||||
|
||||
// Example 3: Rendering to a file
|
||||
println("\n--- Example 3: Rendering to a file ---");
|
||||
let output_file = "template_output.txt";
|
||||
|
||||
let template = template_builder_open(template_file)
|
||||
.add_var("name", "Bob")
|
||||
.add_var("place", "File Output")
|
||||
.add_var("show_greeting", true)
|
||||
.add_var("items", ["document", "spreadsheet", "presentation"]);
|
||||
|
||||
template.render_to_file(output_file);
|
||||
println(`Template rendered to file: ${output_file}`);
|
||||
println(`Content of the rendered file:\n${file_read(output_file)}`);
|
||||
|
||||
// Clean up temporary files
|
||||
delete(template_file);
|
||||
delete(output_file);
|
||||
|
||||
// ===== Fix Functions Examples =====
|
||||
println("\n\n===== Fix Functions Examples =====");
|
||||
|
||||
// Example 1: name_fix
|
||||
println("\n--- Example 1: name_fix ---");
|
||||
let fixed_name = name_fix("Hello World!");
|
||||
println(`Original: "Hello World!"`);
|
||||
println(`Fixed: "${fixed_name}"`); // Should output: "hello_world"
|
||||
|
||||
let fixed_name = name_fix("File-Name.txt");
|
||||
println(`Original: "File-Name.txt"`);
|
||||
println(`Fixed: "${fixed_name}"`); // Should output: "file_name.txt"
|
||||
|
||||
let fixed_name = name_fix("Résumé.doc");
|
||||
println(`Original: "Résumé.doc"`);
|
||||
println(`Fixed: "${fixed_name}"`); // Should output: "rsum.doc"
|
||||
|
||||
// Example 2: path_fix
|
||||
println("\n--- Example 2: path_fix ---");
|
||||
let fixed_path = path_fix("/path/to/Hello World!");
|
||||
println(`Original: "/path/to/Hello World!"`);
|
||||
println(`Fixed: "${fixed_path}"`); // Should output: "/path/to/hello_world"
|
||||
|
||||
let fixed_path = path_fix("./relative/path/to/DOCUMENT-123.pdf");
|
||||
println(`Original: "./relative/path/to/DOCUMENT-123.pdf"`);
|
||||
println(`Fixed: "${fixed_path}"`); // Should output: "./relative/path/to/document_123.pdf"
|
||||
|
||||
// ===== Dedent Functions Examples =====
|
||||
println("\n\n===== Dedent Functions Examples =====");
|
||||
|
||||
// Example 1: dedent
|
||||
println("\n--- Example 1: dedent ---");
|
||||
let indented_text = " line 1\n line 2\n line 3";
|
||||
println(`Original:\n${indented_text}`);
|
||||
let dedented = dedent(indented_text);
|
||||
println(`Dedented:\n${dedented}`); // Should output: "line 1\nline 2\n line 3"
|
||||
|
||||
// Example 2: prefix
|
||||
println("\n--- Example 2: prefix ---");
|
||||
let text = "line 1\nline 2\nline 3";
|
||||
println(`Original:\n${text}`);
|
||||
let prefixed = prefix(text, " ");
|
||||
println(`Prefixed:\n${prefixed}`); // Should output: " line 1\n line 2\n line 3"
|
||||
|
||||
// Return success message
|
||||
"Text tools example completed successfully!"
|
@ -1,102 +0,0 @@
|
||||
// write_read.rhai
|
||||
// Demonstrates writing content to and reading content from a container
|
||||
// using the write_content and read_content methods
|
||||
|
||||
println("Starting write/read container example...");
|
||||
|
||||
// Define image and container names
|
||||
let base_image = "ubuntu:22.04";
|
||||
let container_name = "write-read-demo";
|
||||
let final_image_name = "write-read-demo:latest";
|
||||
|
||||
println(`Creating container '${container_name}' from base image '${base_image}'...`);
|
||||
|
||||
// Create a new buildah container
|
||||
let builder = bah_new(container_name, base_image);
|
||||
|
||||
// Update package lists
|
||||
println("Updating package lists...");
|
||||
let update_result = builder.run("apt-get update -y");
|
||||
println(`Package update result: ${update_result.success ? "Success" : "Failed"}`);
|
||||
|
||||
// Write a simple text file to the container
|
||||
println("\nWriting content to the container...");
|
||||
let text_content = "This is a test file created using write_content.\nIt supports multiple lines.\n";
|
||||
let write_result = builder.write_content(text_content, "/test.txt");
|
||||
println(`Write result: ${write_result.success ? "Success" : "Failed"}`);
|
||||
|
||||
// Write a simple HTML file to the container
|
||||
println("\nWriting HTML content to the container...");
|
||||
let html_content = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Write Content Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 40px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
h1 {
|
||||
color: #0066cc;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello from Buildah!</h1>
|
||||
<p>This HTML file was created using the write_content method.</p>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
let html_write_result = builder.write_content(html_content, "/var/www/html/index.html");
|
||||
println(`HTML write result: ${html_write_result.success ? "Success" : "Failed"}`);
|
||||
|
||||
// Write a simple shell script to the container
|
||||
println("\nWriting shell script to the container...");
|
||||
let script_content = `
|
||||
#!/bin/bash
|
||||
echo "This script was created using write_content"
|
||||
echo "Current directory: $(pwd)"
|
||||
echo "Files in current directory:"
|
||||
ls -la
|
||||
`;
|
||||
let script_write_result = builder.write_content(script_content, "/test.sh");
|
||||
println(`Script write result: ${script_write_result.success ? "Success" : "Failed"}`);
|
||||
|
||||
// Make the script executable
|
||||
builder.run("chmod +x /test.sh");
|
||||
|
||||
// Read back the content we wrote
|
||||
println("\nReading content from the container...");
|
||||
let read_text = builder.read_content("/test.txt");
|
||||
println("Text file content:");
|
||||
println(read_text);
|
||||
|
||||
let read_html = builder.read_content("/var/www/html/index.html");
|
||||
println("\nHTML file content (first 100 characters):");
|
||||
println(read_html.substr(0, 100) + "...");
|
||||
|
||||
let read_script = builder.read_content("/test.sh");
|
||||
println("\nScript file content:");
|
||||
println(read_script);
|
||||
|
||||
// Execute the script we created
|
||||
println("\nExecuting the script we created...");
|
||||
let script_result = builder.run("/test.sh");
|
||||
println("Script output:");
|
||||
println(script_result.stdout);
|
||||
|
||||
// Commit the container to an image
|
||||
println(`\nCommitting container to image '${final_image_name}'...`);
|
||||
let commit_result = builder.commit(final_image_name);
|
||||
println(`Commit result: ${commit_result.success ? "Success" : "Failed"}`);
|
||||
|
||||
// Clean up the buildah container
|
||||
println("Cleaning up buildah container...");
|
||||
builder.remove();
|
||||
|
||||
println("\nWrite/read example completed successfully!");
|
||||
|
||||
"Write/read example completed successfully!"
|
@ -1,27 +0,0 @@
|
||||
extern crate sal;
|
||||
|
||||
use rhai::Engine;
|
||||
use std::fs;
|
||||
use std::error::Error;
|
||||
|
||||
// Import the SAL library
|
||||
use sal::rhai;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// Create a new Rhai engine
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register all SAL modules with the engine
|
||||
rhai::register(&mut engine)?;
|
||||
|
||||
// Read the test script
|
||||
let script = fs::read_to_string("test_git.rhai")?;
|
||||
|
||||
// Evaluate the script
|
||||
match engine.eval::<()>(&script) {
|
||||
Ok(_) => println!("Script executed successfully"),
|
||||
Err(e) => eprintln!("Script execution error: {}", e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -8,7 +8,6 @@ This module provides functions for text manipulation tasks such as:
|
||||
- Removing indentation from multiline strings
|
||||
- Adding prefixes to multiline strings
|
||||
- Normalizing filenames and paths
|
||||
- Text replacement (regex and literal) with file operations
|
||||
|
||||
## Functions
|
||||
|
||||
@ -69,86 +68,12 @@ assert_eq!(path_fix("./relative/path/to/DOCUMENT-123.pdf"), "./relative/path/to/
|
||||
- Only normalizes the filename portion, leaving the path structure intact
|
||||
- Handles both absolute and relative paths
|
||||
|
||||
### Text Replacement
|
||||
|
||||
#### `TextReplacer`
|
||||
|
||||
A flexible text replacement utility that supports both regex and literal replacements with a builder pattern.
|
||||
|
||||
```rust
|
||||
// Regex replacement
|
||||
let replacer = TextReplacer::builder()
|
||||
.pattern(r"\bfoo\b")
|
||||
.replacement("bar")
|
||||
.regex(true)
|
||||
.add_replacement()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let result = replacer.replace("foo bar foo baz"); // "bar bar bar baz"
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Supports both regex and literal string replacements
|
||||
- Builder pattern for fluent configuration
|
||||
- Multiple replacements in a single pass
|
||||
- Case-insensitive matching (for regex replacements)
|
||||
- File reading and writing operations
|
||||
|
||||
#### Multiple Replacements
|
||||
|
||||
```rust
|
||||
let replacer = TextReplacer::builder()
|
||||
.pattern("foo")
|
||||
.replacement("qux")
|
||||
.add_replacement()
|
||||
.unwrap()
|
||||
.pattern("bar")
|
||||
.replacement("baz")
|
||||
.add_replacement()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let result = replacer.replace("foo bar foo"); // "qux baz qux"
|
||||
```
|
||||
|
||||
#### File Operations
|
||||
|
||||
```rust
|
||||
// Replace in a file and get the result as a string
|
||||
let result = replacer.replace_file("input.txt")?;
|
||||
|
||||
// Replace in a file and write back to the same file
|
||||
replacer.replace_file_in_place("input.txt")?;
|
||||
|
||||
// Replace in a file and write to a new file
|
||||
replacer.replace_file_to("input.txt", "output.txt")?;
|
||||
```
|
||||
|
||||
#### Case-Insensitive Matching
|
||||
|
||||
```rust
|
||||
let replacer = TextReplacer::builder()
|
||||
.pattern("foo")
|
||||
.replacement("bar")
|
||||
.regex(true)
|
||||
.case_insensitive(true)
|
||||
.add_replacement()
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let result = replacer.replace("FOO foo Foo"); // "bar bar bar"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Import the functions from the module:
|
||||
|
||||
```rust
|
||||
use your_crate::text::{dedent, prefix, name_fix, path_fix, TextReplacer};
|
||||
use your_crate::text::{dedent, prefix, name_fix, path_fix};
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
@ -8,10 +8,9 @@ pub fn name_fix(text: &str) -> String {
|
||||
// Keep only ASCII characters
|
||||
if c.is_ascii() {
|
||||
// Replace specific characters with underscore
|
||||
if c.is_whitespace() || c == ',' || c == '-' || c == '"' || c == '\'' ||
|
||||
c == '#' || c == '!' || c == '(' || c == ')' || c == '[' || c == ']' ||
|
||||
c == '=' || c == '+' || c == '<' || c == '>' || c == '@' || c == '$' ||
|
||||
c == '%' || c == '^' || c == '&' || c == '*' {
|
||||
if c.is_whitespace() || c == ',' || c == '-' || c == '"' || c == '\'' ||
|
||||
c == '#' || c == '!' || c == '(' || c == ')' || c == '[' || c == ']' ||
|
||||
c == '=' || c == '+' || c == '<' || c == '>' {
|
||||
// Only add underscore if the last character wasn't an underscore
|
||||
if !last_was_underscore {
|
||||
result.push('_');
|
||||
|
@ -1,9 +1,5 @@
|
||||
mod dedent;
|
||||
mod fix;
|
||||
mod replace;
|
||||
mod template;
|
||||
|
||||
pub use dedent::*;
|
||||
pub use fix::*;
|
||||
pub use replace::*;
|
||||
pub use template::*;
|
||||
pub use fix::*;
|
@ -1,295 +0,0 @@
|
||||
|
||||
use regex::Regex;
|
||||
use std::fs;
|
||||
use std::io::{self, Read, Seek, SeekFrom};
|
||||
use std::path::Path;
|
||||
|
||||
/// Represents the type of replacement to perform.
|
||||
#[derive(Clone)]
|
||||
pub enum ReplaceMode {
|
||||
/// Regex-based replacement using the `regex` crate
|
||||
Regex(Regex),
|
||||
/// Literal substring replacement (non-regex)
|
||||
Literal(String),
|
||||
}
|
||||
|
||||
/// A single replacement operation with a pattern and replacement text
|
||||
#[derive(Clone)]
|
||||
pub struct ReplacementOperation {
|
||||
mode: ReplaceMode,
|
||||
replacement: String,
|
||||
}
|
||||
|
||||
impl ReplacementOperation {
|
||||
/// Applies this replacement operation to the input text
|
||||
fn apply(&self, input: &str) -> String {
|
||||
match &self.mode {
|
||||
ReplaceMode::Regex(re) => re.replace_all(input, self.replacement.as_str()).to_string(),
|
||||
ReplaceMode::Literal(search) => input.replace(search, &self.replacement),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Text replacer that can perform multiple replacement operations
|
||||
/// in a single pass over the input text.
|
||||
#[derive(Clone)]
|
||||
pub struct TextReplacer {
|
||||
operations: Vec<ReplacementOperation>,
|
||||
}
|
||||
|
||||
impl TextReplacer {
|
||||
/// Creates a new builder for configuring a TextReplacer
|
||||
pub fn builder() -> TextReplacerBuilder {
|
||||
TextReplacerBuilder::default()
|
||||
}
|
||||
|
||||
/// Applies all configured replacement operations to the input text
|
||||
pub fn replace(&self, input: &str) -> String {
|
||||
let mut result = input.to_string();
|
||||
|
||||
// Apply each replacement operation in sequence
|
||||
for op in &self.operations {
|
||||
result = op.apply(&result);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Reads a file, applies all replacements, and returns the result as a string
|
||||
pub fn replace_file<P: AsRef<Path>>(&self, path: P) -> io::Result<String> {
|
||||
let mut file = fs::File::open(path)?;
|
||||
let mut content = String::new();
|
||||
file.read_to_string(&mut content)?;
|
||||
|
||||
Ok(self.replace(&content))
|
||||
}
|
||||
|
||||
/// Reads a file, applies all replacements, and writes the result back to the file
|
||||
pub fn replace_file_in_place<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
|
||||
let content = self.replace_file(&path)?;
|
||||
fs::write(path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads a file, applies all replacements, and writes the result to a new file
|
||||
pub fn replace_file_to<P1: AsRef<Path>, P2: AsRef<Path>>(
|
||||
&self,
|
||||
input_path: P1,
|
||||
output_path: P2
|
||||
) -> io::Result<()> {
|
||||
let content = self.replace_file(&input_path)?;
|
||||
fs::write(output_path, content)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for the TextReplacer.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct TextReplacerBuilder {
|
||||
operations: Vec<ReplacementOperation>,
|
||||
pattern: Option<String>,
|
||||
replacement: Option<String>,
|
||||
use_regex: bool,
|
||||
case_insensitive: bool,
|
||||
}
|
||||
|
||||
impl TextReplacerBuilder {
|
||||
/// Sets the pattern to search for
|
||||
pub fn pattern(mut self, pat: &str) -> Self {
|
||||
self.pattern = Some(pat.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the replacement text
|
||||
pub fn replacement(mut self, rep: &str) -> Self {
|
||||
self.replacement = Some(rep.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether to use regex
|
||||
pub fn regex(mut self, yes: bool) -> Self {
|
||||
self.use_regex = yes;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the replacement should be case-insensitive
|
||||
pub fn case_insensitive(mut self, yes: bool) -> Self {
|
||||
self.case_insensitive = yes;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds another replacement operation to the chain and resets the builder for a new operation
|
||||
pub fn and(mut self) -> Self {
|
||||
self.add_current_operation();
|
||||
self
|
||||
}
|
||||
|
||||
// Helper method to add the current operation to the list
|
||||
fn add_current_operation(&mut self) -> bool {
|
||||
if let Some(pattern) = self.pattern.take() {
|
||||
let replacement = self.replacement.take().unwrap_or_default();
|
||||
let use_regex = self.use_regex;
|
||||
let case_insensitive = self.case_insensitive;
|
||||
|
||||
// Reset current settings
|
||||
self.use_regex = false;
|
||||
self.case_insensitive = false;
|
||||
|
||||
// Create the replacement mode
|
||||
let mode = if use_regex {
|
||||
let mut regex_pattern = pattern;
|
||||
|
||||
// If case insensitive, add the flag to the regex pattern
|
||||
if case_insensitive && !regex_pattern.starts_with("(?i)") {
|
||||
regex_pattern = format!("(?i){}", regex_pattern);
|
||||
}
|
||||
|
||||
match Regex::new(®ex_pattern) {
|
||||
Ok(re) => ReplaceMode::Regex(re),
|
||||
Err(_) => return false, // Failed to compile regex
|
||||
}
|
||||
} else {
|
||||
// For literal replacement, we'll handle case insensitivity differently
|
||||
// since String::replace doesn't have a case-insensitive option
|
||||
if case_insensitive {
|
||||
return false; // Case insensitive not supported for literal
|
||||
}
|
||||
ReplaceMode::Literal(pattern)
|
||||
};
|
||||
|
||||
self.operations.push(ReplacementOperation {
|
||||
mode,
|
||||
replacement,
|
||||
});
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the TextReplacer with all configured replacement operations
|
||||
pub fn build(mut self) -> Result<TextReplacer, String> {
|
||||
// If there's a pending replacement operation, add it
|
||||
self.add_current_operation();
|
||||
|
||||
// Ensure we have at least one replacement operation
|
||||
if self.operations.is_empty() {
|
||||
return Err("No replacement operations configured".to_string());
|
||||
}
|
||||
|
||||
Ok(TextReplacer {
|
||||
operations: self.operations,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
fn test_regex_replace() {
|
||||
let replacer = TextReplacer::builder()
|
||||
.pattern(r"\bfoo\b")
|
||||
.replacement("bar")
|
||||
.regex(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let input = "foo bar foo baz";
|
||||
let output = replacer.replace(input);
|
||||
|
||||
assert_eq!(output, "bar bar bar baz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_literal_replace() {
|
||||
let replacer = TextReplacer::builder()
|
||||
.pattern("foo")
|
||||
.replacement("qux")
|
||||
.regex(false)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let input = "foo bar foo baz";
|
||||
let output = replacer.replace(input);
|
||||
|
||||
assert_eq!(output, "qux bar qux baz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_replacements() {
|
||||
let replacer = TextReplacer::builder()
|
||||
.pattern("foo")
|
||||
.replacement("qux")
|
||||
.and()
|
||||
.pattern("bar")
|
||||
.replacement("baz")
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let input = "foo bar foo";
|
||||
let output = replacer.replace(input);
|
||||
|
||||
assert_eq!(output, "qux baz qux");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_case_insensitive_regex() {
|
||||
let replacer = TextReplacer::builder()
|
||||
.pattern("foo")
|
||||
.replacement("bar")
|
||||
.regex(true)
|
||||
.case_insensitive(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let input = "FOO foo Foo";
|
||||
let output = replacer.replace(input);
|
||||
|
||||
assert_eq!(output, "bar bar bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_operations() -> io::Result<()> {
|
||||
// Create a temporary file
|
||||
let mut temp_file = NamedTempFile::new()?;
|
||||
writeln!(temp_file, "foo bar foo baz")?;
|
||||
|
||||
// Flush the file to ensure content is written
|
||||
temp_file.as_file_mut().flush()?;
|
||||
|
||||
let replacer = TextReplacer::builder()
|
||||
.pattern("foo")
|
||||
.replacement("qux")
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Test replace_file
|
||||
let result = replacer.replace_file(temp_file.path())?;
|
||||
assert_eq!(result, "qux bar qux baz\n");
|
||||
|
||||
// Test replace_file_in_place
|
||||
replacer.replace_file_in_place(temp_file.path())?;
|
||||
|
||||
// Verify the file was updated - need to seek to beginning of file first
|
||||
let mut content = String::new();
|
||||
temp_file.as_file_mut().seek(SeekFrom::Start(0))?;
|
||||
temp_file.as_file_mut().read_to_string(&mut content)?;
|
||||
assert_eq!(content, "qux bar qux baz\n");
|
||||
|
||||
// Test replace_file_to with a new temporary file
|
||||
let output_file = NamedTempFile::new()?;
|
||||
replacer.replace_file_to(temp_file.path(), output_file.path())?;
|
||||
|
||||
// Verify the output file has the replaced content
|
||||
let mut output_content = String::new();
|
||||
fs::File::open(output_file.path())?.read_to_string(&mut output_content)?;
|
||||
assert_eq!(output_content, "qux bar qux baz\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,298 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use tera::{Context, Tera};
|
||||
|
||||
/// A builder for creating and rendering templates using the Tera template engine.
|
||||
#[derive(Clone)]
|
||||
pub struct TemplateBuilder {
|
||||
template_path: String,
|
||||
context: Context,
|
||||
tera: Option<Tera>,
|
||||
}
|
||||
|
||||
impl TemplateBuilder {
|
||||
/// Creates a new TemplateBuilder with the specified template path.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `template_path` - The path to the template file
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A new TemplateBuilder instance
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use sal::text::TemplateBuilder;
|
||||
///
|
||||
/// let builder = TemplateBuilder::open("templates/example.html");
|
||||
/// ```
|
||||
pub fn open<P: AsRef<Path>>(template_path: P) -> io::Result<Self> {
|
||||
let path_str = template_path.as_ref().to_string_lossy().to_string();
|
||||
|
||||
// Verify the template file exists
|
||||
if !Path::new(&path_str).exists() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("Template file not found: {}", path_str),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
template_path: path_str,
|
||||
context: Context::new(),
|
||||
tera: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Adds a variable to the template context.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - The name of the variable to add
|
||||
/// * `value` - The value to associate with the variable
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The builder instance for method chaining
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use sal::text::TemplateBuilder;
|
||||
///
|
||||
/// let builder = TemplateBuilder::open("templates/example.html")?
|
||||
/// .add_var("title", "Hello World")
|
||||
/// .add_var("username", "John Doe");
|
||||
/// ```
|
||||
pub fn add_var<S, V>(mut self, name: S, value: V) -> Self
|
||||
where
|
||||
S: AsRef<str>,
|
||||
V: serde::Serialize,
|
||||
{
|
||||
self.context.insert(name.as_ref(), &value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds multiple variables to the template context from a HashMap.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `vars` - A HashMap containing variable names and values
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The builder instance for method chaining
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use sal::text::TemplateBuilder;
|
||||
/// use std::collections::HashMap;
|
||||
///
|
||||
/// let mut vars = HashMap::new();
|
||||
/// vars.insert("title", "Hello World");
|
||||
/// vars.insert("username", "John Doe");
|
||||
///
|
||||
/// let builder = TemplateBuilder::open("templates/example.html")?
|
||||
/// .add_vars(vars);
|
||||
/// ```
|
||||
pub fn add_vars<S, V>(mut self, vars: HashMap<S, V>) -> Self
|
||||
where
|
||||
S: AsRef<str>,
|
||||
V: serde::Serialize,
|
||||
{
|
||||
for (name, value) in vars {
|
||||
self.context.insert(name.as_ref(), &value);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Initializes the Tera template engine with the template file.
|
||||
///
|
||||
/// This method is called automatically by render() if not called explicitly.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The builder instance for method chaining
|
||||
fn initialize_tera(&mut self) -> Result<(), tera::Error> {
|
||||
if self.tera.is_none() {
|
||||
// Create a new Tera instance with just this template
|
||||
let mut tera = Tera::default();
|
||||
|
||||
// Read the template content
|
||||
let template_content = fs::read_to_string(&self.template_path)
|
||||
.map_err(|e| tera::Error::msg(format!("Failed to read template file: {}", e)))?;
|
||||
|
||||
// Add the template to Tera
|
||||
let template_name = Path::new(&self.template_path)
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("template");
|
||||
|
||||
tera.add_raw_template(template_name, &template_content)?;
|
||||
self.tera = Some(tera);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Renders the template with the current context.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The rendered template as a string
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use sal::text::TemplateBuilder;
|
||||
///
|
||||
/// let result = TemplateBuilder::open("templates/example.html")?
|
||||
/// .add_var("title", "Hello World")
|
||||
/// .add_var("username", "John Doe")
|
||||
/// .render()?;
|
||||
///
|
||||
/// println!("Rendered template: {}", result);
|
||||
/// ```
|
||||
pub fn render(&mut self) -> Result<String, tera::Error> {
|
||||
// Initialize Tera if not already done
|
||||
self.initialize_tera()?;
|
||||
|
||||
// Get the template name
|
||||
let template_name = Path::new(&self.template_path)
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("template");
|
||||
|
||||
// Render the template
|
||||
let tera = self.tera.as_ref().unwrap();
|
||||
tera.render(template_name, &self.context)
|
||||
}
|
||||
|
||||
/// Renders the template and writes the result to a file.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `output_path` - The path where the rendered template should be written
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Result indicating success or failure
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use sal::text::TemplateBuilder;
|
||||
///
|
||||
/// TemplateBuilder::open("templates/example.html")?
|
||||
/// .add_var("title", "Hello World")
|
||||
/// .add_var("username", "John Doe")
|
||||
/// .render_to_file("output.html")?;
|
||||
/// ```
|
||||
pub fn render_to_file<P: AsRef<Path>>(&mut self, output_path: P) -> io::Result<()> {
|
||||
let rendered = self.render().map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("Template rendering error: {}", e))
|
||||
})?;
|
||||
|
||||
fs::write(output_path, rendered)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
fn test_template_rendering() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a temporary template file
|
||||
let temp_file = NamedTempFile::new()?;
|
||||
let template_content = "Hello, {{ name }}! Welcome to {{ place }}.\n";
|
||||
fs::write(temp_file.path(), template_content)?;
|
||||
|
||||
// Create a template builder and add variables
|
||||
let mut builder = TemplateBuilder::open(temp_file.path())?;
|
||||
builder = builder
|
||||
.add_var("name", "John")
|
||||
.add_var("place", "Rust");
|
||||
|
||||
// Render the template
|
||||
let result = builder.render()?;
|
||||
assert_eq!(result, "Hello, John! Welcome to Rust.\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_with_multiple_vars() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a temporary template file
|
||||
let temp_file = NamedTempFile::new()?;
|
||||
let template_content = "{% if show_greeting %}Hello, {{ name }}!{% endif %}\n{% for item in items %}{{ item }}{% if not loop.last %}, {% endif %}{% endfor %}\n";
|
||||
fs::write(temp_file.path(), template_content)?;
|
||||
|
||||
// Create a template builder and add variables
|
||||
let mut builder = TemplateBuilder::open(temp_file.path())?;
|
||||
|
||||
// Add variables including a boolean and a vector
|
||||
builder = builder
|
||||
.add_var("name", "Alice")
|
||||
.add_var("show_greeting", true)
|
||||
.add_var("items", vec!["apple", "banana", "cherry"]);
|
||||
|
||||
// Render the template
|
||||
let result = builder.render()?;
|
||||
assert_eq!(result, "Hello, Alice!\napple, banana, cherry\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_with_hashmap_vars() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a temporary template file
|
||||
let mut temp_file = NamedTempFile::new()?;
|
||||
writeln!(temp_file, "{{{{ greeting }}}}, {{{{ name }}}}!")?;
|
||||
temp_file.flush()?;
|
||||
|
||||
// Create a HashMap of variables
|
||||
let mut vars = HashMap::new();
|
||||
vars.insert("greeting", "Hi");
|
||||
vars.insert("name", "Bob");
|
||||
|
||||
// Create a template builder and add variables from HashMap
|
||||
let mut builder = TemplateBuilder::open(temp_file.path())?;
|
||||
builder = builder.add_vars(vars);
|
||||
|
||||
// Render the template
|
||||
let result = builder.render()?;
|
||||
assert_eq!(result, "Hi, Bob!\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
||||
fn test_render_to_file() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a temporary template file
|
||||
let temp_file = NamedTempFile::new()?;
|
||||
let template_content = "{{ message }}\n";
|
||||
fs::write(temp_file.path(), template_content)?;
|
||||
|
||||
|
||||
// Create an output file
|
||||
let output_file = NamedTempFile::new()?;
|
||||
|
||||
// Create a template builder, add a variable, and render to file
|
||||
let mut builder = TemplateBuilder::open(temp_file.path())?;
|
||||
builder = builder.add_var("message", "This is a test");
|
||||
builder.render_to_file(output_file.path())?;
|
||||
|
||||
// Read the output file and verify its contents
|
||||
let content = fs::read_to_string(output_file.path())?;
|
||||
assert_eq!(content, "This is a test\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,851 +0,0 @@
|
||||
use crate::process::CommandResult;
|
||||
use crate::virt::buildah::{execute_buildah_command, BuildahError, Image, thread_local_debug, set_thread_local_debug};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Builder struct for buildah operations
|
||||
#[derive(Clone)]
|
||||
pub struct Builder {
|
||||
/// Name of the container
|
||||
name: String,
|
||||
/// Container ID
|
||||
container_id: Option<String>,
|
||||
/// Base image
|
||||
image: String,
|
||||
/// Debug mode
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
/// Create a new builder with a container from the specified image
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - Name for the container
|
||||
/// * `image` - Image to create the container from
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Self, BuildahError>` - Builder instance or error
|
||||
pub fn new(name: &str, image: &str) -> Result<Self, BuildahError> {
|
||||
// Try to create a new container
|
||||
let result = execute_buildah_command(&["from", "--name", name, image]);
|
||||
|
||||
match result {
|
||||
Ok(success_result) => {
|
||||
// Container created successfully
|
||||
let container_id = success_result.stdout.trim().to_string();
|
||||
|
||||
Ok(Self {
|
||||
name: name.to_string(),
|
||||
container_id: Some(container_id),
|
||||
image: image.to_string(),
|
||||
debug: false,
|
||||
})
|
||||
},
|
||||
Err(BuildahError::CommandFailed(error_msg)) => {
|
||||
// Check if the error is because the container already exists
|
||||
if error_msg.contains("that name is already in use") {
|
||||
// Extract the container ID from the error message
|
||||
// Error format: "the container name "name" is already in use by container_id. You have to remove that container to be able to reuse that name: that name is already in use"
|
||||
let container_id = error_msg
|
||||
.split("already in use by ")
|
||||
.nth(1)
|
||||
.and_then(|s| s.split('.').next())
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
if !container_id.is_empty() {
|
||||
// Container already exists, continue with it
|
||||
Ok(Self {
|
||||
name: name.to_string(),
|
||||
container_id: Some(container_id),
|
||||
image: image.to_string(),
|
||||
debug: false,
|
||||
})
|
||||
} else {
|
||||
// Couldn't extract container ID
|
||||
Err(BuildahError::Other("Failed to extract container ID from error message".to_string()))
|
||||
}
|
||||
} else {
|
||||
// Other command failure
|
||||
Err(BuildahError::CommandFailed(error_msg))
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
// Other error
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the container ID
|
||||
pub fn container_id(&self) -> Option<&String> {
|
||||
self.container_id.as_ref()
|
||||
}
|
||||
|
||||
/// Get the container name
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Get the debug mode
|
||||
pub fn debug(&self) -> bool {
|
||||
self.debug
|
||||
}
|
||||
|
||||
/// Set the debug mode
|
||||
pub fn set_debug(&mut self, debug: bool) -> &mut Self {
|
||||
self.debug = debug;
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the base image
|
||||
pub fn image(&self) -> &str {
|
||||
&self.image
|
||||
}
|
||||
|
||||
/// Run a command in the container
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `command` - The command to run
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn run(&self, command: &str) -> Result<CommandResult, BuildahError> {
|
||||
if let Some(container_id) = &self.container_id {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag from the Builder's debug flag
|
||||
set_thread_local_debug(self.debug);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&["run", container_id, "sh", "-c", command]);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
result
|
||||
} else {
|
||||
Err(BuildahError::Other("No container ID available".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a command in the container with specified isolation
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `command` - The command to run
|
||||
/// * `isolation` - Isolation method (e.g., "chroot", "rootless", "oci")
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn run_with_isolation(&self, command: &str, isolation: &str) -> Result<CommandResult, BuildahError> {
|
||||
if let Some(container_id) = &self.container_id {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag from the Builder's debug flag
|
||||
set_thread_local_debug(self.debug);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&["run", "--isolation", isolation, container_id, "sh", "-c", command]);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
result
|
||||
} else {
|
||||
Err(BuildahError::Other("No container ID available".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy files into the container
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `source` - Source path
|
||||
/// * `dest` - Destination path in the container
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn copy(&self, source: &str, dest: &str) -> Result<CommandResult, BuildahError> {
|
||||
if let Some(container_id) = &self.container_id {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag from the Builder's debug flag
|
||||
set_thread_local_debug(self.debug);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&["copy", container_id, source, dest]);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
result
|
||||
} else {
|
||||
Err(BuildahError::Other("No container ID available".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Add files into the container
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `source` - Source path
|
||||
/// * `dest` - Destination path in the container
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn add(&self, source: &str, dest: &str) -> Result<CommandResult, BuildahError> {
|
||||
if let Some(container_id) = &self.container_id {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag from the Builder's debug flag
|
||||
set_thread_local_debug(self.debug);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&["add", container_id, source, dest]);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
result
|
||||
} else {
|
||||
Err(BuildahError::Other("No container ID available".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Commit the container to an image
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `image_name` - Name for the new image
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn commit(&self, image_name: &str) -> Result<CommandResult, BuildahError> {
|
||||
if let Some(container_id) = &self.container_id {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag from the Builder's debug flag
|
||||
set_thread_local_debug(self.debug);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&["commit", container_id, image_name]);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
result
|
||||
} else {
|
||||
Err(BuildahError::Other("No container ID available".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the container
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn remove(&self) -> Result<CommandResult, BuildahError> {
|
||||
if let Some(container_id) = &self.container_id {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag from the Builder's debug flag
|
||||
set_thread_local_debug(self.debug);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&["rm", container_id]);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
result
|
||||
} else {
|
||||
Err(BuildahError::Other("No container ID available".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the builder by removing the container and clearing the container_id
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<(), BuildahError>` - Success or error
|
||||
pub fn reset(&mut self) -> Result<(), BuildahError> {
|
||||
if let Some(container_id) = &self.container_id {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag from the Builder's debug flag
|
||||
set_thread_local_debug(self.debug);
|
||||
|
||||
// Try to remove the container
|
||||
let result = execute_buildah_command(&["rm", container_id]);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
// Clear the container_id regardless of whether the removal succeeded
|
||||
self.container_id = None;
|
||||
|
||||
// Return the result of the removal operation
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} else {
|
||||
// No container to remove
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure container metadata
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `options` - Map of configuration options
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn config(&self, options: HashMap<String, String>) -> Result<CommandResult, BuildahError> {
|
||||
if let Some(container_id) = &self.container_id {
|
||||
let mut args_owned: Vec<String> = Vec::new();
|
||||
args_owned.push("config".to_string());
|
||||
|
||||
// Process options map
|
||||
for (key, value) in options.iter() {
|
||||
let option_name = format!("--{}", key);
|
||||
args_owned.push(option_name);
|
||||
args_owned.push(value.clone());
|
||||
}
|
||||
|
||||
args_owned.push(container_id.clone());
|
||||
|
||||
// Convert Vec<String> to Vec<&str> for execute_buildah_command
|
||||
let args: Vec<&str> = args_owned.iter().map(|s| s.as_str()).collect();
|
||||
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag from the Builder's debug flag
|
||||
set_thread_local_debug(self.debug);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&args);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
result
|
||||
} else {
|
||||
Err(BuildahError::Other("No container ID available".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the entrypoint for the container
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `entrypoint` - The entrypoint command
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn set_entrypoint(&self, entrypoint: &str) -> Result<CommandResult, BuildahError> {
|
||||
if let Some(container_id) = &self.container_id {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag from the Builder's debug flag
|
||||
set_thread_local_debug(self.debug);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&["config", "--entrypoint", entrypoint, container_id]);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
result
|
||||
} else {
|
||||
Err(BuildahError::Other("No container ID available".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the default command for the container
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cmd` - The default command
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn set_cmd(&self, cmd: &str) -> Result<CommandResult, BuildahError> {
|
||||
if let Some(container_id) = &self.container_id {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag from the Builder's debug flag
|
||||
set_thread_local_debug(self.debug);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&["config", "--cmd", cmd, container_id]);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
result
|
||||
} else {
|
||||
Err(BuildahError::Other("No container ID available".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// List images in local storage
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<Vec<Image>, BuildahError>` - List of images or error
|
||||
pub fn images() -> Result<Vec<Image>, BuildahError> {
|
||||
// Use default debug value (false) for static method
|
||||
let result = execute_buildah_command(&["images", "--json"])?;
|
||||
|
||||
// Try to parse the JSON output
|
||||
match serde_json::from_str::<serde_json::Value>(&result.stdout) {
|
||||
Ok(json) => {
|
||||
if let serde_json::Value::Array(images_json) = json {
|
||||
let mut images = Vec::new();
|
||||
|
||||
for image_json in images_json {
|
||||
// Extract image ID
|
||||
let id = match image_json.get("id").and_then(|v| v.as_str()) {
|
||||
Some(id) => id.to_string(),
|
||||
None => return Err(BuildahError::ConversionError("Missing image ID".to_string())),
|
||||
};
|
||||
|
||||
// Extract image names
|
||||
let names = match image_json.get("names").and_then(|v| v.as_array()) {
|
||||
Some(names_array) => {
|
||||
let mut names_vec = Vec::new();
|
||||
for name_value in names_array {
|
||||
if let Some(name_str) = name_value.as_str() {
|
||||
names_vec.push(name_str.to_string());
|
||||
}
|
||||
}
|
||||
names_vec
|
||||
},
|
||||
None => Vec::new(), // Empty vector if no names found
|
||||
};
|
||||
|
||||
// Extract image size
|
||||
let size = match image_json.get("size").and_then(|v| v.as_str()) {
|
||||
Some(size) => size.to_string(),
|
||||
None => "Unknown".to_string(), // Default value if size not found
|
||||
};
|
||||
|
||||
// Extract creation timestamp
|
||||
let created = match image_json.get("created").and_then(|v| v.as_str()) {
|
||||
Some(created) => created.to_string(),
|
||||
None => "Unknown".to_string(), // Default value if created not found
|
||||
};
|
||||
|
||||
// Create Image struct and add to vector
|
||||
images.push(Image {
|
||||
id,
|
||||
names,
|
||||
size,
|
||||
created,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(images)
|
||||
} else {
|
||||
Err(BuildahError::JsonParseError("Expected JSON array".to_string()))
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
Err(BuildahError::JsonParseError(format!("Failed to parse image list JSON: {}", e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove an image
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `image` - Image ID or name
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn image_remove(image: &str) -> Result<CommandResult, BuildahError> {
|
||||
// Use default debug value (false) for static method
|
||||
execute_buildah_command(&["rmi", image])
|
||||
}
|
||||
|
||||
/// Remove an image with debug output
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `image` - Image ID or name
|
||||
/// * `debug` - Whether to enable debug output
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn image_remove_with_debug(image: &str, debug: bool) -> Result<CommandResult, BuildahError> {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag
|
||||
set_thread_local_debug(debug);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&["rmi", image]);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Pull an image from a registry
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `image` - Image name
|
||||
/// * `tls_verify` - Whether to verify TLS
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn image_pull(image: &str, tls_verify: bool) -> Result<CommandResult, BuildahError> {
|
||||
// Use default debug value (false) for static method
|
||||
let mut args = vec!["pull"];
|
||||
|
||||
if !tls_verify {
|
||||
args.push("--tls-verify=false");
|
||||
}
|
||||
|
||||
args.push(image);
|
||||
|
||||
execute_buildah_command(&args)
|
||||
}
|
||||
|
||||
/// Pull an image from a registry with debug output
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `image` - Image name
|
||||
/// * `tls_verify` - Whether to verify TLS
|
||||
/// * `debug` - Whether to enable debug output
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn image_pull_with_debug(image: &str, tls_verify: bool, debug: bool) -> Result<CommandResult, BuildahError> {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag
|
||||
set_thread_local_debug(debug);
|
||||
|
||||
let mut args = vec!["pull"];
|
||||
|
||||
if !tls_verify {
|
||||
args.push("--tls-verify=false");
|
||||
}
|
||||
|
||||
args.push(image);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&args);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Push an image to a registry
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `image` - Image name
|
||||
/// * `destination` - Destination registry
|
||||
/// * `tls_verify` - Whether to verify TLS
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn image_push(image: &str, destination: &str, tls_verify: bool) -> Result<CommandResult, BuildahError> {
|
||||
// Use default debug value (false) for static method
|
||||
let mut args = vec!["push"];
|
||||
|
||||
if !tls_verify {
|
||||
args.push("--tls-verify=false");
|
||||
}
|
||||
|
||||
args.push(image);
|
||||
args.push(destination);
|
||||
|
||||
execute_buildah_command(&args)
|
||||
}
|
||||
|
||||
/// Push an image to a registry with debug output
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `image` - Image name
|
||||
/// * `destination` - Destination registry
|
||||
/// * `tls_verify` - Whether to verify TLS
|
||||
/// * `debug` - Whether to enable debug output
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn image_push_with_debug(image: &str, destination: &str, tls_verify: bool, debug: bool) -> Result<CommandResult, BuildahError> {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag
|
||||
set_thread_local_debug(debug);
|
||||
|
||||
let mut args = vec!["push"];
|
||||
|
||||
if !tls_verify {
|
||||
args.push("--tls-verify=false");
|
||||
}
|
||||
|
||||
args.push(image);
|
||||
args.push(destination);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&args);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Tag an image
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `image` - Image ID or name
|
||||
/// * `new_name` - New tag for the image
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn image_tag(image: &str, new_name: &str) -> Result<CommandResult, BuildahError> {
|
||||
// Use default debug value (false) for static method
|
||||
execute_buildah_command(&["tag", image, new_name])
|
||||
}
|
||||
|
||||
/// Tag an image with debug output
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `image` - Image ID or name
|
||||
/// * `new_name` - New tag for the image
|
||||
/// * `debug` - Whether to enable debug output
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn image_tag_with_debug(image: &str, new_name: &str, debug: bool) -> Result<CommandResult, BuildahError> {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag
|
||||
set_thread_local_debug(debug);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&["tag", image, new_name]);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Commit a container to an image with advanced options
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `container` - Container ID or name
|
||||
/// * `image_name` - Name for the new image
|
||||
/// * `format` - Optional format (oci or docker)
|
||||
/// * `squash` - Whether to squash layers
|
||||
/// * `rm` - Whether to remove the container after commit
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn image_commit(container: &str, image_name: &str, format: Option<&str>, squash: bool, rm: bool) -> Result<CommandResult, BuildahError> {
|
||||
// Use default debug value (false) for static method
|
||||
let mut args = vec!["commit"];
|
||||
|
||||
if let Some(format_str) = format {
|
||||
args.push("--format");
|
||||
args.push(format_str);
|
||||
}
|
||||
|
||||
if squash {
|
||||
args.push("--squash");
|
||||
}
|
||||
|
||||
if rm {
|
||||
args.push("--rm");
|
||||
}
|
||||
|
||||
args.push(container);
|
||||
args.push(image_name);
|
||||
|
||||
execute_buildah_command(&args)
|
||||
}
|
||||
|
||||
/// Commit a container to an image with advanced options and debug output
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `container` - Container ID or name
|
||||
/// * `image_name` - Name for the new image
|
||||
/// * `format` - Optional format (oci or docker)
|
||||
/// * `squash` - Whether to squash layers
|
||||
/// * `rm` - Whether to remove the container after commit
|
||||
/// * `debug` - Whether to enable debug output
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn image_commit_with_debug(container: &str, image_name: &str, format: Option<&str>, squash: bool, rm: bool, debug: bool) -> Result<CommandResult, BuildahError> {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag
|
||||
set_thread_local_debug(debug);
|
||||
|
||||
let mut args = vec!["commit"];
|
||||
|
||||
if let Some(format_str) = format {
|
||||
args.push("--format");
|
||||
args.push(format_str);
|
||||
}
|
||||
|
||||
if squash {
|
||||
args.push("--squash");
|
||||
}
|
||||
|
||||
if rm {
|
||||
args.push("--rm");
|
||||
}
|
||||
|
||||
args.push(container);
|
||||
args.push(image_name);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&args);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Build an image from a Containerfile/Dockerfile
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `tag` - Optional tag for the image
|
||||
/// * `context_dir` - Directory containing the Containerfile/Dockerfile
|
||||
/// * `file` - Path to the Containerfile/Dockerfile
|
||||
/// * `isolation` - Optional isolation method
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn build(tag: Option<&str>, context_dir: &str, file: &str, isolation: Option<&str>) -> Result<CommandResult, BuildahError> {
|
||||
// Use default debug value (false) for static method
|
||||
let mut args = Vec::new();
|
||||
args.push("build");
|
||||
|
||||
if let Some(tag_value) = tag {
|
||||
args.push("-t");
|
||||
args.push(tag_value);
|
||||
}
|
||||
|
||||
if let Some(isolation_value) = isolation {
|
||||
args.push("--isolation");
|
||||
args.push(isolation_value);
|
||||
}
|
||||
|
||||
args.push("-f");
|
||||
args.push(file);
|
||||
|
||||
args.push(context_dir);
|
||||
|
||||
execute_buildah_command(&args)
|
||||
}
|
||||
|
||||
/// Build an image from a Containerfile/Dockerfile with debug output
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `tag` - Optional tag for the image
|
||||
/// * `context_dir` - Directory containing the Containerfile/Dockerfile
|
||||
/// * `file` - Path to the Containerfile/Dockerfile
|
||||
/// * `isolation` - Optional isolation method
|
||||
/// * `debug` - Whether to enable debug output
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn build_with_debug(tag: Option<&str>, context_dir: &str, file: &str, isolation: Option<&str>, debug: bool) -> Result<CommandResult, BuildahError> {
|
||||
// Save the current debug flag
|
||||
let previous_debug = thread_local_debug();
|
||||
|
||||
// Set the thread-local debug flag
|
||||
set_thread_local_debug(debug);
|
||||
|
||||
let mut args = Vec::new();
|
||||
args.push("build");
|
||||
|
||||
if let Some(tag_value) = tag {
|
||||
args.push("-t");
|
||||
args.push(tag_value);
|
||||
}
|
||||
|
||||
if let Some(isolation_value) = isolation {
|
||||
args.push("--isolation");
|
||||
args.push(isolation_value);
|
||||
}
|
||||
|
||||
args.push("-f");
|
||||
args.push(file);
|
||||
|
||||
args.push(context_dir);
|
||||
|
||||
// Execute the command
|
||||
let result = execute_buildah_command(&args);
|
||||
|
||||
// Restore the previous debug flag
|
||||
set_thread_local_debug(previous_debug);
|
||||
|
||||
result
|
||||
}
|
||||
}
|
@ -5,22 +5,7 @@ use super::BuildahError;
|
||||
|
||||
|
||||
/// Execute a buildah command and return the result
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `args` - The command arguments
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn execute_buildah_command(args: &[&str]) -> Result<CommandResult, BuildahError> {
|
||||
// Get the debug flag from thread-local storage
|
||||
let debug = thread_local_debug();
|
||||
|
||||
if debug {
|
||||
println!("Executing buildah command: buildah {}", args.join(" "));
|
||||
}
|
||||
|
||||
let output = Command::new("buildah")
|
||||
.args(args)
|
||||
.output();
|
||||
@ -37,59 +22,15 @@ pub fn execute_buildah_command(args: &[&str]) -> Result<CommandResult, BuildahEr
|
||||
code: output.status.code().unwrap_or(-1),
|
||||
};
|
||||
|
||||
// Always output stdout/stderr when debug is true
|
||||
if debug {
|
||||
if !result.stdout.is_empty() {
|
||||
println!("Command stdout: {}", result.stdout);
|
||||
}
|
||||
|
||||
if !result.stderr.is_empty() {
|
||||
println!("Command stderr: {}", result.stderr);
|
||||
}
|
||||
|
||||
if result.success {
|
||||
println!("Command succeeded with code {}", result.code);
|
||||
} else {
|
||||
println!("Command failed with code {}", result.code);
|
||||
}
|
||||
}
|
||||
|
||||
if result.success {
|
||||
Ok(result)
|
||||
} else {
|
||||
// If command failed and debug is false, output stderr
|
||||
if !debug {
|
||||
println!("Command failed with code {}: {}", result.code, result.stderr.trim());
|
||||
}
|
||||
Err(BuildahError::CommandFailed(format!("Command failed with code {}: {}",
|
||||
Err(BuildahError::CommandFailed(format!("Command failed with code {}: {}",
|
||||
result.code, result.stderr.trim())))
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
// Always output error information
|
||||
println!("Command execution failed: {}", e);
|
||||
Err(BuildahError::CommandExecutionFailed(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Thread-local storage for debug flag
|
||||
thread_local! {
|
||||
static DEBUG: std::cell::RefCell<bool> = std::cell::RefCell::new(false);
|
||||
}
|
||||
|
||||
/// Set the debug flag for the current thread
|
||||
pub fn set_thread_local_debug(debug: bool) {
|
||||
DEBUG.with(|cell| {
|
||||
*cell.borrow_mut() = debug;
|
||||
});
|
||||
}
|
||||
|
||||
/// Get the debug flag for the current thread
|
||||
pub fn thread_local_debug() -> bool {
|
||||
DEBUG.with(|cell| {
|
||||
*cell.borrow()
|
||||
})
|
||||
}
|
||||
|
||||
// This function is no longer needed as the debug functionality is now integrated into execute_buildah_command
|
||||
|
@ -24,32 +24,32 @@ pub fn run(container: &str, command: &str) -> Result<CommandResult, BuildahError
|
||||
/// * `container` - The container ID or name
|
||||
/// * `command` - The command to run
|
||||
/// * `isolation` - Isolation method (e.g., "chroot", "rootless", "oci")
|
||||
pub fn bah_run_with_isolation(container: &str, command: &str, isolation: &str) -> Result<CommandResult, BuildahError> {
|
||||
pub fn run_with_isolation(container: &str, command: &str, isolation: &str) -> Result<CommandResult, BuildahError> {
|
||||
execute_buildah_command(&["run", "--isolation", isolation, container, "sh", "-c", command])
|
||||
}
|
||||
|
||||
/// Copy files into a container
|
||||
pub fn bah_copy(container: &str, source: &str, dest: &str) -> Result<CommandResult, BuildahError> {
|
||||
pub fn copy(container: &str, source: &str, dest: &str) -> Result<CommandResult, BuildahError> {
|
||||
execute_buildah_command(&["copy", container, source, dest])
|
||||
}
|
||||
|
||||
pub fn bah_add(container: &str, source: &str, dest: &str) -> Result<CommandResult, BuildahError> {
|
||||
pub fn add(container: &str, source: &str, dest: &str) -> Result<CommandResult, BuildahError> {
|
||||
execute_buildah_command(&["add", container, source, dest])
|
||||
}
|
||||
|
||||
/// Commit a container to an image
|
||||
pub fn bah_commit(container: &str, image_name: &str) -> Result<CommandResult, BuildahError> {
|
||||
pub fn commit(container: &str, image_name: &str) -> Result<CommandResult, BuildahError> {
|
||||
execute_buildah_command(&["commit", container, image_name])
|
||||
}
|
||||
|
||||
|
||||
/// Remove a container
|
||||
pub fn bah_remove(container: &str) -> Result<CommandResult, BuildahError> {
|
||||
pub fn remove(container: &str) -> Result<CommandResult, BuildahError> {
|
||||
execute_buildah_command(&["rm", container])
|
||||
}
|
||||
|
||||
/// List containers
|
||||
pub fn bah_list() -> Result<CommandResult, BuildahError> {
|
||||
pub fn list() -> Result<CommandResult, BuildahError> {
|
||||
execute_buildah_command(&["containers"])
|
||||
}
|
||||
|
||||
@ -61,7 +61,7 @@ pub fn bah_list() -> Result<CommandResult, BuildahError> {
|
||||
/// * `context_dir` - The directory containing the Containerfile/Dockerfile (usually ".")
|
||||
/// * `file` - Optional path to a specific Containerfile/Dockerfile
|
||||
/// * `isolation` - Optional isolation method (e.g., "chroot", "rootless", "oci")
|
||||
pub fn bah_build(tag: Option<&str>, context_dir: &str, file: &str, isolation: Option<&str>) -> Result<CommandResult, BuildahError> {
|
||||
pub fn build(tag: Option<&str>, context_dir: &str, file: &str, isolation: Option<&str>) -> Result<CommandResult, BuildahError> {
|
||||
let mut args = Vec::new();
|
||||
args.push("build");
|
||||
|
||||
|
@ -11,7 +11,6 @@ mod tests {
|
||||
lazy_static! {
|
||||
static ref LAST_COMMAND: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
static ref SHOULD_FAIL: Mutex<bool> = Mutex::new(false);
|
||||
static ref TEST_MUTEX: Mutex<()> = Mutex::new(()); // Add a mutex for test synchronization
|
||||
}
|
||||
|
||||
fn reset_test_state() {
|
||||
@ -65,35 +64,34 @@ mod tests {
|
||||
test_execute_buildah_command(&["from", image])
|
||||
}
|
||||
|
||||
fn test_run(container: &str, command: &str) -> Result<CommandResult, BuildahError> {
|
||||
test_execute_buildah_command(&["run", container, "sh", "-c", command])
|
||||
fn test_run(container: &str, command: &str, isolation: Option<&str>) -> Result<CommandResult, BuildahError> {
|
||||
match isolation {
|
||||
Some(iso) => test_execute_buildah_command(&["run", "--isolation", iso, container, "sh", "-c", command]),
|
||||
None => test_execute_buildah_command(&["run", container, "sh", "-c", command])
|
||||
}
|
||||
}
|
||||
|
||||
fn test_bah_run_with_isolation(container: &str, command: &str, isolation: &str) -> Result<CommandResult, BuildahError> {
|
||||
test_execute_buildah_command(&["run", "--isolation", isolation, container, "sh", "-c", command])
|
||||
}
|
||||
|
||||
fn test_bah_copy(container: &str, source: &str, dest: &str) -> Result<CommandResult, BuildahError> {
|
||||
fn test_copy(container: &str, source: &str, dest: &str) -> Result<CommandResult, BuildahError> {
|
||||
test_execute_buildah_command(&["copy", container, source, dest])
|
||||
}
|
||||
|
||||
fn test_bah_add(container: &str, source: &str, dest: &str) -> Result<CommandResult, BuildahError> {
|
||||
fn test_add(container: &str, source: &str, dest: &str) -> Result<CommandResult, BuildahError> {
|
||||
test_execute_buildah_command(&["add", container, source, dest])
|
||||
}
|
||||
|
||||
fn test_bah_commit(container: &str, image_name: &str) -> Result<CommandResult, BuildahError> {
|
||||
fn test_commit(container: &str, image_name: &str) -> Result<CommandResult, BuildahError> {
|
||||
test_execute_buildah_command(&["commit", container, image_name])
|
||||
}
|
||||
|
||||
fn test_bah_remove(container: &str) -> Result<CommandResult, BuildahError> {
|
||||
fn test_remove(container: &str) -> Result<CommandResult, BuildahError> {
|
||||
test_execute_buildah_command(&["rm", container])
|
||||
}
|
||||
|
||||
fn test_bah_list() -> Result<CommandResult, BuildahError> {
|
||||
fn test_list() -> Result<CommandResult, BuildahError> {
|
||||
test_execute_buildah_command(&["containers"])
|
||||
}
|
||||
|
||||
fn test_bah_build(tag: Option<&str>, context_dir: &str, file: &str, isolation: Option<&str>) -> Result<CommandResult, BuildahError> {
|
||||
fn test_build(tag: Option<&str>, context_dir: &str, file: Option<&str>) -> Result<CommandResult, BuildahError> {
|
||||
let mut args = Vec::new();
|
||||
args.push("build");
|
||||
|
||||
@ -102,14 +100,11 @@ mod tests {
|
||||
args.push(tag_value);
|
||||
}
|
||||
|
||||
if let Some(isolation_value) = isolation {
|
||||
args.push("--isolation");
|
||||
args.push(isolation_value);
|
||||
if let Some(file_path) = file {
|
||||
args.push("-f");
|
||||
args.push(file_path);
|
||||
}
|
||||
|
||||
args.push("-f");
|
||||
args.push(file);
|
||||
|
||||
args.push(context_dir);
|
||||
|
||||
test_execute_buildah_command(&args)
|
||||
@ -118,7 +113,6 @@ mod tests {
|
||||
// Tests for each function
|
||||
#[test]
|
||||
fn test_from_function() {
|
||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
||||
reset_test_state();
|
||||
|
||||
let image = "alpine:latest";
|
||||
@ -131,43 +125,32 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_run_function() {
|
||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
||||
reset_test_state();
|
||||
|
||||
let container = "my-container";
|
||||
let command = "echo hello";
|
||||
|
||||
// Test without isolation
|
||||
let result = test_run(container, command);
|
||||
let result = test_run(container, command, None);
|
||||
assert!(result.is_ok());
|
||||
let cmd = get_last_command();
|
||||
assert_eq!(cmd, vec!["run", "my-container", "sh", "-c", "echo hello"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bah_run_with_isolation_function() {
|
||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
||||
reset_test_state();
|
||||
|
||||
let container = "my-container";
|
||||
let command = "echo hello";
|
||||
let isolation = "chroot";
|
||||
|
||||
let result = test_bah_run_with_isolation(container, command, isolation);
|
||||
// Test with isolation
|
||||
let result = test_run(container, command, Some("chroot"));
|
||||
assert!(result.is_ok());
|
||||
let cmd = get_last_command();
|
||||
assert_eq!(cmd, vec!["run", "--isolation", "chroot", "my-container", "sh", "-c", "echo hello"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bah_copy_function() {
|
||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
||||
fn test_copy_function() {
|
||||
reset_test_state();
|
||||
|
||||
let container = "my-container";
|
||||
let source = "/local/path";
|
||||
let dest = "/container/path";
|
||||
let result = test_bah_copy(container, source, dest);
|
||||
let result = test_copy(container, source, dest);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let cmd = get_last_command();
|
||||
@ -175,14 +158,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bah_add_function() {
|
||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
||||
fn test_add_function() {
|
||||
reset_test_state();
|
||||
|
||||
let container = "my-container";
|
||||
let source = "/local/path";
|
||||
let dest = "/container/path";
|
||||
let result = test_bah_add(container, source, dest);
|
||||
let result = test_add(container, source, dest);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let cmd = get_last_command();
|
||||
@ -190,13 +172,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bah_commit_function() {
|
||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
||||
fn test_commit_function() {
|
||||
reset_test_state();
|
||||
|
||||
let container = "my-container";
|
||||
let image_name = "my-image:latest";
|
||||
let result = test_bah_commit(container, image_name);
|
||||
let result = test_commit(container, image_name);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let cmd = get_last_command();
|
||||
@ -204,12 +185,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bah_remove_function() {
|
||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
||||
fn test_remove_function() {
|
||||
reset_test_state();
|
||||
|
||||
let container = "my-container";
|
||||
let result = test_bah_remove(container);
|
||||
let result = test_remove(container);
|
||||
|
||||
assert!(result.is_ok());
|
||||
let cmd = get_last_command();
|
||||
@ -217,11 +197,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bah_list_function() {
|
||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
||||
fn test_list_function() {
|
||||
reset_test_state();
|
||||
|
||||
let result = test_bah_list();
|
||||
let result = test_list();
|
||||
|
||||
assert!(result.is_ok());
|
||||
let cmd = get_last_command();
|
||||
@ -229,36 +208,30 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bah_build_function() {
|
||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
||||
fn test_build_function() {
|
||||
reset_test_state();
|
||||
|
||||
// Test with tag, context directory, file, and no isolation
|
||||
let result = test_bah_build(Some("my-app:latest"), ".", "Dockerfile", None);
|
||||
// Test with tag and context directory
|
||||
let result = test_build(Some("my-app:latest"), ".", None);
|
||||
assert!(result.is_ok());
|
||||
let cmd = get_last_command();
|
||||
assert_eq!(cmd, vec!["build", "-t", "my-app:latest", "-f", "Dockerfile", "."]);
|
||||
assert_eq!(cmd, vec!["build", "-t", "my-app:latest", "."]);
|
||||
|
||||
reset_test_state(); // Reset state between sub-tests
|
||||
|
||||
// Test with tag, context directory, file, and isolation
|
||||
let result = test_bah_build(Some("my-app:latest"), ".", "Dockerfile.custom", Some("chroot"));
|
||||
// Test with tag, context directory, and file
|
||||
let result = test_build(Some("my-app:latest"), ".", Some("Dockerfile.custom"));
|
||||
assert!(result.is_ok());
|
||||
let cmd = get_last_command();
|
||||
assert_eq!(cmd, vec!["build", "-t", "my-app:latest", "--isolation", "chroot", "-f", "Dockerfile.custom", "."]);
|
||||
assert_eq!(cmd, vec!["build", "-t", "my-app:latest", "-f", "Dockerfile.custom", "."]);
|
||||
|
||||
reset_test_state(); // Reset state between sub-tests
|
||||
|
||||
// Test with just context directory and file
|
||||
let result = test_bah_build(None, ".", "Dockerfile", None);
|
||||
// Test with just context directory
|
||||
let result = test_build(None, ".", None);
|
||||
assert!(result.is_ok());
|
||||
let cmd = get_last_command();
|
||||
assert_eq!(cmd, vec!["build", "-f", "Dockerfile", "."]);
|
||||
assert_eq!(cmd, vec!["build", "."]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_handling() {
|
||||
let _lock = TEST_MUTEX.lock().unwrap(); // Acquire lock for test
|
||||
reset_test_state();
|
||||
set_should_fail(true);
|
||||
|
||||
|
@ -1,82 +0,0 @@
|
||||
use crate::process::CommandResult;
|
||||
use crate::virt::buildah::{execute_buildah_command, BuildahError};
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
/// Functions for working with file content in buildah containers
|
||||
pub struct ContentOperations;
|
||||
|
||||
impl ContentOperations {
|
||||
/// Write content to a file in the container
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `container_id` - The container ID
|
||||
/// * `content` - The content to write
|
||||
/// * `dest_path` - Destination path in the container
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<CommandResult, BuildahError>` - Command result or error
|
||||
pub fn write_content(container_id: &str, content: &str, dest_path: &str) -> Result<CommandResult, BuildahError> {
|
||||
// Create a temporary file
|
||||
let mut temp_file = NamedTempFile::new()
|
||||
.map_err(|e| BuildahError::Other(format!("Failed to create temporary file: {}", e)))?;
|
||||
|
||||
// Write content to the temporary file
|
||||
temp_file.write_all(content.as_bytes())
|
||||
.map_err(|e| BuildahError::Other(format!("Failed to write to temporary file: {}", e)))?;
|
||||
|
||||
// Flush the file to ensure content is written
|
||||
temp_file.flush()
|
||||
.map_err(|e| BuildahError::Other(format!("Failed to flush temporary file: {}", e)))?;
|
||||
|
||||
// Copy the temporary file to the container
|
||||
let temp_path = temp_file.path().to_string_lossy().to_string();
|
||||
// Use add instead of copy for better handling of paths
|
||||
execute_buildah_command(&["add", container_id, &temp_path, dest_path])
|
||||
}
|
||||
|
||||
/// Read content from a file in the container
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `container_id` - The container ID
|
||||
/// * `source_path` - Source path in the container
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Result<String, BuildahError>` - File content or error
|
||||
pub fn read_content(container_id: &str, source_path: &str) -> Result<String, BuildahError> {
|
||||
// Create a temporary file
|
||||
let temp_file = NamedTempFile::new()
|
||||
.map_err(|e| BuildahError::Other(format!("Failed to create temporary file: {}", e)))?;
|
||||
|
||||
let temp_path = temp_file.path().to_string_lossy().to_string();
|
||||
|
||||
// Copy the file from the container to the temporary file
|
||||
// Use mount to access the container's filesystem
|
||||
let mount_result = execute_buildah_command(&["mount", container_id])?;
|
||||
let mount_point = mount_result.stdout.trim();
|
||||
|
||||
// Construct the full path to the file in the container
|
||||
let full_source_path = format!("{}{}", mount_point, source_path);
|
||||
|
||||
// Copy the file from the mounted container to the temporary file
|
||||
execute_buildah_command(&["copy", container_id, &full_source_path, &temp_path])?;
|
||||
|
||||
// Unmount the container
|
||||
execute_buildah_command(&["umount", container_id])?;
|
||||
|
||||
// Read the content from the temporary file
|
||||
let mut file = File::open(temp_file.path())
|
||||
.map_err(|e| BuildahError::Other(format!("Failed to open temporary file: {}", e)))?;
|
||||
|
||||
let mut content = String::new();
|
||||
file.read_to_string(&mut content)
|
||||
.map_err(|e| BuildahError::Other(format!("Failed to read from temporary file: {}", e)))?;
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
}
|
@ -190,7 +190,7 @@ pub fn image_commit(container: &str, image_name: &str, format: Option<&str>, squ
|
||||
///
|
||||
/// # Returns
|
||||
/// * Result with command output or error
|
||||
pub fn bah_config(container: &str, options: HashMap<String, String>) -> Result<CommandResult, BuildahError> {
|
||||
pub fn config(container: &str, options: HashMap<String, String>) -> Result<CommandResult, BuildahError> {
|
||||
let mut args_owned: Vec<String> = Vec::new();
|
||||
args_owned.push("config".to_string());
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
mod containers;
|
||||
mod images;
|
||||
mod cmd;
|
||||
mod builder;
|
||||
mod content;
|
||||
#[cfg(test)]
|
||||
mod containers_test;
|
||||
|
||||
@ -45,13 +43,6 @@ impl Error for BuildahError {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Re-export the Builder
|
||||
pub use builder::Builder;
|
||||
|
||||
// Re-export existing functions for backward compatibility
|
||||
#[deprecated(since = "0.2.0", note = "Use Builder::new() instead")]
|
||||
pub use containers::*;
|
||||
#[deprecated(since = "0.2.0", note = "Use Builder methods instead")]
|
||||
pub use images::*;
|
||||
pub use cmd::*;
|
||||
pub use content::ContentOperations;
|
||||
pub use cmd::*;
|
@ -1,3 +1,2 @@
|
||||
pub mod buildah;
|
||||
pub mod nerdctl;
|
||||
pub mod rfs;
|
||||
pub mod nerdctl;
|
@ -1,374 +0,0 @@
|
||||
# Container API for nerdctl
|
||||
|
||||
This module provides a Rust API for managing containers using nerdctl, a Docker-compatible CLI for containerd.
|
||||
|
||||
## Overview
|
||||
|
||||
The Container API is designed with a builder pattern to make it easy to create and manage containers. It provides a fluent interface for configuring container options and performing operations on containers.
|
||||
|
||||
## Key Components
|
||||
|
||||
- `Container`: The main struct representing a container
|
||||
- `HealthCheck`: Configuration for container health checks
|
||||
- `ContainerStatus`: Information about a container's status
|
||||
- `ResourceUsage`: Information about a container's resource usage
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- nerdctl must be installed on your system
|
||||
- containerd must be running
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Add the following to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sal = { path = "/path/to/sal" }
|
||||
```
|
||||
|
||||
Then import the Container API in your Rust code:
|
||||
|
||||
```rust
|
||||
use sal::virt::nerdctl::Container;
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Getting a Container by Name
|
||||
|
||||
You can get a reference to an existing container by name:
|
||||
|
||||
```rust
|
||||
use sal::virt::nerdctl::Container;
|
||||
|
||||
// Get a container by name (if it exists)
|
||||
match Container::new("existing-container") {
|
||||
Ok(container) => {
|
||||
if container.container_id.is_some() {
|
||||
println!("Found container with ID: {}", container.container_id.unwrap());
|
||||
|
||||
// Perform operations on the existing container
|
||||
let status = container.status()?;
|
||||
println!("Container status: {}", status.status);
|
||||
} else {
|
||||
println!("Container exists but has no ID");
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error getting container: {}", e);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Creating a Container
|
||||
|
||||
You can create a new container from an image using the builder pattern:
|
||||
|
||||
```rust
|
||||
use sal::virt::nerdctl::Container;
|
||||
|
||||
// Create a container from an image
|
||||
let container = Container::from_image("my-nginx", "nginx:latest")?
|
||||
.with_port("8080:80")
|
||||
.with_env("NGINX_HOST", "example.com")
|
||||
.with_volume("/tmp/nginx:/usr/share/nginx/html")
|
||||
.with_health_check("curl -f http://localhost/ || exit 1")
|
||||
.with_detach(true)
|
||||
.build()?;
|
||||
```
|
||||
|
||||
### Container Operations
|
||||
|
||||
Once you have a container, you can perform various operations on it:
|
||||
|
||||
```rust
|
||||
// Execute a command in the container
|
||||
let result = container.exec("echo 'Hello from container'")?;
|
||||
println!("Command output: {}", result.stdout);
|
||||
|
||||
// Get container status
|
||||
let status = container.status()?;
|
||||
println!("Container status: {}", status.status);
|
||||
|
||||
// Get resource usage
|
||||
let resources = container.resources()?;
|
||||
println!("CPU usage: {}", resources.cpu_usage);
|
||||
println!("Memory usage: {}", resources.memory_usage);
|
||||
|
||||
// Stop and remove the container
|
||||
container.stop()?;
|
||||
container.remove()?;
|
||||
```
|
||||
|
||||
## Container Configuration Options
|
||||
|
||||
The Container API supports a wide range of configuration options through its builder pattern:
|
||||
|
||||
### Ports
|
||||
|
||||
Map container ports to host ports:
|
||||
|
||||
```rust
|
||||
// Map a single port
|
||||
.with_port("8080:80")
|
||||
|
||||
// Map multiple ports
|
||||
.with_ports(&["8080:80", "8443:443"])
|
||||
```
|
||||
|
||||
### Volumes
|
||||
|
||||
Mount host directories or volumes in the container:
|
||||
|
||||
```rust
|
||||
// Mount a single volume
|
||||
.with_volume("/host/path:/container/path")
|
||||
|
||||
// Mount multiple volumes
|
||||
.with_volumes(&["/host/path1:/container/path1", "/host/path2:/container/path2"])
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Set environment variables in the container:
|
||||
|
||||
```rust
|
||||
// Set a single environment variable
|
||||
.with_env("KEY", "value")
|
||||
|
||||
// Set multiple environment variables
|
||||
let mut env_map = HashMap::new();
|
||||
env_map.insert("KEY1", "value1");
|
||||
env_map.insert("KEY2", "value2");
|
||||
.with_envs(&env_map)
|
||||
```
|
||||
|
||||
### Network Configuration
|
||||
|
||||
Configure container networking:
|
||||
|
||||
```rust
|
||||
// Set the network
|
||||
.with_network("bridge")
|
||||
|
||||
// Add a network alias
|
||||
.with_network_alias("my-container")
|
||||
|
||||
// Add multiple network aliases
|
||||
.with_network_aliases(&["alias1", "alias2"])
|
||||
```
|
||||
|
||||
### Resource Limits
|
||||
|
||||
Set CPU and memory limits:
|
||||
|
||||
```rust
|
||||
// Set CPU limit (e.g., 0.5 for half a CPU, 2 for 2 CPUs)
|
||||
.with_cpu_limit("0.5")
|
||||
|
||||
// Set memory limit (e.g., 512m for 512MB, 1g for 1GB)
|
||||
.with_memory_limit("512m")
|
||||
|
||||
// Set memory swap limit
|
||||
.with_memory_swap_limit("1g")
|
||||
|
||||
// Set CPU shares (relative weight)
|
||||
.with_cpu_shares("1024")
|
||||
```
|
||||
|
||||
### Health Checks
|
||||
|
||||
Configure container health checks:
|
||||
|
||||
```rust
|
||||
// Simple health check
|
||||
.with_health_check("curl -f http://localhost/ || exit 1")
|
||||
|
||||
// Health check with custom options
|
||||
.with_health_check_options(
|
||||
"curl -f http://localhost/ || exit 1", // Command
|
||||
Some("30s"), // Interval
|
||||
Some("10s"), // Timeout
|
||||
Some(3), // Retries
|
||||
Some("5s") // Start period
|
||||
)
|
||||
```
|
||||
|
||||
### Other Options
|
||||
|
||||
Other container configuration options:
|
||||
|
||||
```rust
|
||||
// Set restart policy
|
||||
.with_restart_policy("always") // Options: no, always, on-failure, unless-stopped
|
||||
|
||||
// Set snapshotter
|
||||
.with_snapshotter("native") // Options: native, fuse-overlayfs, etc.
|
||||
|
||||
// Set detach mode
|
||||
.with_detach(true) // Run in detached mode
|
||||
```
|
||||
|
||||
## Container Operations
|
||||
|
||||
Once a container is created, you can perform various operations on it:
|
||||
|
||||
### Basic Operations
|
||||
|
||||
```rust
|
||||
// Start the container
|
||||
container.start()?;
|
||||
|
||||
// Stop the container
|
||||
container.stop()?;
|
||||
|
||||
// Remove the container
|
||||
container.remove()?;
|
||||
```
|
||||
|
||||
### Command Execution
|
||||
|
||||
```rust
|
||||
// Execute a command in the container
|
||||
let result = container.exec("echo 'Hello from container'")?;
|
||||
println!("Command output: {}", result.stdout);
|
||||
```
|
||||
|
||||
### File Operations
|
||||
|
||||
```rust
|
||||
// Copy files between the container and host
|
||||
container.copy("container_name:/path/in/container", "/path/on/host")?;
|
||||
container.copy("/path/on/host", "container_name:/path/in/container")?;
|
||||
|
||||
// Export the container to a tarball
|
||||
container.export("/path/to/export.tar")?;
|
||||
```
|
||||
|
||||
### Image Operations
|
||||
|
||||
```rust
|
||||
// Commit the container to an image
|
||||
container.commit("my-custom-image:latest")?;
|
||||
```
|
||||
|
||||
### Status and Monitoring
|
||||
|
||||
```rust
|
||||
// Get container status
|
||||
let status = container.status()?;
|
||||
println!("Container state: {}", status.state);
|
||||
println!("Container status: {}", status.status);
|
||||
println!("Created: {}", status.created);
|
||||
println!("Started: {}", status.started);
|
||||
|
||||
// Get health status
|
||||
let health_status = container.health_status()?;
|
||||
println!("Health status: {}", health_status);
|
||||
|
||||
// Get resource usage
|
||||
let resources = container.resources()?;
|
||||
println!("CPU usage: {}", resources.cpu_usage);
|
||||
println!("Memory usage: {}", resources.memory_usage);
|
||||
println!("Memory limit: {}", resources.memory_limit);
|
||||
println!("Memory percentage: {}", resources.memory_percentage);
|
||||
println!("Network I/O: {} / {}", resources.network_input, resources.network_output);
|
||||
println!("Block I/O: {} / {}", resources.block_input, resources.block_output);
|
||||
println!("PIDs: {}", resources.pids);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The Container API uses a custom error type `NerdctlError` that can be one of the following:
|
||||
|
||||
- `CommandExecutionFailed`: The nerdctl command failed to execute
|
||||
- `CommandFailed`: The nerdctl command executed but returned an error
|
||||
- `JsonParseError`: Failed to parse JSON output
|
||||
- `ConversionError`: Failed to convert data
|
||||
- `Other`: Generic error
|
||||
|
||||
Example error handling:
|
||||
|
||||
```rust
|
||||
match Container::new("non-existent-container") {
|
||||
Ok(container) => {
|
||||
// Container exists
|
||||
println!("Container found");
|
||||
},
|
||||
Err(e) => {
|
||||
match e {
|
||||
NerdctlError::CommandExecutionFailed(io_error) => {
|
||||
println!("Failed to execute nerdctl command: {}", io_error);
|
||||
},
|
||||
NerdctlError::CommandFailed(error_msg) => {
|
||||
println!("nerdctl command failed: {}", error_msg);
|
||||
},
|
||||
_ => {
|
||||
println!("Other error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The Container API is implemented in several modules:
|
||||
|
||||
- `container_types.rs`: Contains the struct definitions
|
||||
- `container.rs`: Contains the main Container implementation
|
||||
- `container_builder.rs`: Contains the builder pattern methods
|
||||
- `container_operations.rs`: Contains the container operations
|
||||
- `health_check.rs`: Contains the HealthCheck implementation
|
||||
|
||||
This modular approach makes the code more maintainable and easier to understand.
|
||||
|
||||
## Complete Example
|
||||
|
||||
Here's a complete example that demonstrates the Container API:
|
||||
|
||||
```rust
|
||||
use std::error::Error;
|
||||
use sal::virt::nerdctl::Container;
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// Create a container from an image
|
||||
println!("Creating container from image...");
|
||||
let container = Container::from_image("my-nginx", "nginx:latest")?
|
||||
.with_port("8080:80")
|
||||
.with_env("NGINX_HOST", "example.com")
|
||||
.with_volume("/tmp/nginx:/usr/share/nginx/html")
|
||||
.with_health_check("curl -f http://localhost/ || exit 1")
|
||||
.with_detach(true)
|
||||
.build()?;
|
||||
|
||||
println!("Container created successfully");
|
||||
|
||||
// Execute a command in the container
|
||||
println!("Executing command in container...");
|
||||
let result = container.exec("echo 'Hello from container'")?;
|
||||
println!("Command output: {}", result.stdout);
|
||||
|
||||
// Get container status
|
||||
println!("Getting container status...");
|
||||
let status = container.status()?;
|
||||
println!("Container status: {}", status.status);
|
||||
|
||||
// Get resource usage
|
||||
println!("Getting resource usage...");
|
||||
let resources = container.resources()?;
|
||||
println!("CPU usage: {}", resources.cpu_usage);
|
||||
println!("Memory usage: {}", resources.memory_usage);
|
||||
|
||||
// Stop and remove the container
|
||||
println!("Stopping and removing container...");
|
||||
container.stop()?;
|
||||
container.remove()?;
|
||||
|
||||
println!("Container stopped and removed");
|
||||
|
||||
Ok(())
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user