2026-07-18 23:27:41 +02:00
|
|
|
/*
|
|
|
|
|
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;
|
|
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
// 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");
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 23:27:41 +02:00
|
|
|
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
2026-08-05 04:18:37 +02:00
|
|
|
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, 7> 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",
|
|
|
|
|
};
|
|
|
|
|
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");
|
|
|
|
|
|
2026-08-05 05:41:22 +02:00
|
|
|
// 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.
|
|
|
|
|
// $ORIGIN makes the loader also look next to the binary itself, and CI
|
|
|
|
|
// ships the .so alongside it into /srv/catcrafts-app.
|
|
|
|
|
cfg.linkFlags.push_back("-Wl,-rpath,$ORIGIN");
|
|
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
ApplyReleaseTrimming(cfg);
|
|
|
|
|
return cfg;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── web (default) ─────────────────────────────────────────────────
|
2026-07-18 23:27:41 +02:00
|
|
|
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);
|
|
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
// 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 = {
|
2026-07-18 23:27:41 +02:00
|
|
|
"interfaces/Catcrafts",
|
|
|
|
|
"interfaces/Catcrafts-Views",
|
|
|
|
|
"interfaces/Catcrafts-Root",
|
2026-07-19 01:13:30 +02:00
|
|
|
"interfaces/Catcrafts-Demo",
|
2026-07-18 23:27:41 +02:00
|
|
|
};
|
2026-08-05 04:18:37 +02:00
|
|
|
std::array<fs::path, 4> impls = {
|
2026-07-18 23:27:41 +02:00
|
|
|
"implementations/main",
|
|
|
|
|
"implementations/Catcrafts-Root",
|
|
|
|
|
"implementations/Catcrafts-Views",
|
2026-07-19 01:13:30 +02:00
|
|
|
"implementations/Catcrafts-Demo",
|
2026-07-18 23:27:41 +02:00
|
|
|
};
|
|
|
|
|
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
|
|
|
|
|
|
|
|
|
cfg.files.emplace_back(fs::path("styles/styles.css"));
|
|
|
|
|
cfg.files.emplace_back(fs::path("robots.txt"));
|
2026-08-05 04:18:37 +02:00
|
|
|
// 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.
|
2026-07-18 23:27:41 +02:00
|
|
|
cfg.files.emplace_back(fs::path("sitemap.xml"));
|
2026-08-05 04:18:37 +02:00
|
|
|
cfg.files.emplace_back(fs::path("feed.xml"));
|
2026-07-18 23:27:41 +02:00
|
|
|
cfg.files.emplace_back(fs::path("favicon.svg"));
|
2026-07-19 01:13:30 +02:00
|
|
|
// 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"));
|
2026-07-18 23:27:41 +02:00
|
|
|
// 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"));
|
2026-08-05 04:18:37 +02:00
|
|
|
// 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"));
|
2026-07-18 23:27:41 +02:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
ApplyReleaseTrimming(cfg);
|
2026-07-18 23:27:41 +02:00
|
|
|
EnableWasiBrowserRuntime(cfg);
|
|
|
|
|
|
|
|
|
|
return cfg;
|
|
|
|
|
}
|