switch to lua and implement when check

This commit is contained in:
LordMZTE 2021-10-12 23:12:29 +02:00
parent 05052031e3
commit 0f37d94625
7 changed files with 277 additions and 55 deletions

View File

@ -7,9 +7,14 @@ edition = "2018"
[dependencies] [dependencies]
clap = { version = "3.0.0-beta.4", features = ["derive"] } clap = { version = "3.0.0-beta.4", features = ["derive"] }
home = "0.5.3" dirs = "4.0.0"
miette = { version = "3.2.0", features = ["fancy"] } miette = { version = "3.2.0", features = ["fancy"] }
mlua = { version = "0.6.6", features = ["luajit", "serialize"] }
owo-colors = "2.1.0" owo-colors = "2.1.0"
serde = { version = "1.0.130", features = ["derive"] } serde = { version = "1.0.130", features = ["derive"] }
sled = "0.34.7"
thiserror = "1.0.30" thiserror = "1.0.30"
toml = "0.5.8" walkdir = "2.3.2"
xxhash-rust = { version = "0.8.2", features = ["xxh3"] }
[features]

7
exampleconfig.lua Normal file
View File

@ -0,0 +1,7 @@
steps = {
{
unint_alt = {"paru", "-Syu", "--noconfirm"},
interactive = true,
command = {"paru", "-Syu"},
}
}

View File

@ -1,4 +0,0 @@
[[steps]]
unint_alt = ["paru", "-Syu", "--noconfirm"]
interactive = true
command = ["paru", "-Syu"]

View File

@ -1,26 +1,25 @@
use miette::Diagnostic; use miette::{Context, Diagnostic, IntoDiagnostic};
use std::process::Command; use mlua::{prelude::*, Lua};
use std::{path::PathBuf, process::Command};
use thiserror::Error; use thiserror::Error;
use serde::Deserialize; use serde::Deserialize;
#[derive(Debug, Deserialize)] pub struct Config<'lua> {
pub struct Config { pub steps: Vec<Step<'lua>>,
pub steps: Vec<Step>,
#[serde(default = "default_shell")]
pub shell: Vec<String>, pub shell: Vec<String>,
} }
fn default_shell() -> Vec<String> {
vec!["sh".to_string(), "-c".to_string()]
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Step { pub struct Step<'lua> {
pub command: CfgCommand, pub command: CfgCommand,
#[serde(default)] #[serde(default)]
pub interactive: bool, pub interactive: bool,
pub unint_alt: Option<CfgCommand>, pub unint_alt: Option<CfgCommand>,
pub workdir: Option<PathBuf>,
#[serde(skip)]
pub when: Option<LuaFunction<'lua>>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -65,3 +64,50 @@ impl CfgCommand {
} }
} }
} }
pub fn run_config<'lua>(lua: &'lua Lua, content: &[u8]) -> miette::Result<Config<'lua>> {
lua.load(content)
.set_name("config")
.into_diagnostic()?
.exec()
.into_diagnostic()?;
let lua_conf = lua.globals();
let mut conf = Config {
steps: vec![],
shell: lua_conf
.get::<_, Option<_>>("shell")
.into_diagnostic()
.wrap_err("config must declare `shell` variable!")?
.unwrap_or_else(|| vec!["sh".to_string(), "-c".to_string()]),
};
for pair in lua_conf
.get::<_, LuaTable>("steps")
.into_diagnostic()
.wrap_err("config must declare `steps` variable!")?
.pairs::<LuaValue, LuaTable>()
{
let (_, step) = pair.into_diagnostic()?;
let when = step
.get::<_, Option<LuaFunction>>("when")
.into_diagnostic()
.wrap_err("Failed to get `when` from step")?;
let mut step = lua
.from_value_with::<Step>(
LuaValue::Table(step),
mlua::serde::de::Options::new().deny_unsupported_types(false),
)
.into_diagnostic()
.wrap_err("Failed to deserialize step")?;
step.when = when;
conf.steps.push(step);
}
Ok(conf)
}

82
src/lib.rs Normal file
View File

