Skip to content

Obelisk v2: vanilla cabal builds, WASM frontend, Nix module system and simplified architecture#1139

Open
ymeister wants to merge 56 commits into
developfrom
next
Open

Obelisk v2: vanilla cabal builds, WASM frontend, Nix module system and simplified architecture#1139
ymeister wants to merge 56 commits into
developfrom
next

Conversation

@ymeister
Copy link
Copy Markdown
Collaborator

@ymeister ymeister commented Mar 22, 2026

Summary

Complete rewrite of the nix build system around nix-haskell and a NixOS-style
module system. Replaces reflex-platform with direct haskell.nix integration.
Adds WASM frontend target alongside GHCJS. Adds GHC 9.14 support.

Vanilla cabal builds

  • Projects can now be built with plain cabal build / cabal run without any
    nix wrapper. The ob CLI tool is no longer required for development.
  • ob-run development script uses cabal run backend directly with inotifywait
    for file watching, forwarding optimization flags to cross-compilation builds.
  • ob-repl starts cabal repl with optimizations disabled for fast iteration.
  • Custom Setup.hs hooks (obelisk-setup) handle cross-compilation of the
    frontend (WASM or GHCJS) and static asset generation transparently during
    a normal cabal build, with no nix involvement at build time.

Nix module system

  • New nix/module.nix module with declarative options:
    • obelisk.static.path / obelisk.static.compress / obelisk.static.compressed: static asset pipeline with cache-busting hashes, brotli + gzip compression
    • obelisk.frontend.target: select "js" (GHCJS) or "wasm" (default) frontend compilation target
    • obelisk.frontend.js.{package,optimization,optimized,compress,compressed}: GHCJS pipeline with closure-compiler (ADVANCED mode by default)
    • obelisk.frontend.wasm.{package,optimization,optimized,compress,compressed}: WASM pipeline with wasm-opt + wasm-tools strip
  • New nix/lib.nix with composable haskell.nix overrides: buildTypeOverride, staticManifestOverride, frontendDataOverride, backendDataOverride, jsexeOverride
  • All overrides use mkOptionalPackages to safely skip absent packages in cross-compilation projects
  • Default compiler: GHC 9.14 (compiler-nix-name = "ghc914")
  • nix/assets.nix asset compression pipeline with brotli (quality 11) + gzip via mkAssets / unionEncodings

WASM frontend

  • Complete wasm32-wasi frontend compilation target producing a frontend.jsexe directory the backend serves unchanged
  • Browser bootstrap shim (nix/wasm/shim.js) using async IIFE with dynamic import(), resolving assets relative to script URL
  • Vendored @bjorn3/browser_wasi_shim for WASI browser support
  • wasm-opt optimization and wasm-tools strip for production builds
  • GHC JSFFI extraction via post-link.mjs

obelisk-setup

  • New obelisk-setup package with reusable Setup.hs hooks:
    • Obelisk.Setup.Backend: symlinks frontend assets into backend data dir
    • Obelisk.Setup.Frontend.Js: GHCJS cross-compilation with optimization forwarding
    • Obelisk.Setup.Frontend.Wasm: WASM cross-compilation with jsexe assembly, JSFFI extraction, optional wasm-opt/wasm-tools
    • Obelisk.Setup.Static: static manifest generation via obelisk-asset-manifest-generate
    • Obelisk.Setup.Utils: shared utilities (project root discovery, symlinks, cabal-level optimization flags)

Static asset generation

  • Replace hackage overlay approach with obelisk-generated-static / obelisk-generated-static-custom package pair:
    • obelisk-generated-static (build-type: Simple) builds on all platforms including JS/WASM
    • obelisk-generated-static-custom (build-type: Custom) runs Setup.hs to generate Obelisk.Generated.Static module, buildable: False on JS/WASM
  • obelisk-asset-manifest-generate --module-only generates only the Haskell module without overwriting the .cabal file
  • Fix GHC 9.14 Symbol name resolution (GHC.Types.Symbol vs GHC.Internal.Types.Symbol)

Backend

  • Add _backendConfig_frontendGhcjsAssets field to BackendConfig for separate frontend asset paths

Development tools

  • ob-run: watch-and-rebuild development server with inotifywait, forwards optimization level to cross-builds via OBELISK_CROSS_CABAL_ARGS, proper cleanup of all child processes on exit
  • ob-repl: optimizations-disabled REPL, defaults to loading backend + common + frontend
  • ob-hoogle: local Hoogle documentation server with start/stop/restart, automatic cleanup on shell exit

Deployment

  • nix/server.nix: NixOS module (services.obelisk) with nginx reverse proxy (WebSocket support), ACME/HTTPS, systemd service, firewall rules, domain redirects
  • mkServerExe: assembles flat deployment directory (backend binary + compressed assets)
  • mkContainerImage: OCI container image via dockerTools.buildLayeredImage for podman/docker

Project skeleton

  • skeleton/: minimal obelisk project template with complete directory structure:
    • backend, frontend, common, static packages
    • frontend-js and frontend-wasm cross-compilation wrappers
    • project.nix with source-repository-packages from obelisk, cross-platform shell (ghcjs + wasi32), hoogle
    • cabal.project with arch conditionals excluding native-only packages from cross-builds

Other

  • flake.nix exposing lib and docs packages
  • Generated module option documentation (nix/docs.nix)
  • Broadened CPP guards from ghcjs_HOST_OS to defined(ghcjs_HOST_OS) || defined(wasm32_HOST_ARCH) throughout frontend code

