Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat: v4 - go small go home #130

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
562 changes: 326 additions & 236 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 1 addition & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = ["crates/*"]
exclude = ["guide/linkchecker"]
resolver = "2"

[workspace.package]
authors = ["Michael Lohr <michael@lohr.dev>", "Shemnei"]
Expand Down Expand Up @@ -35,10 +36,6 @@ opt-level = 0
# and is only used when debugging.
debug = 1

[profile.dev.package.backtrace]
# color-eyre: Improves performance for debug builds
opt-level = 3

[profile.release]
lto = "thin"
# Optimize for binary size. In this case also turns out to be the fastest to
Expand Down
20 changes: 20 additions & 0 deletions crates/punktf-lib-gsgh/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "punktf-lib-gsgh"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
keywords.workspace = true

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
cfg-if = "1.0.0"
log.workspace = true
minijinja = { version = "1.0.6", default-features = false, features = ["builtins", "adjacent_loop_items", "debug", "deserialization"] }
semver = { version = "1.0.18", features = ["serde"] }
serde = { workspace = true, features = ["derive"] }
serde_yaml = "0.9.25"
thiserror.workspace = true
version-compare = "0.1.1"
versions = { version = "5.0.0", features = ["serde"] }
47 changes: 47 additions & 0 deletions crates/punktf-lib-gsgh/profile.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
version: 1.0.0

aliases:
- Foo
- Bar

extends:
- Parent

env:
Foo: Bar
Bool: true
Number: 2.4

transformers:
- type: line_terminator
with: LF

target: /tmp

pre_hooks:
type: inline
with: |
set -eoux pipefail
echo 'Foo'

items:
- priority: 5
env:
Foo: Bar
Bool: true
pre_hook:
type: inline
with: |-
set -eoux pipefail
echo 'Foo'
path: /dev/null
merge:
type: hook
with:
type: inline
with: |
#!/usr/bin/env bash

set -eoux pipefail

echo "test"
81 changes: 81 additions & 0 deletions crates/punktf-lib-gsgh/src/env.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use std::{
collections::{btree_set, BTreeMap, BTreeSet, HashSet},

Check warning

Code scanning / clippy

unused imports: `HashSet`, `ops::Deref`

unused imports: `HashSet`, `ops::Deref`
ops::Deref,

Check warning

Code scanning / clippy

unused imports: `HashSet`, `ops::Deref`

unused imports: `HashSet`, `ops::Deref`
};

use serde::{Deserialize, Serialize};

use crate::value::Value;

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Environment(pub BTreeMap<String, Value>);

impl Environment {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}

#[derive(Default, Debug, Clone, PartialEq)]
pub struct LayeredEnvironment(Vec<(&'static str, Environment)>);

impl LayeredEnvironment {
pub fn push(&mut self, name: &'static str, env: Environment) {
self.0.push((name, env));
}

pub fn pop(&mut self) -> Option<(&'static str, Environment)> {
self.0.pop()
}

pub fn keys(&self) -> BTreeSet<&str> {
self.0
.iter()
.flat_map(|(_, layer)| layer.0.keys())
.map(|key| key.as_str())
.collect()
}

pub fn get(&self, key: &str) -> Option<&Value> {
for (_, layer) in self.0.iter() {
if let Some(value) = layer.0.get(key) {
return Some(value);
}
}

return None;

Check warning

Code scanning / clippy

unneeded `return` statement

unneeded `return` statement
}

pub fn iter(&self) -> LayeredIter<'_> {
LayeredIter::new(self)
}

pub fn as_str_map(&self) -> BTreeMap<&str, String> {
self.iter()
// TODO: Optimize
// `trim` to remove trailing `\n`
.map(|(k, v)| (k, serde_yaml::to_string(v).unwrap().trim().into()))
.collect()
}
}

pub struct LayeredIter<'a> {
env: &'a LayeredEnvironment,
keys: btree_set::IntoIter<&'a str>,
}

impl<'a> LayeredIter<'a> {
pub fn new(env: &'a LayeredEnvironment) -> Self {
let keys = env.keys().into_iter();
Self { env, keys }
}
}

impl<'a> Iterator for LayeredIter<'a> {
type Item = (&'a str, &'a Value);

fn next(&mut self) -> Option<Self::Item> {
let key = self.keys.next()?;
Some((key, self.env.get(key)?))
}
}
170 changes: 170 additions & 0 deletions crates/punktf-lib-gsgh/src/hook.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use std::{
io::{BufRead, BufReader},
path::{Path, PathBuf},
process::{Command, Stdio},
};

