refactor: Move templates to their own crate (#83)

This PR logically separates out parts of the code to their own crates. This will be useful for future Tauri App development.
This commit is contained in:
Gerald Pinder 2024-02-25 15:45:33 -05:00 committed by GitHub
parent ce8f889dc2
commit 910e0434b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 620 additions and 512 deletions

120
Cargo.lock generated
View file

@ -130,9 +130,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.79"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
[[package]]
name = "askama"
@ -147,7 +147,7 @@ dependencies = [
"percent-encoding",
"serde",
"serde_json",
"serde_yaml 0.9.31",
"serde_yaml 0.9.32",
]
[[package]]
@ -281,31 +281,27 @@ name = "blue-build"
version = "0.8.0"
dependencies = [
"anyhow",
"askama",
"chrono",
"blue-build-recipe",
"blue-build-template",
"blue-build-utils",
"clap",
"clap-verbosity-flag",
"clap_complete",
"clap_complete_nushell",
"colorized",
"derive_builder",
"directories",
"dunce",
"env_logger",
"format_serde_error",
"futures-util",
"fuzzy-matcher",
"indexmap 2.2.3",
"log",
"open",
"os_info",
"podman-api",
"process_control",
"requestty",
"rusty-hook",
"serde",
"serde_json",
"serde_yaml 0.9.31",
"serde_yaml 0.9.32",
"shadow-rs",
"signal-hook",
"signal-hook-tokio",
@ -315,6 +311,51 @@ dependencies = [
"urlencoding",
"users",
"uuid",
]
[[package]]
name = "blue-build-recipe"
version = "0.8.0"
dependencies = [
"anyhow",
"blue-build-utils",
"chrono",
"format_serde_error",
"indexmap 2.2.3",
"log",
"serde",
"serde_json",
"serde_yaml 0.9.32",
"typed-builder",
]
[[package]]
name = "blue-build-template"
version = "0.8.0"
dependencies = [
"askama",
"blue-build-recipe",
"blue-build-utils",
"log",
"serde",
"serde_json",
"serde_yaml 0.9.32",
"typed-builder",
"uuid",
]
[[package]]
name = "blue-build-utils"
version = "0.8.0"
dependencies = [
"anyhow",
"directories",
"format_serde_error",
"log",
"process_control",
"serde",
"serde_json",
"serde_yaml 0.9.32",
"which",
]
@ -649,21 +690,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
name = "crossterm"
version = "0.25.0"
@ -859,37 +885,6 @@ dependencies = [
"serde",
]
[[package]]
name = "derive_builder"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f59169f400d8087f238c5c0c7db6a28af18681717f3b623227d92f397e938c7"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4ec317cc3e7ef0928b0ca6e4a634a4d6c001672ae210438cf114a83e56b018d"
dependencies = [
"darling 0.14.4",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "derive_builder_macro"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "870368c3fb35b8031abb378861d4460f573b92238ec2152c927a21f77e3e0127"
dependencies = [
"derive_builder_core",
"syn 1.0.109",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -2518,14 +2513,13 @@ dependencies = [
[[package]]
name = "process_control"
version = "4.0.3"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e056a69288d0a211f4c74c48391c6eb86e714fdcb9dc58a9f34302da9c20bf"
checksum = "4d18334c4a4b2770ee894e63cf466d5a9ea449cf29e321101b0b135a747afb6f"
dependencies = [
"crossbeam-channel",
"libc",
"signal-hook",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@ -3073,9 +3067,9 @@ dependencies = [
[[package]]
name = "serde_yaml"
version = "0.9.31"
version = "0.9.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adf8a49373e98a4c5f0ceb5d05aa7c648d75f63774981ed95b7c7443bbd50c6e"
checksum = "8fd075d994154d4a774f95b51fb96bdc2832b0ea48425c92546073816cda1f2f"
dependencies = [
"indexmap 2.2.3",
"itoa",

View file

@ -1,51 +1,80 @@
[package]
name = "blue-build"
[workspace]
members = [ "utils", "recipe","template"]
[workspace.package]
version = "0.8.0"
edition = "2021"
description = "A CLI tool built for creating Containerfile templates based on the Ublue Community Project"
edition = "2021"
repository = "https://github.com/blue-build/cli"
license = "Apache-2.0"
categories = ["command-line-utilities"]
[workspace.dependencies]
anyhow = "1"
format_serde_error = "0.3.0"
log = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9.30"
typed-builder = "0.18.1"
uuid = { version = "1.7.0", features = ["v4"] }
[workspace.lints.rust]
unsafe_code = "forbid"
[workspace.lints.clippy]
correctness = "warn"
suspicious = "warn"
perf = "warn"
style = "warn"
nursery = "warn"
[package]
name = "blue-build"
build = "build.rs"
version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
license.workspace = true
[dependencies]
anyhow = "1"
askama = { version = "0.12", features = ["serde-json", "serde-yaml"] }
chrono = "0.4"
blue-build-recipe = { path = "./recipe" }
blue-build-template = { path = "./template" }
blue-build-utils = { path = "./utils" }
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"
format_serde_error = "0.3.0"
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
os_info = "3.7"
podman-api = { version = "0.10.0", optional = true }
process_control = { version = "4.0.3", features = ["crossbeam-channel"] }
os_info = "3.7" # update os module config and tests when upgrading os_info
requestty = { version = "0.5", features = ["macros", "termion"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9.30"
shadow-rs = { version = "0.26" }
urlencoding = "2.1.3"
users = "0.11.0"
# Optional Dependencies
futures-util = { version = "0.3", optional = true }
podman-api = { version = "0.10.0", optional = true }
signal-hook = { version = "0.3.17", optional = true }
signal-hook-tokio = { version = "0.3.1", features = [
"futures-v0_3",
], optional = true }
shadow-rs = { version = "0.26" }
sigstore = { version = "0.8.0", optional = true }
tokio = { version = "1", features = ["full"], optional = true }
typed-builder = "0.18.1"
urlencoding = "2.1.3"
users = "0.11.0"
uuid = { version = "1.7.0", features = ["v4"] }
which = "6"
# Workspace dependencies
anyhow.workspace = true
log.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
typed-builder.workspace = true
uuid.workspace = true
[features]
default = []
@ -66,6 +95,9 @@ rusty-hook = "0.11.2"
shadow-rs = { version = "0.26.1", default-features = false }
dunce = "1.0.4"
[lints]
workspace = true
[profile.release]
lto = true
codegen-units = 1

View file

@ -54,7 +54,7 @@ common:
FROM ghcr.io/blue-build/earthly-lib/cargo-builder
WORKDIR /app
COPY --keep-ts --dir src/ templates/ /app
COPY --keep-ts --dir src/ template/ recipe/ utils/ /app
COPY --keep-ts Cargo.* /app
COPY --keep-ts *.md /app
COPY --keep-ts LICENSE /app

26
recipe/Cargo.toml Normal file
View file

@ -0,0 +1,26 @@
[package]
name = "blue-build-recipe"
version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
blue-build-utils = { path = "../utils" }
chrono = "0.4"
indexmap = { version = "2", features = ["serde"] }
anyhow.workspace = true
format_serde_error.workspace = true
log.workspace = true
serde.workspace = true
serde_yaml.workspace = true
serde_json.workspace = true
typed-builder.workspace = true
[lints]
workspace = true

View file

@ -0,0 +1,23 @@
use serde::Deserialize;
use serde_json::Value;
use std::collections::HashMap;
#[derive(Deserialize, Debug, Clone)]
pub struct ImageInspection {
#[serde(alias = "Labels")]
labels: HashMap<String, Value>,
}
impl ImageInspection {
pub fn get_version(&self) -> Option<String> {
Some(
self.labels
.get("org.opencontainers.image.version")?
.as_str()
.map(std::string::ToString::to_string)?
.split('.')
.take(1)
.collect(),
)
}
}

11
recipe/src/lib.rs Normal file
View file

@ -0,0 +1,11 @@
pub mod akmods_info;
pub mod image_inspection;
pub mod module;
pub mod module_ext;
pub mod recipe;
pub use akmods_info::*;
pub use image_inspection::*;
pub use module::*;
pub use module_ext::*;
pub use recipe::*;

133
recipe/src/module.rs Normal file
View file

@ -0,0 +1,133 @@
use std::{borrow::Cow, process};
use indexmap::IndexMap;
use log::{error, trace};
use serde::{Deserialize, Serialize};
use serde_yaml::Value;
use typed_builder::TypedBuilder;
use crate::{AkmodsInfo, ModuleExt};
#[derive(Serialize, Deserialize, Debug, Clone, TypedBuilder)]
pub struct Module<'a> {
#[builder(default, setter(into, strip_option))]
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub module_type: Option<Cow<'a, str>>,
#[builder(default, setter(into, strip_option))]
#[serde(rename = "from-file", skip_serializing_if = "Option::is_none")]
pub from_file: Option<Cow<'a, str>>,
#[builder(default, setter(into, strip_option))]
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<Cow<'a, str>>,
#[serde(flatten)]
#[builder(default, setter(into))]
pub config: IndexMap<String, Value>,
}
impl<'a> Module<'a> {
#[must_use]
pub fn get_modules(modules: &[Self]) -> Vec<Self> {
modules
.iter()
.flat_map(|module| {
module.from_file.as_ref().map_or_else(
|| vec![module.clone()],
|file_name| match ModuleExt::parse_module_from_file(file_name) {
Err(e) => {
error!("Failed to get module from {file_name}: {e}");
vec![]
}
Ok(module_ext) => Self::get_modules(&module_ext.modules),
},
)
})
.collect()
}
#[must_use]
pub fn get_module_type_list(&'a self, typ: &str, list_key: &str) -> Option<Vec<String>> {
if self.module_type.as_ref()? == typ {
Some(
self.config
.get(list_key)?
.as_sequence()?
.iter()
.filter_map(|t| Some(t.as_str()?.to_owned()))
.collect(),
)
} else {
None
}
}
#[must_use]
pub fn get_containerfile_list(&'a self) -> Option<Vec<String>> {
self.get_module_type_list("containerfile", "containerfiles")
}
#[must_use]
pub fn get_containerfile_snippets(&'a self) -> Option<Vec<String>> {
self.get_module_type_list("containerfile", "snippets")
}
pub fn print_module_context(&'a self) -> String {
serde_json::to_string(self).unwrap_or_else(|e| {
error!("Failed to parse module!!!!!: {e}");
process::exit(1);
})
}
pub fn get_files_list(&'a self) -> Option<Vec<(String, String)>> {
Some(
self.config
.get("files")?
.as_sequence()?
.iter()
.filter_map(|entry| entry.as_mapping())
.flatten()
.filter_map(|(src, dest)| {
Some((
format!("./config/files/{}", src.as_str()?),
dest.as_str()?.to_string(),
))
})
.collect(),
)
}
pub fn generate_akmods_info(&'a self, os_version: &str) -> AkmodsInfo {
trace!("generate_akmods_base({self:#?}, {os_version})");
let base = self
.config
.get("base")
.map(|b| b.as_str().unwrap_or_default());
let nvidia_version = self
.config
.get("nvidia-version")
.map(|v| v.as_u64().unwrap_or_default());
AkmodsInfo::builder()
.images(match (base, nvidia_version) {
(Some(b), Some(nv)) if !b.is_empty() && nv > 0 => (
format!("akmods:{b}-{os_version}"),
Some(format!("akmods-nvidia:{b}-{os_version}-{nv}")),
),
(Some(b), _) if !b.is_empty() => (format!("akmods:{b}-{os_version}"), None),
(_, Some(nv)) if nv > 0 => (
format!("akmods:main-{os_version}"),
Some(format!("akmods-nvidia:main-{os_version}")),
),
_ => (format!("akmods:main-{os_version}"), None),
})
.stage_name(format!(
"{}{}",
base.unwrap_or("main"),
nvidia_version.map_or_else(String::default, |nv| format!("-{nv}"))
))
.build()
}
}

54
recipe/src/module_ext.rs Normal file
View file

@ -0,0 +1,54 @@
use std::{borrow::Cow, collections::HashSet, fs, path::PathBuf};
use anyhow::Result;
use log::trace;
use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder;
use crate::{AkmodsInfo, Module};
#[derive(Default, Serialize, Clone, Deserialize, Debug, TypedBuilder)]
pub struct ModuleExt<'a> {
#[builder(default, setter(into))]
pub modules: Cow<'a, [Module<'a>]>,
}
impl ModuleExt<'_> {
/// # Parse a module file returning a [`ModuleExt`]
///
/// # Errors
/// Can return an `anyhow` Error if the file cannot be read or deserialized
/// into a [`ModuleExt`]
pub fn parse_module_from_file(file_name: &str) -> Result<Self> {
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)?;
serde_yaml::from_str::<Self>(&file).map_or_else(
|_| -> Result<Self> {
let module = serde_yaml::from_str::<Module>(&file)
.map_err(blue_build_utils::serde_yaml_err(&file))?;
Ok(Self::builder().modules(vec![module]).build())
},
Ok,
)
}
pub fn get_akmods_info_list(&self, os_version: &str) -> Vec<AkmodsInfo> {
trace!("get_akmods_image_list({self:#?}, {os_version})");
let mut seen = HashSet::new();
self.modules
.iter()
.filter(|module| module.module_type.as_ref().is_some_and(|t| t == "akmods"))
.map(|module| module.generate_akmods_info(os_version))
.filter(|image| seen.insert(image.clone()))
.collect()
}
}

View file

@ -1,26 +1,16 @@
use std::{
borrow::Cow,
collections::{HashMap, HashSet},
env, fs,
path::{Path, PathBuf},
process::{self, Command},
};
use std::{borrow::Cow, env, fs, path::Path, process::Command};
use anyhow::Result;
use blue_build_utils::constants::*;
use chrono::Local;
use format_serde_error::SerdeError;
use indexmap::IndexMap;
use log::{debug, error, info, trace, warn};
use log::{debug, info, trace, warn};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use serde_yaml::Value;
use typed_builder::TypedBuilder;
use crate::{
akmods_info::AkmodsInfo,
constants::*,
ops::{self, check_command_exists},
};
use crate::{ImageInspection, Module, ModuleExt};
#[derive(Default, Serialize, Clone, Deserialize, Debug, TypedBuilder)]
pub struct Recipe<'a> {
@ -146,8 +136,8 @@ impl<'a> Recipe<'a> {
debug!("Recipe contents: {file}");
let mut recipe =
serde_yaml::from_str::<Recipe>(&file).map_err(ops::serde_yaml_err(&file))?;
let mut recipe = serde_yaml::from_str::<Recipe>(&file)
.map_err(blue_build_utils::serde_yaml_err(&file))?;
recipe.modules_ext.modules = Module::get_modules(&recipe.modules_ext.modules).into();
@ -157,7 +147,7 @@ impl<'a> Recipe<'a> {
pub fn get_os_version(&self) -> String {
trace!("Recipe::get_os_version()");
if check_command_exists("skopeo").is_err() {
if blue_build_utils::check_command_exists("skopeo").is_err() {
warn!("The 'skopeo' command doesn't exist, falling back to version defined in recipe");
return self.image_version.to_string();
}
@ -205,193 +195,3 @@ impl<'a> Recipe<'a> {
})
}
}
#[derive(Default, Serialize, Clone, Deserialize, Debug, TypedBuilder)]
pub struct ModuleExt<'a> {
#[builder(default, setter(into))]
pub modules: Cow<'a, [Module<'a>]>,
}
impl ModuleExt<'_> {
/// # Parse a module file returning a [`ModuleExt`]
///
/// # Errors
/// Can return an `anyhow` Error if the file cannot be read or deserialized
/// into a [`ModuleExt`]
pub fn parse_module_from_file(file_name: &str) -> Result<Self> {
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)?;
serde_yaml::from_str::<Self>(&file).map_or_else(
|_| -> Result<Self> {
let module =
serde_yaml::from_str::<Module>(&file).map_err(ops::serde_yaml_err(&file))?;
Ok(Self::builder().modules(vec![module]).build())
},
Ok,
)
}
pub fn get_akmods_info_list(&self, os_version: &str) -> Vec<AkmodsInfo> {
trace!("get_akmods_image_list({self:#?}, {os_version})");
let mut seen = HashSet::new();
self.modules
.iter()
.filter(|module| module.module_type.as_ref().is_some_and(|t| t == "akmods"))
.map(|module| module.generate_akmods_info(os_version))
.filter(|image| seen.insert(image.clone()))
.collect()
}
}
#[derive(Serialize, Deserialize, Debug, Clone, TypedBuilder)]
pub struct Module<'a> {
#[builder(default, setter(into, strip_option))]
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub module_type: Option<Cow<'a, str>>,
#[builder(default, setter(into, strip_option))]
#[serde(rename = "from-file", skip_serializing_if = "Option::is_none")]
pub from_file: Option<Cow<'a, str>>,
#[builder(default, setter(into, strip_option))]
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<Cow<'a, str>>,
#[serde(flatten)]
#[builder(default, setter(into))]
pub config: IndexMap<String, Value>,
}
impl<'a> Module<'a> {
#[must_use]
pub fn get_modules(modules: &[Self]) -> Vec<Self> {
modules
.iter()
.flat_map(|module| {
module.from_file.as_ref().map_or_else(
|| vec![module.clone()],
|file_name| match ModuleExt::parse_module_from_file(file_name) {
Err(e) => {
error!("Failed to get module from {file_name}: {e}");
vec![]
}
Ok(module_ext) => Self::get_modules(&module_ext.modules),
},
)
})
.collect()
}
#[must_use]
pub fn get_module_type_list(&'a self, typ: &str, list_key: &str) -> Option<Vec<String>> {
if self.module_type.as_ref()? == typ {
Some(
self.config
.get(list_key)?
.as_sequence()?
.iter()
.filter_map(|t| Some(t.as_str()?.to_owned()))
.collect(),
)
} else {
None
}
}
#[must_use]
pub fn get_containerfile_list(&'a self) -> Option<Vec<String>> {
self.get_module_type_list("containerfile", "containerfiles")
}
#[must_use]
pub fn get_containerfile_snippets(&'a self) -> Option<Vec<String>> {
self.get_module_type_list("containerfile", "snippets")
}
pub fn print_module_context(&'a self) -> String {
serde_json::to_string(self).unwrap_or_else(|e| {
error!("Failed to parse module!!!!!: {e}");
process::exit(1);
})
}
pub fn get_files_list(&'a self) -> Option<Vec<(String, String)>> {
Some(
self.config
.get("files")?
.as_sequence()?
.iter()
.filter_map(|entry| entry.as_mapping())
.flatten()
.filter_map(|(src, dest)| {
Some((
format!("./config/files/{}", src.as_str()?),
dest.as_str()?.to_string(),
))
})
.collect(),
)
}
pub fn generate_akmods_info(&'a self, os_version: &str) -> AkmodsInfo {
trace!("generate_akmods_base({self:#?}, {os_version})");
let base = self
.config
.get("base")
.map(|b| b.as_str().unwrap_or_default());
let nvidia_version = self
.config
.get("nvidia-version")
.map(|v| v.as_u64().unwrap_or_default());
AkmodsInfo::builder()
.images(match (base, nvidia_version) {
(Some(b), Some(nv)) if !b.is_empty() && nv > 0 => (
format!("akmods:{b}-{os_version}"),
Some(format!("akmods-nvidia:{b}-{os_version}-{nv}")),
),
(Some(b), _) if !b.is_empty() => (format!("akmods:{b}-{os_version}"), None),
(_, Some(nv)) if nv > 0 => (
format!("akmods:main-{os_version}"),
Some(format!("akmods-nvidia:main-{os_version}")),
),
_ => (format!("akmods:main-{os_version}"), None),
})
.stage_name(format!(
"{}{}",
base.unwrap_or("main"),
nvidia_version.map_or_else(String::default, |nv| format!("-{nv}"))
))
.build()
}
}
#[derive(Deserialize, Debug, Clone)]
struct ImageInspection {
#[serde(alias = "Labels")]
labels: HashMap<String, JsonValue>,
}
impl ImageInspection {
pub fn get_version(&self) -> Option<String> {
Some(
self.labels
.get("org.opencontainers.image.version")?
.as_str()
.map(std::string::ToString::to_string)?
.split('.')
.take(1)
.collect(),
)
}
}

View file

@ -3,6 +3,8 @@ use log::error;
use clap::{command, crate_authors, Parser, Subcommand};
use clap_verbosity_flag::{InfoLevel, Verbosity};
use crate::shadow;
pub mod bug_report;
pub mod build;
pub mod completions;
@ -10,7 +12,6 @@ pub mod completions;
pub mod init;
pub mod local;
pub mod template;
pub mod utils;
pub trait BlueBuildCommand {
/// Runs the command and returns a result
@ -29,8 +30,6 @@ pub trait BlueBuildCommand {
}
}
shadow_rs::shadow!(shadow);
#[derive(Parser, Debug)]
#[clap(
name = "BlueBuild",

View file

@ -1,17 +1,17 @@
use askama::Template;
use blue_build_recipe::Recipe;
use blue_build_template::{GithubIssueTemplate, Template};
use blue_build_utils::constants::*;
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::time::Duration;
use typed_builder::TypedBuilder;
use super::utils::exec_cmd;
use super::BlueBuildCommand;
use crate::{constants::*, module_recipe::Recipe, shadow};
use crate::shadow;
#[derive(Default, Debug, Clone, TypedBuilder, Args)]
pub struct BugReportRecipe {
@ -272,7 +272,7 @@ fn get_shell_version(shell: &str) -> String {
error!("Powershell is not supported.");
None
}
_ => exec_cmd(shell, &["--version"], time_limit),
_ => blue_build_utils::exec_cmd(shell, &["--version"], time_limit),
}
.map_or_else(
|| UNKNOWN_VERSION.to_string(),
@ -284,52 +284,6 @@ fn get_shell_version(shell: &str) -> 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)
}