I have:

  • Based work on latest develop branch
  • Followed the contribution guide
  • Looked for lint in my changes with hlint . (lint found code you did not write can be left alone)
  • Run the test suite: $(nix-build -A selftest --no-out-link)
  • Updated the changelog
  • (Optional) Run CI tests locally: nix-build release.nix -A build.x86_64-linux --no-out-link (or x86_64-darwin on macOS)

  • Build skeleton project with nix-build using the new module system
  • Verify GHCJS and WASM frontend targets both compile
  • Test ob-run, ob-repl, and ob-hoogle scripts in dev shell
  • Build OCI container image and verify it runs
  • Test NixOS deployment module configuration
  • Verify asset compression pipeline (brotli + closure-compiler) produces correct output
  • Run route test suite (lib/route/test/)

ymeister added 30 commits March 20, 2026 13:21
Extend existing arch(javascript) buildable guards to also cover
arch(wasm32), and add missing guards to executables (ob,
obelisk-asset-manifest-generate, obelisk-asset-th-generate,
obelisk-selftest) that cannot be built for non-native targets
Replace per-package listings with glob patterns and merge
cabal.dependencies.project inline. Add allow-newer constraints
for GHC 9.14 and guard older workarounds behind impl(ghc < 9.14).
The change adds a _backendConfig_frontendGhcjsAssets field to BackendConfig
, separates it from the general static assets
, and uses it to construct a GhcjsApp via serveObeliskApp instead of serveDefaultObeliskApp.
- default.nix: top-level entry point delegating to nix/
- nix/default.nix: project function wrapping nix-haskell with obelisk
  module pre-imported
- nix/module.nix: declare obelisk.static and obelisk.frontend.js as
  NixOS module options (replacing function arguments)
- nix/lib.nix: rename ghcjsFrontend to frontendJs, add reflex-dom and
  reflex-dom-core as source-repository-packages
- Add reflex-dom and nix-haskell submodules
- Pin nix-haskell to commit with named attrset source-repository-packages
- Add nix-haskell-patches/js/splitmix import to module.nix
Extracts shared build logic (backend asset linking, GHCJS frontend
cross-compilation, static manifest generation) into a library so
downstream projects only need a one-line Setup.hs.
…k-setup

- Use optional-packages in cabal.project instead of source-repository-packages
to avoid cabal-install 3.16 componentAvailableTargetStatus bug.
- Add ScopedTypeVariables to Utils.hs for exception handler type annotation.
obelisk-generated-static was previously produced as a hackage overlay:
a nix derivation ran obelisk-asset-manifest-generate to create a full
cabal package (including .cabal file), then sed-patched the generated
Haskell source to fix GHC 9.14's pprint emitting GHC.Internal.Types
instead of GHC.Types. This was fragile and duplicated logic.

Now obelisk-generated-static is a real package in the project tree
(static/generated/) with its own .cabal file, and nix generates only the
Haskell module via a preBuild override using --module-only.

Changes:

Obelisk.Asset.Promoted:
- Add writeStaticModule: writes just the .hs file without generating a
  .cabal, for use when the package already exists (nix preBuild, Setup.hs)
- Fix GHC 9.14 Symbol name: on base >= 4.21, construct an explicit
  GHC.Types.Symbol TH Name instead of using ''Symbol which resolves to
  GHC.Internal.Types.Symbol and produces uncompilable generated code
- GHC.TypeLits import is now conditional (only needed on older GHCs)

obelisk-asset-manifest-generate:
- Add --module-only flag that calls writeStaticModule instead of
  writeStaticProject, avoiding .cabal file overwrites that caused
  version mismatches during nix builds

nix/lib.nix:
- Remove obelisk-generated-static-manifest derivation and hackage overlay
- Add staticManifestOverride using --module-only in preBuild
- Rename static-manifest -> obelisk-generated-static in buildTypeOverride

nix/module.nix:
- Remove hackage-overlays config
- Add staticManifestOverride to overrides list

obelisk-setup:
- Rename Obelisk.Setup.Manifest -> Obelisk.Setup.Static
- Deduplicate: use obelisk-asset-manifest (gatherHashedPaths,
  writeStaticModule) instead of reimplementing hashing with sha256sum
- Make native-only modules (Backend, Frontend, Static) and their
  dependencies conditional on !(arch(javascript) || arch(wasm32))
- Update static/manifest paths to static/generated
Split static module generation into Simple/Custom package pair following
the frontend/frontend-custom pattern:

- obelisk-generated-static (Simple) builds on all platforms including JS
- obelisk-generated-static-custom (Custom) runs Setup.hs to generate the
  Obelisk.Generated.Static module, buildable: False on JS/WASM

The Simple package conditionally depends on the Custom package on native
to force correct build order: custom's Setup.hs generates the module
into the shared src/ directory (via symlink) before the Simple package
compiles it.

nix/lib.nix:
- staticManifestOverride generates module for both packages via preBuild
- Remove dangling src symlink before mkdir in preBuild
- Override build-type to Simple for obelisk-generated-static-custom

lib/setup/Frontend.hs:
- Fix static asset symlink path: static/manifest -> static/generated
Move assets.nix from lib/asset/ to nix/ so it lives alongside the
module system that consumes it. The asset pipeline (mkAssets) produces a
directory structure with type/encodings entries for obelisk-asset-serve-snap.

