Skip to content

Commit

Permalink
Running hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
Keats committed Dec 6, 2024
1 parent d1c806c commit 0a9afc4
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 35 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ cleanup = [
# Hooks can also be run conditionally depending on a variable value.
# If a hook is meant to fail, make sure to exit with a non 0 error code.
# The files need to be executable, no restrictions otherwise. It can be python, bash, bat etc.
# Hooks are automatically ignored, no need to add them to the ignore array

# pre-gen hooks are run after all the questions have been answered. This can be used for example to do more complex
# validations
Expand Down Expand Up @@ -232,6 +233,7 @@ You can use these like any other filter, e.g. `{{variable_name | camel_case}}`.
- Templates with a `directory` field will now no longer include that directory name in the output
- `copy_without_render` elements are now templated and refer to the template relative path if specified
- Avoid path traversals in cleanup
- Add pre-gen and post-gen hooks

### 0.4.0 (2023-08-02)

Expand Down
3 changes: 3 additions & 0 deletions examples/hooks/do_stuff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env python3

print("This is a pre-gen hook where we don't have variables yet")
3 changes: 3 additions & 0 deletions examples/hooks/greet.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash

echo "Hello {{greeting_recipient}}!"
26 changes: 26 additions & 0 deletions examples/hooks/template.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name = "Super basic"
description = "A very simple template"
kickstart_version = 1

pre_gen_hooks = [
{ name = "can run python scripts", path = "do_stuff.py" },
]

post_gen_hooks = [
{ name = "greeting", path = "greet.sh" },
]

[[variables]]
name = "directory_name"
default = "Hello"
prompt = "Directory name?"

[[variables]]
name = "file_name"
default = "Howdy"
prompt = "File name?"

[[variables]]
name = "greeting_recipient"
default = "Vincent"
prompt = "Who am I greeting?"
1 change: 1 addition & 0 deletions examples/hooks/{{directory_name}}/{{file_name}}.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
print("Hello, {{greeting_recipient}}!")
30 changes: 25 additions & 5 deletions src/bin/kickstart.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::error::Error;
use std::path::PathBuf;
use std::process::Command as StdCommand;

use clap::{Parser, Subcommand};
use tera::Context;
Expand Down Expand Up @@ -35,6 +36,10 @@ pub struct Cli {
#[clap(long, default_value_t = false)]
pub no_input: bool,

/// Whether to run the hooks
#[clap(long, default_value_t = true)]
pub run_hooks: bool,

#[clap(subcommand)]
pub command: Option<Command>,
}
Expand Down Expand Up @@ -143,7 +148,9 @@ macro_rules! bail_if_err {
}

fn execute_hook(hook: &HookFile) -> Result<()> {
todo!("write me")
terminal::bold(&format!(" - {}\n", hook.name()));
StdCommand::new(hook.path()).status().expect("sh command failed to start");
Ok(())
}

fn main() {
Expand All @@ -164,24 +171,37 @@ fn main() {
}
}
None => {
let template = bail_if_err!(Template::from_input(
let mut template = bail_if_err!(Template::from_input(
&cli.template.unwrap(),
cli.directory.as_deref()
));

// 1. ask questions
let variables = bail_if_err!(ask_questions(&template.definition, cli.no_input));
template.set_variables(variables);

// 2. run pre-gen hooks
let pre_gen_hooks = bail_if_err!(template.get_pre_gen_hooks());
if !pre_gen_hooks.is_empty() {}
if cli.run_hooks && !pre_gen_hooks.is_empty() {
terminal::bold("Running pre-gen hooks...\n");
for hook in &pre_gen_hooks {
execute_hook(hook).expect("todo handle error")
}
println!();
}

// 3. generate
bail_if_err!(template.generate(&cli.output_dir, &variables));
bail_if_err!(template.generate(&cli.output_dir));

// 4. run post-gen hooks
let post_gen_hooks = bail_if_err!(template.get_post_gen_hooks());
if !post_gen_hooks.is_empty() {}
if cli.run_hooks && !post_gen_hooks.is_empty() {
terminal::bold("Running post-gen hooks...\n");
for hook in &post_gen_hooks {
execute_hook(hook).expect("todo handle error")
}
println!();
}

terminal::success("\nEverything done, ready to go!\n");
}
Expand Down
8 changes: 8 additions & 0 deletions src/definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ pub struct TemplateDefinition {
}

impl TemplateDefinition {
pub(crate) fn all_hooks_paths(&self) -> Vec<String> {
self.pre_gen_hooks
.iter()
.chain(self.post_gen_hooks.iter())
.map(|h| format!("{}", h.path.display()))
.collect()
}

/// Returns the default values for all the variables that have one while following conditions
/// TODO: probably remove that fn? see how to test things
pub fn default_values(&self) -> Result<HashMap<String, Value>> {
Expand Down
78 changes: 48 additions & 30 deletions src/generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use std::collections::HashMap;
use std::env;
use std::fs::{self, File};
use std::io::{Read, Write};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::str;
Expand All @@ -22,19 +24,23 @@ use crate::utils::{
/// to the templated version
#[derive(Debug)]
pub struct HookFile {
original_path: PathBuf,
hook: Hook,
file: NamedTempFile,
}

impl HookFile {
pub fn name(&self) -> &str {
&self.hook.name
}
/// The hook original path in the template folder
pub fn original_path(&self) -> &Path {
self.original_path.as_path()
&self.hook.path
}

/// The rendered hook file, this is what you want to execute.
pub fn path(&self) -> &Path {
self.file.path()
/// The rendered hook canonicalized file path, this is what you want to execute.
pub fn path(&self) -> PathBuf {
// We _just_ created this file so it should exist and be fine
self.file.path().canonicalize().expect("should be able to canonicalize")
}
}

Expand Down Expand Up @@ -123,13 +129,17 @@ impl Template {
}

// Then we will read the content of the file and run it through Tera
let content = read_file(&hook.path)?;
let content = read_file(&self.path.join(&hook.path))?;
let rendered = render_one_off_template(&content, &context, Some(hook.path.clone()))?;

// Then we save it in a temporary file
let mut file = NamedTempFile::new()?;
write!(file, "{}", rendered)?;
hooks_files.push(HookFile { file, original_path: hook.path.clone() });
// TODO: how to make it work for windows
if cfg!(unix) {
fs::set_permissions(file.path(), fs::Permissions::from_mode(0o755))?;
}
hooks_files.push(HookFile { file, hook: hook.clone() });
}

Ok(hooks_files)
Expand All @@ -150,16 +160,16 @@ impl Template {
}

/// Generate the template at the given output directory
pub fn generate(&self, output_dir: &Path, variables: &HashMap<String, Value>) -> Result<()> {
let output_dir = output_dir.canonicalize()?;
pub fn generate(&self, output_dir: &Path) -> Result<()> {
let mut context = Context::new();
for (key, val) in variables {
for (key, val) in &self.variables {
context.insert(key, val);
}

if !output_dir.exists() {
create_directory(&output_dir)?;
create_directory(output_dir)?;
}
let output_dir = output_dir.canonicalize()?;

// Create the glob patterns of files to copy without rendering first, only once
let mut patterns = Vec::with_capacity(self.definition.copy_without_render.len());
Expand Down Expand Up @@ -198,6 +208,8 @@ impl Template {
})
.filter_map(|e| e.ok());

let hooks_paths = self.definition.all_hooks_paths();

'outer: for entry in walker {
// Skip root folder and the template.toml
if entry.path() == self.path || entry.path() == self.path.join("template.toml") {
Expand All @@ -215,6 +227,11 @@ impl Template {
}
}

// We automatically ignore hooks file
if hooks_paths.contains(&path_str) {
continue 'outer;
}

let path_str = path_str.replace("$$", "|");
let tpl = render_one_off_template(&path_str, &context, None)?;
let real_path = output_dir.join(Path::new(&tpl));
Expand Down Expand Up @@ -248,7 +265,7 @@ impl Template {
}

for cleanup in &self.definition.cleanup {
if let Some(val) = variables.get(&cleanup.name) {
if let Some(val) = self.variables.get(&cleanup.name) {
if *val == cleanup.value {
for p in &cleanup.paths {
let actual_path = render_one_off_template(p, &context, None)?;
Expand Down Expand Up @@ -280,9 +297,9 @@ mod tests {
#[test]
fn can_generate_from_local_path() {
let dir = tempdir().unwrap();
let tpl = Template::from_input("examples/complex", None).unwrap();
let res =
tpl.generate(&dir.path().to_path_buf(), &tpl.definition.default_values().unwrap());
let mut tpl = Template::from_input("examples/complex", None).unwrap();
tpl.set_variables(tpl.definition.default_values().unwrap());
let res = tpl.generate(&dir.path().to_path_buf());

assert!(res.is_ok());
assert!(!dir.path().join("some-project").join("template.toml").exists());
Expand All @@ -292,19 +309,19 @@ mod tests {
#[test]
fn can_generate_from_local_path_with_directory() {
let dir = tempdir().unwrap();
let tpl = Template::from_input("examples/with-directory", None).unwrap();
let res =
tpl.generate(&dir.path().to_path_buf(), &tpl.definition.default_values().unwrap());
let mut tpl = Template::from_input("examples/with-directory", None).unwrap();
tpl.set_variables(tpl.definition.default_values().unwrap());
let res = tpl.generate(&dir.path().to_path_buf());
assert!(res.is_ok());
assert!(dir.path().join("template_root").join("Howdy.py").exists());
}

#[test]
fn can_generate_from_local_path_with_directory_param() {
let dir = tempdir().unwrap();
let tpl = Template::from_input("./", Some("examples/complex")).unwrap();
let res =
tpl.generate(&dir.path().to_path_buf(), &tpl.definition.default_values().unwrap());
let mut tpl = Template::from_input("./", Some("examples/complex")).unwrap();
tpl.set_variables(tpl.definition.default_values().unwrap());
let res = tpl.generate(&dir.path().to_path_buf());
assert!(res.is_ok());
assert!(!dir.path().join("some-project").join("template.toml").exists());
assert!(dir.path().join("some-project").join("logo.png").exists());
Expand All @@ -313,9 +330,10 @@ mod tests {
#[test]
fn can_generate_from_remote_repo() {
let dir = tempdir().unwrap();
let tpl = Template::from_input("https://github.com/Keats/rust-cli-template", None).unwrap();
let res =
tpl.generate(&dir.path().to_path_buf(), &tpl.definition.default_values().unwrap());
let mut tpl =
Template::from_input("https://github.com/Keats/rust-cli-template", None).unwrap();
tpl.set_variables(tpl.definition.default_values().unwrap());
let res = tpl.generate(&dir.path().to_path_buf());

assert!(res.is_ok());
assert!(!dir.path().join("My-CLI").join("template.toml").exists());
Expand All @@ -325,11 +343,11 @@ mod tests {
#[test]
fn can_generate_from_remote_repo_with_directory() {
let dir = tempdir().unwrap();
let tpl =
let mut tpl =
Template::from_input("https://github.com/Keats/kickstart", Some("examples/complex"))
.unwrap();
let res =
tpl.generate(&dir.path().to_path_buf(), &tpl.definition.default_values().unwrap());
tpl.set_variables(tpl.definition.default_values().unwrap());
let res = tpl.generate(&dir.path().to_path_buf());

assert!(res.is_ok());
assert!(!dir.path().join("some-project").join("template.toml").exists());
Expand All @@ -339,9 +357,9 @@ mod tests {
#[test]
fn can_generate_handling_slugify() {
let dir = tempdir().unwrap();
let tpl = Template::from_input("examples/slugify", None).unwrap();
let res =
tpl.generate(&dir.path().to_path_buf(), &tpl.definition.default_values().unwrap());
let mut tpl = Template::from_input("examples/slugify", None).unwrap();
tpl.set_variables(tpl.definition.default_values().unwrap());
let res = tpl.generate(&dir.path().to_path_buf());
assert!(res.is_ok());
assert!(!dir.path().join("template.toml").exists());
assert!(dir.path().join("hello.md").exists());
Expand Down
5 changes: 5 additions & 0 deletions src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub fn error(message: &str) {
Ok(_) => {
write!(t, "{}", message).unwrap();
t.reset().unwrap();
t.flush().unwrap();
}
Err(_) => writeln!(t, "{}", message).unwrap(),
};
Expand All @@ -23,6 +24,7 @@ pub fn success(message: &str) {
Ok(_) => {
write!(t, "{}", message).unwrap();
t.reset().unwrap();
t.flush().unwrap();
}
Err(_) => writeln!(t, "{}", message).unwrap(),
};
Expand All @@ -38,6 +40,7 @@ pub fn bold(message: &str) {
Ok(_) => {
write!(t, "{}", message).unwrap();
t.reset().unwrap();
t.flush().unwrap();
}
Err(_) => write!(t, "{}", message).unwrap(),
};
Expand Down Expand Up @@ -69,6 +72,7 @@ pub fn basic_question<T: fmt::Display>(prompt: &str, default: &T, validation: &O
write!(t, "[default: {}]: ", default).unwrap();
}
t.reset().unwrap();
t.flush().unwrap();
} else {
eprint!("{} [default: {}]: ", prompt, default);
}
Expand All @@ -94,6 +98,7 @@ pub fn bool_question(prompt: &str, default: bool) {
write!(t, "[y/N]: ").unwrap()
}
t.reset().unwrap();
t.flush().unwrap();
} else {
eprint!("{} {}: ", prompt, default_str);
}
Expand Down

0 comments on commit 0a9afc4

Please sign in to comment.