Merge branch 'recipe-logs' into 'master'

Write build logs

See merge request redox-os/cookbook!679
This commit is contained in:
Jeremy Soller 2025-11-03 06:46:27 -07:00
commit eb52883e86
8 changed files with 190 additions and 181 deletions

87
Cargo.lock generated
View File

@ -429,12 +429,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
@ -772,12 +766,6 @@ dependencies = [
"syn",
]
[[package]]
name = "downcast-rs"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "dryoc"
version = "0.6.2"
@ -1585,18 +1573,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "nix"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.9.1",
"cfg-if 1.0.1",
"cfg_aliases 0.1.1",
"libc",
]
[[package]]
name = "nom"
version = "7.1.3"
@ -1818,27 +1794,6 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "portable-pty"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e"
dependencies = [
"anyhow",
"bitflags 1.3.2",
"downcast-rs",
"filedescriptor",
"lazy_static",
"libc",
"log",
"nix",
"serial2",
"shared_library",
"shell-words",
"winapi",
"winreg",
]
[[package]]
name = "potential_utf"
version = "0.1.2"
@ -1888,7 +1843,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8"
dependencies = [
"bytes",
"cfg_aliases 0.2.1",
"cfg_aliases",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
@ -1928,7 +1883,7 @@ version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970"
dependencies = [
"cfg_aliases 0.2.1",
"cfg_aliases",
"libc",
"once_cell",
"socket2",
@ -2107,13 +2062,13 @@ dependencies = [
"pkgar 0.1.19",
"pkgar-core 0.1.19",
"pkgar-keys 0.1.19",
"portable-pty",
"ratatui",
"redox-pkg",
"redox_installer",
"redoxer",
"regex",
"serde",
"strip-ansi-escapes",
"tempfile",
"termion",
"toml 0.8.23",
@ -2535,17 +2490,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serial2"
version = "0.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cc76fa68e25e771492ca1e3c53d447ef0be3093e05cd3b47f4b712ba10c6f3c"
dependencies = [
"cfg-if 1.0.1",
"libc",
"winapi",
]
[[package]]
name = "sha2"
version = "0.10.9"
@ -2557,22 +2501,6 @@ dependencies = [
"digest 0.10.7",
]
[[package]]
name = "shared_library"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
dependencies = [
"lazy_static",
"libc",
]
[[package]]
name = "shell-words"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]]
name = "shlex"
version = "1.3.0"
@ -3579,15 +3507,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"

View File

@ -20,9 +20,15 @@ name = "cookbook"
path = "src/lib.rs"
doctest = false
[features]
#TODO: Actually make without tui feature works
default = ["tui"]
tui = ["ratatui", "ansi-to-tui", "filedescriptor", "strip-ansi-escapes"]
[dependencies]
anyhow = "1"
blake3 = "=1.5.3" # 1.5.4 is incompatible with blake3 0.3 dependency from pkgar
# blake3 1.5.4 is incompatible with 0.3 dependency from pkgar
blake3 = "=1.5.3"
libc = "0.2"
ignore = "0.4"
object = { version = "0.36", features = ["build_core"] }
@ -30,22 +36,23 @@ pbr = "1.0.2"
pkgar = { path = "pkgar/pkgar" }
pkgar-core = { path = "pkgar/pkgar-core" }
pkgar-keys = { path = "pkgar/pkgar-keys" }
portable-pty = "0.9.0"
redox-pkg = "0.2.8"
redox_installer = "0.2.37"
redoxer = "0.2.56"
regex = "1.11"
serde = { version = "=1.0.197", features = ["derive"] }
termion = "4"
toml = "0.8"
walkdir = "2.3.1"
filedescriptor = "0.8.3"
ansi-to-tui = "7.0.0"
redox_installer = "0.2.37"
filedescriptor = { version = "0.8.3", optional = true }
ansi-to-tui = { version = "7.0.0", optional = true }
strip-ansi-escapes = { version = "0.2.1", optional = true }
[dependencies.ratatui]
version = "0.29.0"
default-features = false
features = ["termion"]
optional = true
[dev-dependencies]
tempfile = "3"

View File

@ -5,8 +5,10 @@ use cookbook::cook::cook_build::build;
use cookbook::cook::fetch::{fetch, fetch_offline};
use cookbook::cook::fs::{create_target_dir, run_command};
use cookbook::cook::package::package;
use cookbook::cook::pty::{PtyOut, UnixSlavePty, setup_pty};
use cookbook::cook::pty::{PtyOut, UnixSlavePty, flush_pty, setup_pty};
use cookbook::cook::script::KILL_ALL_PID;
use cookbook::cook::tree::{display_tree_entry, format_size};
use cookbook::log_to_pty;
use cookbook::recipe::{BuildKind, CookRecipe};
use pkg::PackageName;
use pkg::package::PackageError;
@ -72,6 +74,7 @@ struct CliConfig {
cookbook_dir: PathBuf,
repo_dir: PathBuf,
sysroot_dir: PathBuf,
logs_dir: Option<PathBuf>,
category: Option<PathBuf>,
filesystem: Option<redox_installer::Config>,
with_package_deps: bool,
@ -140,6 +143,12 @@ impl CliConfig {
//FIXME: This config is unused as redox-pkg harcoded this to $PWD/recipes
cookbook_dir: current_dir.join("recipes"),
repo_dir: current_dir.join("repo"),
// build dir here is hardcoded in repo_builder as well
logs_dir: if get_config().cook.tui_logs {
Some(current_dir.join("build/logs"))
} else {
None
},
category: None,
sysroot_dir: if cfg!(target_os = "redox") {
PathBuf::from("/")
@ -342,6 +351,10 @@ fn parse_args(args: Vec<String>) -> anyhow::Result<(CliConfig, CliCommand, Vec<C
// need to prefix by cookbook dir
config.category = Some(PathBuf::from("recipes").join(c));
}
if let Some(c) = config.logs_dir.as_mut() {
*c = c.join(redoxer::target());
fs::create_dir_all(c).map_err(|e| anyhow!(e))?;
}
let command = command.ok_or(anyhow!("Error: No command specified."))?;
let command: CliCommand = str::parse(&command)?;
@ -599,6 +612,7 @@ enum StatusUpdate {
Cooked(CookRecipe),
FailCook(CookRecipe, String),
PushLog(PackageName, Vec<u8>),
FlushLog(PackageName, PathBuf),
FetchThreadFinished,
CookThreadFinished,
}
@ -676,6 +690,55 @@ impl TuiApp {
}
}
pub fn get_active_name(&self) -> Option<PackageName> {
if self.log_view_job == JobType::Cook {
self.active_cook.clone()
} else {
self.active_fetch.clone()
}
}
pub fn get_active_log(
&self,
) -> (
Option<PackageName>,
Option<&Vec<String>>,
Option<Cow<'_, str>>,
) {
let active_name = self.get_active_name();
let (log_text, log_line) = if let Some(active_name) = active_name.as_ref() {
self.get_recipe_log(active_name)
} else {
(None, None)
};
(active_name, log_text, log_line)
}
pub fn get_recipe_log(
&self,
recipe_name: &PackageName,
) -> (Option<&Vec<String>>, Option<Cow<'_, str>>) {
let log_text = self.logs.get(recipe_name);
let log_line = if let Some(b) = self.log_byte_buffer.get(recipe_name) {
Some(String::from_utf8_lossy(b))
} else {
None
};
(log_text, log_line)
}
pub fn write_log(&self, recipe_name: &PackageName, log_path: &PathBuf) -> anyhow::Result<()> {
let (Some(logs), line) = self.get_recipe_log(recipe_name) else {
return Ok(());
};
let str = strip_ansi_escapes::strip_str(join_logs(logs, line));
if !str.trim_end().is_empty() {
fs::write(log_path, str)?;
}
return Ok(());
}
// Update the state based on a message from a worker thread
fn update_status(&mut self, update: StatusUpdate) {
let (name, new_status) = match update {
@ -711,6 +774,12 @@ impl TuiApp {
}
return;
}
StatusUpdate::FlushLog(name, path) => {
// TODO: This blocks the TUI, maybe open separate thread?
// FIXME: handle error here?
let _ = self.write_log(&name, &path);
return;
}
StatusUpdate::Cooked(recipe) => {
if self.active_cook.as_ref() == Some(&recipe.name) {
self.active_cook = None;
@ -782,15 +851,26 @@ fn run_tui_cook(
.send(StatusUpdate::StartCook(name.clone()))
.unwrap();
let (mut stdout_writer, mut stderr_writer) = setup_logger(&cooker_status_tx, &name);
let logger = Some((&mut stdout_writer, &mut stderr_writer));
let mut logger = Some((&mut stdout_writer, &mut stderr_writer));
'again: loop {
match handle_cook(
let handler = handle_cook(
&recipe,
&cooker_config,
source_dir.clone(),
is_deps,
&logger,
) {
);
if let Some(log_path) = cooker_config.logs_dir.as_ref() {
if let Err(err_ctx) = &handler {
log_to_pty!(&logger, "\n{:?}", err_ctx)
}
flush_pty(&mut logger);
let log_path = log_path.join(format!("{}.log", name.as_str()));
cooker_status_tx
.send(StatusUpdate::FlushLog(name.clone(), log_path))
.unwrap_or_default();
}
match handler {
Ok(()) => {
cooker_status_tx
.send(StatusUpdate::Cooked(recipe))
@ -867,10 +947,24 @@ fn run_tui_cook(
.unwrap();
let (mut stdout_writer, mut stderr_writer) = setup_logger(&fetcher_status_tx, &name);
let logger = Some((&mut stdout_writer, &mut stderr_writer));
let mut logger = Some((&mut stdout_writer, &mut stderr_writer));
'again: loop {
match handle_fetch(&recipe, &fetcher_config, &logger) {
let handler = handle_fetch(&recipe, &fetcher_config, &logger);
if let Some(log_path) = fetcher_config.logs_dir.as_ref()
// successful fetch log usually not that helpful
&& handler.is_err()
{
if let Err(err_ctx) = &handler {
log_to_pty!(&logger, "\n{:?}", err_ctx)
}
flush_pty(&mut logger);
let log_path = log_path.join(format!("{}.log", name.as_str()));
fetcher_status_tx
.send(StatusUpdate::FlushLog(name.clone(), log_path))
.unwrap_or_default();
}
match handler {
Ok(source_dir) => {
fetcher_status_tx
.send(StatusUpdate::Fetched(recipe.clone()))
@ -1041,7 +1135,7 @@ fn run_tui_cook(
);
f.render_stateful_widget(cook_list, cook_chunk, &mut app.cook_list_state);
let (active_name, log_text, log_line) = get_active_log(&app);
let (active_name, log_text, log_line) = app.get_active_log();
let log_title = if let Some(active_name) = active_name {
format!(
" {} Log: {} ",
@ -1152,16 +1246,11 @@ fn run_tui_cook(
if let Some((app, res)) = handle_prompt_input(&event, &mut app) {
prompting.swap(res as u32, Ordering::SeqCst);
if res == PromptOption::Exit {
let (name, log, line) = get_active_log(&app);
let (name, log, line) = app.get_active_log();
if let Some(name) = name
&& let Some(log) = log
{
let mut logs = log.join("\n");
if let Some(line) = line {
logs.push_str("\n");
logs.push_str(handle_cr(&line));
}
app.dump_logs_on_exit = Some((name.to_owned(), logs));
app.dump_logs_on_exit = Some((name.to_owned(), join_logs(log, line)));
}
running.store(false, Ordering::SeqCst);
}
@ -1194,38 +1283,20 @@ fn run_tui_cook(
Ok(app.dump_logs_on_exit)
}
fn join_logs(log: &Vec<String>, line: Option<Cow<'_, str>>) -> String {
let mut logs = log.join("\n");
if let Some(line) = line {
logs.push_str("\n");
logs.push_str(handle_cr(&line));
}
logs
}
fn handle_cr<'a>(buffer: &'a Cow<'_, str>) -> &'a str {
let st = buffer.trim_end();
st.rsplit('\r').next().unwrap_or(&st)
}
fn get_active_log(
app: &TuiApp,
) -> (
Option<PackageName>,
Option<&Vec<String>>,
Option<Cow<'_, str>>,
) {
let active_name = if app.log_view_job == JobType::Cook {
app.active_cook.clone()
} else {
app.active_fetch.clone()
};
let log_text = if let Some(active_name) = &active_name {
app.logs.get(active_name)
} else {
None
};
let log_line = if let Some(active_name) = &active_name
&& let Some(b) = app.log_byte_buffer.get(active_name)
{
Some(String::from_utf8_lossy(b))
} else {
None
};
(active_name, log_text, log_line)
}
fn handle_main_event(app: &mut TuiApp, event: &Event) {
match event {
Event::Key(key) => match key {
@ -1238,12 +1309,11 @@ fn handle_main_event(app: &mut TuiApp, event: &Event) {
Key::Char('c') => {
// as compilers still running, we use this way to stop it
let pid = std::process::id();
Command::new("pkill")
.arg("-9")
.arg("-P")
.arg(pid.to_string())
Command::new("bash")
.arg("-c")
.arg(KILL_ALL_PID.replace("$PID", &pid.to_string()))
.spawn()
.expect("unable to spawn pkill");
.expect("unable to spawn kill");
}
Key::Up => {
app.auto_scroll = false;

View File

@ -12,6 +12,9 @@ pub struct CookConfigOpt {
/// whether to use TUI to allow parallel build
/// default value is yes if "CI" env unset and STDIN is open.
pub tui: Option<bool>,
/// whether to write logs to build/logs dir
/// only usable when tui is used
pub tui_logs: Option<bool>,
/// whether to ignore build errors
pub nonstop: Option<bool>,
/// whether to print success recipes info and warnings
@ -24,6 +27,7 @@ pub struct CookConfig {
pub offline: bool,
pub jobs: usize,
pub tui: bool,
pub tui_logs: bool,
pub nonstop: bool,
pub verbose: bool,
}
@ -34,6 +38,7 @@ impl From<CookConfigOpt> for CookConfig {
offline: value.offline.unwrap(),
jobs: value.jobs.unwrap(),
tui: value.tui.unwrap(),
tui_logs: value.tui_logs.unwrap(),
nonstop: value.nonstop.unwrap(),
verbose: value.verbose.unwrap(),
}
@ -75,6 +80,9 @@ pub fn init_config() {
.unwrap_or(1),
));
}
if config.cook_opt.tui_logs.is_none() {
config.cook_opt.tui_logs = config.cook_opt.tui;
}
if config.cook_opt.offline.is_none() {
config.cook_opt.offline = Some(extract_env("COOKBOOK_OFFLINE", false));
}

View File

@ -19,25 +19,10 @@ use std::{
time::SystemTime,
};
use crate::is_redox;
use crate::{is_redox, log_to_pty};
use crate::REMOTE_PKG_SOURCE;
macro_rules! log_warn {
($logger:expr, $($arg:tt)+) => {
use std::io::Write;
if $logger.is_some() {
let _ = $logger.as_ref().unwrap().1.try_clone().unwrap().write(
format!($($arg)+)
.as_bytes(),
);
} else {
eprintln!($($arg)+);
}
};
}
fn auto_deps_from_dynamic_linking(
stage_dir: &Path,
dep_pkgars: &BTreeSet<(PackageName, PathBuf)>,
@ -61,7 +46,7 @@ fn auto_deps_from_dynamic_linking(
};
if visited.contains(&dir) {
#[cfg(debug_assertions)]
log_warn!(
log_to_pty!(
logger,
"DEBUG: auto_deps => Skipping `{dir:?}` (already visited)"
);
@ -111,7 +96,7 @@ fn auto_deps_from_dynamic_linking(
continue;
};
if let Ok(relative_path) = path.strip_prefix(stage_dir) {
log_warn!(logger, "DEBUG: {} needs {}", relative_path.display(), name);
log_to_pty!(logger, "DEBUG: {} needs {}", relative_path.display(), name);
}
needed.insert(name.to_string());
}
@ -145,7 +130,7 @@ fn auto_deps_from_dynamic_linking(
continue;
};
if needed.contains(child_name) {
log_warn!(logger, "DEBUG: {} provides {}", dep, child_name);
log_to_pty!(logger, "DEBUG: {} provides {}", dep, child_name);
deps.insert(dep.clone());
missing.remove(child_name);
}
@ -155,7 +140,7 @@ fn auto_deps_from_dynamic_linking(
}
for name in missing {
log_warn!(logger, "WARN: {} missing", name);
log_to_pty!(logger, "WARN: {} missing", name);
}
deps
@ -225,7 +210,7 @@ pub fn build(
if sysroot_dir.is_dir() {
let sysroot_modified = modified_dir(&sysroot_dir)?;
if sysroot_modified < source_modified || sysroot_modified < deps_modified {
log_warn!(
log_to_pty!(
logger,
"DEBUG: '{}' newer than '{}'",
source_dir.display(),
@ -275,7 +260,7 @@ pub fn build(
if stage_dir.is_dir() {
let stage_modified = modified_dir(&stage_dir)?;
if stage_modified < source_modified || stage_modified < deps_modified {
log_warn!(
log_to_pty!(
logger,
"DEBUG: '{}' newer than '{}'",
source_dir.display(),

View File

@ -3,6 +3,7 @@ use crate::cook::fs::*;
use crate::cook::pty::PtyOut;
use crate::cook::script::*;
use crate::is_redox;
use crate::log_to_pty;
use crate::recipe::BuildKind;
use crate::recipe::Recipe;
use crate::{blake3, recipe::SourceRecipe};
@ -10,21 +11,6 @@ use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
macro_rules! log_warn {
($logger:expr, $($arg:tt)+) => {
use std::io::Write;
if $logger.is_some() {
let _ = $logger.as_ref().unwrap().1.try_clone().unwrap().write(
format!($($arg)+)
.as_bytes(),
);
} else {
eprintln!($($arg)+);
}
};
}
pub(crate) fn get_blake3(path: &PathBuf, show_progress: bool) -> Result<String, String> {
if show_progress {
blake3::blake3_progress(&path)
@ -120,7 +106,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &PtyOut) -> Result<Path
Some(SourceRecipe::Path { path }) => {
if !source_dir.is_dir() || modified_dir(Path::new(&path))? > modified_dir(&source_dir)?
{
log_warn!(
log_to_pty!(
logger,
"[DEBUG]: {} is newer than {}",
path,
@ -266,7 +252,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &PtyOut) -> Result<Path
"The downloaded tar blake3 '{source_tar_blake3}' is not equal to blake3 in recipe.toml"
));
} else {
log_warn!(
log_to_pty!(
logger,
"DEBUG: source tar blake3 is different and need redownload"
);
@ -278,7 +264,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &PtyOut) -> Result<Path
}
} else {
//TODO: set blake3 hash on the recipe with something like "cook fix"
log_warn!(
log_to_pty!(
logger,
"WARNING: set blake3 for '{}' to '{}'",
source_tar.display(),
@ -289,7 +275,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &PtyOut) -> Result<Path
} {}
if source_dir.is_dir() {
if tar_updated || fetch_is_patches_newer(recipe_dir, patches, &source_dir)? {
log_warn!(
log_to_pty!(
logger,
"DEBUG: source tar or patches is newer than the source directory"
);
@ -310,7 +296,7 @@ pub fn fetch(recipe_dir: &Path, recipe: &Recipe, logger: &PtyOut) -> Result<Path
// Local Sources
None => {
if !source_dir.is_dir() {
log_warn!(
log_to_pty!(
logger,
"WARNING: Recipe without source section expected source dir at '{}'",
source_dir.display(),

View File

@ -1,11 +1,12 @@
use anyhow::{Error, bail};
use filedescriptor::FileDescriptor;
use libc::{self, winsize};
use std::io::Read;
use std::io::{Read, Write};
use std::os::fd::FromRawFd;
use std::os::unix::io::AsRawFd;
use std::os::unix::process::CommandExt;
use std::process::Child;
use std::time::Duration;
use std::{io, mem, ptr};
use std::{
io::{PipeReader, PipeWriter},
@ -17,10 +18,9 @@ pub use std::os::unix::io::RawFd;
#[macro_export]
macro_rules! log_to_pty {
($logger:expr, $($arg:tt)+) => {
use std::io::Write;
if $logger.is_some() {
let _ = $logger.as_ref().unwrap().1.try_clone().unwrap().write(
use std::io::Write;
let _ = $logger.as_ref().unwrap().1.try_clone().unwrap().write(
format!($($arg)+)
.as_bytes(),
);
@ -57,6 +57,17 @@ pub fn setup_pty() -> (
(pty_reader, log_reader, pipes)
}
pub fn flush_pty(logger: &mut PtyOut) {
let Some((pty, file)) = logger else {
return;
};
// Not sure if flush actually working
let _ = pty.flush();
std::thread::sleep(Duration::from_millis(100));
let _ = file.flush();
std::thread::sleep(Duration::from_millis(100));
}
pub fn spawn_to_pipe(command: &mut Command, stdout_pipe: &PtyOut) -> Result<Child, Error> {
match stdout_pipe {
Some(stdout) => stdout.0.spawn_command(command.into()),
@ -282,6 +293,10 @@ impl PtyFd {
Ok(child)
}
fn flush(&mut self) -> std::io::Result<()> {
self.0.flush()
}
}
/// Represents the master end of a pty.
@ -319,6 +334,9 @@ impl UnixSlavePty {
fn spawn_command(&self, builder: &mut Command) -> Result<std::process::Child, Error> {
Ok(self.fd.spawn_command(builder)?)
}
fn flush(&mut self) -> Result<(), anyhow::Error> {
Ok(self.fd.flush()?)
}
}
impl UnixMasterPty {

View File

@ -351,3 +351,19 @@ if [ "$(git rev-parse HEAD)" != "$(git rev-parse $ORIGIN_BRANCH)" ]
then
git checkout -B "$(echo "$ORIGIN_BRANCH" | cut -d / -f 2-)" "$ORIGIN_BRANCH"
fi"#;
pub static KILL_ALL_PID: &str = r#"
THISPID=$$
CHILDREN=$(ps -o pid= --ppid $PID | grep -v $THISPID);
ALL_DESCENDANTS='';
while [ -n "$CHILDREN" ]; do
ALL_DESCENDANTS="$ALL_DESCENDANTS $CHILDREN";
CHILDREN=$(ps -o pid= --ppid $(echo $CHILDREN) | tr '\n' ' ');
done;
if [ -n "$ALL_DESCENDANTS" ]; then
kill -9 $ALL_DESCENDANTS;
fi
"#;