Skip to content
This repository has been archived by the owner on Mar 22, 2020. It is now read-only.

Commit

Permalink
Support INC_PROJECT_DIR environment variable in commands
Browse files Browse the repository at this point in the history
Using INC_PROJECT_DIR allows you to reference a specific path. Making
the command independent of path.
  • Loading branch information
ethankhall committed Sep 27, 2018
1 parent f61d191 commit 76dcdbb
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 154 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
42 changes: 30 additions & 12 deletions inc-commands/src/exec.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<CommandAndEnv> = 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<String> =
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<String, String> = 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);
}
}
Expand All @@ -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<CommandAndEnv> = 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;
Expand Down
97 changes: 59 additions & 38 deletions inc-lib/src/core/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,8 +13,8 @@ use serde::de::{self, value, Deserialize, Deserializer, Visitor, SeqAccess};

#[derive(Debug, Clone)]
pub struct ConfigContainer {
pub(crate) project_config: Vec<ProjectConfig>,
pub(crate) home_config: HomeConfig,
pub(crate) project_config: Vec<ConfigWithPath<ProjectConfig>>,
pub(crate) home_config: ConfigWithPath<HomeConfig>,
}

#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
Expand All @@ -28,53 +28,67 @@ pub struct CheckoutConfigs {
pub default_provider: Option<String>
}

#[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<String>,
#[serde(default = "default_ignore_failures")]
pub ignore_failures: bool,
#[serde(default = "default_description")]
pub description: String,
#[serde(rename = "commands")]
pub commands: Vec<Commands>,
}

#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct ProjectConfig {
pub exec: HashMap<String, ExecCommandConfig>,
}

fn string_or_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where D: Deserializer<'de>
{
struct StringOrVec;

impl<'de> Visitor<'de> for StringOrVec {
type Value = Vec<String>;

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<String, String>
}

fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where E: de::Error
{
Ok(vec![s.to_owned()])
}
#[derive(Debug, Clone)]
pub struct ConfigWithPath<T> {
pub config: T,
pub file: Option<PathBuf>
}

fn visit_seq<S>(self, seq: S) -> Result<Self::Value, S::Error>
where S: SeqAccess<'de>
{
Deserialize::deserialize(value::SeqAccessDeserializer::new(seq))
}
impl <T> ConfigWithPath<T> {
pub fn new(config: T, file: Option<PathBuf>) -> ConfigWithPath<T> {
return ConfigWithPath { config: config, file: file };
}

deserializer.deserialize_any(StringOrVec)
pub fn no_file(config: T) -> ConfigWithPath<T> {
return ConfigWithPath { config: config, file: None };
}
}

// API class, internally
#[derive(Debug)]
pub struct ExecConfig {
pub commands: HashMap<String, ExecCommandConfig>
pub commands: HashMap<String, ExecCommandConfig>,
pub command_defintions: HashMap<String, PathBuf>
}

fn default_description() -> String {
Expand All @@ -87,17 +101,17 @@ fn default_ignore_failures() -> bool {

impl ConfigContainer {
pub fn new() -> Result<Self, String> {
let project_config: Vec<ProjectConfig> = match collapse_the_configs::<ProjectConfig>(search_up_for_config_files()) {
let project_config: Vec<ConfigWithPath<ProjectConfig>> = match collapse_the_configs::<ProjectConfig>(search_up_for_config_files()) {
Ok(value) => value,
Err(s) => return Err(s)
};
let home_configs: Vec<HomeConfig> = match collapse_the_configs::<HomeConfig>(search_for_home_config()) {
let home_configs: Vec<ConfigWithPath<HomeConfig>> = match collapse_the_configs::<HomeConfig>(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);
Expand All @@ -110,36 +124,43 @@ impl ConfigContainer {

pub fn get_exec_configs(&self) -> ExecConfig {
let mut command_map: HashMap<String, ExecCommandConfig> = HashMap::new();
for config in self.project_config.clone().into_iter() {
let mut command_defintion_map: HashMap<String, PathBuf> = 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<T>(config_files: Vec<PathBuf>) -> Result<Vec<T>, String>
fn collapse_the_configs<T>(config_files: Vec<PathBuf>) -> Result<Vec<ConfigWithPath<T>>, String>
where
T: DeserializeOwned,
{
let mut return_configs: Vec<T> = Vec::new();
let mut return_configs: Vec<ConfigWithPath<T>> = Vec::new();

for val in config_files {
match read_file(&val) {
Ok(config) => {
match serde_yaml::from_str::<T>(&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))
};
}
Expand Down
38 changes: 11 additions & 27 deletions inc-lib/src/core/config_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<ProjectConfig>(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 =
Expand All @@ -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);
}

Expand Down Expand Up @@ -69,8 +53,8 @@ pub mod test {
let yaml3 = serde_yaml::from_str::<ProjectConfig>(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();
Expand All @@ -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"))));
}
}
Loading

0 comments on commit 76dcdbb

Please sign in to comment.