diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e1f4902 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,89 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "alma" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "serde" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cdd151213925e7f1ab45a9bbfb129316bd00799784b174b7cc7bcd16961c49e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fc80d722935453bcafdc2c9a73cd6fac4dc1938f0346035d84bf99fa9e33217" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7629f7c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "alma" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" \ No newline at end of file diff --git a/src/application/mod.rs b/src/application/mod.rs new file mode 100644 index 0000000..84cd8f0 --- /dev/null +++ b/src/application/mod.rs @@ -0,0 +1,19 @@ +use crate::command::Command; +use crate::commands; + +pub fn run(args: Vec) { + if args.len() < 2 { + println!("No command provided"); + return; + } + + let command: Option> = match args[1].as_str() { + "link" => Some(Box::new(commands::link::LinkCommand {})), + _ => None, + }; + + match command { + Some(command) => command.run(&args[2..]), + None => println!("Invalid command: {}", args[1]), + } +} diff --git a/src/command/mod.rs b/src/command/mod.rs new file mode 100644 index 0000000..a592168 --- /dev/null +++ b/src/command/mod.rs @@ -0,0 +1,3 @@ +pub trait Command { + fn run(&self, args: &[std::string::String]); +} diff --git a/src/commands/link/mod.rs b/src/commands/link/mod.rs new file mode 100644 index 0000000..be844ba --- /dev/null +++ b/src/commands/link/mod.rs @@ -0,0 +1,73 @@ +use crate::command::Command; +use crate::configuration::module::resolver::*; +use crate::contants::*; +use crate::utils::path::path::*; + +pub struct LinkCommand {} + +impl LinkCommand { + pub fn run(args: &[std::string::String]) -> Result<(), String> { + let (repository_name, module_name) = get_repository_and_module_name(args, false); + + if module_name.is_none() { + println!("No module specified!"); + return Ok(()); + } + + let (repo_source, repo_target) = + match get_repo_source_and_target_dir_with_default(repository_name) { + Ok((source, target)) => (source, target), + Err(e) => return Err(e.to_string()), + }; + + let repo_source_path = std::path::Path::new(&repo_source); + if !repo_source_path.exists() { + return Err(format!( + "Repository source directory does not exist: {}", + repo_source + )); + } + + let module_name_as_path = module_name + .unwrap() + .replace("/", &std::path::MAIN_SEPARATOR.to_string()); + let module_dir = repo_source_path + .join(module_name_as_path) + .display() + .to_string(); + + let module_config_dir = std::path::Path::new(&module_dir); + if !module_config_dir.exists() { + return Err(format!( + "Module source directory does not exist: {}", + module_dir + )); + } + let module_config_file_stub = module_config_dir + .join(MODULE_CONFIG_FILE_STUB) + .display() + .to_string(); + + let module_configuration = resolve_module_configuration(module_config_file_stub); + + if module_configuration.is_err() { + return Err(module_configuration.err().unwrap().to_string()); + } + + let module_configuration = module_configuration.unwrap(); + for l in module_configuration.links.unwrap().iter() { + print!("Linking {} to {}...", l.0, l.1); + } + + return Ok(()); + } +} + +impl Command for LinkCommand { + fn run(&self, args: &[std::string::String]) { + match Self::run(args) { + Ok(_) => {} + Err(e) => println!("Error while linking: {}", e), + } + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..d3b1092 --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1 @@ +pub mod link; \ No newline at end of file diff --git a/src/configuration/module/mod.rs b/src/configuration/module/mod.rs new file mode 100644 index 0000000..e317f2e --- /dev/null +++ b/src/configuration/module/mod.rs @@ -0,0 +1,62 @@ +pub mod resolver; + +use std::{collections::HashMap, fmt::{Display, Formatter}}; + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct ModuleConfiguration { + pub target: Option, + pub install: Option, + pub configure: Option, + pub links: Option>, +} + +impl ModuleConfiguration { + pub fn empty() -> ModuleConfiguration { + return ModuleConfiguration { + target: None, + install: None, + configure: None, + links: None, + }; + } + + fn clone_option(s: &Option) -> Option { + match s { + Some(s) => Some(s.clone()), + None => None, + } + } + + pub fn merge(self, to_merge: &ModuleConfiguration) -> ModuleConfiguration { + let mut merged_links = self.links.unwrap_or(HashMap::new()).clone(); + let links_to_merge = Self::clone_option::>(&to_merge.links) + .unwrap_or(HashMap::new()) + .clone(); + for (key, value) in links_to_merge { + merged_links.insert(key, value); + } + + let to_merge_target = Self::clone_option::(&to_merge.target); + let to_merge_install = Self::clone_option::(&to_merge.install); + let to_merge_configure = Self::clone_option::(&to_merge.configure); + + return ModuleConfiguration { + target: to_merge_target.or(self.target), + install: to_merge_install.or(self.install), + configure: to_merge_configure.or(self.configure), + links: Some(merged_links), + }; + } +} + +impl Display for ModuleConfiguration { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "target: {:?}, install: {:?}, configure: {:?}, links: {:?}", + self.target, self.install, self.configure, self.links + ) + } +} \ No newline at end of file diff --git a/src/configuration/module/resolver.rs b/src/configuration/module/resolver.rs new file mode 100644 index 0000000..79929a3 --- /dev/null +++ b/src/configuration/module/resolver.rs @@ -0,0 +1,24 @@ +use std::collections::HashMap; + +use crate::configuration::module::ModuleConfiguration; +use crate::configuration::reader; +use crate::utils::os_information::*; + +pub fn resolve_module_configuration( + config_file_stub: String, +) -> std::io::Result { + let configurations = + reader::read_config::>(config_file_stub)?; + let os_identifier = get_os_identifier(); + + let configurations: Vec<&ModuleConfiguration> = configurations + .iter() + .filter(|(k, _)| is_on_platform(&os_identifier, k)) + .map(|c| c.1) + .collect(); + + let configuration = configurations.iter() + .fold(ModuleConfiguration::empty(), |acc, c| acc.merge(c)); + + return Ok(configuration); +} diff --git a/src/configuration/reader.rs b/src/configuration/reader.rs new file mode 100644 index 0000000..ad4a975 --- /dev/null +++ b/src/configuration/reader.rs @@ -0,0 +1,7 @@ +pub fn read_config(config_file_stub: String) -> std::io::Result where T: serde::de::DeserializeOwned { + let config_file_path = format!("{}.json", config_file_stub); + let config_file = std::fs::File::open(config_file_path)?; + let config: T = serde_json::from_reader(config_file)?; + + return Ok(config); +} \ No newline at end of file diff --git a/src/configuration/repository/mod.rs b/src/configuration/repository/mod.rs new file mode 100644 index 0000000..a918837 --- /dev/null +++ b/src/configuration/repository/mod.rs @@ -0,0 +1,29 @@ +use std::path::Path; + +use serde::{Serialize, Deserialize}; + +use crate::utils::folder::get_config_home_path; + +use super::reader; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RepositoryConfigurationEntry { + pub name: Option, + pub repository_path: Option, + pub link_path: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct RepositoryConfigurationRoot { + pub repositories: Vec +} + +pub fn read_repository_configuration() -> std::io::Result { + let config_file_path = get_config_home_path(); + let repo_config_file_path_stub = Path::new(&config_file_path).join("repository").display().to_string(); + + let config = reader::read_config::(repo_config_file_path_stub)?; + + return Ok(config); +} \ No newline at end of file diff --git a/src/contants.rs b/src/contants.rs new file mode 100644 index 0000000..75b4897 --- /dev/null +++ b/src/contants.rs @@ -0,0 +1 @@ +pub const MODULE_CONFIG_FILE_STUB: &str = ".alma-config"; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c4cb3d8 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,22 @@ +use std::env; + +mod application; +mod command; +mod commands; +mod contants; +mod utils { + pub mod path; + pub mod folder; + pub mod os_information; +} + +pub mod configuration { + pub mod module; + pub mod reader; + pub mod repository; +} + +fn main() { + let args: Vec = env::args().collect(); + application::run(args); +} diff --git a/src/utils/folder.rs b/src/utils/folder.rs new file mode 100644 index 0000000..e5d60f6 --- /dev/null +++ b/src/utils/folder.rs @@ -0,0 +1,18 @@ +use std::path::Path; + +const APPLICATION_SUBFOLDER_NAME: &str = "alma"; + +pub fn get_config_home_path() -> String { + let config_home_path = std::env::var("XDG_CONFIG_HOME") + .map_or_else( + |_| { + let home_path = std::env::var("HOME").unwrap(); + Path::new(&home_path).join(".config") + }, + |c| Path::new(&c).to_path_buf(), + ) + .join(APPLICATION_SUBFOLDER_NAME) + .display() + .to_string(); + return config_home_path; +} diff --git a/src/utils/os_information.rs b/src/utils/os_information.rs new file mode 100644 index 0000000..27b1897 --- /dev/null +++ b/src/utils/os_information.rs @@ -0,0 +1,60 @@ +use std::{ + fs::File, + io::{BufRead, BufReader}, + path::Path, +}; + +pub const OS_IDENTIFIER_DEFAULT: &str = "default"; +pub const OS_IDENTIFIER_WINDOWS: &str = "windows"; +pub const OS_IDENTIFIER_MAC: &str = "macos"; +pub const OS_IDENTIFIER_LINUX: &str = "linux"; +pub const OS_IDENTIFIER_FREEBSD: &str = "freebsd"; + +pub fn is_on_platform(current_os_identifier: &String, taget_os_identifier: &String) -> bool { + return taget_os_identifier == OS_IDENTIFIER_DEFAULT + || current_os_identifier == taget_os_identifier; +} + +pub fn get_os_identifier() -> String { + let base_os_identifier = match std::env::consts::OS { + "windows" => OS_IDENTIFIER_WINDOWS.to_string(), + "macos" => OS_IDENTIFIER_MAC.to_string(), + "linux" => get_linux_identifier(), + "freebsd" => OS_IDENTIFIER_FREEBSD.to_string(), + _ => String::from("unknown"), + }; + + let processor_architecture = std::env::consts::ARCH; + + return base_os_identifier + "-" + processor_architecture; +} + +fn get_linux_identifier() -> String { + let os_release_path = Path::new("/etc/os-release"); + let distroname = if os_release_path.exists() { + get_os_release(os_release_path) + } else { + None + }; + + distroname.map_or(OS_IDENTIFIER_LINUX.to_string(), |s| { + OS_IDENTIFIER_LINUX.to_string() + "-" + &s + }) +} + +fn get_os_release(os_release_path: &Path) -> Option { + let os_release_file = File::open(os_release_path).unwrap(); + let reader = BufReader::new(os_release_file); + for line in reader.lines() { + if line.is_err() { + continue; + } + + let line = line.unwrap(); + if line.starts_with("ID=") { + let os_identifier = line.replace("ID=", ""); + return Some(os_identifier); + } + } + return None; +} diff --git a/src/utils/path.rs b/src/utils/path.rs new file mode 100644 index 0000000..8af4885 --- /dev/null +++ b/src/utils/path.rs @@ -0,0 +1,81 @@ +pub mod path { + use std::env::current_dir; + + use crate::configuration::repository::read_repository_configuration; + + pub fn get_repository_and_module_name( + args: &[std::string::String], + single_param_is_repo: bool, + ) -> (Option, Option) { + //TODO: handle parameters + + match args.len() { + 0 => (None, None), + 1 => { + return if single_param_is_repo { + (Some(args[0].clone()), None) + } else { + (None, Some(args[0].clone())) + }; + } + _ => (Some(args[0].clone()), Some(args[1].clone())), + } + } + + pub fn get_repo_source_and_target_dir_with_default( + repo_name: Option, + ) -> std::io::Result<(String, String)> { + if current_dir().is_err() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Could not get current directory!", + )); + } + let source_dir_fallback = current_dir()?.display().to_string(); + + let mut target_dir_fallback = current_dir()?; + target_dir_fallback.push(".."); + let target_dir_fallback = target_dir_fallback.display().to_string(); + + let (repo_source_dir, repo_target_dir) = + get_repo_source_and_target_dir(repo_name, source_dir_fallback, target_dir_fallback)?; + + return Ok((repo_source_dir, repo_target_dir)); + } + + pub fn get_repo_source_and_target_dir( + repo_name: Option, + fallback_source: String, + fallback_target: String, + ) -> std::io::Result<(String, String)> { + if repo_name.is_none() { + return Ok((fallback_source, fallback_target)); + } + + let repo_name = repo_name.unwrap(); + + let repo_configurtaion = read_repository_configuration()?; + let repo_config = repo_configurtaion + .repositories + .iter() + .find(|x| x.name.clone().map_or(false, |s| s == repo_name)); + + if repo_config.is_none() { + println!( + "No repository configuration found for repository: {}", + repo_name + ); + return Ok((fallback_source, fallback_target)); + } + + let repo_config = repo_config.unwrap(); + + let source_path = repo_config + .repository_path + .clone() + .unwrap_or(fallback_source); + let target_path = repo_config.link_path.clone().unwrap_or(fallback_target); + + return Ok((source_path, target_path)); + } +}