use super::{default_tracing, Executable}; use crate::common::resolve_dataflow; use dora_core::{ config::OperatorId, descriptor::{Descriptor, DescriptorExt, NodeExt, SINGLE_OPERATOR_DEFAULT_ID}, }; use dora_message::descriptor::EnvValue; use eyre::{eyre, Context}; use std::{collections::BTreeMap, path::Path, process::Command}; #[derive(Debug, clap::Args)] /// Run build commands provided in the given dataflow. pub struct Build { /// Path to the dataflow descriptor file #[clap(value_name = "PATH")] dataflow: String, // Use UV to build nodes. #[clap(long, action)] uv: bool, } impl Executable for Build { fn execute(self) -> eyre::Result<()> { default_tracing()?; build(self.dataflow, self.uv) } } pub fn build(dataflow: String, uv: bool) -> eyre::Result<()> { let dataflow = resolve_dataflow(dataflow).context("could not resolve dataflow")?; let descriptor = Descriptor::blocking_read(&dataflow)?; let dataflow_absolute = if dataflow.is_relative() { std::env::current_dir().unwrap().join(dataflow) } else { dataflow.to_owned() }; let working_dir = dataflow_absolute.parent().unwrap(); let default_op_id = OperatorId::from(SINGLE_OPERATOR_DEFAULT_ID.to_string()); for node in descriptor.nodes { match node.kind()? { dora_core::descriptor::NodeKind::Standard(_) => { run_build_command(node.build.as_deref(), working_dir, uv, node.env.clone()) .with_context(|| { format!("build command failed for standard node `{}`", node.id) })? } dora_core::descriptor::NodeKind::Runtime(runtime_node) => { for operator in &runtime_node.operators { run_build_command( operator.config.build.as_deref(), working_dir, uv, node.env.clone(), ) .with_context(|| { format!( "build command failed for operator `{}/{}`", node.id, operator.id ) })?; } } dora_core::descriptor::NodeKind::Custom(custom_node) => run_build_command( custom_node.build.as_deref(), working_dir, uv, node.env.clone(), ) .with_context(|| format!("build command failed for custom node `{}`", node.id))?, dora_core::descriptor::NodeKind::Operator(operator) => run_build_command( operator.config.build.as_deref(), working_dir, uv, node.env.clone(), ) .with_context(|| { format!( "build command failed for operator `{}/{}`", node.id, operator.id.as_ref().unwrap_or(&default_op_id) ) })?, } } Ok(()) } fn run_build_command( build: Option<&str>, working_dir: &Path, uv: bool, envs: Option>, ) -> eyre::Result<()> { if let Some(build) = build { let lines = build.lines().collect::>(); for build_line in lines { let mut split = build_line.split_whitespace(); let program = split .next() .ok_or_else(|| eyre!("build command is empty"))?; let mut cmd = if uv && (program == "pip" || program == "pip3") { let mut cmd = Command::new("uv"); cmd.arg("pip"); cmd } else { Command::new(program) }; cmd.args(split); // Inject Environment Variables if let Some(envs) = envs.clone() { for (key, value) in envs { let value = value.to_string(); cmd.env(key, value); } } cmd.current_dir(working_dir); let exit_status = cmd .status() .wrap_err_with(|| format!("failed to run `{}`", build))?; if !exit_status.success() { return Err(eyre!("build command `{build_line}` returned {exit_status}")); } } Ok(()) } else { Ok(()) } }