g/src/bootstrap.rs
JMARyA f702e0ce15
All checks were successful
ci/woodpecker/push/build Pipeline was successful
🚑️ fix scripts
2025-04-10 18:44:23 +02:00

312 lines
9.7 KiB
Rust

use std::collections::HashMap;
use either::Either;
use serde::Deserialize;
use subprocess::Exec;
use crate::git::switch_branch;
#[derive(Debug, Deserialize)]
pub struct BootstrapConfig {
pub ask: HashMap<String, AskConfig>,
pub actions: ActionsConfig,
}
#[derive(Debug, Deserialize)]
pub enum AskKind {
#[serde(rename = "text")]
Text,
#[serde(rename = "number")]
Number,
#[serde(rename = "selection")]
Selection,
#[serde(rename = "bool")]
Bool,
}
pub enum AskValue {
Text(String, String),
Number(String, i64),
Selection(String, String),
Bool(String, bool),
}
impl AskValue {
pub fn has_name(&self, name: &str) -> bool {
match self {
AskValue::Text(n, _) => n.as_str() == name,
AskValue::Number(n, _) => n.as_str() == name,
AskValue::Selection(n, _) => n.as_str() == name,
AskValue::Bool(n, _) => n.as_str() == name,
}
}
pub fn text(&self) -> String {
match self {
AskValue::Text(_, t) => t.clone(),
AskValue::Number(_, n) => n.to_string(),
AskValue::Selection(_, s) => s.clone(),
AskValue::Bool(_, b) => b.to_string(),
}
}
}
#[derive(Debug, Deserialize)]
pub struct AskConfig {
pub kind: AskKind,
pub prompt: String,
pub options: Option<Vec<String>>,
pub order: Option<u64>,
}
#[derive(Debug, Deserialize)]
pub struct ActionsConfig {
pub replace: Vec<ReplaceAction>,
pub branch: Vec<BranchAction>,
pub script: Vec<ScriptAction>,
}
#[derive(Debug, Deserialize)]
pub struct VarCompare {
pub var: String,
pub eq: String,
}
type WhenExpression = Either<Option<String>, Option<VarCompare>>;
#[derive(Debug, Deserialize)]
pub struct ReplaceAction {
pub when: Option<toml::Value>,
pub from: String,
pub to: Option<String>,
pub to_var: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct BranchAction {
pub when: Option<toml::Value>,
pub branch: String,
}
#[derive(Debug, Deserialize)]
pub struct ScriptAction {
pub when: Option<toml::Value>,
pub script: String,
pub expose: Option<Vec<String>>,
}
pub fn eval(when: &WhenExpression, vars: &[AskValue]) -> bool {
if when.is_left() {
let when = when.as_ref().left().unwrap().as_ref().unwrap();
let var = vars.into_iter().find(|x| x.has_name(&when)).unwrap();
if let AskValue::Bool(_, ret) = var {
return *ret;
}
} else {
let when = when.as_ref().right().unwrap().as_ref().unwrap();
let var = vars.into_iter().find(|x| x.has_name(&when.var)).unwrap();
match var {
AskValue::Text(_, t) => {
return *t == when.eq;
}
AskValue::Number(_, n) => {
return n.to_string() == when.eq;
}
AskValue::Selection(_, s) => {
return *s == when.eq;
}
AskValue::Bool(_, b) => {
return b.to_string() == when.eq;
}
}
}
panic!("Could not evaluate condition");
}
pub fn build_script_vars(expose: Option<Vec<String>>, vars: &[AskValue]) -> String {
if let Some(expose) = expose {
let mut exp = Vec::new();
for ex in expose {
let v = vars.iter().find(|x| x.has_name(&ex)).unwrap();
exp.push(v);
}
return exp
.iter()
.map(|x| match x {
AskValue::Text(name, text) => {
format!("export {name}={}", enquote::enquote('\'', text))
}
AskValue::Number(name, num) => format!("export {name}={num}"),
AskValue::Selection(name, select) => {
format!("export {name}={}", enquote::enquote('\'', select))
}
AskValue::Bool(name, b) => format!("export {name}='{}'", b.to_string()),
})
.collect::<Vec<_>>()
.join("\n");
}
String::new()
}
pub fn do_script(action: &ScriptAction, vars: &[AskValue], name: &str, verify: bool) {
let pre = build_script_vars(action.expose.clone(), &vars);
if verify {
let script =
std::fs::read_to_string(std::path::Path::new(name).join(&action.script)).unwrap();
println!("Running script:\n{pre}\n{script}");
if !inquire::prompt_confirmation("Run this script?").unwrap() {
return;
}
}
println!("Running script '{}'", action.script);
Exec::cmd("sh")
.arg("-c")
.arg(format!("{pre}\nbash {}", action.script))
.cwd(std::path::Path::new(name))
.popen()
.unwrap()
.wait()
.unwrap();
}
pub fn do_replace(action: &ReplaceAction, name: &str, vars: &[AskValue]) {
let to = if let Some(var) = &action.to_var {
vars.iter().find(|x| x.has_name(var)).unwrap().text()
} else {
action.to.clone().unwrap()
};
println!("Replacing '{}' -> '{}'", action.from, to);
Exec::cmd("fd")
.arg(".")
.arg("-tf")
.arg("-x")
.arg("sd")
.arg(&action.from)
.arg(&to)
.cwd(std::path::Path::new(name))
.popen()
.unwrap()
.wait()
.unwrap();
}
/// Bootstrap a git repository using a base template
pub fn bootstrap(base: &str, name: &str, verify: bool) {
Exec::cmd("git")
.arg("clone")
.arg(base)
.arg(name)
.popen()
.unwrap()
.wait()
.unwrap();
if std::fs::exists(format!("{name}/bootstrap.toml")).unwrap_or(false) {
let config: BootstrapConfig =
toml::from_str(&std::fs::read_to_string(&format!("{name}/bootstrap.toml")).unwrap())
.unwrap();
let mut vars = Vec::new();
let mut ask_vec: Vec<(&String, &AskConfig)> = config.ask.iter().collect();
ask_vec.sort_by(|a, b| a.1.order.unwrap_or(0).cmp(&b.1.order.unwrap_or(0)));
for (var_name, def) in ask_vec {
match def.kind {
AskKind::Text => {
let a = inquire::prompt_text(&def.prompt).unwrap();
vars.push(AskValue::Text(var_name.clone(), a));
}
AskKind::Number => {
let a = inquire::prompt_text(&def.prompt).unwrap();
let a: i64 = a.parse().unwrap();
vars.push(AskValue::Number(var_name.clone(), a));
}
AskKind::Selection => {
let a = inquire::Select::new(&def.prompt, def.options.clone().unwrap())
.prompt()
.unwrap();
vars.push(AskValue::Selection(var_name.clone(), a));
}
AskKind::Bool => {
let a = inquire::prompt_confirmation(&def.prompt).unwrap();
vars.push(AskValue::Bool(var_name.clone(), a));
}
}
}
for action in config.actions.branch {
if let Some(when) = &action.when {
let when = match when {
toml::Value::String(s) => WhenExpression::Left(Some(s.clone())),
toml::Value::Table(map) => WhenExpression::Right(Some(VarCompare {
var: map.get("var").unwrap().as_str().unwrap().to_string(),
eq: map.get("eq").unwrap().as_str().unwrap().to_string(),
})),
_ => panic!("Invalid value in when condition"),
};
if eval(&when, &vars) {
println!("Switching to '{}' branch", action.branch);
switch_branch(&action.branch, name);
}
} else {
println!("Switching to '{}' branch", action.branch);
switch_branch(&action.branch, name);
}
}
for action in config.actions.replace {
if let Some(when) = &action.when {
let when = match when {
toml::Value::String(s) => WhenExpression::Left(Some(s.clone())),
toml::Value::Table(map) => WhenExpression::Right(Some(VarCompare {
var: map.get("var").unwrap().as_str().unwrap().to_string(),
eq: map.get("eq").unwrap().as_str().unwrap().to_string(),
})),
_ => panic!("Invalid value in when condition"),
};
if eval(&when, &vars) {
do_replace(&action, name, &vars);
}
} else {
do_replace(&action, name, &vars);
}
}
for action in config.actions.script {
if let Some(when) = &action.when {
let when = match when {
toml::Value::String(s) => WhenExpression::Left(Some(s.clone())),
toml::Value::Table(map) => WhenExpression::Right(Some(VarCompare {
var: map.get("var").unwrap().as_str().unwrap().to_string(),
eq: map.get("eq").unwrap().as_str().unwrap().to_string(),
})),
_ => panic!("Invalid value in when condition"),
};
if eval(&when, &vars) {
do_script(&action, &vars, name, verify);
}
} else {
do_script(&action, &vars, name, verify);
}
}
}
let _ = std::fs::remove_file("bootstrap.toml");
std::fs::remove_dir_all(std::path::Path::new(name).join(".git")).unwrap();
Exec::cmd("git")
.arg("init")
.arg("--quiet")
.cwd(std::path::Path::new(name))
.popen()
.unwrap()
.wait()
.unwrap();
}