diff --git a/default.nix b/default.nix index 29fd2126f..32555d9fe 100644 --- a/default.nix +++ b/default.nix @@ -68,16 +68,24 @@ in rec { ''; nullIfAbsent = p: if lib.pathExists p then p else null; #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. - processAssets = { src, packageName ? "obelisk-generated-static", moduleName ? "Obelisk.Generated.Static", exe ? "obelisk-asset-th-generate" }: pkgs.runCommand "asset-manifest" { - inherit src; - outputs = [ "out" "haskellManifest" "symlinked" ]; - nativeBuildInputs = [ ghcObelisk.obelisk-asset-manifest ]; - } '' - set -euo pipefail - touch "$out" - mkdir -p "$symlinked" - ${exe} "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked" - ''; + processAssets = + { src + , packageName ? "obelisk-generated-static" + , moduleName ? "Obelisk.Generated.Static" + #, staticFunctionName ? packageName + , staticPath ? "static" + , staticName ? "static" + , exe ? "obelisk-asset-th-generate" + }: pkgs.runCommand "asset-manifest" { + inherit src; + outputs = [ "out" "haskellManifest" "symlinked" ]; + nativeBuildInputs = [ ghcObelisk.obelisk-asset-manifest ]; + } '' + set -euo pipefail + touch "$out" + mkdir -p "$symlinked" + ${exe} "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked" ${staticName} + '' // {inherit packageName;}; compressedJs = frontend: optimizationLevel: externs: pkgs.runCommand "compressedJs" {} '' set -euo pipefail @@ -213,12 +221,16 @@ in rec { exeBackend = if profiling then backend else haskellLib.justStaticExecutables backend; exeFrontend = compressedJs frontend optimizationLevel externjs; exeFrontendAssets = mkAssets exeFrontend; - exeAssets = mkAssets assets; + exeAssets = lib.mapAttrs (n: args: mkAssets (if args.isDrv then (import args.path args.drvArgs) else args.path)) assets; in pkgs.runCommand "serverExe" {} '' mkdir $out set -eux ln -s '${exeBackend}'/bin/* $out/ - ln -s '${exeAssets}' $out/static.assets + mkdir $out/static.assets + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: assets: '' + ln -s '${assets}' $out/static.assets/${name} + '') exeAssets)} + for d in '${exeFrontendAssets}'/*/; do ln -s "$d" "$out"/"$(basename "$d").assets" done @@ -270,19 +282,48 @@ in rec { inherit args; userSettings = { inherit android ios packages overrides tools shellToolOverrides withHoogle externjs __closureCompilerOptimizationLevel __withGhcide __deprecated; - staticFiles = if staticFiles == null then self.base + /static else staticFiles; + staticFiles = + if staticFiles == null + then { static = { path = self.base + /static; isDrv = false; drvArgs = null; moduleName = "Obelisk.Generated.Static"; }; } + else staticFiles; }; frontendName = "frontend"; backendName = "backend"; commonName = "common"; - staticName = "obelisk-generated-static"; - staticFilesImpure = let fs = self.userSettings.staticFiles; in if lib.isDerivation fs then fs else toString fs; - processedStatic = processAssets { - src = self.userSettings.staticFiles; - exe = if lib.attrByPath ["userSettings" "__deprecated" "useObeliskAssetManifestGenerate"] false self - then builtins.trace "obelisk-asset-manifest-generate is deprecated. Use obelisk-asset-th-generate instead." "obelisk-asset-manifest-generate" - else "obelisk-asset-th-generate"; - }; + #staticName = "obelisk-generated-static"; + staticFilesImpure = + let fs = self.userSettings.staticFiles; + in lib.mapAttrs (_: staticArgs: + # path attr just allows us to watch for file changes + if staticArgs.isDrv + then { path = staticArgs.path; src = import staticArgs.path staticArgs.drvArgs; } + else { path = staticArgs.path; src = toString staticArgs.path; } + ) fs; + processedStatic = + let processAssets' = + { path + , drvArgs + , isDrv + , staticName + , packageName + , moduleName ? "Obelisk.Generated.Static" + , mobile ? true + }@staticDrvArgs: processAssets { + src = if isDrv then (import path drvArgs) else path; + staticName = staticName; + packageName = packageName; + moduleName = moduleName; + exe = if lib.attrByPath ["userSettings" "__deprecated" "useObeliskAssetManifestGenerate"] false self + then builtins.trace "obelisk-asset-manifest-generate is deprecated. Use obelisk-asset-th-generate instead." "obelisk-asset-manifest-generate" + else "obelisk-asset-th-generate"; + } // { inherit mobile; }; + in + lib.mapAttrs + (name: staticArgs: processAssets' + (staticArgs + // { packageName = name; staticName = name; } + )) + self.userSettings.staticFiles; # The packages whose names and roles are defined by this package predefinedPackages = lib.filterAttrs (_: x: x != null) { ${self.frontendName} = nullIfAbsent (self.base + "/frontend"); @@ -291,10 +332,12 @@ in rec { }; shellPackages = {}; combinedPackages = self.predefinedPackages // self.userSettings.packages // self.shellPackages; - projectOverrides = self': super': { - ${self.staticName} = haskellLib.dontHaddock (self'.callCabal2nix self.staticName self.processedStatic.haskellManifest {}); + projectOverrides = self': super': ({ ${self.backendName} = haskellLib.addBuildDepend super'.${self.backendName} self'.obelisk-run; - }; + } // ( + # unique name | <<- unique packageName <<- unique module name, in order to depend on all of them together in one pkgset + lib.mapAttrs (pkgName: assets: self'.callCabal2nix pkgName assets.haskellManifest {}) self.processedStatic + )); totalOverrides = lib.composeExtensions self.projectOverrides self.userSettings.overrides; privateConfigDirs = ["config/backend"]; injectableConfig = builtins.filterSource (path: _: @@ -303,19 +346,19 @@ in rec { __androidWithConfig = configPath: { ${if self.userSettings.android == null then null else self.frontendName} = { executableName = "frontend"; - ${if builtins.pathExists self.userSettings.staticFiles then "assets" else null} = - nixpkgs.obeliskExecutableConfig.platforms.android.inject + "assets" = #${if builtins.pathExists self.userSettings.staticFiles.staticAssets then "assets" else null} = + nixpkgs.obeliskExecutableConfig.platforms.android.injectMany (self.injectableConfig configPath) - self.processedStatic.symlinked; + (lib.filterAttrs (name: static: static.mobile) self.processedStatic); #.staticAssets.symlinked; } // self.userSettings.android; }; __iosWithConfig = configPath: { ${if self.userSettings.ios == null then null else self.frontendName} = { executableName = "frontend"; - ${if builtins.pathExists self.userSettings.staticFiles then "staticSrc" else null} = - nixpkgs.obeliskExecutableConfig.platforms.ios.inject + "staticSrc" = + nixpkgs.obeliskExecutableConfig.platforms.ios.injectMany (self.injectableConfig configPath) - self.processedStatic.symlinked; + (lib.filterAttrs (name: static: static.mobile) self.processedStatic); } // self.userSettings.ios; }; diff --git a/dep/reflex-platform/github.json b/dep/reflex-platform/github.json index e03dc5192..6fff0e511 100644 --- a/dep/reflex-platform/github.json +++ b/dep/reflex-platform/github.json @@ -1,8 +1,8 @@ { - "owner": "reflex-frp", + "owner": "Ace-Interview-Prep", "repo": "reflex-platform", - "branch": "release/1.2.0.0", + "branch": "gs/optimize-android-builds", "private": false, - "rev": "f231e2425ac92339b8491cdd970930d63d9ad1ad", - "sha256": "0b042x423p04shhidni08f47ydgpfj0rpqhb0m6gj2lg8b3s9l8k" + "rev": "ec391db332f6533dae6fa9c209891c5880ad7743", + "sha256": "1rim6qvnlwq4k34lbk3w73zfghfdlzh7jwh5lylsk4rr16yz2af2" } diff --git a/lib/asset/manifest/src-bin/static-th.hs b/lib/asset/manifest/src-bin/static-th.hs index e04f1284b..bfcc548e8 100644 --- a/lib/asset/manifest/src-bin/static-th.hs +++ b/lib/asset/manifest/src-bin/static-th.hs @@ -8,7 +8,7 @@ import System.FilePath main :: IO () main = do --TODO: Usage - [root, haskellTarget, packageName, moduleName, fileTarget] <- getArgs + [root, haskellTarget, packageName, moduleName, fileTarget, staticAttrName] <- getArgs paths <- gatherHashedPaths root writeCabalProject haskellTarget $ SimplePkg { _simplePkg_name = T.pack packageName @@ -20,6 +20,7 @@ main = do ] , _simplePkg_moduleContents = T.pack $ unlines [ "{-# Language CPP #-}" + , "{-# Language PackageImports #-}" , "{-|" , " Description:" , " Automatically generated module that provides the 'static' TH function" @@ -28,15 +29,15 @@ main = do , "module " <> moduleName <> " ( static, staticFilePath ) where" , "" , "import Obelisk.Asset.TH" - , "import Language.Haskell.TH" + , "import \"template-haskell\" Language.Haskell.TH" , "" , "static, staticFilePath :: FilePath -> Q Exp" , "#ifdef OBELISK_ASSET_PASSTHRU" - , "static = staticAssetRaw" - , "staticFilePath = staticAssetFilePathRaw \"static.out\"" + , "static = staticAssetRaw" <> " " <> show staticAttrName + , "staticFilePath = staticAssetFilePathRaw \"static.out\"" <> " " <> show staticAttrName , "#else" - , "static = staticAssetHashed " <> show root - , "staticFilePath = staticAssetFilePath " <> show root + , "static = staticAssetHashed " <> show root <> " " <> show staticAttrName + , "staticFilePath = staticAssetFilePath " <> show root , "#endif" ] } diff --git a/lib/asset/manifest/src/Obelisk/Asset/TH.hs b/lib/asset/manifest/src/Obelisk/Asset/TH.hs index 9de64a3b2..faaeeb9ca 100644 --- a/lib/asset/manifest/src/Obelisk/Asset/TH.hs +++ b/lib/asset/manifest/src/Obelisk/Asset/TH.hs @@ -41,12 +41,14 @@ staticOutPath = "static.out" -- -- If the filepath can not be found in the static output directory, -- this will throw a compile-time error. -staticAssetRaw :: FilePath -> Q Exp -staticAssetRaw = staticAssetWorker staticPrefix staticOutPath +staticAssetRaw :: FilePath -> FilePath -> Q Exp +staticAssetRaw staticName fp = staticAssetWorker staticPrefix staticOutPath staticName fp -staticAssetHashed :: FilePath -> FilePath -> Q Exp -staticAssetHashed root fp = do - LitE . StringL . (staticPrefix ) <$> hashedAssetFilePath root fp +-- | Generate URL to reference handler which will serve this asset path +-- | where "/static" route maps to the base directory static.assets +staticAssetHashed :: FilePath -> FilePath -> FilePath -> Q Exp +staticAssetHashed root staticPkgName fp = do + LitE . StringL . (\p -> staticPrefix staticPkgName p) <$> hashedAssetFilePath root fp -- | Embed a filepath via template haskell. Differently to 'staticAssetRaw' -- this points to a local filepath instead of an URL during deployment. @@ -57,10 +59,13 @@ staticAssetFilePathRaw :: FilePath -- ^ Add this prefix directory to the embedded filepath @fp@. -> FilePath + -- ^ Static Project specific prefix to avoid collisions + -> FilePath -- ^ Filepath you want to embed. -> Q Exp -staticAssetFilePathRaw root = staticAssetWorker root staticOutPath +staticAssetFilePathRaw root staticName = staticAssetWorker root staticOutPath staticName +-- | Return the real location of 'relativePath' staticAssetFilePath :: FilePath -> FilePath -> Q Exp staticAssetFilePath root relativePath = do let fullPath = root relativePath @@ -77,13 +82,16 @@ staticAssetWorker :: FilePath -- ^ Add this prefix directory to the embedded filepath @fp@. -> FilePath - -- ^ Directory to which the filepath must have been copied. + -- ^ Base Directory to which the filepath must have been copied. + -- If @fp@ does not exist within this directory, this function will fail. + -> FilePath + -- ^ Static Project Directory to which the filepath must have been copied. -- If @fp@ does not exist within this directory, this function will fail. -> FilePath -- ^ Filepath you want to embed. -> Q Exp -staticAssetWorker root staticOut fp = do - exists <- runIO $ doesFileExist $ staticOut fp +staticAssetWorker root staticOut staticName fp = do + exists <- runIO $ doesFileExist $ staticOut staticName fp when (not exists) $ - fail $ "The file " <> fp <> " was not found in " <> staticOut - return $ LitE $ StringL $ root fp + fail $ "The file " <> fp <> " was not found in " <> staticOut staticName + return $ LitE $ StringL $ root staticName fp diff --git a/lib/command/src/Obelisk/Command/Project.hs b/lib/command/src/Obelisk/Command/Project.hs index 24ece8fc7..f2eb7d3e5 100644 --- a/lib/command/src/Obelisk/Command/Project.hs +++ b/lib/command/src/Obelisk/Command/Project.hs @@ -22,10 +22,12 @@ module Obelisk.Command.Project , withProjectRoot , bashEscape , shEscape - , getHaskellManifestProjectPath + , getStaticHaskellManifestProjectPaths , AssetSource(..) + , StaticInfo(..) , describeImpureAssetSource , watchStaticFilesDerivation + , staticOut ) where import Control.Concurrent.MVar (MVar, newMVar, withMVarMasked) @@ -42,8 +44,9 @@ import qualified Data.ByteString.Lazy as BSL import Data.Default (def) import qualified Data.Foldable as F (toList) import Data.Function ((&), on) -import Data.Map (Map) +import qualified Data.Map as Map (Map, toList, fromList) import qualified Data.Set as Set +import qualified Data.List as List import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeUtf8, encodeUtf8) @@ -76,6 +79,15 @@ import Obelisk.Command.Utils (nixBuildExePath, nixExePath, toNixPath, cp, nixShe --TODO: Make this module resilient to random exceptions + +staticOut :: FilePath +staticOut = "static.out" + +-- | Common constant used for symlinked static drv +dotOut :: FilePath +dotOut = ".out" + + --TODO: Don't hardcode this -- | Source for the Obelisk project obeliskSource :: ThunkSource @@ -340,7 +352,7 @@ mkObNixShellProc => FilePath -- ^ Path to project root -> Bool -- ^ Should this be a pure shell? -> Bool -- ^ Should we chdir to the package root in the shell? - -> Map Text FilePath -- ^ Package names mapped to their paths + -> Map.Map Text FilePath -- ^ Package names mapped to their paths -> String -- ^ Shell attribute to use (e.g. @"ghc"@, @"ghcjs"@, etc.) -> Maybe String -- ^ If 'Just' run the given command; otherwise just open the interactive shell -> m ProcessSpec @@ -364,7 +376,7 @@ nixShellWithoutPkgs => FilePath -- ^ Path to project root -> Bool -- ^ Should this be a pure shell? -> Bool -- ^ Should we chdir to the package root in the shell? - -> Map Text FilePath -- ^ Package names mapped to their paths + -> Map.Map Text FilePath -- ^ Package names mapped to their paths -> String -- ^ Shell attribute to use (e.g. @"ghc"@, @"ghcjs"@, etc.) -> Maybe String -- ^ If 'Just' run the given command; otherwise just open the interactive shell -> m () @@ -387,6 +399,23 @@ data AssetSource = AssetSource_Derivation | AssetSource_Files deriving (Eq) +data StaticInfo = StaticInfo + { _staticInfo_name :: T.Text + , _staticInfo_assetSource :: AssetSource + , _staticInfo_path :: FilePath + } + +instance Json.FromJSON StaticInfo where + parseJSON = Json.withObject "StaticInfo" $ \o -> do + name <- o Json..: "staticName" + drvBool <- o Json..: "isDrv" + path <- o Json..: "staticPath" + return $ StaticInfo + { _staticInfo_name = name + , _staticInfo_assetSource = if drvBool then AssetSource_Derivation else AssetSource_Files + , _staticInfo_path = path + } + -- | Some log messages to make it easier to tell where static files are coming from describeImpureAssetSource :: AssetSource -> Text -> Text describeImpureAssetSource src path = case src of @@ -395,40 +424,42 @@ describeImpureAssetSource src path = case src of -- | Determine where the static files of a project are and whether they're plain files or a derivation. -- If they are a derivation, that derivation will be built. -findProjectAssets :: MonadObelisk m => FilePath -> m (AssetSource, Text) +findProjectAssets :: MonadObelisk m => FilePath -> m [StaticInfo] findProjectAssets root = do isDerivation <- readProcessAndLogStderr Debug $ setCwd (Just root) $ proc nixExePath [ "eval" , "--impure" , "--expr" - , "(let a = import ./. {}; in toString (a.reflex.nixpkgs.lib.isDerivation a.passthru.staticFilesImpure))" - , "--raw" - -- `--raw` is not available with old nix-instantiate. It drops quotation - -- marks and trailing newline, so is very convenient for shelling out. + , "(let a = import ./. {}; in builtins.attrValues (builtins.mapAttrs (n: value: {staticName=n;isDrv = a.reflex.nixpkgs.lib.isDerivation value.src; staticPath = value.path;} ) a.passthru.staticFilesImpure))" + , "--json" ] - -- Check whether the impure static files are a derivation (and so must be built) - if isDerivation == "1" - then do - _ <- buildStaticFilesDerivationAndSymlink - (readProcessAndLogStderr Debug) - root - pure (AssetSource_Derivation, T.pack $ root "static.out") - else fmap (AssetSource_Files,) $ do - path <- readProcessAndLogStderr Debug $ setCwd (Just root) $ - proc nixExePath ["eval", "-f", ".", "passthru.staticFilesImpure", "--raw"] - _ <- readProcessAndLogStderr Debug $ setCwd (Just root) $ - proc lnPath ["-sfT", T.unpack path, "./static.out"] - pure path + case Json.eitherDecode . BSL.fromStrict . encodeUtf8 $ isDerivation of + Left _ -> fail "Unable to get StaticInfo" + Right (statics :: [StaticInfo]) -> do + liftIO $ createDirectoryIfMissing False staticOut + -- Build symlink path at static.out/ + forM_ statics $ \staticInfo -> do + if _staticInfo_assetSource staticInfo == AssetSource_Derivation + then do + void $ buildStaticFilesDerivationAndSymlink + (readProcessAndLogStderr Debug) + root + (_staticInfo_name staticInfo) + else do + void $ readProcessAndLogStderr Debug $ setCwd (Just root) $ + proc lnPath ["-sfT", (_staticInfo_path staticInfo), staticOut T.unpack (_staticInfo_name staticInfo)] + pure statics -- | Get the nix store path to the generated static asset manifest module (e.g., "obelisk-generated-static") -getHaskellManifestProjectPath :: MonadObelisk m => FilePath -> m Text -getHaskellManifestProjectPath root = fmap T.strip $ readProcessAndLogStderr Debug $ setCwd (Just root) $ - proc nixBuildExePath +getStaticHaskellManifestProjectPaths :: MonadObelisk m => FilePath -> m [Text] +getStaticHaskellManifestProjectPaths root = do + stdout <- readProcessAndLogStderr Debug $ setCwd (Just root) $ proc nixBuildExePath [ "--no-out-link" , "-E" - , "(let a = import ./. {}; in a.passthru.processedStatic.haskellManifest)" + , "(let a = import ./. {}; in builtins.mapAttrs (_: x: x.haskellManifest) a.passthru.processedStatic)" ] + pure $ T.strip <$> T.lines stdout -- | Watch the common, backend, frontend, and static directories for file -- changes and check whether those file changes cause changes in the static @@ -436,8 +467,11 @@ getHaskellManifestProjectPath root = fmap T.strip $ readProcessAndLogStderr Debu watchStaticFilesDerivation :: (MonadIO m, MonadObelisk m) => FilePath + -- ^ root folder + -> [StaticInfo] + -- ^ which static folder are we watching -> m () -watchStaticFilesDerivation root = do +watchStaticFilesDerivation root statics = do ob <- getObelisk liftIO $ runHeadlessApp $ do pb <- getPostBuild @@ -467,17 +501,18 @@ watchStaticFilesDerivation root = do else WatchModeOS } watch' pkg = fmap (:[]) <$> watchDirectoryTree cfg (root pkg <$ pb) (filterEvents . eventPath) + -- TODO: similar to previous todo, we should check if frontend and backend depend on this particular static package + -- eg. if this static package is only needed by backend rebuild <- batchOccurrences 0.25 =<< mergeWith (<>) <$> mapM watch' - [ "frontend" + ([ "frontend" , "backend" , "common" - , "static" - ] + ] <> (_staticInfo_path <$> statics)) performEvent_ $ liftIO . runObelisk ob . putLog Debug - . ("Regenerating static.out due to file changes: "<>) + . (("Regenerating static due to file changes: ") <>) . T.intercalate ", " . Set.toList . Set.fromList @@ -488,43 +523,48 @@ watchStaticFilesDerivation root = do void $ flip throttleBatchWithLag rebuild $ \e -> performEvent $ ffor e $ \_ -> liftIO $ runObelisk ob $ do putLog Notice "Static assets being built..." - buildStaticCatchErrors >>= \case + sequenceA <$> traverse buildStaticCatchErrors (_staticInfo_name <$> statics) >>= \case Nothing -> pure () - Just n -> do - putLog Notice $ "Static assets built and symlinked to static.out" - putLog Debug $ "Generated static asset nix path: " <> n + Just ns -> forM_ ns $ \n -> do + putLog Notice $ "Static assets built and symlinked to " + <> (maybe "static.out" ((<>) ".out" . _staticInfo_name) $ (List.find ((== drvNameFromPath n) . _staticInfo_name) statics)) + putLog Debug $ "Generated static asset nix path: " <> T.pack n pure never where + drvNameFromPath = T.pack . drop 1 . dropWhile (/= '-') . last . splitDirectories handleBuildFailure :: MonadObelisk m => (ExitCode, String, String) - -> m (Maybe Text) + -> m (Maybe FilePath) handleBuildFailure (ex, out, err) = case ex of ExitSuccess -> let out' = T.strip $ T.pack out - in pure $ if T.null out' then Nothing else Just out' + in pure $ if T.null out' then Nothing else Just $ T.unpack out' _ -> do putLog Error $ ("Static assets build failed: " <>) $ T.unlines $ reverse $ take 20 $ reverse $ T.lines $ T.pack err pure Nothing - buildStaticCatchErrors :: MonadObelisk m => m (Maybe Text) - buildStaticCatchErrors = handleBuildFailure =<< + --buildStaticsCatchErrors staticAttrs = traverse buildStaticCatchErrors staticAttrs + buildStaticCatchErrors :: MonadObelisk m => Text -> m (Maybe FilePath) + buildStaticCatchErrors staticA = handleBuildFailure =<< buildStaticFilesDerivationAndSymlink readCreateProcessWithExitCode root + staticA buildStaticFilesDerivationAndSymlink :: MonadObelisk m => (ProcessSpec -> m a) -> FilePath + -> Text -> m a -buildStaticFilesDerivationAndSymlink f root = f $ +buildStaticFilesDerivationAndSymlink f root staticName = f $ setCwd (Just root) $ ProcessSpec { _processSpec_createProcess = Proc.proc nixBuildExePath - [ "-o", "static.out" - , "-E", "(import ./. {}).passthru.staticFilesImpure" + [ "-o", staticOut T.unpack staticName + , "-E", "(import ./. {}).passthru.staticFilesImpure." <> T.unpack staticName <> ".src" ] , _processSpec_overrideEnv = Nothing } diff --git a/lib/command/src/Obelisk/Command/Run.hs b/lib/command/src/Obelisk/Command/Run.hs index 4204e0cbe..8b72e42ad 100644 --- a/lib/command/src/Obelisk/Command/Run.hs +++ b/lib/command/src/Obelisk/Command/Run.hs @@ -17,7 +17,7 @@ import Control.Arrow ((&&&)) import Control.Exception (Exception, bracket) import Control.Lens (ifor, (.~), (&), view) import Control.Concurrent (forkIO) -import Control.Monad (filterM, void) +import Control.Monad (filterM, forM, void) import Control.Monad.Except (runExceptT, throwError) import Control.Monad.IO.Class (liftIO) import Control.Monad.Reader (MonadIO) @@ -168,8 +168,8 @@ profile profileBasePattern rtsFlags = withProjectRoot "." $ \root -> do , _target_attr = Just "__unstable__.profiledObRun" , _target_expr = Nothing } - (assetType, assets) <- findProjectAssets root - putLog Debug $ describeImpureAssetSource assetType assets + staticInfos <- findProjectAssets root + forM staticInfos $ \info -> putLog Debug $ describeImpureAssetSource (_staticInfo_assetSource info) (T.pack $ _staticInfo_path info) time <- liftIO getCurrentTime let profileBaseName = formatTime defaultTimeLocale profileBasePattern time liftIO $ createDirectoryIfMissing True $ takeDirectory $ root profileBaseName @@ -177,7 +177,7 @@ profile profileBasePattern rtsFlags = withProjectRoot "." $ \root -> do freePort <- getFreePort runProcess_ $ setCwd (Just root) $ setDelegateCtlc True $ proc (outPath "bin" "ob-run") $ [ show freePort - , T.unpack assets + , staticOut , profileBaseName , "+RTS" , "-po" <> profileBaseName @@ -197,23 +197,23 @@ run -> m () run certDir portOverride root interpretPaths = do pkgs <- getParsedLocalPkgs root interpretPaths - (assetType, assets) <- findProjectAssets root - manifestPkg <- parsePackagesOrFail . (:[]) . T.unpack =<< getHaskellManifestProjectPath root - putLog Debug $ describeImpureAssetSource assetType assets - case assetType of - AssetSource_Derivation -> do + staticInfos <- findProjectAssets root + manifestPkgs <- parsePackagesOrFail . fmap T.unpack =<< getStaticHaskellManifestProjectPaths root + forM staticInfos $ \info -> putLog Debug $ describeImpureAssetSource (_staticInfo_assetSource info) (T.pack $ _staticInfo_path info) + case filter ((==AssetSource_Derivation) . _staticInfo_assetSource) staticInfos of + [] -> pure () + paths@(_:_) -> do ob <- getObelisk putLog Debug "Starting static file derivation watcher..." - void $ liftIO $ forkIO $ runObelisk ob $ watchStaticFilesDerivation root - _ -> pure () - ghciArgs <- getGhciSessionSettings (pkgs <> manifestPkg) root + void $ liftIO $ forkIO $ runObelisk ob $ watchStaticFilesDerivation root paths + ghciArgs <- getGhciSessionSettings (pkgs <> manifestPkgs) root freePort <- getFreePort withGhciScriptArgs [] pkgs $ \dotGhciArgs -> do runGhcid root True (ghciArgs <> dotGhciArgs) pkgs $ Just $ unwords [ "Obelisk.Run.run (Obelisk.Run.defaultRunApp" , "Backend.backend" , "Frontend.frontend" - , "(Obelisk.Run.runServeAsset " ++ show assets ++ ")" + , "(Obelisk.Run.runServeAsset " ++ (show $ root staticOut) ++ ")" , ") { Obelisk.Run._runApp_backendPort =", show freePort , ", Obelisk.Run._runApp_forceFrontendPort =", show portOverride , ", Obelisk.Run._runApp_tlsCertDirectory =", show certDir diff --git a/lib/executable-config/default.nix b/lib/executable-config/default.nix index c6425e106..d6856bd4d 100644 --- a/lib/executable-config/default.nix +++ b/lib/executable-config/default.nix @@ -8,11 +8,74 @@ }: let + setup = '' + set -x + mkdir -p $out + mkdir -p $out/static + ''; + createConfig = config: lib.optionalString (!(builtins.isNull config)) '' + if ! mkdir $out/config; then + 2>&1 echo config directory already exists or could not be created + exit 1 + fi + cp -a "${config}"/* "$out/config" + # Needed for android deployments + find "$out/config" -type f -printf '%P\0' > "$out/config.files" + ''; + # Walk recursively, look for hashed files, remove the plain originals + removeNonHashed = '' + assets_root="$out/static/" + find "$assets_root" -type f -regextype posix-extended -regex '.*/[A-Za-z0-9]{16,}-[^/]+$' | while read -r hashed; do + dir=$(dirname "$hashed") + base=$(basename "$hashed") + orig="''${base#*-}" + orig_path="$dir/$orig" + if [ -f "$orig_path" ]; then + echo "Removing duplicate original: $orig_path" + rm -f -- "$orig_path" + fi + done + ''; + mkStaticDir = name: assets: '' + cp --no-preserve=mode -Lr "${assets}" $out/static/${name} + ''; + #mkStaticDirs = + injectMany = config: processedStatic: + let staticDirs = lib.mapAttrsToList (name: assetDrv: mkStaticDir name assetDrv.symlinked) processedStatic; + in + runCommand "inject-config" {} ( + '''' + + setup + + (lib.concatStrings staticDirs) + + removeNonHashed + + createConfig config + + '''' + ); + + + #lib.mapAttrs (name: assetDrv: mkStaticDir ) processedStatic + + #map (injectConfig config) assetsMany injectConfig = config: assets: runCommand "inject-config" {} ('' set -x mkdir -p $out - cp --no-preserve=mode -Lr "${assets}" $out/static + mkdir -p $out/static + cp --no-preserve=mode -Lr "${assets}" $out/static/staticAssets chmod +w "$out" + + assets_root="$out/static/staticAssets" + # Walk recursively, look for hashed files, remove the plain originals + find "$assets_root" -type f -regextype posix-extended -regex '.*/[A-Za-z0-9]{16,}-[^/]+$' | while read -r hashed; do + dir=$(dirname "$hashed") + base=$(basename "$hashed") + orig="''${base#*-}" + orig_path="$dir/$orig" + if [ -f "$orig_path" ]; then + echo "Removing duplicate original: $orig_path" + rm -f -- "$orig_path" + fi + done + '' + lib.optionalString (!(builtins.isNull config)) '' if ! mkdir $out/config; then 2>&1 echo config directory already exists or could not be created @@ -43,10 +106,12 @@ in platforms = { android = { # Inject the given config directory into an android assets folder + injectMany = injectMany; inject = injectConfig; }; ios = { # Inject the given config directory into an iOS app + injectMany = injectMany; inject = injectConfig; }; web = { diff --git a/lib/frontend/src/Obelisk/Frontend.hs b/lib/frontend/src/Obelisk/Frontend.hs index 01b5920ed..2a132709c 100644 --- a/lib/frontend/src/Obelisk/Frontend.hs +++ b/lib/frontend/src/Obelisk/Frontend.hs @@ -25,7 +25,6 @@ module Obelisk.Frontend , module Obelisk.Frontend.Cookie ) where - #ifdef __GLASGOW_HASKELL__ #if __GLASGOW_HASKELL__ < 810 import Data.Monoid ((<>)) @@ -50,6 +49,7 @@ import Data.ByteString (ByteString) import Data.Foldable (for_) import Data.Map (Map) import Data.Maybe (catMaybes) +import qualified Data.Text as T import Data.Text (Text) import qualified GHCJS.DOM as DOM import qualified GHCJS.DOM.Types as DOM @@ -140,7 +140,7 @@ setInitialRoute useHash = do initialUri <- getLocationUri initialLocation history <- DOM.getHistory window DOM.replaceState history jsNull ("" :: Text) $ Just $ - show $ setAdaptedUriPath useHash "/" initialUri + show $ setAdaptedUriPath useHash "/login" initialUri data FrontendMode = FrontendMode { _frontendMode_hydrate :: Bool @@ -185,16 +185,29 @@ runFrontend validFullEncoder frontend = do runFrontendWithConfigsAndCurrentRoute :: forall backendRoute frontendRoute +-- . (GShow backendRoute, GShow frontendRoute) . FrontendMode -> Map Text ByteString -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName -> Frontend (R frontendRoute) -> JSM () runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = do - let ve = validFullEncoder . hoistParse errorLeft (reviewEncoder (rPrism $ _FullRoute_Frontend . _ObeliskRoute_App)) - errorLeft = \case - Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen." - Right x -> Identity x + let ---ve :: _ + ve = validFullEncoder . hoistParse errorLeft (reviewEncoder' (rPrism $ _FullRoute_Frontend . _ObeliskRoute_App)) + errorLeft + :: forall t. + Either (R (FullRoute backendRoute frontendRoute)) t + -> Identity t + errorLeft = + Identity . either + (\e -> error $ + "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. " + <> "This shouldn't happen. with route " + <> T.unpack (renderObeliskRoute validFullEncoder e)) + Prelude.id + -- errorLeft = \case + -- Left (e) -> error $ "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen. with route" <> (T.unpack $ renderObeliskRoute validFullEncoder e) + -- Right x -> Identity x w :: ( RawDocument (DomBuilderSpace (HydrationDomBuilderT s DomTimeline m)) ~ DOM.Document , Ref (Performable m) ~ Ref IO , Ref m ~ Ref IO diff --git a/lib/route/src/Obelisk/Route.hs b/lib/route/src/Obelisk/Route.hs index 6f6f6e8b4..2f59e2c69 100644 --- a/lib/route/src/Obelisk/Route.hs +++ b/lib/route/src/Obelisk/Route.hs @@ -110,6 +110,7 @@ module Obelisk.Route , shadowEncoder , prismEncoder , reviewEncoder + , reviewEncoder' , obeliskRouteEncoder , obeliskRouteSegment , pageNameEncoder @@ -925,7 +926,15 @@ reviewEncoder p = unsafeMkEncoder $ EncoderImpl { _encoderImpl_encode = review p , _encoderImpl_decode = \r -> case r ^? p of Just a -> pure a - Nothing -> throwError "reviewEncoder: value is not present in the prism" + Nothing -> throwError $ "reviewEncoder: value is not present in the prism: " + } + +reviewEncoder' :: (Applicative check, MonadError b parse) => Prism' b a -> Encoder check parse a b +reviewEncoder' p = unsafeMkEncoder $ EncoderImpl + { _encoderImpl_encode = review p + , _encoderImpl_decode = \(r) -> case r ^? p of + Just a -> pure a + Nothing -> throwError r--- $ "reviewEncoder: value is not present in the prism: " } -- | A URL path and query string, in which trailing slashes don't matter in the path diff --git a/skeleton/default.nix b/skeleton/default.nix index adc19fea3..569f817e3 100644 --- a/skeleton/default.nix +++ b/skeleton/default.nix @@ -16,6 +16,14 @@ }: with obelisk; project ./. ({ ... }: { + staticFiles = { + obelisk-generated-static = { + path = ./static; + isDrv = false; + drvArgs = null; + moduleName = "Obelisk.Generated.Static"; + }; + }; android.applicationId = "systems.obsidian.obelisk.examples.minimal"; android.displayName = "Obelisk Minimal Example"; ios.bundleIdentifier = "systems.obsidian.obelisk.examples.minimal";