feat: run clippy + BlueBuildTrait (#4)

* feat: run clippy + BlueBuildTrait

* chore: add default run impl; more clippy

* chore: remove vscode folder; not needed

* cleanups

* Move to commands.rs

* Move functions; remove run function implementation from each command

* Remove run impl from init commands

* Use error log

---------

Co-authored-by: Gerald Pinder <gmpinder@gmail.com>
This commit is contained in:
N16hth4wk 2024-01-21 21:26:35 -06:00 committed by GitHub
parent 9454baa750
commit dbbd087b5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 259 additions and 264 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target
.sccache/
.vscode/

View file

@ -8,23 +8,23 @@ license = "Apache-2.0"
categories = ["command-line-utilities"]
[dependencies]
anyhow = "1.0.75"
anyhow = "1"
askama = { version = "0.12.1", features = ["serde-json"] }
cfg-if = "1.0.0"
chrono = "0.4.31"
clap = { version = "4.4.4", features = ["derive"] }
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"] }
log = "0.4.20"
log = "0.4"
podman-api = { version = "0.10.0", optional = true }
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.107"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9.25"
sigstore = { version = "0.8.0", optional = true }
tokio = { version = "1.35.1", features = ["full"], optional = true }
tokio = { version = "1", features = ["full"], optional = true }
typed-builder = "0.18.0"
users = "0.11.0"

View file

@ -1,11 +1,17 @@
use blue_build::{self, build, local, template};
#![warn(clippy::pedantic, clippy::nursery)]
use clap::{Parser, Subcommand};
use clap_verbosity_flag::{InfoLevel, Verbosity};
use env_logger::WriteStyle;
use log::trace;
use blue_build::{
self,
commands::{build, local, template, BlueBuildCommand},
};
#[cfg(feature = "init")]
use blue_build::init;
use blue_build::commands::init;
#[derive(Parser, Debug)]
#[command(name = "BlueBuild", author, version, about, long_about = None)]
@ -68,15 +74,15 @@ fn main() {
trace!("{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::Template(command) => command.run(),
CommandArgs::Upgrade(command) => command.run(),
CommandArgs::Rebase(command) => command.run(),
#[cfg(feature = "init")]
CommandArgs::Init(command) => command.run(),
#[cfg(feature = "init")]
CommandArgs::New(command) => command.run(),
CommandArgs::Rebase(mut command) => command.run(),
CommandArgs::Upgrade(mut command) => command.run(),
}
}

20
src/commands.rs Normal file
View file

@ -0,0 +1,20 @@
#[cfg(feature = "init")]
pub mod init;
pub mod build;
pub mod local;
pub mod template;
use log::error;
pub trait BlueBuildCommand {
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);
}
}
}

View file

