catcrafts.net/project.cpp
Jorijn van der Graaf 2fa6e70af1
All checks were successful
Deploy / build-deploy (push) Successful in 6m14s
order confirmation
2026-08-09 00:14:09 +02:00

244 lines
11 KiB
C++

/*
catcrafts.net
Copyright (C) 2026 Catcrafts
The source code of this website is made available for viewing purposes only.
No permission is granted to copy, modify, distribute, or create derivative works.
*/
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
// Two products come out of this repo, selected with `--product=`:
//
// web (default) — the wasm32-wasip1 browser bundle. Crafter.Graphics for
// the DOM, Catcrafts.Shared for the page renderers.
// crafter-build
//
// server — the native HTTP server that will render the same pages
// server-side for crawlers and no-JS clients, and later
// host the shop API. No Crafter.Graphics.
// crafter-build -- --product=server
//
// One project file rather than three, following the imsd convention: a single
// CrafterBuildProject returns a single Configuration, so the products are
// branches and the shared library is a `static unique_ptr<Configuration>`
// both branches can depend on.
// Catcrafts.Shared — target-neutral page renderers, built as a static library
// for whichever target the selected product is using. `static` because the
// Configuration must outlive this function: cfg.dependencies holds a raw
// pointer to it.
//
// `args` MUST carry the consumer's --target=. ApplyStandardArgs is what reads
// it, and without it the library silently builds for the host triple while a
// wasm consumer links against it — which currently "works" only because there
// are no implementation units, so the static archive is empty. The moment a
// .cpp lands here that would be a wrong-architecture link.
static Configuration* SharedLibrary(std::span<const std::string_view> args) {
static auto shared = std::make_unique<Configuration>();
shared->path = "./";
shared->name = "Catcrafts.Shared";
shared->outputName = "Catcrafts.Shared";
ApplyStandardArgs(*shared, args); // inherits --target / --debug from the parent
shared->type = ConfigurationType::LibraryStatic;
std::array<fs::path, 9> ifaces = {
"shared/interfaces/Catcrafts.Shared",
"shared/interfaces/Catcrafts.Shared-Html",
"shared/interfaces/Catcrafts.Shared-Form",
"shared/interfaces/Catcrafts.Shared-Json",
"shared/interfaces/Catcrafts.Shared-Model",
"shared/interfaces/Catcrafts.Shared-Content",
"shared/interfaces/Catcrafts.Shared-Money",
"shared/interfaces/Catcrafts.Shared-Route",
"shared/interfaces/Catcrafts.Shared-Views",
};
std::array<fs::path, 0> impls = {};
shared->GetInterfacesAndImplementations(ifaces, impls);
return shared.get();
}
// Payload trimming, release only.
//
// The wasi-libc / wasi-libc++ static libs ship with DWARF and wasm-ld keeps
// debug sections by default, so a build that never passed -g still ended up
// ~84% debug info (3.3 MB of 3.9 MB). Stripping takes the bundle from
// 3.9 MB / 1.05 MB gzip to ~700 KB / ~195 KB gzip.
static void ApplyReleaseTrimming(Configuration& cfg) {
if (cfg.debug) return; // --debug builds want their symbols
cfg.compileFlags.push_back("-ffunction-sections");
cfg.compileFlags.push_back("-fdata-sections");
cfg.linkFlags.push_back("-Wl,--gc-sections");
cfg.linkFlags.push_back("-Wl,--strip-debug");
}
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
bool wantServer = false;
for (std::string_view a : args) {
if (a == "--product=server") wantServer = true;
}
// ── server ────────────────────────────────────────────────────────
if (wantServer) {
// Crafter.Network supplies the HTTP layer. Notably NOT cpp-httplib or
// libcurl: ListenerHTTP1 covers the inbound side (TLS is Caddy's job —
// it terminates and reverse-proxies plaintext to localhost, which is
// exactly why HTTP/1.1 rather than the HTTP/3 listener, since Caddy
// cannot proxy to an h3 upstream), and ClientHTTP1 with
// TLSClientCredentials covers outbound HTTPS for the payment and
// shipping APIs later — it verifies certificate chain and hostname by
// default.
std::vector<std::string> netArgs(args.begin(), args.end());
bool useLocalNet = false;
for (std::string_view a : args) {
if (a == "--local") { useLocalNet = true; break; }
}
Configuration* network = useLocalNet
? LocalProject({
.projectFile = "../Crafter/Crafter.Network/project.cpp",
.args = netArgs,
})
: GitProject({
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Network.git" },
.args = netArgs,
});
Configuration cfg;
cfg.path = "./";
cfg.name = "Catcrafts.Server";
cfg.outputName = "catcrafts-server";
cfg.type = ConfigurationType::Executable;
ApplyStandardArgs(cfg, args);
cfg.dependencies = { SharedLibrary(args), network };
std::array<fs::path, 1> ifaces = {
"server/interfaces/Catcrafts.Server",
};
std::array<fs::path, 8> impls = {
"server/implementations/main",
"server/implementations/Catcrafts.Server-Http",
"server/implementations/Catcrafts.Server-Orders",
"server/implementations/Catcrafts.Server-Mollie",
"server/implementations/Catcrafts.Server-Invoice",
"server/implementations/Catcrafts.Server-Bunq",
"server/implementations/Catcrafts.Server-Shipping",
"server/implementations/Catcrafts.Server-Mail",
};
cfg.GetInterfacesAndImplementations(ifaces, impls);
// The bunq client signs requests with an RSA key (OpenSSL EVP). The
// TLS transport already links libssl through Crafter.Network; libcrypto
// is named explicitly because the signing code calls it directly.
cfg.linkFlags.push_back("-lssl");
cfg.linkFlags.push_back("-lcrypto");
// libmsquic.so.2 is built in crafter-build's external cache, and the
// RUNPATH pointing there only exists on the build machine — the first
// deploy to a real host died in the loader (exit 127) because of it.
// CI ships the .so alongside the binary into /srv/catcrafts-app, and
// this rpath makes the loader look there. The path is spelled out
// rather than $ORIGIN: the flag passes through a shell on its way to
// the linker, where $ORIGIN expands to empty — leaving a bare -rpath
// that swallows the next input file (libCatcrafts.Shared.a) and fails
// the link with every Shared symbol undefined.
cfg.linkFlags.push_back("-Wl,-rpath,/srv/catcrafts-app");
ApplyReleaseTrimming(cfg);
return cfg;
}
// ── web (default) ─────────────────────────────────────────────────
std::vector<std::string> depArgs(args.begin(), args.end());
depArgs.push_back("--target=wasm32-wasip1");
// --local resolves the Crafter.Graphics dep from a sibling working tree
// instead of fetching it from forgejo. Mirrors Crafter.Graphics's own
// project.cpp convention so edits across the two repos pick up without
// commit-and-pull.
bool useLocal = false;
for (std::string_view a : args) {
if (a == "--local") { useLocal = true; break; }
}
if (useLocal && std::find(depArgs.begin(), depArgs.end(), std::string("--local")) == depArgs.end()) {
depArgs.push_back("--local");
}
Configuration* graphics = useLocal
? LocalProject({
.projectFile = "../Crafter/Crafter.Graphics/project.cpp",
.args = depArgs,
})
: GitProject({
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Graphics.git" },
.args = depArgs,
});
Configuration cfg;
cfg.path = "./";
cfg.name = "Catcrafts.Net";
cfg.outputName = "catcrafts";
cfg.type = ConfigurationType::Executable;
cfg.target = "wasm32-wasip1";
ApplyStandardArgs(cfg, args);
// Catcrafts.Shared has to be built for wasm here, not the host. `args` on
// its own does not carry --target= (the caller just runs `crafter-build`),
// so hand it the same augmented list the Crafter.Graphics dep gets.
std::vector<std::string_view> sharedArgs(depArgs.begin(), depArgs.end());
cfg.dependencies = { graphics, SharedLibrary(sharedArgs) };
std::array<fs::path, 4> ifaces = {
"interfaces/Catcrafts",
"interfaces/Catcrafts-Views",
"interfaces/Catcrafts-Root",
"interfaces/Catcrafts-Demo",
};
std::array<fs::path, 4> impls = {
"implementations/main",
"implementations/Catcrafts-Root",
"implementations/Catcrafts-Views",
"implementations/Catcrafts-Demo",
};
cfg.GetInterfacesAndImplementations(ifaces, impls);
cfg.files.emplace_back(fs::path("styles/styles.css"));
cfg.files.emplace_back(fs::path("robots.txt"));
// sitemap.xml and feed.xml are GENERATED by the server product before this
// build runs (see .forgejo/workflows/deploy.yaml), from the same route
// table and Post model the pages use. Checked-in copies would drift.
cfg.files.emplace_back(fs::path("sitemap.xml"));
cfg.files.emplace_back(fs::path("feed.xml"));
cfg.files.emplace_back(fs::path("favicon.svg"));
// WGSL for the ray-traced WebGPU demo embedded in the blog (see
// interfaces/Catcrafts-Demo.cppm). Fetched at runtime by WebGPUShader.
cfg.files.emplace_back(fs::path("shaders/raygen.wgsl"));
cfg.files.emplace_back(fs::path("shaders/miss.wgsl"));
cfg.files.emplace_back(fs::path("shaders/closesthit.wgsl"));
cfg.files.emplace_back(fs::path("shaders/resolve.wgsl"));
// Loaded as a <script type="module"> before runtime.js by
// EnableWasiBrowserRuntime — sets <title>/<link> tags since the
// Dom partition has no head-element access.
cfg.files.emplace_back(fs::path("catcrafts-head.js"));
// Product photography. CC BY-SA 4.0, © Fairphone (official render via
// Wikimedia Commons) — the attribution lives on /legal/imprint. NOT in
// cfg.assets: that pipeline transcodes to .ctex, which <img> cannot decode.
cfg.files.emplace_back(fs::path("images/fp6-pmos.jpg"));
// ECB rates ride with the content so the wasm-rendered shop (backend-down
// fallback) can emit the same indicative prices the server does.
cfg.files.emplace_back(fs::path("content/rates.json"));
// Site content. Loaded from the VFS at startup rather than compiled in, so
// editing a project blurb or refreshing the post list is not a recompile.
// NOTE: cfg.files flattens to the bundle root (copied by filename), so
// this is read back as "posts.json". Products, projects, legal and demos
// are COMPILED IN (Catcrafts.Shared:Content) — only pipeline-generated
// data still travels as files.
cfg.files.emplace_back(fs::path("content/posts.json"));
ApplyReleaseTrimming(cfg);
EnableWasiBrowserRuntime(cfg);
return cfg;
}