diff --git a/README.md b/README.md index 4a50d94..cb914f0 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,13 @@ A simple example of this is CI jobs using different providers. If you have a pro Another common use-case is describing the acceptance tests using `inc exec`. This allows open source teams to describe whats needs locally before a merge will happen. This ensures that everyone handles a PR the same way and gives the contributor an idea of what's expected to work. -Like the rest of inc, we describe the commands in `inc.toml` files. An example deceleration of the 'build' command like: +Like the rest of inc, we describe the commands in `inc.yaml` files. An example deceleration of the 'build' command like: ``` -[exec.build] -commands = "cargo build" -description = "Run a normal debug build" +exec: + build: + description = Run a normal debug build + commands: + - cargo build ``` Here we give `inc` the command to run as a string, it could also be a list when multiple commands should be executed. We can also specify a description for when `inc exec --list` is run, to you can tell people why you would want to execute this command. diff --git a/inc-commands/src/exec.rs b/inc-commands/src/exec.rs index 1a8b071..df70302 100644 --- a/inc-commands/src/exec.rs +++ b/inc-commands/src/exec.rs @@ -1,6 +1,7 @@ -use inc_lib::core::config::{ConfigContainer, ExecConfig}; +use inc_lib::core::config::{ConfigContainer, ExecConfig, CommandAndEnv}; use inc_lib::exec::executor::{execute_external_command, CliResult, CliError}; use std::path::PathBuf; +use std::collections::HashMap; use std::fmt::Write; use clap::{App, AppSettings, Arg, ArgMatches, SubCommand}; use inc_lib::core::command::AvaliableCommands; @@ -45,25 +46,40 @@ pub fn execute( } }; - for command_entry in config.clone().commands.into_iter() { - if config.clone().commands.len() > 1 { - info!("** Executing `{}`", command_entry); + let command_defined_in = exec_configs.command_defintions.get(command_to_exec); + + let commands: Vec = config.clone().commands.into_iter().map(|x| x.to_command_and_envs()).collect(); + let command_count = commands.len(); + + for command_entry in commands.into_iter() { + if command_count > 1 { + info!("** Executing `{}`", command_entry.command); } + let mut command_list: Vec = - command_entry.split(" ").map(|x| String::from(x)).collect(); + command_entry.command.split(" ").map(|x| String::from(x)).collect(); let command_exec = command_list.remove(0); - debug!("Executing {:?} {:?}", command_exec, command_list); - let result = execute_external_command(&PathBuf::from(command_exec.clone()), &command_list); + let mut extra_env: HashMap = HashMap::new(); + + for (key, value) in command_entry.command_env { + extra_env.insert(key, value); + } + if let Some(path) = command_defined_in { + extra_env.insert(s!("INC_PROJECT_DIR"), s!(path.parent().unwrap().to_str().unwrap())); + } + + debug!("Executing {:?} {:?} defined in {:?}", command_exec, command_list, command_defined_in); + let result = execute_external_command(&PathBuf::from(command_exec.clone()), &command_list, extra_env); match result { Ok(value) => { if value != 0 { - error!("Command: `{}` returned {}", command_entry, value); + error!("Command: `{}` returned {}", command_entry.command, value); return Ok(value); } } Err(_err) => { - error!("Error while executing `{:?}`!", command_entry); + error!("Error while executing `{:?}`!", command_entry.command); return Ok(17); } } @@ -80,12 +96,14 @@ fn generate_list_options(config: &ExecConfig) -> String { commands.sort(); for key in commands.iter() { - let value = command_map.get(*key).unwrap(); + let value = command_map.get(*key).unwrap().clone(); write!(&mut list, " - name: {}\n", key).unwrap(); write!(&mut list, " description: {}\n", value.description).unwrap(); write!(&mut list, " commands:\n").unwrap(); - for command in value.commands.iter() { - write!(&mut list, " - {}\n", command).unwrap(); + let command_list: Vec = value.commands.into_iter().map(|x| x.to_command_and_envs()).collect(); + for command in command_list { + write!(&mut list, " - command: {}\n", command.command).unwrap(); + write!(&mut list, " env: {:?}\n", command.command_env).unwrap(); } } return list; diff --git a/inc-lib/src/core/config.rs b/inc-lib/src/core/config.rs index 3d02682..c0a1fc6 100644 --- a/inc-lib/src/core/config.rs +++ b/inc-lib/src/core/config.rs @@ -2,7 +2,7 @@ use std::vec::Vec; use std::io::Error as IoError; use std::env::current_dir; use dirs::home_dir; -use std::path::PathBuf; +use std::path::{PathBuf}; use std::fs::File; use std::io::prelude::*; use std::collections::HashMap; @@ -13,8 +13,8 @@ use serde::de::{self, value, Deserialize, Deserializer, Visitor, SeqAccess}; #[derive(Debug, Clone)] pub struct ConfigContainer { - pub(crate) project_config: Vec, - pub(crate) home_config: HomeConfig, + pub(crate) project_config: Vec>, + pub(crate) home_config: ConfigWithPath, } #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] @@ -28,14 +28,30 @@ pub struct CheckoutConfigs { pub default_provider: Option } +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum Commands { + CommandAndEnv(CommandAndEnv), + CommandList(String), +} + +impl Commands { + pub fn to_command_and_envs(self) -> CommandAndEnv { + return match self { + Commands::CommandAndEnv(commands) => commands, + Commands::CommandList(string) => { CommandAndEnv{command: string, command_env: HashMap::new()} } + } + } +} + #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] pub struct ExecCommandConfig { - #[serde(deserialize_with = "string_or_vec")] - pub commands: Vec, #[serde(default = "default_ignore_failures")] pub ignore_failures: bool, #[serde(default = "default_description")] pub description: String, + #[serde(rename = "commands")] + pub commands: Vec, } #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] @@ -43,38 +59,36 @@ pub struct ProjectConfig { pub exec: HashMap, } -fn string_or_vec<'de, D>(deserializer: D) -> Result, D::Error> - where D: Deserializer<'de> -{ - struct StringOrVec; - - impl<'de> Visitor<'de> for StringOrVec { - type Value = Vec; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("string or list of strings") - } +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +pub struct CommandAndEnv { + pub command: String, + + #[serde(default)] + #[serde(rename = "env")] + pub command_env: HashMap +} - fn visit_str(self, s: &str) -> Result - where E: de::Error - { - Ok(vec![s.to_owned()]) - } +#[derive(Debug, Clone)] +pub struct ConfigWithPath { + pub config: T, + pub file: Option +} - fn visit_seq(self, seq: S) -> Result - where S: SeqAccess<'de> - { - Deserialize::deserialize(value::SeqAccessDeserializer::new(seq)) - } +impl ConfigWithPath { + pub fn new(config: T, file: Option) -> ConfigWithPath { + return ConfigWithPath { config: config, file: file }; } - deserializer.deserialize_any(StringOrVec) + pub fn no_file(config: T) -> ConfigWithPath { + return ConfigWithPath { config: config, file: None }; + } } // API class, internally #[derive(Debug)] pub struct ExecConfig { - pub commands: HashMap + pub commands: HashMap, + pub command_defintions: HashMap } fn default_description() -> String { @@ -87,17 +101,17 @@ fn default_ignore_failures() -> bool { impl ConfigContainer { pub fn new() -> Result { - let project_config: Vec = match collapse_the_configs::(search_up_for_config_files()) { + let project_config: Vec> = match collapse_the_configs::(search_up_for_config_files()) { Ok(value) => value, Err(s) => return Err(s) }; - let home_configs: Vec = match collapse_the_configs::(search_for_home_config()) { + let home_configs: Vec> = match collapse_the_configs::(search_for_home_config()) { Ok(value) => value, Err(s) => return Err(s) }; let home_configs = match home_configs.first() { Some(value) => value.clone(), - None => HomeConfig { checkout: CheckoutConfigs { default_provider: None } } + None => ConfigWithPath::no_file(HomeConfig { checkout: CheckoutConfigs { default_provider: None } } ) }; trace!("Project Configs Found: {:?}", project_config); @@ -110,36 +124,43 @@ impl ConfigContainer { pub fn get_exec_configs(&self) -> ExecConfig { let mut command_map: HashMap = HashMap::new(); - for config in self.project_config.clone().into_iter() { + let mut command_defintion_map: HashMap = HashMap::new(); + + for project_config in self.project_config.clone().into_iter() { - for (key, value) in config.exec.into_iter() { + for (key, value) in project_config.config.exec.into_iter() { if !command_map.contains_key(&key) { - command_map.insert(key, value); + command_map.insert(key.clone(), value); + + if let Some(file) = project_config.file.clone() { + command_defintion_map.insert(key, file); + } } } } return ExecConfig { commands: command_map, + command_defintions: command_defintion_map }; } pub fn get_home_configs(&self) -> HomeConfig { - return self.home_config.clone(); + return self.home_config.config.clone(); } } -fn collapse_the_configs(config_files: Vec) -> Result, String> +fn collapse_the_configs(config_files: Vec) -> Result>, String> where T: DeserializeOwned, { - let mut return_configs: Vec = Vec::new(); + let mut return_configs: Vec> = Vec::new(); for val in config_files { match read_file(&val) { Ok(config) => { match serde_yaml::from_str::(&config) { - Ok(value) => return_configs.push(value), + Ok(value) => return_configs.push(ConfigWithPath::new(value, Some(val))), Err(err) => return Err(format!("Error trying to parse {:?}: '{}'", val, err)) }; } diff --git a/inc-lib/src/core/config_test.rs b/inc-lib/src/core/config_test.rs index 252f9eb..ca38b42 100644 --- a/inc-lib/src/core/config_test.rs +++ b/inc-lib/src/core/config_test.rs @@ -3,22 +3,6 @@ pub mod test { use core::config::*; use serde_yaml; - #[test] - fn test_can_find_single_command() { - let foo_commands = -"exec: - foo: - commands: bar"; - let result = serde_yaml::from_str::(foo_commands).unwrap(); - assert!(result.exec.contains_key("foo"), "foo didn't exist"); - - let foo = result.exec.get("foo").unwrap(); - let foo_commands = foo.clone().commands; - assert_eq!(foo_commands.len(), 1); - assert_eq!(foo_commands.get(0).unwrap(), &String::from("bar")); - assert_eq!(foo.clone().ignore_failures, false); - } - #[test] fn test_can_find_list_of_command() { let foo_commands = @@ -33,8 +17,8 @@ pub mod test { let foo = result.exec.get("foo").unwrap(); let foo_commands = foo.clone().commands; assert_eq!(foo_commands.len(), 2); - assert_eq!(foo_commands.get(0).unwrap(), &String::from("bar")); - assert_eq!(foo_commands.get(1).unwrap(), &String::from("baz")); + assert_eq!(foo_commands.get(0).unwrap(), &Commands::CommandList(String::from("bar"))); + assert_eq!(foo_commands.get(1).unwrap(), &Commands::CommandList(String::from("baz"))); assert_eq!(foo.clone().ignore_failures, false); } @@ -69,8 +53,8 @@ pub mod test { let yaml3 = serde_yaml::from_str::(yaml3).unwrap(); let config_container = ConfigContainer { - project_config: vec![yaml1, yaml2, yaml3], - home_config: HomeConfig { checkout: CheckoutConfigs { default_provider: None } }, + project_config: vec![ConfigWithPath::no_file(yaml1),ConfigWithPath::no_file( yaml2), ConfigWithPath::no_file(yaml3)], + home_config: ConfigWithPath::no_file(HomeConfig { checkout: CheckoutConfigs { default_provider: None } }), }; let exec_configs = config_container.get_exec_configs(); @@ -82,18 +66,18 @@ pub mod test { let foo_command = exec_configs.commands.get("foo").unwrap().clone().commands; assert_eq!(foo_command.len(), 2); - assert_eq!(foo_command.get(0), Some(&String::from("bar1"))); - assert_eq!(foo_command.get(1), Some(&String::from("baz1"))); + assert_eq!(foo_command.get(0), Some(&Commands::CommandList(String::from("bar1")))); + assert_eq!(foo_command.get(1), Some(&Commands::CommandList(String::from("baz1")))); let bar_command = exec_configs.commands.get("bar").unwrap().clone().commands; assert_eq!(bar_command.len(), 2); - assert_eq!(bar_command.get(0), Some(&String::from("bar2"))); - assert_eq!(bar_command.get(1), Some(&String::from("baz2"))); + assert_eq!(bar_command.get(0), Some(&Commands::CommandList(String::from("bar2")))); + assert_eq!(bar_command.get(1), Some(&Commands::CommandList(String::from("baz2")))); let baz_command = exec_configs.commands.get("baz").unwrap().clone().commands; assert_eq!(baz_command.len(), 3); - assert_eq!(baz_command.get(0), Some(&String::from("bar3"))); - assert_eq!(baz_command.get(1), Some(&String::from("baz3"))); - assert_eq!(baz_command.get(2), Some(&String::from("flig3"))); + assert_eq!(baz_command.get(0), Some(&Commands::CommandList(String::from("bar3")))); + assert_eq!(baz_command.get(1), Some(&Commands::CommandList(String::from("baz3")))); + assert_eq!(baz_command.get(2), Some(&Commands::CommandList(String::from("flig3")))); } } diff --git a/inc-lib/src/exec/executor.rs b/inc-lib/src/exec/executor.rs index b870745..fe91c78 100644 --- a/inc-lib/src/exec/executor.rs +++ b/inc-lib/src/exec/executor.rs @@ -1,7 +1,8 @@ use std::env::{self, current_exe, var}; -use std::process::Command; +use std::process::{Command, Stdio, Child}; use std::collections::HashMap; use std::path::PathBuf; +use std::io::Error as IoError; pub struct CliError { pub code: i32, @@ -32,67 +33,39 @@ impl From for CliError { } } -pub fn execute_external_command(cmd: &PathBuf, args: &[String]) -> CliResult { +pub fn execute_external_command(cmd: &PathBuf, args: &[String], extra_env: HashMap) -> CliResult { let command_exe = format!("{:?}{}", cmd.to_str().unwrap(), env::consts::EXE_SUFFIX); - let mut command = build_command(command_exe, args); - let swawn = command.envs(build_env_updates()).spawn(); - - if let Err(value) = swawn { - return Err(CliError { - code: 10, - message: format!("Unable to execute command: {}", value), - }); - } - - let output = swawn.unwrap().wait(); - - return match output { - Ok(code) => Ok(code.code().unwrap_or_else(|| 0)), - Err(value) => Err(CliError { - code: 10, - message: format!("Unable to run {:?} it returned {}", args, value), - }), + return match run_command(command_exe, args, extra_env, false) { + (_, _, Ok(code)) => Ok(code), + (_, _, Err(err)) => Err(err) }; } pub fn execute_external_command_for_output( cmd: &PathBuf, args: &[String], - env: &HashMap<&str, &str>, + extra_env: HashMap, ) -> Result { let command_exe = format!("{}{}", cmd.to_str().unwrap(), env::consts::EXE_SUFFIX); - let mut command = build_command(command_exe, args); - command.envs(env.into_iter()); - - let output = command.output(); - - if let Err(value) = output { - return Err(CliError { - code: 12, - message: format!("Unable to execute command: {}", value), - }); - } - let output = output.unwrap(); - - if !output.status.success() { - for line in String::from_utf8_lossy(&output.stdout).to_string().lines() { + return match run_command(command_exe, args, extra_env, true) { + (stdout, _, Ok(_)) => { + Ok(stdout.trim().to_string()) + } , + (stdout, stderr, Err(err)) => { + for line in stdout.lines() { error!("OUT: {}", line); + } + for line in stderr.lines() { + error!("ERR: {}", line); + } + Err(err) } - for line in String::from_utf8_lossy(&output.stderr).to_string().lines() { - error!("ERR: {}", line); - } - return Err(CliError { - code: 12, - message: format!("Unable to run {:?} it returned {}", args, output.status), - }); - } - - return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); + }; } -fn build_command(cmd: String, args: &[String]) -> Box { +fn run_command<'a>(cmd: String, args: &[String], extra_env: HashMap, capture_output: bool) -> (String, String, Result) { let mut command_string = String::new(); command_string.push_str(cmd.as_str()); for arg in args.iter() { @@ -100,28 +73,66 @@ fn build_command(cmd: String, args: &[String]) -> Box { command_string.push_str(arg.as_str()); } - let mut command = build_cmd_for_platform(); - command.arg(command_string).envs(build_env_updates()); + let (stdout, stderr) = if capture_output { + (Stdio::piped(), Stdio::piped()) + } else { + (Stdio::inherit(), Stdio::inherit()) + }; + + let env_map = build_env_updates(extra_env); + let child = match build_cmd(command_string, env_map, stdout, stderr) { + Err(value) => return (s!(""), s!(""), Err(CliError { code: 10, message: format!("Unable to execute command: {}", value) })), + Ok(child) => child + }; + + let result = child.wait_with_output(); - return Box::from(command); + return match result { + Ok(output) => { + (String::from_utf8_lossy(&output.stdout).to_string(), + String::from_utf8_lossy(&output.stderr).to_string(), + Ok(output.status.code().unwrap_or_else(|| 0))) + } + Err(value) => (s!(""), s!(""), Err(CliError {code: 10,message: format!("Unable to run {:?} it returned {}", args, value)})), + }; } -fn build_cmd_for_platform() -> Command { - if cfg!(target_os = "windows") { - let mut cmd = Command::new("cmd"); - cmd.arg("/C"); - return cmd; - } else { - let mut cmd = Command::new("sh"); - cmd.arg("-c"); - return cmd; - } +#[cfg(windows)] +fn build_cmd<'a>(command: String, env: HashMap, stdout: Stdio, stderr: Stdio) -> Result { + return Command::new("cmd") + .arg("/C") + .stdout(stdout) + .stderr(stderr) + .arg(command) + .envs(&env) + .spawn(); } -fn build_env_updates() -> HashMap { +#[cfg(unix)] +fn build_cmd(command: String, env: HashMap, stdout: Stdio, stderr: Stdio) -> Result { + return Command::new("sh") + .arg("-c") + .stdout(stdout) + .stderr(stderr) + .arg(command) + .envs(&env) + .spawn(); +} + +fn build_env_updates(extra_env: HashMap) -> HashMap { let mut results: HashMap = HashMap::new(); results.insert(String::from("PATH"), build_path()); + for (key, value) in env::vars() { + results.insert(key, value); + } + + for (key, value) in extra_env { + results.insert(key, value); + } + + debug!("Using ENV: {:?}", results); + return results; } diff --git a/inc-lib/src/libs/scm/services.rs b/inc-lib/src/libs/scm/services.rs index a435390..c71c240 100644 --- a/inc-lib/src/libs/scm/services.rs +++ b/inc-lib/src/libs/scm/services.rs @@ -71,12 +71,12 @@ impl ScmService for ExternalScmService { let use_ssh_env = if use_ssh { "TRUE" } else { "FALSE" }; let mut env = HashMap::new(); - env.insert("INC_CHECKOUT_SSH", use_ssh_env); + env.insert(s!("INC_CHECKOUT_SSH"), s!(use_ssh_env)); let result = execute_external_command_for_output( &(self.binary.clone().path), &(vec![user_input]), - &env, + env ); return match result { diff --git a/inc.yaml b/inc.yaml index e5bc7b2..0c4c547 100644 --- a/inc.yaml +++ b/inc.yaml @@ -9,5 +9,6 @@ exec: - cargo build --target=x86_64-apple-darwin --release - cargo build --target=x86_64-pc-windows-gnu --release build: - commands: cargo build + commands: + - cargo build description: Run a normal debug build diff --git a/inc/src/bin/inc.rs b/inc/src/bin/inc.rs index c8ff254..4e104bf 100644 --- a/inc/src/bin/inc.rs +++ b/inc/src/bin/inc.rs @@ -13,6 +13,7 @@ use inc_lib::exec::executor::{execute_external_command, CliError}; use std::process; use clap::{App, AppSettings, Arg, ArgGroup}; use inc_lib::core::BASE_APPLICATION_NAME; +use std::collections::HashMap; use std::string::String; use inc_commands::checkout; @@ -95,7 +96,7 @@ fn main() { Some(v) => v.map(|x| s!(x)).collect(), None => Vec::new(), }; - execute_external_command(&cmd.binary().path, &values) + execute_external_command(&cmd.binary().path, &values, HashMap::new()) } }, e @ _ => { diff --git a/inc/tests/inc-exec.rs b/inc/tests/inc-exec.rs index 408e87d..44fcb83 100644 --- a/inc/tests/inc-exec.rs +++ b/inc/tests/inc-exec.rs @@ -49,11 +49,13 @@ ARGS: - name: build description: Build the project commands: - - echo \"Hello World\" + - command: echo \"Hello World\" + env: {} - name: run description: No Description Provided commands: - - echo \"Goodbye World!\"", + - command: echo \"Goodbye World!\" + env: {}", ) .unwrap(); }); @@ -108,8 +110,11 @@ ARGS: - name: build description: Build the project commands: - - echo \"Hello World\" - - echo \"Goodbye World!\"", + - command: echo \"Hello World\" + env: {} + - command: echo \"Goodbye World!\" + env: {} +", ) .unwrap(); @@ -151,9 +156,12 @@ Goodbye World! - name: build description: This should fail, due to the false. commands: - - echo \"Hello World\" - - false - - echo \"Goodbye World!\"", + - command: echo \"Hello World\" + env: {} + - command: false + env: {} + - command: echo \"Goodbye World!\" + env: {}", ) .unwrap(); diff --git a/inc/tests/resources/sample1.yaml b/inc/tests/resources/sample1.yaml index 110e0bd..f466bc3 100644 --- a/inc/tests/resources/sample1.yaml +++ b/inc/tests/resources/sample1.yaml @@ -4,4 +4,5 @@ exec: - echo "Hello World" description: "Build the project" run: - commands: echo "Goodbye World!" \ No newline at end of file + commands: + - echo "Goodbye World!" \ No newline at end of file