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.Applicative ((<|>))
45import Control.Exception (catch, try)
46import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
47import Control.Monad.Trans.Except
48import Control.Monad.Error.Class (MonadError(..))
49import Control.Monad.Writer.Strict
51import System.Directory (doesFileExist, findExecutable)
52import System.Process (readProcess)
53import System.Exit (exitFailure)
54import System.FilePath (pathSeparator)
55import qualified System.FilePath.Glob as Glob
56import qualified System.Info
58import Web.Bower.PackageMeta (PackageMeta(..), BowerError(..), PackageName,
59 runPackageName, parsePackageName, Repository(..))
60import qualified Web.Bower.PackageMeta as Bower
62import qualified Language.PureScript as P (version)
63import qualified Language.PureScript.Docs as D
64import Language.PureScript.Publish.Utils
65import Language.PureScript.Publish.ErrorsWarnings
67data PublishOptions = PublishOptions
68 { -- | How to obtain the version tag and version that the data being
69 -- generated will refer to.
70 publishGetVersion :: PrepareM (String, Version)
71 , -- | What to do when the working tree is dirty
72 publishWorkingTreeDirty :: PrepareM ()
75defaultPublishOptions :: PublishOptions
76defaultPublishOptions = PublishOptions
77 { publishGetVersion = getVersionFromGitTag
78 , publishWorkingTreeDirty = userError DirtyWorkingTree
81-- | Attempt to retrieve package metadata from the current directory.
82-- Calls exitFailure if no package metadata could be retrieved.
83preparePackage :: PublishOptions -> IO D.UploadedPackage
85 runPrepareM (preparePackage' opts)
86 >>= either (\e -> printError e >> exitFailure)
89 handleWarnings (result, warns) = do
94 PrepareM { unPrepareM :: WriterT [PackageWarning] (ExceptT PackageError IO) a }
95 deriving (Functor, Applicative, Monad,
96 MonadWriter [PackageWarning],
97 MonadError PackageError)
99-- This MonadIO instance ensures that IO errors don't crash the program.
100instance MonadIO PrepareM where
102 lift' (try act) >>= either (otherError . IOExceptionThrown) return
104 lift' :: IO a -> PrepareM a
105 lift' = PrepareM . lift . lift
107runPrepareM :: PrepareM a -> IO (Either PackageError (a, [PackageWarning]))
108runPrepareM = runExceptT . runWriterT . unPrepareM
110warn :: PackageWarning -> PrepareM ()
113userError :: UserError -> PrepareM a
114userError = throwError . UserError
116internalError :: InternalError -> PrepareM a
117internalError = throwError . InternalError
119otherError :: OtherError -> PrepareM a
120otherError = throwError . OtherError
122catchLeft :: Applicative f => Either a b -> (a -> f b) -> f b
123catchLeft a f = either f pure a
125preparePackage' :: PublishOptions -> PrepareM D.UploadedPackage
126preparePackage' opts = do
127 exists <- liftIO (doesFileExist "bower.json")
128 unless exists (userError BowerJSONNotFound)
130 checkCleanWorkingTree opts
132 pkgMeta <- liftIO (Bower.decodeFile "bower.json")
133 >>= flip catchLeft (userError . CouldntDecodeBowerJSON)
134 (pkgVersionTag, pkgVersion) <- publishGetVersion opts
135 pkgGithub <- getBowerInfo pkgMeta
136 (pkgBookmarks, pkgModules) <- getModulesAndBookmarks
138 let declaredDeps = map fst (bowerDependencies pkgMeta ++
139 bowerDevDependencies pkgMeta)
140 pkgResolvedDependencies <- getResolvedDependencies declaredDeps
142 let pkgUploader = D.NotYetKnown
143 let pkgCompilerVersion = P.version
147getModulesAndBookmarks :: PrepareM ([D.Bookmark], [D.Module])
148getModulesAndBookmarks = do
149 (inputFiles, depsFiles) <- liftIO getInputAndDepsFiles
150 liftIO (D.parseAndDesugar inputFiles depsFiles renderModules)
151 >>= either (userError . CompileError) return
153 renderModules bookmarks modules =
154 return (bookmarks, map D.convertModule modules)
156data TreeStatus = Clean | Dirty deriving (Show, Read, Eq, Ord, Enum)
158getGitWorkingTreeStatus :: PrepareM TreeStatus
159getGitWorkingTreeStatus = do
160 out <- readProcess' "git" ["status", "--porcelain"] ""
162 if all null . lines $ out
166checkCleanWorkingTree :: PublishOptions -> PrepareM ()
167checkCleanWorkingTree opts = do
168 status <- getGitWorkingTreeStatus
169 unless (status == Clean) $
170 publishWorkingTreeDirty opts
172getVersionFromGitTag :: PrepareM (String, Version)
173getVersionFromGitTag = do
174 out <- readProcess' "git" ["tag", "--list", "--points-at", "HEAD"] ""
175 let vs = map trimWhitespace (lines out)
176 case mapMaybe parseMay vs of
177 [] -> userError TagMustBeCheckedOut
179 xs -> userError (AmbiguousVersions (map snd xs))
182 dropWhile isSpace >>> reverse >>> dropWhile isSpace >>> reverse
184 (str,) <$> D.parseVersion' (dropPrefix "v" str)
185 dropPrefix prefix str =
186 fromMaybe str (stripPrefix prefix str)
188getBowerInfo :: PackageMeta -> PrepareM (D.GithubUser, D.GithubRepo)
189getBowerInfo = either (userError . BadRepositoryField) return . tryExtract
192 case bowerRepository pkgMeta of
193 Nothing -> Left RepositoryFieldMissing
194 Just Repository{..} -> do
195 unless (repositoryType == "git")
196 (Left (BadRepositoryType repositoryType))
197 maybe (Left NotOnGithub) Right (extractGithub repositoryUrl)
199extractGithub :: String -> Maybe (D.GithubUser, D.GithubRepo)
202 >>> fmap (splitOn "/")
204 >>> fmap (D.GithubUser *** (D.GithubRepo . dropDotGit))
207 takeTwo :: [a] -> Maybe (a, a)
208 takeTwo [x, y] = Just (x, y)
211 dropDotGit :: String -> String
213 | ".git" `isSuffixOf` str = take (length str - 4) str
216 matchUrl :: String -> Maybe String
218 stripPrefix "git@github.com:" str <|> stripPrefix "git://github.com/" str
220readProcess' :: String -> [String] -> String -> PrepareM String
221readProcess' prog args stdin = do
222 out <- liftIO (catch (Right <$> readProcess prog args stdin)
224 either (otherError . ProcessFailed prog args) return out
228 -- ^ Listed in bower.json, but not installed.
230 -- ^ In the output of `bower list --json --offline`, there was no
231 -- _resolution key. This can be caused by adding the dependency using
232 -- `bower link`, or simply copying it into bower_components instead of
233 -- installing it normally.
234 | ResolvedOther String
235 -- ^ Resolved, but to something other than a version. The String argument
236 -- is the resolution type. The values it can take that I'm aware of are
237 -- "commit" and "branch".
238 | ResolvedVersion String
239 -- ^ Resolved to a version. The String argument is the resolution tag (eg,
241 deriving (Show, Read, Eq)
243-- Go through all bower dependencies which contain purescript code, and
244-- extract their versions.
246-- In the case where a bower dependency is taken from a particular version,
247-- that's easy; take that version. In any other case (eg, a branch, or a commit
248-- sha) we print a warning that documentation links will not work, and avoid
249-- linking to documentation for any types from that package.
251-- The rationale for this is: people will prefer to use a released version
252-- where possible. If they are not using a released version, then this is
253-- probably for a reason. However, docs are only ever available for released
254-- versions. Therefore there will probably be no version of the docs which is
255-- appropriate to link to, and we should omit links.
256getResolvedDependencies :: [PackageName] -> PrepareM [(PackageName, Version)]
257getResolvedDependencies declaredDeps = do
258 bower <- findBowerExecutable
259 depsBS <- packUtf8 <$> readProcess' bower ["list", "--json", "--offline"] ""
261 -- Check for undeclared dependencies
262 toplevels <- catchJSON (parse asToplevelDependencies depsBS)
263 warnUndeclared declaredDeps toplevels
265 deps <- catchJSON (parse asResolvedDependencies depsBS)
269 packUtf8 = TL.encodeUtf8 . TL.pack
270 catchJSON = flip catchLeft (internalError . JSONError FromBowerList)
272findBowerExecutable :: PrepareM String
273findBowerExecutable = do
274 mname <- liftIO . runMaybeT . msum . map (MaybeT . findExecutable) $ names
275 maybe (userError (BowerExecutableNotFound names)) return mname
277 names = case System.Info.os of
278 "mingw32" -> ["bower", "bower.cmd"]
281-- | Extracts all dependencies and their versions from
282-- `bower list --json --offline`
283asResolvedDependencies :: Parse BowerError [(PackageName, DependencyStatus)]
284asResolvedDependencies = nubBy ((==) `on` fst) <$> go
287 fmap (fromMaybe []) $
288 keyMay "dependencies" $
289 (++) <$> eachInObjectWithKey (parsePackageName . T.unpack)
291 <*> (concatMap snd <$> eachInObject asResolvedDependencies)
293-- | Extracts only the top level dependency names from the output of
294-- `bower list --json --offline`
295asToplevelDependencies :: Parse BowerError [PackageName]
296asToplevelDependencies =
299 eachInObjectWithKey (parsePackageName . T.unpack) (return ())
301asDependencyStatus :: Parse e DependencyStatus
302asDependencyStatus = do
303 isMissing <- keyOrDefault "missing" False asBool
309 keyOrDefault "_resolution" NoResolution $ do
310 type_ <- key "type" asString
312 "version" -> ResolvedVersion <$> key "tag" asString
313 other -> return (ResolvedOther other)
315warnUndeclared :: [PackageName] -> [PackageName] -> PrepareM ()
316warnUndeclared declared actual =
317 traverse_ (warn . UndeclaredDependency) (actual \\ declared)
320 [(PackageName, DependencyStatus)] -> PrepareM [(PackageName, Version)]
322 let (missing, noVersion, installed) = partitionDeps deps
325 userError (MissingDependencies (x :| xs))
327 traverse_ (warn . NoResolvedVersion) noVersion
328 withVersions <- catMaybes <$> traverse tryExtractVersion' installed
329 filterM (liftIO . isPureScript . bowerDir . fst) withVersions
332 partitionDeps = foldr go ([], [], [])
333 go (pkgName, d) (ms, os, is) =
335 Missing -> (pkgName : ms, os, is)
336 NoResolution -> (ms, pkgName : os, is)
337 ResolvedOther _ -> (ms, pkgName : os, is)
338 ResolvedVersion v -> (ms, os, (pkgName, v) : is)
340 bowerDir pkgName = "bower_components/" ++ runPackageName pkgName
342 -- Try to extract a version, and warn if unsuccessful.
343 tryExtractVersion' pair =
344 maybe (warn (UnacceptableVersion pair) >> return Nothing)
346 (tryExtractVersion pair)
348tryExtractVersion :: (PackageName, String) -> Maybe (PackageName, Version)
349tryExtractVersion (pkgName, tag) =
350 let tag' = fromMaybe tag (stripPrefix "v" tag)
351 in (pkgName,) <$> D.parseVersion' tag'
353-- | Returns whether it looks like there is a purescript package checked out
354-- in the given directory.
355isPureScript :: FilePath -> IO Bool
357 files <- Glob.globDir1 purescriptSourceFiles dir
358 return (not (null files))
360getInputAndDepsFiles :: IO ([FilePath], [(PackageName, FilePath)])
361getInputAndDepsFiles = do
362 inputFiles <- globRelative purescriptSourceFiles
363 depsFiles' <- globRelative purescriptDepsFiles
364 return (inputFiles, mapMaybe withPackageName depsFiles')
366withPackageName :: FilePath -> Maybe (PackageName, FilePath)
367withPackageName fp = (,fp) <$> getPackageName fp
369getPackageName :: FilePath -> Maybe PackageName
370getPackageName fp = do
371 let xs = splitOn [pathSeparator] fp
372 ys <- stripPrefix ["bower_components"] xs
374 case Bower.mkPackageName y of
375 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