...
This commit is contained in:
parent
7bf2ffe47d
commit
0fa9eddd1c
@ -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.
|
||||
|
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
|
134
src/rhai/os.rs
134
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
|
||||
@ -45,6 +46,20 @@ pub fn register_os_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>>
|
||||
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(())
|
||||
}
|
||||
|
||||
@ -166,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
|
||||
//
|
||||
@ -211,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(),
|
||||
}
|
||||
}
|
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);
|
||||
|
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
|
||||
```
|
@ -2,25 +2,24 @@
|
||||
|
||||
|
||||
fn nerdctl_download(){
|
||||
// let name="nerdctl";
|
||||
// let url="https://github.com/containerd/nerdctl/releases/download/v2.0.4/nerdctl-2.0.4-linux-amd64.tar.gz";
|
||||
// download(url,`/tmp/${name}`,20000);
|
||||
// copy(`/tmp/${name}/*`,"/root/hero/bin/");
|
||||
// delete(`/tmp/${name}`);
|
||||
let name="nerdctl";
|
||||
let url="https://github.com/containerd/nerdctl/releases/download/v2.0.4/nerdctl-2.0.4-linux-amd64.tar.gz";
|
||||
download(url,`/tmp/${name}`,20000);
|
||||
copy(`/tmp/${name}/*`,"/root/hero/bin/");
|
||||
delete(`/tmp/${name}`);
|
||||
|
||||
// let name="containerd";
|
||||
// let url="https://github.com/containerd/containerd/releases/download/v2.0.4/containerd-2.0.4-linux-amd64.tar.gz";
|
||||
// download(url,`/tmp/${name}`,20000);
|
||||
// copy(`/tmp/${name}/bin/*`,"/root/hero/bin/");
|
||||
// delete(`/tmp/${name}`);
|
||||
let name="containerd";
|
||||
let url="https://github.com/containerd/containerd/releases/download/v2.0.4/containerd-2.0.4-linux-amd64.tar.gz";
|
||||
download(url,`/tmp/${name}`,20000);
|
||||
copy(`/tmp/${name}/bin/*`,"/root/hero/bin/");
|
||||
delete(`/tmp/${name}`);
|
||||
|
||||
// run("apt-get -y install buildah runc")
|
||||
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(url);
|
||||
copy(`/tmp/rfs`,"/root/hero/bin/");
|
||||
//delete(`/tmp/rfs`);
|
||||
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.
|
||||
|
Loading…
Reference in New Issue
Block a user