Refactor the obelisk.static option from a single path into three nested
sub-options:
  - obelisk.static.path: raw static assets path or derivation
  - obelisk.static.compress: toggle zopfli/gzip compression (default true)
  - obelisk.static.compressed: final compressed assets derivation

In module.nix, hash static files into cache-busting names via
obelisk-asset-manifest-generate --module-only, then optionally compress
with mkAssets. The uncompressed hashed output (hashedStatic) feeds the
manifest generator and static symlinks, while the compressed output
feeds obelisk-asset-serve-snap as static.assets.

Add compressedStatic parameter to frontendDataOverride and
backendDataOverride in lib.nix, which symlinks the compressed assets
into the data directory as static.assets alongside the uncompressed
static directory. Export obelisk-asset-manifest-generate from lib.nix
for use in module.nix's hashedStatic derivation.
Add brotliEncodings which compresses assets with brotli at maximum
quality (-q 11), producing ~20% better compression than gzip. Add
unionEncodings which composes multiple encoding functions via
symlinkJoin, allowing encodings to be mixed without duplicating the
identity/gzip logic.

Change defaultEncodings from platform-conditional zopfli/gzip to
unionEncodings [ brotliEncodings gzipEncodings ], serving brotli (br)
with gzip fallback for older clients. No changes needed in
obelisk-asset-serve-snap — it already picks the best encoding from
whatever files exist in the encodings directory.
Refactor obelisk.frontend.js from a single option into nested
sub-options:
  - obelisk.frontend.js.package: the GHCJS-compiled frontend derivation
  - obelisk.frontend.js.compressed: mkAssets-processed frontend.jsexe
    for obelisk-asset-serve-snap, controlled by obelisk.static.compress

Add compressedFrontendJs parameter to backendDataOverride in lib.nix,
which symlinks the processed output as frontend.jsexe.assets in the
backend data directory. The backend already serves from
frontend.jsexe.assets (processed) with frontend.jsexe (unprocessed) as
fallback via serveAsset.
Add frontend.js.optimize and frontend.js.optimized options. When
optimize is true (default), closure-compiler runs SIMPLE optimizations
on every .js file in the GHCJS jsexe output. When false, optimized
passes through the raw jsexe directory unchanged.

Add frontend.js.compress flag (defaults to static.compress) so frontend
JS compression can be toggled independently of static assets. The
compressed option now feeds from optimized rather than raw package,
forming the pipeline: package → optimized → compressed.

When either flag is false the corresponding stage is a passthrough,
so disabling both serves the raw GHCJS output directly.
Replace the per-file SIMPLE compilation with a whole-program ADVANCED
optimization that compiles all.js as a single unit, using GHCJS's
all.externs.js for extern declarations. The optimized output replaces
all.js while leaving the original files intact.

Uses --isolation_mode IIFE, --assume_function_wrapper, and
--emit_use_strict for safe ADVANCED mode with GHCJS output.
--jscomp_off=undefinedVars is needed because GHCJS generates cross-file
globals and unexpanded macros (MK_INTEGER_S, h$stg_paniczh) that
closure-compiler cannot resolve even with all files present.

Refactor frontend.js.optimize into frontend.js.optimization sub-options:
  - optimization.enable: toggle (default true)
  - optimization.level: BUNDLE/WHITESPACE_ONLY/SIMPLE/TRANSPILE_ONLY/ADVANCED
    (default ADVANCED)
  - optimization.externs: additional extern files for closure-compiler
  - optimization.extraFlags: arbitrary extra closure-compiler flags

Add frontend.js.compress flag (defaults to static.compress) so frontend
JS compression can be toggled independently. The pipeline is now:
package -> optimized (closure-compiler) -> compressed (mkAssets brotli+gzip).
When either stage is disabled, its output passes through unchanged.
Introduce a complete WASM frontend pipeline that compiles the frontend
via wasm32-wasi and assembles the output into a frontend.jsexe directory
the backend can serve unchanged, reusing the existing GHCJS asset
infrastructure.

Nix pipeline (module.nix, lib.nix):
- Add `obelisk.frontend.target` option ("js" or "wasm", default "wasm")
  to select which pipeline feeds the backend
- Add `obelisk.frontend.wasm` option group mirroring frontend.js:
  package, optimization (wasm-opt via binaryen), optimized, compress,
  compressed — with wasm-tools strip for binary size reduction
- Add `frontendWasm` helper in lib.nix for wasi32 cross-compiled frontend
- Vendor @bjorn3/browser_wasi_shim via fetchTarball for WASI browser support
- Remove hardcoded `/bin/frontend.jsexe` suffix from backendDataOverride
  since frontendOutput.optimized is already the jsexe directory for both
  JS and WASM targets

WASM browser bootstrap (nix/wasm/shim.js):
- Classic script (not ES module) using async IIFE with dynamic import()
  so it works with the existing `type="text/javascript"` script tag
- Captures document.currentScript.src synchronously before any await to
  resolve sibling assets (frontend.wasm, wasi-shim.js, ghc_wasm_jsffi.js)
  relative to the script URL rather than the page URL
- Uses WebAssembly.instantiate(arrayBuffer) instead of
  instantiateStreaming to avoid MIME type requirements

Haskell changes:
- Broaden CPP guards in Frontend.hs from `ghcjs_HOST_OS` to
  `defined(ghcjs_HOST_OS) || defined(wasm32_HOST_ARCH)` for hydration
  mode and route adjustment, since wasm32 runs in the browser like GHCJS
