feat: working app

This commit is contained in:
Strix 2025-03-04 22:08:08 +01:00
parent 344e1693bd
commit ea70650c45
19 changed files with 653 additions and 197 deletions

View file

@ -1,7 +1,7 @@
#![allow(unused)]
use std::collections::HashMap;
use std::{collections::HashMap, fs::read_to_string};
use serde::{Deserialize, Serialize};
@ -13,3 +13,9 @@ pub struct Config {
pub daemon: daemon::Daemon,
pub source: HashMap<String, crate::source::Source>
}
impl Config {
pub fn parse(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
Ok(toml::from_str::<Config>(&read_to_string(path)?)?)
}
}

View file

@ -0,0 +1,89 @@
use std::{
collections::HashMap, fs, process::{Command, Stdio}, time::Duration
};
use dbus::{
arg::Variant,
blocking::{BlockingSender, Connection},
Message,
};
use log::{debug, error, info, trace};
use resolve_path::PathResolveExt;
use serde::{Deserialize, Serialize};
use crate::{tags::Tag};
#[derive(Serialize, Deserialize, Debug)]
pub struct Actions {
/// command actions
#[serde(rename = "command")]
pub commands: Option<Vec<CommandAction>>,
/// link actions
#[serde(rename = "link")]
pub links: Option<Vec<LinkAction>>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CommandAction {
#[serde(default = "whoami::username")]
user: String,
command: String,
pub description: Option<String>,
pub require: Option<Vec<Tag>>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct LinkAction {
pub src: String,
pub dest: String,
}
impl CommandAction {
pub fn new<S: Into<String>>(command: S, user: S) -> Self {
Self {
command: command.into(),
user: user.into(),
description: None,
require: None
}
}
pub fn run(&self) -> Result<i32, Box<dyn std::error::Error>> {
trace!("running \"{}\" as {}...", &self.command, &self.user);
if self.user != whoami::username() {
Ok(Command::new("sudo")
.arg("-u")
.arg(&self.user)
.arg("--")
.arg("sh")
.arg("-c")
.arg(&self.command)
.status()?
.code()
.unwrap_or(1))
} else {
Ok(Command::new("sh")
.arg("-c")
.arg(&self.command)
.status()?
.code()
.unwrap_or(1))
}
}
}
impl LinkAction {
pub fn link(&self) -> Result<(), Box<dyn std::error::Error>> {
trace!("linking from {:?} to {:?}...", &self.src.resolve(), &self.dest.resolve());
if let Ok(existing) = fs::read_link(&self.dest.resolve()) {
if existing == self.src.resolve() {
debug!("link OK");
return Ok(());
} else {
return Err("Destination is linked to a different path".into());
}
}
std::os::unix::fs::symlink(&self.src.resolve(), &self.dest.resolve())?;
Ok(())
}
}

View file

@ -0,0 +1,28 @@
use std::fmt::Display;
use serde::{Deserialize, Serialize};
use super::{action::Actions, package::Package};
#[derive(Serialize, Deserialize, Debug)]
pub struct CrateManifest {
#[serde(rename = "crate")]
pub crate_info: CrateInfo,
pub packages: Option<Vec<Package>>,
pub actions: Option<Actions>,
pub metadata: Option<Metadata>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CrateInfo {
pub name: String,
pub description: String,
pub author: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Metadata {
pub homepage: Option<String>,
pub repository: Option<String>,
pub issues: Option<String>,
}

View file

@ -1,34 +1,62 @@
use std::collections::HashMap;
use log::{error, info, warn};
use manifest::CrateManifest;
use package::PackageManager;
use serde::{Deserialize, Serialize};
mod pm;
pub mod action;
pub mod manifest;
pub mod package;
#[derive(Debug, Serialize, Deserialize)]
pub struct Package {
name: String,
}
impl Package {
pub fn install(&self) {
todo!()
}
pub fn uninstall(&self) {
todo!()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CrateAction {
pub name: String,
pub command: String,
pub args: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Crate {
pub pkgs: HashMap<String, Package>,
pub actions: HashMap<String, CrateAction>,
pub super_actions: HashMap<String, CrateAction>,
pub manifest: CrateManifest,
}
impl Crate {
pub fn from_toml_str(string: &str) -> Result<Self, toml::de::Error> {
Ok(Crate {
manifest: toml::from_str::<manifest::CrateManifest>(string)?,
})
}
pub fn install_packages(&self) -> bool {
if let Some(packages) = &self.manifest.packages {
info!("Installing packages...");
let pkgs: Vec<String> = packages
.iter()
.map(|p| p.get_correct_package_name())
.collect();
info!(target: "item", "pkgs: {}", pkgs.join(", "));
if let Some(pm) = PackageManager::get_available() {
pm.install(pkgs).is_ok()
} else {
false
}
} else {
false
}
}
pub fn run_actions(&self) -> Result<(), Box<dyn std::error::Error>> {
if let Some(actions) = &self.manifest.actions {
if let Some(commands) = &actions.commands {
for command in commands {
info!(
"Running {}...",
&command.description.clone().unwrap_or("action".to_string())
);
command.run()?;
}
}
if let Some(links) = &actions.links {
for link in links {
info!("Link {} -> {}...", link.src, link.dest);
if let Err(e) = link.link() {
error!("could not link: {e}");
continue;
}
}
}
}
Ok(())
}
}

View file

@ -0,0 +1,78 @@
use std::{collections::HashMap, io};
use log::{error, info};
use serde::{Deserialize, Serialize};
use super::action::CommandAction;
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash)]
pub enum PackageManager {
#[serde(rename = "pacman")]
Pacman,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Package {
pub name: String,
pub distro_name_mapping: Option<HashMap<PackageManager, String>>,
}
impl PackageManager {
pub fn install(&self, packages: Vec<String>) -> Result<bool, Box<dyn std::error::Error>> {
match &self {
PackageManager::Pacman => {
CommandAction::new(
format!("pacman -S --noconfirm {}", packages.join(" ")),
"root".to_string(),
)
.run()?;
}
}
Ok(true)
}
pub fn get_available() -> Option<Self> {
Self::from_str(match whoami::distro().as_str() {
"Manjaro Linux" => "pacman",
_ => "unknown"
})
}
pub fn from_str(distro: &str) -> Option<Self> {
match distro {
"pacman" => Some(PackageManager::Pacman),
_ => None,
}
}
}
impl Package {
pub fn get_correct_package_name(&self) -> String {
if let Some(pm) = PackageManager::get_available() {
if let Some(mappings) = &self.distro_name_mapping {
if let Some(name) = mappings.get(&pm) {
name.to_string()
} else {
self.name.clone()
}
} else {
self.name.clone()
}
} else {
self.name.clone()
}
}
pub fn install(&self) -> Result<bool, Box<dyn std::error::Error>> {
if let Some(pm) = PackageManager::get_available() {
pm.install(vec![self.get_correct_package_name()])?;
} else {
error!("no package manager found...");
return Err(Box::new(io::Error::new(
io::ErrorKind::NotFound,
"package manager not found",
)));
}
Ok(false)
}
}

View file

@ -1,90 +0,0 @@
use lazy_static::lazy_static;
use regex::Regex;
use std::{process::Command, str::FromStr};
const pm_cfg: &str = include_str!("../../package_manager.list");
/// regex: `(?<pm>[a-z]+)>(?<action>(?:un)?install+): (?<command>.*)`
/// example: pacman>install: pacman -Sy %args
lazy_static! {
static ref PM_REGEX: Regex =
Regex::new(r"(?P<pm>[a-z]+)>(?P<action>(?:un)?install+): (?P<command>.*)").unwrap();
}
#[derive(Debug)]
struct PackageManager {
name: String,
command: String,
args: Vec<String>,
}
impl PackageManager {
fn new(name: &str, command: &str, args: Vec<String>) -> Self {
Self {
name: name.to_string(),
command: command.to_string(),
args,
}
}
pub fn install(&self, packages: Vec<String>) -> Result<(), Vec<String>> {
Command::new(&self.command)
.args(&self.args)
.args(packages)
.spawn()
.expect("err");
Ok(())
}
pub fn uninstall(&self, packages: Vec<String>) -> Result<(), Vec<String>> {
todo!();
}
}
impl FromStr for PackageManager {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let caps = PM_REGEX.captures(s).ok_or("invalid package manager")?;
let name = caps.name("pm").ok_or("invalid package manager")?.as_str();
let command = caps
.name("command")
.ok_or("invalid package manager")?
.as_str();
let args = caps
.name("args")
.ok_or("invalid package manager")?
.as_str()
.split_whitespace()
.map(|s| s.to_string())
.collect();
Ok(Self::new(name, command, args))
}
}
#[derive(Debug)]
struct Package {
name: String,
}
impl FromStr for Package {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let caps = PM_REGEX.captures(s).ok_or("invalid package")?;
let name = caps.name("name").ok_or("invalid package")?.as_str();
Ok(Self::new(name))
}
}
impl Package {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
}
}
}
pub fn package_managers() -> Vec<PackageManager> {
pm_cfg
.lines()
.map(|s| s.to_string())
.map(|s| s.parse::<PackageManager>())
.collect::<Result<Vec<PackageManager>, String>>()
.expect("invalid package manager")
}

View file

@ -19,16 +19,13 @@ mod crates;
mod logging;
mod prelude;
mod source;
mod tags;
fn main() -> Result<(), Box<dyn std::error::Error>> {
logging::setup_logger()?;
let git_sha1 = String::from_utf8(
command_args!("git", "rev-parse", "HEAD")
.stdout(Stdio::piped())
.execute_output()?
.stdout,
)?;
info!(target: "item", "user: {}", whoami::username());
info!(target: "item", "distro: {}", whoami::distro());
let action = Action::parse();
match action {
@ -38,8 +35,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
trace!("setting config dir as cwd... {config_path}");
set_current_dir(config_path)?;
}
let config =
toml::from_str::<Config>(&read_to_string(abspath("./syncr.toml").unwrap())?)?;
let config = Config::parse(&abspath("./syncr.toml").unwrap())?;
info!("syncing \"{}\"...", config.title.bold());
info!("updating sources...");
@ -63,10 +59,24 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
for source in available_sources {
// cd to source dir
source.go_to_dir()?;
for c in source.get_crates()? {
info!("{} pkgs", c.pkgs.len())
for (mut path, c) in source.get_crates()? {
path.pop();
set_current_dir(absolute(path)?)?;
info!("Syncing crate: {}...", c.manifest.crate_info.name);
c.install_packages();
if let Err(e) = c.run_actions() {
error!("action failed: {e}");
}
set_current_dir(&oldpwd)?; // i hate this but im lazy okay
source.go_to_dir()?;
}
}
set_current_dir(oldpwd)?;
info!("Completed sync.");
}
_ => {
println!("{action:#?}");

View file

@ -1,5 +1,9 @@
use std::path::{Path, PathBuf};
use std::env;
pub fn abspath(p: &str) -> Option<String> {
let exp_path = shellexpand::full(p).ok()?;
let can_path = std::fs::canonicalize(exp_path.as_ref()).ok()?;
can_path.into_os_string().into_string().ok()
}
}

View file

@ -1,8 +1,11 @@
use std::{
env::current_dir, fs::{self, create_dir_all, exists}, io::Write, path::{Path, PathBuf}
env::current_dir,
fs::{self, create_dir_all, exists},
io::Write,
path::{Path, PathBuf},
};
use git2::Repository;
use git2::{build::RepoBuilder, Repository};
use log::{debug, info, trace, warn};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256, Sha512};
@ -30,13 +33,13 @@ impl Git {
fn branch_hash(&self) -> String {
let mut hasher = Sha256::new();
hasher.update(&self.url);
hasher.update(&self.branch);
let hash = hasher.finalize();
hex::encode(hash)
}
pub fn repository_path_str(&self) -> String {
format!(".data/git/{}{}", self.url_hash(), self.branch_hash())
format!(".data/git/{}.{}", self.url_hash(), self.branch_hash())
}
pub fn repository_path(&self) -> Result<PathBuf, std::io::Error> {
@ -48,7 +51,9 @@ impl Git {
}
pub fn clone_repository(&self) -> Result<Repository, git2::Error> {
Repository::clone_recurse(&self.url, Path::new(&self.repository_path_str()))
RepoBuilder::new()
.branch(&self.branch)
.clone(&self.url, Path::new(&self.repository_path_str()))
}
pub fn repository(&self) -> Result<Repository, git2::Error> {
@ -61,7 +66,7 @@ impl Git {
}
}
fn up_to_date(&self) -> Result<bool, Box<dyn std::error::Error>>{
fn up_to_date(&self) -> Result<bool, Box<dyn std::error::Error>> {
debug!("checking repo up to date...");
let repo = self.repository()?;
let mut remote = repo.find_remote("origin")?;
@ -72,7 +77,6 @@ impl Git {
let fetch_head = repo.refname_to_id(&format!("refs/remotes/origin/{}", self.branch))?;
let local_head = repo.refname_to_id(&format!("refs/heads/{}", self.branch))?;
Ok(fetch_head == local_head)
}

View file

@ -1,4 +1,8 @@
use std::{env::{current_dir, set_current_dir}, fs::{create_dir_all, read_to_string}, path::PathBuf};
use std::{
env::{current_dir, set_current_dir},
fs::{create_dir_all, exists, read_to_string},
path::PathBuf,
};
use log::{debug, info, trace};
use serde::{Deserialize, Serialize};
@ -12,6 +16,7 @@ pub mod git;
pub struct Source {
interval: u64,
git: Option<git::Git>,
dir: Option<String>,
}
impl Default for Source {
@ -19,6 +24,7 @@ impl Default for Source {
Source {
interval: 60,
git: None,
dir: None,
}
}
}
@ -29,40 +35,44 @@ impl Source {
trace!("checking git...");
return git.ensure().is_ok();
}
if let Some(dir) = &self.dir {
return exists(dir).is_ok();
}
false
}
pub fn go_to_dir(&self) -> Result<(), Box<dyn std::error::Error>> {
if let Some(git) = &self.git {
if PathBuf::from(git.repository_path_str()) == current_dir()? {
return Ok(())
return Ok(());
}
let dir = git.ensure()?.repository_path()?;
trace!("setting git dir as cwd... ({}@{}, {})", git.url, git.branch, dir.display());
trace!(
"setting git dir as cwd... ({}@{}, {})",
git.url,
git.branch,
dir.display()
);
set_current_dir(dir)?;
}
if let Some(path) = &self.dir {
set_current_dir(path)?;
}
Ok(())
}
pub fn get_crates(&self) -> Result<Vec<Crate>, Box<dyn std::error::Error>> {
pub fn get_crates(&self) -> Result<Vec<(PathBuf, Crate)>, Box<dyn std::error::Error>> {
let mut crates = vec![];
if let Some(git) = &self.git {
trace!("getting crates from git...");
debug!("{}", current_dir()?.display());
// get crates (read dir, crates/*/crate.toml)
for crate_file in glob::glob("crates/*/crate.toml").expect("err") {
debug!("{crate_file:#?}");
match crate_file {
Ok(cd) =>{
debug!("{}", cd.display());
crates.push(toml::from_str(&read_to_string(cd)?)?)
},
_ => continue
// get crates (read dir, crates/*/crate.toml)
for crate_file in glob::glob("crates/*/crate.toml").expect("err") {
match crate_file {
Ok(cd) => {
debug!("found {}", cd.display());
crates.push((cd.clone(), Crate::from_toml_str(&read_to_string(cd)?)?))
}
_ => continue,
}
}
debug!("{:#?}", crates);
Ok(crates)
}
}

71
sync-runner/src/tags.rs Normal file
View file

@ -0,0 +1,71 @@
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug)]
pub struct Tag {
pub category: String,
pub value: String
}
impl<'de> Deserialize<'de> for Tag {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
// First, deserialize the string
let s: String = Deserialize::deserialize(deserializer)?;
// Split the string into category and value by ":"
let parts: Vec<&str> = s.splitn(2, ':').collect();
if parts.len() != 2 {
return Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Str(&s),
&"a string in the format 'category:value'",
));
}
// Return a Tag with the split parts
Ok(Tag {
category: parts[0].to_string(),
value: parts[1].to_string(),
})
}
}
impl Serialize for Tag {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&format!("{}:{}", self.category, self.value))
}
}
impl Into<String> for Tag {
fn into(self) -> String {
self.category + " : " + &self.value
}
}
impl TryFrom<String> for Tag {
type Error = std::io::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
let parts: Vec<&str> = value.split(':').collect();
if parts.len() != 2 {
Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "could not match string to tag"))
} else {
Ok(Self {
category: parts[0].into(),
value: parts[1].into()
})
}
}
}
impl Tag {
pub fn distro() -> Tag {
Tag {
category: "distro".into(),
value: whoami::distro()
}
}
}