@ -29,10 +29,12 @@ use futures_util::StreamExt;
use tokio::runtime::Runtime;
use crate::{
commands::template::TemplateCommand,
ops::{self, ARCHIVE_SUFFIX},
template::{Recipe, TemplateCommand},
};
use super::{template::Recipe, BlueBuildCommand};
#[derive(Debug, Clone, Args, TypedBuilder)]
pub struct BuildCommand {
/// The recipe file to build an image
@ -127,9 +129,9 @@ pub struct BuildCommand {
private_key: Option<String>,
}
impl BuildCommand {
impl BlueBuildCommand for BuildCommand {
/// Runs the command and returns a result.
pub fn try_run(&mut self) -> Result<()> {
fn try_run(&mut self) -> Result<()> {
trace!("BuildCommand::try_run()");
if self.push && self.archive.is_some() {
@ -168,18 +170,9 @@ impl BuildCommand {
#[cfg(not(feature = "podman-api"))]
self.build_image()
}
}
/// Runs the command and exits if there is an error.
pub fn run(&mut self) {
trace!("BuildCommand::run()");
if let Err(e) = self.try_run() {
error!("Failed to build image: {e}");
process::exit(1);
}
info!("Finished building!");
}
impl BuildCommand {
#[cfg(feature = "podman-api")]
async fn build_image_podman_api(&self, client: Podman) -> Result<()> {
use podman_api::opts::ImageTagOpts;
@ -327,7 +320,7 @@ impl BuildCommand {
}
}
self.sign_images(&image_name, tags.first().map(|x| x.as_str()))?;
sign_images(&image_name, tags.first().map(String::as_str))?;
}
Ok(())
}
@ -434,6 +427,9 @@ impl BuildCommand {
Ok(())
}
/// # Errors
///
/// Will return `Err` if the image name cannot be generated.
pub fn generate_full_image_name(&self, recipe: &Recipe) -> Result<String> {
trace!("BuildCommand::generate_full_image_name({recipe:#?})");
info!("Generating full image name");
@ -483,7 +479,7 @@ impl BuildCommand {
if self.push {
bail!("Need '--registry' and '--registry-path' in order to push image");
}
recipe.name.to_owned()
recipe.name.clone()
}
}
};
@ -493,6 +489,9 @@ impl BuildCommand {
Ok(image_name)
}
/// # Errors
///
/// Will return `Err` if the build fails.
fn run_build(&self, image_name: &str, tags: &[String]) -> Result<()> {
trace!("BuildCommand::run_build({image_name}, {tags:#?})");
@ -579,7 +578,7 @@ impl BuildCommand {
if self.push {
debug!("Pushing all images");
for tag in tags.iter() {
for tag in tags {
debug!("Pushing image {image_name}:{tag}");
let tag_image = format!("{image_name}:{tag}");
@ -605,138 +604,143 @@ impl BuildCommand {
.status()?;
if status.success() {
info!("Successfully pushed {image_name}:{tag}!")
info!("Successfully pushed {image_name}:{tag}!");
} else {
bail!("Failed to push image {image_name}:{tag}");
}
}
self.sign_images(image_name, tags.first().map(|x| x.as_str()))?;
}
Ok(())
}
fn sign_images(&self, image_name: &str, tag: Option<&str>) -> Result<()> {
trace!("BuildCommand::sign_images({image_name}, {tag:?})");
env::set_var("COSIGN_PASSWORD", "");
env::set_var("COSIGN_YES", "true");
let image_digest = get_image_digest(image_name, tag)?;
let image_name_tag = tag
.map(|t| format!("{image_name}:{t}"))
.unwrap_or(image_name.to_owned());
match (
env::var("CI_DEFAULT_BRANCH"),
env::var("CI_COMMIT_REF_NAME"),
env::var("CI_PROJECT_URL"),
env::var("CI_SERVER_PROTOCOL"),
env::var("CI_SERVER_HOST"),
env::var("SIGSTORE_ID_TOKEN"),
env::var("GITHUB_EVENT_NAME"),
env::var("GITHUB_REF_NAME"),
env::var("COSIGN_PRIVATE_KEY"),
) {
(
Ok(ci_default_branch),
Ok(ci_commit_ref),
Ok(ci_project_url),
Ok(ci_server_protocol),
Ok(ci_server_host),
Ok(_),
_,
_,
_,
) if ci_default_branch == ci_commit_ref => {
trace!("CI_PROJECT_URL={ci_project_url}, CI_DEFAULT_BRANCH={ci_default_branch}, CI_COMMIT_REF_NAME={ci_commit_ref}, CI_SERVER_PROTOCOL={ci_server_protocol}, CI_SERVER_HOST={ci_server_host}");
debug!("On default branch");
info!("Signing image: {image_digest}");
trace!("cosign sign {image_digest}");
if Command::new("cosign")
.arg("sign")
.arg(&image_digest)
.status()?
.success()
{
info!("Successfully signed image!");
} else {
bail!("Failed to sign image: {image_digest}");
}
let cert_ident =
format!("{ci_project_url}//.gitlab-ci.yml@refs/heads/{ci_default_branch}");
let cert_oidc = format!("{ci_server_protocol}://{ci_server_host}");
trace!("cosign verify --certificate-identity {cert_ident} --certificate-oidc-issuer {cert_oidc} {image_name_tag}");
if !Command::new("cosign")
.arg("verify")
.arg("--certificate-identity")
.arg(&cert_ident)
.arg("--certificate-oidc-issuer")
.arg(&cert_oidc)
.arg(&image_name_tag)
.status()?
.success()
{
bail!("Failed to verify image!");
}
}
(_, _, _, _, _, _, Ok(github_event_name), Ok(github_ref_name), Ok(_))
if github_event_name != "pull_request" && github_ref_name == "live" =>
{
trace!("GITHUB_EVENT_NAME={github_event_name}, GITHUB_REF_NAME={github_ref_name}");
debug!("On live branch");
info!("Signing image: {image_digest}");
trace!("cosign sign --key=env://COSIGN_PRIVATE_KEY {image_digest}");
if Command::new("cosign")
.arg("sign")
.arg("--key=env://COSIGN_PRIVATE_KEY")
.arg(&image_digest)
.status()?
.success()
{
info!("Successfully signed image!");
} else {
bail!("Failed to sign image: {image_digest}");
}
trace!("cosign verify --key ./cosign.pub {image_name_tag}");
if !Command::new("cosign")
.arg("verify")
.arg("--key=./cosign.pub")
.arg(&image_name_tag)
.status()?
.success()
{
bail!("Failed to verify image!");
}
}
_ => debug!("Not running in CI with cosign variables, not signing"),
sign_images(image_name, tags.first().map(String::as_str))?;
}
Ok(())
}
}
// ======================================================== //
// ========================= Helpers ====================== //
// ======================================================== //
fn sign_images(image_name: &str, tag: Option<&str>) -> Result<()> {
trace!("BuildCommand::sign_images({image_name}, {tag:?})");
env::set_var("COSIGN_PASSWORD", "");
env::set_var("COSIGN_YES", "true");
let image_digest = get_image_digest(image_name, tag)?;
let image_name_tag = tag
.map(|t| format!("{image_name}:{t}"))
.unwrap_or(image_name.to_owned());
match (
env::var("CI_DEFAULT_BRANCH"),
env::var("CI_COMMIT_REF_NAME"),
env::var("CI_PROJECT_URL"),
env::var("CI_SERVER_PROTOCOL"),
env::var("CI_SERVER_HOST"),
env::var("SIGSTORE_ID_TOKEN"),
env::var("GITHUB_EVENT_NAME"),
env::var("GITHUB_REF_NAME"),
env::var("COSIGN_PRIVATE_KEY"),
) {
(
Ok(ci_default_branch),
Ok(ci_commit_ref),
Ok(ci_project_url),
Ok(ci_server_protocol),
Ok(ci_server_host),
Ok(_),
_,
_,
_,
) if ci_default_branch == ci_commit_ref => {
trace!("CI_PROJECT_URL={ci_project_url}, CI_DEFAULT_BRANCH={ci_default_branch}, CI_COMMIT_REF_NAME={ci_commit_ref}, CI_SERVER_PROTOCOL={ci_server_protocol}, CI_SERVER_HOST={ci_server_host}");
debug!("On default branch");
info!("Signing image: {image_digest}");
trace!("cosign sign {image_digest}");
if Command::new("cosign")
.arg("sign")
.arg(&image_digest)
.status()?
.success()
{
info!("Successfully signed image!");
} else {
bail!("Failed to sign image: {image_digest}");
}
let cert_ident =
format!("{ci_project_url}//.gitlab-ci.yml@refs/heads/{ci_default_branch}");
let cert_oidc = format!("{ci_server_protocol}://{ci_server_host}");
trace!("cosign verify --certificate-identity {cert_ident} --certificate-oidc-issuer {cert_oidc} {image_name_tag}");
if !Command::new("cosign")
.arg("verify")
.arg("--certificate-identity")
.arg(&cert_ident)
.arg("--certificate-oidc-issuer")
.arg(&cert_oidc)
.arg(&image_name_tag)
.status()?
.success()
{
bail!("Failed to verify image!");
}
}
(_, _, _, _, _, _, Ok(github_event_name), Ok(github_ref_name), Ok(_))
if github_event_name != "pull_request" && github_ref_name == "live" =>
{
trace!("GITHUB_EVENT_NAME={github_event_name}, GITHUB_REF_NAME={github_ref_name}");
debug!("On live branch");
info!("Signing image: {image_digest}");
trace!("cosign sign --key=env://COSIGN_PRIVATE_KEY {image_digest}");
if Command::new("cosign")
.arg("sign")
.arg("--key=env://COSIGN_PRIVATE_KEY")
.arg(&image_digest)
.status()?
.success()
{
info!("Successfully signed image!");
} else {
bail!("Failed to sign image: {image_digest}");
}
trace!("cosign verify --key ./cosign.pub {image_name_tag}");
if !Command::new("cosign")
.arg("verify")
.arg("--key=./cosign.pub")
.arg(&image_name_tag)
.status()?
.success()
{
bail!("Failed to verify image!");
}
}
_ => debug!("Not running in CI with cosign variables, not signing"),
}
Ok(())
}
fn get_image_digest(image_name: &str, tag: Option<&str>) -> Result<String> {
trace!("get_image_digest({image_name}, {tag:?})");
let image_url = match tag {
Some(tag) => format!("docker://{image_name}:{tag}"),
None => format!("docker://{image_name}"),
let image_url = if let Some(tag) = tag {
format!("docker://{image_name}:{tag}")
} else {
format!("docker://{image_name}")
};
trace!("skopeo inspect --format='{{.Digest}}' {image_url}");

View file

@ -8,9 +8,11 @@ use clap::Args;
use log::error;
use typed_builder::TypedBuilder;
const GITLAB_CI_FILE: &'static str = include_str!("../templates/init/gitlab-ci.yml.tera");
const RECIPE_FILE: &'static str = include_str!("../templates/init/recipe.yml.tera");
const LICENSE_FILE: &'static str = include_str!("../LICENSE");
use super::BlueBuildCommand;
const GITLAB_CI_FILE: &'static str = include_str!("../../templates/init/gitlab-ci.yml.tera");
const RECIPE_FILE: &'static str = include_str!("../../templates/init/recipe.yml.tera");
const LICENSE_FILE: &'static str = include_str!("../../LICENSE");
#[derive(Debug, Clone, Default, Args, TypedBuilder)]
pub struct NewInitCommon {
@ -30,8 +32,8 @@ pub struct InitCommand {
common: NewInitCommon,
}
impl InitCommand {
pub fn try_run(&self) -> Result<()> {
impl BlueBuildCommand for InitCommand {
fn try_run(&mut self) -> Result<()> {
let base_dir = match self.dir.as_ref() {
Some(dir) => dir,
None => std::path::Path::new("./"),
@ -40,14 +42,9 @@ impl InitCommand {
self.initialize_directory(base_dir);
Ok(())
}
}
pub fn run(&self) {
if let Err(e) = self.try_run() {
error!("Failed to init ublue project: {e}");
process::exit(1);
}
}
impl InitCommand {
fn initialize_directory(&self, base_dir: &Path) {
let recipe_path = base_dir.join("recipe.yml");
@ -74,19 +71,12 @@ pub struct NewCommand {
common: NewInitCommon,
}
impl NewCommand {
pub fn try_run(&self) -> Result<()> {
impl BlueBuildCommand for NewCommand {
fn try_run(&mut self) -> Result<()> {
InitCommand::builder()
.dir(self.dir.clone())
.common(self.common.clone())
.build()
.try_run()
}
pub fn run(&self) {
if let Err(e) = self.try_run() {
error!("Failed to create new project: {e}");
process::exit(1);
}
}
}

View file

@ -11,11 +11,12 @@ use typed_builder::TypedBuilder;
use users::{Users, UsersCache};
use crate::{
build::BuildCommand,
commands::{build::BuildCommand, template::Recipe},
ops::{self, ARCHIVE_SUFFIX, LOCAL_BUILD},
template::Recipe,
};
use super::BlueBuildCommand;
#[derive(Default, Clone, Debug, TypedBuilder, Args)]
pub struct LocalCommonArgs {
/// The recipe file to build an image.
@ -35,8 +36,8 @@ pub struct UpgradeCommand {
common: LocalCommonArgs,
}
impl UpgradeCommand {
pub fn try_run(&self) -> Result<()> {
impl BlueBuildCommand for UpgradeCommand {
fn try_run(&mut self) -> Result<()> {
trace!("UpgradeCommand::try_run()");
check_can_run()?;
@ -78,15 +79,6 @@ impl UpgradeCommand {
}
Ok(())
}
pub fn run(&self) {
trace!("UpgradeCommand::run()");
if let Err(e) = self.try_run() {
error!("Failed to upgrade image: {e}");
process::exit(1);
}
}
}
#[derive(Default, Clone, Debug, TypedBuilder, Args)]
@ -95,8 +87,8 @@ pub struct RebaseCommand {
common: LocalCommonArgs,
}
impl RebaseCommand {
pub fn try_run(&self) -> Result<()> {
impl BlueBuildCommand for RebaseCommand {
fn try_run(&mut self) -> Result<()> {
trace!("RebaseCommand::try_run()");
check_can_run()?;
@ -142,24 +134,18 @@ impl RebaseCommand {
}
Ok(())
}
pub fn run(&self) {
trace!("RebaseCommand::run()");
if let Err(e) = self.try_run() {
error!("Failed to rebase onto new image: {e}");
process::exit(1);
}
}
}
// ======================================================== //
// ========================= Helpers ====================== //
// ======================================================== //
fn check_can_run() -> Result<()> {
trace!("check_can_run()");
ops::check_command_exists("rpm-ostree")?;
let cache = UsersCache::new();
if cache.get_current_uid() != 0 {
bail!("You need to be root to rebase a local image! Try using 'sudo'.");
}
@ -180,13 +166,7 @@ fn clean_local_build_dir(image_name: &str, rebase: bool) -> Result<()> {
);
}
if !local_build_path.exists() {
debug!(
"Creating build output dir at {}",
local_build_path.display()
);
fs::create_dir_all(local_build_path)?;
} else {
if local_build_path.exists() {
debug!("Cleaning out build dir {LOCAL_BUILD}");
let entries = fs::read_dir(LOCAL_BUILD)?;
@ -205,6 +185,12 @@ fn clean_local_build_dir(image_name: &str, rebase: bool) -> Result<()> {
fs::remove_file(path)?;
}
}
} else {
debug!(
"Creating build output dir at {}",
local_build_path.display()
);
fs::create_dir_all(local_build_path)?;
}
Ok(())

View file

@ -15,6 +15,8 @@ use serde::{Deserialize, Serialize};
use serde_yaml::Value;
use typed_builder::TypedBuilder;
use super::BlueBuildCommand;
#[derive(Debug, Clone, Template, TypedBuilder)]
#[template(path = "Containerfile")]
pub struct ContainerFileTemplate<'a> {
@ -58,6 +60,7 @@ pub struct Recipe {
}
impl Recipe {
#[must_use]
pub fn generate_tags(&self) -> Vec<String> {
trace!("Recipe::generate_tags()");
debug!("Generating image tags for {}", &self.name);
@ -83,14 +86,14 @@ impl Recipe {
}
}
if default_branch != commit_branch {
debug!("Running on branch {commit_branch}");
tags.push(format!("{commit_branch}-{image_version}"));
} else {
if default_branch == commit_branch {
debug!("Running on the default branch");
tags.push(image_version.to_string());
tags.push(format!("{image_version}-{timestamp}"));
tags.push(timestamp.to_string());
tags.push(timestamp);
} else {
debug!("Running on branch {commit_branch}");
tags.push(format!("{commit_branch}-{image_version}"));
}
tags.push(format!("{commit_sha}-{image_version}"));
@ -108,7 +111,7 @@ impl Recipe {
trace!("GITHUB_EVENT_NAME={github_event_name},PR_EVENT_NUMBER={github_event_number},GITHUB_SHA={github_sha},GITHUB_REF_NAME={github_ref_name}");
warn!("Detected running in Github, pulling information from GITHUB variables");
let mut short_sha = github_sha.clone();
let mut short_sha = github_sha;
short_sha.truncate(7);
if github_event_name == "pull_request" {
@ -167,20 +170,15 @@ pub struct TemplateCommand {
output: Option<PathBuf>,
}
impl TemplateCommand {
pub fn try_run(&self) -> Result<()> {
impl BlueBuildCommand for TemplateCommand {
fn try_run(&mut self) -> Result<()> {
info!("Templating for recipe at {}", self.recipe.display());
self.template_file()
}
}
pub fn run(&self) {
if let Err(e) = self.try_run() {
error!("Failed to template file: {e}");
process::exit(1);
}
}
impl TemplateCommand {
fn template_file(&self) -> Result<()> {
trace!("TemplateCommand::template_file()");
@ -194,18 +192,14 @@ impl TemplateCommand {
.build();
let output_str = template.render()?;
if let Some(output) = self.output.as_ref() {
debug!("Templating to file {}", output.display());
trace!("Containerfile:\n{output_str}");
match self.output.as_ref() {
Some(output) => {
debug!("Templating to file {}", output.display());
trace!("Containerfile:\n{output_str}");
std::fs::write(output, output_str)?;
}
None => {
debug!("Templating to stdout");
println!("{output_str}");
}
std::fs::write(output, output_str)?;
} else {
debug!("Templating to stdout");
println!("{output_str}");
}
info!("Finished templating Containerfile");
@ -213,6 +207,10 @@ impl TemplateCommand {
}
}
// ======================================================== //
// ========================= Helpers ====================== //
// ======================================================== //
fn print_script(script_contents: &ExportsTemplate) -> String {
trace!("print_script({script_contents})");
@ -284,17 +282,18 @@ fn get_module_from_file(file_name: &str) -> String {
process::exit(1);
};
if let Ok(module_ext) = serde_yaml::from_str::<ModuleExt>(file.as_str()) {
module_ext.render().unwrap_or_else(template_err_fn)
} else {
let module = serde_yaml::from_str::<Module>(file.as_str()).unwrap_or_else(serde_err_fn);
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);
ModuleExt::builder()
.modules(vec![module])
.build()
.render()
.unwrap_or_else(template_err_fn)
}
ModuleExt::builder()
.modules(vec![module])
.build()
.render()
.unwrap_or_else(template_err_fn)
},
|module_ext| module_ext.render().unwrap_or_else(template_err_fn),
)
}
fn print_module_context(module: &Module) -> String {

View file

@ -1,16 +1,12 @@
//! The root library for blue-build.
#![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)]
#[cfg(feature = "init")]
pub mod init;
pub mod build;
pub mod local;
pub mod commands;
mod ops;
pub mod template;

View file

@ -1,12 +1,6 @@
use std::{
env,
path::{Path, PathBuf},
process::Command,
};
use anyhow::{anyhow, bail, Result};
use clap::ValueEnum;
use anyhow::{anyhow, Result};
use log::{debug, trace};
use std::process::Command;
pub const LOCAL_BUILD: &str = "/etc/blue-build";
pub const ARCHIVE_SUFFIX: &str = ".tar.gz";
@ -16,18 +10,17 @@ pub fn check_command_exists(command: &str) -> Result<()> {
debug!("Checking if {command} exists");
trace!("which {command}");
match Command::new("which")
if Command::new("which")
.arg(command)
.output()?
.status
.success()
{
true => {
debug!("Command {command} does exist");
Ok(())
}
false => Err(anyhow!(
debug!("Command {command} does exist");
Ok(())
} else {
Err(anyhow!(
"Command {command} doesn't exist and is required to build the image"
)),
))
}
}