- Change getConfigs call to use JSM directly on ghcjs/wasm32 (no liftIO)
  since on wasm32 JSM ≠ IO unlike GHCJS where JSM = IO
- Rename src-ghcjs to src-js and change getConfigs signature from
  IO (Map Text ByteString) to JSM (Map Text ByteString) — this works on
  both GHCJS (where JSM = IO) and wasm32 (where JSM provides the
  jsaddle-wasm bridge to ghcjs-dom)
- Add arch(wasm32) to cabal conditional so wasm32 uses the DOM-based
  config lookup (src-js + ghcjs-dom) instead of the directory-based one
Split Obelisk.Setup.Frontend into separate Js and Wasm modules:
- Frontend.Js: renamed from Frontend, unchanged GHCJS cross-build hook
- Frontend.Wasm: new Setup.hs hook that builds with wasm32-unknown-wasi-cabal,
  runs post-link.mjs for JSFFI extraction, assembles jsexe directory with
  browser WASI shim, and optionally runs wasm-opt/wasm-tools

Rename frontend-custom to frontend-js/frontend-wasm in buildTypeOverride.

Skip cross-compilation and asset generation in nix-shell by guarding
obelisk.frontend.js.package, obelisk.frontend.wasm.package, and
obelisk.static.compressed option defaults with lib.inNixShell.

Export OBELISK_WASI_SHIM in shell.shellHook for cabal-based WASM builds.

Bump nix-haskell submodule (shell.shellHook types.str → types.lines).
Add nix/docs.nix to generate documentation for obelisk.* module options,
reusing eval.nix and docs.nix extracted from nix-haskell.

Reorganize docs/:
- docs/module.{md,man}: generated obelisk options documentation
- docs/nix-haskell/: symlinks to nix-haskell module documentation
- Remove old top-level symlinks to nix-haskell docs

Bump nix-haskell submodule (eval.nix/docs.nix extraction, store path
cleanup in generated docs).
Add flake.nix exposing lib (nix/default.nix) and packages (docs).
Export assets.nix and docs.nix from nix/lib.nix for downstream access.
Add cache.nixos.org to flake substituters.
Bump nix-haskell submodule (cache.nixos.org substituter).
Set optimizations.all = true (mkDefault) in obelisk module so projects
get -O2, -fexpose-all-unfoldings, -fspecialise-aggressively,
-flate-specialise, -fcross-module-specialise, and -fspecialise
out of the box. Can be overridden per-project.

Bump nix-haskell submodule (optimizations module).
Let individual projects decide their own optimization flags rather than
setting them in the framework.
Extend the project return value with exe.wasm and exe.js, each building
the backend executable with the corresponding obelisk.frontend.target
override. Uses proj.override to reuse the base project evaluation.
Pulls nix-haskell ef79179 which pins hoogle to 5.0.19.0 and adds
conditional allow-newer/constraints for GHC 9.14's bumped boot
libraries (base-4.22, containers-0.8, etc.). This fixes the dev shell
failing to build hoogle-with-packages when using ghc914, where the
cabal solver previously fell back to the ancient hoogle 3.1.
1. ob-run dev script:

Add ob-run development script for fast backend iteration

ob-run watches backend/, common/, and frontend/ for .hs, .cabal, and
.project file changes using inotifywait, then rebuilds and restarts the
backend. Pressing Enter forces a manual restart. Extra arguments are
passed through to cabal run.

Disables all optimization flags (--ghc-options=-O0, -fno-specialise,
-fno-expose-all-unfoldings, -fno-specialise-aggressively,
-fno-late-specialise, -fno-cross-module-specialise) for faster dev
rebuilds. Cross builds receive the same flags via OBELISK_CROSS_CABAL_ARGS.

The script is provided as a writeShellApplication with inotify-tools,
added to shell.nativeBuildInputs. Usage info is printed on nix-shell entry.

2. Setup.hs optimization forwarding:
Forward host optimization level and env flags to cross builds

Switch Frontend.Js and Frontend.Wasm Setup.hs hooks from preBuild/postBuild
to buildHook, which provides access to LocalBuildInfo. This allows reading
withOptimization to forward the host -O level to cross cabal builds.

Additionally, read OBELISK_CROSS_CABAL_ARGS env var for extra cabal flags
(used by ob-run to disable optimizations in cross builds). Env var flags
are appended after optLevelFlags so they take precedence (last-flag-wins).

Deduplicate optLevelFlags, crossCabalArgs, and strip into Utils. Remove
the global MVar + unsafePerformIO pattern in favor of a local MVar
created inside buildHook.
ymeister added 10 commits March 22, 2026 23:34
ob-hoogle starts a Hoogle server in the background with start/stop/restart
commands. Defaults to port 8080. Detects already-running instances via
kill -0 on the PID file to avoid duplicates.

The shellHook exports HOOGLE_PIDFILE and sets an EXIT trap to automatically
stop the server when exiting nix-shell, even if started manually mid-session.
Implement deployment support based on the original obelisk serverModules,
adapted to the new nix-haskell module system as a proper NixOS module.

nix/server.nix:
  NixOS module (services.obelisk) consolidating the old mkBaseEc2,
  mkDefaultNetworking, and mkObeliskApp into a single module with options:
  nginx reverse proxy with WebSocket support, ACME/HTTPS via Let's Encrypt,
  systemd service (symlinks exe contents into working dir, auto-restarts),
  firewall rules, system user/group, and domain redirect virtual hosts.

