on master help sign in
Merge remote-tracking branch 'origin/qa' into use-posix-escape
commit43450eb6697163496aa2fd530a7ddf1f70d0a5b3 authorElliot Cameron <eacameron@gmail.com> dateSat 4 Jan 2020, 02:00 (7 years ago) parentd49c2a66a01a24
4 files changed+93−73
default.nix+2 lib/command/src/Obelisk/Command/Run.hs+67−62 lib/selftest/obelisk-selftest.cabal+3−2 lib/selftest/src/Obelisk/SelfTest.hs+21−9
default.nix+2
@@ -47,6 +47,8 @@ let
4747 pkgs = self.callPackage ({ pkgs }: pkgs) {};
4848 onLinux = pkg: f: if pkgs.stdenv.isLinux then f pkg else pkg;
4949 in {
50+ shelly = self.callHackage "shelly" "1.9.0" {};
51+
5052 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
5153
5254 obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (cleanSource ./lib/asset/manifest) {};
lib/command/src/Obelisk/Command/Run.hs+67−62
@@ -4,15 +4,18 @@
44{-# LANGUAGE OverloadedStrings #-}
55{-# LANGUAGE ScopedTypeVariables #-}
66{-# LANGUAGE TemplateHaskell #-}
7+{-# LANGUAGE TupleSections #-}
78module Obelisk.Command.Run where
89
9−import Control.Exception (bracket)
10+import Control.Applicative (liftA2)
11+import Control.Exception (Exception, bracket)
1012import Control.Monad
13+import Control.Monad.Except (runExceptT, throwError)
1114import Control.Monad.IO.Class (liftIO)
1215import Control.Monad.Reader (MonadIO)
13−import Data.ByteString (ByteString)
1416import qualified Data.ByteString.UTF8 as BSU
1517import Data.Either
18+import Data.Foldable (for_)
1619import qualified Data.List as L
1720import qualified Data.List.NonEmpty as NE
1821import Data.Maybe
@@ -26,9 +29,9 @@ import Distribution.Types.CondTree
2629import Distribution.Types.GenericPackageDescription
2730import Distribution.Types.Library
2831import Distribution.Utils.Generic
29−import Hpack.Config
30−import Hpack.Render
31−import Hpack.Yaml
32+import qualified Hpack.Config as Hpack
33+import qualified Hpack.Render as Hpack
34+import qualified Hpack.Yaml as Hpack
3235import Language.Haskell.Extension
3336import Network.Socket hiding (Debug)
3437import System.Directory
@@ -38,10 +41,7 @@ import System.Which (staticWhich)
3841import Text.ShellEscape (bash, bytes)
3942
4043import Obelisk.App (MonadObelisk, ObeliskT)
41−import Obelisk.CliApp
42− ( CliT (..), HasCliConfig, Severity (..)
43− , callCommand, failWith, getCliConfig, putLog, proc
44− , readProcessAndLogStderr, runCli)
44+import Obelisk.CliApp (Severity (..) , callCommand, failWith, putLog, proc, readProcessAndLogStderr)
4545import Obelisk.Command.Project (inProjectProc, withProjectRoot)
4646
4747data CabalPackageInfo = CabalPackageInfo
@@ -107,6 +107,27 @@ runWatch = do
107107 pkgs <- getLocalPkgs
108108 withGhciScript pkgs $ \dotGhciPath -> runGhcid dotGhciPath Nothing
109109
110+data GuessPackageFileError = GuessPackageFileError_Ambiguous [FilePath] | GuessPackageFileError_NotFound
111+ deriving (Eq, Ord, Show)
112+instance Exception GuessPackageFileError
113+
114+-- | Given a directory, try to guess what the appropriate @.cabal@ or @package.yaml@ file is for the package.
115+guessCabalPackageFile
116+ :: MonadIO m
117+ => FilePath -- ^ Directory to search for cabal package
118+ -> m (Either GuessPackageFileError (Either FilePath FilePath))
119+ -- ^ 'Right' 'Left' for hpack package, 'Right' 'Right' for cabal package; 'FilePath' is relative to given directory.
120+guessCabalPackageFile dir = do
121+ candidates <- liftIO $
122+ filterM (doesFileExist . (dir </>))
123+ =<< filter (liftA2 (||) (== Hpack.packageConfig) (".cabal" `L.isSuffixOf`))
124+ <$> listDirectory dir
125+ pure $ case L.partition (== Hpack.packageConfig) candidates of
126+ ([hpack], _) -> Right $ Left hpack
127+ ([], [cabal]) -> Right $ Right cabal
128+ ([], []) -> Left GuessPackageFileError_NotFound
129+ (hpacks, cabals) -> Left $ GuessPackageFileError_Ambiguous $ hpacks <> cabals
130+
110131-- | Relative paths to local packages of an obelisk project
111132-- TODO a way to query this
112133getLocalPkgs :: Applicative f => f [FilePath]
@@ -116,54 +137,38 @@ parseCabalPackage
116137 :: (MonadObelisk m)
117138 => FilePath -- ^ package directory
118139 -> m (Maybe CabalPackageInfo)
119−parseCabalPackage dir = do
120− let cabalFp = dir </> (takeBaseName dir <> ".cabal")
121− hpackFp = dir </> "package.yaml"
122− hasCabal <- liftIO $ doesFileExist cabalFp
123− hasHpack <- liftIO $ doesFileExist hpackFp
124−
125− mCabalContents <- if hasCabal
126− then Just <$> liftIO (readUTF8File cabalFp)
127− else if hasHpack
128− then do
129− let decodeOptions = DecodeOptions (ProgramName "ob") hpackFp Nothing decodeYaml
130− liftIO (readPackageConfig decodeOptions) >>= \case
131− Left err -> do
132− putLog Error $ T.pack $ "Failed to parse " <> hpackFp <> ": " <> err
133− return Nothing
134− Right (DecodeResult hpackPackage _ _ _) -> do
135− return $ Just $ renderPackage [] hpackPackage
136− else do
137− putLog Error $ T.pack "Found neither cabal nor hpack file"
138− return Nothing
139−
140− fmap join $ forM mCabalContents $ \cabalContents -> do
141− let (warnings, result) = runParseResult $ parseGenericPackageDescription $
142− toUTF8BS cabalContents
143− mapM_ (putLog Warning) $ fmap (T.pack . show) warnings
144− case result of
145− Right gpkg -> do
146− return $ do
147− (_, lib) <- simplifyCondTree (const $ pure True) <$> condLibrary gpkg
148− pure $ CabalPackageInfo
149− { _cabalPackageInfo_packageRoot = takeDirectory cabalFp
150− , _cabalPackageInfo_sourceDirs =
151− fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
152− , _cabalPackageInfo_defaultExtensions =
153− defaultExtensions $ libBuildInfo lib
154− , _cabalPackageInfo_defaultLanguage =
155− defaultLanguage $ libBuildInfo lib
156− , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
157− }
158− Left (_, errors) -> do
159− putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
160− mapM_ (putLog Error) $ fmap (T.pack . show) errors
161− return Nothing
140+parseCabalPackage dir = either ((Nothing <$) . putLog Error) (pure . Just) <=< runExceptT $ do
141+ (cabalContents, packageFile) <- guessCabalPackageFile dir >>= \case
142+ Left GuessPackageFileError_NotFound -> throwError $ "No .cabal or package.yaml file found in " <> T.pack dir
143+ Left (GuessPackageFileError_Ambiguous _) -> throwError $ "Unable to determine which .cabal file to use in " <> T.pack dir
144+ Right (Right cabalFileName) -> let file = dir </> cabalFileName in (, file) <$> liftIO (readUTF8File file)
145+ Right (Left hpackFileName) -> do
146+ let
147+ file = dir </> hpackFileName
148+ decodeOptions = Hpack.DecodeOptions (Hpack.ProgramName "ob") file Nothing Hpack.decodeYaml
149+ liftIO (Hpack.readPackageConfig decodeOptions) >>= \case
150+ Left err -> throwError $ T.pack $ "Failed to parse " <> file <> ": " <> err
151+ Right (Hpack.DecodeResult hpackPackage _ _ _) -> pure (Hpack.renderPackage [] hpackPackage, file)
162152
163−withUTF8FileContentsM :: (MonadIO m, HasCliConfig e m) => FilePath -> (ByteString -> CliT e IO a) -> m a
164−withUTF8FileContentsM fp f = do
165− c <- getCliConfig
166− liftIO $ withUTF8FileContents fp $ runCli c . f . toUTF8BS
153+ let (warnings, result) = runParseResult $ parseGenericPackageDescription $
154+ toUTF8BS cabalContents
155+ for_ warnings $ putLog Warning . T.pack . show
156+ case condLibrary <$> result of
157+ Right (Just condLib) -> do
158+ let (_, lib) = simplifyCondTree (const $ pure True) condLib
159+ pure $ CabalPackageInfo
160+ { _cabalPackageInfo_packageRoot = dir
161+ , _cabalPackageInfo_sourceDirs =
162+ fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
163+ , _cabalPackageInfo_defaultExtensions =
164+ defaultExtensions $ libBuildInfo lib
165+ , _cabalPackageInfo_defaultLanguage =
166+ defaultLanguage $ libBuildInfo lib
167+ , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
168+ }
169+ Right Nothing -> throwError "Haskell package has no library component"
170+ Left (_, errors) ->
171+ throwError $ T.pack $ "Failed to parse " <> packageFile <> ":\n" <> unlines (map show errors)
167172
168173-- | Create ghci configuration to load the given packages
169174withGhciScript
@@ -171,14 +176,14 @@ withGhciScript
171176 => [FilePath] -- ^ List of packages to load into ghci
172177 -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporary .ghci
173178 -> m ()
174−withGhciScript pkgs f = do
175− (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM pkgs $ \pkg -> do
176− flip fmap (parseCabalPackage pkg) $ \case
177− Nothing -> Left pkg
179+withGhciScript dirs f = do
180+ (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM dirs $ \dir -> do
181+ flip fmap (parseCabalPackage dir) $ \case
182+ Nothing -> Left dir
178183 Just packageInfo -> Right packageInfo
179184
180185 when (null packageInfos) $
181− failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " pkgs
186+ failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " dirs
182187 unless (null pkgDirErrs) $
183188 putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
184189
@@ -235,7 +240,7 @@ runGhcid dotGhci mcmd = callCommand $ unwords $ $(staticWhich "ghcid") : opts
235240 opts =
236241 [ "-W"
237242 --TODO: The decision of whether to use -fwarn-redundant-constraints should probably be made by the user
238− , "--command=" <> (BSU.toString $ bytes $ bash $ BSU.fromString $
243+ , "--command=" <> BSU.toString (bytes $ bash $ BSU.fromString $
239244 "ghci -Wall -ignore-dot-ghci -fwarn-redundant-constraints " <> unwords (makeBaseGhciOptions dotGhci))
240245 , "--reload=config"
241246 , "--outputfile=ghcid-output.txt"
lib/selftest/obelisk-selftest.cabal+3−2
@@ -11,16 +11,17 @@ library
1111 base
1212 , containers
1313 , directory
14+ , filepath
1415 , hspec
1516 , http-client
1617 , http-types
1718 , HUnit
19+ , network
1820 , obelisk-cliapp
1921 , obelisk-executable-config-lookup
2022 , obelisk-run
2123 , process
22− , network
23− , shelly
24+ , shelly >= 1.9.0
2425 , temporary
2526 , text
2627 exposed-modules:
lib/selftest/src/Obelisk/SelfTest.hs+21−9
@@ -25,6 +25,7 @@ import Shelly
2525import System.Directory (withCurrentDirectory, getDirectoryContents)
2626import System.Environment
2727import System.Exit (ExitCode (..))
28+import System.FilePath (replaceBaseName, takeBaseName)
2829import qualified System.Info
2930import System.IO (Handle, hClose)
3031import System.IO.Temp
@@ -95,13 +96,12 @@ main = do
9596 f obeliskImpl initCache
9697
9798 httpManager <- HTTP.newManager HTTP.defaultManagerSettings
98− [p0, p1, p2, p3] <- liftIO $ getFreePorts 4
9999
100100 withObeliskImpl $ \obeliskImpl initCache ->
101101 hspec $ parallel $ do
102102 let shelly_ = void . shellyOb verbosity
103103
104− inTmp :: (Shelly.FilePath -> Sh a) -> IO ()
104+ inTmp :: (FilePath -> Sh a) -> IO ()
105105 inTmp f = withTmp (chdir <*> f)
106106
107107 withTmp f = shelly_ . withSystemTempDirectory "test λ" $ f . fromString
@@ -149,9 +149,12 @@ main = do
149149
150150 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
151151 -- when not run by 'nix-build --attr selftest'
152− describe "ob run" $ parallel $ do
153− it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir p0 p1 Nothing httpManager
154− it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir p2 p3 (Just "frontend") httpManager
152+ describe "ob run" $ {- NOT parallel $ -} do
153+ it "works in root directory" $ inTmpObInit $ \_ -> testObRunInDir Nothing httpManager
154+ it "works in sub directory" $ inTmpObInit $ \_ -> testObRunInDir (Just "frontend") httpManager
155+ it "works with differently named cabal files" $ inTmpObInit $ \_ -> do
156+ changeCabalPackageName "backend/backend.cabal" "new-backend"
157+ testObRunInDir Nothing httpManager
155158
156159 describe "obelisk project" $ parallel $ do
157160 it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , toTextIgnore obeliskImpl]
@@ -167,7 +170,7 @@ main = do
167170 if System.Info.os == "darwin"
168171 then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
169172 else it "can build android after accepting license" $ inTmpObInit $ \dir -> do
170− let defaultNixPath = dir </> ("default.nix" :: Shelly.FilePath)
173+ let defaultNixPath = dir </> ("default.nix" :: FilePath)
171174 writefile defaultNixPath
172175 =<< T.replace
173176 "# config.android_sdk.accept_license = false;"
@@ -249,8 +252,9 @@ main = do
249252
250253
251254-- | Run `ob run` in the given directory (maximum of one level deep)
252−testObRunInDir :: Socket.PortNumber -> Socket.PortNumber -> Maybe Shelly.FilePath -> HTTP.Manager -> Sh ()
253−testObRunInDir p0 p1 mdir httpManager = handle_sh (\case ExitSuccess -> pure (); e -> throw e) $ do
255+testObRunInDir :: Maybe FilePath -> HTTP.Manager -> Sh ()
256+testObRunInDir mdir httpManager = handle_sh (\case ExitSuccess -> pure (); e -> throw e) $ do
257+ [p0, p1] <- liftIO $ getFreePorts 2
254258 let uri p = "http://localhost:" <> T.pack (show p) <> "/" -- trailing slash required for comparison
255259 writefile "config/common/route" $ uri p0
256260 maybe id chdir mdir $ runHandle "ob" ["run", "-v"] $ \stdout -> do
@@ -264,7 +268,7 @@ testObRunInDir p0 p1 mdir httpManager = handle_sh (\case ExitSuccess -> pure ();
264268 then errorExit $ "Reloading failed: expected " <> newUri <> " but got " <> runningUri
265269 else exit 0
266270
267−testThunkPack :: Shelly.FilePath -> Sh ()
271+testThunkPack :: FilePath -> Sh ()
268272testThunkPack path' = withTempFile (T.unpack $ toTextIgnore path') "test-file" $ \file handle -> do
269273 let pack' = readProcessWithExitCode "ob" ["thunk", "pack", T.unpack $ toTextIgnore path'] ""
270274 ensureThunkPackFails q = liftIO $ pack' >>= \case
@@ -330,6 +334,14 @@ obRunCheck httpManager _stdout frontendUri = do
330334 req frontendUri >>= \r -> when (HTTP.responseStatus r /= HTTP.ok200) $ errorExit $
331335 "Request to frontend server failed: " <> T.pack (show r)
332336
337+-- | Rename a cabal file and do a really dumb, brittle search/replace in its content to update the name.
338+changeCabalPackageName :: FilePath -> Text -> Sh ()
339+changeCabalPackageName cabalFile newName = do
340+ contents <- readfile cabalFile
341+ writefile (replaceBaseName cabalFile (T.unpack newName)) $
342+ T.replace (" " <> T.pack (takeBaseName cabalFile)) (" " <> newName) contents -- WARNING: Super brittle
343+ rm cabalFile
344+
333345getFreePorts :: Int -> IO [Socket.PortNumber]
334346getFreePorts 0 = pure []
335347getFreePorts n = Socket.withSocketsDo $ do