@@ -20,12 +20,21 @@ module Stack.Setup
2020 , SetupOpts (.. )
2121 , defaultSetupInfoYaml
2222 , removeHaskellEnvVars
23+
24+ -- * Stack binary download
25+ , StackReleaseInfo
26+ , getDownloadVersion
27+ , stackVersion
28+ , preferredPlatforms
29+ , downloadStackReleaseInfo
30+ , downloadStackExe
2331 ) where
2432
33+ import qualified Codec.Archive.Tar as Tar
2534import Control.Applicative
2635import Control.Concurrent.Async.Lifted (Concurrently (.. ))
2736import Control.Exception.Safe (catchIO , tryAny )
28- import Control.Monad (liftM , when , join , void , unless )
37+ import Control.Monad (liftM , when , join , void , unless , guard )
2938import Control.Monad.Catch
3039import Control.Monad.IO.Class (MonadIO , liftIO )
3140import Control.Monad.Logger
@@ -38,11 +47,14 @@ import qualified Data.ByteString as S
3847import qualified Data.ByteString.Char8 as S8
3948import qualified Data.ByteString.Lazy as LBS
4049import Data.Char (isSpace )
41- import Data.Conduit (Conduit , (=$) , await , yield , awaitForever )
50+ import Data.Conduit (Conduit , (=$) , await , yield , awaitForever , (.|) )
51+ import Data.Conduit.Lazy (lazyConsume )
4252import Data.Conduit.Lift (evalStateC )
4353import qualified Data.Conduit.List as CL
54+ import Data.Conduit.Zlib (ungzip )
4455import Data.Either
4556import Data.Foldable hiding (concatMap , or , maximum )
57+ import qualified Data.HashMap.Strict as HashMap
4658import Data.IORef
4759import Data.IORef.RunOnce (runOnce )
4860import Data.List hiding (concat , elem , maximumBy , any )
@@ -65,9 +77,8 @@ import Distribution.System (OS (Linux), Arch (..), Platform (..))
6577import qualified Distribution.System as Cabal
6678import Distribution.Text (simpleParse )
6779import Lens.Micro (set )
68- import Network.HTTP.Client (parseUrlThrow )
69- import Network.HTTP.Simple (getResponseBody , httpLBS )
70- import Network.HTTP.Download.Verified
80+ import Network.HTTP.Simple (getResponseBody , httpLBS , withResponse , getResponseStatusCode )
81+ import Network.HTTP.Download
7182import Path
7283import Path.Extra (toFilePathNoTrailingSep )
7384import Path.IO hiding (findExecutable )
@@ -103,6 +114,10 @@ import System.Process.Read
103114import System.Process.Run (runCmd , Cmd (.. ))
104115import Text.Printf (printf )
105116
117+ #if !WINDOWS
118+ import System.Posix.Files (setFileMode )
119+ #endif
120+
106121-- | Default location of the stack-setup.yaml file
107122defaultSetupInfoYaml :: String
108123defaultSetupInfoYaml =
@@ -186,10 +201,10 @@ instance Show SetupException where
186201 " stack does not yet support using --ghc-variant with GHCJS"
187202 show GHCJSNotBooted =
188203 " GHCJS does not yet have its boot packages installed. Use \" stack setup\" to attempt to run ghcjs-boot."
189- show (DockerStackExeNotFound stackVersion osKey) = concat
204+ show (DockerStackExeNotFound stackVersion' osKey) = concat
190205 [ stackProgName
191206 , " -"
192- , versionString stackVersion
207+ , versionString stackVersion'
193208 , " executable not found for "
194209 , T. unpack osKey
195210 , " \n Use the '"
@@ -593,42 +608,16 @@ ensureDockerStackExe containerPlatform = do
593608 config <- asks getConfig
594609 containerPlatformDir <- runReaderT platformOnlyRelDir (containerPlatform,PlatformVariantNone )
595610 let programsPath = configLocalProgramsBase config </> containerPlatformDir
596- stackVersion = fromCabalVersion Meta. version
597611 tool = Tool (PackageIdentifier $ (mkPackageName " stack" ) stackVersion)
598- stackExePath <- (</> $ (mkRelFile " stack" )) <$> installDir programsPath tool
612+ stackExeDir <- installDir programsPath tool
613+ let stackExePath = stackExeDir </> $ (mkRelFile " stack" )
599614 stackExeExists <- doesFileExist stackExePath
600- unless stackExeExists $
601- do
602- $ logInfo $ mconcat [" Downloading Docker-compatible " , T. pack stackProgName, " executable" ]
603- si <- getSetupInfo defaultSetupInfoYaml
604- osKey <- getOSKey containerPlatform
605- info <-
606- case Map. lookup osKey (siStack si) of
607- Just versions ->
608- case Map. lookup stackVersion versions of
609- Just x -> return x
610- Nothing ->
611- case mapMaybe (matchMinor stackVersion) (Map. keys versions) of
612- (v: _) ->
613- case Map. lookup v versions of
614- Just x -> return x
615- Nothing -> throwM (DockerStackExeNotFound v osKey)
616- [] -> throwM (DockerStackExeNotFound stackVersion osKey)
617- Nothing -> throwM (DockerStackExeNotFound stackVersion osKey)
618- _ <-
619- downloadAndInstallTool
620- programsPath
621- si
622- info
623- tool
624- installDockerStackExe
625- return ()
615+ unless stackExeExists $ do
616+ $ logInfo $ mconcat [" Downloading Docker-compatible " , T. pack stackProgName, " executable" ]
617+ sri <- downloadStackReleaseInfo Nothing Nothing (Just (versionString stackVersion))
618+ let platforms = preferredPlatforms (containerPlatform, PlatformVariantNone )
619+ downloadStackExe platforms sri stackExeDir (const $ return () )
626620 return stackExePath
627- where
628- matchMinor stackVersion v =
629- if checkVersion MatchMinor stackVersion v
630- then Just v
631- else Nothing
632621
633622-- | Install the newest version of Cabal globally
634623upgradeCabal :: (StackM env m , HasConfig env , HasGHCVariant env )
@@ -1093,30 +1082,6 @@ installGHCJS si archiveFile archiveType _tempDir destDir = do
10931082 copyFile optionsFile dest
10941083 $ logStickyDone " Installed GHCJS."
10951084
1096- -- Install the downloaded stack binary distribution
1097- installDockerStackExe
1098- :: (StackM env m , HasConfig env )
1099- => SetupInfo
1100- -> Path Abs File
1101- -> ArchiveType
1102- -> Path Abs Dir
1103- -> Path Abs Dir
1104- -> m ()
1105- installDockerStackExe _ archiveFile _ tempDir destDir = do
1106- (_,tarTool) <-
1107- checkDependencies $
1108- (,) <$> checkDependency " gzip" <*> checkDependency " tar"
1109- menv <- getMinimalEnvOverride
1110- readProcessNull
1111- (Just tempDir)
1112- menv
1113- tarTool
1114- [" xf" , toFilePath archiveFile, " --strip-components" , " 1" ]
1115- ensureDir destDir
1116- renameFile
1117- (tempDir </> $ (mkRelFile stackProgName))
1118- (destDir </> $ (mkRelFile stackProgName))
1119-
11201085ensureGhcjsBooted :: (StackM env m , HasConfig env )
11211086 => EnvOverride -> CompilerVersion -> Bool -> m ()
11221087ensureGhcjsBooted menv cv shouldBoot = do
@@ -1675,3 +1640,166 @@ getUtf8EnvVars menv compilerVer =
16751640 fallbackPrefixes = [" C." , " en_US." , " en_" ]
16761641 -- Suffixes of UTF-8 locales (case-insensitive)
16771642 utf8Suffixes = [" .UTF-8" , " .utf8" ]
1643+
1644+ -- Binary Stack upgrades
1645+
1646+ newtype StackReleaseInfo = StackReleaseInfo Value
1647+
1648+ downloadStackReleaseInfo :: MonadIO m
1649+ => Maybe String -- Github org
1650+ -> Maybe String -- Github repo
1651+ -> Maybe String -- ^ optional version
1652+ -> m StackReleaseInfo
1653+ downloadStackReleaseInfo morg mrepo mver = liftIO $ do
1654+ let org = fromMaybe " commercialhaskell" morg
1655+ repo = fromMaybe " stack" mrepo
1656+ let url = concat
1657+ [ " https://api.github.com/repos/"
1658+ , org
1659+ , " /"
1660+ , repo
1661+ , " /releases/"
1662+ , case mver of
1663+ Nothing -> " latest"
1664+ Just ver -> " tags/v" ++ ver
1665+ ]
1666+ req <- parseRequest url
1667+ res <- httpJSON $ setGithubHeaders req
1668+ let code = getResponseStatusCode res
1669+ if code >= 200 && code < 300
1670+ then return $ StackReleaseInfo $ getResponseBody res
1671+ else error $ " Could not get release information for Stack from: " ++ url
1672+
1673+ preferredPlatforms :: (MonadReader env m , HasPlatform env )
1674+ => m [(Bool , String )]
1675+ preferredPlatforms = do
1676+ Platform arch' os' <- asks getPlatform
1677+ (isWindows, os) <-
1678+ case os' of
1679+ Cabal. Linux -> return (False , " linux" )
1680+ Cabal. Windows -> return (True , " windows" )
1681+ Cabal. OSX -> return (False , " osx" )
1682+ Cabal. FreeBSD -> return (False , " freebsd" )
1683+ _ -> error $ " Binary upgrade not yet supported on OS: " ++ show os'
1684+ arch <-
1685+ case arch' of
1686+ I386 -> return " i386"
1687+ X86_64 -> return " x86_64"
1688+ Arm -> return " arm"
1689+ _ -> error $ " Binary upgrade not yet supported on arch: " ++ show arch'
1690+ hasgmp4 <- return False -- FIXME import relevant code from Stack.Setup? checkLib $(mkRelFile "libgmp.so.3")
1691+ let suffixes
1692+ | hasgmp4 = [" -static" , " -gmp4" , " " ]
1693+ | otherwise = [" -static" , " " ]
1694+ return $ map (\ suffix -> (isWindows, concat [os, " -" , arch, suffix])) suffixes
1695+
1696+ downloadStackExe
1697+ :: (MonadIO m , MonadLogger m , MonadReader env m , HasConfig env )
1698+ => [(Bool , String )] -- ^ acceptable platforms
1699+ -> StackReleaseInfo
1700+ -> Path Abs Dir -- ^ destination directory
1701+ -> (Path Abs File -> IO () ) -- ^ test the temp exe before renaming
1702+ -> m ()
1703+ downloadStackExe platforms0 archiveInfo destDir testExe = do
1704+ (isWindows, archiveURL) <-
1705+ let loop [] = error $ " Unable to find binary Stack archive for platforms: "
1706+ ++ unwords (map snd platforms0)
1707+ loop ((isWindows, p'): ps) = do
1708+ let p = T. pack p'
1709+ $ logInfo $ " Querying for archive location for platform: " <> p
1710+ case findArchive archiveInfo p of
1711+ Just x -> return (isWindows, x)
1712+ Nothing -> loop ps
1713+ in loop platforms0
1714+
1715+ let (destFile, tmpFile)
1716+ | isWindows =
1717+ ( destDir </> $ (mkRelFile " stack.exe" )
1718+ , destDir </> $ (mkRelFile " stack.tmp.exe" )
1719+ )
1720+ | otherwise =
1721+ ( destDir </> $ (mkRelFile " stack" )
1722+ , destDir </> $ (mkRelFile " stack.tmp" )
1723+ )
1724+
1725+ $ logInfo $ " Downloading from: " <> archiveURL
1726+
1727+ liftIO $ do
1728+ case () of
1729+ ()
1730+ | " .tar.gz" `T.isSuffixOf` archiveURL -> handleTarball tmpFile isWindows archiveURL
1731+ | " .zip" `T.isSuffixOf` archiveURL -> error " FIXME: Handle zip files"
1732+ | otherwise -> error $ " Unknown archive format for Stack archive: " ++ T. unpack archiveURL
1733+
1734+ $ logInfo " Download complete, testing executable"
1735+
1736+ liftIO $ do
1737+ #if !WINDOWS
1738+ setFileMode (toFilePath tmpFile) 0o755
1739+ #endif
1740+
1741+ testExe tmpFile
1742+
1743+ renameFile tmpFile destFile
1744+
1745+ $ logInfo $ T. pack $ " New stack executable available at " ++ toFilePath destFile
1746+ where
1747+
1748+ findArchive (StackReleaseInfo val) pattern = do
1749+ Object top <- return val
1750+ Array assets <- HashMap. lookup " assets" top
1751+ getFirst $ fold $ fmap (First . findMatch pattern') assets
1752+ where
1753+ pattern' = mconcat [" -" , pattern , " ." ]
1754+
1755+ findMatch pattern'' (Object o) = do
1756+ String name <- HashMap. lookup " name" o
1757+ guard $ not $ " .asc" `T.isSuffixOf` name
1758+ guard $ pattern'' `T.isInfixOf` name
1759+ String url <- HashMap. lookup " browser_download_url" o
1760+ Just url
1761+ findMatch _ _ = Nothing
1762+
1763+ handleTarball :: Path Abs File -> Bool -> T. Text -> IO ()
1764+ handleTarball tmpFile isWindows url = do
1765+ req <- fmap setGithubHeaders $ parseUrlThrow $ T. unpack url
1766+ withResponse req $ \ res -> do
1767+ entries <- fmap (Tar. read . LBS. fromChunks)
1768+ $ lazyConsume
1769+ $ getResponseBody res .| ungzip
1770+ let loop Tar. Done = error $ concat
1771+ [ " Stack executable "
1772+ , show exeName
1773+ , " not found in archive from "
1774+ , T. unpack url
1775+ ]
1776+ loop (Tar. Fail e) = throwM e
1777+ loop (Tar. Next e es)
1778+ | Tar. entryPath e == exeName =
1779+ case Tar. entryContent e of
1780+ Tar. NormalFile lbs _ -> do
1781+ ensureDir destDir
1782+ LBS. writeFile (toFilePath tmpFile) lbs
1783+ _ -> error $ concat
1784+ [ " Invalid file type for tar entry named "
1785+ , exeName
1786+ , " downloaded from "
1787+ , T. unpack url
1788+ ]
1789+ | otherwise = loop es
1790+ loop entries
1791+ where
1792+ -- The takeBaseName drops the .gz, dropExtension drops the .tar
1793+ exeName =
1794+ let base = FP. dropExtension (FP. takeBaseName (T. unpack url)) FP. </> " stack"
1795+ in if isWindows then base FP. <.> " exe" else base
1796+
1797+ getDownloadVersion :: StackReleaseInfo -> Maybe Version
1798+ getDownloadVersion (StackReleaseInfo val) = do
1799+ Object o <- Just val
1800+ String rawName <- HashMap. lookup " name" o
1801+ -- drop the "v" at the beginning of the name
1802+ parseVersion $ T. drop 1 rawName
1803+
1804+ stackVersion :: Version
1805+ stackVersion = fromCabalVersion Meta. version
0 commit comments