Compare commits
4 Commits
88e4a2a4b1
...
0fa9eddd1c
Author | SHA1 | Date | |
---|---|---|---|
0fa9eddd1c | |||
7bf2ffe47d | |||
768542afd3 | |||
e9a73327e3 |
@ -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!
|
||||
```
|
||||
|
@ -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", "./")` |
|
||||
|
@ -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
237
src/docs/docs/sal/text.md
Normal 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.
|
@ -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
|
||||
}
|
||||
|
||||
|
133
src/os/fs.rs
133
src/os/fs.rs
@ -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.
|
||||
*
|
||||
|
@ -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
421
src/os/package.rs
Normal 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
|
||||
}
|
565
src/package_implementation_plan.md
Normal file
565
src/package_implementation_plan.md
Normal 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
|
@ -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() {
|
||||
|
150
src/rhai/os.rs
150
src/rhai/os.rs
@ -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(),
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
@ -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...");
|
||||
|
210
src/rhaiexamples/containerd_grpc_setup.rhai
Normal file
210
src/rhaiexamples/containerd_grpc_setup.rhai
Normal 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
|
||||
--------------------------------------
|
||||
`);
|
@ -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"
|
||||
|
124
src/rhaiexamples/instructions_grpc.md
Normal file
124
src/rhaiexamples/instructions_grpc.md
Normal 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
|
||||
```
|
@ -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/");
|
||||
|
||||
}
|
||||
|
||||
|
113
src/rhaiexamples/package_management.rhai
Normal file
113
src/rhaiexamples/package_management.rhai
Normal 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();
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
Loading…
Reference in New Issue
Block a user