on master help sign in
Merge branch 'develop' into develop
commit35619bf800f999e99251d897da1698c932c3b448 authorAli Abrar <aliabrar@gmail.com> dateFri 16 Apr 2021, 08:58 (5 years ago) parent3842939a6f4df6
30 files changed+484−119
.github/pull_request_template.md+2 ChangeLog.md+28 README.md+6−3 all-builds.nix+1−1 default.nix+18−7 dep/reflex-platform/github.json+3−3 haskell-overlays/misc-deps.nix+8−2 lib/asset/README.md+2−2 lib/asset/manifest/obelisk-asset-manifest.cabal+11−1 lib/asset/manifest/src-bin/static-th.hs+43 lib/asset/manifest/src/Obelisk/Asset/Cabal.hs+42 lib/asset/manifest/src/Obelisk/Asset/Gather.hs+13−13 lib/asset/manifest/src/Obelisk/Asset/Promoted.hs+8−27 lib/asset/manifest/src/Obelisk/Asset/TH.hs+34−3 lib/cliapp/src/Obelisk/CliApp/Logging.hs+5−1 lib/command/obelisk-command.cabal+4−1 lib/command/src/Obelisk/Command/Preprocessor.hs+4−2 lib/command/src/Obelisk/Command/Project.hs+142−13 lib/command/src/Obelisk/Command/Run.hs+78−21 lib/command/src/Obelisk/Command/Utils.hs+3 lib/executable-config/lookup/src/Obelisk/Configs.hs+2 lib/route/src/Obelisk/Route.hs+3−2 lib/run/src/Obelisk/Run.hs+3−3 lib/snap-extras/src/Obelisk/Snap/Extras.hs+1−1 skeleton/.gitignore+1 skeleton/backend/backend.cabal+2−2 skeleton/common/common.cabal+1−1 skeleton/default.nix+10−3 skeleton/frontend/frontend.cabal+3−3 skeleton/frontend/src/Frontend.hs+3−4
.github/pull_request_template.md+2
@@ -3,6 +3,8 @@
33I have:
44
55 - [ ] Based work on latest `develop` branch
6+ - [ ] Followed the [contribution guide](https://github.com/obsidiansystems/obelisk/blob/develop/CONTRIBUTING.md#submitting-changes)
67 - [ ] Looked for lint in my changes with `hlint .` (lint found code you did not write can be left alone)
78 - [ ] Run the test suite: `$(nix-build -A selftest --no-out-link)`
9+ - [ ] [Updated the changelog](https://github.com/obsidiansystems/obelisk/blob/develop/CONTRIBUTING.md#in-the-changelog)
810 - [ ] (Optional) Run CI tests locally: `nix-build release.nix -A build.x86_64-linux --no-out-link` (or `x86_64-darwin` on macOS)
ChangeLog.md+28
@@ -4,6 +4,34 @@ This project's release branch is `master`. This log is written from the perspect
44
55## Unreleased
66
7+* Use TemplateHaskell to determine asset file paths
8+ * Migration: All uses of `static @"some/path"` become `$(static "some/path")`. Instead of requiring `TypeApplications` and `DataKinds`, modules calling `static` must now enable `TemplateHaskell`.
9+ * Deprecation: Deprecate static asset modules generated via 'obelisk-asset-manifest-generate' in favor of modules generated via 'obelisk-asset-th-generate'. The new executable takes the same arguments as the old and should be a drop-in replacement. To preserve the old behavior, set `__deprecated.useObeliskAssetManifestGenerate = true;` in your obelisk project configuration.
10+ * Feature: Files added to the static directory while `ob run` is active no longer require `ob run` to be restarted
11+* Feature: When `staticFiles` is a derivation, as opposed to a regular directory, produce a symlink to the result of that derivation at `static.out` and have `ob run` serve static assets from that symlink. This makes it possible for the static asset derivation to be rebuilt and the new results served without restarting `ob run`.
12+* Feature: Rebuild static asset derivations while `ob run` is active as long as the change to the derivation is within the project folder. `ob run` now displays a message ("Static assets rebuilt and symlinked to static.out") whenever static assets have been rebuilt and the new static assets are being served.
13+* Feature: Add `staticFilePath` to `Obelisk.Generated.Static`. Like `static`, this uses TH to generate a reference to a file. Unlike `static`, this `staticFilePath` generates a path on the filesystem instead of URL path.
14+
15+## v0.9.1.0
16+
17+* [#801](https://github.com/obsidiansystems/obelisk/pull/801): Remove errors and warning for local packages without a library component
18+* [#812](https://github.com/obsidiansystems/obelisk/pull/812): Add support for `NoImplicitPrelude` and other extensions disabled via `No`
19+* Pinned version bumps:
20+ * reflex-platform [0.7.0.0](https://github.com/reflex-frp/reflex-platform/releases/tag/v0.7.0.0)
21+ * hnix 0.8.0
22+* [#787](https://github.com/obsidiansystems/obelisk/pull/787): Set `immutable` cache control directive when serving content-addressed static assets
23+* Use iOS SDK 13.2
24+
25+
26+## v0.9.0.1
27+
28+* ([#810](https://github.com/obsidiansystems/obelisk/pull/810)) Fix loading of `all.js` in fully compiled web apps.
29+
30+## v0.9.0.0
31+
32+* **(Breaking change)** Backport nixpkgs upgrades to ACME/Let's Encrypt handling so that HTTPS deployments continue to work flawlessly. If your deployment is having trouble renewing [Let's Encrypt](https://letsencrypt.org/) certificates, upgrade to this version.
33+ * **IMPORTANT:** In order to use [Let's Encrypt](https://letsencrypt.org/) you must now accept their [terms of service](https://letsencrypt.org/repository/). To do that, add `terms.security.acme.acceptTerms = true;` to the `import ./.obelisk/impl {` section in your `default.nix`. The new skeleton application may serve as an [example](https://github.com/obsidiansystems/obelisk/blob/4759342ab3570888c027d4c58cb5694cb832d624/skeleton/default.nix#L13).
34+* Update reflex-platform dependency to v0.6.0.0
735* ([#715](https://github.com/obsidiansystems/obelisk/pull/715)) In `Obelisk.Route` deprecate `isoEncoder` and `prismEncoder` in favor of more precisely named `viewEncoder` and `reviewEncoder` (respectively) and improve documentation regarding contravariance of `reviewEncoder`.
836* ([#739](https://github.com/obsidiansystems/obelisk/pull/739)) Improve `ob shell` by allowing commands to be passed verbatim after a `--` argument. For example, `ob shell 'run command'` can now be written `ob shell -- run command`.
937* ([#735](https://github.com/obsidiansystems/obelisk/pull/735)) Fix regression causing custom `Prelude`s to break `ob run`/`ob watch`/`ob repl`.
README.md+6−3
@@ -66,7 +66,10 @@ Obelisk assumes basic knowledge of [Haskell](https://www.haskell.org/) and [Refl
6666 sudo launchctl stop org.nixos.nix-daemon
6767 sudo launchctl start org.nixos.nix-daemon
6868 ```
69−1. Install obelisk: `nix-env -f https://github.com/obsidiansystems/obelisk/archive/master.tar.gz -iA command`
69+1. Install obelisk:
70+ ```bash
71+ nix-env -f https://github.com/obsidiansystems/obelisk/archive/master.tar.gz -iA command
72+ ```
7073
7174### Accessing private repositories
7275
@@ -111,7 +114,7 @@ First create a new EC2 instance:
1111141. Launch a NixOS 19.09 EC2 instance (we recommend [this AMI](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#LaunchInstanceWizard:ami=ami-00a8eeaf232a74f84))
1121151. In the instance configuration wizard ensure that your instance has at least 1GB RAM and 10GB disk space.
1131161. When prompted save your AWS private key (`~/myaws.pem`) somewhere safe. We'll need it later during deployment.
114−1. Go to "Security Groups", select your instance's security group and under "Inbound" tab add a new rule for HTTP port 80 and 443.
117+1. Go to "Security Groups", select your instance's security group and under "Inbound" tab add a new rule for HTTP port 80 and HTTPS port 443.
115118
116119At this stage your instance should be booting and become accessible shortly. Note down the hostname of your EC2 instance.
117120
@@ -218,7 +221,7 @@ Your account must also belong to an Apple Developer Team, if you want to access
2182211. Click accept to authorize on both the computer and the iPhone.
219222
220223##### Xcode
221−Install Xcode 8.2 (contains iOS SDK 10.2) and open it so that it runs its post install tool setup.
224+Install Xcode 11.2 (contains iOS SDK 13.2) and open it so that it runs its post install tool setup.
222225
223226These versions will work out of the box but iOS SDKs prior to 11.3 should also work. You can choose another installed version in `default.nix`
224227
all-builds.nix+1−1
@@ -1,6 +1,6 @@
11{ self-args ? {
22 config.android_sdk.accept_license = true;
3− iosSdkVersion = "10.2";
3+ iosSdkVersion = "13.2";
44 }
55, local-self ? import ./. self-args
66, supportedSystems ? [ builtins.currentSystem ]
default.nix+18−7
@@ -1,7 +1,10 @@
11{ system ? builtins.currentSystem
22, profiling ? false
3−, iosSdkVersion ? "10.2"
3+, iosSdkVersion ? "13.2"
44, config ? {}
5+, terms ? { # Accepted terms, conditions, and licenses
6+ security.acme.acceptTerms = false;
7+ }
58, reflex-platform-func ? import ./dep/reflex-platform
69}:
710let
@@ -62,7 +65,7 @@ in rec {
6265 '';
6366 nullIfAbsent = p: if lib.pathExists p then p else null;
6467 #TODO: Avoid copying files within the nix store. Right now, obelisk-asset-manifest-generate copies files into a big blob so that the android/ios static assets can be imported from there; instead, we should get everything lined up right before turning it into an APK, so that copies, if necessary, only exist temporarily.
65− processAssets = { src, packageName ? "obelisk-generated-static", moduleName ? "Obelisk.Generated.Static" }: pkgs.runCommand "asset-manifest" {
68+ processAssets = { src, packageName ? "obelisk-generated-static", moduleName ? "Obelisk.Generated.Static", exe ? "obelisk-asset-th-generate" }: pkgs.runCommand "asset-manifest" {
6669 inherit src;
6770 outputs = [ "out" "haskellManifest" "symlinked" ];
6871 nativeBuildInputs = [ ghcObelisk.obelisk-asset-manifest ];
@@ -70,7 +73,7 @@ in rec {
7073 set -euo pipefail
7174 touch "$out"
7275 mkdir -p "$symlinked"
73− obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
76+ ${exe} "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
7477 '';
7578
7679 compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
@@ -114,7 +117,7 @@ in rec {
114117 "${routeHost}".email = adminEmail;
115118 } else {};
116119
117− security.acme.${if enableHttps then "acceptTerms" else null} = true;
120+ security.acme.${if enableHttps && (terms.security.acme.acceptTerms or false) then "acceptTerms" else null} = true;
118121 };
119122
120123 mkObeliskApp =
@@ -183,7 +186,9 @@ in rec {
183186 set -eux
184187 ln -s '${if profiling then backend else haskellLib.justStaticExecutables backend}'/bin/* $out/
185188 ln -s '${mkAssets assets}' $out/static.assets
186− ln -s '${mkAssets (compressedJs frontend optimizationLevel)}'/* $out
189+ for d in '${mkAssets (compressedJs frontend optimizationLevel)}'/*/; do
190+ ln -s "$d" "$out"/"$(basename "$d").assets"
191+ done
187192 echo ${version} > $out/version
188193 '';
189194
@@ -232,13 +237,14 @@ in rec {
232237 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
233238 , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
234239 , __withGhcide ? false
240+ , __deprecated ? {}
235241 }:
236242 let
237243 allConfig = nixpkgs.lib.makeExtensible (self: {
238244 base = base';
239245 inherit args;
240246 userSettings = {
241− inherit android ios packages overrides tools shellToolOverrides withHoogle __closureCompilerOptimizationLevel __withGhcide;
247+ inherit android ios packages overrides tools shellToolOverrides withHoogle __closureCompilerOptimizationLevel __withGhcide __deprecated;
242248 staticFiles = if staticFiles == null then self.base + /static else staticFiles;
243249 };
244250 frontendName = "frontend";
@@ -246,7 +252,12 @@ in rec {
246252 commonName = "common";
247253 staticName = "obelisk-generated-static";
248254 staticFilesImpure = let fs = self.userSettings.staticFiles; in if lib.isDerivation fs then fs else toString fs;
249− processedStatic = processAssets { src = self.userSettings.staticFiles; };
255+ processedStatic = processAssets {
256+ src = self.userSettings.staticFiles;
257+ exe = if lib.attrByPath ["userSettings" "__deprecated" "useObeliskAssetManifestGenerate"] false self
258+ then builtins.trace "obelisk-asset-manifest-generate is deprecated. Use obelisk-asset-th-generate instead." "obelisk-asset-manifest-generate"
259+ else "obelisk-asset-th-generate";
260+ };
250261 # The packages whose names and roles are defined by this package
251262 predefinedPackages = lib.filterAttrs (_: x: x != null) {
252263 ${self.frontendName} = nullIfAbsent (self.base + "/frontend");
dep/reflex-platform/github.json+3−3
@@ -1,8 +1,8 @@
11{
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
4− "branch": "develop",
4+ "branch": "release/0.7.0.0",
55 "private": false,
6− "rev": "c21ec7036c8351185cd2feef9ce577b6a61c4f22",
7− "sha256": "0m4sdpz6qvngralpyprrbvsyi2zmfh2l3q5vhihwfp3842blvbfg"
6+ "rev": "27a7851a780ff21472d85f9a02fb84635ef9027a",
7+ "sha256": "1m4zlw91dpjlbdchjyxhvhv3k7lilm0qa17dhhww868xxc27sann"
88}
haskell-overlays/misc-deps.nix+8−2
@@ -9,8 +9,14 @@ let
99in
1010
1111{
12− hnix = haskellLib.dontCheck (haskellLib.doJailbreak (self.callCabal2nix "hnix" (hackGet ../dep/hnix) {}));
13− hnix-store-core = self.callHackage "hnix-store-core" "0.1.0.0" {};
12+ regex-base = self.callHackage "regex-base" "0.94.0.0" {};
13+ regex-posix = self.callHackage "regex-posix" "0.96.0.0" {};
14+ regex-tdfa = self.callHackage "regex-tdfa" "1.3.1.0" {};
15+ test-framework = haskellLib.dontCheck (self.callHackage "test-framework" "0.8.2.0" {});
16+ hnix = haskellLib.dontCheck (self.callHackage "hnix" "0.8.0" {});
17+ hnix-store-core = self.callHackage "hnix-store-core" "0.2.0.0" {};
18+
19+ aeson-gadt-th = self.callHackage "aeson-gadt-th" "0.2.4" {};
1420
1521 ghcid = self.callCabal2nix "ghcid" (hackGet ../dep/ghcid) {};
1622 # Exports more internals
lib/asset/README.md+2−2
@@ -5,7 +5,7 @@
55
66**Efficiently served, aggressively cached static web resources.**
77
8−The `assets.nix` file contains nix expressions that are a mutually recursive set of attributes used to create static asset directories with hashable file encodings. This file should be incorporated within your project's `default.nix` file. (See `example/default.nix`, line 2)
8+The `assets.nix` file contains nix expressions that are a mutually recursive set of attributes used to create static asset directories with hashable file encodings. This file should be incorporated within your project's `default.nix` file.
99
1010```nix
1111{ assets ? import ./../assets.nix { inherit nixpkgs; } }:
@@ -19,7 +19,7 @@ In this example, `mkAssets` is used on a directory that has a `.png` file inside
1919myAssets = assets.mkAssets ./static;
2020```
2121
22−Once you have successfully incorporated `assets.nix` into your project's nix file(s), use `nix-build` to generate a symlink of your `mkAssets` expression. The following command from `example/run-example` (line 3) will generate an immutable symlink that holds hashed static assets.
22+Once you have successfully incorporated `assets.nix` into your project's nix file(s), use `nix-build` to generate a symlink of your `mkAssets` expression. The following command will generate an immutable symlink that holds hashed static assets.
2323
2424```bash
2525nix-build -o static.assets -A myAssets
lib/asset/manifest/obelisk-asset-manifest.cabal+11−1
@@ -27,6 +27,7 @@ library
2727 , unix-compat
2828 , vector
2929 exposed-modules:
30+ Obelisk.Asset.Cabal
3031 Obelisk.Asset.Gather
3132 Obelisk.Asset.Promoted
3233 Obelisk.Asset.Symlink
@@ -35,7 +36,7 @@ library
3536 other-extensions: TemplateHaskell
3637 ghc-options:
3738 -Wall -Werror -Wredundant-constraints -Wincomplete-uni-patterns -Wincomplete-record-updates -O2
38− -fno-warn-unused-do-bind -funbox-strict-fields -fprof-auto-calls
39+ -fno-warn-unused-do-bind -funbox-strict-fields -fprof-auto-calls
3940
4041executable obelisk-asset-manifest-generate
4142 hs-source-dirs: src-bin
@@ -44,3 +45,12 @@ executable obelisk-asset-manifest-generate
4445 base
4546 , obelisk-asset-manifest
4647 , text
48+
49+executable obelisk-asset-th-generate
50+ hs-source-dirs: src-bin
51+ main-is: static-th.hs
52+ build-depends:
53+ base
54+ , obelisk-asset-manifest
55+ , filepath
56+ , text
lib/asset/manifest/src-bin/static-th.hsnew file+43
@@ -0,0 +1,43 @@
1+import qualified Data.Text as T
2+import Obelisk.Asset.Cabal
3+import Obelisk.Asset.Gather
4+import Obelisk.Asset.Symlink
5+import System.Environment
6+import System.FilePath
7+
8+main :: IO ()
9+main = do
10+ --TODO: Usage
11+ [root, haskellTarget, packageName, moduleName, fileTarget] <- getArgs
12+ paths <- gatherHashedPaths root
13+ writeCabalProject haskellTarget $ SimplePkg
14+ { _simplePkg_name = T.pack packageName
15+ , _simplePkg_moduleName = T.pack moduleName
16+ , _simplePkg_dependencies = map T.pack
17+ [ "base"
18+ , "obelisk-asset-manifest"
19+ , "template-haskell"
20+ ]
21+ , _simplePkg_moduleContents = T.pack $ unlines
22+ [ "{-# Language CPP #-}"
23+ , "{-|"
24+ , " Description:"
25+ , " Automatically generated module that provides the 'static' TH function"
26+ , " to generate paths to static assets."
27+ , "-}"
28+ , "module " <> moduleName <> " ( static, staticFilePath ) where"
29+ , ""
30+ , "import Obelisk.Asset.TH"
31+ , "import Language.Haskell.TH"
32+ , ""
33+ , "static, staticFilePath :: FilePath -> Q Exp"
34+ , "#ifdef OBELISK_ASSET_PASSTHRU"
35+ , "static = staticAssetRaw"
36+ , "staticFilePath = staticAssetFilePathRaw \"static.out\""
37+ , "#else"
38+ , "static = staticAssetHashed " <> show root
39+ , "staticFilePath = staticAssetFilePathRaw " <> show root
40+ , "#endif"
41+ ]
42+ }
43+ copyAndSymlink paths root fileTarget
lib/asset/manifest/src/Obelisk/Asset/Cabal.hsnew file+42
@@ -0,0 +1,42 @@
1+{-# Language OverloadedStrings #-}
2+module Obelisk.Asset.Cabal where
3+
4+import qualified Data.List as L
5+import Data.Text (Text)
6+import qualified Data.Text as T
7+import qualified Data.Text.IO as T
8+import System.Directory
9+import System.FilePath
10+
11+-- | A generated single-module package
12+data SimplePkg = SimplePkg
13+ { _simplePkg_name :: Text
14+ , _simplePkg_moduleName :: Text
15+ , _simplePkg_moduleContents :: Text
16+ , _simplePkg_dependencies :: [Text]
17+ }
18+
19+cabalFile :: SimplePkg -> Text
20+cabalFile (SimplePkg packageName moduleName _ deps) = T.unlines
21+ [ "name: " <> packageName
22+ , "version: 0"
23+ , "cabal-version: >= 1.2"
24+ , "build-type: Simple"
25+ , ""
26+ , "library"
27+ , " hs-source-dirs: src"
28+ , " build-depends:"
29+ , " " <> T.intercalate ", " deps
30+ , " exposed-modules: " <> moduleName
31+ ]
32+
33+writeCabalProject :: FilePath -> SimplePkg -> IO ()
34+writeCabalProject target pkg = do
35+ createDirectoryIfMissing True target
36+ T.writeFile (target </> T.unpack (_simplePkg_name pkg) <.> "cabal") $ cabalFile pkg
37+ let moduleName = _simplePkg_moduleName pkg
38+ (modName', moduleDirPath) = case L.uncons (reverse $ T.splitOn "." moduleName) of
39+ Nothing -> error $ "writeStaticProject: invalid module name " <> T.unpack moduleName
40+ Just (name, parents) -> (name, target </> "src" </> T.unpack (T.intercalate "/" $ reverse parents))
41+ createDirectoryIfMissing True moduleDirPath
42+ T.writeFile (moduleDirPath </> T.unpack modName' <.> "hs") $ _simplePkg_moduleContents pkg
lib/asset/manifest/src/Obelisk/Asset/Gather.hs+13−13
@@ -6,21 +6,21 @@ module Obelisk.Asset.Gather
66 , toHashedPath
77 ) where
88
9−import Control.DeepSeq
9+import Control.DeepSeq (force)
1010import Control.Monad (forM)
11−import Data.Bits
12−import qualified Data.ByteString.Lazy as LBS
13−import qualified Data.ByteString.Lazy.Builder as LBS
14−import Data.Char
15−import Data.Digest.Pure.SHA
11+import Data.Bits (shift, (.|.), (.&.))
12+import qualified Data.ByteString.Lazy as LBS (readFile, toStrict, ByteString, length, index)
13+import qualified Data.ByteString.Lazy.Builder as LBS (toLazyByteString, word8)
14+import Data.Char (ord)
15+import Data.Digest.Pure.SHA (bytestringDigest, sha256)
1616import Data.Map (Map)
17−import qualified Data.Map as Map
18−import qualified Data.Text as T
19−import Data.Text.Encoding
20−import qualified Data.Vector.Unboxed as UV
21−import Data.Word
22−import System.FilePath.Posix
23−import System.Directory
17+import qualified Data.Map as Map (singleton)
18+import qualified Data.Text as T (unpack)
19+import Data.Text.Encoding (decodeUtf8)
20+import qualified Data.Vector.Unboxed as UV ((!), fromList)
21+import Data.Word (Word8)
22+import System.FilePath.Posix ((</>), splitFileName, normalise)
23+import System.Directory (listDirectory, doesFileExist)
2424
2525-- | Given a path, recursively explore it, creating hashed paths for all files found
2626gatherHashedPaths
lib/asset/manifest/src/Obelisk/Asset/Promoted.hs+8−27
@@ -10,7 +10,6 @@ module Obelisk.Asset.Promoted
1010import Obelisk.Asset.Gather
1111
1212import Data.Foldable
13−import qualified Data.List as L
1413import Language.Haskell.TH (runQ, pprint)
1514import Language.Haskell.TH.Syntax hiding (lift)
1615import GHC.TypeLits
@@ -18,12 +17,11 @@ import Data.Sequence (Seq)
1817import qualified Data.Sequence as Seq
1918import Control.Monad.Trans.Writer
2019import System.FilePath
21−import System.Directory
2220import Data.Text (Text)
2321import qualified Data.Text as T
24−import qualified Data.Text.IO as T
2522import Data.Map (Map)
2623import qualified Data.Map as Map
24+import Obelisk.Asset.Cabal (writeCabalProject, SimplePkg(..))
2725
2826data StaticConfig = StaticConfig
2927 { _staticConfig_packageName :: Text --TODO: Better type
@@ -32,31 +30,14 @@ data StaticConfig = StaticConfig
3230
3331writeStaticProject :: Map FilePath FilePath -> FilePath -> StaticConfig -> IO ()
3432writeStaticProject paths target cfg = do
35− createDirectoryIfMissing True target
36− T.writeFile (target </> T.unpack (_staticConfig_packageName cfg) <.> "cabal") $ staticCabalFile cfg
3733 let modName = _staticConfig_moduleName cfg
38− (modName', moduleDirPath) = case L.uncons (reverse $ T.splitOn "." modName) of
39− Nothing -> error $ "writeStaticProject: invalid module name " <> T.unpack modName
40− Just (name, parents) -> (name, target </> "src" </> T.unpack (T.intercalate "/" $ reverse parents))
41− createDirectoryIfMissing True moduleDirPath
4234 modContents <- staticModuleFile modName paths
43− T.writeFile (moduleDirPath </> T.unpack modName' <.> "hs") modContents
44−
45−staticCabalFile :: StaticConfig -> Text
46−staticCabalFile cfg = T.unlines
47− [ "name: " <> _staticConfig_packageName cfg
48− , "version: 0"
49− , "cabal-version: >= 1.2"
50− , "build-type: Simple"
51− , ""
52− , "library"
53− , " hs-source-dirs: src"
54− , " build-depends:"
55− , " base,"
56− , " ghc-prim,"
57− , " text"
58− , " exposed-modules: " <> _staticConfig_moduleName cfg
59− ]
35+ writeCabalProject target $ SimplePkg
36+ { _simplePkg_name = _staticConfig_packageName cfg
37+ , _simplePkg_moduleName = _staticConfig_moduleName cfg
38+ , _simplePkg_moduleContents = modContents
39+ , _simplePkg_dependencies = ["base", "ghc-prim", "text"]
40+ }
6041
6142staticModuleFile :: Text -> Map FilePath FilePath -> IO Text
6243staticModuleFile moduleName paths = do
@@ -69,7 +50,7 @@ staticModuleFile moduleName paths = do
6950 , "{-# LANGUAGE OverloadedStrings #-}"
7051 , "{-# LANGUAGE ScopedTypeVariables #-}"
7152 , "{-# LANGUAGE TypeApplications #-}"
72− , "module " <> moduleName <> " where"
53+ , "module " <> moduleName <> " {-# DEPRECATED \"Generate this module with the 'obelisk-asset-th-generate' executable instead.\" #-} where"
7354 , ""
7455 , "import qualified GHC.Types"
7556 , "import Data.Text (Text)"
lib/asset/manifest/src/Obelisk/Asset/TH.hs+34−3
@@ -1,14 +1,45 @@
1+{-|
2+Description:
3+ Template Haskell for generating asset paths.
4+-}
15module Obelisk.Asset.TH
26 ( assetPath
7+ , staticAssetRaw
8+ , staticAssetHashed
9+ , staticAssetFilePath
10+ , staticAssetFilePathRaw
311 ) where
412
513import Obelisk.Asset.Gather
614
15+import Language.Haskell.TH
716import Language.Haskell.TH.Syntax
817import System.FilePath.Posix
918
19+-- | Produces the hashed path of a file
20+hashedAssetFilePath :: FilePath -> FilePath -> Q FilePath
21+hashedAssetFilePath root relativePath = do
22+ qAddDependentFile $ root </> relativePath
23+ runIO (toHashedPath root relativePath)
24+
1025-- | Produces a string literal with the hashed path of the file
1126assetPath :: FilePath -> FilePath -> Q Exp
12−assetPath root relativePath = do
13− qAddDependentFile $ root </> relativePath
14− LitE . StringL <$> runIO (toHashedPath root relativePath)
27+assetPath root relativePath =
28+ LitE . StringL <$> hashedAssetFilePath root relativePath
29+
30+staticPrefix :: FilePath
31+staticPrefix = "/static"
32+
33+staticAssetRaw :: FilePath -> Q Exp
34+staticAssetRaw fp = returnQ $ LitE $ StringL $ staticPrefix </> fp
35+
36+staticAssetHashed :: FilePath -> FilePath -> Q Exp
37+staticAssetHashed root fp = do
38+ LitE . StringL . (staticPrefix </>) <$> hashedAssetFilePath root fp
39+
40+staticAssetFilePathRaw :: FilePath -> FilePath -> Q Exp
41+staticAssetFilePathRaw root fp = returnQ $ LitE $ StringL $ root </> fp
42+
43+staticAssetFilePath :: FilePath -> FilePath -> Q Exp
44+staticAssetFilePath root fp = do
45+ LitE . StringL . (root </>) <$> hashedAssetFilePath root fp
lib/cliapp/src/Obelisk/CliApp/Logging.hs+5−1
@@ -44,7 +44,7 @@ import Data.Text (Text)
4444import qualified Data.Text as T
4545import qualified Data.Text.IO as T
4646import GHC.IO.Encoding.Types
47−import System.Console.ANSI (Color (Red, Yellow), ColorIntensity (Vivid),
47+import System.Console.ANSI (Color (..), ColorIntensity (Vivid),
4848 ConsoleIntensity (FaintIntensity), ConsoleLayer (Foreground),
4949 SGR (SetColor, SetConsoleIntensity), clearLine)
5050import System.Exit (ExitCode (..))
@@ -184,6 +184,8 @@ writeLog withNewLine noColor (WithSeverity severity s) = if T.null s then pure (
184184 | noColor && severity <= Warning = liftIO $ putFn $ T.pack (show severity) <> ": " <> s
185185 | not noColor && severity <= Error = TS.putStrWithSGR errorColors h withNewLine s
186186 | not noColor && severity <= Warning = TS.putStrWithSGR warningColors h withNewLine s
187+ | not noColor && severity == Notice = TS.putStrWithSGR noticeColors h withNewLine s
188+ | not noColor && severity == Informational = TS.putStrWithSGR infoColors h withNewLine s
187189 | not noColor && severity >= Debug = TS.putStrWithSGR debugColors h withNewLine s
188190 | otherwise = liftIO $ putFn s
189191
@@ -191,6 +193,8 @@ writeLog withNewLine noColor (WithSeverity severity s) = if T.null s then pure (
191193 h = if severity <= Error then stderr else stdout
192194 errorColors = [SetColor Foreground Vivid Red]
193195 warningColors = [SetColor Foreground Vivid Yellow]
196+ infoColors = [SetColor Foreground Vivid Green]
197+ noticeColors = [SetColor Foreground Vivid Blue]
194198 debugColors = [SetConsoleIntensity FaintIntensity]
195199
196200-- | Allow the user to immediately switch to verbose logging upon pressing a particular key.
lib/command/obelisk-command.cabal+4−1
@@ -1,5 +1,5 @@
11name: obelisk-command
2−version: 0.8.0.0
2+version: 0.9.0.1
33cabal-version: >= 1.8
44build-type: Simple
55
@@ -20,6 +20,7 @@ library
2020 , exceptions
2121 , extra
2222 , filepath
23+ , fsnotify
2324 , git
2425 , github
2526 , here
@@ -39,6 +40,8 @@ library
3940 , optparse-applicative
4041 , placeholders
4142 , process
43+ , reflex
44+ , reflex-fsnotify
4245 , shell-escape
4346 , temporary
4447 , terminal-size
lib/command/src/Obelisk/Command/Preprocessor.hs+4−2
@@ -47,7 +47,8 @@ applyPackages origPath inPath outPath packagePaths' = do
4747 Left err -> do
4848 hPutStrLn stderr $ "Error: Unable to parse cabal package " <> packagePath <> "; Skipping preprocessor on " <> origPath <> ". Error: " <> show err
4949 pure Nothing
50− Right (_, packageInfo) -> pure $ Just packageInfo
50+ Right (Just (_, packageInfo)) -> pure $ Just packageInfo
51+ Right Nothing -> pure Nothing
5152
5253 writeOutput packageInfo' inPath outPath
5354
@@ -58,6 +59,7 @@ writeOutput packageInfo' origPath outPath = withFile outPath WriteMode $ \hOut -
5859 where
5960 hPutTextBuilder h = BU.hPutBuilder h . TL.encodeUtf8Builder . TL.toLazyText
6061
62+--NOTE: We cannot restrict the package set by adding '-package' flags to OPTIONS_GHC, because GHC rejects them there. It seems that we won't be able to properly handle that situation until GHC itself supports loading multiple packages officially in GHCi
6163generateHeader :: FilePath -> CabalPackageInfo -> TL.Builder
6264generateHeader origPath packageInfo =
6365 hsExtensions <> ghcOptions <> lineNumberPragma origPath
@@ -75,7 +77,7 @@ generateHeader origPath packageInfo =
7577 ext -> (TL.fromString (show ext) :)
7678 showExt = \case
7779 EnableExtension ext -> [TL.fromString (show ext)]
78− DisableExtension _ -> []
80+ DisableExtension ext -> ["No" <> TL.fromString (show ext)]
7981 UnknownExtension ext -> [TL.fromString ext]
8082
8183 ghcOptions =
lib/command/src/Obelisk/Command/Project.hs+142−13
@@ -1,6 +1,9 @@
11{-# LANGUAGE FlexibleContexts #-}
22{-# LANGUAGE LambdaCase #-}
33{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE ScopedTypeVariables #-}
5+{-# LANGUAGE TupleSections #-}
6+
47module Obelisk.Command.Project
58 ( InitSource (..)
69 , findProjectObeliskCommand
@@ -11,17 +14,24 @@ module Obelisk.Command.Project
1114 , nixShellRunProc
1215 , nixShellWithHoogle
1316 , nixShellWithoutPkgs
17+ , mkObNixShellProc
1418 , obeliskDirName
1519 , toImplDir
1620 , toObeliskDir
1721 , withProjectRoot
1822 , bashEscape
23+ , getHaskellManifestProjectPath
24+ , AssetSource(..)
25+ , describeImpureAssetSource
26+ , watchStaticFilesDerivation
1927 ) where
2028
2129import Control.Concurrent.MVar (MVar, newMVar, withMVarMasked)
22−import Control.Lens ((.~), (?~), (<&>))
30+import Control.Lens ((.~), (?~), (<&>), (^.), _2, _3)
2331import Control.Monad
32+import Control.Monad.Except
2433import Control.Monad.IO.Class (liftIO)
34+import Control.Monad.Log
2535import Control.Monad.State
2636import qualified Data.Aeson as Json
2737import qualified Data.ByteString.UTF8 as BSU
@@ -30,28 +40,36 @@ import qualified Data.ByteString.Lazy as BSL
3040import Data.Default (def)
3141import Data.Function ((&), on)
3242import Data.Map (Map)
43+import Data.Maybe (isJust)
3344import Data.Text (Text)
3445import qualified Data.Text as T
3546import Data.Text.Encoding (decodeUtf8, encodeUtf8)
3647import Data.Traversable (for)
48+import Reflex
49+import Reflex.FSNotify
50+import Reflex.Host.Headless
3751import System.Directory
3852import System.Environment (lookupEnv)
53+import System.Exit (ExitCode(..))
3954import System.FilePath
55+import System.FSNotify (defaultConfig, eventPath, WatchConfig(..))
56+import qualified System.Info as SysInfo
4057import System.IO.Temp
4158import System.IO.Unsafe (unsafePerformIO)
4259import System.PosixCompat.Files
4360import System.PosixCompat.Types
4461import System.PosixCompat.User
62+import qualified System.Process as Proc
4563import Text.ShellEscape (bash, bytes)
4664
4765import GitHub.Data.GitData (Branch)
4866import GitHub.Data.Name (Name)
4967
50−import Obelisk.App (MonadObelisk)
68+import Obelisk.App (MonadObelisk, runObelisk, getObelisk)
5169import Obelisk.CliApp
5270import Obelisk.Command.Nix
5371import Obelisk.Command.Thunk
54−import Obelisk.Command.Utils (nixBuildExePath, nixExePath, toNixPath, cp, nixShellPath)
72+import Obelisk.Command.Utils (nixBuildExePath, nixExePath, toNixPath, cp, nixShellPath, lnPath)
5573
5674--TODO: Make this module resilient to random exceptions
5775
@@ -300,7 +318,7 @@ bashEscape = BSU.toString . bytes . bash . BSU.fromString
300318nixShellRunProc :: NixShellConfig -> ProcessSpec
301319nixShellRunProc cfg = setDelegateCtlc True $ proc nixShellPath $ runNixShellConfig cfg
302320
303−nixShellWithoutPkgs
321+mkObNixShellProc
304322 :: MonadObelisk m
305323 => FilePath -- ^ Path to project root
306324 -> Bool -- ^ Should this be a pure shell?
@@ -308,12 +326,12 @@ nixShellWithoutPkgs
308326 -> Map Text FilePath -- ^ Package names mapped to their paths
309327 -> String -- ^ Shell attribute to use (e.g. @"ghc"@, @"ghcjs"@, etc.)
310328 -> Maybe String -- ^ If 'Just' run the given command; otherwise just open the interactive shell
311− -> m ()
312−nixShellWithoutPkgs root isPure chdirToRoot packageNamesAndPaths shellAttr command = do
329+ -> m ProcessSpec
330+mkObNixShellProc root isPure chdirToRoot packageNamesAndPaths shellAttr command = do
313331 packageNamesAndAbsPaths <- liftIO $ for packageNamesAndPaths makeAbsolute
314332 defShellConfig <- nixShellRunConfig root isPure command
315333 let setCwd_ = if chdirToRoot then setCwd (Just root) else id
316− runProcess_ $ setCwd_ $ nixShellRunProc $ defShellConfig
334+ pure $ setCwd_ $ nixShellRunProc $ defShellConfig
317335 & nixShellConfig_common . nixCmdConfig_target . target_expr ?~
318336 "{root, pkgs, shell}: ((import root {}).passthru.__unstable__.self.extend (_: _: {\
319337 \shellPackages = builtins.fromJSON pkgs;\
@@ -324,6 +342,18 @@ nixShellWithoutPkgs root isPure chdirToRoot packageNamesAndPaths shellAttr comma
324342 , strArg "shell" shellAttr
325343 ]
326344
345+nixShellWithoutPkgs
346+ :: MonadObelisk m
347+ => FilePath -- ^ Path to project root
348+ -> Bool -- ^ Should this be a pure shell?
349+ -> Bool -- ^ Should we chdir to the package root in the shell?
350+ -> Map Text FilePath -- ^ Package names mapped to their paths
351+ -> String -- ^ Shell attribute to use (e.g. @"ghc"@, @"ghcjs"@, etc.)
352+ -> Maybe String -- ^ If 'Just' run the given command; otherwise just open the interactive shell
353+ -> m ()
354+nixShellWithoutPkgs root isPure chdirToRoot packageNamesAndPaths shellAttr command = do
355+ runProcess_ =<< mkObNixShellProc root isPure chdirToRoot packageNamesAndPaths shellAttr command
356+
327357nixShellWithHoogle :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
328358nixShellWithHoogle root isPure shell' command = do
329359 defShellConfig <- nixShellRunConfig root isPure command
@@ -334,7 +364,21 @@ nixShellWithHoogle root isPure shell' command = do
334364 \})).project.shells.${shell}"
335365 & nixShellConfig_common . nixCmdConfig_args .~ [ strArg "shell" shell' ]
336366
337−findProjectAssets :: MonadObelisk m => FilePath -> m Text
367+-- | Describes the provenance of static assets (i.e., are they the result of a derivation
368+-- that was built, or just a folder full of files.
369+data AssetSource = AssetSource_Derivation
370+ | AssetSource_Files
371+ deriving (Eq)
372+
373+-- | Some log messages to make it easier to tell where static files are coming from
374+describeImpureAssetSource :: AssetSource -> Text -> Text
375+describeImpureAssetSource src path = case src of
376+ AssetSource_Files -> "Assets impurely loaded from: " <> path
377+ AssetSource_Derivation -> "Assets derivation built and impurely loaded from: " <> path
378+
379+-- | Determine where the static files of a project are and whether they're plain files or a derivation.
380+-- If they are a derivation, that derivation will be built.
381+findProjectAssets :: MonadObelisk m => FilePath -> m (AssetSource, Text)
338382findProjectAssets root = do
339383 isDerivation <- readProcessAndLogStderr Debug $ setCwd (Just root) $
340384 proc nixExePath
@@ -346,10 +390,95 @@ findProjectAssets root = do
346390 ]
347391 -- Check whether the impure static files are a derivation (and so must be built)
348392 if isDerivation == "1"
349− then fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $ -- Strip whitespace here because nix-build has no --raw option
350− proc nixBuildExePath
351− [ "--no-out-link"
393+ then do
394+ _ <- buildStaticFilesDerivationAndSymlink
395+ (readProcessAndLogStderr Debug)
396+ root
397+ pure (AssetSource_Derivation, T.pack $ root </> "static.out")
398+ else fmap (AssetSource_Files,) $ do
399+ path <- readProcessAndLogStderr Debug $ setCwd (Just root) $
400+ proc nixExePath ["eval", "-f", ".", "passthru.staticFilesImpure", "--raw"]
401+ _ <- readProcessAndLogStderr Debug $ setCwd (Just root) $
402+ proc lnPath ["-sfT", T.unpack path, "./static.out"]
403+ pure path
404+
405+-- | Get the nix store path to the generated static asset manifest module (e.g., "obelisk-generated-static")
406+getHaskellManifestProjectPath :: MonadObelisk m => FilePath -> m Text
407+getHaskellManifestProjectPath root = fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $
408+ proc nixBuildExePath
409+ [ "--no-out-link"
410+ , "-E"
411+ , "(let a = import ./. {}; in a.passthru.processedStatic.haskellManifest)"
412+ ]
413+
414+-- | Watch the project directory for file changes and check whether those file changes
415+-- cause changes in the static files nix derivation. If so, rebuild it.
416+watchStaticFilesDerivation
417+ :: (MonadIO m, MonadObelisk m)
418+ => FilePath
419+ -> m ()
420+watchStaticFilesDerivation root = do
421+ ob <- getObelisk
422+ drv0 <- showDerivation
423+ liftIO $ runHeadlessApp $ do
424+ pb <- getPostBuild
425+ checkForChanges <- batchOccurrences 0.25 =<< watchDirectoryTree
426+ -- On macOS, use the polling backend due to https://github.com/luite/hfsevents/issues/13
427+ (defaultConfig { confUsePolling = SysInfo.os == "darwin", confPollInterval = 250000 })
428+ (root <$ pb)
429+ ((/="static.out") . takeFileName . eventPath)
430+ drv <- performEvent $ ffor checkForChanges $ \_ ->
431+ liftIO $ runObelisk ob showDerivation
432+ drvs <- foldDyn (\new (_, old, _) -> (old, new, old /= new)) (drv0, drv0, False) drv
433+ void $ throttleBatchWithLag
434+ (\e -> performEvent $ ffor e $ \_ -> liftIO $ runObelisk ob $ do
435+ putLog Notice "Static assets being built..."
436+ buildStaticCatchErrors >>= \case
437+ Nothing -> pure ()
438+ Just _ -> putLog Notice "Static assets built and symlinked to static.out"
439+ )
440+ ((() <$) . ffilter (\x -> isJust (x ^._2) && x ^._3) $ updated drvs)
441+ pure never
442+ where
443+ handleBuildFailure
444+ :: MonadObelisk m
445+ => (ExitCode, String, String)
446+ -> m (Maybe Text)
447+ handleBuildFailure (ex, out, err) = case ex of
448+ ExitSuccess -> pure $ Just $ T.pack out
449+ _ -> do
450+ putLog Error $
451+ ("Static assets build failed: " <>) $
452+ T.unlines $ reverse $ take 10 $ reverse $ T.lines $ T.pack err
453+ pure Nothing
454+ showDerivation :: MonadObelisk m => m (Maybe Text)
455+ showDerivation =
456+ handleBuildFailure <=< readCreateProcessWithExitCode $
457+ setCwd (Just root) $ ProcessSpec
458+ { _processSpec_createProcess = Proc.proc nixExePath
459+ [ "show-derivation"
460+ , "-f", "."
461+ , "passthru.staticFilesImpure"
462+ ]
463+ , _processSpec_overrideEnv = Nothing
464+ }
465+ buildStaticCatchErrors :: MonadObelisk m => m (Maybe Text)
466+ buildStaticCatchErrors = handleBuildFailure =<<
467+ buildStaticFilesDerivationAndSymlink
468+ readCreateProcessWithExitCode
469+ root
470+
471+buildStaticFilesDerivationAndSymlink
472+ :: MonadObelisk m
473+ => (ProcessSpec -> m a)
474+ -> FilePath
475+ -> m a
476+buildStaticFilesDerivationAndSymlink f root = f $
477+ setCwd (Just root) $ ProcessSpec
478+ { _processSpec_createProcess = Proc.proc
479+ nixBuildExePath
480+ [ "-o", "static.out"
352481 , "-E", "(import ./. {}).passthru.staticFilesImpure"
353482 ]
354− else readProcessAndLogStderr Debug $ setCwd (Just root) $
355− proc nixExePath ["eval", "-f", ".", "passthru.staticFilesImpure", "--raw"]
483+ , _processSpec_overrideEnv = Nothing
484+ }
lib/command/src/Obelisk/Command/Run.hs+78−21
@@ -14,7 +14,8 @@ module Obelisk.Command.Run where
1414import Control.Arrow ((&&&))
1515import Control.Exception (Exception, bracket)
1616import Control.Lens (ifor, (.~), (&))
17−import Control.Monad (filterM, unless)
17+import Control.Concurrent (forkIO)
18+import Control.Monad (filterM, void)
1819import Control.Monad.Except (runExceptT, throwError)
1920import Control.Monad.IO.Class (liftIO)
2021import Control.Monad.Reader (MonadIO)
@@ -45,13 +46,23 @@ import Debug.Trace (trace)
4546import Distribution.Compiler (CompilerFlavor(..))
4647import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
4748import Distribution.Parsec.ParseResult (runParseResult)
49+import Distribution.Pretty (prettyShow)
50+import Distribution.Simple.Compiler (PackageDB (GlobalPackageDB))
51+import Distribution.Simple.Configure (configCompilerEx, getInstalledPackages)
52+import Distribution.Simple.PackageIndex (InstalledPackageIndex, lookupDependency)
53+import Distribution.Simple.Program.Db (defaultProgramDb)
4854import qualified Distribution.System as Dist
49−import Distribution.Types.BuildInfo (buildable, cppOptions, defaultExtensions, defaultLanguage, hsSourceDirs, options)
55+import Distribution.Types.BuildInfo (buildable, cppOptions, defaultExtensions, defaultLanguage, hsSourceDirs, options, targetBuildDepends)
5056import Distribution.Types.CondTree (simplifyCondTree)
57+import Distribution.Types.Dependency (Dependency (..), depPkgName)
5158import Distribution.Types.GenericPackageDescription (ConfVar (Arch, Impl, OS), condLibrary)
59+import Distribution.Types.InstalledPackageInfo (compatPackageKey)
5260import Distribution.Types.Library (libBuildInfo)
61+import Distribution.Types.PackageName (mkPackageName)
62+import Distribution.Types.VersionRange (anyVersion)
5363import Distribution.Utils.Generic (toUTF8BS, readUTF8File)
5464import qualified Distribution.Parsec.Common as Dist
65+import qualified Distribution.Verbosity as Verbosity (silent)
5566import qualified Hpack.Config as Hpack
5667import qualified Hpack.Render as Hpack
5768import qualified Hpack.Yaml as Hpack
@@ -63,7 +74,7 @@ import System.FilePath
6374import qualified System.Info
6475import System.IO.Temp (withSystemTempDirectory)
6576
66−import Obelisk.App (MonadObelisk)
77+import Obelisk.App (MonadObelisk, getObelisk, runObelisk)
6778import Obelisk.CliApp (
6879 Severity (..),
6980 failWith,
@@ -77,7 +88,7 @@ import Obelisk.CliApp (
7788 withSpinner,
7889 )
7990import Obelisk.Command.Nix
80−import Obelisk.Command.Project (nixShellWithoutPkgs, withProjectRoot, findProjectAssets, bashEscape)
91+import Obelisk.Command.Project
8192import Obelisk.Command.Thunk (attrCacheFileName)
8293import Obelisk.Command.Utils (findExePath, ghcidExePath)
8394
@@ -96,6 +107,8 @@ data CabalPackageInfo = CabalPackageInfo
96107 -- ^ List of compiler-specific options (e.g., the "ghc-options" field of the cabal file)
97108 , _cabalPackageInfo_cppOptions :: [String]
98109 -- ^ List of CPP (C Preprocessor) options (e.g. the "cpp-options" field of the cabal file)
110+ , _cabalPackageInfo_buildDepends :: [Dependency]
111+ -- ^ List of build dependencies listed in the cabal file
99112 }
100113
101114-- | 'Bool' with a better name for its purpose.
@@ -144,8 +157,8 @@ profile profileBasePattern rtsFlags = withProjectRoot "." $ \root -> do
144157 , _target_attr = Just "__unstable__.profiledObRun"
145158 , _target_expr = Nothing
146159 }
147− assets <- findProjectAssets root
148− putLog Debug $ "Assets impurely loaded from: " <> assets
160+ (assetType, assets) <- findProjectAssets root
161+ putLog Debug $ describeImpureAssetSource assetType assets
149162 time <- liftIO getCurrentTime
150163 let profileBaseName = formatTime defaultTimeLocale profileBasePattern time
151164 liftIO $ createDirectoryIfMissing True $ takeDirectory $ root </> profileBaseName
@@ -163,9 +176,16 @@ profile profileBasePattern rtsFlags = withProjectRoot "." $ \root -> do
163176run :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
164177run root interpretPaths = do
165178 pkgs <- getParsedLocalPkgs root interpretPaths
166− assets <- findProjectAssets root
167− putLog Debug $ "Assets impurely loaded from: " <> assets
168− ghciArgs <- getGhciSessionSettings pkgs root True
179+ (assetType, assets) <- findProjectAssets root
180+ manifestPkg <- parsePackagesOrFail . (:[]) . T.unpack =<< getHaskellManifestProjectPath root
181+ putLog Debug $ describeImpureAssetSource assetType assets
182+ case assetType of
183+ AssetSource_Derivation -> do
184+ ob <- getObelisk
185+ putLog Debug "Starting static file derivation watcher..."
186+ void $ liftIO $ forkIO $ runObelisk ob $ watchStaticFilesDerivation root
187+ _ -> pure ()
188+ ghciArgs <- getGhciSessionSettings (pkgs <> manifestPkg) root True
169189 freePort <- getFreePort
170190 withGhciScriptArgs pkgs $ \dotGhciArgs -> do
171191 runGhcid root True (ghciArgs <> dotGhciArgs) pkgs $ Just $ unwords
@@ -222,7 +242,12 @@ getLocalPkgs root interpretPaths = do
222242 let rootsAndExclusions = calcIntepretFinds "" interpretPaths
223243
224244 fmap fold $ for (MMap.toAscList rootsAndExclusions) $ \(interpretPathRoot, exclusions) ->
225− let allExclusions = obeliskPackageExclusions <> exclusions <> Set.singleton ("*" </> attrCacheFileName)
245+ let allExclusions = obeliskPackageExclusions
246+ <> exclusions
247+ <> Set.singleton ("*" </> attrCacheFileName)
248+ <> Set.singleton ("*" </> "lib/asset/manifest") -- NB: obelisk-asset-manifest is excluded because it generates
249+ -- a module that in turn imports it. This will cause ob run to
250+ -- fail in its current implementation.
226251 in fmap (Set.fromList . map normalise) $ runFind $
227252 ["-L", interpretPathRoot, "(", "-name", "*.cabal", "-o", "-name", Hpack.packageConfig, ")", "-a", "-type", "f"]
228253 <> concat [["-not", "-path", p </> "*"] | p <- toList allExclusions]
@@ -304,15 +329,16 @@ parseCabalPackage
304329 -> m (Maybe CabalPackageInfo)
305330parseCabalPackage dir = parseCabalPackage' dir >>= \case
306331 Left err -> Nothing <$ putLog Error err
307− Right (warnings, pkgInfo) -> do
332+ Right (Just (warnings, pkgInfo)) -> do
308333 for_ warnings $ putLog Warning . T.pack . show
309334 pure $ Just pkgInfo
335+ Right Nothing -> pure Nothing
310336
311337-- | Like 'parseCabalPackage' but returns errors and warnings directly so as to avoid 'MonadObelisk'.
312338parseCabalPackage'
313339 :: (MonadIO m)
314340 => FilePath -- ^ Package directory
315− -> m (Either T.Text ([Dist.PWarning], CabalPackageInfo))
341+ -> m (Either T.Text (Maybe ([Dist.PWarning], CabalPackageInfo)))
316342parseCabalPackage' pkg = runExceptT $ do
317343 (cabalContents, packageFile, packageName) <- guessCabalPackageFile pkg >>= \case
318344 Left GuessPackageFileError_NotFound -> throwError $ "No .cabal or package.yaml file found in " <> T.pack pkg
@@ -340,7 +366,7 @@ parseCabalPackage' pkg = runExceptT $ do
340366 case condLibrary <$> result of
341367 Right (Just condLib) -> do
342368 let (_, lib) = simplifyCondTree evalConfVar condLib
343− pure $ (warnings,) $ CabalPackageInfo
369+ pure $ Just $ (warnings,) $ CabalPackageInfo
344370 { _cabalPackageInfo_packageName = T.pack packageName
345371 , _cabalPackageInfo_packageFile = packageFile
346372 , _cabalPackageInfo_packageRoot = takeDirectory packageFile
@@ -353,18 +379,19 @@ parseCabalPackage' pkg = runExceptT $ do
353379 defaultLanguage $ libBuildInfo lib
354380 , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
355381 , _cabalPackageInfo_cppOptions = cppOptions $ libBuildInfo lib
382+ , _cabalPackageInfo_buildDepends = targetBuildDepends $ libBuildInfo lib
356383 }
357− Right Nothing -> throwError "Haskell package has no library component"
384+ Right Nothing -> pure Nothing
358385 Left (_, errors) ->
359386 throwError $ T.pack $ "Failed to parse " <> packageFile <> ":\n" <> unlines (map show errors)
360387
361388parsePackagesOrFail :: (MonadObelisk m, Foldable f) => f FilePath -> m (NE.NonEmpty CabalPackageInfo)
362389parsePackagesOrFail dirs' = do
363− (pkgDirErrs, packageInfos') <- fmap partitionEithers $ for dirs $ \dir -> do
390+ packageInfos' <- fmap catMaybes $ for dirs $ \dir -> do
364391 flip fmap (parseCabalPackage dir) $ \case
365392 Just packageInfo
366− | _cabalPackageInfo_buildable packageInfo -> Right packageInfo
367− _ -> Left dir
393+ | _cabalPackageInfo_buildable packageInfo -> Just packageInfo
394+ _ -> Nothing
368395
369396 -- Sort duplicate packages such that we prefer shorter paths, but fall back to alphabetical ordering.
370397 let packagesByName = Map.map (NE.sortBy $ comparing $ \p -> let n = _cabalPackageInfo_packageFile p in (length n, n))
@@ -386,9 +413,6 @@ parsePackagesOrFail dirs' = do
386413 "No valid, buildable packages found" <> (if null dirs then "" else " in " <> intercalate ", " dirs)
387414 Just xs -> pure xs
388415
389− unless (null pkgDirErrs) $
390− putLog Warning $ T.pack $ "Failed to find buildable packages in " <> intercalate ", " pkgDirErrs
391−
392416 pure packageInfos
393417 where
394418 dirs = toList dirs'
@@ -402,7 +426,8 @@ withGhciScriptArgs
402426 => f CabalPackageInfo -- ^ List of packages to load into ghci
403427 -> ([String] -> m ()) -- ^ Action to run with the extra ghci arguments
404428 -> m ()
405−withGhciScriptArgs packageInfos f = withGhciScript loadPreludeManually packageInfos $ \fp -> f ["-XNoImplicitPrelude", "-ghci-script", fp]
429+withGhciScriptArgs packageInfos f = withGhciScript loadPreludeManually packageInfos $ \fp ->
430+ f ["-XNoImplicitPrelude", "-ghci-script", fp]
406431 where
407432 -- These lines must be first and allow the session to support a custom Prelude when @-XNoImplicitPrelude@
408433 -- is passed to the ghci session.
@@ -452,6 +477,7 @@ getGhciSessionSettings (toList -> packageInfos) pathBase useRelativePaths = do
452477 -- all paths to 'pathBase'.
453478 selfExe <- liftIO $ canonicalizePath =<< getExecutablePath
454479 canonicalPathBase <- liftIO $ canonicalizePath pathBase
480+ installedPackageIndex <- loadPackageIndex packageInfos pathBase
455481
456482 (pkgFiles, pkgSrcPaths :: [NonEmpty FilePath]) <- fmap unzip $ liftIO $ for packageInfos $ \pkg -> do
457483 canonicalSrcDirs <- traverse canonicalizePath $ (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
@@ -460,16 +486,47 @@ getGhciSessionSettings (toList -> packageInfos) pathBase useRelativePaths = do
460486
461487 pure
462488 $ baseGhciOptions
489+ <> ["-DOBELISK_ASSET_PASSTHRU"] -- For passthrough static assets
463490 <> ["-F", "-pgmF", selfExe, "-optF", preprocessorIdentifier]
464491 <> concatMap (\p -> ["-optF", p]) pkgFiles
465492 <> [ "-i" <> intercalate ":" (concatMap toList pkgSrcPaths) ]
493+ <> concatMap (\packageId -> ["-package-id", packageId ])
494+ (packageIds installedPackageIndex)
466495 where
467496 relativeTo' = if useRelativePaths then relativeTo else const
497+ -- Package names we're building and not needed from the package DB
498+ packageNames =
499+ map (mkPackageName . T.unpack . _cabalPackageInfo_packageName)
500+ packageInfos
501+ packageIds installedPackageIndex =
502+ map (dependencyPackageId installedPackageIndex) $
503+ filter ((`notElem` packageNames) . depPkgName) $
504+ concatMap _cabalPackageInfo_buildDepends packageInfos <>
505+ [Dependency (mkPackageName "obelisk-run") anyVersion]
506+ dependencyPackageId installedPackageIndex dep =
507+ case lookupDependency installedPackageIndex dep of
508+ ((_version,installedPackageInfo:_) :_) ->
509+ compatPackageKey installedPackageInfo
510+ _ -> error $ "Couldn't resolve dependency for " <> prettyShow dep
511+
512+-- Load the package index used by the GHC in this path's nix project
513+loadPackageIndex :: MonadObelisk m => [CabalPackageInfo] -> FilePath -> m InstalledPackageIndex
514+loadPackageIndex packageInfos root = do
515+ ghcPath <- getPathInNixEnvironment "bash -c 'type -p ghc'"
516+ ghcPkgPath <- getPathInNixEnvironment "bash -c 'type -p ghc-pkg'"
517+ (compiler, _platform, programDb) <- liftIO
518+ $ configCompilerEx (Just GHC) (Just ghcPath) (Just ghcPkgPath) defaultProgramDb Verbosity.silent
519+ liftIO $ getInstalledPackages Verbosity.silent compiler [GlobalPackageDB] programDb
520+ where
521+ getPathInNixEnvironment cmd = do
522+ path <- readProcessAndLogStderr Debug =<< mkObNixShellProc root False True (packageInfoToNamePathMap packageInfos) "ghc" (Just cmd)
523+ liftIO $ canonicalizePath $ T.unpack $ T.strip path
468524
469525baseGhciOptions :: [String]
470526baseGhciOptions =
471527 [ "-ignore-dot-ghci"
472528 , "-no-user-package-db"
529+ , "-hide-all-packages"
473530 , "-package-env", "-"
474531 ]
475532
lib/command/src/Obelisk/Command/Utils.hs+3
@@ -75,6 +75,9 @@ gitPath = $(staticWhich "git")
7575whichPath :: FilePath
7676whichPath = $(staticWhich "which")
7777
78+lnPath :: FilePath
79+lnPath = $(staticWhich "ln")
80+
7881sshKeygenPath :: FilePath
7982sshKeygenPath = $(staticWhich "ssh-keygen")
8083
lib/executable-config/lookup/src/Obelisk/Configs.hs+2
@@ -22,6 +22,7 @@ import Control.Applicative (Alternative)
2222import Control.Monad (MonadPlus)
2323import Control.Monad.Base (MonadBase)
2424import Control.Monad.Catch (MonadThrow)
25+import Control.Monad.Fail (MonadFail)
2526import Control.Monad.Fix (MonadFix)
2627import Control.Monad.IO.Class (MonadIO)
2728import Control.Monad.Morph (MFunctor)
@@ -87,6 +88,7 @@ newtype ConfigsT m a = ConfigsT { unConfigsT :: ReaderT (Map Text ByteString) m
8788 , Monad
8889 , MonadPlus
8990 , Alternative
91+ , MonadFail
9092 , MonadFix
9193 , MonadThrow
9294 , MonadIO
lib/route/src/Obelisk/Route.hs+3−2
@@ -170,6 +170,7 @@ import Data.Text (Text)
170170import qualified Data.Text as T
171171import qualified Data.Text.Encoding as T
172172import Data.Text.Lens (IsText, packed, unpacked)
173+import Data.Type.Equality
173174import Data.Universe
174175import Data.Universe.Some
175176import Network.HTTP.Types.URI
@@ -1038,8 +1039,8 @@ someSumEncoder = Encoder $ pure $ EncoderImpl
10381039
10391040data Void1 :: * -> * where {}
10401041
1041−instance Universe (Some Void1) where
1042− universe = []
1042+instance UniverseSome Void1 where
1043+ universeSome = []
10431044
10441045void1Encoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Some Void1) a
10451046void1Encoder = Encoder $ pure $ EncoderImpl
lib/run/src/Obelisk/Run.hs+3−3
@@ -144,9 +144,9 @@ runWidget conf configs frontend validFullEncoder = do
144144
145145 cert <- X509.newX509 >>= X509Request.makeX509FromReq certRequest
146146 _ <- X509.setPublicKey cert privateKey
147− now <- getCurrentTime
148− _ <- X509.setNotBefore cert $ addUTCTime (-1) now
149− _ <- X509.setNotAfter cert $ addUTCTime (365 * 24 * 60 * 60) now
147+ timenow <- getCurrentTime
148+ _ <- X509.setNotBefore cert $ addUTCTime (-1) timenow
149+ _ <- X509.setNotAfter cert $ addUTCTime (365 * 24 * 60 * 60) timenow
150150 _ <- X509.signX509 cert privateKey Nothing
151151
152152 certByteString <- BSUTF8.fromString <$> PEM.writeX509 cert
lib/snap-extras/src/Obelisk/Snap/Extras.hs+1−1
@@ -17,7 +17,7 @@ import System.Directory
1717-- | Set response header for "permanent" caching
1818cachePermanently :: MonadSnap m => m ()
1919cachePermanently = do
20− modifyResponse $ setHeader "Cache-Control" "public, max-age=315360000"
20+ modifyResponse $ setHeader "Cache-Control" "public, max-age=315360000, immutable"
2121 modifyResponse $ setHeader "Expires" "Tue, 01 Feb 2050 00:00:00 GMT" --TODO: This should be set to "approximately one year from the time the response is sent"
2222
2323-- | Set response header to not cache
skeleton/.gitignore+1
@@ -13,3 +13,4 @@ result
1313result-*
1414tags
1515TAGS
16+static.out
skeleton/backend/backend.cabal+2−2
@@ -14,12 +14,12 @@ library
1414 , obelisk-route
1515 exposed-modules:
1616 Backend
17− ghc-options: -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wincomplete-record-updates -O
17+ ghc-options: -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wincomplete-record-updates -O -fno-show-valid-hole-fits
1818
1919executable backend
2020 main-is: main.hs
2121 hs-source-dirs: src-bin
22− ghc-options: -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wincomplete-record-updates -O -threaded
22+ ghc-options: -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wincomplete-record-updates -O -threaded -fno-show-valid-hole-fits
2323 if impl(ghcjs)
2424 buildable: False
2525 build-depends: base
skeleton/common/common.cabal+1−1
@@ -12,4 +12,4 @@ library
1212 exposed-modules:
1313 Common.Api
1414 Common.Route
15− ghc-options: -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wincomplete-record-updates -O
15+ ghc-options: -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wincomplete-record-updates -O -fno-show-valid-hole-fits
skeleton/default.nix+10−3
@@ -1,10 +1,17 @@
1−{ obelisk ? import ./.obelisk/impl {
2− system = builtins.currentSystem;
3− iosSdkVersion = "10.2";
1+{ system ? builtins.currentSystem
2+, obelisk ? import ./.obelisk/impl {
3+ inherit system;
4+ iosSdkVersion = "13.2";
5+
46 # You must accept the Android Software Development Kit License Agreement at
57 # https://developer.android.com/studio/terms in order to build Android apps.
68 # Uncomment and set this to `true` to indicate your acceptance:
79 # config.android_sdk.accept_license = false;
10+
11+ # In order to use Let's Encrypt for HTTPS deployments you must accept
12+ # their terms of service at https://letsencrypt.org/repository/.
13+ # Uncomment and set this to `true` to indicate your acceptance:
14+ # terms.security.acme.acceptTerms = false;
815 }
916}:
1017with obelisk;
skeleton/frontend/frontend.cabal+3−3
@@ -10,13 +10,13 @@ library
1010 , obelisk-frontend
1111 , obelisk-route
1212 , jsaddle
13− , reflex-dom
13+ , reflex-dom-core
1414 , obelisk-executable-config-lookup
1515 , obelisk-generated-static
1616 , text
1717 exposed-modules:
1818 Frontend
19− ghc-options: -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wincomplete-record-updates -O
19+ ghc-options: -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wincomplete-record-updates -O -fno-show-valid-hole-fits
2020
2121executable frontend
2222 main-is: main.hs
@@ -28,7 +28,7 @@ executable frontend
2828 , reflex-dom
2929 , obelisk-generated-static
3030 , frontend
31− ghc-options: -threaded -O -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wincomplete-record-updates
31+ ghc-options: -threaded -O -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wincomplete-record-updates -fno-show-valid-hole-fits
3232 if impl(ghcjs)
3333 ghc-options: -dedupe
3434 cpp-options: -DGHCJS_BROWSER
skeleton/frontend/src/Frontend.hs+3−4
@@ -1,6 +1,5 @@
1−{-# LANGUAGE DataKinds #-}
21{-# LANGUAGE OverloadedStrings #-}
3−{-# LANGUAGE TypeApplications #-}
2+{-# LANGUAGE TemplateHaskell #-}
43
54module Frontend where
65
@@ -27,7 +26,7 @@ frontend :: Frontend (R FrontendRoute)
2726frontend = Frontend
2827 { _frontend_head = do
2928 el "title" $ text "Obelisk Minimal Example"
30− elAttr "link" ("href" =: static @"main.css" <> "type" =: "text/css" <> "rel" =: "stylesheet") blank
29+ elAttr "link" ("href" =: $(static "main.css") <> "type" =: "text/css" <> "rel" =: "stylesheet") blank
3130 , _frontend_body = do
3231 el "h1" $ text "Welcome to Obelisk!"
3332 el "p" $ text $ T.pack commonStuff
@@ -38,7 +37,7 @@ frontend = Frontend
3837 -- print "Hello, World!" on the client.
3938 prerender_ blank $ liftJSM $ void $ eval ("console.log('Hello, World!')" :: T.Text)
4039
41− elAttr "img" ("src" =: static @"obelisk.jpg") blank
40+ elAttr "img" ("src" =: $(static "obelisk.jpg")) blank
4241 el "div" $ do
4342 exampleConfig <- getConfig "common/example"
4443 case exampleConfig of