@ -0,0 +1,82 @@
use crate::config::CfgCommand;
use config::Config;
use miette::{Context, IntoDiagnostic};
use mlua::prelude::*;
use owo_colors::OwoColorize;
use std::path::{Path, PathBuf};
pub mod config;
pub mod luautil;
pub fn exec_steps(conf: Config, uninteractive: bool) -> miette::Result<()> {
for step in conf.steps {
if step.interactive && uninteractive {
if let Some(cmd) = step.unint_alt {
exec_cmd(cmd, &conf.shell, step.when, step.workdir)?;
}
} else {
exec_cmd(step.command, &conf.shell, step.when, step.workdir)?;
}
}
Ok(())
}
pub fn exec_cmd(
cmd: CfgCommand,
shell: &[String],
when: Option<LuaFunction>,
workdir: Option<PathBuf>,
) -> miette::Result<()> {
let cmd_str = cmd.to_string();
let len = cmd_str.len();
if let Some(func) = when {
let val: bool = func
.call(())
.into_diagnostic()
.wrap_err("Failed to call when function")?;
if !val {
println!("Skipping {}", cmd_str.blue());
return Ok(());
}
}
let mut cmd = cmd.into_command(shell)?;
if let Some(workdir) = workdir {
cmd.current_dir(workdir);
}
println!(
"{:=<len$}\nRunning {}\n{:=<len$}",
"",
cmd_str.green(),
"",
len = 8 + len
);
cmd.spawn().into_diagnostic()?.wait().into_diagnostic()?;
Ok(())
}
pub enum PathStatus {
NonExistant,
Directory,
File,
}
pub trait PathExt {
fn status(&self) -> PathStatus;
}
impl PathExt for Path {
fn status(&self) -> PathStatus {
match self.metadata() {
Ok(m) if m.is_file() => PathStatus::File,
Ok(m) if m.is_dir() => PathStatus::Directory,
_ => PathStatus::NonExistant,
}
}
}

97
src/luautil.rs Normal file
View File

