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 (modules', bookmarks) <- parseAndBookmark inputFiles depsFiles
151 case runExcept (D.convertModulesInPackage modules') of
152 Right modules -> return (bookmarks, modules)
153 Left err -> userError (CompileError err)
156 parseAndBookmark inputFiles depsFiles = do
157 r <- liftIO . runExceptT $ D.parseAndBookmark inputFiles depsFiles
162 userError (CompileError err)
164data TreeStatus = Clean | Dirty deriving (Show, Read, Eq, Ord, Enum)
166getGitWorkingTreeStatus :: PrepareM TreeStatus
167getGitWorkingTreeStatus = do
168 out <- readProcess' "git" ["status", "--porcelain"] ""
170 if all null . lines $ out
174checkCleanWorkingTree :: PublishOptions -> PrepareM ()
175checkCleanWorkingTree opts = do
176 status <- getGitWorkingTreeStatus
177 unless (status == Clean) $
178 publishWorkingTreeDirty opts
180getVersionFromGitTag :: PrepareM (String, Version)
181getVersionFromGitTag = do
182 out <- readProcess' "git" ["tag", "--list", "--points-at", "HEAD"] ""
183 let vs = map trimWhitespace (lines out)
184 case mapMaybe parseMay vs of
185 [] -> userError TagMustBeCheckedOut
187 xs -> userError (AmbiguousVersions (map snd xs))
190 dropWhile isSpace >>> reverse >>> dropWhile isSpace >>> reverse
192 (str,) <$> D.parseVersion' (dropPrefix "v" str)
193 dropPrefix prefix str =
194 fromMaybe str (stripPrefix prefix str)
196getBowerInfo :: PackageMeta -> PrepareM (D.GithubUser, D.GithubRepo)
197getBowerInfo = either (userError . BadRepositoryField) return . tryExtract
200 case bowerRepository pkgMeta of
201 Nothing -> Left RepositoryFieldMissing
202 Just Repository{..} -> do
203 unless (repositoryType == "git")
204 (Left (BadRepositoryType repositoryType))
205 maybe (Left NotOnGithub) Right (extractGithub repositoryUrl)
207extractGithub :: String -> Maybe (D.GithubUser, D.GithubRepo)
208extractGithub = stripGitHubPrefixes
209 >>> fmap (splitOn "/")
211 >>> fmap (D.GithubUser *** (D.GithubRepo . dropDotGit))
214 takeTwo :: [a] -> Maybe (a, a)
215 takeTwo [x, y] = Just (x, y)
218 stripGitHubPrefixes :: String -> Maybe String
219 stripGitHubPrefixes = stripPrefixes [ "git://github.com/"
220 , "https://github.com/"
224 stripPrefixes :: [String] -> String -> Maybe String
225 stripPrefixes prefixes str = msum $ (`stripPrefix` str) <$> prefixes
227 dropDotGit :: String -> String
229 | ".git" `isSuffixOf` str = take (length str - 4) str
232readProcess' :: String -> [String] -> String -> PrepareM String
233readProcess' prog args stdin = do
234 out <- liftIO (catch (Right <$> readProcess prog args stdin)
236 either (otherError . ProcessFailed prog args) return out
240 -- ^ Listed in bower.json, but not installed.
242 -- ^ In the output of `bower list --json --offline`, there was no
243 -- _resolution key. This can be caused by adding the dependency using
244 -- `bower link`, or simply copying it into bower_components instead of
245 -- installing it normally.
246 | ResolvedOther String
247 -- ^ Resolved, but to something other than a version. The String argument
248 -- is the resolution type. The values it can take that I'm aware of are
249 -- "commit" and "branch".
250 | ResolvedVersion String
251 -- ^ Resolved to a version. The String argument is the resolution tag (eg,
253 deriving (Show, Read, Eq)
255-- Go through all bower dependencies which contain purescript code, and
256-- extract their versions.
258-- In the case where a bower dependency is taken from a particular version,
259-- that's easy; take that version. In any other case (eg, a branch, or a commit
260-- sha) we print a warning that documentation links will not work, and avoid
261-- linking to documentation for any types from that package.
263-- The rationale for this is: people will prefer to use a released version
264-- where possible. If they are not using a released version, then this is
265-- probably for a reason. However, docs are only ever available for released
266-- versions. Therefore there will probably be no version of the docs which is
267-- appropriate to link to, and we should omit links.
268getResolvedDependencies :: [PackageName] -> PrepareM [(PackageName, Version)]
269getResolvedDependencies declaredDeps = do
270 bower <- findBowerExecutable
271 depsBS <- packUtf8 <$> readProcess' bower ["list", "--json", "--offline"] ""
273 -- Check for undeclared dependencies
274 toplevels <- catchJSON (parse asToplevelDependencies depsBS)
275 warnUndeclared declaredDeps toplevels
277 deps <- catchJSON (parse asResolvedDependencies depsBS)
281 packUtf8 = TL.encodeUtf8 . TL.pack
282 catchJSON = flip catchLeft (internalError . JSONError FromBowerList)
284findBowerExecutable :: PrepareM String
285findBowerExecutable = do
286 mname <- liftIO . runMaybeT . msum . map (MaybeT . findExecutable) $ names
287 maybe (userError (BowerExecutableNotFound names)) return mname
289 names = case System.Info.os of
290 "mingw32" -> ["bower", "bower.cmd"]
293-- | Extracts all dependencies and their versions from
294-- `bower list --json --offline`
295asResolvedDependencies :: Parse BowerError [(PackageName, DependencyStatus)]
296asResolvedDependencies = nubBy ((==) `on` fst) <$> go
299 fmap (fromMaybe []) $
300 keyMay "dependencies" $
301 (++) <$> eachInObjectWithKey (parsePackageName . T.unpack)
303 <*> (concatMap snd <$> eachInObject asResolvedDependencies)
305-- | Extracts only the top level dependency names from the output of
306-- `bower list --json --offline`
307asToplevelDependencies :: Parse BowerError [PackageName]
308asToplevelDependencies =
311 eachInObjectWithKey (parsePackageName . T.unpack) (return ())
313asDependencyStatus :: Parse e DependencyStatus
314asDependencyStatus = do
315 isMissing <- keyOrDefault "missing" False asBool
321 keyOrDefault "_resolution" NoResolution $ do
322 type_ <- key "type" asString
324 "version" -> ResolvedVersion <$> key "tag" asString
325 other -> return (ResolvedOther other)
327warnUndeclared :: [PackageName] -> [PackageName] -> PrepareM ()
328warnUndeclared declared actual =
329 traverse_ (warn . UndeclaredDependency) (actual \\ declared)
332 [(PackageName, DependencyStatus)] -> PrepareM [(PackageName, Version)]
334 let (missing, noVersion, installed) = partitionDeps deps
337 userError (MissingDependencies (x :| xs))
339 traverse_ (warn . NoResolvedVersion) noVersion
340 withVersions <- catMaybes <$> traverse tryExtractVersion' installed
341 filterM (liftIO . isPureScript . bowerDir . fst) withVersions
344 partitionDeps = foldr go ([], [], [])
345 go (pkgName, d) (ms, os, is) =
347 Missing -> (pkgName : ms, os, is)
348 NoResolution -> (ms, pkgName : os, is)
349 ResolvedOther _ -> (ms, pkgName : os, is)
350 ResolvedVersion v -> (ms, os, (pkgName, v) : is)
352 bowerDir pkgName = "bower_components/" ++ runPackageName pkgName
354 -- Try to extract a version, and warn if unsuccessful.
355 tryExtractVersion' pair =
356 maybe (warn (UnacceptableVersion pair) >> return Nothing)
358 (tryExtractVersion pair)
360tryExtractVersion :: (PackageName, String) -> Maybe (PackageName, Version)
361tryExtractVersion (pkgName, tag) =
362 let tag' = fromMaybe tag (stripPrefix "v" tag)
363 in (pkgName,) <$> D.parseVersion' tag'
365-- | Returns whether it looks like there is a purescript package checked out
366-- in the given directory.
367isPureScript :: FilePath -> IO Bool
369 files <- Glob.globDir1 purescriptSourceFiles dir
370 return (not (null files))
372getInputAndDepsFiles :: IO ([FilePath], [(PackageName, FilePath)])
373getInputAndDepsFiles = do
374 inputFiles <- globRelative purescriptSourceFiles
375 depsFiles' <- globRelative purescriptDepsFiles
376 return (inputFiles, mapMaybe withPackageName depsFiles')
378withPackageName :: FilePath -> Maybe (PackageName, FilePath)
379withPackageName fp = (,fp) <$> getPackageName fp
381getPackageName :: FilePath -> Maybe PackageName
382getPackageName fp = do
383 let xs = splitOn [pathSeparator] fp
384 ys <- stripPrefix ["bower_components"] xs
386 case Bower.mkPackageName y of
387 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