mirror of
https://github.com/Noratrieb/website.git
synced 2026-01-14 08:55:01 +01:00
hot girl shit
This commit is contained in:
parent
ce88f65f92
commit
ce8c65aeaf
15 changed files with 263 additions and 184 deletions
1452
builder/Cargo.lock
generated
Normal file
1452
builder/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
17
builder/Cargo.toml
Normal file
17
builder/Cargo.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "website-builder"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
color-eyre = "0.6.2"
|
||||
notify = "6.1.1"
|
||||
rand = "0.8.5"
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
serde_derive = "1.0.195"
|
||||
tera = "1.19.1"
|
||||
toml = "0.8.8"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
12
builder/default.nix
Normal file
12
builder/default.nix
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{ pkgs ? import <nixpkgs> { }, ... }: pkgs.rustPlatform.buildRustPackage {
|
||||
pname = "website-builder";
|
||||
version = "0.1.0";
|
||||
|
||||
src = ./.;
|
||||
|
||||
check = false;
|
||||
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
};
|
||||
}
|
||||
37
builder/src/build/blog.rs
Normal file
37
builder/src/build/blog.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
//! Builds my blog, built with hugo.
|
||||
|
||||
use std::{path::Path, process::Command};
|
||||
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
|
||||
use crate::utils;
|
||||
|
||||
pub fn build(blog: &Path, dist: &Path) -> Result<()> {
|
||||
info!("Building blog with hugo");
|
||||
|
||||
utils::run_process(
|
||||
Command::new("git")
|
||||
.args(["submodule", "init"])
|
||||
.current_dir(blog),
|
||||
)?;
|
||||
|
||||
utils::run_process(
|
||||
Command::new("git")
|
||||
.args(["submodule", "update"])
|
||||
.current_dir(blog),
|
||||
)?;
|
||||
|
||||
// Patch config
|
||||
let config =
|
||||
std::fs::read_to_string(blog.join("config.toml")).wrap_err("reading blog config")?;
|
||||
let config = config.replace("baseURL = \"/\"", "baseURL = \"/blog/\"");
|
||||
std::fs::write(blog.join("config.toml"), config).wrap_err("writing patched config.toml")?;
|
||||
|
||||
utils::run_process(
|
||||
Command::new("hugo")
|
||||
.args(["--minify", "--destination", dist.to_str().unwrap()])
|
||||
.current_dir(blog),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
39
builder/src/build/mod.rs
Normal file
39
builder/src/build/mod.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
//! This module assembles and builds the website.
|
||||
|
||||
mod blog;
|
||||
mod slides;
|
||||
mod statics;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
|
||||
use crate::Config;
|
||||
|
||||
pub fn assemble_website(
|
||||
rng: &mut rand::rngs::StdRng,
|
||||
config: &Config,
|
||||
statics: &Path,
|
||||
submodules: &Path,
|
||||
dist: &Path,
|
||||
) -> Result<()> {
|
||||
blog::build(&submodules.join("blog"), &dist.join("blog")).wrap_err("building blog")?;
|
||||
slides::build(
|
||||
&config.slides,
|
||||
&submodules.join("slides"),
|
||||
&dist.join("slides"),
|
||||
)
|
||||
.wrap_err("building slides")?;
|
||||
|
||||
statics::build(rng, &config.slides, statics, dist).wrap_err("building root files")?;
|
||||
|
||||
add_cname(dist)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_cname(dist: &Path) -> Result<()> {
|
||||
let cname = "nilstrieb.dev\n";
|
||||
std::fs::write(dist.join("CNAME"), cname).wrap_err("writing cname")?;
|
||||
Ok(())
|
||||
}
|
||||
26
builder/src/build/slides.rs
Normal file
26
builder/src/build/slides.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
//! Moving the slides from the reveal.js repo
|
||||
//! The setup is currently a bit bad but I'm not sure what the best solution would look like.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use color_eyre::{eyre::WrapErr, Result};
|
||||
|
||||
use crate::{utils, SlidesConfig};
|
||||
|
||||
pub fn build(config: &SlidesConfig, slides: &Path, dist: &Path) -> Result<()> {
|
||||
info!("Building slides");
|
||||
|
||||
debug!("Copying reveal.js dist");
|
||||
|
||||
utils::cp_r(&slides.join("dist"), &dist.join("dist")).wrap_err("copying reveal.js dist")?;
|
||||
utils::cp_r(&slides.join("plugin"), &dist.join("plugin")).wrap_err("copying reveal.js dist")?;
|
||||
|
||||
for talk in &config.talks {
|
||||
let path = slides.join(talk.dir_name());
|
||||
let dist = dist.join(talk.dir_name());
|
||||
|
||||
utils::cp_r(&path, &dist).wrap_err("copying slide data")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
49
builder/src/build/statics.rs
Normal file
49
builder/src/build/statics.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
//! Root index.html and some other static stuff
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use color_eyre::{eyre::WrapErr, Result};
|
||||
use rand::seq::SliceRandom;
|
||||
|
||||
use crate::{utils, SlidesConfig};
|
||||
|
||||
pub fn build(
|
||||
rng: &mut rand::rngs::StdRng,
|
||||
config: &SlidesConfig,
|
||||
statics: &Path,
|
||||
dist: &Path,
|
||||
) -> Result<()> {
|
||||
let back_alley_name = b"abcdefghijklmnopqrstuvwxyz"
|
||||
.choose_multiple(rng, 6)
|
||||
.map(|&c| char::from_u32(c.into()).unwrap())
|
||||
.collect::<String>();
|
||||
|
||||
let back_alley_name = format!("back-alley-{back_alley_name}.html");
|
||||
|
||||
let mut context = tera::Context::new();
|
||||
|
||||
context.insert("back_alley_name", back_alley_name.as_str());
|
||||
context.insert("talks", &config.talks);
|
||||
|
||||
utils::copy_fn(&statics.join("root"), dist, |content, ext, opts| {
|
||||
if ext.is_some_and(|ext| matches!(ext, "html" | "css")) {
|
||||
if opts.dest_path.ends_with("back-alley.html") {
|
||||
opts.dest_path.set_file_name(&back_alley_name);
|
||||
}
|
||||
|
||||
let content = String::from_utf8(content).wrap_err("HTML or CSS is invalid UTF-8")?;
|
||||
let mut tera = tera::Tera::default();
|
||||
tera.add_raw_template("template", &content)
|
||||
.wrap_err("parsing template")?;
|
||||
return tera
|
||||
.render("template", &context)
|
||||
.wrap_err("failed to render")
|
||||
.map(String::into_bytes);
|
||||
}
|
||||
|
||||
Ok(content)
|
||||
})
|
||||
.wrap_err("copying root files")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
184
builder/src/main.rs
Normal file
184
builder/src/main.rs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
mod build;
|
||||
mod submodule;
|
||||
mod utils;
|
||||
|
||||
#[macro_use]
|
||||
extern crate tracing;
|
||||
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::{self, Stdio},
|
||||
time,
|
||||
};
|
||||
|
||||
use color_eyre::{
|
||||
eyre::{bail, eyre, Context},
|
||||
Result,
|
||||
};
|
||||
use notify::{RecursiveMode, Watcher};
|
||||
use rand::SeedableRng;
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Serialize;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Config {
|
||||
slides: SlidesConfig,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct SlidesConfig {
|
||||
talks: Vec<Talk>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
struct Talk {
|
||||
name: String,
|
||||
date: String,
|
||||
location: String,
|
||||
#[serde(skip_deserializing)]
|
||||
dir_name: String,
|
||||
}
|
||||
|
||||
impl Talk {
|
||||
fn dir_name(&self) -> String {
|
||||
format!(
|
||||
"{}-{}",
|
||||
self.date,
|
||||
self.name.replace(' ', "-").to_lowercase()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy(),
|
||||
)
|
||||
.init();
|
||||
|
||||
let root = std::env::current_dir().wrap_err("getting current directory")?;
|
||||
|
||||
// Set the current dir to nonsense to fail everything that relies on it
|
||||
let _ = std::env::set_current_dir("/");
|
||||
|
||||
match std::env::args().nth(1).as_deref() {
|
||||
Some("clean") => {
|
||||
info!("Cleaning dist");
|
||||
match std::fs::remove_dir_all(root.join("dist")) {
|
||||
Ok(()) => {}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
e => return e.wrap_err("removing dist"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Some("watch") => watch(root),
|
||||
Some("build") => {
|
||||
let mut rng = match std::env::var("WEBSITE_RNG_SEED") {
|
||||
Ok(seed) => rand::rngs::StdRng::seed_from_u64(
|
||||
seed.parse().wrap_err("WEBSITE_RNG_SEED must be a u64")?,
|
||||
),
|
||||
Err(_) => rand::rngs::StdRng::from_entropy(),
|
||||
};
|
||||
build(&mut rng, &root)
|
||||
}
|
||||
Some(cmd) => bail!("invalid subcommand {cmd}"),
|
||||
None => bail!("no subcommand provided"),
|
||||
}
|
||||
}
|
||||
|
||||
fn watch(root: PathBuf) -> Result<()> {
|
||||
let seed: u64 = rand::random();
|
||||
|
||||
let rng = move || rand::rngs::StdRng::seed_from_u64(seed);
|
||||
|
||||
build(&mut rng(), &root).wrap_err("initial build")?;
|
||||
let (send, recv) = std::sync::mpsc::sync_channel(1);
|
||||
let mut watcher = notify::recommended_watcher(move |res| match res {
|
||||
Ok(_) => {
|
||||
let _ = send.send(());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("watch error: {e:?}");
|
||||
}
|
||||
})
|
||||
.wrap_err("creating watcher")?;
|
||||
|
||||
watcher.watch(&root.join("static"), RecursiveMode::Recursive)?;
|
||||
watcher.watch(&root.join("config.toml"), RecursiveMode::NonRecursive)?;
|
||||
|
||||
info!("Starting webserver");
|
||||
let root1 = root.clone();
|
||||
std::thread::spawn(move || {
|
||||
let root = root1;
|
||||
let run = || -> Result<()> {
|
||||
let path = root.join("dist");
|
||||
let mut server = process::Command::new("live-server");
|
||||
server
|
||||
.current_dir(path)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
|
||||
let mut child = server.spawn().wrap_err("failed to spawn `live-server`.\
|
||||
Install https://github.com/tapio/live-server into your PATH, for example with nix, see shell.nix")?;
|
||||
let exit = child.wait().wrap_err("interrupt waiting for live-server")?;
|
||||
bail!("live-server exited early, exit: {exit}");
|
||||
};
|
||||
|
||||
if let Err(e) = run() {
|
||||
error!(?e);
|
||||
process::exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
info!("Starting loop");
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let mut last = time::SystemTime::now();
|
||||
for () in recv {
|
||||
let now = time::SystemTime::now();
|
||||
if now.duration_since(last).unwrap_or_default().as_millis() < 500 {
|
||||
continue;
|
||||
}
|
||||
|
||||
last = now;
|
||||
info!("Received update, rebuilding");
|
||||
if let Err(e) = build(&mut rng(), &root) {
|
||||
error!(?e);
|
||||
}
|
||||
}
|
||||
})
|
||||
.join()
|
||||
.map_err(|_| eyre!("build thread panicked"))
|
||||
}
|
||||
|
||||
fn build(rng: &mut rand::rngs::StdRng, root: &Path) -> Result<()> {
|
||||
let config =
|
||||
std::fs::read_to_string(root.join("config.toml")).wrap_err("reading config.toml")?;
|
||||
|
||||
let mut config = toml::from_str::<Config>(&config).wrap_err("parsing config.toml")?;
|
||||
config.slides.talks.iter_mut().for_each(|talk| {
|
||||
talk.dir_name = talk.dir_name();
|
||||
});
|
||||
|
||||
let sub_config = std::fs::read_to_string(root.join("submodules.toml"))
|
||||
.wrap_err("reading submodules.toml")?;
|
||||
let sub_config =
|
||||
submodule::Submodules::parse(&sub_config).wrap_err("invalid submodules.toml")?;
|
||||
let submodules_path = root.join("submodules");
|
||||
submodule::sync(&submodules_path, &sub_config).wrap_err("syncing subtrees")?;
|
||||
|
||||
let dist_path = root.join("dist");
|
||||
build::assemble_website(
|
||||
rng,
|
||||
&config,
|
||||
&root.join("static"),
|
||||
&submodules_path,
|
||||
&dist_path,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
132
builder/src/submodule.rs
Normal file
132
builder/src/submodule.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
//! Implements a simplistic form of git submodules.
|
||||
//!
|
||||
//! The config format is as follows:
|
||||
//!
|
||||
//! A list of
|
||||
//! ```toml
|
||||
//! [[submodule]]
|
||||
//! name = ""
|
||||
//! url = ""
|
||||
//! commit = ""
|
||||
//! ```
|
||||
//!
|
||||
//! For example,
|
||||
//! ```toml
|
||||
//! [[submodule]]
|
||||
//! name = "nixos"
|
||||
//! url = "https://github.com/Noratrieb/nixos.git"
|
||||
//! commit = "c5b2fc10b9266b105d792d958b8f13479866a7bd"
|
||||
//! ```
|
||||
//!
|
||||
//! This module will check them out into a directory called `submodules` in the current directory.
|
||||
//! Make sure to put this directory into `.gitignore`.
|
||||
|
||||
use std::{path::Path, process};
|
||||
|
||||
use color_eyre::{
|
||||
eyre::{Context, OptionExt},
|
||||
Result,
|
||||
};
|
||||
|
||||
use crate::utils;
|
||||
|
||||
pub struct Submodules {
|
||||
configs: Vec<SyncConfig>,
|
||||
}
|
||||
|
||||
pub struct SyncConfig {
|
||||
name: String,
|
||||
url: String,
|
||||
commit: String,
|
||||
}
|
||||
|
||||
impl Submodules {
|
||||
pub fn parse(s: &str) -> Result<Submodules> {
|
||||
let doc = s.parse::<toml::Table>().wrap_err("invalid toml")?;
|
||||
let subs = doc
|
||||
.get("submodule")
|
||||
.ok_or_eyre("no top-level submodule tables")?;
|
||||
let mods = subs.as_array().ok_or_eyre("submodule is not an array")?;
|
||||
|
||||
let mut configs = Vec::new();
|
||||
|
||||
for module in mods {
|
||||
let map = module.as_table().ok_or_eyre("submodule is not a table")?;
|
||||
|
||||
let get_str = |name| -> Result<String> {
|
||||
Ok(map
|
||||
.get(name)
|
||||
.ok_or_eyre(format!("{name} is missing"))?
|
||||
.as_str()
|
||||
.ok_or_eyre(format!("{name} is not a string"))?
|
||||
.into())
|
||||
};
|
||||
|
||||
configs.push(SyncConfig {
|
||||
name: get_str("name")?,
|
||||
url: get_str("url")?,
|
||||
commit: get_str("commit")?,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self { configs })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sync(path: &Path, config: &Submodules) -> color_eyre::Result<()> {
|
||||
info!("Syncing submodules...");
|
||||
|
||||
utils::create_dir_if_not_exist(path)?;
|
||||
|
||||
for sync in &config.configs {
|
||||
let name = &sync.name;
|
||||
let url = sync.url.as_str();
|
||||
|
||||
let span = info_span!("Syncing submodule", ?name, ?url);
|
||||
let _span = span.enter();
|
||||
|
||||
let sub_path = path.join(name);
|
||||
if !sub_path.exists() {
|
||||
info!(?name, ?url, "Cloning");
|
||||
let mut cmd = process::Command::new("git");
|
||||
cmd.args(["clone", url, sub_path.to_str().unwrap()]);
|
||||
utils::run_process(&mut cmd)?;
|
||||
} else {
|
||||
debug!(?name, ?url, "Repo already exists");
|
||||
}
|
||||
|
||||
let current_commit = utils::run_process(
|
||||
process::Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.current_dir(&sub_path),
|
||||
)
|
||||
.wrap_err("running git rev-parse HEAD")?;
|
||||
|
||||
debug!(?current_commit, "Current commit");
|
||||
|
||||
if current_commit.trim() != sync.commit {
|
||||
info!("Need to change commit");
|
||||
let commit_exists = utils::run_process(
|
||||
process::Command::new("git")
|
||||
.args(["cat-file", "-t", sync.commit.as_str()])
|
||||
.current_dir(&sub_path),
|
||||
);
|
||||
if !commit_exists.is_ok_and(|typ| typ == *"commit\n") {
|
||||
info!("Must fetch commit");
|
||||
utils::run_process(process::Command::new("git").current_dir(&sub_path).args([
|
||||
"fetch",
|
||||
"origin",
|
||||
sync.commit.as_str(),
|
||||
]))?;
|
||||
}
|
||||
|
||||
utils::run_process(process::Command::new("git").current_dir(&sub_path).args([
|
||||
"reset",
|
||||
"--hard",
|
||||
sync.commit.as_str(),
|
||||
]))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
97
builder/src/utils.rs
Normal file
97
builder/src/utils.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
use color_eyre::{
|
||||
eyre::{bail, Context},
|
||||
Result,
|
||||
};
|
||||
use std::{fs, io, os::unix::ffi::OsStrExt, path::{Path, PathBuf}, process::Command};
|
||||
|
||||
pub fn run_process(cmd: &mut Command) -> Result<String> {
|
||||
fn run_process_inner(cmd: &mut Command) -> Result<String> {
|
||||
let name = cmd.get_program().to_os_string();
|
||||
let output = cmd
|
||||
.output()
|
||||
.wrap_err(format!("failed to spawn process {name:?}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"command returned error: {}",
|
||||
String::from_utf8(output.stderr).wrap_err("stderr is not UTF-8")?
|
||||
);
|
||||
}
|
||||
|
||||
String::from_utf8(output.stdout).wrap_err("stdout is not UTF-8")
|
||||
}
|
||||
run_process_inner(cmd).wrap_err(format!(
|
||||
"{} {}",
|
||||
cmd.get_program().to_str().unwrap(),
|
||||
cmd.get_args()
|
||||
.map(|arg| format!("'{}'", arg.to_str().unwrap()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
))
|
||||
}
|
||||
|
||||
pub fn create_dir_if_not_exist(p: &Path) -> Result<()> {
|
||||
match std::fs::create_dir(p) {
|
||||
Ok(()) => debug!(?p, "Created directory"),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
|
||||
e => return e.wrap_err("failed to create submodules"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cp_r(from: &Path, to: &Path) -> Result<()> {
|
||||
copy_fn(from, to, |content, _, _| Ok(content))
|
||||
}
|
||||
|
||||
pub struct CopyOpts {
|
||||
pub dest_path: PathBuf,
|
||||
}
|
||||
|
||||
pub fn copy_fn(
|
||||
from: &Path,
|
||||
to: &Path,
|
||||
mut map: impl FnMut(Vec<u8>, Option<&str>, &mut CopyOpts) -> Result<Vec<u8>>,
|
||||
) -> Result<()> {
|
||||
let mut worklist = vec![from.to_owned()];
|
||||
|
||||
while let Some(ref item) = worklist.pop() {
|
||||
let mut process = || -> Result<()> {
|
||||
let meta = fs::metadata(item).wrap_err("getting metadata")?;
|
||||
let relative = item.strip_prefix(from).wrap_err("subpath stripping")?;
|
||||
let dest = to.join(relative);
|
||||
|
||||
if meta.is_dir() {
|
||||
let items = fs::read_dir(item).wrap_err("read_dir")?;
|
||||
for item in items {
|
||||
let item = item.wrap_err("entry")?;
|
||||
worklist.push(item.path());
|
||||
}
|
||||
|
||||
match fs::create_dir_all(&dest) {
|
||||
Ok(()) => {}
|
||||
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
|
||||
e => return e.wrap_err_with(|| format!("creating {}", dest.display())),
|
||||
}
|
||||
} else {
|
||||
let content = fs::read(item).wrap_err("reading file")?;
|
||||
let ext = match item.extension() {
|
||||
Some(ext) => Some(
|
||||
std::str::from_utf8(ext.as_bytes())
|
||||
.wrap_err("file extension is invalid UTF-8")?,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
let mut opts = CopyOpts {
|
||||
dest_path: dest.clone(),
|
||||
};
|
||||
let result = map(content, ext, &mut opts).wrap_err("applying mapping")?;
|
||||
fs::write(opts.dest_path, result)
|
||||
.wrap_err_with(|| format!("creating {}", dest.display()))?;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
process().wrap_err_with(|| format!("copying {}", item.display()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue