use nitro_parse::conditions::ConditionKind;
use nitro_parse::instruction::{InstrKind, Instruction};
use nitro_parse::parse::{Block, Parsed};
use nitro_parse::routine::INSTALL_ROUTINE;
use nitro_parse::vars::{Value, VariableStore};
use nitro_parse::FailReason;
use anyhow::{anyhow, bail, Context};
use nitro_shared::addon::AddonKind;
use nitro_shared::pkg::{PackageAddonOptionalHashes, PackageID};
use serde::{Deserialize, Serialize};
use crate::{RecommendedPackage, RequiredPackage};
#[async_trait::async_trait]
pub trait ScriptEvaluator {
type Shared<'a>;
type VariableStore: VariableStore;
fn get_variable_store<'a>(
&self,
shared: &'a mut Self::Shared<'_>,
) -> &'a mut Self::VariableStore;
fn eval_condition(
&mut self,
shared: &mut Self::Shared<'_>,
condition: &ConditionKind,
) -> anyhow::Result<bool>;
fn add_dependency(
&mut self,
shared: &mut Self::Shared<'_>,
dep: Vec<RequiredPackage>,
) -> anyhow::Result<()>;
fn add_conflict(&mut self, shared: &mut Self::Shared<'_>, pkg: PackageID)
-> anyhow::Result<()>;
fn add_recommendation(
&mut self,
shared: &mut Self::Shared<'_>,
pkg: RecommendedPackage,
) -> anyhow::Result<()>;
fn add_bundled(&mut self, shared: &mut Self::Shared<'_>, pkg: PackageID) -> anyhow::Result<()>;
fn add_compat(
&mut self,
shared: &mut Self::Shared<'_>,
compat: (PackageID, PackageID),
) -> anyhow::Result<()>;
fn add_extension(
&mut self,
shared: &mut Self::Shared<'_>,
pkg: PackageID,
) -> anyhow::Result<()>;
fn add_notice(&mut self, shared: &mut Self::Shared<'_>, notice: String) -> anyhow::Result<()>;
fn add_command(
&mut self,
shared: &mut Self::Shared<'_>,
command: Vec<String>,
) -> anyhow::Result<()>;
fn add_addon(
&mut self,
shared: &mut Self::Shared<'_>,
addon: AddonInstructionData,
) -> anyhow::Result<()>;
async fn run_custom(
&mut self,
shared: &mut Self::Shared<'_>,
command: String,
args: Vec<String>,
) -> anyhow::Result<()>;
}
pub struct ScriptEvalConfig {
pub reason: EvalReason,
}
#[derive(Debug, Clone, Copy)]
pub enum EvalReason {
Install,
Resolve,
}
pub async fn eval_script_package<E: ScriptEvaluator>(
parsed: &Parsed,
e: &mut E,
shared: &mut E::Shared<'_>,
config: &ScriptEvalConfig,
) -> anyhow::Result<()> {
let routine_id = parsed
.routines
.get(INSTALL_ROUTINE)
.ok_or(anyhow!("Routine {} does not exist", INSTALL_ROUTINE))?;
let block = parsed
.blocks
.get(routine_id)
.ok_or(anyhow!("Routine {} does not exist", INSTALL_ROUTINE))?;
for instr in &block.contents {
let result = eval_instr(instr, parsed, e, shared, config).await?;
if result.finish {
break;
}
}
Ok(())
}
async fn eval_block<E: ScriptEvaluator>(
block: &Block,
parsed: &Parsed,
e: &mut E,
shared: &mut E::Shared<'_>,
config: &ScriptEvalConfig,
) -> anyhow::Result<EvalResult> {
let mut out = EvalResult::new();
for instr in &block.contents {
let result = eval_instr(instr, parsed, e, shared, config).await?;
if result.finish {
out.finish = true;
break;
}
}
Ok(out)
}
pub async fn eval_instr<E: ScriptEvaluator>(
instr: &Instruction,
parsed: &Parsed,
e: &mut E,
shared: &mut E::Shared<'_>,
config: &ScriptEvalConfig,
) -> anyhow::Result<EvalResult> {
let mut out = EvalResult::new();
let result = {
match config.reason {
EvalReason::Install | EvalReason::Resolve => match &instr.kind {
InstrKind::If {
condition,
if_block,
else_blocks,
} => {
if e.eval_condition(shared, &condition.kind)? {
let block = parsed.blocks.get(if_block).expect("If block missing");
out = Box::pin(eval_block(block, parsed, e, shared, config)).await?;
} else {
for else_block in else_blocks {
if let Some(condition) = &else_block.condition {
if !e.eval_condition(shared, &condition.kind)? {
continue;
}
}
let block = parsed
.blocks
.get(&else_block.block)
.expect("If else block missing");
out = Box::pin(eval_block(block, parsed, e, shared, config)).await?;
}
}
}
InstrKind::Call(routine) => {
let routine = routine.get();
let routine = parsed.routines.get(routine).ok_or(anyhow!(
"Call instruction routine '{routine}' does not exist"
))?;
let block = parsed.blocks.get(routine).expect("Block does not exist");
out = Box::pin(eval_block(block, parsed, e, shared, config)).await?;
}
InstrKind::Set(var, val) => {
let var = var.get();
let val = val.get(e.get_variable_store(shared))?;
e.get_variable_store(shared)
.try_set_var(var.to_owned(), val)
.with_context(|| "Failed to set variable".to_string())?;
}
InstrKind::Finish() => out.finish = true,
InstrKind::Fail(reason) => {
out.finish = true;
let reason = reason.as_ref().unwrap_or(&FailReason::None).clone();
bail!("Package script failed explicitly with reason: {}", reason,);
}
InstrKind::Require(deps) => {
if let EvalReason::Resolve = config.reason {
for dep in deps {
let mut dep_to_push = Vec::new();
for dep in dep {
dep_to_push.push(RequiredPackage {
value: dep.value.get(e.get_variable_store(shared))?.into(),
explicit: dep.explicit,
});
}
e.add_dependency(shared, dep_to_push)?;
}
}
}
InstrKind::Refuse(package) => {
if let EvalReason::Resolve = config.reason {
let package = package.get(e.get_variable_store(shared))?;
e.add_conflict(shared, package.into())?;
}
}
InstrKind::Recommend(invert, package) => {
if let EvalReason::Resolve = config.reason {
let recommendation = RecommendedPackage {
value: package.get(e.get_variable_store(shared))?.into(),
invert: *invert,
};
e.add_recommendation(shared, recommendation)?;
}
}
InstrKind::Bundle(package) => {
if let EvalReason::Resolve = config.reason {
let package = package.get(e.get_variable_store(shared))?;
e.add_bundled(shared, package.into())?;
}
}
InstrKind::Compat(package, compat) => {
if let EvalReason::Resolve = config.reason {
let package = package.get(e.get_variable_store(shared))?;
let compat = compat.get(e.get_variable_store(shared))?;
e.add_compat(shared, (package.into(), compat.into()))?;
}
}
InstrKind::Extend(package) => {
if let EvalReason::Resolve = config.reason {
let package = package.get(e.get_variable_store(shared))?;
e.add_extension(shared, package.into())?;
}
}
InstrKind::Notice(notice) => {
let notice = notice.get(e.get_variable_store(shared))?;
e.add_notice(shared, notice)?;
}
InstrKind::Cmd(command) => {
if let EvalReason::Install = config.reason {
let command = get_value_vec(command, e.get_variable_store(shared))?;
e.add_command(shared, command)?;
}
}
InstrKind::Addon {
id,
file_name,
kind,
url,
path,
version,
hashes,
} => {
if let EvalReason::Install = config.reason {
let id = id.get(e.get_variable_store(shared))?;
let kind = kind.as_ref().expect("Addon kind missing");
let hashes = PackageAddonOptionalHashes {
sha256: hashes.sha256.get_as_option(e.get_variable_store(shared))?,
sha512: hashes.sha512.get_as_option(e.get_variable_store(shared))?,
};
let data = AddonInstructionData {
id,
file_name: file_name.get_as_option(e.get_variable_store(shared))?,
kind: *kind,
url: url.get_as_option(e.get_variable_store(shared))?,
path: path.get_as_option(e.get_variable_store(shared))?,
version: version.get_as_option(e.get_variable_store(shared))?,
hashes,
};
e.add_addon(shared, data)?;
}
}
InstrKind::Custom(cmd, args) => {
e.run_custom(shared, cmd.get_clone(), args.clone()).await?;
}
_ => bail!("Instruction is not allowed in this routine context"),
},
}
Ok::<(), anyhow::Error>(())
};
result.with_context(|| format!("In {} instruction at {}", instr, instr.pos))?;
Ok(out)
}
pub struct EvalResult {
pub finish: bool,
}
impl EvalResult {
pub fn new() -> Self {
Self { finish: false }
}
}
impl Default for EvalResult {
fn default() -> Self {
Self::new()
}
}
fn get_value_vec(vec: &[Value], vars: &impl VariableStore) -> anyhow::Result<Vec<String>> {
let out = vec.iter().map(|x| x.get(vars));
let out = out.collect::<anyhow::Result<_>>()?;
Ok(out)
}
#[derive(Serialize, Deserialize, Clone)]
pub struct AddonInstructionData {
pub id: String,
pub file_name: Option<String>,
pub kind: AddonKind,
pub url: Option<String>,
pub path: Option<String>,
pub version: Option<String>,
pub hashes: PackageAddonOptionalHashes,
}