Compare commits

...

3 Commits

Author SHA1 Message Date
Dustin 554c1e9cf4 container: Build static executable
dustin/tmpl/pipeline/head This commit looks good Details
Building a static executable and distributing it in a "from scratch"
container image dramatically reduces the image size: down to 8 MB from
102 MB.
2024-01-18 19:54:58 -06:00
Dustin 09bd82d1df hooks: Run hooks in the order they were defined
Iterating over a `HashSet` produces items in an arbitrary order.  A
`BTreeSet`, on the other hand, produces items in the same order they
were inserted.  Thus, this structure is more appropriate, since the
execution order of hooks may be important in some cases.
2024-01-18 19:48:13 -06:00
Dustin 1099fa40c7 write_file: Do not overwrite with same content
The `write_file` function will no longer overwrite existing files if the
content has not changed.  This will ensure file metadata timestamps are
accurate, and reduce unnecessary filesystem activity.
2024-01-18 19:46:25 -06:00
4 changed files with 51 additions and 20 deletions

7
Cargo.lock generated
View File

@ -300,6 +300,12 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "humansize" name = "humansize"
version = "2.1.3" version = "2.1.3"
@ -923,6 +929,7 @@ dependencies = [
"argparse", "argparse",
"blake2", "blake2",
"file-mode", "file-mode",
"hex",
"pwd-grp", "pwd-grp",
"serde", "serde",
"serde_yaml", "serde_yaml",

View File

@ -9,6 +9,7 @@ edition = "2021"
argparse = "0.2.2" argparse = "0.2.2"
blake2 = "0.10.6" blake2 = "0.10.6"
file-mode = { version = "0.1.2", features = ["serde"] } file-mode = { version = "0.1.2", features = ["serde"] }
hex = "0.4.3"
pwd-grp = "0.1.1" pwd-grp = "0.1.1"
serde = { version = "1.0.195", features = ["derive"] } serde = { version = "1.0.195", features = ["derive"] }
serde_yaml = "0.9.30" serde_yaml = "0.9.30"

View File

@ -1,23 +1,23 @@
FROM registry.fedoraproject.org/fedora-minimal:39 AS build FROM docker.io/library/rust:1.73-alpine AS build
RUN --mount=type=cache,target=/var/cache \ RUN --mount=type=cache,target=/var/cache \
microdnf install -y \ apk add \
--setopt install_weak_deps=0 \ musl-dev \
cargo\
&& : && :
COPY . /src COPY . /src
WORKDIR /src WORKDIR /src
RUN cargo build --release --locked RUN cargo build --release --locked \
&& strip target/release/tmpl
FROM registry.fedoraproject.org/fedora-minimal:39 FROM scratch
COPY --from=build /src/target/release/tmpl /usr/local/bin COPY --from=build /src/target/release/tmpl /tmpl
ENTRYPOINT ["/usr/local/bin/tmpl"] ENTRYPOINT ["/tmpl"]
LABEL name='tmpl' \ LABEL name='tmpl' \
vendor='Dustin C. Hatch' \ vendor='Dustin C. Hatch' \

View File

@ -1,7 +1,7 @@
mod model; mod model;
mod templating; mod templating;
use std::collections::HashSet; use std::collections::BTreeSet;
use std::io::{Read, Seek, Write}; use std::io::{Read, Seek, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
@ -12,7 +12,7 @@ use serde::de::DeserializeOwned;
use serde_yaml::Value; use serde_yaml::Value;
use shlex::Shlex; use shlex::Shlex;
use tera::{Context, Tera}; use tera::{Context, Tera};
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, trace, warn};
use model::Instructions; use model::Instructions;
@ -149,7 +149,7 @@ fn process_instructions(
); );
let ctx = Context::from_serialize(&values)?; let ctx = Context::from_serialize(&values)?;
let mut post_hooks = HashSet::new(); let mut post_hooks = BTreeSet::new();
for i in instructions.render { for i in instructions.render {
let out = match tera.render(&i.template, &ctx) { let out = match tera.render(&i.template, &ctx) {
Ok(o) => o, Ok(o) => o,
@ -161,13 +161,18 @@ fn process_instructions(
}; };
let mut dest = PathBuf::from(destdir.as_ref()); let mut dest = PathBuf::from(destdir.as_ref());
dest.push(i.dest.strip_prefix("/").unwrap_or(i.dest.as_path())); dest.push(i.dest.strip_prefix("/").unwrap_or(i.dest.as_path()));
let orig_cksm = checksum(&dest).ok(); let changed = match write_file(&dest, out.as_bytes()) {
if let Err(e) = write_file(&dest, out.as_bytes()) { Ok(c) => c,
error!("Failed to write output file {}: {}", dest.display(), e); Err(e) => {
continue; error!(
} "Failed to write output file {}: {}",
let new_cksm = checksum(&dest).ok(); dest.display(),
if orig_cksm != new_cksm { e
);
continue;
}
};
if changed {
info!("File {} was changed", dest.display()); info!("File {} was changed", dest.display());
if let Some(hooks) = i.hooks { if let Some(hooks) = i.hooks {
if let Some(changed) = hooks.changed { if let Some(changed) = hooks.changed {
@ -219,19 +224,37 @@ fn process_instructions(
fn write_file( fn write_file(
dest: impl AsRef<Path>, dest: impl AsRef<Path>,
data: &[u8], data: &[u8],
) -> Result<(), std::io::Error> { ) -> Result<bool, std::io::Error> {
if let Some(p) = dest.as_ref().parent() { if let Some(p) = dest.as_ref().parent() {
if !p.exists() { if !p.exists() {
info!("Creating directory {}", p.display()); info!("Creating directory {}", p.display());
std::fs::create_dir_all(p)?; std::fs::create_dir_all(p)?;
} }
} }
if let Ok(orig_cksm) = checksum(&dest) {
trace!(
"Original checksum: {}: {}",
dest.as_ref().display(),
hex::encode(&orig_cksm)
);
let mut blake = Blake2b512::new();
blake.update(data);
let new_cksm = blake.finalize().to_vec();
trace!(
"New checksum: {}: {}",
dest.as_ref().display(),
hex::encode(&new_cksm)
);
if orig_cksm == new_cksm {
return Ok(false);
}
}
debug!("Writing output: {}", dest.as_ref().display()); debug!("Writing output: {}", dest.as_ref().display());
let mut f = std::fs::File::create(&dest)?; let mut f = std::fs::File::create(&dest)?;
f.write_all(data)?; f.write_all(data)?;
let size = f.stream_position()?; let size = f.stream_position()?;
debug!("Wrote output: {} ({} bytes)", dest.as_ref().display(), size); debug!("Wrote output: {} ({} bytes)", dest.as_ref().display(), size);
Ok(()) Ok(true)
} }
fn chown( fn chown(