View file

@ -8,6 +8,8 @@ use std::{
};
use anyhow::{anyhow, bail, Result};
use blue_build_recipe::Recipe;
use blue_build_utils::constants::*;
use clap::Args;
use colorized::{Color, Colors};
use log::{debug, info, trace, warn};
@ -35,12 +37,7 @@ use tokio::{
sync::oneshot::{self, Sender},
};
use crate::{
commands::template::TemplateCommand,
constants::{self, *},
module_recipe::Recipe,
ops,
};
use crate::commands::template::TemplateCommand;
use super::BlueBuildCommand;
@ -181,19 +178,19 @@ impl BlueBuildCommand for BuildCommand {
// -> If it does => *Ask* to add to .gitignore and remove from git
// -> If it doesn't => *Ask* to continue and override the file
let container_file_path = Path::new(constants::CONTAINER_FILE);
let container_file_path = Path::new(CONTAINER_FILE);
if !self.force && container_file_path.exists() {
let gitignore = fs::read_to_string(constants::GITIGNORE_PATH)?;
let gitignore = fs::read_to_string(GITIGNORE_PATH)?;
let is_ignored = gitignore
.lines()
.any(|line: &str| line.contains(constants::CONTAINER_FILE));
.any(|line: &str| line.contains(CONTAINER_FILE));
if !is_ignored {
let containerfile = fs::read_to_string(container_file_path)?;
let has_label = containerfile.lines().any(|line| {
let label = format!("LABEL {}", constants::BUILD_ID_LABEL);
let label = format!("LABEL {}", BUILD_ID_LABEL);
line.to_string().trim().starts_with(&label)
});
@ -211,9 +208,9 @@ impl BlueBuildCommand for BuildCommand {
if let Ok(answer) = requestty::prompt_one(question) {
if answer.as_bool().unwrap_or(false) {
ops::append_to_file(
constants::GITIGNORE_PATH,
&format!("/{}", constants::CONTAINER_FILE),
blue_build_utils::append_to_file(
GITIGNORE_PATH,
&format!("/{}", CONTAINER_FILE),
)?;
}
}
@ -231,15 +228,15 @@ impl BlueBuildCommand for BuildCommand {
.unwrap_or_else(|| PathBuf::from(RECIPE_PATH));
#[cfg(not(feature = "podman-api"))]
if let Err(e1) = ops::check_command_exists("buildah") {
ops::check_command_exists("podman").map_err(|e2| {
if let Err(e1) = blue_build_utils::check_command_exists("buildah") {
blue_build_utils::check_command_exists("podman").map_err(|e2| {
anyhow!("Need either 'buildah' or 'podman' commands to proceed: {e1}, {e2}")
})?;
}
if self.push {
ops::check_command_exists("cosign")?;
ops::check_command_exists("skopeo")?;
blue_build_utils::check_command_exists("cosign")?;
blue_build_utils::check_command_exists("skopeo")?;
check_cosign_files()?;
}
@ -420,8 +417,8 @@ impl BuildCommand {
info!("Logging into the registry, {registry}");
let login_output = match (
ops::check_command_exists("buildah"),
ops::check_command_exists("podman"),
blue_build_utils::check_command_exists("buildah"),
blue_build_utils::check_command_exists("podman"),
) {
(Ok(()), _) => {
trace!("buildah login -u {username} -p [MASKED] {registry}");
@ -550,8 +547,8 @@ impl BuildCommand {
info!("Building image {full_image}");
let status = match (
ops::check_command_exists("buildah"),
ops::check_command_exists("podman"),
blue_build_utils::check_command_exists("buildah"),
blue_build_utils::check_command_exists("podman"),
) {
(Ok(()), _) => {
trace!("buildah build -t {full_image}");
@ -588,7 +585,7 @@ impl BuildCommand {
let retry_count = if retry { self.retry_count } else { 0 };
// Push images with retries (1s delay between retries)
ops::retry(retry_count, 1000, || push_images(tags, image_name))?;
blue_build_utils::retry(retry_count, 1000, || push_images(tags, image_name))?;
sign_images(image_name, tags.first().map(String::as_str))?;
}
@ -963,8 +960,8 @@ fn tag_images(tags: &[String], image_name: &str, full_image: &str) -> Result<()>
let tag_image = format!("{image_name}:{tag}");
let status = match (
ops::check_command_exists("buildah"),
ops::check_command_exists("podman"),
blue_build_utils::check_command_exists("buildah"),
blue_build_utils::check_command_exists("podman"),
) {
(Ok(()), _) => {
trace!("buildah tag {full_image} {tag_image}");
@ -1001,8 +998,8 @@ fn push_images(tags: &[String], image_name: &str) -> Result<()> {
let tag_image = format!("{image_name}:{tag}");
let status = match (
ops::check_command_exists("buildah"),
ops::check_command_exists("podman"),
blue_build_utils::check_command_exists("buildah"),
blue_build_utils::check_command_exists("podman"),
) {
(Ok(()), _) => {
trace!("buildah push {tag_image}");

View file

@ -4,10 +4,9 @@ use std::{
};
use anyhow::{bail, Result};
use blue_build_utils::constants::*;
use log::trace;
use crate::{constants::*, ops};
#[cfg(feature = "podman-api")]
#[derive(Debug, Clone, Default)]
pub enum BuildStrategy {
@ -29,8 +28,8 @@ impl BuildStrategy {
PathBuf::from(RUN_PODMAN_SOCK),
PathBuf::from(VAR_RUN_PODMAN_PODMAN_SOCK),
PathBuf::from(VAR_RUN_PODMAN_SOCK),
ops::check_command_exists("buildah"),
ops::check_command_exists("podman"),
blue_build_utils::check_command_exists("buildah"),
blue_build_utils::check_command_exists("podman"),
) {
(Ok(xdg_runtime), _, _, _, _, _)
if Path::new(&format!("{xdg_runtime}/podman/podman.sock")).exists() =>

View file

@ -5,17 +5,14 @@ use std::{
};
use anyhow::{bail, Result};
use blue_build_recipe::Recipe;
use blue_build_utils::constants::*;
use clap::Args;
use log::{debug, info, trace};
use typed_builder::TypedBuilder;
use users::{Users, UsersCache};
use crate::{
commands::build::BuildCommand,
constants::{ARCHIVE_SUFFIX, LOCAL_BUILD},
module_recipe::Recipe,
ops,
};
use crate::commands::build::BuildCommand;
use super::BlueBuildCommand;
@ -148,7 +145,7 @@ impl BlueBuildCommand for RebaseCommand {
fn check_can_run() -> Result<()> {
trace!("check_can_run()");
ops::check_command_exists("rpm-ostree")?;
blue_build_utils::check_command_exists("rpm-ostree")?;
let cache = UsersCache::new();
if cache.get_current_uid() != 0 {

View file

@ -1,57 +1,16 @@
use std::{
env, fs,
path::{Path, PathBuf},
process,
};
use std::path::PathBuf;
use anyhow::{anyhow, Result};
use askama::Template;
use blue_build_recipe::Recipe;
use blue_build_template::{ContainerFileTemplate, Template};
use blue_build_utils::constants::*;
use clap::Args;
use log::{debug, error, info, trace};
use log::{debug, info, trace};
use typed_builder::TypedBuilder;
use uuid::Uuid;
use crate::{constants::*, module_recipe::Recipe};
use super::BlueBuildCommand;
#[derive(Debug, Clone, Template, TypedBuilder)]
#[template(path = "Containerfile.j2", escape = "none")]
pub struct ContainerFileTemplate<'a> {
recipe: &'a Recipe<'a>,
#[builder(setter(into))]
recipe_path: &'a Path,
#[builder(setter(into))]
build_id: Uuid,
#[builder(default)]
export_script: ExportsTemplate,
}
#[derive(Debug, Clone, Default, Template)]
#[template(path = "export.sh", escape = "none")]
pub struct ExportsTemplate;
impl ExportsTemplate {
fn print_script(&self) -> String {
trace!("print_script({self})");
format!(
"\"{}\"",
self.render()
.unwrap_or_else(|e| {
error!("Failed to render export.sh script: {e}");
process::exit(1);
})
.replace('\n', "\\n")
.replace('\"', "\\\"")
.replace('$', "\\$")
)
}
}
#[derive(Debug, Clone, Args, TypedBuilder)]
pub struct TemplateCommand {
/// The recipe file to create a template from
@ -127,48 +86,3 @@ impl TemplateCommand {
// ======================================================== //
// ========================= Helpers ====================== //
// ======================================================== //
fn has_cosign_file() -> bool {
trace!("has_cosign_file()");
std::env::current_dir()
.map(|p| p.join(COSIGN_PATH).exists())
.unwrap_or(false)
}
#[must_use]
fn print_containerfile(containerfile: &str) -> String {
trace!("print_containerfile({containerfile})");
debug!("Loading containerfile contents for {containerfile}");
let path = format!("config/containerfiles/{containerfile}/Containerfile");
let file = fs::read_to_string(&path).unwrap_or_else(|e| {
error!("Failed to read file {path}: {e}");
process::exit(1);
});
debug!("Containerfile contents {path}:\n{file}");
file
}
fn get_github_repo_owner() -> Option<String> {
Some(env::var(GITHUB_REPOSITORY_OWNER).ok()?.to_lowercase())
}
fn get_gitlab_registry_path() -> Option<String> {
Some(
format!(
"{}/{}/{}",
env::var(CI_REGISTRY).ok()?,
env::var(CI_PROJECT_NAMESPACE).ok()?,
env::var(CI_PROJECT_NAME).ok()?,
)
.to_lowercase(),
)
}
fn modules_exists() -> bool {
let mod_path = Path::new("modules");
mod_path.exists() && mod_path.is_dir()
}

View file

@ -1,19 +1,6 @@
//! The root library for blue-build.
#![warn(
clippy::correctness,
clippy::suspicious,
clippy::perf,
clippy::style,
clippy::nursery
)]
#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
#![allow(clippy::module_name_repetitions)]
shadow_rs::shadow!(shadow);
pub mod akmods_info;
pub mod commands;
pub mod constants;
pub mod module_recipe;
mod ops;

25
template/Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
[package]
name = "blue-build-template"
version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
askama = { version = "0.12", features = ["serde-json", "serde-yaml"] }
blue-build-recipe = { path = "../recipe" }
blue-build-utils = { path = "../utils" }
log.workspace = true
serde.workspace = true
serde_yaml.workspace = true
serde_json.workspace = true
typed-builder.workspace = true
uuid.workspace = true
[lints]
workspace = true

137
template/src/lib.rs Normal file
View file

@ -0,0 +1,137 @@
use std::{borrow::Cow, env, fs, path::Path, process};
use blue_build_recipe::Recipe;
use blue_build_utils::constants::*;
use log::{debug, error, trace};
use typed_builder::TypedBuilder;
use uuid::Uuid;
pub use askama::Template;
#[derive(Debug, Clone, Template, TypedBuilder)]
#[template(path = "Containerfile.j2", escape = "none")]
pub struct ContainerFileTemplate<'a> {
recipe: &'a Recipe<'a>,
#[builder(setter(into))]
recipe_path: &'a Path,
#[builder(setter(into))]
build_id: Uuid,
#[builder(default)]
export_script: ExportsTemplate,
}
#[derive(Debug, Clone, Default, Template)]
#[template(path = "export.sh", escape = "none")]
pub struct ExportsTemplate;
impl ExportsTemplate {
fn print_script(&self) -> String {
trace!("print_script({self})");
format!(
"\"{}\"",
self.render()
.unwrap_or_else(|e| {
error!("Failed to render export.sh script: {e}");
process::exit(1);
})
.replace('\n', "\\n")
.replace('\"', "\\\"")
.replace('$', "\\$")
)
}
}
#[derive(Debug, Clone, Template, TypedBuilder)]
#[template(path = "github_issue.j2", escape = "md")]
pub 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 has_cosign_file() -> bool {
trace!("has_cosign_file()");
std::env::current_dir()
.map(|p| p.join(COSIGN_PATH).exists())
.unwrap_or(false)
}
#[must_use]
fn print_containerfile(containerfile: &str) -> String {
trace!("print_containerfile({containerfile})");
debug!("Loading containerfile contents for {containerfile}");
let path = format!("config/containerfiles/{containerfile}/Containerfile");
let file = fs::read_to_string(&path).unwrap_or_else(|e| {
error!("Failed to read file {path}: {e}");
process::exit(1);
});
debug!("Containerfile contents {path}:\n{file}");
file
}
fn get_github_repo_owner() -> Option<String> {
Some(env::var(GITHUB_REPOSITORY_OWNER).ok()?.to_lowercase())
}
fn get_gitlab_registry_path() -> Option<String> {
Some(
format!(
"{}/{}/{}",
env::var(CI_REGISTRY).ok()?,
env::var(CI_PROJECT_NAMESPACE).ok()?,
env::var(CI_PROJECT_NAME).ok()?,
)
.to_lowercase(),
)
}
fn modules_exists() -> bool {
let mod_path = Path::new("modules");
mod_path.exists() && mod_path.is_dir()
}

View file

@ -23,7 +23,7 @@ RUN printf {{ export_script.print_script() }} >> /exports.sh && chmod +x /export
FROM {{ recipe.base_image }}:{{ recipe.image_version }}
LABEL {{ crate::constants::BUILD_ID_LABEL }}="{{ build_id }}"
LABEL {{ blue_build_utils::constants::BUILD_ID_LABEL }}="{{ build_id }}"
LABEL org.opencontainers.image.title="{{ recipe.name }}"
LABEL org.opencontainers.image.description="{{ recipe.description }}"
LABEL io.artifacthub.package.readme-url=https://raw.githubusercontent.com/blue-build/cli/main/README.md

25
utils/Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
[package]
name = "blue-build-utils"
version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
directories = "5"
process_control = { version = "4.0.3", features = ["crossbeam-channel"] }
which = "6"
anyhow.workspace = true
format_serde_error.workspace = true
log.workspace = true
serde.workspace = true
serde_yaml.workspace = true
serde_json.workspace = true
[lints]
workspace = true

View file

@ -1,19 +1,12 @@
use std::{
ffi::OsStr,
fmt::Debug,
io::{Error, ErrorKind, Result},
process::{Command, Stdio},
time::{Duration, Instant},
};
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 {
@ -27,7 +20,7 @@ pub struct CommandOutput {
/// #
/// # Errors
///
pub fn create_command<T: AsRef<OsStr>>(binary_name: T) -> Result<Command> {
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);
@ -42,7 +35,6 @@ pub fn create_command<T: AsRef<OsStr>>(binary_name: T) -> Result<Command> {
}
};
#[allow(clippy::disallowed_methods)]
let mut cmd = Command::new(full_path);
cmd.stderr(Stdio::piped())
.stdout(Stdio::piped())
@ -71,7 +63,7 @@ fn internal_exec_cmd<T: AsRef<OsStr> + Debug, U: AsRef<OsStr> + Debug>(
exec_timeout(&mut cmd, time_limit)
}
pub fn exec_timeout(cmd: &mut Command, time_limit: Duration) -> Option<CommandOutput> {
fn exec_timeout(cmd: &mut Command, time_limit: Duration) -> Option<CommandOutput> {
let start = Instant::now();
let process = match cmd.spawn() {
Ok(process) => process,

View file

@ -1,9 +1,13 @@
use std::{io::Write, process::Command};
pub mod command_output;
pub mod constants;
use std::{io::Write, path::PathBuf, process::Command, thread, time::Duration};
use anyhow::{anyhow, Result};
use format_serde_error::SerdeError;
use log::{debug, trace};
use std::{thread, time::Duration};
pub use command_output::*;
pub fn check_command_exists(command: &str) -> Result<()> {
trace!("check_command_exists({command})");
@ -68,3 +72,8 @@ where
};
}
}
#[must_use]
pub fn home_dir() -> Option<PathBuf> {
directories::BaseDirs::new().map(|base_dirs| base_dirs.home_dir().to_path_buf())
}