feat: Bugreport command (#28)
Add a bug report + completions command(not complete yet) so that new users can easily submit bugs to us, and I wanted completions for bb (super easy with clap) --------- Co-authored-by: Gerald Pinder <gmpinder@gmail.com>
This commit is contained in:
parent
bdbbcea7cc
commit
e069346e15
22 changed files with 1595 additions and 280 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,3 +1,6 @@
|
|||
/target
|
||||
.sccache/
|
||||
.vscode/
|
||||
|
||||
# Local testing for bb recipe files
|
||||
/config/
|
||||
|
|
|
|||
|
|
@ -1,2 +1,6 @@
|
|||
[language-server.rust-analyzer.config]
|
||||
cargo.features = ["nightly"]
|
||||
|
||||
[language-server.rust-analyzer.config.check]
|
||||
command = "clippy"
|
||||
args = ["--no-deps"]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
[hooks]
|
||||
pre-commit = "cargo fmt && cargo test && cargo clippy -- -D warnings"
|
||||
pre-commit = "cargo fmt --check && cargo test && cargo clippy -- -D warnings"
|
||||
|
||||
[logging]
|
||||
verbose = true
|
||||
|
|
|
|||
756
Cargo.lock
generated
756
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
44
Cargo.toml
44
Cargo.toml
|
|
@ -6,27 +6,41 @@ description = "A CLI tool built for creating Containerfile templates based on th
|
|||
repository = "https://github.com/blue-build/cli"
|
||||
license = "Apache-2.0"
|
||||
categories = ["command-line-utilities"]
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
askama = { version = "0.12.1", features = ["serde-json"] }
|
||||
cfg-if = "1.0.0"
|
||||
askama = { version = "0.12", features = ["serde-json", "serde-yaml"] }
|
||||
chrono = "0.4"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
clap-verbosity-flag = "2.1.1"
|
||||
derive_builder = "0.12.0"
|
||||
env_logger = "0.10.1"
|
||||
futures-util = { version = "0.3.30", optional = true }
|
||||
indexmap = { version = "2.1.0", features = ["serde"] }
|
||||
clap = { version = "4", features = ["derive", "cargo", "unicode"] }
|
||||
clap-verbosity-flag = "2"
|
||||
clap_complete = "4"
|
||||
clap_complete_nushell = "4"
|
||||
colorized = "1"
|
||||
derive_builder = "0.13"
|
||||
directories = "5"
|
||||
env_logger = "0.11"
|
||||
futures-util = { version = "0.3", optional = true }
|
||||
fuzzy-matcher = "0.3"
|
||||
indexmap = { version = "2", features = ["serde"] }
|
||||
log = "0.4"
|
||||
open = "5"
|
||||
# update os module config and tests when upgrading os_info
|
||||
shadow-rs = { version = "0.26" }
|
||||
os_info = "3.7"
|
||||
podman-api = { version = "0.10.0", optional = true }
|
||||
process_control = { version = "4.0.3", features = ["crossbeam-channel"] }
|
||||
requestty = { version = "0.5", features = ["macros", "termion"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9.25"
|
||||
serde_yaml = "0.9.30"
|
||||
sigstore = { version = "0.8.0", optional = true }
|
||||
tokio = { version = "1", features = ["full"], optional = true }
|
||||
typed-builder = "0.18.0"
|
||||
typed-builder = "0.18.1"
|
||||
urlencoding = "2.1.3"
|
||||
users = "0.11.0"
|
||||
which = "6"
|
||||
format_serde_error = "0.3.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
|
@ -37,3 +51,13 @@ init = []
|
|||
|
||||
[dev-dependencies]
|
||||
rusty-hook = "0.11.2"
|
||||
|
||||
[build-dependencies]
|
||||
shadow-rs = { version = "0.26.1", default-features = false }
|
||||
dunce = "1.0.4"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
debug = false
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ common:
|
|||
COPY --keep-ts Cargo.* /app
|
||||
COPY --keep-ts *.md /app
|
||||
COPY --keep-ts LICENSE /app
|
||||
COPY --keep-ts build.rs /app
|
||||
|
||||
DO cargo+INIT
|
||||
|
||||
|
|
|
|||
50
build.rs
Normal file
50
build.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use shadow_rs::SdResult;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() -> SdResult<()> {
|
||||
shadow_rs::new_hook(hook)
|
||||
}
|
||||
|
||||
fn hook(file: &File) -> SdResult<()> {
|
||||
append_write_const(file)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn append_write_const(mut file: &File) -> SdResult<()> {
|
||||
let hash = Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.map(|x| {
|
||||
String::from_utf8(x.stdout)
|
||||
.ok()
|
||||
.map(|x| x.trim().to_string())
|
||||
})
|
||||
.unwrap_or(None);
|
||||
|
||||
let short_hash = Command::new("git")
|
||||
.args(["rev-parse", "--short", "HEAD"])
|
||||
.output()
|
||||
.map(|x| {
|
||||
String::from_utf8(x.stdout)
|
||||
.ok()
|
||||
.map(|x| x.trim().to_string())
|
||||
})
|
||||
.unwrap_or(None);
|
||||
|
||||
let hook_const: &str = &format!(
|
||||
"{}\n{}",
|
||||
&format!(
|
||||
"pub const BB_COMMIT_HASH: &str = \"{}\";",
|
||||
hash.unwrap_or_default()
|
||||
),
|
||||
&format!(
|
||||
"pub const BB_COMMIT_HASH_SHORT: &str = \"{}\";",
|
||||
short_hash.unwrap_or_default()
|
||||
)
|
||||
);
|
||||
|
||||
writeln!(file, "{hook_const}")?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,3 +1,8 @@
|
|||
[toolchain]
|
||||
channel = "stable"
|
||||
targets = ["x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl"]
|
||||
targets = [
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-unknown-linux-musl",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-musl",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,66 +1,6 @@
|
|||
#![warn(clippy::pedantic, clippy::nursery)]
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap_verbosity_flag::{InfoLevel, Verbosity};
|
||||
use blue_build::commands::*;
|
||||
use clap::Parser;
|
||||
use env_logger::WriteStyle;
|
||||
use log::trace;
|
||||
|
||||
use blue_build::{
|
||||
self,
|
||||
commands::{build, local, template, BlueBuildCommand},
|
||||
};
|
||||
|
||||
#[cfg(feature = "init")]
|
||||
use blue_build::commands::init;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "BlueBuild", author, version, about, long_about = None)]
|
||||
struct BlueBuildArgs {
|
||||
#[command(subcommand)]
|
||||
command: CommandArgs,
|
||||
|
||||
#[clap(flatten)]
|
||||
verbosity: Verbosity<InfoLevel>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum CommandArgs {
|
||||
/// Build an image from a recipe
|
||||
Build(build::BuildCommand),
|
||||
|
||||
/// Generate a Containerfile from a recipe
|
||||
Template(template::TemplateCommand),
|
||||
|
||||
/// Upgrade your current OS with the
|
||||
/// local image saved at `/etc/blue-build/`.
|
||||
///
|
||||
/// This requires having rebased already onto
|
||||
/// a local archive already by using the `rebase`
|
||||
/// subcommand.
|
||||
///
|
||||
/// NOTE: This can only be used if you have `rpm-ostree`
|
||||
/// installed and if the `--push` and `--rebase` option isn't
|
||||
/// used. This image will not be signed.
|
||||
Upgrade(local::UpgradeCommand),
|
||||
|
||||
/// Rebase your current OS onto the image
|
||||
/// being built.
|
||||
///
|
||||
/// This will create a tarball of your image at
|
||||
/// `/etc/blue-build/` and invoke `rpm-ostree` to
|
||||
/// rebase onto the image using `oci-archive`.
|
||||
///
|
||||
/// NOTE: This can only be used if you have `rpm-ostree`
|
||||
/// installed.
|
||||
Rebase(local::RebaseCommand),
|
||||
|
||||
/// Initialize a new Ublue Starting Point repo
|
||||
#[cfg(feature = "init")]
|
||||
Init(init::InitCommand),
|
||||
|
||||
#[cfg(feature = "init")]
|
||||
New(init::NewCommand),
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = BlueBuildArgs::parse();
|
||||
|
|
@ -71,18 +11,18 @@ fn main() {
|
|||
.write_style(WriteStyle::Always)
|
||||
.init();
|
||||
|
||||
trace!("{args:#?}");
|
||||
log::trace!("Parsed arguments: {args:#?}");
|
||||
|
||||
match args.command {
|
||||
#[cfg(feature = "init")]
|
||||
CommandArgs::Init(mut command) => command.run(),
|
||||
|
||||
#[cfg(feature = "init")]
|
||||
CommandArgs::New(mut command) => command.run(),
|
||||
|
||||
CommandArgs::Template(mut command) => command.run(),
|
||||
CommandArgs::Build(mut command) => command.run(),
|
||||
CommandArgs::Rebase(mut command) => command.run(),
|
||||
CommandArgs::Upgrade(mut command) => command.run(),
|
||||
CommandArgs::Template(mut command) => command.run(),
|
||||
CommandArgs::BugReport(mut command) => command.run(),
|
||||
CommandArgs::Completions(mut command) => command.run(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
#[cfg(feature = "init")]
|
||||
pub mod init;
|
||||
|
||||
pub mod build;
|
||||
pub mod local;
|
||||
pub mod template;
|
||||
|
||||
use log::error;
|
||||
|
||||
pub trait BlueBuildCommand {
|
||||
/// Runs the command and returns a result
|
||||
/// of the execution
|
||||
///
|
||||
/// # Errors
|
||||
/// Can return an `anyhow` Error
|
||||
fn try_run(&mut self) -> anyhow::Result<()>;
|
||||
|
||||
/// Runs the command and exits if there is an error.
|
||||
fn run(&mut self) {
|
||||
if let Err(e) = self.try_run() {
|
||||
error!("{e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
471
src/commands/bug_report.rs
Normal file
471
src/commands/bug_report.rs
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
use crate::module_recipe::{Module, ModuleExt, Recipe};
|
||||
use crate::shadow;
|
||||
|
||||
use anyhow::Result;
|
||||
use askama::Template;
|
||||
use clap::Args;
|
||||
use clap_complete::Shell;
|
||||
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
|
||||
use log::{debug, error, trace};
|
||||
use requestty::question::{completions, Completions};
|
||||
use std::borrow::Cow;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use super::utils::exec_cmd;
|
||||
use super::BlueBuildCommand;
|
||||
|
||||
const UNKNOWN_SHELL: &str = "<unknown shell>";
|
||||
const UNKNOWN_VERSION: &str = "<unknown version>";
|
||||
const UNKNOWN_TERMINAL: &str = "<unknown terminal>";
|
||||
const GITHUB_CHAR_LIMIT: usize = 8100; // Magic number accepted by Github
|
||||
|
||||
#[derive(Default, Debug, Clone, TypedBuilder, Args)]
|
||||
pub struct BugReportRecipe {
|
||||
recipe_dir: Option<String>,
|
||||
recipe_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Args, TypedBuilder)]
|
||||
pub struct BugReportCommand {
|
||||
/// Path to the recipe file
|
||||
#[arg(short, long)]
|
||||
#[builder(default)]
|
||||
recipe_path: Option<String>,
|
||||
}
|
||||
|
||||
impl BlueBuildCommand for BugReportCommand {
|
||||
fn try_run(&mut self) -> anyhow::Result<()> {
|
||||
debug!(
|
||||
"Generating bug report for hash: {}\n",
|
||||
shadow::BB_COMMIT_HASH
|
||||
);
|
||||
debug!("Shadow Versioning:\n{}", shadow::VERSION.trim());
|
||||
|
||||
self.create_bugreport()
|
||||
}
|
||||
}
|
||||
|
||||
impl BugReportCommand {
|
||||
/// Create a pre-populated GitHub issue with information about your configuration
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if it fails to open the issue in your browser.
|
||||
/// If this happens, you can copy the generated report and open an issue manually.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function will panic if it fails to get the current shell or terminal version.
|
||||
pub fn create_bugreport(&self) -> anyhow::Result<()> {
|
||||
use colorized::{Color, Colors};
|
||||
|
||||
let os_info = os_info::get();
|
||||
let recipe = self.get_recipe();
|
||||
|
||||
let environment = Environment {
|
||||
os_type: os_info.os_type(),
|
||||
shell_info: get_shell_info(),
|
||||
terminal_info: get_terminal_info(),
|
||||
os_version: os_info.version().clone(),
|
||||
};
|
||||
|
||||
let issue_body = match generate_github_issue(&environment, &recipe) {
|
||||
Ok(body) => body,
|
||||
Err(e) => {
|
||||
println!(
|
||||
"{}: {e}",
|
||||
"Failed to generate bug report".color(Colors::BrightRedFg)
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
println!(
|
||||
"\n{}\n{}\n",
|
||||
"Generated bug report:".color(Colors::BrightGreenFg),
|
||||
issue_body
|
||||
.color(Colors::BrightBlackBg)
|
||||
.color(Colors::BrightWhiteFg)
|
||||
);
|
||||
|
||||
const WARNING_MESSAGE: &str = "Please copy the above report and open an issue manually.";
|
||||
let question = requestty::Question::confirm("anonymous")
|
||||
.message(
|
||||
"Forward the pre-filled report above to GitHub in your browser?"
|
||||
.color(Colors::BrightYellowFg),
|
||||
)
|
||||
.default(true)
|
||||
.build();
|
||||
|
||||
println!("{} To avoid any sensitive data from being exposed, please review the included information before proceeding.", "Warning:".color(Colors::BrightRedBg).color(Colors::BrightWhiteFg));
|
||||
println!("Data forwarded to GitHub is subject to GitHub's privacy policy. For more information, see https://docs.github.com/en/github/site-policy/github-privacy-statement.\n");
|
||||
match requestty::prompt_one(question) {
|
||||
Ok(answer) => {
|
||||
if answer.as_bool().unwrap() {
|
||||
let link = make_github_issue_link(&issue_body);
|
||||
if let Err(e) = open::that(&link) {
|
||||
println!("Failed to open issue report in your browser: {e}");
|
||||
println!("Please copy the above report and open an issue manually, or try opening the following link:\n{link}");
|
||||
return Err(e.into());
|
||||
}
|
||||
} else {
|
||||
println!("{WARNING_MESSAGE}");
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Will not open an issue in your browser! {WARNING_MESSAGE}");
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"\n{}",
|
||||
"Thanks for using the BlueBuild bug report tool!".color(Colors::BrightCyanFg)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_recipe(&self) -> Option<Recipe> {
|
||||
let recipe_path = if let Some(recipe_path) = self.recipe_path.clone() {
|
||||
recipe_path
|
||||
} else if let Ok(recipe) = get_config_file("recipe", "Enter path to recipe file") {
|
||||
recipe
|
||||
} else {
|
||||
trace!("Failed to get recipe");
|
||||
String::new()
|
||||
};
|
||||
|
||||
Recipe::parse(&recipe_path).ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_config_file(title: &str, message: &str) -> anyhow::Result<String> {
|
||||
use std::path::Path;
|
||||
|
||||
let question = requestty::Question::input(title)
|
||||
.message(message)
|
||||
.auto_complete(|p, _| auto_complete(p))
|
||||
.validate(|p, _| {
|
||||
if (p.as_ref() as &Path).exists() {
|
||||
Ok(())
|
||||
} else if p.is_empty() {
|
||||
Err("No file specified. Please enter a file path".to_string())
|
||||
} else {
|
||||
Err(format!("file `{p}` doesn't exist"))
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
match requestty::prompt_one(question) {
|
||||
Ok(requestty::Answer::String(path)) => Ok(path),
|
||||
Ok(_) => unreachable!(),
|
||||
Err(e) => {
|
||||
trace!("Failed to get file: {}", e);
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn auto_complete(p: String) -> Completions<String> {
|
||||
use std::path::Path;
|
||||
|
||||
let current: &Path = p.as_ref();
|
||||
let (mut dir, last) = if p.ends_with('/') {
|
||||
(current, "")
|
||||
} else {
|
||||
let dir = current.parent().unwrap_or_else(|| "/".as_ref());
|
||||
let last = current
|
||||
.file_name()
|
||||
.and_then(std::ffi::OsStr::to_str)
|
||||
.unwrap_or("");
|
||||
(dir, last)
|
||||
};
|
||||
|
||||
if dir.to_str().unwrap().is_empty() {
|
||||
dir = ".".as_ref();
|
||||
}
|
||||
|
||||
let mut files: Completions<_> = match dir.read_dir() {
|
||||
Ok(files) => files
|
||||
.flatten()
|
||||
.filter_map(|file| {
|
||||
let path = file.path();
|
||||
let is_dir = path.is_dir();
|
||||
match path.into_os_string().into_string() {
|
||||
Ok(s) if is_dir => Some(s + "/"),
|
||||
Ok(s) => Some(s),
|
||||
Err(_) => None,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
Err(_) => {
|
||||
return completions![p];
|
||||
}
|
||||
};
|
||||
|
||||
if files.is_empty() {
|
||||
return completions![p];
|
||||
}
|
||||
|
||||
let fuzzer = SkimMatcherV2::default();
|
||||
files.sort_by_cached_key(|file| fuzzer.fuzzy_match(file, last).unwrap_or(i64::MAX));
|
||||
files
|
||||
}
|
||||
|
||||
// ============================================================================= //
|
||||
|
||||
struct Environment {
|
||||
shell_info: ShellInfo,
|
||||
os_type: os_info::Type,
|
||||
terminal_info: TerminalInfo,
|
||||
os_version: os_info::Version,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TerminalInfo {
|
||||
name: String,
|
||||
version: String,
|
||||
}
|
||||
|
||||
fn get_terminal_info() -> TerminalInfo {
|
||||
let terminal = std::env::var("TERM_PROGRAM")
|
||||
.or_else(|_| std::env::var("LC_TERMINAL"))
|
||||
.unwrap_or_else(|_| UNKNOWN_TERMINAL.to_string());
|
||||
|
||||
let version = std::env::var("TERM_PROGRAM_VERSION")
|
||||
.or_else(|_| std::env::var("LC_TERMINAL_VERSION"))
|
||||
.unwrap_or_else(|_| UNKNOWN_VERSION.to_string());
|
||||
|
||||
TerminalInfo {
|
||||
name: terminal,
|
||||
version,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ShellInfo {
|
||||
name: String,
|
||||
version: String,
|
||||
}
|
||||
|
||||
fn get_shell_info() -> ShellInfo {
|
||||
let failure_shell_info = ShellInfo {
|
||||
name: UNKNOWN_SHELL.to_string(),
|
||||
version: UNKNOWN_VERSION.to_string(),
|
||||
};
|
||||
|
||||
let current_shell = match Shell::from_env() {
|
||||
Some(shell) => shell.to_string(),
|
||||
None => return failure_shell_info,
|
||||
};
|
||||
|
||||
let version = get_shell_version(¤t_shell);
|
||||
|
||||
ShellInfo {
|
||||
version,
|
||||
name: current_shell.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_shell_version(shell: &str) -> String {
|
||||
let time_limit = Duration::from_millis(500);
|
||||
match shell {
|
||||
"powershecll" => {
|
||||
error!("Powershell is not supported.");
|
||||
None
|
||||
}
|
||||
_ => exec_cmd(shell, &["--version"], time_limit),
|
||||
}
|
||||
.map_or_else(
|
||||
|| UNKNOWN_VERSION.to_string(),
|
||||
|output| output.stdout.trim().to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================= //
|
||||
// Git
|
||||
// ============================================================================= //
|
||||
|
||||
#[derive(Debug, Clone, Template, TypedBuilder)]
|
||||
#[template(path = "github_issue.j2", escape = "md")]
|
||||
struct GithubIssueTemplate<'a> {
|
||||
#[builder(setter(into))]
|
||||
bb_version: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
build_rust_channel: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
build_time: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
git_commit_hash: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
os_name: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
os_version: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
pkg_branch_tag: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
recipe: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
rust_channel: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
rust_version: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
shell_name: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
shell_version: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
terminal_name: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
terminal_version: Cow<'a, str>,
|
||||
}
|
||||
|
||||
fn get_pkg_branch_tag() -> String {
|
||||
format!("{} ({})", shadow::BRANCH, shadow::LAST_TAG)
|
||||
}
|
||||
|
||||
fn generate_github_issue(
|
||||
environment: &Environment,
|
||||
recipe: &Option<Recipe>,
|
||||
) -> anyhow::Result<String> {
|
||||
let recipe = recipe
|
||||
.as_ref()
|
||||
.map_or_else(Default::default, |r| print_full_recipe(r));
|
||||
|
||||
let github_template = GithubIssueTemplate::builder()
|
||||
.bb_version(shadow::PKG_VERSION)
|
||||
.build_rust_channel(shadow::BUILD_RUST_CHANNEL)
|
||||
.build_time(shadow::BUILD_TIME)
|
||||
.git_commit_hash(shadow::BB_COMMIT_HASH)
|
||||
.os_name(format!("{}", environment.os_type))
|
||||
.os_version(format!("{}", environment.os_version))
|
||||
.pkg_branch_tag(get_pkg_branch_tag())
|
||||
.recipe(recipe)
|
||||
.rust_channel(shadow::RUST_CHANNEL)
|
||||
.rust_version(shadow::RUST_VERSION)
|
||||
.shell_name(environment.shell_info.name.clone())
|
||||
.shell_version(environment.shell_info.version.clone())
|
||||
.terminal_name(environment.terminal_info.name.clone())
|
||||
.terminal_version(environment.terminal_info.version.clone())
|
||||
.build();
|
||||
|
||||
Ok(github_template.render()?)
|
||||
}
|
||||
|
||||
fn make_github_issue_link(body: &str) -> String {
|
||||
let escaped = urlencoding::encode(body).replace("%20", "+");
|
||||
|
||||
format!(
|
||||
"https://github.com/blue-build/cli/issues/new?template={}&body={}",
|
||||
urlencoding::encode("Bug_report.md"),
|
||||
escaped
|
||||
)
|
||||
.chars()
|
||||
.take(GITHUB_CHAR_LIMIT)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_module_from_file(file_name: &str) -> Result<ModuleExt> {
|
||||
let file_path = PathBuf::from("config").join(file_name);
|
||||
let file_path = if file_path.is_absolute() {
|
||||
file_path
|
||||
} else {
|
||||
std::env::current_dir()?.join(file_path)
|
||||
};
|
||||
|
||||
let file = fs::read_to_string(file_path.clone())?;
|
||||
|
||||
serde_yaml::from_str::<ModuleExt>(file.as_str()).map_or_else(
|
||||
|_| -> Result<ModuleExt> {
|
||||
let module = serde_yaml::from_str::<Module>(file.as_str())?;
|
||||
|
||||
Ok(ModuleExt::builder().modules(vec![module]).build())
|
||||
},
|
||||
Ok,
|
||||
)
|
||||
}
|
||||
|
||||
fn get_modules(modules: &[Module]) -> Vec<Module> {
|
||||
modules
|
||||
.iter()
|
||||
.flat_map(|module| {
|
||||
if let Some(file_name) = &module.from_file {
|
||||
match get_module_from_file(file_name) {
|
||||
Err(e) => {
|
||||
error!("Failed to get module from {file_name}: {e}");
|
||||
vec![]
|
||||
}
|
||||
Ok(module_ext) => get_modules(&module_ext.modules),
|
||||
}
|
||||
} else {
|
||||
vec![module.clone()]
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn print_full_recipe(recipe: &Recipe) -> String {
|
||||
let module_list: Vec<Module> = get_modules(&recipe.modules_ext.modules);
|
||||
|
||||
let recipe = Recipe::builder()
|
||||
.name(recipe.name.as_ref())
|
||||
.description(recipe.description.as_ref())
|
||||
.base_image(recipe.base_image.as_ref())
|
||||
.image_version(recipe.image_version.as_ref())
|
||||
.extra(recipe.extra.clone())
|
||||
.modules_ext(ModuleExt::builder().modules(module_list).build())
|
||||
.build();
|
||||
|
||||
serde_yaml::to_string(&recipe).unwrap_or_else(|e| {
|
||||
error!("Failed to serialize recipe: {e}");
|
||||
format!("Error rendering recipe!!\n{e}")
|
||||
})
|
||||
}
|
||||
// ============================================================================= //
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::env;
|
||||
|
||||
#[test]
|
||||
fn test_make_github_link() {
|
||||
let environment = Environment {
|
||||
os_type: os_info::Type::Linux,
|
||||
os_version: os_info::Version::Semantic(1, 2, 3),
|
||||
shell_info: ShellInfo {
|
||||
version: "2.3.4".to_string(),
|
||||
name: "test_shell".to_string(),
|
||||
},
|
||||
terminal_info: TerminalInfo {
|
||||
name: "test_terminal".to_string(),
|
||||
version: "5.6.7".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
let recipe = Recipe::default();
|
||||
let body = generate_github_issue(&environment, &Some(recipe)).unwrap();
|
||||
let link = make_github_issue_link(&body);
|
||||
|
||||
assert!(link.contains(clap::crate_version!()));
|
||||
assert!(link.contains("Linux"));
|
||||
assert!(link.contains("1.2.3"));
|
||||
assert!(link.contains("test_shell"));
|
||||
assert!(link.contains("2.3.4"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,16 @@
|
|||
#[cfg(feature = "podman-api")]
|
||||
mod build_strategy;
|
||||
|
||||
use std::{
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
process::{self, Command},
|
||||
};
|
||||
use std::{env, fs, path::PathBuf, process::Command};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Args;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use log::{debug, info, trace, warn};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
#[cfg(feature = "podman-api")]
|
||||
use podman_api::{
|
||||
api::Image,
|
||||
opts::{ImageBuildOpts, ImageListOpts, ImagePushOpts, RegistryAuth},
|
||||
opts::{ImageBuildOpts, ImagePushOpts, RegistryAuth},
|
||||
Podman,
|
||||
};
|
||||
|
||||
|
|
@ -183,8 +178,6 @@ impl BlueBuildCommand for BuildCommand {
|
|||
impl BuildCommand {
|
||||
#[cfg(feature = "podman-api")]
|
||||
async fn build_image_podman_api(&self, client: Podman) -> Result<()> {
|
||||
use podman_api::opts::ImageTagOpts;
|
||||
|
||||
trace!("BuildCommand::build_image({client:#?})");
|
||||
|
||||
let credentials = self.get_login_creds();
|
||||
|
|
|
|||
27
src/commands/completions.rs
Normal file
27
src/commands/completions.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use clap::{Args, CommandFactory};
|
||||
use clap_complete::{generate, Shell as CompletionShell};
|
||||
|
||||
use crate::commands::BlueBuildArgs;
|
||||
|
||||
use super::BlueBuildCommand;
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct CompletionsCommand {
|
||||
#[arg(value_enum)]
|
||||
shell: CompletionShell,
|
||||
}
|
||||
|
||||
impl BlueBuildCommand for CompletionsCommand {
|
||||
fn try_run(&mut self) -> anyhow::Result<()> {
|
||||
log::debug!("Generating completions for {}", self.shell);
|
||||
|
||||
generate(
|
||||
self.shell,
|
||||
&mut BlueBuildArgs::command(),
|
||||
"bb",
|
||||
&mut std::io::stdout().lock(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::{self, Command},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::{Args, Subcommand};
|
||||
use log::{debug, error, info, trace};
|
||||
use clap::Args;
|
||||
use log::{debug, info, trace};
|
||||
use typed_builder::TypedBuilder;
|
||||
use users::{Users, UsersCache};
|
||||
|
||||
|
|
|
|||
94
src/commands/mod.rs
Normal file
94
src/commands/mod.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
use log::error;
|
||||
|
||||
use clap::{command, crate_authors, Parser, Subcommand};
|
||||
use clap_verbosity_flag::{InfoLevel, Verbosity};
|
||||
|
||||
pub mod bug_report;
|
||||
pub mod build;
|
||||
pub mod completions;
|
||||
#[cfg(feature = "init")]
|
||||
pub mod init;
|
||||
pub mod local;
|
||||
pub mod template;
|
||||
pub mod utils;
|
||||
|
||||
pub trait BlueBuildCommand {
|
||||
/// Runs the command and returns a result
|
||||
/// of the execution
|
||||
///
|
||||
/// # Errors
|
||||
/// Can return an `anyhow` Error
|
||||
fn try_run(&mut self) -> anyhow::Result<()>;
|
||||
|
||||
/// Runs the command and exits if there is an error.
|
||||
fn run(&mut self) {
|
||||
if let Err(e) = self.try_run() {
|
||||
error!("{e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shadow_rs::shadow!(shadow);
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(
|
||||
name = "BlueBuild",
|
||||
about,
|
||||
long_about = None,
|
||||
author=crate_authors!(),
|
||||
version=shadow::PKG_VERSION,
|
||||
long_version=shadow::CLAP_LONG_VERSION,
|
||||
)]
|
||||
pub struct BlueBuildArgs {
|
||||
#[command(subcommand)]
|
||||
pub command: CommandArgs,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub verbosity: Verbosity<InfoLevel>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum CommandArgs {
|
||||
/// Build an image from a recipe
|
||||
Build(build::BuildCommand),
|
||||
|
||||
/// Generate a Containerfile from a recipe
|
||||
Template(template::TemplateCommand),
|
||||
|
||||
/// Upgrade your current OS with the
|
||||
/// local image saved at `/etc/blue-build/`.
|
||||
///
|
||||
/// This requires having rebased already onto
|
||||
/// a local archive already by using the `rebase`
|
||||
/// subcommand.
|
||||
///
|
||||
/// NOTE: This can only be used if you have `rpm-ostree`
|
||||
/// installed and if the `--push` and `--rebase` option isn't
|
||||
/// used. This image will not be signed.
|
||||
Upgrade(local::UpgradeCommand),
|
||||
|
||||
/// Rebase your current OS onto the image
|
||||
/// being built.
|
||||
///
|
||||
/// This will create a tarball of your image at
|
||||
/// `/etc/blue-build/` and invoke `rpm-ostree` to
|
||||
/// rebase onto the image using `oci-archive`.
|
||||
///
|
||||
/// NOTE: This can only be used if you have `rpm-ostree`
|
||||
/// installed.
|
||||
Rebase(local::RebaseCommand),
|
||||
|
||||
/// Initialize a new Ublue Starting Point repo
|
||||
#[cfg(feature = "init")]
|
||||
Init(init::InitCommand),
|
||||
|
||||
#[cfg(feature = "init")]
|
||||
New(init::NewCommand),
|
||||
|
||||
/// Create a pre-populated GitHub issue with information about your configuration
|
||||
BugReport(bug_report::BugReportCommand),
|
||||
|
||||
/// Generate shell completions for your shell to stdout
|
||||
Completions(completions::CompletionsCommand),
|
||||
}
|
||||
|
|
@ -1,18 +1,13 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
process,
|
||||
};
|
||||
|
||||
use anyhow::{Error, Result};
|
||||
use anyhow::Result;
|
||||
use askama::Template;
|
||||
use chrono::Local;
|
||||
use clap::Args;
|
||||
use indexmap::IndexMap;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Value;
|
||||
use log::{debug, error, info, trace};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use crate::module_recipe::{Module, ModuleExt, Recipe};
|
||||
|
|
@ -22,7 +17,7 @@ use super::BlueBuildCommand;
|
|||
#[derive(Debug, Clone, Template, TypedBuilder)]
|
||||
#[template(path = "Containerfile")]
|
||||
pub struct ContainerFileTemplate<'a> {
|
||||
recipe: &'a Recipe,
|
||||
recipe: &'a Recipe<'a>,
|
||||
recipe_path: &'a Path,
|
||||
|
||||
module_template: ModuleTemplate<'a>,
|
||||
|
|
@ -67,7 +62,7 @@ impl TemplateCommand {
|
|||
trace!("TemplateCommand::template_file()");
|
||||
|
||||
debug!("Deserializing recipe");
|
||||
let recipe_de = serde_yaml::from_str::<Recipe>(fs::read_to_string(&self.recipe)?.as_str())?;
|
||||
let recipe_de = Recipe::parse(&self.recipe)?;
|
||||
trace!("recipe_de: {recipe_de:#?}");
|
||||
|
||||
let template = ContainerFileTemplate::builder()
|
||||
|
|
@ -119,11 +114,11 @@ fn print_script(script_contents: &ExportsTemplate) -> String {
|
|||
|
||||
fn running_gitlab_actions() -> bool {
|
||||
trace!(" running_gitlab_actions()");
|
||||
|
||||
env::var("GITHUB_ACTIONS").is_ok_and(|e| e == "true")
|
||||
}
|
||||
|
||||
fn get_containerfile_list(module: &Module) -> Option<Vec<String>> {
|
||||
#[must_use]
|
||||
pub fn get_containerfile_list(module: &Module) -> Option<Vec<String>> {
|
||||
if module.module_type.as_ref()? == "containerfile" {
|
||||
Some(
|
||||
module
|
||||
|
|
@ -139,8 +134,9 @@ fn get_containerfile_list(module: &Module) -> Option<Vec<String>> {
|
|||
}
|
||||
}
|
||||
|
||||
fn print_containerfile(containerfile: &str) -> String {
|
||||
trace!("print_containerfile({containerfile})");
|
||||
#[must_use]
|
||||
pub fn print_containerfile(containerfile: &str) -> String {
|
||||
debug!("print_containerfile({containerfile})");
|
||||
debug!("Loading containerfile contents for {containerfile}");
|
||||
|
||||
let path = format!("config/containerfiles/{containerfile}/Containerfile");
|
||||
|
|
@ -150,27 +146,20 @@ fn print_containerfile(containerfile: &str) -> String {
|
|||
process::exit(1);
|
||||
});
|
||||
|
||||
trace!("Containerfile contents {path}:\n{file}");
|
||||
debug!("Containerfile contents {path}:\n{file}");
|
||||
|
||||
file
|
||||
}
|
||||
|
||||
fn get_module_from_file(file_name: &str) -> String {
|
||||
trace!("get_module_from_file({file_name})");
|
||||
|
||||
let io_err_fn = |e| {
|
||||
error!("Failed to read module {file_name}: {e}");
|
||||
process::exit(1);
|
||||
};
|
||||
#[must_use]
|
||||
pub fn template_module_from_file(file_name: &str) -> String {
|
||||
debug!("get_module_from_file({file_name})");
|
||||
|
||||
let file_path = PathBuf::from("config").join(file_name);
|
||||
|
||||
let file = fs::read_to_string(file_path).unwrap_or_else(io_err_fn);
|
||||
|
||||
let serde_err_fn = |e| {
|
||||
error!("Failed to deserialize module {file_name}: {e}");
|
||||
let file = fs::read_to_string(file_path).unwrap_or_else(|e| {
|
||||
error!("Failed to read module {file_name}: {e}");
|
||||
process::exit(1);
|
||||
};
|
||||
});
|
||||
|
||||
let template_err_fn = |e| {
|
||||
error!("Failed to render module {file_name}: {e}");
|
||||
|
|
@ -179,7 +168,10 @@ fn get_module_from_file(file_name: &str) -> String {
|
|||
|
||||
serde_yaml::from_str::<ModuleExt>(file.as_str()).map_or_else(
|
||||
|_| {
|
||||
let module = serde_yaml::from_str::<Module>(file.as_str()).unwrap_or_else(serde_err_fn);
|
||||
let module = serde_yaml::from_str::<Module>(file.as_str()).unwrap_or_else(|e| {
|
||||
error!("Failed to deserialize module {file_name}: {e}");
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
ModuleTemplate::builder()
|
||||
.module_ext(&ModuleExt::builder().modules(vec![module]).build())
|
||||
|
|
|
|||
136
src/commands/utils.rs
Normal file
136
src/commands/utils.rs
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
use process_control::{ChildExt, Control};
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::Debug;
|
||||
use std::io::{Error, ErrorKind, Result};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[must_use]
|
||||
pub fn home_dir() -> Option<PathBuf> {
|
||||
directories::BaseDirs::new().map(|base_dirs| base_dirs.home_dir().to_path_buf())
|
||||
}
|
||||
|
||||
// ================================================================================================= //
|
||||
// CommandOutput
|
||||
// ================================================================================================= //
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CommandOutput {
|
||||
pub stdout: String,
|
||||
pub stderr: String,
|
||||
}
|
||||
|
||||
/// # Attempt to resolve `binary_name` from and creates a new `Command` pointing at it
|
||||
/// # This allows executing cmd files on Windows and prevents running executable from cwd on Windows
|
||||
/// # This function also initializes std{err,out,in} to protect against processes changing the console mode
|
||||
/// #
|
||||
/// # Errors
|
||||
///
|
||||
pub fn create_command<T: AsRef<OsStr>>(binary_name: T) -> Result<Command> {
|
||||
let binary_name = binary_name.as_ref();
|
||||
log::trace!("Creating Command for binary {:?}", binary_name);
|
||||
|
||||
let full_path = match which::which(binary_name) {
|
||||
Ok(full_path) => {
|
||||
log::trace!("Using {:?} as {:?}", full_path, binary_name);
|
||||
full_path
|
||||
}
|
||||
Err(error) => {
|
||||
log::trace!("Unable to find {:?} in PATH, {:?}", binary_name, error);
|
||||
return Err(Error::new(ErrorKind::NotFound, error));
|
||||
}
|
||||
};
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
let mut cmd = Command::new(full_path);
|
||||
cmd.stderr(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stdin(Stdio::null());
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Execute a command and return the output on stdout and stderr if successful
|
||||
pub fn exec_cmd<T: AsRef<OsStr> + Debug, U: AsRef<OsStr> + Debug>(
|
||||
cmd: T,
|
||||
args: &[U],
|
||||
time_limit: Duration,
|
||||
) -> Option<CommandOutput> {
|
||||
log::trace!("Executing command {:?} with args {:?}", cmd, args);
|
||||
internal_exec_cmd(cmd, args, time_limit)
|
||||
}
|
||||
|
||||
fn internal_exec_cmd<T: AsRef<OsStr> + Debug, U: AsRef<OsStr> + Debug>(
|
||||
cmd: T,
|
||||
args: &[U],
|
||||
time_limit: Duration,
|
||||
) -> Option<CommandOutput> {
|
||||
let mut cmd = create_command(cmd).ok()?;
|
||||
cmd.args(args);
|
||||
exec_timeout(&mut cmd, time_limit)
|
||||
}
|
||||
|
||||
pub fn exec_timeout(cmd: &mut Command, time_limit: Duration) -> Option<CommandOutput> {
|
||||
let start = Instant::now();
|
||||
let process = match cmd.spawn() {
|
||||
Ok(process) => process,
|
||||
Err(error) => {
|
||||
log::trace!("Unable to run {:?}, {:?}", cmd.get_program(), error);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
match process
|
||||
.controlled_with_output()
|
||||
.time_limit(time_limit)
|
||||
.terminate_for_timeout()
|
||||
.wait()
|
||||
{
|
||||
Ok(Some(output)) => {
|
||||
let stdout_string = match String::from_utf8(output.stdout) {
|
||||
Ok(stdout) => stdout,
|
||||
Err(error) => {
|
||||
log::warn!("Unable to decode stdout: {:?}", error);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let stderr_string = match String::from_utf8(output.stderr) {
|
||||
Ok(stderr) => stderr,
|
||||
Err(error) => {
|
||||
log::warn!("Unable to decode stderr: {:?}", error);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
log::trace!(
|
||||
"stdout: {:?}, stderr: {:?}, exit code: \"{:?}\", took {:?}",
|
||||
stdout_string,
|
||||
stderr_string,
|
||||
output.status.code(),
|
||||
start.elapsed()
|
||||
);
|
||||
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(CommandOutput {
|
||||
stdout: stdout_string,
|
||||
stderr: stderr_string,
|
||||
})
|
||||
}
|
||||
Ok(None) => {
|
||||
log::warn!("Executing command {:?} timed out.", cmd.get_program());
|
||||
log::warn!("You can set command_timeout in your config to a higher value to allow longer-running commands to keep executing.");
|
||||
None
|
||||
}
|
||||
Err(error) => {
|
||||
log::trace!(
|
||||
"Executing command {:?} failed by: {:?}",
|
||||
cmd.get_program(),
|
||||
error
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/lib.rs
11
src/lib.rs
|
|
@ -1,19 +1,14 @@
|
|||
//! The root library for blue-build.
|
||||
#![warn(
|
||||
clippy::correctness,
|
||||
clippy::suspicious,
|
||||
clippy::perf,
|
||||
clippy::style,
|
||||
clippy::pedantic
|
||||
)]
|
||||
#![warn(clippy::correctness, clippy::suspicious, clippy::perf, clippy::style)]
|
||||
#![doc(
|
||||
html_logo_url = "https://gitlab.com/wunker-bunker/blue-build/-/raw/main/logos/BlueBuild-logo.png"
|
||||
)]
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![forbid(unsafe_code)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
|
||||
shadow_rs::shadow!(shadow);
|
||||
|
||||
pub mod commands;
|
||||
pub mod module_recipe;
|
||||
mod ops;
|
||||
|
|
|
|||
|
|
@ -1,32 +1,31 @@
|
|||
use std::{env, fs, path::PathBuf, process};
|
||||
use std::{borrow::Cow, env, fs, path::Path, process};
|
||||
|
||||
use askama::Template;
|
||||
use chrono::Local;
|
||||
use indexmap::IndexMap;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use log::{debug, error, trace, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Value;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
#[derive(Serialize, Clone, Deserialize, Debug, TypedBuilder)]
|
||||
pub struct Recipe {
|
||||
#[derive(Default, Serialize, Clone, Deserialize, Debug, TypedBuilder)]
|
||||
pub struct Recipe<'a> {
|
||||
#[builder(setter(into))]
|
||||
pub name: String,
|
||||
pub name: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
pub description: String,
|
||||
pub description: Cow<'a, str>,
|
||||
|
||||
#[serde(alias = "base-image")]
|
||||
#[builder(setter(into))]
|
||||
pub base_image: String,
|
||||
pub base_image: Cow<'a, str>,
|
||||
|
||||
#[serde(alias = "image-version")]
|
||||
#[builder(setter(into))]
|
||||
pub image_version: String,
|
||||
pub image_version: Cow<'a, str>,
|
||||
|
||||
#[serde(alias = "blue-build-tag")]
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub blue_build_tag: Option<String>,
|
||||
pub blue_build_tag: Option<Cow<'a, str>>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub modules_ext: ModuleExt,
|
||||
|
|
@ -36,11 +35,11 @@ pub struct Recipe {
|
|||
pub extra: IndexMap<String, Value>,
|
||||
}
|
||||
|
||||
impl Recipe {
|
||||
impl<'a> Recipe<'a> {
|
||||
#[must_use]
|
||||
pub fn generate_tags(&self) -> Vec<String> {
|
||||
trace!("Recipe::generate_tags()");
|
||||
debug!("Generating image tags for {}", &self.name);
|
||||
trace!("Generating image tags for {}", &self.name);
|
||||
|
||||
let mut tags: Vec<String> = Vec::new();
|
||||
let image_version = &self.image_version;
|
||||
|
|
@ -95,7 +94,7 @@ impl Recipe {
|
|||
debug!("Running in a PR");
|
||||
tags.push(format!("pr-{github_event_number}-{image_version}"));
|
||||
} else if github_ref_name == "live" {
|
||||
tags.push(image_version.to_owned());
|
||||
tags.push(image_version.to_string());
|
||||
tags.push(format!("{image_version}-{timestamp}"));
|
||||
tags.push("latest".to_string());
|
||||
} else {
|
||||
|
|
@ -106,13 +105,40 @@ impl Recipe {
|
|||
warn!("Running locally");
|
||||
tags.push(format!("{image_version}-local"));
|
||||
}
|
||||
info!("Finished generating tags!");
|
||||
debug!("Finished generating tags!");
|
||||
debug!("Tags: {tags:#?}");
|
||||
tags
|
||||
}
|
||||
|
||||
/// # Parse a recipe file
|
||||
/// #
|
||||
/// # Errors
|
||||
pub fn parse<P: AsRef<Path>>(path: &P) -> anyhow::Result<Self> {
|
||||
let file_path = if Path::new(path.as_ref()).is_absolute() {
|
||||
path.as_ref().to_path_buf()
|
||||
} else {
|
||||
std::env::current_dir()?.join(path.as_ref())
|
||||
};
|
||||
|
||||
let recipe_path = fs::canonicalize(file_path)?;
|
||||
let recipe_path_string = recipe_path.display().to_string();
|
||||
debug!("Recipe::parse_recipe({recipe_path_string})");
|
||||
|
||||
let file = fs::read_to_string(recipe_path).unwrap_or_else(|e| {
|
||||
error!("Failed to read file {recipe_path_string}: {e}");
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
debug!("Recipe contents: {file}");
|
||||
|
||||
serde_yaml::from_str::<Recipe>(file.as_str()).map_err(|e| {
|
||||
error!("Failed to parse recipe {recipe_path_string}: {e}");
|
||||
process::exit(1);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Deserialize, Debug, TypedBuilder)]
|
||||
#[derive(Default, Serialize, Clone, Deserialize, Debug, TypedBuilder)]
|
||||
pub struct ModuleExt {
|
||||
#[builder(default, setter(into))]
|
||||
pub modules: Vec<Module>,
|
||||
|
|
@ -120,19 +146,15 @@ pub struct ModuleExt {
|
|||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, TypedBuilder)]
|
||||
pub struct Module {
|
||||
#[serde(rename = "type")]
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
|
||||
pub module_type: Option<String>,
|
||||
|
||||
#[serde(rename = "from-file")]
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
#[serde(rename = "from-file", skip_serializing_if = "Option::is_none")]
|
||||
pub from_file: Option<String>,
|
||||
|
||||
#[serde(flatten)]
|
||||
#[builder(default, setter(into))]
|
||||
pub config: IndexMap<String, Value>,
|
||||
}
|
||||
|
||||
// ======================================================== //
|
||||
// ========================= Helpers ====================== //
|
||||
// ======================================================== //
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use log::{debug, trace};
|
||||
use std::{path::Path, process::Command};
|
||||
use std::process::Command;
|
||||
|
||||
pub const LOCAL_BUILD: &str = "/etc/blue-build";
|
||||
pub const ARCHIVE_SUFFIX: &str = "tar.gz";
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
RUN chmod +x /tmp/modules/{{ type }}/{{ type }}.sh && source /tmp/exports.sh && /tmp/modules/{{ type }}/{{ type }}.sh '{{ self::print_module_context(module) }}'
|
||||
{%- endif %}
|
||||
{%- else if let Some(from_file) = module.from_file %}
|
||||
{{ self::get_module_from_file(from_file) }}
|
||||
{{ self::template_module_from_file(from_file) }}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
|
||||
|
|
|
|||
35
templates/github_issue.j2
Normal file
35
templates/github_issue.j2
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#### Current Behavior
|
||||
<!-- A clear and concise description of the behavior. -->
|
||||
|
||||
#### Expected Behavior
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
#### Additional context/Screenshots
|
||||
<!-- Add any other context about the problem here. If applicable, add screenshots to help explain. -->
|
||||
|
||||
#### Possible Solution
|
||||
<!--- Only if you have suggestions on a fix for the bug -->
|
||||
|
||||
#### Environment
|
||||
- Blue Build Version: {{ bb_version }}
|
||||
- Operating system: {{ os_name }} {{ os_version }}
|
||||
- Branch/Tag: {{ pkg_branch_tag }}
|
||||
- Git Commit Hash: {{ git_commit_hash }}
|
||||
|
||||
#### Shell
|
||||
- Name: {{ shell_name }}
|
||||
- Version: {{ shell_version }}
|
||||
- Terminal emulator: {{ terminal_name }} {{ terminal_version }}
|
||||
|
||||
#### Rust
|
||||
- Rust Version: {{ rust_version }}
|
||||
- Rust channel: {{ rust_channel }} {{ build_rust_channel }}
|
||||
- Build Time: {{ build_time }}
|
||||
|
||||
{%- if !recipe.is_empty() %}
|
||||
|
||||
#### Recipe:
|
||||
```yml
|
||||
{{ recipe }}
|
||||
```
|
||||
{%- endif %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue