on master help sign in
Merge pull request #599 from alexfmpe/fix-package-name

Allow packages named differently from the containing directory

commit6a01a243f1988785ab5bcf859a81821a095f80a5 authorElliot Cameron <3noch@users.noreply.github.com> dateSat 4 Jan 2020, 01:51 (7 years ago) parent3eb6f2cd642eed
4 files changed+92−72
default.nix+2 lib/command/src/Obelisk/Command/Run.hs+66−61 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+66−61
@@ -4,13 +4,17 @@
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)
1316import Data.Either
17+import Data.Foldable (for_)
1418import qualified Data.List as L
1519import qualified Data.List.NonEmpty as NE
1620import Data.Maybe
@@ -24,22 +28,18 @@ import Distribution.Types.CondTree
2428import Distribution.Types.GenericPackageDescription
2529import Distribution.Types.Library
2630import Distribution.Utils.Generic
27−import Hpack.Config
28−import Hpack.Render
29−import Hpack.Yaml
31+import qualified Hpack.Config as Hpack
32+import qualified Hpack.Render as Hpack
33+import qualified Hpack.Yaml as Hpack
3034import Language.Haskell.Extension
3135import Network.Socket hiding (Debug)
3236import System.Directory
3337import System.FilePath
3438import System.IO.Temp (withSystemTempDirectory)
3539import System.Which (staticWhich)
36−import Data.ByteString (ByteString)
3740
3841import Obelisk.App (MonadObelisk, ObeliskT)
39−import Obelisk.CliApp
40− ( CliT (..), HasCliConfig, Severity (..)
41− , callCommand, failWith, getCliConfig, putLog, proc
42− , readProcessAndLogStderr, runCli)
42+import Obelisk.CliApp (Severity (..) , callCommand, failWith, putLog, proc, readProcessAndLogStderr)
4343import Obelisk.Command.Project (inProjectShell, withProjectRoot)
4444
4545data CabalPackageInfo = CabalPackageInfo
@@ -105,6 +105,27 @@ runWatch = do
105105 pkgs <- getLocalPkgs
106106 withGhciScript pkgs $ \dotGhciPath -> runGhcid dotGhciPath Nothing
107107
108+data GuessPackageFileError = GuessPackageFileError_Ambiguous [FilePath] | GuessPackageFileError_NotFound
109+ deriving (Eq, Ord, Show)
110+instance Exception GuessPackageFileError
111+
112+-- | Given a directory, try to guess what the appropriate @.cabal@ or @package.yaml@ file is for the package.
113+guessCabalPackageFile
114+ :: MonadIO m
115+ => FilePath -- ^ Directory to search for cabal package
116+ -> m (Either GuessPackageFileError (Either FilePath FilePath))
117+ -- ^ 'Right' 'Left' for hpack package, 'Right' 'Right' for cabal package; 'FilePath' is relative to given directory.
118+guessCabalPackageFile dir = do
119+ candidates <- liftIO $
120+ filterM (doesFileExist . (dir </>))
121+ =<< filter (liftA2 (||) (== Hpack.packageConfig) (".cabal" `L.isSuffixOf`))
122+ <$> listDirectory dir
123+ pure $ case L.partition (== Hpack.packageConfig) candidates of
124+ ([hpack], _) -> Right $ Left hpack
125+ ([], [cabal]) -> Right $ Right cabal
126+ ([], []) -> Left GuessPackageFileError_NotFound
127+ (hpacks, cabals) -> Left $ GuessPackageFileError_Ambiguous $ hpacks <> cabals
128+
108129-- | Relative paths to local packages of an obelisk project
109130-- TODO a way to query this
110131getLocalPkgs :: Applicative f => f [FilePath]
@@ -114,54 +135,38 @@ parseCabalPackage
114135 :: (MonadObelisk m)
115136 => FilePath -- ^ package directory
116137 -> m (Maybe CabalPackageInfo)
117−parseCabalPackage dir = do
118− let cabalFp = dir </> (takeBaseName dir <> ".cabal")
119− hpackFp = dir </> "package.yaml"
120− hasCabal <- liftIO $ doesFileExist cabalFp
121− hasHpack <- liftIO $ doesFileExist hpackFp
122−
123− mCabalContents <- if hasCabal
124− then Just <$> liftIO (readUTF8File cabalFp)
125− else if hasHpack
126− then do
127− let decodeOptions = DecodeOptions (ProgramName "ob") hpackFp Nothing decodeYaml
128− liftIO (readPackageConfig decodeOptions) >>= \case
129− Left err -> do
130− putLog Error $ T.pack $ "Failed to parse " <> hpackFp <> ": " <> err
131− return Nothing
132− Right (DecodeResult hpackPackage _ _ _) -> do
133− return $ Just $ renderPackage [] hpackPackage
134− else do
135− putLog Error $ T.pack "Found neither cabal nor hpack file"
136− return Nothing
137−
138− fmap join $ forM mCabalContents $ \cabalContents -> do
139− let (warnings, result) = runParseResult $ parseGenericPackageDescription $
140− toUTF8BS cabalContents
141− mapM_ (putLog Warning) $ fmap (T.pack . show) warnings
142− case result of
143− Right gpkg -> do
144− return $ do
145− (_, lib) <- simplifyCondTree (const $ pure True) <$> condLibrary gpkg
146− pure $ CabalPackageInfo
147− { _cabalPackageInfo_packageRoot = takeDirectory cabalFp
148− , _cabalPackageInfo_sourceDirs =
149− fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
150− , _cabalPackageInfo_defaultExtensions =
151− defaultExtensions $ libBuildInfo lib
152− , _cabalPackageInfo_defaultLanguage =
153− defaultLanguage $ libBuildInfo lib
154− , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
155− }
156− Left (_, errors) -> do
157− putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
158− mapM_ (putLog Error) $ fmap (T.pack . show) errors
159− return Nothing
138+parseCabalPackage dir = either ((Nothing <$) . putLog Error) (pure . Just) <=< runExceptT $ do
139+ (cabalContents, packageFile) <- guessCabalPackageFile dir >>= \case
140+ Left GuessPackageFileError_NotFound -> throwError $ "No .cabal or package.yaml file found in " <> T.pack dir
141+ Left (GuessPackageFileError_Ambiguous _) -> throwError $ "Unable to determine which .cabal file to use in " <> T.pack dir
142+ Right (Right cabalFileName) -> let file = dir </> cabalFileName in (, file) <$> liftIO (readUTF8File file)
143+ Right (Left hpackFileName) -> do
144+ let
145+ file = dir </> hpackFileName
146+ decodeOptions = Hpack.DecodeOptions (Hpack.ProgramName "ob") file Nothing Hpack.decodeYaml
147+ liftIO (Hpack.readPackageConfig decodeOptions) >>= \case
148+ Left err -> throwError $ T.pack $ "Failed to parse " <> file <> ": " <> err
149+ Right (Hpack.DecodeResult hpackPackage _ _ _) -> pure (Hpack.renderPackage [] hpackPackage, file)
160150
161−withUTF8FileContentsM :: (MonadIO m, HasCliConfig e m) => FilePath -> (ByteString -> CliT e IO a) -> m a
162−withUTF8FileContentsM fp f = do
163− c <- getCliConfig
164− liftIO $ withUTF8FileContents fp $ runCli c . f . toUTF8BS
151+ let (warnings, result) = runParseResult $ parseGenericPackageDescription $
152+ toUTF8BS cabalContents
153+ for_ warnings $ putLog Warning . T.pack . show
154+ case condLibrary <$> result of
155+ Right (Just condLib) -> do
156+ let (_, lib) = simplifyCondTree (const $ pure True) condLib
157+ pure $ CabalPackageInfo
158+ { _cabalPackageInfo_packageRoot = dir
159+ , _cabalPackageInfo_sourceDirs =
160+ fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
161+ , _cabalPackageInfo_defaultExtensions =
162+ defaultExtensions $ libBuildInfo lib
163+ , _cabalPackageInfo_defaultLanguage =
164+ defaultLanguage $ libBuildInfo lib
165+ , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
166+ }
167+ Right Nothing -> throwError "Haskell package has no library component"
168+ Left (_, errors) ->
169+ throwError $ T.pack $ "Failed to parse " <> packageFile <> ":\n" <> unlines (map show errors)
165170
166171-- | Create ghci configuration to load the given packages
167172withGhciScript
@@ -169,14 +174,14 @@ withGhciScript
169174 => [FilePath] -- ^ List of packages to load into ghci
170175 -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporary .ghci
171176 -> m ()
172−withGhciScript pkgs f = do
173− (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM pkgs $ \pkg -> do
174− flip fmap (parseCabalPackage pkg) $ \case
175− Nothing -> Left pkg
177+withGhciScript dirs f = do
178+ (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM dirs $ \dir -> do
179+ flip fmap (parseCabalPackage dir) $ \case
180+ Nothing -> Left dir
176181 Just packageInfo -> Right packageInfo
177182
178183 when (null packageInfos) $
179− failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " pkgs
184+ failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " dirs
180185 unless (null pkgDirErrs) $
181186 putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
182187
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