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 . CompileError) 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)
199extractGithub = stripGitHubPrefixes
200 >>> fmap (splitOn "/")
202 >>> fmap (D.GithubUser *** (D.GithubRepo . dropDotGit))
205 takeTwo :: [a] -> Maybe (a, a)
206 takeTwo [x, y] = Just (x, y)
209 stripGitHubPrefixes :: String -> Maybe String
210 stripGitHubPrefixes = stripPrefixes [ "git://github.com/"
211 , "https://github.com/"
215 stripPrefixes :: [String] -> String -> Maybe String
216 stripPrefixes prefixes str = msum $ (`stripPrefix` str) <$> prefixes
218 dropDotGit :: String -> String
220 | ".git" `isSuffixOf` str = take (length str - 4) str
223readProcess' :: String -> [String] -> String -> PrepareM String
224readProcess' prog args stdin = do
225 out <- liftIO (catch (Right <$> readProcess prog args stdin)
227 either (otherError . ProcessFailed prog args) return out
231 -- ^ Listed in bower.json, but not installed.
233 -- ^ In the output of `bower list --json --offline`, there was no
234 -- _resolution key. This can be caused by adding the dependency using
235 -- `bower link`, or simply copying it into bower_components instead of
236 -- installing it normally.
237 | ResolvedOther String
238 -- ^ Resolved, but to something other than a version. The String argument
239 -- is the resolution type. The values it can take that I'm aware of are
240 -- "commit" and "branch".
241 | ResolvedVersion String
242 -- ^ Resolved to a version. The String argument is the resolution tag (eg,
244 deriving (Show, Read, Eq)
246-- Go through all bower dependencies which contain purescript code, and
247-- extract their versions.
249-- In the case where a bower dependency is taken from a particular version,
250-- that's easy; take that version. In any other case (eg, a branch, or a commit
251-- sha) we print a warning that documentation links will not work, and avoid
252-- linking to documentation for any types from that package.
254-- The rationale for this is: people will prefer to use a released version
255-- where possible. If they are not using a released version, then this is
256-- probably for a reason. However, docs are only ever available for released
257-- versions. Therefore there will probably be no version of the docs which is
258-- appropriate to link to, and we should omit links.
259getResolvedDependencies :: [PackageName] -> PrepareM [(PackageName, Version)]
260getResolvedDependencies declaredDeps = do
261 bower <- findBowerExecutable
262 depsBS <- packUtf8 <$> readProcess' bower ["list", "--json", "--offline"] ""
264 -- Check for undeclared dependencies
265 toplevels <- catchJSON (parse asToplevelDependencies depsBS)
266 warnUndeclared declaredDeps toplevels
268 deps <- catchJSON (parse asResolvedDependencies depsBS)
272 packUtf8 = TL.encodeUtf8 . TL.pack
273 catchJSON = flip catchLeft (internalError . JSONError FromBowerList)
275findBowerExecutable :: PrepareM String
276findBowerExecutable = do
277 mname <- liftIO . runMaybeT . msum . map (MaybeT . findExecutable) $ names
278 maybe (userError (BowerExecutableNotFound names)) return mname
280 names = case System.Info.os of
281 "mingw32" -> ["bower", "bower.cmd"]
284-- | Extracts all dependencies and their versions from
285-- `bower list --json --offline`
286asResolvedDependencies :: Parse BowerError [(PackageName, DependencyStatus)]
287asResolvedDependencies = nubBy ((==) `on` fst) <$> go
290 fmap (fromMaybe []) $
291 keyMay "dependencies" $
292 (++) <$> eachInObjectWithKey (parsePackageName . T.unpack)
294 <*> (concatMap snd <$> eachInObject asResolvedDependencies)
296-- | Extracts only the top level dependency names from the output of
297-- `bower list --json --offline`
298asToplevelDependencies :: Parse BowerError [PackageName]
299asToplevelDependencies =
302 eachInObjectWithKey (parsePackageName . T.unpack) (return ())
304asDependencyStatus :: Parse e DependencyStatus
305asDependencyStatus = do
306 isMissing <- keyOrDefault "missing" False asBool
312 keyOrDefault "_resolution" NoResolution $ do
313 type_ <- key "type" asString
315 "version" -> ResolvedVersion <$> key "tag" asString
316 other -> return (ResolvedOther other)
318warnUndeclared :: [PackageName] -> [PackageName] -> PrepareM ()
319warnUndeclared declared actual =
320 traverse_ (warn . UndeclaredDependency) (actual \\ declared)
323 [(PackageName, DependencyStatus)] -> PrepareM [(PackageName, Version)]
325 let (missing, noVersion, installed) = partitionDeps deps
328 userError (MissingDependencies (x :| xs))
330 traverse_ (warn . NoResolvedVersion) noVersion
331 withVersions <- catMaybes <$> traverse tryExtractVersion' installed
332 filterM (liftIO . isPureScript . bowerDir . fst) withVersions
335 partitionDeps = foldr go ([], [], [])
336 go (pkgName, d) (ms, os, is) =
338 Missing -> (pkgName : ms, os, is)
339 NoResolution -> (ms, pkgName : os, is)
340 ResolvedOther _ -> (ms, pkgName : os, is)
341 ResolvedVersion v -> (ms, os, (pkgName, v) : is)
343 bowerDir pkgName = "bower_components/" ++ runPackageName pkgName
345 -- Try to extract a version, and warn if unsuccessful.
346 tryExtractVersion' pair =
347 maybe (warn (UnacceptableVersion pair) >> return Nothing)
349 (tryExtractVersion pair)
351tryExtractVersion :: (PackageName, String) -> Maybe (PackageName, Version)
352tryExtractVersion (pkgName, tag) =
353 let tag' = fromMaybe tag (stripPrefix "v" tag)
354 in (pkgName,) <$> D.parseVersion' tag'
356-- | Returns whether it looks like there is a purescript package checked out
357-- in the given directory.
358isPureScript :: FilePath -> IO Bool
360 files <- Glob.globDir1 purescriptSourceFiles dir
361 return (not (null files))
363getInputAndDepsFiles :: IO ([FilePath], [(PackageName, FilePath)])
364getInputAndDepsFiles = do
365 inputFiles <- globRelative purescriptSourceFiles
366 depsFiles' <- globRelative purescriptDepsFiles
367 return (inputFiles, mapMaybe withPackageName depsFiles')
369withPackageName :: FilePath -> Maybe (PackageName, FilePath)
370withPackageName fp = (,fp) <$> getPackageName fp
372getPackageName :: FilePath -> Maybe PackageName
373getPackageName fp = do
374 let xs = splitOn [pathSeparator] fp
375 ys <- stripPrefix ["bower_components"] xs
377 case Bower.mkPackageName y of
378 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