|
| 1 | +{- stack script |
| 2 | + --resolver lts-22.7 |
| 3 | + --ghc-options -Wall |
| 4 | +-} |
| 5 | + |
| 6 | +-- As no packages are specified in the `stack script` command in the Stack |
| 7 | +-- interpreter options comment, Stack deduces the required packages from the |
| 8 | +-- module imports, being: Cabal, base, bytestring, directory, extra, process, |
| 9 | +-- shake, tar, zip-archive and zlib. These are either GHC boot packages or in |
| 10 | +-- the snapshot. Stackage LTS Haskell 22.0 does not include boot packages |
| 11 | +-- directly. As GHC 9.6.4 boot packages Cabal and Cabal-syntax expose modules |
| 12 | +-- with the same names, the language extension PackageImports is required. |
| 13 | + |
| 14 | +-- EXPERIMENTAL |
| 15 | + |
| 16 | +-- This corresponds to release.hs but is intended to be run only on |
| 17 | +-- macOS/AArch64 in order to create a statically-linked Linux/AArch64 version of |
| 18 | +-- Stack: |
| 19 | +-- |
| 20 | +-- Install pre-requisites: |
| 21 | +-- |
| 22 | +-- > brew install docker |
| 23 | +-- > brew install colima |
| 24 | +-- |
| 25 | +-- Start colima and run script: |
| 26 | +-- |
| 27 | +-- > colima start |
| 28 | +-- > release-linux-aarch64.hs build --alpine --build-args --docker-stack-exe=image |
| 29 | +-- |
| 30 | +-- Could be incorporated into release.hs, in due course. |
| 31 | + |
| 32 | +{-# LANGUAGE OverloadedRecordDot #-} |
| 33 | +{-# LANGUAGE PackageImports #-} |
| 34 | +{-# LANGUAGE PatternSynonyms #-} |
| 35 | + |
| 36 | +import qualified Codec.Archive.Tar as Tar |
| 37 | +import qualified Codec.Archive.Tar.Entry as TarEntry |
| 38 | +import qualified Codec.Compression.GZip as GZip |
| 39 | +import Control.Exception ( tryJust ) |
| 40 | +import Control.Monad ( guard ) |
| 41 | +import qualified Data.ByteString.Lazy.Char8 as L8 |
| 42 | +import Data.List.Extra ( stripPrefix ) |
| 43 | +import Development.Shake |
| 44 | + ( Action, Change (..), pattern Chatty, Rules |
| 45 | + , ShakeOptions (..), (%>), actionOnException, alwaysRerun |
| 46 | + , cmd, copyFileChanged, getDirectoryFiles, liftIO, need |
| 47 | + , phony, shakeArgsWith, shakeOptions, want |
| 48 | + ) |
| 49 | +import Development.Shake.FilePath |
| 50 | + ( (<.>), (</>), exe, takeDirectory, toStandard ) |
| 51 | +import "Cabal" Distribution.PackageDescription |
| 52 | + ( PackageDescription (..), packageDescription, pkgVersion |
| 53 | + ) |
| 54 | +import Distribution.Simple.PackageDescription |
| 55 | + ( readGenericPackageDescription ) |
| 56 | +import "Cabal" Distribution.System |
| 57 | + ( Arch, OS (..), Platform (..), buildPlatform ) |
| 58 | +import "Cabal" Distribution.Text ( display ) |
| 59 | +import Distribution.Verbosity ( silent ) |
| 60 | +import System.Console.GetOpt ( ArgDescr (..), OptDescr (..) ) |
| 61 | +import System.Directory ( removeFile ) |
| 62 | +import System.IO.Error ( isDoesNotExistError ) |
| 63 | +import System.Process ( readProcess ) |
| 64 | + |
| 65 | +-- | Entrypoint. |
| 66 | +main :: IO () |
| 67 | +main = shakeArgsWith |
| 68 | + shakeOptions { shakeFiles = releaseDir |
| 69 | + , shakeVerbosity = Chatty |
| 70 | + , shakeChange = ChangeModtimeAndDigestInput |
| 71 | + } |
| 72 | + options $ |
| 73 | + \flags args -> do |
| 74 | + -- build the default value of type Global, with predefined constants |
| 75 | + |
| 76 | + -- 'stack build --dry-run' just ensures that 'stack.cabal' is generated from |
| 77 | + -- 'package.yaml'. |
| 78 | + _ <- readProcess "stack" ["build", "--dry-run"] "" |
| 79 | + gStackPackageDescription <- |
| 80 | + packageDescription <$> readGenericPackageDescription silent "stack.cabal" |
| 81 | + |
| 82 | + let Platform arch _ = buildPlatform |
| 83 | + gArch = arch |
| 84 | + gBuildArgs = ["--flag", "stack:-developer-mode"] |
| 85 | + global = foldl |
| 86 | + (flip id) |
| 87 | + Global |
| 88 | + { gStackPackageDescription |
| 89 | + , gArch |
| 90 | + , gBuildArgs |
| 91 | + } |
| 92 | + flags |
| 93 | + |
| 94 | + pure $ Just $ rules global args |
| 95 | + |
| 96 | +-- | Additional command-line options. |
| 97 | +options :: [OptDescr (Either String (Global -> Global))] |
| 98 | +options = |
| 99 | + [ Option "" [alpineOptName] |
| 100 | + ( NoArg $ Right $ \g -> |
| 101 | + g { gBuildArgs = |
| 102 | + gBuildArgs g |
| 103 | + ++ [ "--flag=stack:static" |
| 104 | + , "--docker" |
| 105 | + , "--system-ghc" |
| 106 | + , "--no-install-ghc" |
| 107 | + ] |
| 108 | + } |
| 109 | + ) |
| 110 | + "Build a statically linked binary using an Alpine Docker image." |
| 111 | + , Option "" [buildArgsOptName] |
| 112 | + ( ReqArg |
| 113 | + (\v -> Right $ \g -> g{gBuildArgs = gBuildArgs g ++ words v}) |
| 114 | + "\"ARG1 ARG2 ...\"" |
| 115 | + ) |
| 116 | + "Additional arguments to pass to 'stack build'." |
| 117 | + ] |
| 118 | + |
| 119 | +-- | Shake rules. |
| 120 | +rules :: Global -> [String] -> Rules () |
| 121 | +rules global args = do |
| 122 | + case args of |
| 123 | + [] -> error "No wanted target(s) specified." |
| 124 | + _ -> want args |
| 125 | + |
| 126 | + phony buildPhony $ |
| 127 | + mapM_ (\f -> need [releaseDir </> f]) binaryPkgFileNames |
| 128 | + |
| 129 | + releaseDir </> binaryPkgTarGzFileName %> \out -> do |
| 130 | + stageFiles <- getBinaryPkgStageFiles |
| 131 | + writeTarGz id out releaseStageDir stageFiles |
| 132 | + |
| 133 | + releaseStageDir </> binaryName </> stackExeFileName %> \out -> do |
| 134 | + copyFileChanged (releaseDir </> binaryExeFileName) out |
| 135 | + |
| 136 | + releaseStageDir </> (binaryName ++ "//*") %> \out -> do |
| 137 | + copyFileChanged |
| 138 | + (dropDirectoryPrefix (releaseStageDir </> binaryName) out) |
| 139 | + out |
| 140 | + |
| 141 | + releaseDir </> binaryExeFileName %> \out -> do |
| 142 | + need [releaseBinDir </> binaryName </> stackExeFileName] |
| 143 | + case platformOS of |
| 144 | + OSX -> |
| 145 | + cmd "strip -o" |
| 146 | + [out, releaseBinDir </> binaryName </> stackExeFileName] |
| 147 | + _ -> undefined |
| 148 | + |
| 149 | + releaseBinDir </> binaryName </> stackExeFileName %> \out -> do |
| 150 | + alwaysRerun |
| 151 | + actionOnException |
| 152 | + ( cmd stackProgName |
| 153 | + (stackArgs global) |
| 154 | + ["--local-bin-path=" ++ takeDirectory out] |
| 155 | + "install" |
| 156 | + global.gBuildArgs |
| 157 | + integrationTestFlagArgs |
| 158 | + "--pedantic" |
| 159 | + "stack" |
| 160 | + ) |
| 161 | + (tryJust (guard . isDoesNotExistError) (removeFile out)) |
| 162 | + |
| 163 | + where |
| 164 | + integrationTestFlagArgs = |
| 165 | + -- Explicitly enabling 'hide-dependency-versions' and 'supported-build' to |
| 166 | + -- work around https://github.com/commercialhaskell/stack/issues/4960 |
| 167 | + [ "--flag=stack:hide-dependency-versions" |
| 168 | + , "--flag=stack:supported-build" |
| 169 | + ] |
| 170 | + |
| 171 | + getBinaryPkgStageFiles = do |
| 172 | + docFiles <- getDocFiles |
| 173 | + let stageFiles = concat |
| 174 | + [ [releaseStageDir </> binaryName </> stackExeFileName] |
| 175 | + , map ((releaseStageDir </> binaryName) </>) docFiles |
| 176 | + ] |
| 177 | + need stageFiles |
| 178 | + pure stageFiles |
| 179 | + |
| 180 | + getDocFiles = getDirectoryFiles "." ["LICENSE", "*.md", "doc//*.md"] |
| 181 | + |
| 182 | + buildPhony = "build" |
| 183 | + |
| 184 | + releaseStageDir = releaseDir </> "stage" |
| 185 | + releaseBinDir = releaseDir </> "bin" |
| 186 | + |
| 187 | + binaryPkgFileNames = |
| 188 | + case platformOS of |
| 189 | + OSX -> [binaryExeFileName, binaryPkgTarGzFileName] |
| 190 | + _ -> undefined |
| 191 | + binaryPkgTarGzFileName = binaryName <.> tarGzExt |
| 192 | + binaryExeFileName = binaryName ++ "-bin" <.> exe |
| 193 | + binaryName = concat |
| 194 | + [ stackProgName |
| 195 | + , "-" |
| 196 | + , stackVersionStr global |
| 197 | + , "-" |
| 198 | + , display targetPlatformOS |
| 199 | + , "-" |
| 200 | + , display global.gArch |
| 201 | + ] |
| 202 | + stackExeFileName = stackProgName <.> exe |
| 203 | + |
| 204 | + tarGzExt = tarExt <.> gzExt |
| 205 | + gzExt = ".gz" |
| 206 | + tarExt = ".tar" |
| 207 | + |
| 208 | +-- | Create a .tar.gz files from files. The paths should be absolute, and will |
| 209 | +-- be made relative to the base directory in the tarball. |
| 210 | +writeTarGz :: |
| 211 | + (FilePath -> FilePath) |
| 212 | + -> FilePath |
| 213 | + -> FilePath |
| 214 | + -> [FilePath] |
| 215 | + -> Action () |
| 216 | +writeTarGz fixPath out baseDir inputFiles = liftIO $ do |
| 217 | + content <- Tar.pack baseDir $ map (dropDirectoryPrefix baseDir) inputFiles |
| 218 | + L8.writeFile out $ GZip.compress $ Tar.write $ map fixPath' content |
| 219 | + where |
| 220 | + fixPath' :: Tar.Entry -> Tar.Entry |
| 221 | + fixPath' entry = |
| 222 | + case TarEntry.toTarPath isDir $ fixPath $ TarEntry.entryPath entry of |
| 223 | + Left e -> error $ show (Tar.entryPath entry, e) |
| 224 | + Right tarPath -> entry { TarEntry.entryTarPath = tarPath } |
| 225 | + where |
| 226 | + isDir = |
| 227 | + case TarEntry.entryContent entry of |
| 228 | + TarEntry.Directory -> True |
| 229 | + _ -> False |
| 230 | + |
| 231 | +-- | Drops a directory prefix from a path. The prefix automatically has a path |
| 232 | +-- separator character appended. Fails if the path does not begin with the |
| 233 | +-- prefix. |
| 234 | +dropDirectoryPrefix :: FilePath -> FilePath -> FilePath |
| 235 | +dropDirectoryPrefix prefix path = |
| 236 | + case stripPrefix (toStandard prefix ++ "/") (toStandard path) of |
| 237 | + Nothing -> error |
| 238 | + ( "dropDirectoryPrefix: cannot drop " |
| 239 | + ++ show prefix |
| 240 | + ++ " from " |
| 241 | + ++ show path |
| 242 | + ) |
| 243 | + Just stripped -> stripped |
| 244 | + |
| 245 | +-- | String representation of Stack package version. |
| 246 | +stackVersionStr :: Global -> String |
| 247 | +stackVersionStr = |
| 248 | + display . pkgVersion . package . gStackPackageDescription |
| 249 | + |
| 250 | +-- | Current operating system. |
| 251 | +platformOS :: OS |
| 252 | +platformOS = |
| 253 | + let Platform _ os = buildPlatform |
| 254 | + in os |
| 255 | + |
| 256 | +-- | Target operating system |
| 257 | +targetPlatformOS :: OS |
| 258 | +targetPlatformOS = Linux |
| 259 | + |
| 260 | +-- | Directory in which to store build and intermediate files. |
| 261 | +releaseDir :: FilePath |
| 262 | +releaseDir = "_release" |
| 263 | + |
| 264 | +-- | @--build-args@ command-line option name. |
| 265 | +buildArgsOptName :: String |
| 266 | +buildArgsOptName = "build-args" |
| 267 | + |
| 268 | +-- | @--alpine@ command-line option name. |
| 269 | +alpineOptName :: String |
| 270 | +alpineOptName = "alpine" |
| 271 | + |
| 272 | +-- | Arguments to pass to all 'stack' invocations. |
| 273 | +stackArgs :: Global -> [String] |
| 274 | +stackArgs global = [ "--arch=" ++ display global.gArch |
| 275 | + , "--interleaved-output" |
| 276 | + ] |
| 277 | + |
| 278 | +-- | Name of the 'stack' program. |
| 279 | +stackProgName :: FilePath |
| 280 | +stackProgName = "stack" |
| 281 | + |
| 282 | +-- | Global values and options. |
| 283 | +data Global = Global |
| 284 | + { gStackPackageDescription :: !PackageDescription |
| 285 | + , gArch :: !Arch |
| 286 | + , gBuildArgs :: [String] |
| 287 | + } |
| 288 | + deriving Show |
0 commit comments