diff --git a/README.md b/README.md
index 5fdf03b..bbd2bc4 100644
--- a/README.md
+++ b/README.md
@@ -109,6 +109,13 @@ RUSTFLAGS="-Cprofile-use=target/pgo-data/lpkg.profdata -Cllvm-args=-pgo-warn-mis
cargo pgo-build
```
+Regenerate project artefacts (README and SVG logo):
+
+```bash
+cargo run --bin readme_gen
+cargo run --bin logo_gen
+```
+
Run tests:
```bash
diff --git a/src/bin/logo_gen.rs b/src/bin/logo_gen.rs
new file mode 100644
index 0000000..e85eda4
--- /dev/null
+++ b/src/bin/logo_gen.rs
@@ -0,0 +1,522 @@
+use anyhow::Result;
+use std::fs;
+
+fn main() -> Result<()> {
+ let logo_svg = build_logo_svg();
+ fs::create_dir_all("assets")?;
+ fs::write("assets/logo.svg", logo_svg)?;
+ Ok(())
+}
+
+fn build_logo_svg() -> String {
+ use svg::*;
+
+ let mut doc = Document::new(640, 320)
+ .view_box("0 0 640 320")
+ .role("img")
+ .aria_label("title", "desc")
+ .title("LPKG Logo")
+ .desc("Stylised package icon with circuitry and the letters LPKG.");
+
+ let mut defs = Defs::new();
+ defs = defs.linear_gradient(
+ "bgGradient",
+ Gradient::new("0", "0", "1", "1")
+ .stop("0%", &[("stop-color", "#0f172a")])
+ .stop("100%", &[("stop-color", "#1e293b")]),
+ );
+ defs = defs.linear_gradient(
+ "cubeGradient",
+ Gradient::new("0", "0", "1", "1")
+ .stop("0%", &[("stop-color", "#38bdf8")])
+ .stop("100%", &[("stop-color", "#0ea5e9")]),
+ );
+ defs = defs.linear_gradient(
+ "cubeShadow",
+ Gradient::new("0", "1", "1", "0")
+ .stop("0%", &[("stop-color", "#0ea5e9"), ("stop-opacity", "0.4")])
+ .stop(
+ "100%",
+ &[("stop-color", "#38bdf8"), ("stop-opacity", "0.1")],
+ ),
+ );
+ defs = defs.linear_gradient(
+ "textGradient",
+ Gradient::new("0", "0", "0", "1")
+ .stop("0%", &[("stop-color", "#f8fafc")])
+ .stop("100%", &[("stop-color", "#cbd5f5")]),
+ );
+ defs = defs.filter(
+ "glow",
+ Filter::new()
+ .attr("x", "-20%")
+ .attr("y", "-20%")
+ .attr("width", "140%")
+ .attr("height", "140%")
+ .raw("")
+ .raw(""),
+ );
+
+ doc = doc.add_defs(defs);
+
+ doc = doc.add_element(
+ Element::new("rect")
+ .attr("width", "640")
+ .attr("height", "320")
+ .attr("rx", "28")
+ .attr("fill", "url(#bgGradient)")
+ .empty(),
+ );
+
+ let cube_group = Group::new()
+ .attr("filter", "url(#glow)")
+ .child(
+ Element::new("path")
+ .attr("d", "M222 86l86-42 86 42v96l-86 42-86-42z")
+ .attr("fill", "url(#cubeGradient)")
+ .empty(),
+ )
+ .child(
+ Element::new("path")
+ .attr("d", "M308 44v182l86-42V86z")
+ .attr("fill", "url(#cubeShadow)")
+ .empty(),
+ )
+ .child(
+ Element::new("path")
+ .attr("d", "M262 96l46-22 46 22v48l-46 22-46-22z")
+ .attr("fill", "#0f172a")
+ .attr("opacity", "0.85")
+ .empty(),
+ )
+ .child(
+ Element::new("path")
+ .attr("d", "M308 74l32 15v32l-32 15-32-15v-32z")
+ .attr("fill", "none")
+ .attr("stroke", "#38bdf8")
+ .attr("stroke-width", "4")
+ .attr("stroke-linejoin", "round")
+ .empty(),
+ )
+ .child(
+ Element::new("path")
+ .attr("d", "M308 122l-32-15")
+ .attr("stroke", "#38bdf8")
+ .attr("stroke-width", "4")
+ .attr("stroke-linecap", "round")
+ .attr("opacity", "0.6")
+ .empty(),
+ )
+ .child(
+ Element::new("path")
+ .attr("d", "M308 122l32-15")
+ .attr("stroke", "#38bdf8")
+ .attr("stroke-width", "4")
+ .attr("stroke-linecap", "round")
+ .attr("opacity", "0.6")
+ .empty(),
+ )
+ .child(
+ Element::new("circle")
+ .attr("cx", "276")
+ .attr("cy", "107")
+ .attr("r", "5")
+ .attr("fill", "#38bdf8")
+ .empty(),
+ )
+ .child(
+ Element::new("circle")
+ .attr("cx", "340")
+ .attr("cy", "107")
+ .attr("r", "5")
+ .attr("fill", "#38bdf8")
+ .empty(),
+ );
+ doc = doc.add_element(cube_group);
+
+ let circuits = Group::new()
+ .attr("fill", "none")
+ .attr("stroke", "#38bdf8")
+ .attr("stroke-width", "3")
+ .attr("stroke-linecap", "round")
+ .attr("opacity", "0.55")
+ .child(path("M120 78h72"))
+ .child(path("M120 110h48"))
+ .child(path("M120 142h64"))
+ .child(path("M448 110h72"))
+ .child(path("M472 142h88"))
+ .child(path("M448 174h96"));
+ doc = doc.add_element(circuits);
+
+ let text_group = Group::new()
+ .attr(
+ "font-family",
+ "'Fira Sans', 'Inter', 'Segoe UI', sans-serif",
+ )
+ .attr("font-weight", "600")
+ .attr("font-size", "90")
+ .attr("letter-spacing", "6")
+ .child(
+ Element::new("text")
+ .attr("x", "120")
+ .attr("y", "246")
+ .attr("fill", "url(#textGradient)")
+ .text("LPKG"),
+ );
+ doc = doc.add_element(text_group);
+
+ let tagline_group = Group::new()
+ .attr(
+ "font-family",
+ "'Fira Sans', 'Inter', 'Segoe UI', sans-serif",
+ )
+ .attr("font-size", "22")
+ .attr("fill", "#94a3b8")
+ .child(
+ Element::new("text")
+ .attr("x", "122")
+ .attr("y", "278")
+ .text("Lightweight Package Manager"),
+ );
+ doc = doc.add_element(tagline_group);
+
+ doc.finish()
+}
+
+mod svg {
+ #[derive(Default)]
+ pub struct Document {
+ width: u32,
+ height: u32,
+ view_box: Option,
+ role: Option,
+ aria_label: Option<(String, String)>,
+ title: Option,
+ desc: Option,
+ defs: Vec,
+ elements: Vec,
+ }
+
+ impl Document {
+ pub fn new(width: u32, height: u32) -> Self {
+ Self {
+ width,
+ height,
+ ..Default::default()
+ }
+ }
+
+ pub fn view_box(mut self, value: &str) -> Self {
+ self.view_box = Some(value.to_string());
+ self
+ }
+
+ pub fn role(mut self, value: &str) -> Self {
+ self.role = Some(value.to_string());
+ self
+ }
+
+ pub fn aria_label(mut self, title_id: &str, desc_id: &str) -> Self {
+ self.aria_label = Some((title_id.to_string(), desc_id.to_string()));
+ self
+ }
+
+ pub fn title(mut self, value: &str) -> Self {
+ self.title = Some(value.to_string());
+ self
+ }
+
+ pub fn desc(mut self, value: &str) -> Self {
+ self.desc = Some(value.to_string());
+ self
+ }
+
+ pub fn add_defs(mut self, defs: Defs) -> Self {
+ self.defs.push(defs.finish());
+ self
+ }
+
+ pub fn add_element(mut self, element: impl Into) -> Self {
+ self.elements.push(element.into());
+ self
+ }
+
+ pub fn finish(self) -> String {
+ let Document {
+ width,
+ height,
+ view_box,
+ role,
+ aria_label,
+ title,
+ desc,
+ defs,
+ elements,
+ } = self;
+
+ let mut out = String::new();
+ out.push_str(&format!(
+ "\n");
+ out
+ }
+ }
+
+ pub struct Defs {
+ content: Vec,
+ }
+
+ impl Defs {
+ pub fn new() -> Self {
+ Self {
+ content: Vec::new(),
+ }
+ }
+
+ pub fn linear_gradient(mut self, id: &str, gradient: Gradient) -> Self {
+ self.content.push(format!(" {}\n", gradient.render(id)));
+ self
+ }
+
+ pub fn filter(mut self, id: &str, filter: Filter) -> Self {
+ self.content.push(format!(" {}\n", filter.render(id)));
+ self
+ }
+
+ pub fn finish(self) -> String {
+ self.content.concat()
+ }
+ }
+
+ pub struct Gradient {
+ x1: String,
+ y1: String,
+ x2: String,
+ y2: String,
+ stops: Vec,
+ }
+
+ impl Gradient {
+ pub fn new(x1: &str, y1: &str, x2: &str, y2: &str) -> Self {
+ Self {
+ x1: x1.to_string(),
+ y1: y1.to_string(),
+ x2: x2.to_string(),
+ y2: y2.to_string(),
+ stops: Vec::new(),
+ }
+ }
+
+ pub fn stop(mut self, offset: &str, attrs: &[(&str, &str)]) -> Self {
+ let mut tag = format!("");
+ self.stops.push(tag);
+ self
+ }
+
+ fn render(&self, id: &str) -> String {
+ let mut out = format!(
+ "\n",
+ id, self.x1, self.y1, self.x2, self.y2
+ );
+ for stop in &self.stops {
+ out.push_str(" ");
+ out.push_str(stop);
+ out.push('\n');
+ }
+ out.push_str(" ");
+ out
+ }
+ }
+
+ pub struct Filter {
+ attrs: Vec<(String, String)>,
+ content: Vec,
+ }
+
+ impl Filter {
+ pub fn new() -> Self {
+ Self {
+ attrs: Vec::new(),
+ content: Vec::new(),
+ }
+ }
+
+ pub fn attr(mut self, key: &str, value: &str) -> Self {
+ self.attrs.push((key.to_string(), value.to_string()));
+ self
+ }
+
+ pub fn raw(mut self, markup: &str) -> Self {
+ self.content.push(format!(" {}\n", markup));
+ self
+ }
+
+ fn render(&self, id: &str) -> String {
+ let attrs = self
+ .attrs
+ .iter()
+ .map(|(k, v)| format!(" {}=\"{}\"", k, v))
+ .collect::();
+ let mut out = format!("\n", id, attrs);
+ for child in &self.content {
+ out.push_str(child);
+ }
+ out.push_str(" ");
+ out
+ }
+ }
+
+ pub struct Element {
+ tag: String,
+ attrs: Vec<(String, String)>,
+ content: Option,
+ }
+
+ impl Element {
+ pub fn new(tag: &str) -> Self {
+ Self {
+ tag: tag.to_string(),
+ attrs: Vec::new(),
+ content: None,
+ }
+ }
+
+ pub fn attr(mut self, key: &str, value: &str) -> Self {
+ self.attrs.push((key.to_string(), value.to_string()));
+ self
+ }
+
+ pub fn text(mut self, text: &str) -> String {
+ let content = format!("{}", text);
+ self.content = Some(content);
+ self.render()
+ }
+
+ pub fn empty(mut self) -> String {
+ self.content = None;
+ self.render()
+ }
+
+ fn render(&self) -> String {
+ let attrs = self
+ .attrs
+ .iter()
+ .map(|(k, v)| format!(" {}=\"{}\"", k, v))
+ .collect::();
+ if let Some(content) = &self.content {
+ format!(
+ " <{tag}{attrs}>{content}{tag}>",
+ tag = self.tag,
+ attrs = attrs,
+ content = content
+ )
+ } else {
+ format!(" <{tag}{attrs} />", tag = self.tag, attrs = attrs)
+ }
+ }
+ }
+
+ pub struct Group {
+ attrs: Vec<(String, String)>,
+ children: Vec,
+ }
+
+ impl Group {
+ pub fn new() -> Self {
+ Self {
+ attrs: Vec::new(),
+ children: Vec::new(),
+ }
+ }
+
+ pub fn attr(mut self, key: &str, value: &str) -> Self {
+ self.attrs.push((key.to_string(), value.to_string()));
+ self
+ }
+
+ pub fn child(mut self, element: String) -> Self {
+ self.children.push(element);
+ self
+ }
+
+ pub fn render(&self) -> String {
+ let attrs = self
+ .attrs
+ .iter()
+ .map(|(k, v)| format!(" {}=\"{}\"", k, v))
+ .collect::();
+ let mut out = format!(" \n", attrs);
+ for child in &self.children {
+ out.push_str(child);
+ out.push('\n');
+ }
+ out.push_str(" ");
+ out
+ }
+ }
+
+ impl From for String {
+ fn from(group: Group) -> Self {
+ group.render()
+ }
+ }
+
+ impl From for String {
+ fn from(element: Element) -> Self {
+ element.render()
+ }
+ }
+
+ pub fn path(d: &str) -> String {
+ Element::new("path").attr("d", d).empty()
+ }
+}
diff --git a/src/bin/readme_gen.rs b/src/bin/readme_gen.rs
index cf33e2b..ab0390e 100644
--- a/src/bin/readme_gen.rs
+++ b/src/bin/readme_gen.rs
@@ -136,6 +136,8 @@ llvm-profdata merge -o target/pgo-data/lpkg.profdata target/pgo-data/*.profraw
RUSTFLAGS="-Cprofile-use=target/pgo-data/lpkg.profdata -Cllvm-args=-pgo-warn-missing-function" \
cargo pgo-build"#,
)
+ .paragraph("Regenerate project artefacts (README and SVG logo):")
+ .code_block("bash", "cargo run --bin readme_gen\ncargo run --bin logo_gen")
.paragraph("Run tests:")
.code_block("bash", "cargo test")
.paragraph("You can also run the project directly in the flake shell:")