nix/lib.nix:
  mkServerExe assembles a flat deployment directory from project outputs
  (backend binary, compressed static assets, compressed frontend assets)
  by overriding obelisk.frontend.target and reading the corresponding
  config.obelisk.static.compressed / config.obelisk.frontend.{target}.compressed.
  Also exposes serverModule path.

nix/default.nix:
  Captures full nix-haskell eval to access proj.config and nixpkgs.
  Exposes serverExe.wasm / serverExe.js on the project, and a server
  convenience function that builds a full NixOS system (defaults exe
  to serverExe.wasm).
nix/lib.nix:
  mkContainerImage builds a layered OCI image via dockerTools.buildLayeredImage
  from a serverExe. Packages the backend binary and compressed assets into /app,
  includes cacert (for outbound HTTPS) and gnutar (backend dependency).
  Image name defaults to proj.config.name, tag defaults to "latest".
  Exposes port 8000/tcp with WorkingDir /app.

nix/default.nix:
  Exposes containerImage.wasm / containerImage.js on the project output,
  mirroring the serverExe.wasm / serverExe.js pattern.

Usage:
  nix build .#legacyPackages.x86_64-linux.containerImage.wasm
  podman load < result && podman run -p 8000:8000 <name>:latest
skeleton/:
  Minimal obelisk project template derived from a working project, stripped
  to bare essentials. Provides the complete directory structure and wiring
  needed for a new obelisk app:
  - backend: Custom build-type with obelisk-setup, Paths module for data dir
    lookup (cabal getDataDir / file-embed for REPL), Main.hs configuring
    snap with static and frontend asset paths, minimal Backend.hs
  - frontend: Library exposing Frontend module with placeholder head/body,
    Frontend.Paths with CPP-guarded data dir for native/GHCJS/WASM/REPL,
    executable with WASM foreign export, frontend-js and frontend-wasm
    cross-compilation wrapper packages with obelisk-setup
  - common: Minimal route types (one BackendRoute, one FrontendRoute),
    route encoder, Common re-export module
  - static: mkDerivation-based build (css/html/icons/images/js),
    generate helper script, obelisk-generated-static and
    obelisk-generated-static-custom packages
  - Nix: default.nix, flake.nix, project.nix (name: obelisk-skeleton)
  - cabal.project with optional-packages for cross frontends,
    generated static, and obelisk/reflex-dom deps

nix/module.nix:
  Set compiler-nix-name = mkDefault "ghc914" so projects get a sensible
  default GHC version without having to specify it explicitly.
The cabal.project used a flat package list that included packages with
build-type: Custom depending on obelisk-setup. When the WASM/GHCJS
cross-compiler's cabal resolved the project, it could not find
obelisk-setup (only available in the native ghc-pkg), causing
cross-builds to fail.

Move backend, frontend/js, frontend/wasm, and static/generated/custom
behind an `if !(arch(javascript) || arch(wasm32))` conditional so the
cross-compiler's cabal never attempts to resolve their setup-depends.
Add obelisk-setup as a build-depends of obelisk-generated-static-custom
so it appears in ghc-pkg for native cabal's setup-depends resolution.

Guard frontendDataOverride and backendDataOverride with
mkOptionalPackages so they are silently skipped in cross projects where
those packages do not exist, fixing an infinite recursion during nix
evaluation of projectCross.

Fix ob-run's `cabal list-bin` returning the wrong path under -O0 by
emitting cabal-level optimization flags (-O0) instead of
--ghc-options=-O0, and passing only optimization flags (not the full
extraFlags with --ghc-options) to list-bin/assembleAndLinkFrontend/
linkFrontendAssets.

Fix ob-run EXIT trap to also kill inotifywait and read processes.

Remove unused dependencies (directory from frontend, filepath from
backend library).
- release.nix builds the skeleton's serverExe and containerImage for
  both wasm and js targets via linkFarm, so `nix-build release.nix`
  builds everything in one go.
- Expose config and nixpkgs on project output so downstream consumers
  (like release.nix) can access the evaluated module config and the
  nixpkgs instance without re-importing.
