feat: added better install/uninstall features

This commit is contained in:
Byson94
2025-08-15 14:28:12 +05:30
parent 3af39c68ee
commit 32252932ce
9 changed files with 333 additions and 109 deletions

69
Cargo.lock generated
View File

@@ -213,6 +213,27 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.60.2",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -231,6 +252,7 @@ dependencies = [
"anyhow",
"clap",
"colored",
"dirs",
"env_logger",
"log",
"reqwest",
@@ -746,6 +768,16 @@ version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "libredox"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
dependencies = [
"bitflags",
"libc",
]
[[package]]
name = "linux-raw-sys"
version = "0.9.4"
@@ -878,6 +910,12 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "percent-encoding"
version = "2.3.1"
@@ -950,6 +988,17 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "redox_users"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom 0.2.16",
"libredox",
"thiserror",
]
[[package]]
name = "regex"
version = "1.11.1"
@@ -1295,6 +1344,26 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tinystr"
version = "0.8.1"

View File

@@ -10,6 +10,7 @@ edition = "2024"
anyhow = "1.0.98"
clap = { version = "4.5.43", features = ["derive"] }
colored = "3.0.0"
dirs = "6.0.0"
env_logger = "0.11.8"
log = "0.4.27"
reqwest = { version = "0.12.22", features = ["blocking", "json"] }

View File