use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::env::LayeredEnvironment;

// Have special syntax for skipping deployment on pre_hook
// Analog: <https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops>
// e.g. punktf:skip_deployment

#[derive(Error, Debug)]
pub enum HookError {
#[error("IO Error")]
IoError(#[from] std::io::Error),

#[error("Process failed with status `{0}`")]
ExitStatusError(std::process::ExitStatus),
}

impl From<std::process::ExitStatus> for HookError {
fn from(value: std::process::ExitStatus) -> Self {
Self::ExitStatusError(value)
}
}

pub type Result<T, E = HookError> = std::result::Result<T, E>;

// TODO: Replace once `exit_ok` becomes stable
trait ExitOk {
type Error;

fn exit_ok(self) -> Result<(), Self::Error>;
}

impl ExitOk for std::process::ExitStatus {
type Error = HookError;

fn exit_ok(self) -> Result<(), <Self as ExitOk>::Error> {
if self.success() {
Ok(())
} else {
Err(self.into())
}
}
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", content = "with", rename_all = "snake_case")]
pub enum Hook {
Inline(String),
File(PathBuf),
}

impl Hook {
pub fn run(self, cwd: &Path, env: LayeredEnvironment) -> Result<()> {
let mut child = self
.prepare_command()?
.current_dir(cwd)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.envs(env.as_str_map())
.spawn()?;

// No need to call kill here as the program will immediately exit
// and thereby kill all spawned children
let stdout = child.stdout.take().expect("Failed to get stdout from hook");

for line in BufReader::new(stdout).lines() {
match line {
Ok(line) => println!("hook::stdout > {}", line),
Err(err) => {
// Result is explicitly ignored as an error was already
// encountered
let _ = child.kill();
return Err(err.into());
}
}
}

// No need to call kill here as the program will immediately exit
// and thereby kill all spawned children
let stderr = child.stderr.take().expect("Failed to get stderr from hook");

for line in BufReader::new(stderr).lines() {
match line {
Ok(line) => println!("hook::stderr > {}", line),
Err(err) => {
// Result is explicitly ignored as an error was already
// encountered
let _ = child.kill();
return Err(err.into());
}
}
}

child
.wait_with_output()?
.status
.exit_ok()
.map_err(Into::into)
}

fn prepare_command(&self) -> Result<Command> {
#[allow(unused_assignments)]
let mut cmd = None;

#[cfg(target_family = "windows")]
{
let mut c = Command::new("cmd");
c.arg("/C");
cmd = Some(c);
}

#[cfg(target_family = "unix")]
{
let mut c = Command::new("sh");
c.arg("-c");
cmd = Some(c)
}

let Some(mut cmd) = cmd else {
return Err(HookError::IoError(std::io::Error::new(std::io::ErrorKind::Other, "Hooks are only supported on Windows and Unix-based systems")));
};

match self {
Self::Inline(s) => {
cmd.arg(s);
}
Self::File(path) => {
let s = std::fs::read_to_string(path)?;
cmd.arg(s);
}
}

Ok(cmd)
}
}

#[cfg(test)]
mod tests {
use crate::{env::Environment, value::Value};

use super::*;

#[test]
fn echo_hello_world() {
let env = Environment(
[
("TEST", Value::Bool(true)),
("FOO", Value::String(" BAR Test".into())),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect(),
);

let mut lenv = LayeredEnvironment::default();
lenv.push("test", env);

println!("{:#?}", lenv.as_str_map());

let hook = Hook::Inline(r#"echo "Hello World""#.to_string());
hook.run(Path::new("/tmp"), lenv).unwrap();
}
}
22 changes: 22 additions & 0 deletions crates/punktf-lib-gsgh/src/item.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use std::path::PathBuf;

use serde::{Deserialize, Serialize};

use crate::{merge::MergeMode, profile::Shared};

#[derive(Debug, Serialize, Deserialize)]
pub struct Item {
#[serde(flatten)]
pub shared: Shared,

pub path: PathBuf,

#[serde(skip_serializing_if = "Option::is_none", default)]
pub rename: Option<PathBuf>,

#[serde(skip_serializing_if = "Option::is_none", default)]
pub overwrite_target: Option<PathBuf>,

#[serde(skip_serializing_if = "Option::is_none", default)]
pub merge: Option<MergeMode>,
}
Loading