- Add release to flake.nix packages (nix build .#release).
Rewrite README.md, FAQ.md, and ChangeLog.md to reflect the new
nix-haskell module system, vanilla cabal builds, WASM/GHCJS frontend
targets, and NixOS/OCI deployment.
@ymeister ymeister requested a review from ali-abrar March 22, 2026 21:01
@ymeister ymeister requested review from Copilot and maralorn March 22, 2026 21:41
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces “Obelisk v2” changes aimed at enabling vanilla cabal workflows, adding a WASM frontend target, and replacing the legacy ob/monolithic Nix setup with a composable Nix module system plus updated deployment/dev tooling.

Changes:

  • Replaces legacy ob CLI + old Nix plumbing with a nix-haskell module system, new build/deploy outputs, and flake-based entrypoints.
  • Adds cross frontend targets (frontend-js, frontend-wasm) and obelisk-setup Setup.hs hooks to generate static manifests and trigger cross builds from normal cabal build.
  • Overhauls the skeleton project layout, static asset pipeline (hashing + brotli/gzip + closure compiler), and adds NixOS/OCI deployment support.

Reviewed changes

Copilot reviewed 144 out of 175 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
skeleton/static/main.css Removes old placeholder CSS asset from skeleton.
skeleton/static/lib.js Removes old placeholder JS FFI asset from skeleton.
skeleton/static/generated/src/Obelisk/Generated/Static.hs Adds generated static manifest module for hashed assets.
skeleton/static/generated/obelisk-generated-static.cabal Adds generated static manifest Cabal package.
skeleton/static/generated/data/static Adds generated static data reference for skeleton asset pipeline.
skeleton/static/generated/custom/src Adds custom package src indirection for generated static.
skeleton/static/generated/custom/obelisk-generated-static-custom.cabal Adds custom build-type variant for generated static package.
skeleton/static/generated/custom/Setup.hs Uses obelisk-setup static hook for generation step.
skeleton/static/generated/Setup.hs Uses obelisk-setup static hook for generation step.
skeleton/static/generate Adds Nix-backed static asset build script used by Setup hook.
skeleton/static/default.nix Adds skeleton static asset derivation (customizable pipeline).
skeleton/project.nix Adds skeleton nix-haskell module config (static path + shell).
skeleton/frontend/wasm/frontend-wasm.cabal Adds WASM wrapper/reexport package with Custom setup.
skeleton/frontend/wasm/data Adds WASM wrapper data dir linkage.
skeleton/frontend/wasm/Setup.hs Uses obelisk-setup WASM frontend hook.
skeleton/frontend/src/Frontend/Paths.hs Adds platform-conditional dataDir resolution for frontend.
skeleton/frontend/src/Frontend.hs Simplifies skeleton frontend and switches to generated static import.
skeleton/frontend/src-bin/main.hs Removes old skeleton frontend executable entrypoint.
skeleton/frontend/js/frontend-js.cabal Adds GHCJS wrapper/reexport package with Custom setup.
skeleton/frontend/js/data Adds GHCJS wrapper data dir linkage.
skeleton/frontend/js/Setup.hs Uses obelisk-setup JS frontend hook.
skeleton/frontend/frontend.cabal Modernizes skeleton frontend cabal file + flags + app exe layout.
skeleton/frontend/app/Main.hs Adds new frontend app entrypoint + WASM export glue.
skeleton/flake.nix Adds skeleton flake output for legacy packages per system.
skeleton/deps/obelisk Points skeleton to local obelisk dependency.
skeleton/default.nix Replaces legacy .obelisk/impl wiring with new module-based project import.
skeleton/config/readme.md Removes old skeleton config documentation file.
skeleton/config/common/route Removes old config-based route host.
skeleton/config/common/example Removes old shared config example.
skeleton/common/src/Common/Route.hs Simplifies routes + introduces checked encoder helper.
skeleton/common/src/Common/Api.hs Removes old Common.Api sample.
skeleton/common/src/Common.hs Adds Common re-export module.
skeleton/common/common.cabal Modernizes skeleton common cabal file and warnings baseline.
skeleton/cabal.project Reworks skeleton cabal.project for optional/cross packages.
skeleton/backend/static Removes old backend->static path link.
skeleton/backend/src/Paths.hs Adds backend dataDir/temporaryDir resolver with REPL mode.
skeleton/backend/src/Backend.hs Updates backend to import new Common module.
skeleton/backend/src-bin/main.hs Removes old backend executable entrypoint.
skeleton/backend/frontendJs/frontend.jsexe Removes old frontend asset link.
skeleton/backend/frontend.jsexe Removes old frontend asset link.
skeleton/backend/backend.cabal Modernizes backend cabal file, adds cross/wasm flags and Custom setup.
skeleton/backend/app/Main.hs Adds backend app entrypoint using new BackendConfig asset paths.
skeleton/backend/Setup.hs Uses obelisk-setup backend hook.
skeleton/.obelisk/impl Removes legacy .obelisk/impl pointer.
skeleton/.obelisk/.gitignore Removes legacy .obelisk ignore rules.
skeleton/.gitignore Updates skeleton ignores for new dist dirs / asset outputs.
scripts/ob-run Adds file-watching dev server wrapper around cabal run backend.
scripts/ob-repl Adds convenience cabal repl wrapper tuned for dev iteration.
scripts/ob-hoogle Adds hoogle server manager script.
release.nix Replaces old all-builds/all-tests wiring with skeleton-based outputs.
nixpkgs-overlays/default.nix Removes legacy nixpkgs overlay utilities.
nix/wasm/shim.js Adds browser shim to bootstrap WASI WASM frontend runtime.
nix/server.nix Adds NixOS module for deploying an Obelisk backend behind nginx/ACME.
nix/module.nix Adds nix-haskell module defining obelisk options + asset pipelines.
nix/lib.nix Adds Nix helper library: overrides, serverExe/container image builders.
nix/docs.nix Adds doc generation entrypoint for module options.
nix/default.nix Adds top-level Nix entrypoint exporting module + builders and outputs.
lib/snap-extras/obelisk-snap-extras.cabal Expands non-buildable condition to wasm32.
lib/setupHls.sh Removes legacy HLS helper script.
lib/setup/src/Obelisk/Setup/Utils.hs Adds shared Setup.hs utilities (root lookup, symlink, flags).
lib/setup/src/Obelisk/Setup/Static.hs Adds static asset build+manifest generator Setup hook.
lib/setup/src/Obelisk/Setup/Frontend/Wasm.hs Adds WASM cross-build Setup hook + jsexe assembly.
lib/setup/src/Obelisk/Setup/Frontend/Js.hs Adds GHCJS cross-build Setup hook + symlinking.
lib/setup/src/Obelisk/Setup/Backend.hs Adds backend Setup hook symlinking frontend/static assets.
lib/setup/obelisk-setup.cabal Adds new obelisk-setup package for reusable hooks.
lib/setup/README.md Documents obelisk-setup hooks usage.
lib/selftest/src-bin/obelisk-selftest.hs Removes legacy selftest executable.
lib/selftest/obelisk-selftest.cabal Removes legacy selftest package.
lib/run/src/Obelisk/Run.hs Removes legacy obelisk-run implementation.
lib/run/obelisk-run.cabal Removes legacy obelisk-run package.
lib/route/test/Main.hs Adjusts QuickCheck import to avoid Some name clash.
lib/hie.yaml Removes legacy hie-bios configuration file.
lib/frontend/src/Obelisk/Frontend.hs Extends hydrate/adjustRoute conditions to include wasm; adjusts config lookup.
lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig/Lookup.hs Removes iOS config lookup implementation.
lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig/Lookup.hs Changes GHCJS config lookup to return JSM and minor cleanups.
lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig/Lookup.hs Removes Android config lookup implementation.
lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig/Internal/AssetManager.hsc Removes Android asset manager FFI module.
lib/executable-config/lookup/obelisk-executable-config-lookup.cabal Simplifies platform conditionals; adds wasm32 pathing.
lib/executable-config/lookup/cbits/ExecutableConfig.c Removes Android JNI config implementation.
lib/executable-config/default.nix Removes legacy executable-config nix module.
lib/command/src/Obelisk/Command/VmBuilder.hs Removes legacy ob CLI implementation.
lib/command/src/Obelisk/Command/Utils.hs Removes legacy ob CLI implementation.
lib/command/src/Obelisk/Command/Preprocessor.hs Removes legacy ob CLI implementation.
lib/command/src/Obelisk/Command/Nix.hs Removes legacy ob CLI implementation.
lib/command/src/Obelisk/App.hs Removes legacy ob CLI monad/app scaffolding.
lib/command/src-bin/ob.hs Removes legacy ob executable entrypoint.
lib/command/obelisk-command.cabal Removes legacy obelisk-command package.
lib/cabal.project.config Consolidates constraints/allow-newer for GHC 9.14 and wasm.
lib/cabal.project.ci Removes CI-specific project file.
lib/cabal.project Simplifies package selection and imports consolidated config.
lib/cabal.dependencies.project Removes legacy dependency project file.
lib/backend/src/Obelisk/Backend.hs Extends BackendConfig to carry compiled frontend assets separately.
lib/backend/obelisk-backend.cabal Expands non-buildable condition to wasm32.
lib/asset/serve-snap/obelisk-asset-serve-snap.cabal Expands non-buildable condition to wasm32.
lib/asset/manifest/src/Obelisk/Asset/Promoted.hs Adds writeStaticModule and adjusts Symbol handling for GHC 9.14.
lib/asset/manifest/src-bin/generate.hs Adds --module-only support to generator CLI.
lib/asset/manifest/obelisk-asset-manifest.cabal Expands non-buildable condition to wasm32 and executables.
lib/asset/assets.nix Switches default encodings to brotli+gzip; removes noisy tracing.
lib/README.md Removes HLS-specific dev workflow section.
hydra.json Removes legacy Hydra jobset config.
haskell-overlays/tighten-ob-exes.nix Removes legacy overlay used for ob/selftest packaging.
haskell-overlays/obelisk.nix Removes legacy Haskell overlay packaging.
haskell-overlays/misc-deps.nix Removes legacy overlay for dependency fixes.
guides/app-deploy/README.md Removes outdated deployment guide tied to old ob CLI.
flake.nix Adds top-level flake exporting nix lib and docs/release packages.
docs/nix-haskell/modules.md Adds docs symlink/reference file for nix-haskell modules.
docs/nix-haskell/modules.man Adds docs symlink/reference file for nix-haskell modules.
docs/module.md Adds generated module-option documentation.
docs/module.man Adds generated manpage for module-option documentation.
deps/reflex-dom Adds reflex-dom submodule pointer.
deps/nix-haskell Adds nix-haskell submodule pointer.
dep/reflex-platform/thunk.nix Removes legacy thunked dependency.
dep/reflex-platform/github.json Removes legacy thunked dependency metadata.
dep/reflex-platform/default.nix Removes legacy thunked dependency entrypoint.
dep/nix-thunk/thunk.nix Removes legacy thunked dependency.
dep/nix-thunk/github.json Removes legacy thunked dependency metadata.
dep/nix-thunk/default.nix Removes legacy thunked dependency entrypoint.
dep/hs-git/thunk.nix Removes legacy thunked dependency.
dep/hs-git/github.json Removes legacy thunked dependency metadata.
dep/hs-git/default.nix Removes legacy thunked dependency entrypoint.
dep/hnix/thunk.nix Removes legacy thunked dependency.
dep/hnix/github.json Removes legacy thunked dependency metadata.
dep/hnix/default.nix Removes legacy thunked dependency entrypoint.
dep/gitignore.nix/thunk.nix Removes legacy thunked dependency.
dep/gitignore.nix/github.json Removes legacy thunked dependency metadata.
dep/gitignore.nix/default.nix Removes legacy thunked dependency entrypoint.
dep/cli-nix/thunk.nix Removes legacy thunked dependency.
dep/cli-nix/github.json Removes legacy thunked dependency metadata.
dep/cli-nix/default.nix Removes legacy thunked dependency entrypoint.
dep/cli-git/thunk.nix Removes legacy thunked dependency.
dep/cli-git/github.json Removes legacy thunked dependency metadata.
dep/cli-git/default.nix Removes legacy thunked dependency entrypoint.
dep/cli-extras/thunk.nix Removes legacy thunked dependency.
dep/cli-extras/github.json Removes legacy thunked dependency metadata.
dep/cli-extras/default.nix Removes legacy thunked dependency entrypoint.
dep/README.md Removes legacy dep directory overview.
all-tests.nix Removes legacy test aggregation.
all-builds.nix Removes legacy build aggregation.
ChangeLog.md Adds comprehensive v2 “Unreleased” changelog section.
CONTRIBUTING.md Updates contribution/testing guidance for new skeleton + release outputs.
.hlint.yaml Removes repo-level HLint config.
.gitmodules Adds submodule definitions for nix-haskell and reflex-dom.
.github/workflows/haskell.yml Removes CI step copying cabal.project.ci into place.
Comments suppressed due to low confidence (6)

scripts/ob-run:1

  • set -euo pipefail + trap cleanup EXIT means cleanup can run before PID/INOTIFY_PID/READ_PID are set, causing an “unbound variable” error due to -u. Initialize these variables upfront (e.g., empty) and/or guard each kill with ${VAR:-} checks (or [[ -n "${VAR:-}" ]]) before calling kill.
    nix/lib.nix:1
  • config.packages ? ${name} is not valid Nix syntax for a dynamic attribute check (the ? operator expects an identifier). Use builtins.hasAttr name config.packages (or config.packages.${name} or null style) inside the filter predicate to make this expression evaluate correctly.
    lib/cabal.project.config:1
  • The trailing quote (") at the end of the ghc-options line will make this cabal.project.config invalid/unparseable. Remove the stray quote so cabal can parse the file.
    nix/server.nix:1
  • systemd does not reliably expand ~ in WorkingDirectory, so the service may fail to start or use an unexpected directory. Prefer an absolute path like /var/lib/${cfg.user} (matching the declared user home) or "%h" to use the service user’s home directory.
    lib/setup/README.md:1
  • The README references a module Obelisk.Setup.Frontend (but the code in this PR adds Obelisk.Setup.Frontend.Js and Obelisk.Setup.Frontend.Wasm) and mentions generating Obelisk.Generated.Static.Instances (while the generator writes Obelisk.Generated.Static). Update the README module names/output module name to match the actual exported modules and generated output.
    lib/backend/src/Obelisk/Backend.hs:1
  • _backendConfig_frontendGhcjsAssets is introduced as a general “compiled frontend assets” path used by the backend serving logic, but the name hard-codes “Ghcjs” even though this PR adds a WASM frontend target that still produces/serves a frontend.jsexe directory. Consider renaming this field to a target-agnostic name (e.g., _backendConfig_frontendAssets or _backendConfig_compiledFrontendAssets) to avoid confusion and accidental misuse.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .gitmodules Outdated
ymeister added 11 commits March 23, 2026 06:45
ghcjs is no longer enabled by default in the skeleton's
crossPlatforms. Users can uncomment it and use the -f -wasm
flag with cabal/ob-run to enable JS builds.
> Add cabalProject, cabalProjectLocal, cabalProjectFreeze, cabalProjectFileName passthroughs
>
> These haskell.nix project options are now exposed as top-level
> nix-haskell module options and passed through via intersectAttrs.
>
> Filter null values from the passthrough to avoid overriding
> haskell.nix's internally-computed defaults.
> Remove hardcoded *wasm* from cross-compilation LDFLAGS filters
>
> Instead of always including *wasm* in the linker flag filter patterns,
> conditionally add "wasm" to targetPrefixes (both global and per-wrapper)
> only when a WASM target is present. Skip shellHook entirely when no
> cross platforms are configured.
> Match flake.nix with ./pins
- Add `inputs` parameter to nix-haskell default.nix/eval.nix; when
  input names match pin options, override pins via mkDefault
- Use git+file: for nix-haskell submodule input (path: doesn't
  include submodule contents in store)
- Thread `inputs` through obelisk flake.nix → nix/, release.nix,
  skeleton/ so flake inputs reach nix-haskell
- Legacy nix (inputs={}) continues resolving pins through thunks
Add `flake-compat` flake input (following nix-haskell) and create
`inputs.nix` files that extract flake inputs for non-flake evaluation.
Thread `inputs` parameter through skeleton's `shell.nix` and
`default.nix`. Update `nix-haskell` submodule.
nix-haskell:
  - Add WASM cross-compilation support module with Node.js dependency

reflex-dom:
  - Add flake inputs and shell.nix for nix-haskell integration
@mankyKitty
Copy link
Copy Markdown
Collaborator

Holy poo. >_>

@maralorn
Copy link
Copy Markdown
Collaborator

I have to admit, I am very sceptical of this change. I am still not inclined to use haskell.nix but besides that using a bespoke and confusingly named additional abstraction layer on top of it which has not been used anywhere seems the wrong way to go to me. In my opinion obelisk + r-p is already too much abstraction layer anyway. (I am not against abstraction in general, but these nix abstraction layers are nearly always leaky abstractions with bad complexity/pay-off ratio.)

It seems unlikely to me, that we @heilmannsoftware would switch to an obelisk based on this.

That being said if we don’t release something based on the new backends, the marvel of reflex will just be wasted and this approach is at least a way forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants