Skip to content

Latest commit

 

History

History
361 lines (271 loc) · 11.9 KB

project-development.rst

File metadata and controls

361 lines (271 loc) · 11.9 KB

This document describes how to build real-world applications written in Reflex.

First, create a directory for your project. This will contain all of the files needed for the build process and a checkout of reflex-platform, which will provide all of the Haskell libraries and compilers the project will depend on. To get reflex-platform, it is easiest to use git and add it as a submodule, so that the version being used is consistent amongst your team and updating it is easy.

$ mkdir my-project
$ cd my-project
$ git init
$ git submodule add https://github.com/reflex-frp/reflex-platform

If you’ve never built a project with reflex-platform before, you may need to install Nix and configure Reflex’s binary cache. reflex-platform provides the try-reflex script, which will do this for you and download some of the basic tools and libraries we’ll need ahead of time.

$ reflex-platform/try-reflex

After running this command, you’ll find yourself in a different shell. This is the try-reflex sandbox, which provides GHC and GHCJS with reflex-dom preinstalled. You can use this environment to quickly test things out, but this document only uses it to install Nix, so go ahead and exit out of this shell.

In Reflex-DOM projects, it’s common to have three separate Haskell components: the frontend, the backend, and the common code shared between them. It’s easiest to have a separate cabal package for each of these. We’re going to teach Nix how to build them and how to give us an environment where they can be built by hand.

Create a directory for each package, then run cabal init inside them to create the *.cabal file and directory structure. If you don’t have cabal installed on your system, you can enter the try-reflex sandbox to use the version that comes with that. We will see a better way to get the cabal command later.

$ mkdir common backend frontend
$ (cd common && cabal init --lib)
$ (cd backend && cabal init --exe)
$ (cd frontend && cabal init --exe)

This will create common as a library, and frontend and backend as executables. These cabal files are where the dependencies and build targets of each Haskell component can be described.

In frontend/frontend.cabal and backend/backend.cabal, add common and reflex-dom as Haskell dependencies.

...
  build-depends: base
               , common
               , reflex-dom
...

Finally, Nix will fail to build common if it exports no modules.

-- common/src/Common.hs
module Common where
...
# common/common.cabal
library
  hs-source-dirs: src
  build-depends: base
  exposed-modules: Common
...

Nix will be used to manage installing dependencies and building the project. In the root directory of your project, create this default.nix file:

# default.nix
{ system ? builtins.currentSystem }:
(import ./reflex-platform { inherit system; }).project ({ pkgs, ... }: {
  packages = {
    common = ./common;
    backend = ./backend;
    frontend = ./frontend;
  };

  shells = {
    ghc = ["common" "backend" "frontend"];
    ghcjs = ["common" "frontend"];
  };
})

See project/default.nix for more details on available options.

You can build individual components of your project using -A.

$ nix-build -o backend-result -A ghc.backend
$ nix-build -o frontend-result -A ghcjs.frontend

These commands will create two symlinks (backend-result and frontend-result) that point at the build products in the Nix store.

nix-build is great for release builds since it’s deterministic and sandboxed, but it is not an incremental build system. Changing one file will require nix-build to recompile the entire package. In order to get a dev environment where changing a module only rebuilds the affected modules, even across packages, a more incremental tool is required.

cabal is the only tool that simultaneously supports Nix and GHCJS. The Nix expression in default.nix uses shells to setup nix-shell sandboxes that cabal can use to build your project. The shells field in default.nix defines which platforms we’d like to develop for, and which packages’ dependencies we want available in the development sandbox for that platform. Note that specifying common is important; otherwise it will be treated as a dependency that needs to be built by Nix for the sandbox.

You can use these shells with cabal.project files to build all three packages in a shared incremental environment, for both GHC and GHCJS. cabal.project files are how you configure cabal new-build to build your local project. It’s easiest to have a separate file for GHC and GHCJS.

-- cabal.project
packages:
  common/
  backend/
  frontend/
-- cabal-ghcjs.project
compiler: ghcjs
packages:
  common/
  frontend/

To build with GHC, use the nix-shell command to enter the sandbox shell and use cabal (which is supplied by the sandbox):

$ nix-shell -A shells.ghc
[nix-shell:~/path]$ cabal new-build all

To build with GHCJS:

$ nix-shell -A shells.ghcjs
[nix-shell:~/path]$ cabal --project-file=cabal-ghcjs.project --builddir=dist-ghcjs new-build all

You can also run commands in the nix-shell without entering it interactively using the --run mode. This is useful for scripting.

$ nix-shell -A shells.ghc --run "cabal new-build all"
$ nix-shell -A shells.ghcjs --run "cabal --project-file=cabal-ghcjs.project --builddir=dist-ghcjs new-build all"

