cli/plugin/
runner.rs

1//! # runner
2//!
3//! Runs task plugins.
4//!
5
6#[cfg(test)]
7#[path = "runner_test.rs"]
8mod runner_test;
9
10use crate::environment;
11use crate::plugin::sdk;
12use crate::plugin::types::Plugin;
13use crate::scriptengine::duck_script;
14use crate::types::{Config, DeprecationInfo, FlowInfo, FlowState, RunTaskOptions, Step};
15use duckscript::runner::run_script;
16use duckscript::types::command::Commands;
17use duckscript::types::error::ScriptError;
18use duckscript::types::runtime::Context;
19use indexmap::IndexMap;
20use serde_json::json;
21use std::cell::RefCell;
22use std::env;
23use std::rc::Rc;
24
25/// Resolve aliases to different plugins, checking for cycles
26fn get_plugin_name_recursive(
27    aliases: &IndexMap<String, String>,
28    name: &str,
29    seen: &mut Vec<String>,
30) -> String {
31    let name_string = name.to_string();
32    if seen.contains(&name_string) {
33        error!("Detected cycle while resolving plugin alias: {}", name);
34    }
35    seen.push(name_string);
36
37    match aliases.get(name) {
38        Some(target_name) => get_plugin_name_recursive(aliases, target_name, seen),
39        None => name.to_string(),
40    }
41}
42
43fn get_plugin(config: &Config, plugin_name: &str) -> Option<(String, Plugin)> {
44    match &config.plugins {
45        Some(plugins_config) => {
46            let normalized_plugin_name = match plugins_config.aliases {
47                Some(ref aliases) => {
48                    let mut seen = vec![];
49                    get_plugin_name_recursive(aliases, plugin_name, &mut seen)
50                }
51                None => plugin_name.to_string(),
52            };
53
54            match plugins_config.plugins.get(&normalized_plugin_name) {
55                Some(plugin) => Some((normalized_plugin_name, plugin.clone())),
56                None => None,
57            }
58        }
59        None => None,
60    }
61}
62
63fn run_plugin(
64    flow_info: &FlowInfo,
65    flow_state: Rc<RefCell<FlowState>>,
66    step: &Step,
67    plugin: Plugin,
68    impl_plugin_name: String,
69) {
70    debug!(
71        "Running Task: {} via plugin: {} script:\n{}",
72        &step.name, &impl_plugin_name, &plugin.script
73    );
74
75    let cli_arguments = match flow_info.cli_arguments.clone() {
76        Some(cli_arguments) => cli_arguments.clone(),
77        None => vec![],
78    };
79
80    let mut context = duck_script::create_common_context(&cli_arguments);
81
82    let mut script_text = "exit_on_error true\n".to_string();
83    setup_script_globals(
84        &mut context,
85        flow_info,
86        step,
87        &impl_plugin_name,
88        &mut script_text,
89    );
90    script_text.push_str(&plugin.script);
91
92    match load_sdk(flow_info, flow_state, step, &mut context.commands) {
93        Ok(_) => {
94            let directory = env::current_dir();
95
96            match run_script(&script_text, context, None) {
97                Ok(_) => (),
98                Err(error) => error!("Error while running plugin: {}", error),
99            };
100
101            // revert to originl working directory
102            if let Ok(directory_path) = directory {
103                let path = directory_path.to_string_lossy().into_owned();
104                environment::setup_cwd(Some(&path));
105            }
106        }
107        Err(error) => error!("Unable to load duckscript SDK: {}", error),
108    };
109}
110
111fn setup_script_globals(
112    context: &mut Context,
113    flow_info: &FlowInfo,
114    step: &Step,
115    impl_plugin_name: &str,
116    script: &mut String,
117) {
118    context
119        .variables
120        .insert("flow.task.name".to_string(), flow_info.task.to_string());
121    script.push_str("flow.cli.args = array\n");
122    if let Some(ref cli_arguments) = flow_info.cli_arguments {
123        for arg in cli_arguments {
124            script.push_str("array_push ${flow.cli.args} \"");
125            script.push_str(&arg.replace("$", "\\$"));
126            script.push_str("\"\n");
127        }
128    }
129    context
130        .variables
131        .insert("plugin.impl.name".to_string(), impl_plugin_name.to_string());
132
133    setup_script_globals_for_task(context, step, script);
134}
135
136fn setup_script_globals_for_task(context: &mut Context, step: &Step, script: &mut String) {
137    // all task data as json
138    let task = step.config.clone();
139    let json_string = json!(task);
140    context
141        .variables
142        .insert("task.as_json".to_string(), json_string.to_string());
143
144    // meta info
145    context.variables.insert(
146        "task.has_condition".to_string(),
147        (task.condition.is_some() || task.condition_script.is_some()).to_string(),
148    );
149    context.variables.insert(
150        "task.has_env".to_string(),
151        (task.env_files.is_some() || task.env.is_some()).to_string(),
152    );
153    context.variables.insert(
154        "task.has_install_instructions".to_string(),
155        (task.install_crate.is_some()
156            || task.install_crate_args.is_some()
157            || task.install_script.is_some())
158        .to_string(),
159    );
160    context.variables.insert(
161        "task.has_command".to_string(),
162        task.command.is_some().to_string(),
163    );
164    context.variables.insert(
165        "task.has_script".to_string(),
166        task.script.is_some().to_string(),
167    );
168    context.variables.insert(
169        "task.has_run_task".to_string(),
170        task.run_task.is_some().to_string(),
171    );
172    context.variables.insert(
173        "task.has_dependencies".to_string(),
174        task.dependencies.is_some().to_string(),
175    );
176    context.variables.insert(
177        "task.has_toolchain_specifier".to_string(),
178        task.toolchain.is_some().to_string(),
179    );
180
181    context
182        .variables
183        .insert("task.name".to_string(), step.name.clone());
184
185    context.variables.insert(
186        "task.description".to_string(),
187        task.description.unwrap_or("".to_string()),
188    );
189    context.variables.insert(
190        "task.category".to_string(),
191        task.category.unwrap_or("".to_string()),
192    );
193    context.variables.insert(
194        "task.disabled".to_string(),
195        task.disabled.unwrap_or(false).to_string(),
196    );
197    context.variables.insert(
198        "task.private".to_string(),
199        task.private.unwrap_or(false).to_string(),
200    );
201    let deprecated = match task.deprecated {
202        Some(value) => match value {
203            DeprecationInfo::Boolean(value) => value,
204            DeprecationInfo::Message(_) => true,
205        },
206        None => false,
207    };
208    context
209        .variables
210        .insert("task.deprecated".to_string(), deprecated.to_string());
211    context.variables.insert(
212        "task.workspace".to_string(),
213        task.workspace.unwrap_or(false).to_string(),
214    );
215    context.variables.insert(
216        "task.plugin.name".to_string(),
217        task.plugin.unwrap_or("".to_string()),
218    );
219    context
220        .variables
221        .insert("task.watch".to_string(), task.watch.is_some().to_string());
222    context.variables.insert(
223        "task.ignore_errors".to_string(),
224        task.ignore_errors.unwrap_or(false).to_string(),
225    );
226    context.variables.insert(
227        "task.cwd".to_string(),
228        task.cwd.unwrap_or("".to_string()).to_string(),
229    );
230    context.variables.insert(
231        "task.command".to_string(),
232        task.command.unwrap_or("".to_string()).to_string(),
233    );
234    script.push_str("task.args = array\n");
235    if let Some(args) = task.args {
236        for arg in &args {
237            script.push_str("array_push ${task.args} \"");
238            script.push_str(&arg.replace("$", "\\$"));
239            script.push_str("\"\n");
240        }
241    }
242    context.variables.insert(
243        "task.script_runner".to_string(),
244        task.script_runner.unwrap_or("".to_string()).to_string(),
245    );
246    script.push_str("task.script_runner_args = array\n");
247    if let Some(args) = task.script_runner_args {
248        for arg in &args {
249            script.push_str("array_push ${task.script_runner_args} \"");
250            script.push_str(arg);
251            script.push_str("\"\n");
252        }
253    }
254    context.variables.insert(
255        "task.script_extension".to_string(),
256        task.script_extension.unwrap_or("".to_string()).to_string(),
257    );
258}
259
260fn load_sdk(
261    flow_info: &FlowInfo,
262    flow_state: Rc<RefCell<FlowState>>,
263    step: &Step,
264    commands: &mut Commands,
265) -> Result<(), ScriptError> {
266    duck_script::load_sdk(commands, Some(flow_info), Some(flow_state.clone()))?;
267    sdk::load(flow_info, flow_state, step, commands)?;
268
269    Ok(())
270}
271
272pub(crate) fn run_task(
273    flow_info: &FlowInfo,
274    flow_state: Rc<RefCell<FlowState>>,
275    step: &Step,
276    options: &RunTaskOptions,
277) -> bool {
278    if !options.plugins_enabled {
279        false
280    } else {
281        let plugin_name_option = match flow_state.borrow().forced_plugin {
282            Some(ref value) => Some(value.clone()),
283            None => match step.config.plugin {
284                Some(ref value) => Some(value.clone()),
285                None => None,
286            },
287        };
288
289        match plugin_name_option {
290            Some(ref plugin_name) => match get_plugin(&flow_info.config, plugin_name) {
291                Some((normalized_plugin_name, plugin)) => {
292                    debug!(
293                        "Running Task: {} via plugin: {}",
294                        &step.name, &normalized_plugin_name
295                    );
296
297                    run_plugin(flow_info, flow_state, step, plugin, normalized_plugin_name);
298
299                    true
300                }
301                None => {
302                    error!(
303                        "Invalid task: {}, unknown plugin: {}",
304                        &step.name, plugin_name
305                    );
306                    false
307                }
308            },
309            None => false,
310        }
311    }
312}