@@ -1,3 +1,24 @@
# eiipm
**Eiipm** pronounced as `e-pm` is the package manager of Ewwii (Elkowar's Wacky Widgets Improved Interface) which is a fork of Eww (Elkowar's Widgets Widgets).
**Eiipm** pronounced as **ee-pee-em** is the package manager of Ewwii (Elkowar's Wacky Widgets Improved Interface).
## Common commands
The most common commands in eiipm are the `install`, `uninstall` and `update` commands. Just like their name, they are used to install, uninstall and update packages that you have installed.
**Usage example:**
```bash
# installs the statictranspl binary
eiipm install statictranspl
# uninstalls the statictranspl binary
eiipm uninstall statictranspl
# updates the statictranspl binary to the latest version
eiipm update statictranspl
```
## Uploading a custom plugin
If you made a custom plugin and want to register it to ewwii's package manifest, then you should checkout the [Ewwii-sh/eii-manifests](https://github.com/Ewwii-sh/eii-manifests) repository for more info.

View File

@@ -1,12 +1,27 @@
use colored::{Colorize};
use dirs;
use log::{debug, info, trace};
use reqwest::blocking::get;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::error::Error;
use std::fs::File;
use std::io::Write;
use std::fs;
use std::process::Command;
use log::{debug, info, error, trace};
use std::path::PathBuf;
use colored::{Colorize, ColoredString};
const DB_FILE: &str = "./eiipm/installed.toml";
#[derive(Deserialize, Serialize, Debug)]
struct PackageDB {
packages: HashMap<String, InstalledPackage>,
}
#[derive(Deserialize, Serialize, Debug)]
struct InstalledPackage {
repo_path: String,
files: Vec<String>,
pkg_type: String,
}
#[derive(Deserialize, Debug)]
struct PackageRootMeta {
@@ -16,8 +31,11 @@ struct PackageRootMeta {
#[derive(Deserialize, Debug)]
struct PackageMeta {
name: String,
version: f32,
install_url: String,
#[serde(rename = "type")]
pkg_type: String,
src: String,
files: Vec<String>,
build: Option<String>, // Optional build command
}
pub fn install_package(package_name: &str) -> Result<(), Box<dyn Error>> {
@@ -27,98 +45,130 @@ pub fn install_package(package_name: &str) -> Result<(), Box<dyn Error>> {
"https://raw.githubusercontent.com/Ewwii-sh/eii-manifests/main/manifests/{}.toml",
package_name
);
trace!(" Constructed manifest URL:\n {}", raw_manifest_url.underline());
trace!("Fetching manifest from {}", raw_manifest_url.underline());
let toml_content = http_get_string(&raw_manifest_url)?;
let root_meta: PackageRootMeta = toml::from_str(&toml_content)?;
let meta = &root_meta.metadata;
let toml_content = http_get_string(&raw_manifest_url).map_err(|e| {
error!(" [ERROR] Error fetching manifest for package '{}': {}", package_name.yellow(), e.to_string().red());
e
})?;
debug!(" Fetched manifest content:\n{}", toml_content.dimmed());
let home_dir = dirs::home_dir().ok_or("Failed to get home directory")?;
let eiipm_dir = home_dir.join("./.eiipm");
fs::create_dir_all(&eiipm_dir)?;
let root_meta: PackageRootMeta = toml::from_str(&toml_content).map_err(|e| {
error!(" [ERROR] Failed to parse manifest TOML for package '{}': {}", package_name.yellow(), e.to_string().red());
e
})?;
trace!(" Parsed manifest metadata:\n {:?}", root_meta.metadata);
let repo_name = meta
.src
.split('/')
.last()
.ok_or("Invalid src URL")?
.strip_suffix(".git")
.unwrap_or_else(|| meta.src.split('/').last().unwrap());
let mut install_script_path = std::env::temp_dir();
install_script_path.push(format!("{}.sh", package_name));
let repo_path = eiipm_dir.join(format!("cache/{}", repo_name));
info!(" Downloading install script:");
info!(" From: {}", root_meta.metadata.install_url.underline());
info!(" To: {}", install_script_path.display().to_string().bright_yellow());
// Clone or pull repo
if !repo_path.exists() {
info!("Cloning repository {} to {}", meta.src.underline(), repo_path.display());
let output = Command::new("git")
.args(&["clone", &meta.src, repo_path.to_str().unwrap()])
.output()?;
if !output.status.success() {
return Err(format!("Git clone failed: {}", String::from_utf8_lossy(&output.stderr)).into());
}
} else {
info!("Repository exists, pulling latest changes");
let output = Command::new("git")
.args(&["-C", repo_path.to_str().unwrap(), "pull"])
.output()?;
if !output.status.success() {
return Err(format!("Git pull failed: {}", String::from_utf8_lossy(&output.stderr)).into());
}
}
download_file(&root_meta.metadata.install_url, install_script_path.to_str().unwrap()).map_err(|e| {
error!(" [ERROR] Failed to download install script for package '{}': {}", package_name.yellow(), e.to_string().red());
e
})?;
// Optional build step
if let Some(build_cmd) = &meta.build {
info!("Running build command: {}", build_cmd);
let status = Command::new("sh")
.arg("-c")
.arg(build_cmd)
.current_dir(&repo_path)
.status()?;
if !status.success() {
return Err(format!("Build failed for package '{}'", package_name).into());
}
}
info!(" Running install script: {}", install_script_path.display().to_string().yellow());
// Determine target directory
let target_base_dir = match meta.pkg_type.as_str() {
"binary" => home_dir.join(".eiipm/bin"),
"theme" => env::current_dir()?,
"library" => home_dir.join(format!(".eiipm/lib/{}", package_name)),
other => return Err(format!("Unknown package type '{}'", other).into()),
};
fs::create_dir_all(&target_base_dir)?;
run_script(install_script_path.to_str().unwrap(), package_name)?;
// Copy files and track them
let mut installed_files = Vec::new();
for file in &meta.files {
let source = repo_path.join(file);
if !source.exists() {
return Err(format!("File '{}' not found in repo", source.display()).into());
}
info!(" Installation completed successfully for package '{}'", package_name.yellow().bold());
// Use just the filename for the target
let target = target_base_dir.join(
source
.file_name()
.ok_or_else(|| format!("Invalid file name for '{}'", file))?
);
std::fs::remove_file(install_script_path)?;
fs::create_dir_all(target_base_dir.clone())?;
fs::copy(&source, &target)?;
installed_files.push(target.to_string_lossy().to_string());
}
// Update DB
let mut db = load_db()?;
db.packages.insert(
meta.name.clone(),
InstalledPackage {
repo_path: repo_path.to_string_lossy().to_string(),
files: installed_files,
pkg_type: meta.pkg_type.clone(),
},
);
save_db(&db)?;
info!("Installation complete for '{}'", package_name.yellow().bold());
Ok(())
}
fn load_db() -> Result<PackageDB, Box<dyn Error>> {
let home_dir = dirs::home_dir().ok_or("Failed to get home directory")?;
let db_path = home_dir.join(DB_FILE);
if db_path.exists() {
let content = fs::read_to_string(&db_path)?;
let db: PackageDB = toml::from_str(&content)?;
Ok(db)
} else {
Ok(PackageDB { packages: HashMap::new() })
}
}
fn save_db(db: &PackageDB) -> Result<(), Box<dyn Error>> {
let home_dir = dirs::home_dir().ok_or("Failed to get home directory")?;
let db_path = home_dir.join(DB_FILE);
if let Some(parent) = db_path.parent() {
fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(db)?;
fs::write(db_path, content)?;
Ok(())
}
fn http_get_string(url: &str) -> Result<String, Box<dyn Error>> {
debug!(" Sending GET request to {}", url.dimmed());
debug!("Sending GET request to {}", url);
let response = get(url)?;
if !response.status().is_success() {
error!(" [ERROR] Failed to fetch URL {}: HTTP {}", url.yellow(), response.status());
return Err(format!("Failed to fetch URL {}: HTTP {}", url, response.status()).into());
}
let body = response.text()?;
debug!(" Received response body ({} bytes)", body.len());
Ok(body)
}
fn download_file(url: &str, output_path: &str) -> Result<(), Box<dyn Error>> {
debug!(" Sending GET request to download file from {}", url.dimmed());
let response = get(url)?;
if !response.status().is_success() {
error!(" [ERROR] Failed to download file from {}: HTTP {}", url.yellow(), response.status());
return Err(format!("Failed to download file from {}: HTTP {}", url, response.status()).into());
}
let content = response.text()?;
debug!(" Downloaded file content length: {} bytes", content.len());
debug!(" Creating file at '{}'", output_path.dimmed());
let mut file = File::create(output_path)?;
file.write_all(content.as_bytes())?;
trace!(" Successfully wrote to '{}'", output_path.bright_green());
Ok(())
}
fn run_script(script_path: &str, package_name: &str) -> Result<(), Box<dyn Error>> {
info!(" Executing script: {}", script_path.yellow());
let output = Command::new("sh")
.arg(script_path)
.output()?; // capture output
if !output.status.success() {
error!(" [ERROR] Script failed with status: {}", format!("{:?}", output.status).red());
error!(" [ERROR] stderr:");
for line in String::from_utf8_lossy(&output.stderr).lines() {
error!(" {}", line.red());
}
return Err(format!("Script execution failed with status: {:?}", output.status).into());
} else {
info!("{}", " Script executed successfully.".bright_green());
debug!(" Script stdout:\n{}", String::from_utf8_lossy(&output.stdout).dimmed());
}
Ok(())
Ok(response.text()?)
}

View File

@@ -1 +1,2 @@
pub mod install;
pub mod install;
pub mod uninstall;

View File

@@ -0,0 +1,66 @@
use colored::Colorize;
use dirs;
use log::{info};
use std::collections::HashMap;
use std::error::Error;
use std::fs;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
const DB_FILE: &str = ".config/eiipm/installed.toml";
#[derive(Deserialize, Serialize, Debug)]
struct PackageDB {
packages: HashMap<String, InstalledPackage>,
}
#[derive(Deserialize, Serialize, Debug)]
struct InstalledPackage {
repo_path: String,
files: Vec<String>,
pkg_type: String,
}
pub fn uninstall_package(package_name: &str) -> Result<(), Box<dyn Error>> {
info!("> Uninstalling package '{}'", package_name.yellow().bold());
let mut db = load_db()?;
if let Some(pkg) = db.packages.remove(package_name) {
for file in pkg.files {
let path = PathBuf::from(file);
if path.exists() {
fs::remove_file(&path)?;
info!("Removed file '{}'", path.display());
}
}
save_db(&db)?;
info!("Successfully uninstalled '{}'", package_name.yellow().bold());
} else {
info!("Package '{}' not found in database", package_name.yellow());
}
Ok(())
}
fn load_db() -> Result<PackageDB, Box<dyn Error>> {
let home_dir = dirs::home_dir().ok_or("Failed to get home directory")?;
let db_path = home_dir.join(DB_FILE);
if db_path.exists() {
let content = fs::read_to_string(&db_path)?;
let db: PackageDB = toml::from_str(&content)?;
Ok(db)
} else {
Ok(PackageDB { packages: HashMap::new() })
}
}
fn save_db(db: &PackageDB) -> Result<(), Box<dyn Error>> {
let home_dir = dirs::home_dir().ok_or("Failed to get home directory")?;
let db_path = home_dir.join(DB_FILE);
if let Some(parent) = db_path.parent() {
fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(db)?;
fs::write(db_path, content)?;
Ok(())
}

0
src/functions/update.rs Normal file
View File

View File

@@ -1,24 +1,32 @@
use clap::Parser;
use std::env;
use std::io::Write;
use log::Level;
mod opts;
mod functions;
use opts::Args;
use crate::functions::install::install_package;
use opts::{Args, Commands};
use functions::install::install_package;
use functions::uninstall::uninstall_package;
use clap::Parser;
use log::Level;
fn main() {
let args = Args::parse();
set_debug_levels(args.debug);
if let Some(install_pkg_name) = args.install.as_deref() {
install_package(install_pkg_name);
if args.debug {
log::info!("Debug logging enabled");
set_debug_levels(true);
}
if let Some(uninstall_pkg_name) = args.uninstall.as_deref() {
log::debug!("Uninstalling package: {}", uninstall_pkg_name);
match args.command {
Commands::Install { package } => {
if let Err(e) = install_package(&package) {
log::error!("Error installing '{}': {}", package, e);
}
}
Commands::Uninstall { package } => {
if let Err(e) = uninstall_package(&package) {
log::error!("Error uninstalling '{}': {}", package, e);
}
}
}
}

View File

@@ -1,19 +1,27 @@
use clap::Parser;
use clap::{Parser, Subcommand};
/// Eiipm package manager for ewwii.
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
#[command(version, about)]
pub struct Args {
/// Name of the package to install
#[arg(short, long)]
pub install: Option<String>,
/// Name of the package to uninstall
#[arg(short, long)]
pub uninstall: Option<String>,
/// Show debug logs
#[arg(long)]
pub debug: bool,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
/// Install a package
Install {
/// Name of the package to install
package: String,
},
/// Uninstall a package
Uninstall {
/// Name of the package to uninstall
package: String,
},
}