nix-shell will put you in an environment with all the dependencies needed by your project, including the cabal tool. It reads your *.cabal files to determine what Haskell dependencies to have installed when you enter the sandbox, so you do not need to manually run cabal install to get Haskell dependencies. Just like Stack, all you have to do is add them to the build-depends field in you cabal file.

Note: Cabal may complain with Warning: The package list for 'hackage.haskell.org' does not exist. Run 'cabal update' to download it. This can be ignored since we are using Nix instead of Cabal’s own package manager. Nix uses a package snapshot similar to a Stackage LTS.

GHCJS can be quite slow, especially if you are using Template Haskell. Building the frontend with GHC can drastically speed up build times, and enables you to test from GHCi for even faster reloads.

JSaddle is a set of libraries that allows Reflex-DOM to swap out its JavaScript backend easily. By default, Reflex-DOM’s mainWidget will work on GHC out of the box, using the default backend for your platform (e.g., jsaddle-warp on linux, wkwebview on macOS). So simply building your frontend package using GHC will produce a working program that renders DOM. To select the webkit2gtk backend for reflex-dom on linux, set the useWebkit2Gtk flag to true in default.nix. Note that webkit2gtk only works on x86_64-linux.

To build this with nix-build:

$ nix-build -o ghc-frontend-result -A ghc.frontend

To build it with cabal:

$ nix-shell -A shells.ghc --run "cabal new-build frontend"

jsaddle-warp is an alternative JSaddle backend that uses a local warp server and WebSockets to control a browser from a native Haskell process. This is recommended to allow testing different browsers, and to make use of a browser’s significantly better developer tools.

To use it, enable the useWarp option in default.nix.

# default.nix
(import ./reflex-platform {}).project ({ pkgs, ... }: {
  useWarp = true;

  packages = {
    common = ./common;
    backend = ./backend;
    frontend = ./frontend;
  };

  shells = {
    ghc = ["common" "backend" "frontend"];
    ghcjs = ["common" "frontend"];
  };
})

Running the GHC-built frontend with this option will spawn the Warp server on port 3003, which you can connect your browser to to run the app. It will also compile under GHCJS as is, automatically defaulting back to the GHCJS backend. Both jsaddle-warp and jsaddle-webkit2gtk are safe to use from GHCi, so you can test changes even more quickly with :r.

Note: The native backends for JSaddle have much much better runtime performance than the GHCJS backend. To put it in perspective, the native backends running on most mobile phones will outperform most desktops running the GHCJS backend. GHCJS is quite fast, especially considering all it has to do; but native Haskell is simply much faster than a JS VM for what Reflex is doing.

The project Nix expression also supports defining mobile apps.

(import ./reflex-platform {
  config.android_sdk.accept_license = true;
}).project ({ pkgs, ... }: {
  packages = {
    common = ./common;
    backend = ./backend;
    frontend = ./frontend;
  };

  shells = {
    ghc = ["common" "backend" "frontend"];
    ghcjs = ["common" "frontend"];
  };

  android.frontend = {
    executableName = "frontend";
    applicationId = "org.example.frontend";
    displayName = "Example Android App";
  };

  ios.frontend = {
    executableName = "frontend";
    bundleIdentifier = "org.example.frontend";
    bundleName = "Example iOS App";
  };
})

Note that you must accept the Android Software Development Kit License Agreement and indicate so by setting config.android_sdk.accept_license when instantiating Reflex Platform in order to build Android apps.

Build them with nix-build:

$ # On Linux
$ nix-build -o android-result -A android.frontend
$ # On macOS
$ nix-build -o ios-result -A ios.frontend

Currently, Android apps can only be built on Linux, and iOS apps can only be built on macOS. If you would like to launch builds from an unsupported platform, you can use Nix distributed builds.

For example, to build the Android app from a Mac configured with a Linux remote builder:

$ nix-build -o android-result -A android.frontend --arg config '{system="x86_64-linux";}'

Note that only android.frontend was built in this case. Currently, Android apps can only be built on Linux, and iOS apps can only be built on macOS. If you would like to get both in the result directory, use -A all and make sure to have Nix distributed builds set up. Nix will delegate builds to remote machines automatically to build the apps on their required systems.

For some use-cases it can be required to build derivations to be deployed on a different system than the one used for building. For example, a derivation needs to be deployed to x86_64-linux but the system used for building is x86_64-darwin.

Nix supports delegating builds to other machines using remote builders. For the above example, the nix-docker project might be useful, as it provides a Docker-based Linux build environment usable on Darwin machines. After having set up remote builders, the Reflex application can be built for x86_64-linux by passing the appropriate system argument:

$ nix-build --argstr system x86_64-linux