/* 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` // 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 args) { static auto shared = std::make_unique(); 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 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 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 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 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 ifaces = { "server/interfaces/Catcrafts.Server", }; std::array 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"); // 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 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 sharedArgs(depArgs.begin(), depArgs.end()); cfg.dependencies = { graphics, SharedLibrary(sharedArgs) }; std::array ifaces = { "interfaces/Catcrafts", "interfaces/Catcrafts-Views", "interfaces/Catcrafts-Root", "interfaces/Catcrafts-Demo", }; std::array 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