Compare commits

...

4 Commits

Author SHA1 Message Date
0fa9eddd1c ... 2025-04-05 15:38:43 +02:00
7bf2ffe47d ... 2025-04-05 15:10:02 +02:00
768542afd3 ... 2025-04-05 11:30:20 +02:00
e9a73327e3 ... 2025-04-05 11:21:58 +02:00
21 changed files with 2590 additions and 124 deletions

View File

@ -25,7 +25,6 @@ let image = builder.image;
```
### Builder Methods
The Builder object provides the following methods:
- `run(command)`: Run a command in the container
@ -36,12 +35,18 @@ The Builder object provides the following methods:
- `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
@ -49,6 +54,9 @@ The Builder object provides the following methods:
// 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();
@ -59,6 +67,16 @@ builder = bah_new("my-container", "alpine:latest");
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", "/");
@ -122,3 +140,100 @@ 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!
```

View File

@ -91,27 +91,27 @@ let remove_result = container.remove();
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)` |
| 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
@ -183,16 +183,57 @@ 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", "./")` |
| 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", "./")` |

View File

@ -145,6 +145,28 @@ delete("temp.txt");
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.
@ -264,7 +286,7 @@ chdir("project/src");
### `download(url, dest, min_size_kb)`
Downloads a file from a URL to a destination using the curl command. If the URL ends with a supported archive format, the file will be automatically extracted to the destination directory.
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`
@ -274,15 +296,32 @@ Downloads a file from a URL to a destination using the curl command. If the URL
**Parameters:**
- `url` (string): The URL to download from
- `dest` (string): The destination path to save the file
- `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
download("https://example.com/file.zip", "downloads/file.zip", 10);
// 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)`
@ -301,4 +340,20 @@ Downloads a file and installs it if it's a supported package format.
**Example:**
```js
// Download and install a package
download_install("https://example.com/package.deb", 1000);
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");
```

237
src/docs/docs/sal/text.md Normal file
View File

@ -0,0 +1,237 @@
# 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.

View File

@ -58,34 +58,167 @@ 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 path where the file will be saved
* * `dest` - The destination directory where the file will be saved or extracted
* * `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/file.txt", 0)?;
*
* let path = download("https://example.com/file.txt", "/tmp/", 0)?;
*
* // Download a file with minimum size requirement of 100KB
* let path = download("https://example.com/file.zip", "/tmp/file.zip", 100)?;
* let path = download("https://example.com/file.zip", "/tmp/", 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() {
@ -131,61 +264,73 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Downl
}
}
// 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");
// Rename the temporary file to the final destination
fs::rename(&temp_path, dest).map_err(|e| DownloadError::CreateDirectoryFailed(e))?;
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())
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 !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))
}
/**
@ -223,11 +368,24 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, DownloadE
// Create a proper destination path
let dest_path = format!("/tmp/{}", filename);
let download_result = download(url, &dest_path, 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");
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)?
};
// 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
}

View File

@ -124,7 +124,16 @@ 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
dest_path.join(path.file_name().unwrap_or_default())
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())
}
} else {
// Otherwise use the destination as is (only makes sense for single file)
dest_path.to_path_buf()
@ -186,9 +195,17 @@ pub fn copy(src: &str, dest: &str) -> Result<String, FsError> {
// Copy based on source type
if src_path.is_file() {
// Copy file
fs::copy(src_path, dest_path).map_err(FsError::CopyFailed)?;
Ok(format!("Successfully copied file '{}' to '{}'", src, dest))
// 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))
}
} else if src_path.is_dir() {
// For directories, use platform-specific command
#[cfg(target_os = "windows")]
@ -743,6 +760,114 @@ pub fn file_write_append(path: &str, content: &str) -> Result<String, FsError> {
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.
*

View File

@ -1,5 +1,7 @@
mod fs;
mod download;
pub mod package;
pub use fs::*;
pub use download::*;
pub use download::*;
pub use package::*;

421
src/os/package.rs Normal file
View File

@ -0,0 +1,421 @@
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 {
use super::*;
#[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);
}
// More tests would be added for each platform-specific implementation
// These would likely be integration tests that are conditionally compiled
// based on the platform they're running on
}

View File

@ -0,0 +1,565 @@
# 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

View File

@ -29,10 +29,14 @@ pub fn register_bah_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>
engine.register_fn("copy", builder_copy);
engine.register_fn("add", builder_add);
engine.register_fn("commit", builder_commit);
// Remove the line that's causing the error
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);
@ -41,7 +45,6 @@ pub fn register_bah_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>
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("write_content", builder_write_content);
engine.register_fn("read_content", builder_read_content);
Ok(())
@ -56,8 +59,8 @@ fn register_bah_types(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
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", get_builder_debug);
engine.register_set("debug", set_builder_debug);
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");
@ -150,6 +153,16 @@ pub fn builder_config(builder: &mut Builder, options: Map) -> Result<CommandResu
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() {

View File

@ -4,6 +4,7 @@
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
@ -41,7 +42,23 @@ pub fn register_os_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>>
// 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(())
}
@ -164,6 +181,13 @@ pub fn file_write_append(path: &str, content: &str) -> Result<String, Box<EvalAl
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
//
@ -175,6 +199,13 @@ pub fn download(url: &str, dest: &str, min_size_kb: i64) -> Result<String, Box<E
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.
@ -182,6 +213,13 @@ pub fn download_install(url: &str, min_size_kb: i64) -> Result<String, Box<EvalA
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.
@ -195,4 +233,116 @@ pub fn which(command: &str) -> String {
/// 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(),
}
}

View File

@ -14,6 +14,9 @@ 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)...");
@ -91,6 +94,10 @@ nginx -g "daemon off;"
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:");
@ -108,7 +115,24 @@ builder.remove();
println("\nStarting container from the new image using nerdctl...");
// Create a container using the builder pattern
let container = nerdctl_container_from_image("golang-nginx-demo", final_image_name)
// 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")

View File

@ -14,7 +14,7 @@ let builder = bah_new(container_name, base_image);
// Enable debug mode
println("Enabling debug mode...");
builder.debug = true;
builder.debug_mode = true;
// Run a simple command to see debug output
println("Running a command with debug enabled...");
@ -22,7 +22,7 @@ let result = builder.run("echo 'Hello from debug mode'");
// Disable debug mode
println("Disabling debug mode...");
builder.debug = false;
builder.debug_mode = false;
// Run another command without debug
println("Running a command with debug disabled...");
@ -30,7 +30,7 @@ let result2 = builder.run("echo 'Hello without debug'");
// Enable debug mode again
println("Enabling debug mode again...");
builder.debug = true;
builder.debug_mode = true;
// Remove the container with debug enabled
println("Removing the container with debug enabled...");

View File

@ -0,0 +1,210 @@
// 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
--------------------------------------
`);