@ -0,0 +1,97 @@
use miette::{Context, IntoDiagnostic};
use mlua::prelude::*;
use sled::IVec;
use std::{collections::HashSet, fs::File, io, io::Read, path::Path, rc::Rc};
use walkdir::WalkDir;
use xxhash_rust::xxh3::Xxh3;
use crate::{PathExt, PathStatus};
pub fn get_obj(lua: &Lua, db: Rc<sled::Db>) -> miette::Result<LuaTable> {
let tree = Rc::new(
db.open_tree(b"hashes")
.into_diagnostic()
.wrap_err("Failed to open hashes subtree")?,
);
let table = lua
.create_table()
.into_diagnostic()
.wrap_err("Failed to create util table")?;
table
.set(
"has_changed",
lua.create_function(move |_lua, (path, ignored): (String, _)| {
l_has_changed(&path, Rc::clone(&tree), ignored)
})
.into_diagnostic()
.wrap_err("Failed to create has_changed lua function")?,
)
.into_diagnostic()
.wrap_err("Failed to set has_changed")?;
Ok(table)
}
fn l_has_changed(path_str: &str, db: Rc<sled::Tree>, ignored: HashSet<String>) -> LuaResult<bool> {
let path = Path::new(path_str);
let mut hasher = Xxh3::new();
let ignored = ignored.iter().map(|s| Path::new(s)).collect::<Vec<_>>();
match path.status() {
PathStatus::NonExistant => Ok(false),
PathStatus::File => {
let file = File::open(path)?;
append_read_hash(&mut hasher, file)?;
let hash = IVec::from(&hasher.digest().to_be_bytes());
let eq = db.get(path_str.as_bytes()).map_err(LuaError::external)? != Some(hash.clone());
db.insert(path_str.as_bytes(), hash)
.map_err(LuaError::external)?;
Ok(eq)
},
PathStatus::Directory => {
let walk = WalkDir::new(path);
for entry in walk {
let entry = entry.map_err(LuaError::external)?;
let cur_path = entry.path();
if cur_path.is_file() {
let relpath = cur_path.strip_prefix(path).map_err(LuaError::external)?;
if ignored.iter().any(|p| relpath.starts_with(p)) {
continue;
}
let file = File::open(cur_path)?;
append_read_hash(&mut hasher, file)?;
}
}
let hash = IVec::from(&hasher.digest().to_be_bytes());
let eq = db.get(path_str.as_bytes()).map_err(LuaError::external)? != Some(hash.clone());
db.insert(path_str.as_bytes(), hash)
.map_err(LuaError::external)?;
Ok(eq)
},
}
}
fn append_read_hash(hasher: &mut Xxh3, mut read: impl Read) -> io::Result<()> {
let mut buf = [0u8; 1024];
loop {
match read.read(&mut buf) {
Ok(0) => return Ok(()),
Ok(l) => hasher.update(&buf[0..l]),
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
}

View File

@ -1,12 +1,8 @@
use clap::Clap; use clap::Clap;
use config::CfgCommand;
use miette::{miette, Context, IntoDiagnostic}; use miette::{miette, Context, IntoDiagnostic};
use owo_colors::OwoColorize; use mlua::Lua;
use std::{fs::File, io::Read, path::PathBuf}; use std::{fs::File, io::Read, path::PathBuf, rc::Rc};
use upgr::luautil;
use crate::config::Config;
pub mod config;
#[derive(Clap)] #[derive(Clap)]
struct Opt { struct Opt {
@ -20,7 +16,7 @@ struct Opt {
#[clap( #[clap(
long, long,
short, short,
about = "The config file to use. Defaults to ~/.config/upgr/config.toml" about = "The config file to use. Defaults to ~/.config/upgr/config.lua"
)] )]
config: Option<PathBuf>, config: Option<PathBuf>,
} }
@ -31,9 +27,9 @@ fn main() -> miette::Result<()> {
let cfg_path = if let Some(path) = opt.config { let cfg_path = if let Some(path) = opt.config {
path path
} else { } else {
home::home_dir() dirs::config_dir()
.ok_or_else(|| miette!("Couldn't get home directory"))? .ok_or_else(|| miette!("Couldn't get home directory"))?
.join(".config/upgr/config.toml") .join("upgr/config.lua")
}; };
let mut file = File::open(cfg_path) let mut file = File::open(cfg_path)
@ -45,37 +41,30 @@ fn main() -> miette::Result<()> {
.into_diagnostic() .into_diagnostic()
.wrap_err("Failed to read config")?; .wrap_err("Failed to read config")?;
let config = toml::from_slice::<Config>(&config_bytes) let db = Rc::new(
sled::open(
dirs::cache_dir()
.ok_or_else(|| miette!("Couldn't get cache dir"))?
.join("upgr/db"),
)
.into_diagnostic() .into_diagnostic()
.wrap_err("Failed to parse config")?; .wrap_err("Couldn't open database")?,
for step in config.steps {
if step.interactive && opt.uninteractive {
if let Some(cmd) = step.unint_alt {
exec_cmd(cmd, &config.shell)?;
}
} else {
exec_cmd(step.command, &config.shell)?;
}
}
Ok(())
}
fn exec_cmd(cmd: CfgCommand, shell: &[String]) -> miette::Result<()> {
let cmd_str = cmd.to_string();
let mut cmd = cmd.into_command(shell)?;
let len = 8 + cmd_str.len();
println!(
"{:=<len$}\nRunning {}\n{:=<len$}",
"",
cmd_str.green(),
"",
len = len
); );
cmd.spawn().into_diagnostic()?.wait().into_diagnostic()?; let lua = unsafe { Lua::unsafe_new() };
lua.globals()
.set(
"upgr",
luautil::get_obj(&lua, Rc::clone(&db)).wrap_err("Couldn't create lua upgr object")?,
)
.into_diagnostic()
.wrap_err("Couldn't set upgr lua object")?;
let config = upgr::config::run_config(&lua, &config_bytes).wrap_err("Failed to run config")?;
upgr::exec_steps(config, opt.uninteractive).wrap_err("Failed to execute steps")?;
db.flush()
.into_diagnostic()
.wrap_err("Failed to flush DB")?;
Ok(()) Ok(())
} }