feat: is able to call a script from a project
This commit is contained in:
parent
9b6996c261
commit
ef8deadfd8
@ -8,7 +8,7 @@ anyhow = { version = "1" }
|
|||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tracing = { version = "0.1", features = ["log"] }
|
tracing = { version = "0.1", features = ["log"] }
|
||||||
tracing-subscriber = { version = "0.3.18" }
|
tracing-subscriber = { version = "0.3.18" }
|
||||||
clap = { version = "4", features = ["derive", "env"] }
|
clap = { version = "4", features = ["derive", "env", "cargo", "string"] }
|
||||||
dotenvy = { version = "0.15" }
|
dotenvy = { version = "0.15" }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
uuid = { version = "1.7", features = ["v4"] }
|
uuid = { version = "1.7", features = ["v4"] }
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::{net::SocketAddr, path::PathBuf};
|
use std::{net::SocketAddr, path::PathBuf};
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{FromArgMatches, Parser, Subcommand, crate_authors, crate_description, crate_version};
|
||||||
use colored_json::ToColoredJson;
|
use colored_json::ToColoredJson;
|
||||||
use kdl::KdlDocument;
|
use kdl::KdlDocument;
|
||||||
use rusty_s3::{Bucket, Credentials, S3Action};
|
use rusty_s3::{Bucket, Credentials, S3Action};
|
||||||
@ -11,29 +11,14 @@ use crate::{
|
|||||||
state::SharedState,
|
state::SharedState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod run;
|
||||||
mod template;
|
mod template;
|
||||||
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[command(author, version, about, long_about = None, subcommand_required = true)]
|
|
||||||
struct Command {
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: Option<Commands>,
|
|
||||||
#[arg(
|
|
||||||
env = "FOREST_PROJECT_PATH",
|
|
||||||
long = "project-path",
|
|
||||||
default_value = "."
|
|
||||||
)]
|
|
||||||
project_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
Init {},
|
Init {},
|
||||||
|
|
||||||
Template(template::Template),
|
Template(template::Template),
|
||||||
|
|
||||||
Info {},
|
Info {},
|
||||||
|
|
||||||
Serve {
|
Serve {
|
||||||
#[arg(env = "FOREST_HOST", long, default_value = "127.0.0.1:3000")]
|
#[arg(env = "FOREST_HOST", long, default_value = "127.0.0.1:3000")]
|
||||||
host: SocketAddr,
|
host: SocketAddr,
|
||||||
@ -55,10 +40,36 @@ enum Commands {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute() -> anyhow::Result<()> {
|
fn get_root(include_run: bool) -> clap::Command {
|
||||||
let cli = Command::parse();
|
let mut root_cmd = clap::Command::new("forest")
|
||||||
|
.subcommand_required(true)
|
||||||
|
.author(crate_authors!())
|
||||||
|
.version(crate_version!())
|
||||||
|
.about(crate_description!())
|
||||||
|
.arg(
|
||||||
|
clap::Arg::new("project_path")
|
||||||
|
.long("project-path")
|
||||||
|
.env("FOREST_PROJECT_PATH")
|
||||||
|
.default_value("."),
|
||||||
|
);
|
||||||
|
|
||||||
let project_path = &cli.project_path.canonicalize()?;
|
if include_run {
|
||||||
|
root_cmd = root_cmd
|
||||||
|
.subcommand(clap::Command::new("run").allow_external_subcommands(true))
|
||||||
|
.ignore_errors(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Commands::augment_subcommands(root_cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute() -> anyhow::Result<()> {
|
||||||
|
let matches = get_root(true).get_matches();
|
||||||
|
let project_path = PathBuf::from(
|
||||||
|
&matches
|
||||||
|
.get_one::<String>("project_path")
|
||||||
|
.expect("project path always to be set"),
|
||||||
|
)
|
||||||
|
.canonicalize()?;
|
||||||
let project_file_path = project_path.join("forest.kdl");
|
let project_file_path = project_path.join("forest.kdl");
|
||||||
if !project_file_path.exists() {
|
if !project_file_path.exists() {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
@ -74,7 +85,7 @@ pub async fn execute() -> anyhow::Result<()> {
|
|||||||
tracing::trace!("found a project name: {}", project.name);
|
tracing::trace!("found a project name: {}", project.name);
|
||||||
|
|
||||||
let plan = if let Some(plan_file_path) = PlanReconciler::new()
|
let plan = if let Some(plan_file_path) = PlanReconciler::new()
|
||||||
.reconcile(&project, project_path)
|
.reconcile(&project, &project_path)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
let plan_file = tokio::fs::read_to_string(&plan_file_path).await?;
|
let plan_file = tokio::fs::read_to_string(&plan_file_path).await?;
|
||||||
@ -90,41 +101,54 @@ pub async fn execute() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let context = Context { project, plan };
|
let context = Context { project, plan };
|
||||||
|
|
||||||
match cli.command.unwrap() {
|
let matches = if matches.subcommand_matches("run").is_some() {
|
||||||
Commands::Init {} => {
|
tracing::debug!("run is called, building extra commands, rerunning the parser");
|
||||||
tracing::info!("initializing project");
|
let root = get_root(false);
|
||||||
tracing::trace!("found context: {:?}", context);
|
|
||||||
}
|
|
||||||
|
|
||||||
Commands::Info {} => {
|
let root = run::Run::augment_command(root, &context);
|
||||||
let output = serde_json::to_string_pretty(&context)?;
|
|
||||||
println!("{}", output.to_colored_json_auto().unwrap_or(output));
|
|
||||||
}
|
|
||||||
|
|
||||||
Commands::Template(template) => {
|
root.get_matches()
|
||||||
template.execute(project_path, &context).await?;
|
} else {
|
||||||
}
|
matches
|
||||||
|
};
|
||||||
|
|
||||||
Commands::Serve {
|
match matches.subcommand().unwrap() {
|
||||||
s3_endpoint,
|
("run", args) => {
|
||||||
s3_bucket,
|
run::Run::execute(args, &project_path, &context).await?;
|
||||||
s3_region,
|
}
|
||||||
s3_user,
|
_ => match Commands::from_arg_matches(&matches).unwrap() {
|
||||||
s3_password,
|
Commands::Init {} => {
|
||||||
..
|
tracing::info!("initializing project");
|
||||||
} => {
|
tracing::trace!("found context: {:?}", context);
|
||||||
tracing::info!("Starting server");
|
}
|
||||||
let creds = Credentials::new(s3_user, s3_password);
|
Commands::Info {} => {
|
||||||
let bucket = Bucket::new(
|
let output = serde_json::to_string_pretty(&context)?;
|
||||||
url::Url::parse(&s3_endpoint)?,
|
println!("{}", output.to_colored_json_auto().unwrap_or(output));
|
||||||
rusty_s3::UrlStyle::Path,
|
}
|
||||||
|
Commands::Template(template) => {
|
||||||
|
template.execute(&project_path, &context).await?;
|
||||||
|
}
|
||||||
|
Commands::Serve {
|
||||||
|
s3_endpoint,
|
||||||
s3_bucket,
|
s3_bucket,
|
||||||
s3_region,
|
s3_region,
|
||||||
)?;
|
s3_user,
|
||||||
let put_object = bucket.put_object(Some(&creds), "some-object");
|
s3_password,
|
||||||
let _url = put_object.sign(std::time::Duration::from_secs(30));
|
..
|
||||||
let _state = SharedState::new().await?;
|
} => {
|
||||||
}
|
tracing::info!("Starting server");
|
||||||
|
let creds = Credentials::new(s3_user, s3_password);
|
||||||
|
let bucket = Bucket::new(
|
||||||
|
url::Url::parse(&s3_endpoint)?,
|
||||||
|
rusty_s3::UrlStyle::Path,
|
||||||
|
s3_bucket,
|
||||||
|
s3_region,
|
||||||
|
)?;
|
||||||
|
let put_object = bucket.put_object(Some(&creds), "some-object");
|
||||||
|
let _url = put_object.sign(std::time::Duration::from_secs(30));
|
||||||
|
let _state = SharedState::new().await?;
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
45
crates/forest/src/cli/run.rs
Normal file
45
crates/forest/src/cli/run.rs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
use std::{collections::BTreeMap, path::Path};
|
||||||
|
|
||||||
|
use crate::{model::Context, script::ScriptExecutor};
|
||||||
|
|
||||||
|
// Run is a bit special in that because the arguments dynamically render, we need to do some special magic in
|
||||||
|
// clap to avoid having to do hacks to register clap subcommands midcommand. As such instead, we opt to simply
|
||||||
|
// create a new sub command that encapsulates all the run complexities
|
||||||
|
pub struct Run {}
|
||||||
|
impl Run {
|
||||||
|
pub fn augment_command(root: clap::Command, ctx: &Context) -> clap::Command {
|
||||||
|
let mut run_cmd = clap::Command::new("run")
|
||||||
|
.subcommand_required(true)
|
||||||
|
.about("runs any kind of script from either the project or plan");
|
||||||
|
|
||||||
|
if let Some(scripts) = &ctx.project.scripts {
|
||||||
|
for name in scripts.items.keys() {
|
||||||
|
let cmd = clap::Command::new(name.to_string());
|
||||||
|
run_cmd = run_cmd.subcommand(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root.subcommand(run_cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute(
|
||||||
|
args: &clap::ArgMatches,
|
||||||
|
project_path: &Path,
|
||||||
|
ctx: &Context,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let Some((name, args)) = args.subcommand() else {
|
||||||
|
anyhow::bail!("failed to find a matching run command")
|
||||||
|
};
|
||||||
|
|
||||||
|
let scripts_ctx = ctx.project.scripts.as_ref().expect("to find scripts");
|
||||||
|
let Some(script_ctx) = scripts_ctx.items.get(name) else {
|
||||||
|
anyhow::bail!("failed to find script: {}", name);
|
||||||
|
};
|
||||||
|
|
||||||
|
ScriptExecutor::new(project_path.into(), ctx.clone())
|
||||||
|
.run(script_ctx, name)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
pub mod plan_reconciler;
|
pub mod plan_reconciler;
|
||||||
|
pub mod script;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
@ -130,6 +130,7 @@ impl TryFrom<&KdlValue> for GlobalVariable {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Default)]
|
#[derive(Debug, Clone, Serialize, Default)]
|
||||||
pub struct Global {
|
pub struct Global {
|
||||||
|
#[serde(flatten)]
|
||||||
items: BTreeMap<String, GlobalVariable>,
|
items: BTreeMap<String, GlobalVariable>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,12 +234,23 @@ impl TryFrom<&KdlNode> for Templates {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct Action {}
|
#[serde(tag = "type")]
|
||||||
|
pub enum Script {
|
||||||
|
Shell {},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&KdlNode> for Script {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: &KdlNode) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self::Shell {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct Scripts {
|
pub struct Scripts {
|
||||||
pub path: PathBuf,
|
#[serde(flatten)]
|
||||||
pub actions: BTreeMap<String, Action>,
|
pub items: BTreeMap<String, Script>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&KdlNode> for Scripts {
|
impl TryFrom<&KdlNode> for Scripts {
|
||||||
@ -246,22 +258,21 @@ impl TryFrom<&KdlNode> for Scripts {
|
|||||||
|
|
||||||
fn try_from(value: &KdlNode) -> Result<Self, Self::Error> {
|
fn try_from(value: &KdlNode) -> Result<Self, Self::Error> {
|
||||||
let val = Self {
|
let val = Self {
|
||||||
path: value
|
items: {
|
||||||
.get("path")
|
let mut out = BTreeMap::default();
|
||||||
.and_then(|p| p.as_string())
|
if let Some(children) = value.children() {
|
||||||
.map(PathBuf::from)
|
for entry in children.nodes() {
|
||||||
.unwrap_or(PathBuf::from("scripts/")),
|
let name = entry.name().value();
|
||||||
actions: value
|
let val = entry.try_into()?;
|
||||||
.children()
|
|
||||||
.and_then(|c| c.get("actions"))
|
out.insert(name.to_string(), val);
|
||||||
.and_then(|a| a.children())
|
}
|
||||||
.map(|d| {
|
|
||||||
d.nodes()
|
out
|
||||||
.iter()
|
} else {
|
||||||
.map(|n| (n.name().value().to_string(), Action {}))
|
out
|
||||||
.collect::<BTreeMap<String, Action>>()
|
}
|
||||||
})
|
},
|
||||||
.unwrap_or_default(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(val)
|
Ok(val)
|
||||||
|
78
crates/forest/src/script.rs
Normal file
78
crates/forest/src/script.rs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use shell::ShellExecutor;
|
||||||
|
|
||||||
|
use crate::model::{Context, Script};
|
||||||
|
|
||||||
|
pub mod shell {
|
||||||
|
use std::process::Stdio;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
|
||||||
|
use super::ScriptExecutor;
|
||||||
|
|
||||||
|
pub struct ShellExecutor {
|
||||||
|
root: ScriptExecutor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShellExecutor {
|
||||||
|
pub async fn execute(&self, name: &str) -> anyhow::Result<()> {
|
||||||
|
let path = &self.root.project_path;
|
||||||
|
let script_path = path.join("scripts").join(format!("{name}.sh"));
|
||||||
|
|
||||||
|
if !script_path.exists() {
|
||||||
|
anyhow::bail!("script was not found at: {}", script_path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cmd = tokio::process::Command::new(&script_path);
|
||||||
|
let cmd = cmd.current_dir(path);
|
||||||
|
cmd.stdin(Stdio::inherit());
|
||||||
|
cmd.stdout(Stdio::inherit());
|
||||||
|
cmd.stderr(Stdio::inherit());
|
||||||
|
|
||||||
|
let mut proc = cmd.spawn().context(format!(
|
||||||
|
"failed to spawn process: {}",
|
||||||
|
script_path.display()
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let exit = proc.wait().await?;
|
||||||
|
|
||||||
|
if !exit.success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"command: {name} failed with status: {}",
|
||||||
|
exit.code().unwrap_or(-1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ScriptExecutor> for ShellExecutor {
|
||||||
|
fn from(value: &ScriptExecutor) -> Self {
|
||||||
|
Self {
|
||||||
|
root: value.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ScriptExecutor {
|
||||||
|
project_path: PathBuf,
|
||||||
|
ctx: Context,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScriptExecutor {
|
||||||
|
pub fn new(project_path: PathBuf, ctx: Context) -> Self {
|
||||||
|
Self { project_path, ctx }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&self, script_ctx: &Script, name: &str) -> anyhow::Result<()> {
|
||||||
|
match script_ctx {
|
||||||
|
Script::Shell {} => ShellExecutor::from(self).execute(name).await?,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -26,11 +26,8 @@ project {
|
|||||||
output "output/"
|
output "output/"
|
||||||
}
|
}
|
||||||
|
|
||||||
scripts type=shell {
|
scripts {
|
||||||
path "scripts/"
|
hello type=shell {}
|
||||||
actions {
|
|
||||||
build
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
5
examples/project/scripts/hello.sh
Executable file
5
examples/project/scripts/hello.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env zsh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "hello world"
|
Loading…
x
Reference in New Issue
Block a user