1{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE RecordWildCards #-}
3{-# LANGUAGE TupleSections #-}
4{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6module Language.PureScript.Publish
17 , defaultPublishOptions
18 , getGitWorkingTreeStatus
19 , checkCleanWorkingTree
20 , getVersionFromGitTag
21 , getBowerRepositoryInfo
22 , getModulesAndBookmarks
23 , getResolvedDependencies
27import Prelude.Compat hiding (userError)
30import Data.Char (isSpace)
31import Data.List (stripPrefix, isSuffixOf, (\\), nubBy)
32import Data.List.Split (splitOn)
33import Data.List.NonEmpty (NonEmpty(..))
35import Data.Function (on)
36import Data.Foldable (traverse_)
38import Data.Aeson.BetterErrors
39import qualified Data.Text as T
40import qualified Data.Text.Lazy as TL
41import qualified Data.Text.Lazy.Encoding as TL
42import qualified Data.SPDX as SPDX
44import Control.Category ((>>>))
45import Control.Arrow ((***))
46import Control.Exception (catch, try)
47import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
48import Control.Monad.Trans.Except
49import Control.Monad.Error.Class (MonadError(..))
50import Control.Monad.Writer.Strict
52import System.Directory (doesFileExist, findExecutable)
53import System.Process (readProcess)
54import System.Exit (exitFailure)
55import System.FilePath (pathSeparator)
56import qualified System.FilePath.Glob as Glob
57import qualified System.Info
59import Web.Bower.PackageMeta (PackageMeta(..), BowerError(..), PackageName,
60 runPackageName, parsePackageName, Repository(..))
61import qualified Web.Bower.PackageMeta as Bower
63import qualified Language.PureScript as P (version)
64import qualified Language.PureScript.Docs as D
65import Language.PureScript.Publish.Utils
66import Language.PureScript.Publish.ErrorsWarnings
68data PublishOptions = PublishOptions
69 { -- | How to obtain the version tag and version that the data being
70 -- generated will refer to.
71 publishGetVersion :: PrepareM (String, Version)
72 , -- | What to do when the working tree is dirty
73 publishWorkingTreeDirty :: PrepareM ()
76defaultPublishOptions :: PublishOptions
77defaultPublishOptions = PublishOptions
78 { publishGetVersion = getVersionFromGitTag
79 , publishWorkingTreeDirty = userError DirtyWorkingTree
82-- | Attempt to retrieve package metadata from the current directory.
83-- Calls exitFailure if no package metadata could be retrieved.
84unsafePreparePackage :: PublishOptions -> IO D.UploadedPackage
85unsafePreparePackage opts = either (\e -> printError e >> exitFailure) pure =<< preparePackage opts
87-- | Attempt to retrieve package metadata from the current directory.
88-- Returns a PackageError on failure
89preparePackage :: PublishOptions -> IO (Either PackageError D.UploadedPackage)
91 runPrepareM (preparePackage' opts)
92 >>= either (pure . Left) (fmap Right . handleWarnings)
95 handleWarnings (result, warns) = do
100 PrepareM { unPrepareM :: WriterT [PackageWarning] (ExceptT PackageError IO) a }
101 deriving (Functor, Applicative, Monad,
102 MonadWriter [PackageWarning],
103 MonadError PackageError)
105-- This MonadIO instance ensures that IO errors don't crash the program.
106instance MonadIO PrepareM where
108 lift' (try act) >>= either (otherError . IOExceptionThrown) return
110 lift' :: IO a -> PrepareM a
111 lift' = PrepareM . lift . lift
113runPrepareM :: PrepareM a -> IO (Either PackageError (a, [PackageWarning]))
114runPrepareM = runExceptT . runWriterT . unPrepareM
116warn :: PackageWarning -> PrepareM ()
119userError :: UserError -> PrepareM a
120userError = throwError . UserError
122internalError :: InternalError -> PrepareM a
123internalError = throwError . InternalError
125otherError :: OtherError -> PrepareM a
126otherError = throwError . OtherError
128catchLeft :: Applicative f => Either a b -> (a -> f b) -> f b
129catchLeft a f = either f pure a
131unlessM :: Monad m => m Bool -> m () -> m ()
132unlessM cond act = cond >>= flip unless act
134preparePackage' :: PublishOptions -> PrepareM D.UploadedPackage
135preparePackage' opts = do
136 unlessM (liftIO (doesFileExist "bower.json")) (userError BowerJSONNotFound)
137 checkCleanWorkingTree opts
139 pkgMeta <- liftIO (Bower.decodeFile "bower.json")
140 >>= flip catchLeft (userError . CouldntDecodeBowerJSON)
143 (pkgVersionTag, pkgVersion) <- publishGetVersion opts
144 pkgGithub <- getBowerRepositoryInfo pkgMeta
145 (pkgBookmarks, pkgModules) <- getModulesAndBookmarks
147 let declaredDeps = map fst (bowerDependencies pkgMeta ++
148 bowerDevDependencies pkgMeta)
149 pkgResolvedDependencies <- getResolvedDependencies declaredDeps
151 let pkgUploader = D.NotYetKnown
152 let pkgCompilerVersion = P.version
156getModulesAndBookmarks :: PrepareM ([D.Bookmark], [D.Module])
157getModulesAndBookmarks = do
158 (inputFiles, depsFiles) <- liftIO getInputAndDepsFiles
159 (modules', bookmarks) <- parseAndBookmark inputFiles depsFiles
161 case runExcept (D.convertModulesInPackage modules') of
162 Right modules -> return (bookmarks, modules)
163 Left err -> userError (CompileError err)
166 parseAndBookmark inputFiles depsFiles = do
167 r <- liftIO . runExceptT $ D.parseAndBookmark inputFiles depsFiles
172 userError (CompileError err)
174data TreeStatus = Clean | Dirty deriving (Show, Read, Eq, Ord, Enum)
176getGitWorkingTreeStatus :: PrepareM TreeStatus
177getGitWorkingTreeStatus = do
178 out <- readProcess' "git" ["status", "--porcelain"] ""
180 if all null . lines $ out
184checkCleanWorkingTree :: PublishOptions -> PrepareM ()
185checkCleanWorkingTree opts = do
186 status <- getGitWorkingTreeStatus
187 unless (status == Clean) $
188 publishWorkingTreeDirty opts
190getVersionFromGitTag :: PrepareM (String, Version)
191getVersionFromGitTag = do
192 out <- readProcess' "git" ["tag", "--list", "--points-at", "HEAD"] ""
193 let vs = map trimWhitespace (lines out)
194 case mapMaybe parseMay vs of
195 [] -> userError TagMustBeCheckedOut
197 xs -> userError (AmbiguousVersions (map snd xs))
200 dropWhile isSpace >>> reverse >>> dropWhile isSpace >>> reverse
202 (str,) <$> D.parseVersion' (dropPrefix "v" str)
203 dropPrefix prefix str =
204 fromMaybe str (stripPrefix prefix str)
206getBowerRepositoryInfo :: PackageMeta -> PrepareM (D.GithubUser, D.GithubRepo)
207getBowerRepositoryInfo = either (userError . BadRepositoryField) return . tryExtract
210 case bowerRepository pkgMeta of
211 Nothing -> Left RepositoryFieldMissing
212 Just Repository{..} -> do
213 unless (repositoryType == "git")
214 (Left (BadRepositoryType repositoryType))
215 maybe (Left NotOnGithub) Right (extractGithub repositoryUrl)
217checkLicense :: PackageMeta -> PrepareM ()
218checkLicense pkgMeta =
219 unless (any isValidSPDX (bowerLicense pkgMeta))
220 (userError NoLicenseSpecified)
223-- Check if a string is a valid SPDX license expression.
225isValidSPDX :: String -> Bool
226isValidSPDX = (== 1) . length . SPDX.parseExpression
228extractGithub :: String -> Maybe (D.GithubUser, D.GithubRepo)
229extractGithub = stripGitHubPrefixes
230 >>> fmap (splitOn "/")
232 >>> fmap (D.GithubUser *** (D.GithubRepo . dropDotGit))
235 takeTwo :: [a] -> Maybe (a, a)
236 takeTwo [x, y] = Just (x, y)
239 stripGitHubPrefixes :: String -> Maybe String
240 stripGitHubPrefixes = stripPrefixes [ "git://github.com/"
241 , "https://github.com/"
245 stripPrefixes :: [String] -> String -> Maybe String
246 stripPrefixes prefixes str = msum $ (`stripPrefix` str) <$> prefixes
248 dropDotGit :: String -> String
250 | ".git" `isSuffixOf` str = take (length str - 4) str
253readProcess' :: String -> [String] -> String -> PrepareM String
254readProcess' prog args stdin = do
255 out <- liftIO (catch (Right <$> readProcess prog args stdin)
257 either (otherError . ProcessFailed prog args) return out
261 -- ^ Listed in bower.json, but not installed.
263 -- ^ In the output of `bower list --json --offline`, there was no
264 -- _resolution key. This can be caused by adding the dependency using
265 -- `bower link`, or simply copying it into bower_components instead of
266 -- installing it normally.
267 | ResolvedOther String
268 -- ^ Resolved, but to something other than a version. The String argument
269 -- is the resolution type. The values it can take that I'm aware of are
270 -- "commit" and "branch".
271 | ResolvedVersion String
272 -- ^ Resolved to a version. The String argument is the resolution tag (eg,
274 deriving (Show, Read, Eq)
276-- Go through all bower dependencies which contain purescript code, and
277-- extract their versions.
279-- In the case where a bower dependency is taken from a particular version,
280-- that's easy; take that version. In any other case (eg, a branch, or a commit
281-- sha) we print a warning that documentation links will not work, and avoid
282-- linking to documentation for any types from that package.
284-- The rationale for this is: people will prefer to use a released version
285-- where possible. If they are not using a released version, then this is
286-- probably for a reason. However, docs are only ever available for released
287-- versions. Therefore there will probably be no version of the docs which is
288-- appropriate to link to, and we should omit links.
289getResolvedDependencies :: [PackageName] -> PrepareM [(PackageName, Version)]
290getResolvedDependencies declaredDeps = do
291 bower <- findBowerExecutable
292 depsBS <- packUtf8 <$> readProcess' bower ["list", "--json", "--offline"] ""
294 -- Check for undeclared dependencies
295 toplevels <- catchJSON (parse asToplevelDependencies depsBS)
296 warnUndeclared declaredDeps toplevels
298 deps <- catchJSON (parse asResolvedDependencies depsBS)
302 packUtf8 = TL.encodeUtf8 . TL.pack
303 catchJSON = flip catchLeft (internalError . JSONError FromBowerList)
305findBowerExecutable :: PrepareM String
306findBowerExecutable = do
307 mname <- liftIO . runMaybeT . msum . map (MaybeT . findExecutable) $ names
308 maybe (userError (BowerExecutableNotFound names)) return mname
310 names = case System.Info.os of
311 "mingw32" -> ["bower", "bower.cmd"]
314-- | Extracts all dependencies and their versions from
315-- `bower list --json --offline`
316asResolvedDependencies :: Parse BowerError [(PackageName, DependencyStatus)]
317asResolvedDependencies = nubBy ((==) `on` fst) <$> go
320 fmap (fromMaybe []) $
321 keyMay "dependencies" $
322 (++) <$> eachInObjectWithKey (parsePackageName . T.unpack)
324 <*> (concatMap snd <$> eachInObject asResolvedDependencies)
326-- | Extracts only the top level dependency names from the output of
327-- `bower list --json --offline`
328asToplevelDependencies :: Parse BowerError [PackageName]
329asToplevelDependencies =
332 eachInObjectWithKey (parsePackageName . T.unpack) (return ())
334asDependencyStatus :: Parse e DependencyStatus
335asDependencyStatus = do
336 isMissing <- keyOrDefault "missing" False asBool
342 keyOrDefault "_resolution" NoResolution $ do
343 type_ <- key "type" asString
345 "version" -> ResolvedVersion <$> key "tag" asString
346 other -> return (ResolvedOther other)
348warnUndeclared :: [PackageName] -> [PackageName] -> PrepareM ()
349warnUndeclared declared actual =
350 traverse_ (warn . UndeclaredDependency) (actual \\ declared)
353 [(PackageName, DependencyStatus)] -> PrepareM [(PackageName, Version)]
355 let (missing, noVersion, installed) = partitionDeps deps
358 userError (MissingDependencies (x :| xs))
360 traverse_ (warn . NoResolvedVersion) noVersion
361 withVersions <- catMaybes <$> traverse tryExtractVersion' installed
362 filterM (liftIO . isPureScript . bowerDir . fst) withVersions
365 partitionDeps = foldr go ([], [], [])
366 go (pkgName, d) (ms, os, is) =
368 Missing -> (pkgName : ms, os, is)
369 NoResolution -> (ms, pkgName : os, is)
370 ResolvedOther _ -> (ms, pkgName : os, is)
371 ResolvedVersion v -> (ms, os, (pkgName, v) : is)
373 bowerDir pkgName = "bower_components/" ++ runPackageName pkgName
375 -- Try to extract a version, and warn if unsuccessful.
376 tryExtractVersion' pair =
377 maybe (warn (UnacceptableVersion pair) >> return Nothing)
379 (tryExtractVersion pair)
381tryExtractVersion :: (PackageName, String) -> Maybe (PackageName, Version)
382tryExtractVersion (pkgName, tag) =
383 let tag' = fromMaybe tag (stripPrefix "v" tag)
384 in (pkgName,) <$> D.parseVersion' tag'
386-- | Returns whether it looks like there is a purescript package checked out
387-- in the given directory.
388isPureScript :: FilePath -> IO Bool
390 files <- Glob.globDir1 purescriptSourceFiles dir
391 return (not (null files))
393getInputAndDepsFiles :: IO ([FilePath], [(PackageName, FilePath)])
394getInputAndDepsFiles = do
395 inputFiles <- globRelative purescriptSourceFiles
396 depsFiles' <- globRelative purescriptDepsFiles
397 return (inputFiles, mapMaybe withPackageName depsFiles')
399withPackageName :: FilePath -> Maybe (PackageName, FilePath)
400withPackageName fp = (,fp) <$> getPackageName fp
402getPackageName :: FilePath -> Maybe PackageName
403getPackageName fp = do
404 let xs = splitOn [pathSeparator] fp
405 ys <- stripPrefix ["bower_components"] xs
407 case Bower.mkPackageName y of
408 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