1{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE RecordWildCards #-}
3{-# LANGUAGE TupleSections #-}
4{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6module Language.PureScript.Publish
12 , defaultPublishOptions
13 , getGitWorkingTreeStatus
14 , requireCleanWorkingTree
15 , getVersionFromGitTag
17 , getModulesAndBookmarks
18 , getResolvedDependencies
21import Prelude hiding (userError)
24import Data.Char (isSpace)
25import Data.String (fromString)
26import Data.List (stripPrefix, isSuffixOf, (\\), nubBy)
27import Data.List.Split (splitOn)
28import Data.List.NonEmpty (NonEmpty(..))
30import Data.Function (on)
32import Data.Aeson.BetterErrors
33import qualified Data.Text as T
35import Control.Applicative
36import Control.Category ((>>>))
37import Control.Arrow ((***))
38import Control.Exception (catch, try)
39import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
40import Control.Monad.Trans.Except
41import Control.Monad.Error.Class (MonadError(..))
42import Control.Monad.Writer
44import System.Directory (doesFileExist, findExecutable)
45import System.Process (readProcess)
46import System.Exit (exitFailure)
47import qualified System.FilePath.Glob as Glob
48import qualified System.Info
50import Web.Bower.PackageMeta (PackageMeta(..), BowerError(..), PackageName,
51 runPackageName, parsePackageName, Repository(..))
52import qualified Web.Bower.PackageMeta as Bower
54import qualified Language.PureScript as P (version)
55import qualified Language.PureScript.Docs as D
56import Language.PureScript.Publish.Utils
57import Language.PureScript.Publish.ErrorsWarnings
59data PublishOptions = PublishOptions
60 { -- | How to obtain the version tag and version that the data being
61 -- generated will refer to.
62 publishGetVersion :: PrepareM (String, Version)
65defaultPublishOptions :: PublishOptions
66defaultPublishOptions = PublishOptions
67 { publishGetVersion = getVersionFromGitTag
70-- | Attempt to retrieve package metadata from the current directory.
71-- Calls exitFailure if no package metadata could be retrieved.
72preparePackage :: PublishOptions -> IO D.UploadedPackage
74 runPrepareM (preparePackage' opts)
75 >>= either (\e -> printError e >> exitFailure)
78 handleWarnings (result, warns) = do
83 PrepareM { unPrepareM :: WriterT [PackageWarning] (ExceptT PackageError IO) a }
84 deriving (Functor, Applicative, Monad,
85 MonadWriter [PackageWarning],
86 MonadError PackageError)
88-- This MonadIO instance ensures that IO errors don't crash the program.
89instance MonadIO PrepareM where
91 lift' (try act) >>= either (otherError . IOExceptionThrown) return
93 lift' :: IO a -> PrepareM a
94 lift' = PrepareM . lift . lift
96runPrepareM :: PrepareM a -> IO (Either PackageError (a, [PackageWarning]))
97runPrepareM = runExceptT . runWriterT . unPrepareM
99warn :: PackageWarning -> PrepareM ()
102userError :: UserError -> PrepareM a
103userError = throwError . UserError
105internalError :: InternalError -> PrepareM a
106internalError = throwError . InternalError
108otherError :: OtherError -> PrepareM a
109otherError = throwError . OtherError
111catchLeft :: Applicative f => Either a b -> (a -> f b) -> f b
112catchLeft a f = either f pure a
114preparePackage' :: PublishOptions -> PrepareM D.UploadedPackage
115preparePackage' opts = do
116 exists <- liftIO (doesFileExist "bower.json")
117 unless exists (userError BowerJSONNotFound)
119 requireCleanWorkingTree
121 pkgMeta <- liftIO (Bower.decodeFile "bower.json")
122 >>= flip catchLeft (userError . CouldntParseBowerJSON)
123 (pkgVersionTag, pkgVersion) <- publishGetVersion opts
124 pkgGithub <- getBowerInfo pkgMeta
125 (pkgBookmarks, pkgModules) <- getModulesAndBookmarks
127 let declaredDeps = map fst (bowerDependencies pkgMeta ++
128 bowerDevDependencies pkgMeta)
129 pkgResolvedDependencies <- getResolvedDependencies declaredDeps
131 let pkgUploader = D.NotYetKnown
132 let pkgCompilerVersion = P.version
136getModulesAndBookmarks :: PrepareM ([D.Bookmark], [D.Module])
137getModulesAndBookmarks = do
138 (inputFiles, depsFiles) <- liftIO getInputAndDepsFiles
139 liftIO (D.parseAndDesugar inputFiles depsFiles renderModules)
140 >>= either (userError . ParseAndDesugarError) return
142 renderModules bookmarks modules =
143 return (bookmarks, map D.convertModule modules)
145data TreeStatus = Clean | Dirty deriving (Show, Eq, Ord, Enum)
147getGitWorkingTreeStatus :: PrepareM TreeStatus
148getGitWorkingTreeStatus = do
149 out <- readProcess' "git" ["status", "--porcelain"] ""
151 if null . filter (not . null) . lines $ out
155requireCleanWorkingTree :: PrepareM ()
156requireCleanWorkingTree = do
157 status <- getGitWorkingTreeStatus
158 unless (status == Clean) $
159 userError DirtyWorkingTree
161getVersionFromGitTag :: PrepareM (String, Version)
162getVersionFromGitTag = do
163 out <- readProcess' "git" ["tag", "--list", "--points-at", "HEAD"] ""
164 let vs = map trimWhitespace (lines out)
165 case mapMaybe parseMay vs of
166 [] -> userError TagMustBeCheckedOut
168 xs -> userError (AmbiguousVersions (map snd xs))
171 dropWhile isSpace >>> reverse >>> dropWhile isSpace >>> reverse
173 (str,) <$> D.parseVersion' (dropPrefix "v" str)
174 dropPrefix prefix str =
175 fromMaybe str (stripPrefix prefix str)
177getBowerInfo :: PackageMeta -> PrepareM (D.GithubUser, D.GithubRepo)
178getBowerInfo = either (userError . BadRepositoryField) return . tryExtract
181 case bowerRepository pkgMeta of
182 Nothing -> Left RepositoryFieldMissing
183 Just Repository{..} -> do
184 unless (repositoryType == "git")
185 (Left (BadRepositoryType repositoryType))
186 maybe (Left NotOnGithub) Right (extractGithub repositoryUrl)
188extractGithub :: String -> Maybe (D.GithubUser, D.GithubRepo)
190 stripPrefix "git://github.com/"
191 >>> fmap (splitOn "/")
193 >>> fmap (D.GithubUser *** (D.GithubRepo . dropDotGit))
196 takeTwo :: [a] -> Maybe (a, a)
197 takeTwo [x, y] = Just (x, y)
200 dropDotGit :: String -> String
202 | ".git" `isSuffixOf` str = take (length str - 4) str
205readProcess' :: String -> [String] -> String -> PrepareM String
206readProcess' prog args stdin = do
207 out <- liftIO (catch (Right <$> readProcess prog args stdin)
209 either (otherError . ProcessFailed prog args) return out
213 -- ^ Listed in bower.json, but not installed.
215 -- ^ In the output of `bower list --json --offline`, there was no
216 -- _resolution key. This can be caused by adding the dependency using
217 -- `bower link`, or simply copying it into bower_components instead of
218 -- installing it normally.
219 | ResolvedOther String
220 -- ^ Resolved, but to something other than a version. The String argument
221 -- is the resolution type. The values it can take that I'm aware of are
222 -- "commit" and "branch".
223 | ResolvedVersion String
224 -- ^ Resolved to a version. The String argument is the resolution tag (eg,
228-- Go through all bower dependencies which contain purescript code, and
229-- extract their versions.
231-- In the case where a bower dependency is taken from a particular version,
232-- that's easy; take that version. In any other case (eg, a branch, or a commit
233-- sha) we print a warning that documentation links will not work, and avoid
234-- linking to documentation for any types from that package.
236-- The rationale for this is: people will prefer to use a released version
237-- where possible. If they are not using a released version, then this is
238-- probably for a reason. However, docs are only ever available for released
239-- versions. Therefore there will probably be no version of the docs which is
240-- appropriate to link to, and we should omit links.
241getResolvedDependencies :: [PackageName] -> PrepareM [(PackageName, Version)]
242getResolvedDependencies declaredDeps = do
243 bower <- findBowerExecutable
244 depsBS <- fromString <$> readProcess' bower ["list", "--json", "--offline"] ""
246 -- Check for undeclared dependencies
247 toplevels <- catchJSON (parse asToplevelDependencies depsBS)
248 warnUndeclared declaredDeps toplevels
250 deps <- catchJSON (parse asResolvedDependencies depsBS)
254 catchJSON = flip catchLeft (internalError . JSONError FromBowerList)
256findBowerExecutable :: PrepareM String
257findBowerExecutable = do
258 mname <- liftIO . runMaybeT . msum . map (MaybeT . findExecutable) $ names
259 maybe (userError (BowerExecutableNotFound names)) return mname
261 names = case System.Info.os of
262 "mingw32" -> ["bower", "bower.cmd"]
265-- | Extracts all dependencies and their versions from
266-- `bower list --json --offline`
267asResolvedDependencies :: Parse BowerError [(PackageName, DependencyStatus)]
268asResolvedDependencies = nubBy ((==) `on` fst) <$> go
271 fmap (fromMaybe []) $
272 keyMay "dependencies" $
273 (++) <$> eachInObjectWithKey (parsePackageName . T.unpack)
275 <*> (concatMap snd <$> eachInObject asResolvedDependencies)
277-- | Extracts only the top level dependency names from the output of
278-- `bower list --json --offline`
279asToplevelDependencies :: Parse BowerError [PackageName]
280asToplevelDependencies =
283 eachInObjectWithKey (parsePackageName . T.unpack) (return ())
285asDependencyStatus :: Parse e DependencyStatus
286asDependencyStatus = do
287 isMissing <- keyOrDefault "missing" False asBool
293 keyOrDefault "_resolution" NoResolution $ do
294 type_ <- key "type" asString
296 "version" -> ResolvedVersion <$> key "tag" asString
297 other -> return (ResolvedOther other)
299warnUndeclared :: [PackageName] -> [PackageName] -> PrepareM ()
300warnUndeclared declared actual =
301 mapM_ (warn . UndeclaredDependency) (actual \\ declared)
304 [(PackageName, DependencyStatus)] -> PrepareM [(PackageName, Version)]
306 let (missing, noVersion, installed) = partitionDeps deps
309 userError (MissingDependencies (x :| xs))
311 mapM_ (warn . NoResolvedVersion) noVersion
312 withVersions <- catMaybes <$> mapM tryExtractVersion' installed
313 filterM (liftIO . isPureScript . bowerDir . fst) withVersions
316 partitionDeps = foldr go ([], [], [])
317 go (pkgName, d) (ms, os, is) =
319 Missing -> (pkgName : ms, os, is)
320 NoResolution -> (ms, pkgName : os, is)
321 ResolvedOther _ -> (ms, pkgName : os, is)
322 ResolvedVersion v -> (ms, os, (pkgName, v) : is)
324 bowerDir pkgName = "bower_components/" ++ runPackageName pkgName
326 -- Try to extract a version, and warn if unsuccessful.
327 tryExtractVersion' pair =
328 maybe (warn (UnacceptableVersion pair) >> return Nothing)
330 (tryExtractVersion pair)
332tryExtractVersion :: (PackageName, String) -> Maybe (PackageName, Version)
333tryExtractVersion (pkgName, tag) =
334 let tag' = fromMaybe tag (stripPrefix "v" tag)
335 in (pkgName,) <$> D.parseVersion' tag'
337-- | Returns whether it looks like there is a purescript package checked out
338-- in the given directory.
339isPureScript :: FilePath -> IO Bool
341 files <- Glob.globDir1 purescriptSourceFiles dir
342 return (not (null files))
344getInputAndDepsFiles :: IO ([FilePath], [(PackageName, FilePath)])
345getInputAndDepsFiles = do
346 inputFiles <- globRelative purescriptSourceFiles
347 depsFiles' <- globRelative purescriptDepsFiles
348 return (inputFiles, mapMaybe withPackageName depsFiles')
350withPackageName :: FilePath -> Maybe (PackageName, FilePath)
351withPackageName fp = (,fp) <$> getPackageName fp
353getPackageName :: FilePath -> Maybe PackageName
354getPackageName fp = do
355 let xs = splitOn "/" fp
356 ys <- stripPrefix ["bower_components"] xs
358 case Bower.mkPackageName y of
359 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