From 2af2a5bd2692388da7e0ec351d4f2f5b625e7e27 Mon Sep 17 00:00:00 2001 From: Wildan M Date: Fri, 24 Oct 2025 18:41:35 +0700 Subject: [PATCH] Add command stdout pipe --- src/bin/cook.rs | 5 +- src/bin/repo.rs | 148 +++++++++++++++++++++++++++++++++-------- src/cook/cook_build.rs | 12 ++-- src/cook/fetch.rs | 54 +++++++++------ src/cook/fs.rs | 33 +++++++-- src/recipe.rs | 12 ++-- 6 files changed, 197 insertions(+), 67 deletions(-) diff --git a/src/bin/cook.rs b/src/bin/cook.rs index d5299d702..82e057445 100644 --- a/src/bin/cook.rs +++ b/src/bin/cook.rs @@ -21,8 +21,8 @@ fn cook( is_offline: bool, ) -> Result<(), String> { let source_dir = match is_offline { - true => fetch_offline(recipe_dir, recipe), - false => fetch(recipe_dir, recipe), + true => fetch_offline(recipe_dir, recipe, &None), + false => fetch(recipe_dir, recipe, &None), } .map_err(|err| format!("failed to fetch: {}", err))?; @@ -40,6 +40,7 @@ fn cook( recipe, is_offline, !is_deps, + &None, ) .map_err(|err| format!("failed to build: {}", err))?; diff --git a/src/bin/repo.rs b/src/bin/repo.rs index 1faa76d87..bc73fed4a 100644 --- a/src/bin/repo.rs +++ b/src/bin/repo.rs @@ -1,4 +1,5 @@ -use std::io::stdout; +use std::collections::HashMap; +use std::io::{BufRead, BufReader, PipeReader, stdout}; use std::path::PathBuf; use std::str::FromStr; use std::sync::mpsc; @@ -11,7 +12,7 @@ use cookbook::WALK_DEPTH; use cookbook::config::{CookConfig, get_config, init_config}; use cookbook::cook::cook_build::build; use cookbook::cook::fetch::{fetch, fetch_offline}; -use cookbook::cook::fs::create_target_dir; +use cookbook::cook::fs::{Stdout, create_target_dir}; use cookbook::cook::package::package; use cookbook::recipe::CookRecipe; use pkg::PackageName; @@ -20,7 +21,7 @@ use ratatui::Terminal; use ratatui::layout::{Constraint, Direction, Layout}; use ratatui::prelude::TermionBackend; use ratatui::style::{Color, Style}; -use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph}; +use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap}; use termion::screen::{ToAlternateScreen, ToMainScreen}; // A repo manager, to replace repo.sh @@ -141,11 +142,11 @@ fn main_inner() -> anyhow::Result<()> { for recipe in &recipe_names { match command { CliCommand::Fetch => { - handle_fetch(recipe, &config)?; + handle_fetch(recipe, &config, &None)?; } CliCommand::Cook => { - let source_dir = handle_fetch(recipe, &config)?; - handle_cook(recipe, &config, source_dir, recipe.is_deps)? + let source_dir = handle_fetch(recipe, &config, &None)?; + handle_cook(recipe, &config, source_dir, recipe.is_deps, &None)? } CliCommand::Unfetch => handle_clean(recipe, &config, true, true)?, CliCommand::Clean => handle_clean(recipe, &config, false, true)?, @@ -241,11 +242,15 @@ fn parse_args(args: Vec) -> anyhow::Result<(CliConfig, CliCommand, Vec anyhow::Result { +fn handle_fetch( + recipe: &CookRecipe, + config: &CliConfig, + logger: &Stdout, +) -> anyhow::Result { let recipe_dir = &recipe.dir; let source_dir = match config.cook.offline { - true => fetch_offline(recipe_dir, &recipe.recipe), - false => fetch(recipe_dir, &recipe.recipe), + true => fetch_offline(recipe_dir, &recipe.recipe, logger), + false => fetch(recipe_dir, &recipe.recipe, logger), } .map_err(|e| anyhow!(e))?; @@ -257,10 +262,10 @@ fn handle_cook( config: &CliConfig, source_dir: PathBuf, is_deps: bool, + logger: &Stdout, ) -> anyhow::Result<()> { let recipe_dir = &recipe.dir; let target_dir = create_target_dir(recipe_dir).map_err(|e| anyhow!(e))?; - let (stage_dir, auto_deps) = build( recipe_dir, &source_dir, @@ -269,6 +274,7 @@ fn handle_cook( &recipe.recipe, config.cook.offline, !is_deps, + logger, ) .map_err(|err| anyhow!("failed to build: {}", err))?; @@ -332,6 +338,7 @@ enum StatusUpdate { Fetched(PackageName), FailFetch(PackageName, String), StartCook(PackageName), + CookLog(PackageName, String), Cooked(PackageName), FailCook(PackageName, String), } @@ -342,6 +349,9 @@ struct TuiApp { cook_queue: Vec, done: Vec, failed: Vec, + active_fetch: Option, + active_cook: Option, + logs: HashMap>, } impl TuiApp { @@ -356,16 +366,36 @@ impl TuiApp { cook_queue: Vec::new(), done: Vec::new(), failed: Vec::new(), + active_fetch: None, + active_cook: None, + logs: HashMap::new(), } } // Update the state based on a message from a worker thread fn update_status(&mut self, update: StatusUpdate) { let (name, new_status) = match update { - StatusUpdate::StartFetch(name) => (name, RecipeStatus::Fetching), + StatusUpdate::StartFetch(name) => { + self.active_fetch = Some(name.clone()); + self.logs.insert(name.clone(), Vec::new()); // Clear old logs + (name.clone(), RecipeStatus::Fetching) + } StatusUpdate::Fetched(name) => (name, RecipeStatus::Fetched), StatusUpdate::FailFetch(name, err) => (name, RecipeStatus::Failed(err)), - StatusUpdate::StartCook(name) => (name, RecipeStatus::Cooking), + StatusUpdate::StartCook(name) => { + self.active_cook = Some(name.clone()); // Set active cook + self.logs.insert(name.clone(), Vec::new()); // Clear old logs + (name.clone(), RecipeStatus::Cooking) + } + StatusUpdate::CookLog(name, line) => { + self.logs.entry(name.clone()).or_default().push(line); + // No status change, just return the current state + if let Some((_, status)) = self.recipes.iter().find(|(r, _)| r.name == name) { + (name, status.clone()) + } else { + return; // Should not happen + } + } StatusUpdate::Cooked(name) => (name, RecipeStatus::Done), StatusUpdate::FailCook(name, err) => (name, RecipeStatus::Failed(err)), }; @@ -402,6 +432,26 @@ impl TuiApp { } } +fn spawn_log_reader( + mut pipe_reader: PipeReader, + package_name: PackageName, + status_tx: mpsc::Sender, +) { + thread::spawn(move || { + let reader = BufReader::new(&mut pipe_reader); + for line in reader.lines() { + let line_str = line.unwrap_or_else(|e| format!("[IO Error] {}", e)); + if status_tx + .send(StatusUpdate::CookLog(package_name.clone(), line_str)) + .is_err() + { + // TUI thread hung up + break; + } + } + }); +} + fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<()> { let (work_tx, work_rx) = mpsc::channel::<(CookRecipe, PathBuf)>(); let (status_tx, status_rx) = mpsc::channel::(); @@ -416,8 +466,9 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( cooker_status_tx .send(StatusUpdate::StartCook(name.clone())) .unwrap(); - - match handle_cook(&recipe, &cooker_config, source_dir, is_deps) { + let (mut stdout_writer, mut stderr_writer) = setup_logger(&cooker_status_tx, &name); + let logger = Some((&mut stdout_writer, &mut stderr_writer)); + match handle_cook(&recipe, &cooker_config, source_dir, is_deps, &logger) { Ok(_) => cooker_status_tx.send(StatusUpdate::Cooked(name)).unwrap(), Err(e) => cooker_status_tx .send(StatusUpdate::FailCook(name, e.to_string())) @@ -427,18 +478,22 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( }); // ---- Fetcher Thread ---- + let fetcher_recipes = recipes.clone(); let fetcher_config = config.clone(); let fetcher_handle = thread::spawn(move || { - for recipe in recipes { + for recipe in fetcher_recipes { let name = recipe.name.clone(); status_tx .send(StatusUpdate::StartFetch(name.clone())) .unwrap(); - match handle_fetch(&recipe, &fetcher_config) { + let (mut stdout_writer, mut stderr_writer) = setup_logger(&status_tx, &name); + let logger = Some((&mut stdout_writer, &mut stderr_writer)); + + match handle_fetch(&recipe, &fetcher_config, &logger) { Ok(source_dir) => { status_tx.send(StatusUpdate::Fetched(name)).unwrap(); - if work_tx.send((recipe, source_dir)).is_err() { + if work_tx.send((recipe.clone(), source_dir)).is_err() { // Cooker thread died break; } @@ -455,15 +510,22 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( let mut terminal = Terminal::new(TermionBackend::new(stdout()))?; terminal.clear()?; - let mut app = TuiApp::new(Vec::new()); - let total_recipes = app.recipes.len(); + let mut app = TuiApp::new(recipes); + // let total_recipes = app.recipes.len(); let mut running = true; while running { terminal.draw(|f| { let chunks = Layout::default() .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .constraints( + [ + Constraint::Percentage(20), + Constraint::Percentage(20), + Constraint::Percentage(60), + ] + .as_ref(), + ) .split(f.area()); // Left Pane @@ -509,13 +571,34 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( .block(Block::default().title("Cook Queue").borders(Borders::ALL)); f.render_widget(cook_list, chunks[1]); - let footer = Paragraph::new(format!( - "Done: {}/{} | Failed: {}", - app.done.len(), - total_recipes, - app.failed.len() - )); - f.render_widget(footer, f.area()); + let log_title = if let Some(active_name) = &app.active_cook { + format!("Build Log: {}", active_name.as_str()) + } else { + "Build Log".to_string() + }; + + let log_text: Vec = if let Some(active_name) = &app.active_cook { + app.logs + .get(active_name) + .cloned() + .unwrap_or_else(|| vec!["Waiting for logs...".to_string()]) + } else { + vec!["No active cook job.".to_string()] + }; + + let log_paragraph = Paragraph::new(log_text.join("\n")) + .block(Block::default().title(log_title).borders(Borders::ALL)) + .wrap(Wrap { trim: false }); + + f.render_widget(log_paragraph, chunks[2]); + + // let footer = Paragraph::new(format!( + // "Done: {}/{} | Failed: {}", + // app.done.len(), + // total_recipes, + // app.failed.len() + // )); + // f.render_widget(footer, f.area()); })?; while let Ok(update) = status_rx.try_recv() { @@ -536,3 +619,14 @@ fn run_tui_cook(config: CliConfig, recipes: Vec) -> anyhow::Result<( Ok(()) } + +fn setup_logger( + cooker_status_tx: &mpsc::Sender, + name: &PackageName, +) -> (std::io::PipeWriter, std::io::PipeWriter) { + let (stdout_reader, stdout_writer) = std::io::pipe().expect("Failed to create stdout pipe"); + let (stderr_reader, stderr_writer) = std::io::pipe().expect("Failed to create stderr pipe"); + spawn_log_reader(stdout_reader, name.clone(), cooker_status_tx.clone()); + spawn_log_reader(stderr_reader, name.clone(), cooker_status_tx.clone()); + (stdout_writer, stderr_writer) +} diff --git a/src/cook/cook_build.rs b/src/cook/cook_build.rs index a4773f26d..016e840f0 100644 --- a/src/cook/cook_build.rs +++ b/src/cook/cook_build.rs @@ -148,6 +148,7 @@ pub fn build( recipe: &Recipe, offline_mode: bool, check_source: bool, + logger: &Stdout, ) -> Result<(PathBuf, BTreeSet), String> { let sysroot_dir = target_dir.join("sysroot"); let stage_dir = target_dir.join("stage"); @@ -296,7 +297,7 @@ pub fn build( flags_fn("COOKBOOK_MESON_FLAGS", mesonflags), ), BuildKind::Custom { script } => script.clone(), - BuildKind::Remote => return build_remote(target_dir, name, offline_mode), + BuildKind::Remote => return build_remote(target_dir, name, offline_mode, logger), BuildKind::None => "".to_owned(), }; @@ -341,7 +342,7 @@ pub fn build( "{}\n{}\n{}\n{}", BUILD_PRESCRIPT, SHARED_PRESCRIPT, script, BUILD_POSTSCRIPT ); - run_command_stdin(command, full_script.as_bytes())?; + run_command_stdin(command, full_script.as_bytes(), logger)?; // Move stage.tmp to stage atomically rename(&stage_dir_tmp, &stage_dir)?; @@ -389,6 +390,7 @@ pub fn build_remote( target_dir: &Path, name: &PackageName, offline_mode: bool, + logger: &Stdout, ) -> Result<(PathBuf, BTreeSet), String> { // download straight from remote source then declare pkg dependencies as autodeps dependency let stage_dir = target_dir.join("stage"); @@ -398,9 +400,9 @@ pub fn build_remote( let source_pubkey = target_dir.join("id_ed25519.pub.toml"); if !offline_mode { - download_wget(&get_remote_url(name, "pkgar"), &source_pkgar)?; - download_wget(&get_remote_url(name, "toml"), &source_toml)?; - download_wget(&get_pubkey_url(), &source_pubkey)?; + download_wget(&get_remote_url(name, "pkgar"), &source_pkgar, logger)?; + download_wget(&get_remote_url(name, "toml"), &source_toml, logger)?; + download_wget(&get_pubkey_url(), &source_pubkey, logger)?; } else { offline_check_exists(&source_pkgar)?; offline_check_exists(&source_toml)?; diff --git a/src/cook/fetch.rs b/src/cook/fetch.rs index b9a2bfc6a..a69b38555 100644 --- a/src/cook/fetch.rs +++ b/src/cook/fetch.rs @@ -25,7 +25,11 @@ pub(crate) fn get_blake3(path: &PathBuf, show_progress: bool) -> Result Result { +pub fn fetch_offline( + recipe_dir: &Path, + recipe: &Recipe, + logger: &Stdout, +) -> Result { let source_dir = recipe_dir.join("source"); if recipe.build.kind == BuildKind::None || recipe.build.kind == BuildKind::Remote { // the build function doesn't need source dir exists @@ -33,10 +37,10 @@ pub fn fetch_offline(recipe_dir: &Path, recipe: &Recipe) -> Result { - return fetch(recipe_dir, recipe); + return fetch(recipe_dir, recipe, logger); } Some(SourceRecipe::SameAs { same_as: _ }) => { - return fetch(recipe_dir, recipe); + return fetch(recipe_dir, recipe, logger); } Some(SourceRecipe::Git { git: _, @@ -65,8 +69,8 @@ pub fn fetch_offline(recipe_dir: &Path, recipe: &Recipe) -> Result Result Result { +pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &Stdout) -> Result { let source_dir = recipe_dir.join("source"); if recipe.build.kind == BuildKind::None || recipe.build.kind == BuildKind::Remote { // the build function doesn't need source dir exists @@ -94,7 +98,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { Some(SourceRecipe::SameAs { same_as }) => { let (canon_dir, recipe) = fetch_resolve_canon(recipe_dir, &same_as)?; // recursively fetch - fetch(&canon_dir, &recipe)?; + fetch(&canon_dir, &recipe, logger)?; fetch_make_symlink(&source_dir, &same_as)?; } Some(SourceRecipe::Path { path }) => { @@ -140,7 +144,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { command.arg("--depth").arg("1").arg("--shallow-submodules"); } command.arg(&source_dir_tmp); - run_command(command)?; + run_command(command, logger)?; // Move source.tmp to source atomically rename(&source_dir_tmp, &source_dir)?; @@ -158,13 +162,13 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { let mut command = Command::new("git"); command.arg("-C").arg(&source_dir); command.arg("remote").arg("set-url").arg("origin").arg(git); - run_command(command)?; + run_command(command, logger)?; // Fetch origin let mut command = Command::new("git"); command.arg("-C").arg(&source_dir); command.arg("fetch").arg("origin"); - run_command(command)?; + run_command(command, logger)?; } if let Some(_upstream) = upstream { @@ -179,7 +183,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { let mut command = Command::new("git"); command.arg("-C").arg(&source_dir); command.arg("checkout").arg(rev); - run_command(command)?; + run_command(command, logger)?; } else if !shallow_clone && !is_redox() { //TODO: complicated stuff to check and reset branch to origin //TODO: redox can't undestand this (got exit status 1) @@ -189,7 +193,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { command.env("BRANCH", branch); } command.current_dir(&source_dir); - run_command(command)?; + run_command(command, logger)?; } if !patches.is_empty() || script.is_some() { @@ -197,7 +201,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { let mut command = Command::new("git"); command.arg("-C").arg(&source_dir); command.arg("reset").arg("--hard"); - run_command(command)?; + run_command(command, logger)?; } if !shallow_clone { @@ -205,7 +209,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { let mut command = Command::new("git"); command.arg("-C").arg(&source_dir); command.arg("submodule").arg("sync").arg("--recursive"); - run_command(command)?; + run_command(command, logger)?; // Update submodules let mut command = Command::new("git"); @@ -215,10 +219,10 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { .arg("update") .arg("--init") .arg("--recursive"); - run_command(command)?; + run_command(command, logger)?; } - fetch_apply_patches(recipe_dir, patches, script, &source_dir)?; + fetch_apply_patches(recipe_dir, patches, script, &source_dir, logger)?; } Some(SourceRecipe::Tar { tar, @@ -231,7 +235,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { while { if !source_tar.is_file() { tar_updated = true; - download_wget(&tar, &source_tar)?; + download_wget(&tar, &source_tar, logger)?; } let source_tar_blake3 = get_blake3(&source_tar, tar_updated)?; if let Some(blake3) = blake3 { @@ -268,8 +272,8 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe) -> Result { // Create source.tmp let source_dir_tmp = recipe_dir.join("source.tmp"); create_dir_clean(&source_dir_tmp)?; - fetch_extract_tar(source_tar, &source_dir_tmp)?; - fetch_apply_patches(recipe_dir, patches, script, &source_dir_tmp)?; + fetch_extract_tar(source_tar, &source_dir_tmp, logger)?; + fetch_apply_patches(recipe_dir, patches, script, &source_dir_tmp, logger)?; // Move source.tmp to source atomically rename(&source_dir_tmp, &source_dir)?; @@ -342,6 +346,7 @@ pub(crate) fn fetch_resolve_canon( pub(crate) fn fetch_extract_tar( source_tar: PathBuf, source_dir_tmp: &PathBuf, + logger: &Stdout, ) -> Result<(), String> { let mut command = Command::new("tar"); if is_redox() { @@ -354,7 +359,7 @@ pub(crate) fn fetch_extract_tar( command.arg(&source_tar); command.arg("--directory").arg(source_dir_tmp); command.arg("--strip-components").arg("1"); - run_command(command)?; + run_command(command, logger)?; Ok(()) } @@ -387,6 +392,7 @@ pub(crate) fn fetch_apply_patches( patches: &Vec, script: &Option, source_dir_tmp: &PathBuf, + logger: &Stdout, ) -> Result<(), String> { for patch_name in patches { let patch_file = recipe_dir.join(patch_name); @@ -409,12 +415,16 @@ pub(crate) fn fetch_apply_patches( let mut command = Command::new("patch"); command.arg("--directory").arg(source_dir_tmp); command.arg("--strip=1"); - run_command_stdin(command, patch.as_bytes())?; + run_command_stdin(command, patch.as_bytes(), logger)?; } Ok(if let Some(script) = script { let mut command = Command::new("bash"); command.arg("-ex"); command.current_dir(source_dir_tmp); - run_command_stdin(command, format!("{SHARED_PRESCRIPT}\n{script}").as_bytes())?; + run_command_stdin( + command, + format!("{SHARED_PRESCRIPT}\n{script}").as_bytes(), + logger, + )?; }) } diff --git a/src/cook/fs.rs b/src/cook/fs.rs index a3295d63f..6a5e8b883 100644 --- a/src/cook/fs.rs +++ b/src/cook/fs.rs @@ -1,7 +1,7 @@ use serde::Serialize; use std::{ fs, - io::{self, Write}, + io::{self, PipeWriter, Write}, path::{Path, PathBuf}, process::{self, Command, Stdio}, time::SystemTime, @@ -146,7 +146,25 @@ pub fn rename(src: &Path, dst: &Path) -> Result<(), String> { }) } -pub fn run_command(mut command: process::Command) -> Result<(), String> { +pub type Stdout<'a> = Option<(&'a mut PipeWriter, &'a mut PipeWriter)>; + +fn pipe_to_cmd(command: &mut Command, stdout_pipe: &Stdout) -> Result<(), String> { + Ok(if let Some((stdout, stderr)) = stdout_pipe { + command.stdout::( + stdout + .try_clone() + .map_err(|e| format!("unable to clone stdout fd: {:?}", e))?, + ); + command.stderr( + stderr + .try_clone() + .map_err(|e| format!("unable to clone stderr fd: {:?}", e))?, + ); + }) +} + +pub fn run_command(mut command: process::Command, stdout_pipe: &Stdout) -> Result<(), String> { + pipe_to_cmd(&mut command, stdout_pipe)?; let status = command .status() .map_err(|err| format!("failed to run {:?}: {}\n{:#?}", command, err, err))?; @@ -161,8 +179,13 @@ pub fn run_command(mut command: process::Command) -> Result<(), String> { Ok(()) } -pub fn run_command_stdin(mut command: process::Command, stdin_data: &[u8]) -> Result<(), String> { +pub fn run_command_stdin( + mut command: process::Command, + stdin_data: &[u8], + stdout_pipe: &Stdout, +) -> Result<(), String> { command.stdin(Stdio::piped()); + pipe_to_cmd(&mut command, stdout_pipe)?; let mut child = command .spawn() @@ -217,13 +240,13 @@ pub fn offline_check_exists(path: &PathBuf) -> Result<(), String> { Ok(()) } -pub fn download_wget(url: &str, dest: &PathBuf) -> Result<(), String> { +pub fn download_wget(url: &str, dest: &PathBuf, logger: &Stdout) -> Result<(), String> { if !dest.is_file() { let dest_tmp = PathBuf::from(format!("{}.tmp", dest.display())); let mut command = Command::new("wget"); command.arg(translate_mirror(url)); command.arg("--continue").arg("-O").arg(&dest_tmp); - run_command(command)?; + run_command(command, logger)?; rename(&dest_tmp, &dest)?; } Ok(()) diff --git a/src/recipe.rs b/src/recipe.rs index ce068a640..a1ea1eef5 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -15,7 +15,7 @@ use serde::{ use crate::WALK_DEPTH; /// Specifies how to download the source for a recipe -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(untagged)] pub enum SourceRecipe { /// Reuse the source directory of another package @@ -88,7 +88,7 @@ impl SourceRecipe { } /// Specifies how to build a recipe -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(tag = "template")] pub enum BuildKind { /// Will not build (for meta packages) @@ -134,7 +134,7 @@ impl Default for BuildKind { } } -#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] pub struct BuildRecipe { #[serde(flatten, default)] pub kind: BuildKind, @@ -142,7 +142,7 @@ pub struct BuildRecipe { pub dependencies: Vec, } -#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] pub struct PackageRecipe { #[serde(default)] pub dependencies: Vec, @@ -151,7 +151,7 @@ pub struct PackageRecipe { } /// Everything required to build a Redox package -#[derive(Debug, Default, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] pub struct Recipe { /// Specifies how to download the source for this recipe pub source: Option, @@ -175,7 +175,7 @@ impl Recipe { Ok(recipe) } } -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct CookRecipe { pub name: PackageName, pub dir: PathBuf,