1{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE RecordWildCards #-}
3{-# LANGUAGE TupleSections #-}
4{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6module Language.PureScript.Publish
16 , defaultPublishOptions
17 , getGitWorkingTreeStatus
18 , checkCleanWorkingTree
19 , getVersionFromGitTag
21 , getModulesAndBookmarks
22 , getResolvedDependencies
26import Prelude.Compat hiding (userError)
29import Data.Char (isSpace)
30import Data.List (stripPrefix, isSuffixOf, (\\), nubBy)
31import Data.List.Split (splitOn)
32import Data.List.NonEmpty (NonEmpty(..))
34import Data.Function (on)
35import Data.Foldable (traverse_)
37import Data.Aeson.BetterErrors
38import qualified Data.Text as T
39import qualified Data.Text.Lazy as TL
40import qualified Data.Text.Lazy.Encoding as TL
42import Control.Category ((>>>))
43import Control.Arrow ((***))
44import Control.Exception (catch, try)
45import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
46import Control.Monad.Trans.Except
47import Control.Monad.Error.Class (MonadError(..))
48import Control.Monad.Writer.Strict
50import System.Directory (doesFileExist, findExecutable)
51import System.Process (readProcess)
52import System.Exit (exitFailure)
53import System.FilePath (pathSeparator)
54import qualified System.FilePath.Glob as Glob
55import qualified System.Info
57import Web.Bower.PackageMeta (PackageMeta(..), BowerError(..), PackageName,
58 runPackageName, parsePackageName, Repository(..))
59import qualified Web.Bower.PackageMeta as Bower
61import qualified Language.PureScript as P (version)
62import qualified Language.PureScript.Docs as D
63import Language.PureScript.Publish.Utils
64import Language.PureScript.Publish.ErrorsWarnings
66data PublishOptions = PublishOptions
67 { -- | How to obtain the version tag and version that the data being
68 -- generated will refer to.
69 publishGetVersion :: PrepareM (String, Version)
70 , -- | What to do when the working tree is dirty
71 publishWorkingTreeDirty :: PrepareM ()
74defaultPublishOptions :: PublishOptions
75defaultPublishOptions = PublishOptions
76 { publishGetVersion = getVersionFromGitTag
77 , publishWorkingTreeDirty = userError DirtyWorkingTree
80-- | Attempt to retrieve package metadata from the current directory.
81-- Calls exitFailure if no package metadata could be retrieved.
82preparePackage :: PublishOptions -> IO D.UploadedPackage
84 runPrepareM (preparePackage' opts)
85 >>= either (\e -> printError e >> exitFailure)
88 handleWarnings (result, warns) = do
93 PrepareM { unPrepareM :: WriterT [PackageWarning] (ExceptT PackageError IO) a }
94 deriving (Functor, Applicative, Monad,
95 MonadWriter [PackageWarning],
96 MonadError PackageError)
98-- This MonadIO instance ensures that IO errors don't crash the program.
99instance MonadIO PrepareM where
101 lift' (try act) >>= either (otherError . IOExceptionThrown) return
103 lift' :: IO a -> PrepareM a
104 lift' = PrepareM . lift . lift
106runPrepareM :: PrepareM a -> IO (Either PackageError (a, [PackageWarning]))
107runPrepareM = runExceptT . runWriterT . unPrepareM
109warn :: PackageWarning -> PrepareM ()
112userError :: UserError -> PrepareM a
113userError = throwError . UserError
115internalError :: InternalError -> PrepareM a
116internalError = throwError . InternalError
118otherError :: OtherError -> PrepareM a
119otherError = throwError . OtherError
121catchLeft :: Applicative f => Either a b -> (a -> f b) -> f b
122catchLeft a f = either f pure a
124preparePackage' :: PublishOptions -> PrepareM D.UploadedPackage
125preparePackage' opts = do
126 exists <- liftIO (doesFileExist "bower.json")
127 unless exists (userError BowerJSONNotFound)
129 checkCleanWorkingTree opts
131 pkgMeta <- liftIO (Bower.decodeFile "bower.json")
132 >>= flip catchLeft (userError . CouldntDecodeBowerJSON)
133 (pkgVersionTag, pkgVersion) <- publishGetVersion opts
134 pkgGithub <- getBowerInfo pkgMeta
135 (pkgBookmarks, pkgModules) <- getModulesAndBookmarks
137 let declaredDeps = map fst (bowerDependencies pkgMeta ++
138 bowerDevDependencies pkgMeta)
139 pkgResolvedDependencies <- getResolvedDependencies declaredDeps
141 let pkgUploader = D.NotYetKnown
142 let pkgCompilerVersion = P.version
146getModulesAndBookmarks :: PrepareM ([D.Bookmark], [D.Module])
147getModulesAndBookmarks = do
148 (inputFiles, depsFiles) <- liftIO getInputAndDepsFiles
149 liftIO (D.parseAndDesugar inputFiles depsFiles renderModules)
150 >>= either (userError . ParseAndDesugarError) return
152 renderModules bookmarks modules =
153 return (bookmarks, map D.convertModule modules)
155data TreeStatus = Clean | Dirty deriving (Show, Read, Eq, Ord, Enum)
157getGitWorkingTreeStatus :: PrepareM TreeStatus
158getGitWorkingTreeStatus = do
159 out <- readProcess' "git" ["status", "--porcelain"] ""
161 if all null . lines $ out
165checkCleanWorkingTree :: PublishOptions -> PrepareM ()
166checkCleanWorkingTree opts = do
167 status <- getGitWorkingTreeStatus
168 unless (status == Clean) $
169 publishWorkingTreeDirty opts
171getVersionFromGitTag :: PrepareM (String, Version)
172getVersionFromGitTag = do
173 out <- readProcess' "git" ["tag", "--list", "--points-at", "HEAD"] ""
174 let vs = map trimWhitespace (lines out)
175 case mapMaybe parseMay vs of
176 [] -> userError TagMustBeCheckedOut
178 xs -> userError (AmbiguousVersions (map snd xs))
181 dropWhile isSpace >>> reverse >>> dropWhile isSpace >>> reverse
183 (str,) <$> D.parseVersion' (dropPrefix "v" str)
184 dropPrefix prefix str =
185 fromMaybe str (stripPrefix prefix str)
187getBowerInfo :: PackageMeta -> PrepareM (D.GithubUser, D.GithubRepo)
188getBowerInfo = either (userError . BadRepositoryField) return . tryExtract
191 case bowerRepository pkgMeta of
192 Nothing -> Left RepositoryFieldMissing
193 Just Repository{..} -> do
194 unless (repositoryType == "git")
195 (Left (BadRepositoryType repositoryType))
196 maybe (Left NotOnGithub) Right (extractGithub repositoryUrl)
198extractGithub :: String -> Maybe (D.GithubUser, D.GithubRepo)
200 stripPrefix "git://github.com/"
201 >>> fmap (splitOn "/")
203 >>> fmap (D.GithubUser *** (D.GithubRepo . dropDotGit))
206 takeTwo :: [a] -> Maybe (a, a)
207 takeTwo [x, y] = Just (x, y)
210 dropDotGit :: String -> String
212 | ".git" `isSuffixOf` str = take (length str - 4) str
215readProcess' :: String -> [String] -> String -> PrepareM String
216readProcess' prog args stdin = do
217 out <- liftIO (catch (Right <$> readProcess prog args stdin)
219 either (otherError . ProcessFailed prog args) return out
223 -- ^ Listed in bower.json, but not installed.
225 -- ^ In the output of `bower list --json --offline`, there was no
226 -- _resolution key. This can be caused by adding the dependency using
227 -- `bower link`, or simply copying it into bower_components instead of
228 -- installing it normally.
229 | ResolvedOther String
230 -- ^ Resolved, but to something other than a version. The String argument
231 -- is the resolution type. The values it can take that I'm aware of are
232 -- "commit" and "branch".
233 | ResolvedVersion String
234 -- ^ Resolved to a version. The String argument is the resolution tag (eg,
236 deriving (Show, Read, Eq)
238-- Go through all bower dependencies which contain purescript code, and
239-- extract their versions.
241-- In the case where a bower dependency is taken from a particular version,
242-- that's easy; take that version. In any other case (eg, a branch, or a commit
243-- sha) we print a warning that documentation links will not work, and avoid
244-- linking to documentation for any types from that package.
246-- The rationale for this is: people will prefer to use a released version
247-- where possible. If they are not using a released version, then this is
248-- probably for a reason. However, docs are only ever available for released
249-- versions. Therefore there will probably be no version of the docs which is
250-- appropriate to link to, and we should omit links.
251getResolvedDependencies :: [PackageName] -> PrepareM [(PackageName, Version)]
252getResolvedDependencies declaredDeps = do
253 bower <- findBowerExecutable
254 depsBS <- packUtf8 <$> readProcess' bower ["list", "--json", "--offline"] ""
256 -- Check for undeclared dependencies
257 toplevels <- catchJSON (parse asToplevelDependencies depsBS)
258 warnUndeclared declaredDeps toplevels
260 deps <- catchJSON (parse asResolvedDependencies depsBS)
264 packUtf8 = TL.encodeUtf8 . TL.pack
265 catchJSON = flip catchLeft (internalError . JSONError FromBowerList)
267findBowerExecutable :: PrepareM String
268findBowerExecutable = do
269 mname <- liftIO . runMaybeT . msum . map (MaybeT . findExecutable) $ names
270 maybe (userError (BowerExecutableNotFound names)) return mname
272 names = case System.Info.os of
273 "mingw32" -> ["bower", "bower.cmd"]
276-- | Extracts all dependencies and their versions from
277-- `bower list --json --offline`
278asResolvedDependencies :: Parse BowerError [(PackageName, DependencyStatus)]
279asResolvedDependencies = nubBy ((==) `on` fst) <$> go
282 fmap (fromMaybe []) $
283 keyMay "dependencies" $
284 (++) <$> eachInObjectWithKey (parsePackageName . T.unpack)
286 <*> (concatMap snd <$> eachInObject asResolvedDependencies)
288-- | Extracts only the top level dependency names from the output of
289-- `bower list --json --offline`
290asToplevelDependencies :: Parse BowerError [PackageName]
291asToplevelDependencies =
294 eachInObjectWithKey (parsePackageName . T.unpack) (return ())
296asDependencyStatus :: Parse e DependencyStatus
297asDependencyStatus = do
298 isMissing <- keyOrDefault "missing" False asBool
304 keyOrDefault "_resolution" NoResolution $ do
305 type_ <- key "type" asString
307 "version" -> ResolvedVersion <$> key "tag" asString
308 other -> return (ResolvedOther other)
310warnUndeclared :: [PackageName] -> [PackageName] -> PrepareM ()
311warnUndeclared declared actual =
312 traverse_ (warn . UndeclaredDependency) (actual \\ declared)
315 [(PackageName, DependencyStatus)] -> PrepareM [(PackageName, Version)]
317 let (missing, noVersion, installed) = partitionDeps deps
320 userError (MissingDependencies (x :| xs))
322 traverse_ (warn . NoResolvedVersion) noVersion
323 withVersions <- catMaybes <$> traverse tryExtractVersion' installed
324 filterM (liftIO . isPureScript . bowerDir . fst) withVersions
327 partitionDeps = foldr go ([], [], [])
328 go (pkgName, d) (ms, os, is) =
330 Missing -> (pkgName : ms, os, is)
331 NoResolution -> (ms, pkgName : os, is)
332 ResolvedOther _ -> (ms, pkgName : os, is)
333 ResolvedVersion v -> (ms, os, (pkgName, v) : is)
335 bowerDir pkgName = "bower_components/" ++ runPackageName pkgName
337 -- Try to extract a version, and warn if unsuccessful.
338 tryExtractVersion' pair =
339 maybe (warn (UnacceptableVersion pair) >> return Nothing)
341 (tryExtractVersion pair)
343tryExtractVersion :: (PackageName, String) -> Maybe (PackageName, Version)
344tryExtractVersion (pkgName, tag) =
345 let tag' = fromMaybe tag (stripPrefix "v" tag)
346 in (pkgName,) <$> D.parseVersion' tag'
348-- | Returns whether it looks like there is a purescript package checked out
349-- in the given directory.
350isPureScript :: FilePath -> IO Bool
352 files <- Glob.globDir1 purescriptSourceFiles dir
353 return (not (null files))
355getInputAndDepsFiles :: IO ([FilePath], [(PackageName, FilePath)])
356getInputAndDepsFiles = do
357 inputFiles <- globRelative purescriptSourceFiles
358 depsFiles' <- globRelative purescriptDepsFiles
359 return (inputFiles, mapMaybe withPackageName depsFiles')
361withPackageName :: FilePath -> Maybe (PackageName, FilePath)
362withPackageName fp = (,fp) <$> getPackageName fp
364getPackageName :: FilePath -> Maybe PackageName
365getPackageName fp = do
366 let xs = splitOn [pathSeparator] fp
367 ys <- stripPrefix ["bower_components"] xs
369 case Bower.mkPackageName y of
370 Right name -> Just name
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list