diff --git a/.gitignore b/.gitignore index 7e9708a..4a45cf4 100644 --- a/.gitignore +++ b/.gitignore @@ -16,16 +16,13 @@ Cargo.lock # Visual Studio Code *.code-workspace -/.vscode -.vscode/* -!.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json !.vscode/*.code-snippets .history/ *.vsix -.vscode +/.vscode/ # Packaging /packaging/**/pkg diff --git a/Cargo.toml b/Cargo.toml index 0688588..1fd9e5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,17 @@ [workspace] - members = [ + "paxy", + "paxy-cli", + "paxy-gui", "paxy-plugin", + "tests" +] +default-members = [ "paxy", "paxy-cli", "paxy-gui", - - "tests", + "tests" ] - -resolver = "2" - +resolver = "1" [workspace.package] @@ -20,7 +22,7 @@ homepage = "https://pax-hub.github.io/" authors = [ "shivanandvp ", "lylythechosenone", - "ferriswastaken", + "Ishaan S ", "flyingcakes85", ] keywords = ["package", "package-manager", "paxy", "packager"] @@ -40,6 +42,7 @@ paxy = { path = "paxy" } tracing = "0.1" tracing-appender = "0.2" tracing-subscriber = "0.3" +log = "0.4" # Configuration figment = "0.10" @@ -52,12 +55,12 @@ snafu = "0.8" # Data lazy_static = "1.4" -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1.0.200", features = ["derive"] } serde-aux = "4.5" serde_yaml = "0.9" tracing-serde = "0.1" speedy = "0.8" -log = "0.4" +itertools = "0.12" # Internationalization fluent = "0.16" @@ -71,4 +74,4 @@ console = "0.15" # GUI relm4 = "0.8" -relm4-components = "0.8" \ No newline at end of file +relm4-components = "0.8" diff --git a/Containerfile b/Containerfile index 43d5c99..217c45d 100644 --- a/Containerfile +++ b/Containerfile @@ -3,6 +3,7 @@ FROM archlinux:latest ENV RUSTC_WRAPPER=sccache ENV CARGO_INCREMENTAL=0 ENV CARGO_TARGET_DIR=/paxy/podman-target +ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.cargo/bin # Install Rust RUN pacman -Sy --noconfirm rustup cargo @@ -13,3 +14,6 @@ RUN rustup default nightly-2024-03-17 RUN pacman -Sy --noconfirm gdk-pixbuf2 pango gtk4 pkg-config # Extras RUN pacman -S --noconfirm sccache +RUN cargo install cargo-make +RUN cargo install cargo-binstall +RUN cargo binstall --no-confirm wasmer-cli \ No newline at end of file diff --git a/Makefile.toml b/Makefile.toml new file mode 100644 index 0000000..1e92228 --- /dev/null +++ b/Makefile.toml @@ -0,0 +1,2 @@ +[tasks.build] +args = ["build"] \ No newline at end of file diff --git a/paxy-cli/src/lib.rs b/paxy-cli/src/lib.rs index a0883de..7784a8d 100644 --- a/paxy-cli/src/lib.rs +++ b/paxy-cli/src/lib.rs @@ -1,5 +1,12 @@ +//! Has the [`run_cli`] function and the commandline interface template +//! [`cli_template::CliTemplate`] + +/// Calls the [`ui::run_common::`] function supplying it with the commandline +/// interface template as a type. Any errors are thrown back to the calling +/// function. A debug message is then displayed conveying that the program is +/// being run in the CLI mode. pub fn run_cli() -> Result<(), paxy::Error> { - let (_cli_input, _worker_guards) = ui::run_common::()?; + let (_cli_input, _logging_worker_guards) = ui::run_common::()?; tracing::debug!( "Running in {} mode... {}", @@ -51,7 +58,7 @@ mod cli_template { )] pub struct CliTemplate { #[command(flatten)] - pub global_arguments: ui::cli_template::GlobalArgs, + pub global_args: ui::cli_template::GlobalArgs, #[command(subcommand)] pub entity: Option, @@ -60,43 +67,39 @@ mod cli_template { /// Implement a trait that can extract standard global arguments from our /// own CLI template impl ui::GlobalArguments for CliTemplate { - type L = clap_verbosity_flag::InfoLevel; - - fn config_file(&self) -> &Option { - &self - .global_arguments - .config_file + fn config_filepath(&self) -> &Option { + self.global_args + .config_filepath() } fn is_json(&self) -> bool { - self.global_arguments - .json_flag + self.global_args + .is_json() } fn is_plain(&self) -> bool { - self.global_arguments - .plain_flag + self.global_args + .is_plain() } fn is_debug(&self) -> bool { - self.global_arguments - .debug_flag + self.global_args + .is_debug() } - fn is_no_color(&self) -> bool { - self.global_arguments - .no_color_flag + fn is_test(&self) -> bool { + self.global_args + .is_test() } - fn is_test(&self) -> bool { - self.global_arguments - .test_flag + fn is_no_color(&self) -> bool { + self.global_args + .is_no_color() } - fn verbosity(&self) -> &clap_verbosity_flag::Verbosity { - &self - .global_arguments - .verbose + fn verbosity_filter(&self) -> log::LevelFilter { + self.global_args + .verbosity_filter() } } @@ -408,6 +411,6 @@ mod cli_template { // region: RE-EXPORTS -pub use cli_template::*; +pub use cli_template::*; // Flatten the module heirarchy for easier access // endregion: RE-EXPORTS diff --git a/paxy-cli/src/main.rs b/paxy-cli/src/main.rs index 7a7b4c3..73509b7 100644 --- a/paxy-cli/src/main.rs +++ b/paxy-cli/src/main.rs @@ -1,3 +1,10 @@ +//! Starts execution at the [`main`] function. Offloads the implemenation +//! details to its corresponding library crate. + +/// Calls the [`paxy_cli::run_cli`] function and captures the returned +/// [`Result`]. If there was an error, the error message chain is printed to the +/// standard error stream (`stderr`). The program then returns an `0` or `1` +/// corresponding to "no error" or "error" based on the result. fn main() -> process::ExitCode { let return_value = paxy_cli::run_cli(); match return_value { diff --git a/paxy-gui/src/lib.rs b/paxy-gui/src/lib.rs index 7fb4393..4f97089 100644 --- a/paxy-gui/src/lib.rs +++ b/paxy-gui/src/lib.rs @@ -1,5 +1,12 @@ +//! Has the [`run_gui`] function and the commandline interface template +//! [`gui_cli_template::CliTemplate`] + +/// Calls the [`ui::run_common::`] function supplying it with the commandline +/// interface template as a type. Any errors are thrown back to the calling +/// function. A debug message is then displayed conveying that the program is +/// being run in the GUI mode. pub fn run_gui() -> Result<(), paxy::Error> { - let (_cli_input, _worker_guards) = ui::run_common::()?; + let (_cli_input, _logging_worker_guards) = ui::run_common::()?; tracing::debug!( "Running in {} mode... {}", @@ -15,7 +22,7 @@ pub fn run_gui() -> Result<(), paxy::Error> { pub enum Error { #[non_exhaustive] #[snafu(display(""), visibility(pub))] - GuiDummy {}, + GuiDummy {}, // No errors implemented yet } // region: IMPORTS @@ -28,7 +35,11 @@ use snafu::Snafu; // region: MODULES +/// The commandline interface for the GUI program. Allows one to specify flags +/// that control output on a console. mod gui_cli_template { + + /// The base commandline template consists of global arguments #[derive(Parser, Debug)] #[command(version, author, about, args_conflicts_with_subcommands = true)] pub struct CliTemplate { @@ -36,44 +47,42 @@ mod gui_cli_template { pub global_args: ui::cli_template::GlobalArgs, } + /// Implement a trait that can extract standard global arguments from our + /// own CLI template impl ui::GlobalArguments for CliTemplate { - type L = clap_verbosity_flag::InfoLevel; - - fn config_file(&self) -> &Option { - &self - .global_args - .config_file + fn config_filepath(&self) -> &Option { + self.global_args + .config_filepath() } fn is_json(&self) -> bool { self.global_args - .json_flag + .is_json() } fn is_plain(&self) -> bool { self.global_args - .plain_flag + .is_plain() } fn is_debug(&self) -> bool { self.global_args - .debug_flag + .is_debug() } - fn is_no_color(&self) -> bool { + fn is_test(&self) -> bool { self.global_args - .no_color_flag + .is_test() } - fn is_test(&self) -> bool { + fn is_no_color(&self) -> bool { self.global_args - .test_flag + .is_no_color() } - fn verbosity(&self) -> &clap_verbosity_flag::Verbosity { - &self - .global_args - .verbose + fn verbosity_filter(&self) -> log::LevelFilter { + self.global_args + .verbosity_filter() } } @@ -91,6 +100,6 @@ mod gui_cli_template { // region: RE-EXPORTS -pub use gui_cli_template::*; +pub use gui_cli_template::*; // Flatten the module heirarchy for easier access // endregion: RE-EXPORTS diff --git a/paxy-gui/src/main.rs b/paxy-gui/src/main.rs index edf4ea4..f96597b 100644 --- a/paxy-gui/src/main.rs +++ b/paxy-gui/src/main.rs @@ -1,3 +1,10 @@ +//! Starts execution at the [`main`] function. Offloads the implemenation +//! details to its corresponding library crate. + +/// Calls the [`paxy_gui::run_gui`] function and captures the returned +/// [`Result`]. If there was an error, the error message chain is printed to the +/// standard error stream (`stderr`). The program then returns an `0` or `1` +/// corresponding to "no error" or "error" based on the result. fn main() -> process::ExitCode { let return_value = paxy_gui::run_gui(); match return_value { diff --git a/paxy-plugin/Cargo.toml b/paxy-plugin/Cargo.toml new file mode 100644 index 0000000..3b075c7 --- /dev/null +++ b/paxy-plugin/Cargo.toml @@ -0,0 +1,25 @@ +cargo-features = ["per-package-target"] +[package] +name = "paxy-plugin" +version.workspace = true +description.workspace = true +repository.workspace = true +homepage.workspace = true +authors.workspace = true +keywords.workspace = true +exclude.workspace = true +categories.workspace = true +documentation.workspace = true +readme.workspace = true +edition.workspace = true +license.workspace = true +default-target = "wasm32-wasi" + +[dependencies] +extism-pdk = "1.1.0" +toml = "0.8.12" +semver = {git = "https://github.com/FerrisWasTaken/semver.git", features = ["extism_support"]} +reqwest = { version = "0.11", features = ["blocking", "json"] } + +[lib] +crate-type = ["cdylib"] diff --git a/paxy-plugin/examples/paxy.px b/paxy-plugin/examples/paxy.px new file mode 100644 index 0000000..e7113db --- /dev/null +++ b/paxy-plugin/examples/paxy.px @@ -0,0 +1,13 @@ +name = "Paxy" +repo = "https://github.com/Pax-Hub/paxy" + +[[version]] +ver = "0.1.0" +src = "https://github.com/Pax-hub/paxy/releases/tag/0.1.0" +dependencies = ["rust >= 1.72.0"] +install_inst = "mv pkg/paxy /usr/bin" + +[[version]] +ver = "0.1.1" +src = "https://github.com/Pax-hub/paxy/releases/tag/0.1.1" +install_inst = "mv /pkg/paxy /usr/bin" \ No newline at end of file diff --git a/paxy-plugin/src/lib.rs b/paxy-plugin/src/lib.rs new file mode 100644 index 0000000..51a0a03 --- /dev/null +++ b/paxy-plugin/src/lib.rs @@ -0,0 +1,64 @@ +use std::{fs, process::Command}; + +use extism_pdk::{plugin_fn, FnResult, Json}; +use semver::{Version, VersionReq}; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct Pkg { + name: String, + repo: String, + version: Vec, +} + +#[derive(Debug, Deserialize)] +struct Ver { + ver: Version, + dependencies: Vec, + src: String, + install_inst: String, +} + +fn parse_file() -> Pkg { + toml::from_str( + fs::read_to_string("/pkg") + .unwrap() + .as_str(), + ) + .expect("invalid file") +} + +fn exec_inst(commands: String) { + for stmt in commands.split(';') { + let mut args = stmt + .split(' ') + .collect::>(); + let mut cmd = Command::new( + args.get(0) + .expect("malformed command"), + ); + args.remove(0); + cmd.args(args); + let mut handle = match cmd.spawn() { + Ok(c) => c, + Err(_) => panic!("Illegal expression: {stmt}"), + }; + handle + .wait() + .unwrap(); + } +} + +#[plugin_fn] +pub fn prep(req: Json) -> FnResult { + let pkg = parse_file(); + let ver; + let req = req.0; + for version in pkg.version { + if version.ver == req { + ver = version; + break; + } + } + todo!() +} diff --git a/paxy/Cargo.toml b/paxy/Cargo.toml index 1af4864..eda3d86 100644 --- a/paxy/Cargo.toml +++ b/paxy/Cargo.toml @@ -20,6 +20,7 @@ edition.workspace = true tracing = { workspace = true } tracing-appender = { workspace = true } tracing-subscriber = { workspace = true } +log = { workspace = true, features = ["serde"] } # Configuration figment = { workspace = true, features = ["toml", "json", "yaml", "env"] } @@ -37,7 +38,7 @@ serde-aux = { workspace = true } serde_yaml = { workspace = true } tracing-serde = { workspace = true } speedy = { workspace = true } -log = { workspace = true, features = ["serde"] } +itertools = { workspace = true } # Internationalization fluent = { workspace = true } @@ -53,8 +54,11 @@ console = { workspace = true } home = "0.5.9" toml = "0.8.10" pollster = "0.3" -reqwest = "0.11" +reqwest = "0.12" url = { version = "2.3", features = ["serde"] } extism = "1.2.0" bson = "2.9.0" git2 = {version = "0.18.3", default-features = false, features = ["https"]} + +[dev-dependencies] +serial_test = "3.1.1" \ No newline at end of file diff --git a/paxy/src/actions/mod.rs b/paxy/src/actions/mod.rs index 1c95950..f7b164e 100644 --- a/paxy/src/actions/mod.rs +++ b/paxy/src/actions/mod.rs @@ -10,8 +10,49 @@ pub enum Error { RepositoryError { source: repository::Error }, } +#[macro_export] +macro_rules! home { + () => { + match home::home_dir() { + Some(path) => path, + None => panic!("Impossible to get your home dir!"), + } + }; +} + +#[inline] +pub fn ensure_path(path: Option<&PathBuf>) { + if path.is_none() { + let mut file = home!(); + file.push(".paxy"); + if !file.is_dir() { + ::std::fs::create_dir_all(file).expect("Inufficient permissions"); + } + } else { + if !path + .unwrap() + .is_dir() + { + ::std::fs::create_dir_all( + path.unwrap() + .clone(), + ) + .expect("Inufficient permissions"); + } + } +} + +#[inline] +pub(crate) fn ensure_file(file: &PathBuf, f: F) { + if !file.is_file() { + f(File::create(file).unwrap()) + } +} + // region: IMPORTS +use std::{fs::File, path::{Path, PathBuf}}; + use snafu::Snafu; // endregion: IMPORTS diff --git a/paxy/src/actions/repository/install.rs b/paxy/src/actions/repository/install.rs index f34d834..34fbfcc 100644 --- a/paxy/src/actions/repository/install.rs +++ b/paxy/src/actions/repository/install.rs @@ -6,7 +6,7 @@ fn add_repo(repo: &str, name: &str) { file.push("repos.bson"); let mut doc = if !file.is_file() { warn!("file not found. Creating"); - let doc = doc! {"paxy-official": "https://github.com/Pax-Hub/paxy-pkg-repository.git"}; + let doc = doc! {"paxy-pkgs": "https://github.com/Pax-Hub/paxy-pkg-repository.git"}; let mut buf = vec![]; doc.to_writer(&mut buf) .unwrap(); @@ -25,7 +25,10 @@ fn add_repo(repo: &str, name: &str) { file.push("repos"); file.push(name); ensure_path(Some(&file)); - Repository::clone(repo, file).unwrap(); + if Repository::clone(repo, file.clone()).is_err() { + remove_dir_all(file.clone()).unwrap(); + Repository::clone(repo, file); + } } #[allow(dead_code)] @@ -45,7 +48,7 @@ pub enum Error { // region: IMPORTS use std::{ - fs::{write, File}, + fs::{remove_dir_all, write, File}, path::PathBuf, }; @@ -54,7 +57,7 @@ use git2::Repository; use log::{info, warn}; use snafu::Snafu; -use crate::actions::repository::ensure_path; +use crate::actions::ensure_path; // endregion: IMPORTS @@ -62,18 +65,25 @@ use crate::actions::repository::ensure_path; #[cfg(test)] mod tests { + use std::fs; + use super::*; + use serial_test::serial; #[test] - fn add_repo_norm_test() { + #[serial] + fn repo_add_norm() { + let mut repo_file = home!(); + repo_file.push(".paxy"); + repo_file.push("repos.bson"); + if repo_file.is_file() { + fs::remove_file(&repo_file).unwrap(); + } add_repo("https://github.com/Pax-Hub/paxy-pkg-repository.git", "paxy"); - let mut file = home!(); - file.push(".paxy"); - file.push("repos.bson"); - let doc = Document::from_reader(File::open(file.clone()).unwrap()).unwrap(); + let doc = Document::from_reader(File::open(repo_file.clone()).unwrap()).unwrap(); assert_eq!( doc, - doc! {"paxy-official": "https://github.com/Pax-Hub/paxy-pkg-repository.git", "paxy": "https://github.com/Pax-Hub/paxy-pkg-repository.git"} + doc! {"paxy-pkgs": "https://github.com/Pax-Hub/paxy-pkg-repository.git", "paxy": "https://github.com/Pax-Hub/paxy-pkg-repository.git"} ); } } diff --git a/paxy/src/actions/repository/mod.rs b/paxy/src/actions/repository/mod.rs index 8268886..1a2231f 100644 --- a/paxy/src/actions/repository/mod.rs +++ b/paxy/src/actions/repository/mod.rs @@ -1,35 +1,3 @@ -#[macro_export] -macro_rules! home { - () => { - match home::home_dir() { - Some(path) => path, - None => panic!("Impossible to get your home dir!"), - } - }; -} - -#[inline] -pub fn ensure_path(path: Option<&PathBuf>) { - if path.is_none() { - let mut file = home!(); - file.push(".paxy"); - if !file.is_dir() { - ::std::fs::create_dir_all(file).expect("Inufficient permissions"); - } - } else { - if !path - .unwrap() - .is_dir() - { - ::std::fs::create_dir_all( - path.unwrap() - .clone(), - ) - .expect("Inufficient permissions"); - } - } -} - #[derive(Debug, Snafu)] #[non_exhaustive] pub enum Error { @@ -56,6 +24,10 @@ pub enum Error { #[non_exhaustive] #[snafu(display("Could not downgrade:\n {source}"))] CouldNotDowngrade { source: downgrade::Error }, + + #[non_exhaustive] + #[snafu(display("Could not remove:\n {source}"))] + CouldNotRemove { source: rm_repo::Error } } // region: IMPORTS @@ -74,6 +46,7 @@ pub mod list; pub mod search; pub mod uninstall; pub mod update; +pub mod rm_repo; // endregion: MODULES @@ -92,5 +65,7 @@ pub use search::*; pub use uninstall::*; #[allow(unused_imports)] pub use update::*; +#[allow(unused_imports)] +pub use rm_repo::*; // endregion: RE-EXPORTS diff --git a/paxy/src/actions/repository/rm_repo.rs b/paxy/src/actions/repository/rm_repo.rs new file mode 100644 index 0000000..96fa649 --- /dev/null +++ b/paxy/src/actions/repository/rm_repo.rs @@ -0,0 +1,71 @@ +//region: IMPORTS +use bson::Document; +use crate::data::config::{Config,load_conf}; +use std::{ + path::PathBuf, + io::Write, +}; +use snafu::{Snafu,ensure,OptionExt,ResultExt}; +//endregion: IMPORTS + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to serialize repo data into byte stream {source}"))] + FailedToSerialize {source: bson::ser::Error}, + + #[snafu(display("failed to write to repo data file {source}"))] + FailedToWriteData {source: std::io::Error}, + + #[snafu(display("repo not found"))] + FailedToFindRepo {}, +} + +#[allow(dead_code)] +fn delete_repo(repo_name: &str) -> Result<(),Error> { + let mut config = load_conf(); + let mut readable_data = config.repositories; + + readable_data.get(repo_name).context(FailedToFindRepoSnafu{})?; + readable_data.remove(repo_name); + let mut buf = vec![]; + let rbd_result = readable_data.to_writer(&mut buf); + + rbd_result.context(FailedToSerializeSnafu{})?; + + let mut repos_file_path: PathBuf = home!(); + repos_file_path.push(".paxy"); + repos_file_path.push("repos.bson"); + let mut file = std::fs::OpenOptions::new().write(true).truncate(true).open(repos_file_path).unwrap(); + let ftw_result = file.write_all(&buf); + + ftw_result.context(FailedToWriteDataSnafu{})?; + + config.repositories = readable_data; + + let mut config_toml_path: PathBuf = home!(); + + config_toml_path.push(".paxy"); + config_toml_path.push("config.toml"); + + let mut config_toml_file = std::fs::OpenOptions::new().write(true).truncate(true).open(config_toml_path).unwrap(); + config_toml_file.write_all( + toml::to_string(&config) + .unwrap() + .as_bytes(), + ) + .expect("Permission error"); + + Ok(()) +} + +//region: TESTS +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn delete_repo_test() { + Config::default(); + assert_eq!(delete_repo("paxy-pkgs").is_ok(),true); + } +} +//endregion: TESTS diff --git a/requirements.txt b/paxy/src/actions/rm_repo.rs similarity index 100% rename from requirements.txt rename to paxy/src/actions/rm_repo.rs diff --git a/paxy/src/app/config.rs b/paxy/src/app/config.rs index 0a59e0c..ff512e3 100644 --- a/paxy/src/app/config.rs +++ b/paxy/src/app/config.rs @@ -3,17 +3,21 @@ /// local paths. overridden by environment variables starting with `PAXY_`, /// overridden by the configuration file specified by the commandline. /// Values from only files with supported file extensions would be merged. -pub fn init_config(config_filepath: Option<&Path>) -> Result<(Config, Vec), Error> { - let mut candidate_config_filepath_stubs: Vec = Vec::new(); +pub fn init_config(cli_global_arguments: G) -> Result<(Config, Vec), Error> +where + G: ui::GlobalArguments, + ::L: clap_verbosity_flag::LogLevel, +{ + let mut candidate_config_filepaths: Vec = Vec::new(); // Global directories #[cfg(target_family = "unix")] - candidate_config_filepath_stubs.extend(["/etc/xdg".into(), "/etc".into()]); + candidate_config_filepaths.extend(["/etc/xdg".into(), "/etc".into()]); #[cfg(target_os = "windows")] candidate_config_filepath_stubs.extend([""]); // Local directories - candidate_config_filepath_stubs.push( + candidate_config_filepaths.push( directories::BaseDirs::new() .context(RetreiveConfigUserAppBaseDirectoriesSnafu {})? .config_dir() @@ -21,30 +25,28 @@ pub fn init_config(config_filepath: Option<&Path>) -> Result<(Config, Vec) -> Result<(Config, Vec, +pub struct ConfigTemplate { + pub config_filepaths: Vec, + pub log_filepath_stub: PathBuf, + pub console_output_format: ConsoleOutputFormat, +} - pub log_level_filter: Option, +impl Default for ConfigTemplate { + fn default() -> Self { + Self { + config_filepaths: Vec::new(), + log_filepath_stub: PathBuf::default(), + console_output_format: ConsoleOutputFormat::default(), + } + } +} - pub no_color: Option, +pub struct Config { + pub figment: Figment, } impl Config { pub fn new() -> Self { - Self::default() + Self { + figment: Figment::from(ConfigTemplate::default()), + } } -} -impl Default for Config { - fn default() -> Self { - Config { - log_directory: None, - log_level_filter: Some(log::LevelFilter::Info), - no_color: Some(false), + pub fn with_overriding_file>(&mut self, filepath: P) -> &mut Self { + let filepath: &Path = filepath.as_ref(); + if let Some(file_extension) = filepath.extension() { + file_extension = file_extension + .to_string_lossy() + .to_lowercase(); + match (file_extension) { + "toml" => { + self.figment = self + .figment + .admerge(Toml::file(filepath)); + } + "json" => { + self.figment = self + .figment + .admerge(Json::file(filepath)); + } + "yaml" | "yml" => { + self.figment = self + .figment + .admerge(Yaml::file(filepath)); + } + } } + + self } -} -impl Provider for Config { - fn metadata(&self) -> Metadata { - Metadata::named("Library Config") + pub fn with_overriding_filepath_stub>(&mut self, filepath_stub: P) -> &mut Self { + let filepath_stub: PathBuf = filepath_stub.into(); + + + self } - fn data(&self) -> Result, figment::Error> { - figment::providers::Serialized::defaults(Config::default()).data() + pub fn with_overriding_files(&mut self, filepaths: I) -> &mut Self + where + P: AsRef, + I: Iterator, + { + filepaths.for_each(|filepath| self.with_overriding_file(filepath)); + + self } - fn profile(&self) -> Option { - None + pub fn with_overriding_env>(&mut self, prefix: S) -> &mut Self { + let prefix = prefix.as_ref(); + self.figment = self + .figment + .admerge(Env::prefixed(prefix)); + + self } -} -#[derive(Debug, Snafu)] -#[non_exhaustive] -pub enum Error { - #[non_exhaustive] - #[snafu( - display("could not retreive the XDG base directories for the user"), - visibility(pub) - )] - RetreiveConfigUserAppBaseDirectories {}, + pub fn with_overriding_args(&mut self, cli_arguments: A) -> &mut Self { + if let Some(path) = cli_arguments.config_filepath() { + self.figment = self + .figment + .admerge(("config_filepaths", path)); + } - #[non_exhaustive] - #[snafu( - display("could not retreive configuration information: {source}"), - visibility(pub) - )] - ExtractConfig { source: figment::Error }, + let console_output_mode = cli_arguments.console_output_mode(); + if console_output_mode != ConsoleOutputMode::Regular { + self.figment = self + .figment + .admerge(("console_output_format.mode", console_output_mode)); + } + + let current_max_verbosity = self + .figment + .extract_inner::("console_output_format.max_verbosity"); + let requested_max_verbosity = cli_arguments.max_output_verbosity(); + if let Ok(current_max_verbosity) = current_max_verbosity { + if cli_requested_max_verbosity > current_max_verbosity { + self.figment = self + .figment + .admerge(( + "console_output_format.max_verbosity", + requested_max_verbosity, + )) + } + } + + let current_no_color = self + .figment + .extract_inner::("console_output_format.no_color"); + let requested_no_color = + cli_arguments.is_no_color() || cli_arguments.is_plain() || cli_arguments.is_json(); + let env_no_color = env::var("NO_COLOR").is_ok() + || env::var(format!( + "{}_NO_COLOR", + String::from(*app::APP_NAME).to_uppercase() + )) + .is_ok() + || env::var("TERM").is_ok_and(|env_term_value| env_term_value.to_lowercase == "dumb"); + if !current_no_color && (requested_no_color || env_no_color) { + self.figment = self + .figment + .admerge(("console_output_format.no_color", true)); + } + + self + } + + pub fn object(&self) -> Result { + self.figment + .extract() + .context(ExtractConfigSnafu {})? + } } // region: IMPORTS -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use figment::{ providers::{Env, Format, Json, Toml, Yaml}, @@ -149,9 +240,35 @@ use figment::{ Profile, Provider, }; +use itertools::Itertools; +use log::LevelFilter; use serde::{Deserialize, Serialize}; use snafu::{OptionExt, ResultExt, Snafu}; +use super::ui::{ConsoleOutputMode, GlobalArguments}; use crate::app; +use crate::app::ui; // endregion: IMPORTS + +// region: ERRORS + +#[derive(Debug, Snafu)] +#[non_exhaustive] +pub enum Error { + #[non_exhaustive] + #[snafu( + display("could not retreive the XDG base directories for the user"), + visibility(pub) + )] + RetreiveConfigUserAppBaseDirectories {}, + + #[non_exhaustive] + #[snafu( + display("could not retreive configuration information: {source}"), + visibility(pub) + )] + ExtractConfig { source: figment::Error }, +} + +// endregion: ERRORS diff --git a/paxy/src/app/i18n.rs b/paxy/src/app/i18n.rs index 5a5f554..ef0eadc 100644 --- a/paxy/src/app/i18n.rs +++ b/paxy/src/app/i18n.rs @@ -1,3 +1,13 @@ +// TODO: The module code goes here + +// region: IMPORTS + +use snafu::Snafu; + +// endregion: IMPORTS + +// region: ERRORS + #[derive(Debug, Snafu)] #[non_exhaustive] pub enum Error { @@ -6,8 +16,4 @@ pub enum Error { I18nDummy {}, } -// region: IMPORTS - -use snafu::Snafu; - -// endregion: IMPORTS +// endregion: ERRORS diff --git a/paxy/src/app/logging.rs b/paxy/src/app/logging.rs index c7a6478..86f27f4 100644 --- a/paxy/src/app/logging.rs +++ b/paxy/src/app/logging.rs @@ -1,12 +1,9 @@ -pub fn init_log( - preferred_log_dirpath: Option, - preferred_log_level_filter: Option, -) -> Result<(Handle, PathBuf), Error> { +pub fn init_log(max_output_verbosity: log::LevelFilter) -> Result<(Handle, PathBuf), Error> { let log_filename = format!("{}.log", *app::APP_NAME); let log_dirpath = obtain_log_dirpath(preferred_log_dirpath)?; let log_file_appender = tracing_appender::rolling::daily(log_dirpath.clone(), log_filename.clone()); - let log_level_filter = preferred_log_level_filter.unwrap_or(LevelFilter::INFO); + let log_level_filter = tracing_level_filter_from_log_level_filter(max_output_verbosity); // Obtain writers to various logging destinations and worker guards (for // keeping the streams alive) @@ -188,6 +185,17 @@ fn obtain_log_dirpath(preferred_log_dirpath: Option) -> Result LevelFilter { + match level_filter { + log::LevelFilter::Off => LevelFilter::OFF, + log::LevelFilter::Error => LevelFilter::ERROR, + log::LevelFilter::Warn => LevelFilter::WARN, + log::LevelFilter::Info => LevelFilter::INFO, + log::LevelFilter::Debug => LevelFilter::DEBUG, + log::LevelFilter::Trace => LevelFilter::TRACE, + } +} + type OutputModeSwitchFunction = Box Result<(), Error>>; pub struct Handle { diff --git a/paxy/src/app/mod.rs b/paxy/src/app/mod.rs index ddba724..622a1e8 100644 --- a/paxy/src/app/mod.rs +++ b/paxy/src/app/mod.rs @@ -2,6 +2,18 @@ lazy_static! { pub static ref APP_NAME: &'static str = "paxy"; } +// region: IMPORTS + +use std::path::PathBuf; + +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use snafu::Snafu; + +// endregion: IMPORTS + +// region: ERRORS + #[derive(Debug, Snafu)] #[non_exhaustive] pub enum Error { @@ -19,6 +31,13 @@ pub enum Error { source: config::Error, }, + #[non_exhaustive] + #[snafu(display("in the UI: {source}"), visibility(pub))] + Ui { + #[snafu(backtrace)] + source: ui::Error, + }, + #[non_exhaustive] #[snafu(display("in internationalization: {source}"), visibility(pub))] Internationalization { @@ -27,17 +46,14 @@ pub enum Error { }, } -// region: IMPORTS -use lazy_static::lazy_static; -use snafu::Snafu; - -// endregion: IMPORTS +// endregion: ERRORS -// region: MODULES +// region: EXTERNAL-SUBMODULES pub mod config; pub mod i18n; pub mod logging; +pub mod ui; // endregion: MODULES @@ -49,5 +65,6 @@ pub use config::*; pub use i18n::*; #[allow(unused_imports)] pub use logging::*; +mod plugins; // endregion: RE-EXPORTS diff --git a/paxy/src/app/plugins.rs b/paxy/src/app/plugins.rs new file mode 100644 index 0000000..4420397 --- /dev/null +++ b/paxy/src/app/plugins.rs @@ -0,0 +1,68 @@ +use std::{ + fs::{write, File}, + path::{Path, PathBuf}, + str::FromStr, +}; + +use bson::{doc, Document}; +use extism::{Manifest, PluginBuilder, Wasm}; + +use crate::{actions::ensure_path, home}; + +#[allow(unused)] +#[allow(clippy::boxed_local)] +pub(crate) fn plugin(manifest: Box) -> (Wasm, PathBuf) { + let mut file = home!(); + file.push(".paxy"); + file.push("plugins"); + ensure_path(Some(&file)); + file.push("plugins.bson"); + let plugins = if !file.is_file() { + let mut buf = vec![]; + let doc = doc! {"px": "paxy.wasm"}; + doc.to_writer(&mut buf) + .unwrap(); + write(file, buf).unwrap(); + doc + } else { + Document::from_reader(File::open(file).unwrap()).unwrap() + }; + let plugin = plugins + .get( + manifest + .extension() + .expect("unknown manifest type") + .to_str() + .unwrap(), + ) + .unwrap() + .to_string(); + (Wasm::file(&plugin), PathBuf::from_str(&plugin).unwrap()) +} + +#[allow(unused)] +pub fn call_plugin(wasm: Wasm, pkg: PathBuf) { + let mut tmp = home!(); + tmp.push(".paxy"); + tmp.push("tmp"); + ensure_path(Some(&tmp)); + tmp.pop(); + tmp.push("fakeroot"); + ensure_path(Some(&tmp)); + let manifest = Manifest::new([wasm]).with_allowed_paths( + [ + (tmp.clone(), PathBuf::from("/tmp")), + (pkg, PathBuf::from("/pkg")), + (tmp, PathBuf::from("/")), + ] + .iter() + .cloned(), + ); + let plugin = PluginBuilder::new(manifest).with_wasi(true); + let mut plugin = plugin + .build() + .unwrap(); + plugin + .call::<&str, &str>("process", "") + .unwrap(); +} diff --git a/paxy/src/app/ui.rs b/paxy/src/app/ui.rs new file mode 100644 index 0000000..0aa279f --- /dev/null +++ b/paxy/src/app/ui.rs @@ -0,0 +1,449 @@ +#[tracing::instrument(level = "trace")] +pub fn run_common() -> Result<(C, Vec), crate::Error> +where + C: clap::Parser + GlobalArguments + fmt::Debug, + ::L: LogLevel, +{ + // Obtain CLI arguments + let cli_input = C::parse(); + + // Obtain user configuration + let (config, config_filepath_stubs) = config::init_config(&cli_input.config_file()) + .context(app::ConfigSnafu {}) + .context(crate::AppSnafu)?; + + // Begin logging + let (mut logging_handle, log_filepath) = logging::init_log( + &config + .cli_output_format + .requested_verbosity, + ) + .context(app::LoggingSnafu {}) + .context(crate::AppSnafu {})?; + + // Adjust output formatting if requested + adjust_output_formatting(&config.cli_output_format, &logging_handle); + + emit_welcome_messages(); + + emit_diagnostic_messages(config_filepath_stubs, log_filepath, &cli_input); + + emit_test_messages(); + + Ok((cli_input, logging_handle.worker_guards)) +} + +fn resolve_max_output_verbosity( + cli_output_format: &CliOutputFormat, + cli_global_arguments: G, +) -> log::LevelFilter { + let verbosity_flag_filter = cli_output_format.requested_verbosity; + + if matches!( + cli_output_format.mode, + CliOutputMode::Plain | CliOutputMode::Json + ) { + return Some(LevelFilter::Info); + } else if verbosity_flag_filter < clap_verbosity_flag::LevelFilter::Debug + && cli_global_arguments.is_debug() + { + return Some(LevelFilter::Debug); + } else { + return verbosity_flag_filter + .as_str() + .parse() + .ok(); + } +} + +fn adjust_output_formatting( + cli_output_format: &CliOutputFormat, + mut logging_handle: &logging::Handle, +) { + // Turn off colors if requested + if matches!( + cli_output_format.mode, + CliOutputMode::Plain | CliOutputMode::Json + ) || cli_output_format.no_color + || is_env_variable_set("NO_COLOR") + || is_env_variable_set(format!( + "{}_NO_COLOR", + String::from(*app::APP_NAME).to_uppercase() + )) + { + anstream::ColorChoice::Never.write_global(); + owo_colors::set_override(false); + } + + // Change output mode if requested + match cli_output_format.mode { + CliOutputMode::Plain => logging_handle + .switch_to_plain() + .context(app::LoggingSnafu {}) + .context(crate::AppSnafu {})?, + CliOutputMode::Json => logging_handle + .switch_to_json() + .context(app::LoggingSnafu {}) + .context(crate::AppSnafu {})?, + CliOutputMode::Test => logging_handle + .switch_to_test() + .context(app::LoggingSnafu {}) + .context(crate::AppSnafu {})?, + _ => {} + } +} + +fn is_env_variable_set>(env_variable_name: S) -> bool { + env::var(env_variable_name.as_ref()).map_or(false, |value| !value.is_empty()) +} + +fn emit_welcome_messages() { + // Welcome messages + tracing::debug!( + "{} - {}", + "Paxy".bold(), + "A package manager that gets out of your way".magenta() + ); + tracing::debug!( + "{} {} {}", + console::Emoji("✉️", ""), + "shivanandvp".italic(), + "".italic() + ); +} + +fn emit_diagnostic_messages( + config_filepath_stubs: Vec, + log_filepath: PathBuf, + cli_input: &C, +) where + C: clap::Parser + CliModifier + fmt::Debug, + ::L: LogLevel, +{ + tracing::debug!( + "{} The {} is {}... {}", + console::Emoji("⚙️", ""), + "configuration".cyan(), + "loaded".green(), + console::Emoji("✅", ""), + ); + tracing::debug!( + "{} The {} has {}... {}", + console::Emoji("📝", ""), + "logging".cyan(), + "begun".green(), + console::Emoji("✅", ""), + ); + + tracing::debug!( + "{} {} {:?}", + console::Emoji("📂", ""), + "Config Filepath(s) (without file extensions):".magenta(), + config_dirpaths, + ); + + tracing::debug!( + "{} {} {:?}", + console::Emoji("📂", ""), + "Log Filepath:".magenta(), + log_filepath + ); + + tracing::trace!( + "{} {} {:#?}", + console::Emoji("⌨️", ""), + "CLI input arguments:" + .magenta() + .dimmed(), + cli_input.dimmed() + ); +} + +fn emit_test_messages() { + tracing::debug!( + target:"TEST", "{}{}{}{}{}{}{}{}", + "███".black(), + "███".red(), + "███".green(), + "███".yellow(), + "███".blue(), + "███".purple(), + "███".cyan(), + "███".white() + ); + tracing::debug!( + target:"TEST", "{}{}{}{}{}{}{}{}", + "███".bright_black(), + "███".bright_red(), + "███".bright_green(), + "███".bright_yellow(), + "███".bright_blue(), + "███".bright_purple(), + "███".bright_cyan(), + "███".bright_white() + ); + + tracing::trace!(target:"TEST", "{} Testing trace!...", console::Emoji("🧪", "")); + tracing::debug!(target:"TEST", "{} Testing debug!...", console::Emoji("🧪", "")); + tracing::info!(target:"TEST", "{} Testing info!...", console::Emoji("🧪", "")); + tracing::warn!(target:"TEST", "{} Testing warn!...", console::Emoji("🧪", "")); + tracing::error!(target:"TEST", "{} Testing error!...", console::Emoji("🧪", "")); + + tracing::info!(target:"JSON", "{} Testing: {}", console::Emoji("🧪", ""), "{\"JSON\": \"Target\"}"); + tracing::info!(target:"PLAIN", "{} Testing: Plain Target", console::Emoji("🧪", "")); +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConsoleOutputFormat { + pub mode: ConsoleOutputMode, + + pub max_verbosity: log::LevelFilter, + + pub no_color: bool, +} + +impl Default for ConsoleOutputFormat { + fn default() -> Self { + Self { + mode: ConsoleOutputMode::default(), + max_verbosity: log::LevelFilter::Info, + no_color: false, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ConsoleOutputMode { + Regular, + Plain, + Json, + Test, +} + +impl Default for ConsoleOutputMode { + fn default() -> Self { + CliOutputMode::Regular + } +} + +pub trait CliModifier: GlobalArguments +where + ::L: LogLevel, +{ + fn verbosity_filter(&self) -> Option { + let verbosity_flag_filter = self + .verbosity() + .log_level_filter(); + + if self.is_plain() || self.is_json() { + return Some(LevelFilter::Info); + } else if verbosity_flag_filter < clap_verbosity_flag::LevelFilter::Debug && self.is_debug() + { + return Some(LevelFilter::Debug); + } else { + return verbosity_flag_filter + .as_str() + .parse() + .ok(); + } + } + + fn is_uncolored(&self) -> bool { + self.is_plain() + || self.is_json() + || self.is_no_color() + || env::var(format!( + "{}_NO_COLOR", + String::from(*app::APP_NAME).to_uppercase() + )) + .map_or(false, |value| !value.is_empty()) + } + + fn is_colored(&self) -> bool { + !self.is_uncolored() + } +} + +pub trait GlobalArguments { + fn config_filepath(&self) -> &Option; + + fn is_json(&self) -> bool; + + fn is_plain(&self) -> bool; + + fn is_debug(&self) -> bool; + + fn is_no_color(&self) -> bool; + + fn is_test(&self) -> bool; + + fn verbosity_filter(&self) -> log::LevelFilter; + + fn console_output_mode(&self) -> ConsoleOutputMode { + if self.is_json() { + ConsoleOutputMode::Json + } else if self.is_plain() { + ConsoleOutputMode::Plain + } else if self.is_test() { + ConsoleOutputMode::Test + } else { + ConsoleOutputMode::Regular + } + } + + fn max_output_verbosity(&self) -> log::LevelFilter { + if self.is_plain() || self.is_json() { + log::LevelFilter::Info + } else if self.is_debug() && log::LevelFilter::Debug > self.verbosity_filter() { + log::LevelFilter::Debug + } else { + self.verbosity_filter() + } + } +} + +#[derive(Debug, Snafu)] +#[non_exhaustive] +pub enum Error { + #[non_exhaustive] + #[snafu(display(""))] + Dummy {}, +} + +// region: IMPORTS + +use core::fmt; +use std::{env, path::PathBuf}; + +use log::LevelFilter; +use owo_colors::OwoColorize; +use serde::{Deserialize, Serialize}; +use snafu::{ResultExt, Snafu}; +use tracing::Level; +use tracing_appender::non_blocking::WorkerGuard; + +use crate::app::{self, config, logging}; + +// endregion: IMPORTS + +// region: MODULES + +/// Common commandline interface template for global arguments, intended to be +/// shared between the GUI and CLI programs. +pub mod cli_template { + #[derive(Clone, Debug, Args)] + #[command(next_display_order = usize::MAX - 100)] + pub struct GlobalArgs + where + L: clap_verbosity_flag::LogLevel, + { + #[arg( + long = "config", + short = 'c', + help = "Path to the configuration file to use.", + global = true, + display_order = usize::MAX - 6 + )] + pub config_file: Option, + + #[arg( + long = "json", + help = "Output in the JSON format for machine readability and scripting purposes.", + global = true, + display_order = usize::MAX - 5 + )] + pub json_flag: bool, + + #[arg( + long = "plain", + help = "Output as plain text without extra information, for machine readability and scripting purposes.", + global = true, + display_order = usize::MAX - 4 + )] + pub plain_flag: bool, + + #[arg( + long = "debug", + help = "Output debug messages.", + global = true, + display_order = usize::MAX - 3 + )] + pub debug_flag: bool, + + #[arg( + long = "no-color", + help = "Disable output coloring.", + global = true, + display_order = usize::MAX - 2 + )] + pub no_color_flag: bool, + + #[arg( + long = "test", + help = "Avoid destructive modifications and show all output subject to the commandline filters. Useful for dry-runs and for developers.", + global = true, + display_order = usize::MAX - 1 + )] + pub test_flag: bool, + + #[command(flatten)] + pub verbosity: clap_verbosity_flag::Verbosity, + } + + impl GlobalArguments for GlobalArgs { + fn config_filepath(&self) -> &Option { + self.config_filepath + } + + fn is_json(&self) -> bool { + self.json_flag + } + + fn is_plain(&self) -> bool { + self.plain_flag + } + + fn is_debug(&self) -> bool { + self.debug_flag + } + + fn is_test(&self) -> bool { + self.test_flag + } + + fn is_no_color(&self) -> bool { + self.no_color_flag + } + + fn verbosity_filter(&self) -> log::LevelFilter { + self.verbosity + .log_level_filter() + .and_then(|log_level_filter| { + log_level_filter + .as_str() + .parse() + .ok() + }) + .unwrap_or(log::LevelFilter::Info) + } + } + + // region: IMPORTS + + use std::path::PathBuf; + + use clap::Args; + + use super::GlobalArguments; + + // endregion: IMPORTS +} + +// endregion: MODULES + +// region: RE-EXPORTS + +#[allow(unused_imports)] +pub use cli_template::*; // Flatten the module heirarchy for easier access + +// endregion: RE-EXPORTS diff --git a/paxy/src/data/config.rs b/paxy/src/data/config.rs index 6fa649a..db6b54e 100644 --- a/paxy/src/data/config.rs +++ b/paxy/src/data/config.rs @@ -4,12 +4,15 @@ use std::{ path::PathBuf, }; +use bson::{doc, Document}; use serde::{Deserialize, Serialize}; use url::Url; -#[derive(Serialize, Deserialize)] +use crate::{actions::ensure_file, home}; + +#[derive(Serialize, Deserialize, PartialEq, Debug)] pub struct Config { - pub repositories: Option>, + pub repositories: Document, pub system_install_location: PathBuf, pub user_install_location: PathBuf, pub default_install_type: InstallType, @@ -17,10 +20,7 @@ pub struct Config { impl Default for Config { fn default() -> Self { - let mut user: PathBuf = match home::home_dir() { - Some(path) => path, - None => panic!("Impossible to get your home dir!"), - }; + let mut user: PathBuf = home!(); user.push(".paxy"); user.push("pkgs"); let system = if cfg!(linux) { @@ -28,11 +28,23 @@ impl Default for Config { } else { PathBuf::from("") }; + let mut repo_file = home!(); + repo_file.push(".paxy"); + repo_file.push("repos.bson"); + + ensure_file(&repo_file, |mut f: File| { + let mut buf = Vec::new(); + doc! {"paxy-pkgs": "https://github.com/Pax-Hub/paxy-pkg-repository.git"} + .to_writer(&mut buf) + .unwrap(); + f.write_all(&buf) + .unwrap(); + }); + let repo_file = File::open(repo_file).unwrap(); + let doc = Document::from_reader(repo_file).unwrap(); let conf = Config { - repositories: Some(vec![ - Url::parse("https://github.com/Pax-Hub/Packages.git").unwrap() - ]), + repositories: doc, user_install_location: user.clone(), /* Not harmful since the value is dropped in the * very next line */ system_install_location: system, @@ -41,7 +53,7 @@ impl Default for Config { if !user.is_dir() { create_dir_all(user.clone()).expect("No permission"); // Not harmful since the value is dropped in the soon user.pop(); - user.push("config.ini"); + user.push("config.toml"); if user.is_file() { fs::remove_file(user.clone()).unwrap(); } @@ -59,7 +71,7 @@ impl Default for Config { } } -#[derive(Serialize, Deserialize, Default)] +#[derive(Serialize, Deserialize, Default, PartialEq, Debug)] pub enum InstallType { User, #[default] @@ -67,19 +79,41 @@ pub enum InstallType { } #[allow(dead_code)] -fn load_conf() -> Config { - let mut conf_path: PathBuf = match home::home_dir() { - Some(path) => path, - None => panic!("Impossible to get your home dir!"), - }; +pub fn load_conf() -> Config { + let mut conf_path: PathBuf = home!(); conf_path.push(".paxy"); - conf_path.push("config.ini"); - match toml::from_str::( - fs::read_to_string(&conf_path) - .unwrap() - .as_str(), - ) { - Ok(val) => val, - Err(_) => Config::default(), + conf_path.push("config.toml"); + match fs::read_to_string(&conf_path) { + Ok(val) => match toml::from_str(&val) { + Ok(toml) => toml, + Err(_) => panic!("invalid config file"), + }, + Err(_) => { + let val = Config::default(); + let toml = toml::to_string_pretty::(&val).unwrap(); + let mut file = File::create(conf_path).unwrap(); + file.write_all(toml.as_bytes()) + .unwrap(); + val + } + } +} + +#[cfg(test)] +mod tests { + use serial_test::serial; + + use super::*; + + #[test] + #[serial] + fn load_conf_test_noexist() { + let mut repo_file = home!(); + repo_file.push(".paxy"); + repo_file.push("repos.bson"); + if repo_file.is_file() { + fs::remove_file(repo_file).unwrap(); + } + assert_eq!(load_conf(), Config::default()) } } diff --git a/paxy/src/data/mod.rs b/paxy/src/data/mod.rs index 68a6538..2b894b4 100644 --- a/paxy/src/data/mod.rs +++ b/paxy/src/data/mod.rs @@ -35,4 +35,4 @@ use snafu::Snafu; // pub use some_module::*; // endregion: RE-EXPORTS -mod config; +pub mod config; diff --git a/paxy/src/lib.rs b/paxy/src/lib.rs index 0f1bfa9..0790bf0 100644 --- a/paxy/src/lib.rs +++ b/paxy/src/lib.rs @@ -2,6 +2,16 @@ pub fn type_of(_: &T) -> &str { any::type_name::() } +// region: IMPORTS + +use std::any; + +use snafu::Snafu; + +// endregion: IMPORTS + +// region: ERRORS + #[derive(Debug, Snafu)] #[non_exhaustive] pub enum Error { @@ -12,13 +22,6 @@ pub enum Error { source: app::Error, }, - #[non_exhaustive] - #[snafu(display("in the UI: {source}"), visibility(pub))] - Ui { - #[snafu(backtrace)] - source: ui::Error, - }, - #[non_exhaustive] #[snafu(display("in an action:{source}"), visibility(pub))] Actions { @@ -27,19 +30,12 @@ pub enum Error { }, } -// region: IMPORTS - -use std::any; - -use snafu::Snafu; - -// endregion: IMPORTS +// endregion: ERRORS -// region: MODULES +// region: EXTERNAL-SUBMODULES pub mod actions; pub mod app; pub mod data; -pub mod ui; -// endregion: MODULES +// endregion: EXTERNAL-SUBMODULES diff --git a/paxy/src/ui/mod.rs b/paxy/src/ui/mod.rs deleted file mode 100644 index d1d0756..0000000 --- a/paxy/src/ui/mod.rs +++ /dev/null @@ -1,323 +0,0 @@ -#[tracing::instrument(level = "trace")] -pub fn run_common() -> Result<(C, Vec), crate::Error> -where - C: clap::Parser + CliModifier + fmt::Debug, - ::L: LogLevel, -{ - // Obtain CLI arguments - let cli_input = C::parse(); - - // Obtain user configuration - let (config, config_filepaths) = config::init_config( - cli_input - .config_file() - .as_ref() - .map(|f| PathBuf::as_path(&f)), - ) - .context(app::ConfigSnafu {}) - .context(crate::AppSnafu)?; - - // Turn off colors if needed - let mut is_cli_uncolored = cli_input.is_uncolored(); - if !is_cli_uncolored { - if let Some(no_color) = config.no_color { - is_cli_uncolored = no_color; - } - } - if is_cli_uncolored { - anstream::ColorChoice::Never.write_global(); - owo_colors::set_override(false); - } - - // Begin logging with preferred log directory and preferred verbosity - let config_log_dirpath = config - .log_directory - .as_ref() - .map(PathBuf::from); - let config_verbosity_filter: Option = config - .log_level_filter - .and_then(|lf| { - lf.as_str() - .parse() - .ok() - }); - let verbosity_filter = cli_input - .verbosity_filter() - .or(config_verbosity_filter); - let (mut handle, log_filepath) = logging::init_log(config_log_dirpath, verbosity_filter) - .context(app::LoggingSnafu {}) - .context(crate::AppSnafu {})?; - - // Modify logging behavior if Plain or Json output is desired - if cli_input.is_json() { - handle - .switch_to_json() - .context(app::LoggingSnafu {}) - .context(crate::AppSnafu {})?; - } else if cli_input.is_plain() { - handle - .switch_to_plain() - .context(app::LoggingSnafu {}) - .context(crate::AppSnafu {})?; - } else if cli_input.is_test() { - handle - .switch_to_test() - .context(app::LoggingSnafu {}) - .context(crate::AppSnafu {})?; - } - - // Welcome message - tracing::debug!( - "{} - {}", - "Paxy".bold(), - "A package manager that gets out of your way".magenta() - ); - tracing::debug!( - "{} {} {}", - console::Emoji("✉️", ""), - "shivanandvp".italic(), - "".italic() - ); - tracing::debug!( - target:"TEST", "{}{}{}{}{}{}{}{}", - "███".black(), - "███".red(), - "███".green(), - "███".yellow(), - "███".blue(), - "███".purple(), - "███".cyan(), - "███".white() - ); - tracing::debug!( - target:"TEST", "{}{}{}{}{}{}{}{}", - "███".bright_black(), - "███".bright_red(), - "███".bright_green(), - "███".bright_yellow(), - "███".bright_blue(), - "███".bright_purple(), - "███".bright_cyan(), - "███".bright_white() - ); - - if cli_input.is_test() { - // Test messages - tracing::trace!(target:"TEST", "{} Testing trace!...", console::Emoji("🧪", "")); - tracing::debug!(target:"TEST", "{} Testing debug!...", console::Emoji("🧪", "")); - tracing::info!(target:"TEST", "{} Testing info!...", console::Emoji("🧪", "")); - tracing::warn!(target:"TEST", "{} Testing warn!...", console::Emoji("🧪", "")); - tracing::error!(target:"TEST", "{} Testing error!...", console::Emoji("🧪", "")); - - tracing::info!(target:"JSON", "{} Testing: {}", console::Emoji("🧪", ""), "{\"JSON\": \"Target\"}"); - tracing::info!(target:"PLAIN", "{} Testing: Plain Target", console::Emoji("🧪", "")); - } - - tracing::debug!( - "{} The {} is {}... {}", - console::Emoji("⚙️", ""), - "configuration".cyan(), - "loaded".green(), - console::Emoji("✅", ""), - ); - tracing::debug!( - "{} The {} has {}... {}", - console::Emoji("📝", ""), - "logging".cyan(), - "begun".green(), - console::Emoji("✅", ""), - ); - - tracing::debug!( - "{} {} {:?}", - console::Emoji("📂", ""), - "Config Filepath(s) (without file extensions):".magenta(), - config_filepaths, - ); - tracing::debug!( - "{} {} {:?}", - console::Emoji("📂", ""), - "Log Filepath:".magenta(), - log_filepath - ); - - tracing::trace!( - "{} {} {:#?}", - console::Emoji("⌨️", ""), - "CLI input arguments:" - .magenta() - .dimmed(), - cli_input.dimmed() - ); - - Ok((cli_input, handle.worker_guards)) -} - -impl CliModifier for T -where - T: GlobalArguments, - ::L: LogLevel, -{ -} - -pub trait CliModifier: GlobalArguments -where - ::L: LogLevel, -{ - fn verbosity_filter(&self) -> Option { - if self.is_plain() || self.is_json() { - return Some(LevelFilter::INFO); - } - - let verbosity_flag_filter = self - .verbosity() - .log_level_filter(); - - if verbosity_flag_filter < clap_verbosity_flag::LevelFilter::Debug && self.is_debug() { - return Some(LevelFilter::DEBUG); - } - - verbosity_flag_filter - .as_str() - .parse() - .ok() - } - - fn is_uncolored(&self) -> bool { - self.is_plain() - || self.is_json() - || self.is_no_color() - || env::var(format!( - "{}_NO_COLOR", - String::from(*app::APP_NAME).to_uppercase() - )) - .map_or(false, |value| !value.is_empty()) - } - - fn is_colored(&self) -> bool { - !self.is_uncolored() - } -} - -pub trait GlobalArguments { - type L; - - fn config_file(&self) -> &Option; - - fn is_json(&self) -> bool; - - fn is_plain(&self) -> bool; - - fn is_debug(&self) -> bool; - - fn is_no_color(&self) -> bool; - - fn is_test(&self) -> bool; - - fn verbosity(&self) -> &clap_verbosity_flag::Verbosity - where - Self::L: LogLevel; -} - -#[derive(Debug, Snafu)] -#[non_exhaustive] -pub enum Error { - #[non_exhaustive] - #[snafu(display(""))] - Dummy {}, -} - -// region: IMPORTS - -use core::fmt; -use std::{env, path::PathBuf}; - -use clap_verbosity_flag::LogLevel; -use owo_colors::OwoColorize; -use snafu::{ResultExt, Snafu}; -use tracing_appender::non_blocking::WorkerGuard; -use tracing_subscriber::filter::LevelFilter; - -use crate::app::{self, config, logging}; - -// endregion: IMPORTS - -// region: MODULES - -pub mod cli_template { - #[derive(Clone, Debug, Args)] - #[command(next_display_order = usize::MAX - 100)] - pub struct GlobalArgs - where - L: clap_verbosity_flag::LogLevel, - { - #[arg( - long = "config", - short = 'c', - help = "Path to the configuration file to use.", - global = true, - display_order = usize::MAX - 6 - )] - pub config_file: Option, - - #[arg( - long = "json", - help = "Output in the JSON format for machine readability and scripting purposes.", - global = true, - display_order = usize::MAX - 5 - )] - pub json_flag: bool, - - #[arg( - long = "plain", - help = "Output as plain text without extra information, for machine readability and scripting purposes.", - global = true, - display_order = usize::MAX - 4 - )] - pub plain_flag: bool, - - #[arg( - long = "debug", - help = "Output debug messages.", - global = true, - display_order = usize::MAX - 3 - )] - pub debug_flag: bool, - - #[arg( - long = "no-color", - help = "Disable output coloring.", - global = true, - display_order = usize::MAX - 2 - )] - pub no_color_flag: bool, - - #[arg( - long = "test", - help = "Avoid destructive modifications and show all output subject to the commandline filters. Useful for dry-runs and for developers.", - global = true, - display_order = usize::MAX - 1 - )] - pub test_flag: bool, - - #[command(flatten)] - pub verbose: clap_verbosity_flag::Verbosity, - } - - // region: IMPORTS - - use std::path::PathBuf; - - use clap::Args; - - // endregion: IMPORTS -} - -// endregion: MODULES - -// region: RE-EXPORTS - -#[allow(unused_imports)] -pub use cli_template::*; - -// endregion: RE-EXPORTS diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 175da5a..401f3b6 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -8,4 +8,6 @@ targets = [ "aarch64-unknown-linux-gnu", "aarch64-pc-windows-msvc", "aarch64-apple-darwin", + "wasm32-wasi", + "wasm32-unknown-unknown" ] \ No newline at end of file