View File

@ -49,7 +49,7 @@ print(` Extraction successful: ${success_msg}`);
// Download binary file and check size
print("\nTesting binary download:");
download(binary_url, binary_dest, 8000);
download_file(binary_url, binary_dest, 8000);
// Check file size using our new file_size function
let size_bytes = file_size(binary_dest);
@ -63,6 +63,43 @@ print(` Minimum size check passed:${success_msg}`);
// Clean up test files
delete(download_dir);
print("Cleaned up test directory");
//PART 4
print("\nDownload Tests completed successfully!");
// 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"

View File

@ -0,0 +1,124 @@
## ✅ 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
```

View File

@ -4,17 +4,22 @@
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}`,20);
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}`,20);
download(url,`/tmp/${name}`,20000);
copy(`/tmp/${name}/bin/*`,"/root/hero/bin/");
delete(`/tmp/${name}`);
run("apt-get -y install buildah runc")
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/");
}

View File

@ -0,0 +1,113 @@
// Example script demonstrating the package management functions
// Set debug mode to true to see detailed output
package_set_debug(true);
// Function to demonstrate package management on Ubuntu
fn demo_ubuntu() {
print("Demonstrating package management on Ubuntu...");
// Update package lists
print("Updating package lists...");
let result = package_update();
print(`Update result: ${result}`);
// Check if a package is installed
let package = "htop";
print(`Checking if ${package} is installed...`);
let is_installed = package_is_installed(package);
print(`${package} is installed: ${is_installed}`);
// Install a package if not already installed
if !is_installed {
print(`Installing ${package}...`);
let install_result = package_install(package);
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 package if we installed it
if !is_installed {
print(`Removing ${package}...`);
let remove_result = package_remove(package);
print(`Remove result: ${remove_result}`);
}
}
// Function to demonstrate package management on macOS
fn demo_macos() {
print("Demonstrating package management on macOS...");
// Update package lists
print("Updating package lists...");
let result = package_update();
print(`Update result: ${result}`);
// Check if a package is installed
let package = "wget";
print(`Checking if ${package} is installed...`);
let is_installed = package_is_installed(package);
print(`${package} is installed: ${is_installed}`);
// Install a package if not already installed
if !is_installed {
print(`Installing ${package}...`);
let install_result = package_install(package);
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 package if we installed it
if !is_installed {
print(`Removing ${package}...`);
let remove_result = package_remove(package);
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();

View File

@ -1,6 +1,7 @@
use regex::Regex;
use std::fs;
use std::io::{self, Read};
use std::io::{self, Read, Seek, SeekFrom};
use std::path::Path;
/// Represents the type of replacement to perform.

View File

@ -355,6 +355,64 @@ impl Builder {
}
}
/// 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

View File

@ -34,7 +34,8 @@ impl ContentOperations {
// Copy the temporary file to the container
let temp_path = temp_file.path().to_string_lossy().to_string();
execute_buildah_command(&["copy", container_id, &temp_path, dest_path])
// 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
@ -55,7 +56,18 @@ impl ContentOperations {
let temp_path = temp_file.path().to_string_lossy().to_string();
// Copy the file from the container to the temporary file
execute_buildah_command(&["copy", container_id, source_path, &temp_path])?;
// 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())