<NME> Boot.hs
<BEF> {-# OPTIONS_GHC -O0 #-}

{- |
    The ghcjs-boot program installs the libraries and runtime system for GHCJS

    You can explicitly set the boot source location with the -s option. It
    can either be a directory containing the boot.yaml file or a boot.tar
    archive.

    if the -s option is not set, the following locations are tried in order:

      1. the current directory if it contains boot.yaml
      2. the boot.tar file installed by cabal install in the GHCJS
         data directory

    ghcjs-boot installs the libraries into the GHCJS library directory,
    which is set by the `ghcjs' and `ghcjs-pkg' scripts, which pass the
    library path with the -B option to the underlying binary executable.

    modify the scripts to change the installation location
 -}

{-# LANGUAGE ExtendedDefaultRules,
             OverloadedStrings,
             ScopedTypeVariables,
             TemplateHaskell,
             LambdaCase,
             FlexibleInstances,
             DeriveDataTypeable,
             GeneralizedNewtypeDeriving,
             NoMonomorphismRestriction,
             FlexibleContexts,
             RankNTypes,
             TupleSections
  #-}
module Main (main) where

import           Prelude
  hiding (elem, mapM, mapM_, any, all, concat, concatMap)

-- import qualified Prelude

import qualified Distribution.Simple.Utils       as Cabal
import qualified Distribution.Verbosity          as Cabal


import qualified Codec.Archive.Tar               as Tar
import qualified Codec.Archive.Tar.Entry         as Tar

import           Control.Applicative
import qualified Control.Exception               as Ex
import           Control.Lens                    hiding ((<.>))
import           Control.Monad.Reader            (MonadReader, ReaderT(..), ask )
import           Control.Monad.State

import qualified Data.Aeson                      as Aeson
import qualified Data.ByteString                 as B
import qualified Data.ByteString.Lazy            as BL
                                 , noBuild  :: Bool  -- ^ do not build, only reset the packages
                                 , initTree :: Bool  -- ^ initialize the source tree: configure/build native ghc first
                                 , onlyBasic :: Bool -- ^ only build basic packages: ghc-prim, integer-gmp, base
                                 } deriving (Ord, Eq)

bootSettings :: [String] -> BootSettings
import qualified Data.Text                       as T
                                 (any (=="--nobuild") args)
                                 (any (=="--init") args)
                                 (any (=="--only-basic") args)

main = withSocketsDo $ do
  tmp <- getTemporaryDirectory
import           Options.Applicative             hiding (info)
import qualified Options.Applicative             as O

import           System.Directory
import           System.Environment              (lookupEnv, setEnv)
import           System.Environment.Executable   (getExecutablePath)
import           System.Exit
  (exitSuccess, exitFailure, ExitCode(..))
import qualified System.FilePath                 as FP
import           System.FilePath --  hiding ((<.>), (</>))

import           System.IO
  (hPutStrLn, hSetBuffering, stderr, stdout, BufferMode(..), hClose)
import           System.PosixCompat.Files        (setFileMode)
import           System.Process                  (readCreateProcess, createProcess, waitForProcess, {-CreateProcess(..),-} proc)

-- import           Shelly                          ((<.>), fromText)
-- import qualified Shelly                          as Sh

import           Compiler.GhcjsProgram           (printVersion)
import qualified Compiler.Info                   as Info
import qualified Compiler.Utils                  as Utils
import           Compiler.Settings               (NodeSettings(..))

-- import qualified Paths_ghcjs                     as Paths

import           System.IO.Error                 (isDoesNotExistError)
import           Compiler.Platform               (isWindows)

-- fixme force overwrite?

default (Text)

newtype Verbosity = Verbosity Int deriving (Eq, Ord, Data, Typeable)

trace, info, warn, err :: Verbosity
trace = Verbosity 3
info  = Verbosity 2
warn  = Verbosity 1
err   = Verbosity 0

data BootSettings = BootSettings
  { _bsShowVersion  :: Bool       -- ^show the version and exit
  , _bsDebug        :: Bool       {- ^build debug version of the libraries
                                      (GHCJS records the STG in the object
                                      files for easier inspection) -}
  , _bsProf         :: Bool       -- ^build profiling version of the libraries
  , _bsHaddock      :: Bool       -- ^build documentation
  , _bsVerbosity    :: Verbosity  -- ^verbosity level 0..3, 2 is default
  , _bsWithCabal    :: Maybe Text {- ^location of cabal (cabal-install)
                                      executable, must have GHCJS support -}
  -- , _bsBindir       :: Maybe Text -- ^ bin directory to install GHCJS programs
  -- , _bsLibexecDir :: Maybe Text -- ^ location of GHCJS executables
  -- , _bsGhcjsLibdir  :: Maybe Text -- ^ location to install GHCJS boot libraries
  , _bsWithEmsdk    :: Maybe Text
  , _bsWithGhc      :: Maybe Text {- ^location of GHC compiler (must have a
                                      GHCJS-compatible Cabal library
                                      installed. ghcjs-boot copies some files
                                      from this compiler) -}
  , _bsWithNode     :: Maybe Text -- ^location of the node.js program
  , _bsWithNodePath :: Maybe Text -- ^ NODE_PATH to use when running node.js Template Haskell or REPL
                                  --      (if unspecified, GHCJS uses bundled packages)
  , _bsNodeExtraArgs :: Maybe Text -- ^ extra node arguments
  , _bsSourceDir    :: Maybe Text -- ^source directory (can be a tar file)
  } deriving (Ord, Eq, Data, Typeable)
  case (mghcjs, mghcjspkg) of
    (Just ghcjs, Just ghcjspkg) -> do
      echo $ "Booting: " <> toTextIgnore ghcjs <> " (" <> toTextIgnore ghcjspkg <> ")"
      ghcjsVer <- T.strip <$> run ghcjs ["--numeric-version"]
      when (not $ noBuild settings) $ do
        addWrappers ghcjs ghcjspkg
      initPackageDB
      let corePkgs = corePackages (onlyBasic settings)
      when (not (skipRts settings || noBuild settings)) $ do
           echo "Building RTS"
           run_ "make" ["all_rts","-j4"] `catchany_sh` (const (return ()))
      replacePrimOps
      when (not $ noBuild settings) $ forM_ corePkgs $ \pkg -> do
        echo $ "Building package: " <> pkg
        buildPkg pkg
      installRts
      mapM_ (installPkg ghcjs ghcjspkg) corePkgs
      installFakes
    _ -> echo "Error: ghcjs and ghcjs-pkg must be in the PATH"

-- add inplace/bin/ghcjs and inplace/bin/ghcjs-pkg wrappers
addWrappers :: FilePath -> FilePath -> ShIO ()
addWrappers ghcjs ghcjspkg = do
    ghcwrapper <- readfile "inplace/bin/ghc-stage1"
    writefile "inplace/bin/ghcjs" (fixGhcWrapper ghcwrapper ghcjs)
    makeExecutable "inplace/bin/ghcjs"
    pkgwrapper <- readfile "inplace/bin/ghc-pkg"
    writefile "inplace/bin/ghcjs-pkg" (fixPkgWrapper pkgwrapper ghcjspkg)
    setForeignEncoding utf8
    env <- initBootEnv settings
    printBootEnvSummary False env
        p <- getPermissions f
        setPermissions f (p {executable = True})

fixGhcWrapper :: Text -> FilePath -> Text
fixGhcWrapper wrapper ghcjs = T.unlines . concatMap fixLine . T.lines $ wrapper
    where
      exec = "executablename="
      fixLine line
      cleanCache
      prepareLibDir
      let base = libDir (e ^. beLocations)
              , "export GHCJS_FALLBACK_PLAIN=1"
              , "export GHCJS_NO_NATIVE=1"
              , "export GHCJS_FAKE_NATIVE=1"
              ]
          | otherwise                             = [line]

    <$> v ..: "ghc"
    <*> v ..: "cabal"
    <*> v ..: "node"
    where
      o ..: p = ((\t -> Program p t Nothing Nothing []) <$> o .: p) <|>
                (withArgs p =<< o .: p)
      withArgs :: Text -> Yaml.Value -> Yaml.Parser Program
      withArgs p (Yaml.Object o) | [(k,v)] <- HM.toList o =
        Program p k Nothing Nothing <$> Yaml.parseJSON v
      withArgs _ _ =
        mempty
  parseJSON _ = mempty

instance Yaml.FromJSON BootStages where
  parseJSON (Yaml.Object v) = BootStages
    <$> v ..: "stage1a"
    <*> v ..: "stage1b"
    <*> v  .: "cabal"
    <*> v  .: "ghcjs-prim"
    where
      o ..: p = pkgs =<< o .: p
      pkgs (Yaml.String t) =
        pure [t]
      pkgs (Yaml.Array v) =
        concat <$> mapM pkgs (V.toList v)
      pkgs __ =
        mempty
  parseJSON _ = mempty

instance Yaml.FromJSON BootConfigFile where
  parseJSON (Yaml.Object v) = BootConfigFile
    <$> v .: "packages" <*> v .: "programs"
  parseJSON _ = mempty

optParser' :: ParserInfo BootSettings
optParser' =
  O.info (helper <*> optParser)
         (fullDesc <>
          header "GHCJS booter, build base libraries for the compiler" <>
          progDesc description)

description :: String
description = unlines
  [ "ghcjs-boot builds an initial set of libraries for GHCJS."
  ]

optParser :: Parser BootSettings
optParser =
  BootSettings
    <$> switch
        (long "version" <>
        help "show the ghcjs-boot version")
    <*> switch
          (long "debug" <>
           short 'd' <>
           help "build debug libraries with extra checks")
    <*> (fmap not . switch)
          (long "no-prof" <>
           help "don't generate profiling version of the libraries")
    <*> (fmap not . switch)
          (long "no-haddock" <>
           help "don't generate documentation")
    <*> (fmap Verbosity . option auto)
          (long "verbosity" <>
           short 'v' <>
           value 2 <>
           help "verbose output")
    <*> (optional . fmap T.pack . strOption)
          (long "with-cabal" <> metavar "PROGRAM" <>
           help "cabal program to use")
    <*> (optional . fmap T.pack . strOption)
          (long "with-emsdk" <>
           metavar "DIR" <>
           help "location of Emscripten SDK")
    <*> (optional . fmap T.pack . strOption)
          (long "with-ghc" <>
           metavar "PROGRAM" <>
           help "ghc program to use")
    <*> (optional . fmap T.pack . strOption)
          (long "with-node" <>
           metavar "PROGRAM" <>
           help "node.js program to use")
    <*> (optional . fmap T.pack . strOption)
          (long "with-node-path" <> metavar "PATH" <>
           help "value of NODE_PATH environment variable when running Template Haskell or GHCJSi")
    <*> (optional . fmap T.pack . strOption)
          (long "extra-node-args" <> metavar "ARGS" <>
           help "extra arguments to pass to node.js")
    <*> (optional . fmap T.pack . strOption)
          (long "source-dir" <>
           short 's' <>
           metavar "DIR" <>
           help "location of GHCJS library sources")

initPackageDB :: B ()
initPackageDB = do
  msg info "creating package databases"
  initDB "--global" <^> beLocations . blGlobalDB
  where
    initDB dbName db = do
      rm_rf db
      ghcjs_pkg_ ["init", T.pack db] `catchAny_` return ()
      ghcjs_pkg_ ["recache", dbName]

cleanCache :: B ()
cleanCache =
  liftIO Info.getUserCacheDir >>= \case
    Just p -> rm_rf p `catchAny_` return ()
    Nothing -> return ()

prepareLibDir :: B ()
prepareLibDir = do
  msg info "preparing GHCJS library directory"
  locs <- view beLocations
  pgms <- view bePrograms
  liftIO $ print (locs, pgms)
  ver <- liftIO $ ghcjsVersion pgms
  let globalDB = locs ^. blGlobalDB
      ghcjsLib = libDir locs
      ghcjsTop = locs ^. blGhcjsTopDir
      rtsConfFile   = "rts.conf"
      wrapperSrcDir = (locs ^. blSourceDir) </> "input" </> "wrappers"
      wenv = WrapperEnv { weTopDir     = T.pack (libDir locs)
                        , weBinDir     = T.pack ((locs ^. blGhcjsTopDir) </> "bin")
                        , weLibexecDir = T.pack (libDir locs </> "bin")
                        , weVersion    = ver
                        , weGhcVersion = ver
                        , weEmsdk      = T.pack (locs ^. blEmsdk)
                        }
  rtsConf <- replacePlaceholders wenv <$>
             liftIO (T.readFile ((locs ^. blSourceDir) </> "input" </> "rts" <.> "conf"))
  mkdir_p (ghcjsLib </> "bin")

  msg info "copying GHCJS executables"

  -- copy the executables from the package to the libraries directory
  -- (the executables in the package have been prefixed with `private-ghcjs-`
  -- as a workaround for cabal-install incorrectly generating symlinks for
  -- private executables)
  let cpExe name = do
        let srcName = ghcjsTop </> "bin" </> exe (T.unpack $ "private-ghcjs-" <> name)
            tgtName = ghcjsLib </> "bin" </> exe (T.unpack name)
        cp srcName tgtName
        liftIO . Cabal.setFileExecutable $ tgtName

  mapM_ cpExe [ "unlit", "run" ]

  when isWindows (mapM_ cpExe ["touchy", "wrapper"])

  msg info "creating emscripten wrapper"

  liftIO $ copyWrapper "emcc"
                       Nothing
                       wenv
                       (ghcjsLib </> "bin")
                       wrapperSrcDir
                       (ghcjsLib </> "bin")

  msg info "adding RTS package"
  initPackageDB
  liftIO $ T.writeFile (globalDB </> rtsConfFile) rtsConf
  ghcjs_pkg_ ["recache", "--global", "--no-user-package-db"]
  cp_r ((locs ^. blSourceDir) </> "shims") (ghcjsLib </> "shims")
  cp_r ((locs ^. blSourceDir) </> "data")  ghcjsLib
  liftIO . T.writeFile (ghcjsLib </> "settings") =<< generateSettings
  -- workaround for hardcoded check of mingw dir in ghc when building on Windows
  when isWindows (mkdir_p $ ghcjsTop </> "mingw")
  prepareNodeJs
  unless isWindows $ do
    let runSh = ghcjsLib </> "run" <.> "sh"
    liftIO (T.writeFile runSh "#!/bin/sh\nCOMMAND=$1\nshift\n\"$COMMAND\" \"$@\"\n")
    liftIO (Cabal.setFileExecutable =<< makeAbsolute runSh)
  -- make an empty object file for
  subLib $ do
    liftIO (T.writeFile "empty.c" "")
    ghc_ ["-c", "empty.c"]
  msg info "RTS prepared"

generateSettings :: B Text
generateSettings = do
  emsdk <- view (beLocations . blEmsdk)
  -- do we also need emar, emranlib?
  let opts :: [(String, String)]
      opts = [("GCC extra via C opts", " -fwrapv -fno-builtin"),
              ("C compiler command", emsdk </> "upstream" </> "bin" </> "clang" {-"asmjs-none-gcc"-} {-T.unpack emccProgram-}),
              ("C compiler flags", ""),
              ("C++ compiler flags",""),
              ("C compiler link flags", " "),
              ("C compiler supports -no-pie", "NO"),
              ("Haskell CPP command", emsdk </> "upstream" </> "bin" </> "clang"),
              ("Haskell CPP flags","-E -undef -traditional -Wno-invalid-pp-token -Wno-unicode -Wno-trigraphs"),
              ("ld command", "ld"),
              ("ld flags", ""),
              ("ld supports compact unwind", "YES"),
              ("ld supports build-id", "NO"),
              ("ld supports filelist", "YES"),
              ("ld is GNU ld", "NO"),
              ("Merge objects command", "ld.gold"),
              ("Merge objects flags", "-r"),
              ("ar command", emsdk </> "upstream" </> "emscripten" </> "emar"),
              ("ar flags", "qcls"),
              ("ar supports at file", "NO"),
              ("ranlib command", emsdk </> "upstream" </> "emscripten" </> "emranlib"),
              ("otool command", "otool"),
              ("install_name_tool command", "install_name_tool"),
              ("touch command", if isWindows then "$topdir/bin/touchy.exe" else "touch"),
              ("dllwrap command", "/bin/false"),
              ("windres command", "/bin/false"),
              ("libtool command", "libtool"),
              ("unlit command", "$topdir/bin/unlit"),
              ("cross compiling", "YES"),
              ("target os", "OSUnknown"),
              ("target arch", "ArchJavaScript"),
              ("target platform string", T.unpack ghcjsTriple),
              ("Target platform", T.unpack ghcjsTriple),
              ("target word size", "4"),
              ("target has GNU nonexec stack", "NO"),
              ("target has .ident directive", "YES"),
              ("target has subsections via symbols", "YES"),
              ("target has RTS linker", "YES"),
              ("integer library", "integer-gmp"),
              ("Unregisterised", "YES"),
              ("LLVM target", T.unpack ghcjsTriple),
              ("LLVM llc command", "llc"),
              ("LLVM opt command", "opt"),
              ("LLVM clang command", "clang"),
              ("Tables next to code","YES"),
              ("Use interpreter","YES"),
              ("Use native code generator","YES"),
              ("Support SMP","NO"),
              ("RTS ways","l debug debug_p"),
              ("Leading underscore","NO"),
              ("Use LibFFI","NO"),
              ("Use Threads","YES"),
              ("Use Debugging","NO"),
              ("RTS expects libdw","NO"),
              ("Support parallel --make","YES"),
              ("Support reexported-modules","YES"),
              ("Support thinning and renaming package flags","YES"),
              ("Support Backpack","YES"),
              ("Requires unified installed package IDs","YES"),
              ("Uses package keys","YES"),
              ("Uses unit IDs","YES")
              ]
  pure $ T.pack ("[" ++ intercalate ",\n" (map show opts) ++ "\n]")


prepareNodeJs :: B ()
prepareNodeJs = do
  ghcjsLib <- libDir <$> view beLocations
  buildDir <- view (beLocations . blBuildDir)
  nodeProgram <- view (bePrograms . bpNode . pgmLoc . to (maybe "-" T.pack))
  mbNodePath  <- view (beSettings . bsWithNodePath)
  extraArgs   <- view (beSettings . bsNodeExtraArgs)
  -- If no setting for NODE_PATH is specified, we use the libraries bundled
  -- with the ghcjs-boot submodule. We must run "npm rebuild" to build
  -- any sytem-specific components.
  when (isNothing mbNodePath) $ do
    subLib (mkdir_p "ghcjs-node")
    liftIO $ unpackTar False
                       True
                       ghcjsLib
                       (buildDir </> "ghcjs-node.tar")
    subLib' "ghcjs-node" $ npm_ ["rebuild"]
  -- write nodeSettings.json file
  let nodeSettings = NodeSettings
       { nodeProgram         = T.unpack nodeProgram
       , nodePath            = mbNodePath
       , nodeExtraArgs       = maybeToList extraArgs
       , nodeKeepAliveMaxMem = 536870912
       }
  liftIO $ BL.writeFile (T.unpack . T.pack $ ghcjsLib </> "nodeSettings.json")
                        (Aeson.encode $ Aeson.toJSON nodeSettings)

exe :: FilePath -> FilePath
exe = bool isWindows (<.>"exe") id

buildDocIndex :: B ()
buildDocIndex = subTop' "doc" $ do
  haddockFiles <- findWhen (return . isExtensionOf "haddock") "."
  haddock_ $ ["--gen-contents"
             ,"--gen-index"
             , "-o"
             , "html"
             , "--title=GHCJS Libraries"
             ] ++
             map (\p -> "--read-interface=../" <>
                        T.pack (takeDirectory p) <>
                        "," <>
                        T.pack p)
                 haddockFiles

installCabal :: B ()
installCabal = subBuild $ do
  msg info "installing Cabal library"
  preparePackage "Cabal"
  buildCabalPackage "Cabal"

installGhcjsPrim :: B ()
installGhcjsPrim = do
  msg info "installing ghcjs-prim"
  preparePackage "ghcjs-prim"
  buildCabalPackage "ghcjs-prim"

installGhcjsTh :: B ()
installGhcjsTh = do
  msg info "installing ghcjs-th"
  preparePackage "ghcjs-th"
  buildCabalPackage "ghcjs-th"

installStage1 :: B ()
installStage1 = subBuild $ do
  buildCabalPackage "ghc-prim"
  mapM_ installPackage =<< stagePackages bstStage1a
  installGhcjsPrim
  mapM_ installPackage =<< stagePackages bstStage1b
  installGhcjsTh
  resolveWiredInPackages
    where
      installPackage pkg = do
        msg info ("installing package " <> pkg)
        preparePackage pkg
        buildCabalPackage pkg

resolveWiredInPackages :: B ()
resolveWiredInPackages = subLib $ do
  wips <- readBinary ("wiredinpkgs" <.> "yaml")
  case Yaml.decodeEither' wips of
   Left err   -> failWith $
     "error parsing wired-in packages file wiredinpkgs.yaml\n" <> T.pack (show err)
   Right pkgs -> do
     pkgs' <- forM pkgs $ \p ->
       (p,) . T.strip <$> ghcjs_pkg [ "--simple-output"
                                    , "field"
                                    , p
                                    , "key"
                                    ]
     liftIO (T.writeFile ("wiredinkeys" <.> "yaml") (
       T.unlines ("# resolved wired-in packages" :
                  map (\(p,k) -> p <> ": " <> k) pkgs')))

preparePackage :: Package -> B ()
preparePackage pkg
  | "./" `T.isPrefixOf` pkg || "../" `T.isPrefixOf` pkg = sub $ do
    msg trace ("preparing package " <> pkg)
    cd (T.unpack pkg)
    rm_rf "dist"
  | otherwise = return ()

-- | subshell in path relative to build dir
subBuild :: B a -> B a
subBuild a = sub (view (beLocations . blBuildDir) >>= cd >> a)

-- | subshell in path relative to top installation dir
subTop' :: FilePath -> B a -> B a
subTop' p a = subTop (cd p >> a)

subTop :: B a -> B a
subTop a = sub (view (beLocations . blGhcjsTopDir) >>= cd >> a)

subLib' :: FilePath -> B a -> B a
subLib' p a = subLib (cd p >> a)

subLib :: B a -> B a
subLib a = sub ((libDir <$> view beLocations) >>= cd >> a)

{- |unpack a tar file (does not support compression)
    only supports files, does not try to emulate symlinks -}
unpackTar :: Bool             -- ^strip the first directory component?
          -> Bool             -- ^preserve symbolic links?
          -> Prelude.FilePath -- ^destination to unpack to
          -> Prelude.FilePath -- ^the tar file
          -> IO ()
unpackTar stripFirst preserveSymlinks dest tarFile = do
  createDirectoryIfMissing True dest
  entries <- Tar.read . BL.fromStrict <$> B.readFile tarFile
  void $ Tar.foldEntries (\e -> (>>=checkExtract e))
                         (return Nothing)
                         (\e -> failWith $ "error unpacking tar: " <> showT e)
                         entries
    where
      failSec e msg = failWith $ "tar security check, " <>
                                 msg <>
                                 ": " <>
                                 T.pack (Tar.entryPath e)
      checkExtract e Nothing
        | (p:_) <- FP.splitDirectories (Tar.entryPath e)
            = checkExtract e (Just p)
        | otherwise = failSec e $
            "no path"
      checkExtract e je@(Just expected)
        | FP.isAbsolute ep = failSec e $
            "absolute path"
        | elem ".." epd              = failSec e $
            "'..' in path"
        | listToMaybe epd /= je &&
          isSupportedEntry (Tar.entryContent e) = failSec e $
            "tar bomb, expected path component: " <> T.pack expected
        | otherwise                     = do
            (extractEntry e $ dest FP.</>
                   (FP.joinPath
                                (drop (if stripFirst then 1 else 0) epd)))
            return je
        where ep  = Tar.entryPath e
              epd = FP.splitDirectories ep
      isSupportedEntry Tar.NormalFile{} = True
      isSupportedEntry Tar.Directory{}  = True
      isSupportedEntry _                = False
      extractEntry :: Tar.Entry -> Prelude.FilePath -> IO ()
      extractEntry e tgt
        | Tar.NormalFile bs _size <- Tar.entryContent e = do
            createDirectoryIfMissing True (FP.dropFileName tgt)
            BL.writeFile tgt bs
            setPermissions (Tar.entryPermissions e) tgt
        | Tar.Directory <- Tar.entryContent e = do
            createDirectoryIfMissing True tgt
            setPermissions (Tar.entryPermissions e) tgt
        | Tar.SymbolicLink _linkTgt <- Tar.entryContent e
        , preserveSymlinks =
            createDirectoryIfMissing True (FP.dropFileName tgt)
        | otherwise = hPutStrLn stderr $
            "ignoring unexpected entry type in tar. " <>
            "only normal files and directories (no links) " <>
            "are supported:\n    " <> tgt
      setPermissions mode tgt = do
        absTgt <- makeAbsolute tgt
        setFileMode absTgt mode

run_inplace :: Text -> [Text] -> B Text
run_inplace pgm args = do
  ghcjsTop <- view (beLocations . blGhcjsTopDir)
  run'' (ghcjsTop </> "bin" </> T.unpack pgm) "" args

run_inplace_ :: Text -> [Text] -> B ()
run_inplace_ pgm args = do
  ghcjsTop <- view (beLocations . blGhcjsTopDir)
  run''_ (ghcjsTop </> "bin" </> T.unpack pgm) args

ghc_ :: [Text] -> B ()
ghc_         = runE_ bpGhc

ghcjs_pkg :: [Text] -> B Text
ghcjs_pkg = run_inplace "ghcjs-pkg"

ghcjs_pkg_ :: [Text] -> B ()
ghcjs_pkg_   = run_inplace_ "ghcjs-pkg"

haddock_ :: [Text] -> B ()
haddock_     = run_inplace_ "haddock"

npm_ :: [Text] -> B ()
npm_ args = do
  nodePgm <- view (bePrograms . bpNode)
  case nodePgm ^. pgmLoc of
    Nothing -> failWith "nodejs program not configured when trying to run npm"
    Just loc ->
      if isWindows
      then do
        sysRoot <- fromMaybe "C:\\WINDOWS" <$> liftIO (Utils.getEnvMay "SYSTEMROOT")
        let cmdLoc = sysRoot </> "system32" </> "cmd" <.> "exe"
            npmLoc = FP.takeDirectory loc </> "npm" <.> "cmd"
        run''_ cmdLoc ("/C" : T.pack npmLoc : args)
      else do
        let npmLoc = FP.takeDirectory loc </> "npm"
        run''_ npmLoc args

runE_ :: ((Program -> Const Program Program)
      -> BootPrograms -> Const Program BootPrograms)
      -> [Text] -> B ()
runE_ g a = view (bePrograms . g) >>= \p -> run_ p a

customSetupPackages :: [Text]
customSetupPackages = [ "ghc-prim" ]

cabalConfigureFlags :: B [Text]
cabalConfigureFlags = do
  debug    <- view (beSettings . bsDebug)
  v        <- view (beSettings . bsVerbosity)
  instDir  <- libDir <$> view beLocations
  prof     <- view (beSettings . bsProf)
  locs     <- view beLocations
  let binDir        = (locs ^. blGhcjsTopDir) </> "bin"
      privateBinDir = instDir </> "bin"
  return $ [ "--global"
           , "--ghcjs"
           , "--builddir",      "dist"
           , "--with-compiler", (T.pack $ binDir </> "ghcjs")
           , "--with-hc-pkg",   (T.pack $ binDir </> "ghcjs-pkg")
           , "--with-haddock",  (T.pack $ binDir </> "haddock")
           , "--with-gcc",      (T.pack $ privateBinDir </> "emcc")
           , "--prefix",        T.pack (locs ^. blGhcjsTopDir)
           , "--configure-option", "--host=js-unknown-ghcjs"
           , "--ghcjs-options=-fwrite-ide-info"
           , "--enable-debug-info"
           , "--disable-library-stripping"
           , "--disable-executable-stripping"
           , "-fghci"
           , bool prof
                  "--enable-profiling"
                  "--disable-profiling"
           ] ++
           -- don't slow down Windows builds too much,
           -- on other platforms we get this more
           -- or less for free, thanks to dynamic-too
           bool isWindows [] ["--enable-shared"] ++
           catMaybes [ bj debug "--ghcjs-options=-debug"
                     , bj (v > info) "-v2"
                     ]


buildCabalPackage :: Text -> B ()
buildCabalPackage pkg = subBuild $ do
  cd ("pkg" </> T.unpack pkg) -- hopefully correct
  -- build custom setup if necessary
  msg info ("configuring package: " <> pkg)
  configureFlags <- cabalConfigureFlags
  haddock  <- view (beSettings . bsHaddock)

  -- configure / build
  liftIO $ setEnv "GHCJS_BOOTING" "1"
  case pkg `elem` customSetupPackages of
                True -> do
                  {-
                     XXX there is only one custom setup package

                         we should probably just build the ghc-prim
                         Setup.hs as part of the ghcjs package, so
                         we can avoid having to know about GHC at all
                         (and have proper dependencies)
                    -}
                  ghc <- requirePgmLoc =<< view (bePrograms . bpGhc)
                  run''_ ghc ["Setup.hs"]
                  let setup args = run''_ "./Setup" args
                  setup ("configure" : configureFlags)
                  setup ["build"]
                  when haddock (setup ["haddock"])
                  setup ["copy"]
                  setup ["register"]
                _ -> do
                  cmd <- requirePgmLoc =<< view (bePrograms . bpCabal)
                  let setup args = run''_ cmd args
                  setup ("v1-configure" : configureFlags)
                  setup ["v1-build"]
                  when haddock (setup ["v1-haddock"])
                  setup ["v1-copy"]
                  setup ["v1-register"]

stagePackages :: Getter BootStages Stage -> B [Package]
stagePackages l = view (beStages . l)

failWith :: MonadIO m => Text -> m a
failWith err = liftIO (T.putStrLn ("fatal: " <> err) >> exitFailure)

{- |initialize our boot environment by reading the configuration files,
    finding all programs -}
initBootEnv :: BootSettings -> IO BootEnv
initBootEnv bs = do
  BootConfigFile stgs pgms1 <- readBootConfigFile bs
  pgms2 <- configureBootPrograms bs pgms1
  locs  <- configureBootLocations bs pgms2
  -- installWrappers ((locs ^. blGhcjsTopDir) </> "bin") locs pgms2
  return (BootEnv bs locs pgms2 stgs)

-- the platform triple that GHCJS reports
-- this is in practice mostly used for the toolchain
ghcjsTriple :: Text
ghcjsTriple = "js-unknown-ghcjs"

-- this assumes that the ghcjs-boot version is correct. perhaps we should query the ghcjs executable to verify?
ghcjsVersion :: BootPrograms -> IO Text
ghcjsVersion _ = pure (T.pack Info.getCompilerVersion)

libSubDir :: FilePath
libSubDir = "lib"

libDir :: BootLocations -> FilePath
libDir locs = (locs ^. blGhcjsTopDir) </> libSubDir

data WrapperEnv = WrapperEnv { weTopDir     :: Text
                             , weBinDir     :: Text
                             , weLibexecDir :: Text
                             , weVersion    :: Text
                             , weGhcVersion :: Text
                             , weEmsdk      :: Text
                             } deriving (Show)
--

copyWrapper :: Text
            -> Maybe Text
            -> WrapperEnv
            -> FilePath
            -> FilePath
            -> FilePath
            -> IO ()
copyWrapper = if isWindows then copyWrapperW else copyWrapperU

{- |
     on Windows we can't run shell scripts, so we don't install wrappers
     just copy program.exe to program-{version}-{ghcversion}.exe

     the programs read a program-{version}-{ghcversion}.exe.options file from
     the same directory, which contains the command line arguments to prepend

     installation does not overwrite existing .options files
 -}

copyWrapperW :: Text
             -> Maybe Text
             -> WrapperEnv
             -> FilePath
             -> FilePath
             -> FilePath
             -> IO ()
copyWrapperW programName mbVer env libexecDir wrapperSrcDir binDir = do
    createDirectoryIfMissing False binDir
    Cabal.installExecutableFile verbosity srcExe tgtExe
    whenVer (Cabal.installExecutableFile verbosity srcExe tgtExeVersioned)
    options <- replacePlaceholders env <$> T.readFile (wrapperSrcDir </> T.unpack programName <.> "exe" <.> "options")
    createDirectoryIfMissing False binDir
    Cabal.withTempFile binDir "ghcjs-options-XXXXXX.tmp" $ \tmp h -> do
        T.hPutStr h options
        hClose h
        Cabal.installOrdinaryFile verbosity tmp tgtOptions
        whenVer (Cabal.installOrdinaryFile verbosity tmp tgtOptionsVersioned)
  where
    verbosity           = Cabal.verbose
    whenVer x           = when (isJust mbVer) x
    srcExe              = libexecDir </> "wrapper" <.> "exe"

    tgtPrefix           = binDir </> T.unpack programName
    tgtPrefixVersioned  = binDir </> T.unpack (programName <> "-" <> ver)

    tgtExe              = tgtPrefix <.> "exe"
    tgtOptions          = tgtExe <.> "options"
    tgtExeVersioned     = tgtPrefixVersioned <.> "exe"
    tgtOptionsVersioned = tgtExeVersioned <.> "options"
    ver = fromMaybe "0.0.0" mbVer

{- |
     on non-Windows we copy shell scripts that pass the -B flag to ghcjs,
     ghcjs-pkg etc

     installation updates the symlink, but does not overwrite the wrapper
     scripts if they already exist

     if no wrapper is required, we simply symlink to the executable in the
     libexec directory
 -}

copyWrapperU :: Text
             -> Maybe Text
             -> WrapperEnv
             -> FilePath
             -> FilePath
             -> FilePath
             -> IO ()
copyWrapperU programName mbVer env libexecDir wrapperSrcDir binDir = do
  putStrLn ("installing wrapper for: " ++ show (programName, mbVer, libexecDir, wrapperSrcDir, binDir))
  createDirectoryIfMissing False binDir
  wrapperScript <- replacePlaceholders env <$> T.readFile (wrapperSrcDir </> T.unpack programName <.> "sh")
  Cabal.withTempFile binDir "ghcjs-wrapper-XXXXXX.tmp" $
      (\tmp h -> do T.hPutStr h wrapperScript
                    hClose h
                    Cabal.installExecutableFile Cabal.normal tmp (binDir </> tgt))
  when (isJust mbVer) (linkFileU Cabal.normal binDir tgt tgtPlain)
  where
    tgtPlain = T.unpack programName
    tgt      = maybe tgtPlain
                     (\ver -> T.unpack (programName <> "-" <> ver))
                     mbVer

{- |
     create a symlink, overwriting the target. unix only.

     it looks like the Cabal library does not have this functionality,
     and since we shouldn't use system-specific libraries here,
     we use a shell command instead.

     the symlink is relative if both files are in the same directory
  -}
linkFileU :: Cabal.Verbosity -> FilePath -> FilePath -> FilePath -> IO ()
linkFileU _v workingDir src dest = do
  -- putStrLn ("link file: " ++ show (workingDir, src, dest))
  let ignoreDoesNotExist :: IOError -> IO ()
      ignoreDoesNotExist e | isDoesNotExistError e = return ()
                           | otherwise             = Ex.throw e
  removeFile (workingDir </> dest) `Ex.catch` ignoreDoesNotExist
  wd <- getCurrentDirectory
  setCurrentDirectory workingDir
  createFileLink src dest
  setCurrentDirectory wd

-- | replace placeholders in a wrapper script or options file
replacePlaceholders :: WrapperEnv -> Text -> Text
replacePlaceholders env xs =
  foldl (\ys (p,r) -> T.replace p (r env) ys) xs
    [ ("{topdir}",     weTopDir)
    , ("{bindir}",     weBinDir)
    , ("{libexecdir}", weLibexecDir)
    , ("{version}",    weVersion)
    , ("{ghcversion}", weGhcVersion)
    , ("{emsdk}",      weEmsdk)
    ]

{- | try to find the emsdk directory
       1. check --with-emsdk option
       2. check EMSDK environment variable
       3. find emcc executable in PATH
 -}
configureEmsdkDir :: BootSettings
                  -> IO FilePath
configureEmsdkDir bs
  | Just dir <- bs ^. bsWithEmsdk = pure (T.unpack dir)
  | otherwise = do
    e <- lookupEnv "EMSDK"
    case e of
      Just edir -> pure edir
      _         -> do
        mbEmccExe <- findExecutable "emcc"
        case mbEmccExe of
          Just emccExe ->
            {- if we found the `emcc' executable, we just assume
               that it's two directories deeper than the emsdk root
               path, e.g. fastcomp/bin, fastcomp/emscripten, upstream/bin

               perhaps we should change this to check for the emsdk python
               script itself.
             -}
            let emccDirParts = FP.splitPath (FP.takeDirectory emccExe)
            in  pure (concat . reverse . drop 2 . reverse $ emccDirParts)
          _ -> notFound
  where
    notFound = failWith "emscripten emsdk not found (use --with-emsdk)"

-- | configure the locations
configureBootLocations :: BootSettings
                       -> BootPrograms
                       -> IO BootLocations
configureBootLocations bs _pgms = do
  -- take the location of the ghcjs-boot program and configure the
  -- installation directory relative to that
  bootPath <- getExecutablePath
  ghcjsTopDir <- canonicalizePath (FP.dropFileName bootPath </> "..")
  let ghcjsLibDir = ghcjsTopDir </> libSubDir
      globalDB = ghcjsLibDir </> "package.conf.d"

  -- the source directory is specified by the user
  sourceDir <- bootSourceDir bs
  buildDir <- prepareBuildDir sourceDir ghcjsLibDir
  emsdkDir <- configureEmsdkDir bs
  pure $ BootLocations sourceDir
                        buildDir
                        ghcjsTopDir
                        globalDB
                        emsdkDir

prepareBuildDir :: Prelude.FilePath
                -> Prelude.FilePath
                -> IO Prelude.FilePath
prepareBuildDir srcDir ghcjsLibDir = do
  e <- doesFileExist srcDir
  if e then do let bootDir = ghcjsLibDir FP.</> "boot"
               bootExists <- doesDirectoryExist  bootDir
               when bootExists (removeDirectoryRecursive bootDir)
               unpackTar False False ghcjsLibDir srcDir
               pure bootDir
       else do
         d <- doesDirectoryExist srcDir
         if d then do
                e <- doesFileExist (srcDir FP.</> "boot" FP.<.> "yaml")
                if e then pure srcDir else err
              else err
   where
     err = failWith "source location must contain boot.yaml or be a tar file"

-- |build the program configuration and do some sanity checks
configureBootPrograms :: BootSettings    -- ^command line settings
                      -> BootPrograms    -- ^default programs from config file
                      -> IO BootPrograms -- ^configured programs
configureBootPrograms bs pgms0 = do
  -- first replace all defaults with the overrides from the command line
  let
      r l   = maybe id (pgmSearch .~) (bs ^. l)
      tp    = template :: Traversal' BootPrograms Program
      pgms2 = pgms0 & bpCabal    %~ r bsWithCabal
                    & bpNode     %~ r bsWithNode
  -- resolve all programs
  pgms3 <- mapMOf tp (resolveProgram bs)
             =<< mapMOf tp (resolveProgram bs) pgms2
  traverseOf_ tp (reportProgramLocation bs) pgms3
  return pgms3

resolveProgram :: BootSettings
               -> Program
               -> IO Program
resolveProgram _bs pgm = do
  let search' = pgm ^. pgmSearch . to T.unpack
  absSearch <- (</> search') <$> getCurrentDirectory
  let searchPaths = catMaybes [ Just search'
                              , bj (isRelative search' &&
                                    length (splitDirectories search') > 1)
                                   absSearch
                              ]
  fmap catMaybes (mapM findExecutable searchPaths) >>= \case
      (p':_) -> (\cp -> pgm & pgmLoc ?~ cp) <$> return p'
      _      -> return (pgm & pgmLoc .~ Nothing)

-- | report location of a configured program
reportProgramLocation :: BootSettings -> Program -> IO ()
reportProgramLocation bs p
  | Just l <- p ^. pgmLoc = msg' bs info $ "program " <>
                                           p ^. pgmName <>
                                           " found at " <>
                                           T.pack l
  | otherwise             = msg' bs info $ "program " <>
                                           p ^. pgmName <>
                                           " NOT found, searched for " <>
                                           p ^. pgmSearch

-- | read the boot configuration yaml file
readBootConfigFile :: BootSettings -> IO BootConfigFile
readBootConfigFile bs = do
  bf <- bootConfigFile bs
  case Yaml.decodeEither' bf of
    Left err  -> failWith $
      "error parsing boot.yaml configuration file\n" <>
      T.pack (show err)
    Right bss -> return bss

printBootEnvSummary :: Bool -> BootEnv -> IO ()
printBootEnvSummary after be = do
  section "Boot libraries installation for GHCJS" $ do
    bootLoc  <- getExecutablePath
    bootMod  <- getModificationTime bootLoc
    bootSrc  <- bootSourceDir (be ^. beSettings)
    curDir   <- getCurrentDirectory
    p $ bool after
          ["ghcjs-boot has installed the libraries and runtime system for GHCJS"]
          ["ghcjs-boot will install the libraries and runtime system for GHCJS"]
    h "boot program"
    t "rl" [["ghcjs-boot program version", Info.getCompilerVersion]
           ,["file location", bootLoc]
           ,["last modified", show bootMod],[]
           ,["boot source location", bootSrc]
           ,["current directory", curDir]
           ]
    h "boot configuration"
    t "rl" [["installation directory", path $ beLocations . blGhcjsTopDir]
           ,["global package DB", path $ beLocations . blGlobalDB]
           ,["GHC version", ver "<unknown>" bpGhc]
           ,["location", loc bpGhc]
           ,["cabal-install version", ver "<unknown>" bpCabal]
           ,["location", loc bpCabal],[]
           ]
    h "packages"
    p ["stage 1a"] >> l (stg bstStage1a)
    p ["ghcjs-prim:  " ++ be ^. beStages . bstGhcjsPrim . to str]
    p ["stage 1b"] >> l (stg bstStage1b)
    p ["Cabal:  " ++ be ^. beStages . bstCabal . to str]
  section "Configured programs" $
    t "hlll" $ ["program", "version", "location"] :
      be ^.. bePrograms .
             (template :: Traversal' BootPrograms Program) .
             to pgm
  when after $ do
     p ["GHCJS has been booted."]
     p ["You can now add the binaries directory to your PATH, or create"]
     p ["symbolic to the ghcjs and ghcjs-pkg executables"]
     p ["the binaries have been installed in"]
     p [path $ beLocations . blGhcjsTopDir . to (</> "bin")]

  where
    stg s       = be ^.. beStages . s . traverse . to str
    h xs        = b >>
                  mapM_ (putStrLn . indent 2)
                        [xs, replicate (length xs) '-'] >> b
    p xs        = mapM_ (putStrLn . indent 3) xs >> b
    l xs        = mapM_ (putStrLn . indent 3 . ("- "++)) xs >> b
    t :: String -> [[String]] -> IO ()
    t aln xxs   = let colWidths = map (foldl' (\m xs -> max m (length xs)) 0)
                                      (transpose xxs)
                      (colAlign,hdr) = case aln of
                        ('h':a) -> (a, True)
                        a       -> (a, False)
                      cell w a xs = let pad = sp (w - length xs)
                                    in if a == 'r' then pad ++ xs
                                                   else xs ++ pad
                      cols xs   = sp 3 ++ intercalate (sp 3) xs
                      row xs    = cols (zipWith3 cell colWidths colAlign xs)
                  in case (xxs, hdr) of
                      (x:ys, True) -> putStrLn (row x) >>
                                      putStrLn (cols $ map sp colWidths) >>
                                      mapM_ (putStrLn . row) ys >> b
                      _            -> mapM_ (putStrLn . row) xxs
    b           = putStrLn ""
    sp n        = replicate n ' '
    indent n xs = sp n ++ xs
    sep         = putStrLn (replicate 75 '=')
    section :: String -> IO () -> IO ()
    section t a = b >> b >> sep >> b >> p [t] >> sep >> b >> a >> b
    ver d l     = be ^. bePrograms . l . pgmVersion . to (maybe d T.unpack)
    loc l       = be ^. bePrograms . l . pgmLocString
    path l      = be ^. l
    str         = T.unpack
    pgm x       = [ x ^. pgmName . to str
                  , maybe "-" T.unpack (x ^. pgmVersion)
                  , x ^. pgmLocString
                  ]

-- | boot.yaml
bootConfigFile :: BootSettings -> IO B.ByteString
bootConfigFile bs = do
  sourceDir <- bootSourceDir bs
  let sourceDir' = sourceDir
  e <- doesFileExist sourceDir'
  if e then do -- it's a tar file
         entries <- Tar.read . BL.fromStrict <$>
           B.readFile sourceDir'
         pure (BL.toStrict $ getBootYaml entries)
       else B.readFile (sourceDir </> "boot" <.> "yaml")
  where
    getBootYaml (Tar.Next e es)
      | Tar.entryPath e == "boot/boot.yaml"
      , Tar.NormalFile contents _size <- Tar.entryContent e = contents
      | otherwise = getBootYaml es
    getBootYaml Tar.Done     = error "boot/boot.yaml file not found in archive"
    getBootYaml (Tar.Fail e) = error $ "error reading boot archive: " ++ show e

bootSourceDir :: BootSettings -> IO FilePath
bootSourceDir bs
  | Just dd <- bs ^. bsSourceDir = makeAbsolute (T.unpack dd)
  | otherwise                    = do
      workingDirectory <- getCurrentDirectory
      let configFile =  workingDirectory </> "boot" <.> "yaml"
      configInCurrentDir <- doesFileExist configFile
      if configInCurrentDir
        then pure workingDirectory
        else do
          dataDir <- Info.ghcjsBootDefaultDataDir
          let bootArchive = dataDir </> "boot" <.> "tar"
          bootArchiveExists <- doesFileExist bootArchive
          pure $ if bootArchiveExists then bootArchive else workingDirectory

-- | our boot monad, we wrap around shelly but with a config environment
--   shelly commands are wrapped with logging
type B = ReaderT BootEnv IO

msg' :: BootSettings -> Verbosity -> Text -> IO ()
msg' bs v t = when (bs ^. bsVerbosity >= v) (T.putStrLn t)

msg :: Verbosity -> Text -> B ()
msg v t =
  view beSettings >>= \s -> when (s ^. bsVerbosity >= v) $ lift (T.putStrLn t)

-- | log a message printing the current directory
msgD :: Verbosity -> Text -> B ()
msgD v t = pwd >>= \p -> msg v (T.pack p <> "$ " <> t)

msgD' :: BootSettings -> Verbosity -> Text -> IO ()
msgD' bs v t = getCurrentDirectory >>= \p -> msg' bs v (T.pack p <> "$ " <> t)

{-
  lifted versions of the shelly operations we need. everything that
  makes externally visible changes is logged at the info (-v2)
  verbosity level.

  internal changes and file reads are logged at trace (-v3) level.
 -}

mkdir_p :: FilePath -> B ()
mkdir_p p      = msgD info ("mkdir_p " <> T.pack p) >> lift (createDirectoryIfMissing True p)

cp :: FilePath -> FilePath -> B ()
cp f t         = msgD info ("cp "      <> T.pack f <> " -> " <> T.pack t) >>
                 lift (copyFile f t)

cp_r :: FilePath -> FilePath -> B ()
cp_r src tgt = do
  liftIO (createDirectoryIfMissing True tgt)
  mapM_ f =<< liftIO (listDirectory src)
  where
    f entry = do
      let se = src </> entry
          te = tgt </> entry
      isDir <- liftIO (doesDirectoryExist se)
      if isDir then cp_r se te else liftIO (copyFile se te)

rm_rf :: FilePath -> B ()
rm_rf p        = msgD info ("rm_rf "   <> T.pack p) >> lift (removePathForcibly p)

cd :: FilePath -> B ()
cd p = do
  msgD trace ("cd " <> T.pack p)
  cur <- lift getCurrentDirectory
  lift (setCurrentDirectory (cur </> p))


sub :: B a -> B a
sub x = do
  workingDir <- liftIO getCurrentDirectory
  r <- x
  liftIO (setCurrentDirectory workingDir)
  pure r

run_ :: Program -> [Text] -> B ()
run_ p xs = do
  traceDir
  msgD info (traceRun p xs)
  requirePgmLoc p >>= \loc -> do
    let pc = proc loc (map T.unpack $ p ^. pgmArgs ++ xs)
    (_, _, _, ph) <- liftIO (createProcess pc)
    ex <- liftIO (waitForProcess ph)
    when (ex /= ExitSuccess)
       (failWith $ "program " <>
                   p ^. pgmLocText <>
                   " returned a nonzero exit code")

run'' :: FilePath -> Text -> [Text] -> B Text
run'' loc stdin xs = do
  msgD info (traceRun' loc xs)
  let pc = proc loc (map T.unpack xs)
  T.pack <$> lift (readCreateProcess pc (T.unpack stdin))

run''_ :: FilePath -> [Text] -> B ()
run''_ loc xs = do
  msgD info (traceRun' loc xs)
  let pc = proc loc (map T.unpack xs)
  (_, _, _, ph) <- liftIO (createProcess pc)
  ex <- liftIO (waitForProcess ph)
  when (ex /= ExitSuccess)
      (failWith $ "program " <>
                  T.pack loc <>
                  " returned a nonzero exit code")

readBinary :: FilePath -> B B.ByteString
readBinary path = do
  msgD trace ("reading " <> T.pack path)
  liftIO (B.readFile path)

pwd :: B FilePath
pwd = lift getCurrentDirectory


findWhen :: (FilePath -> B Bool) -> FilePath -> B [FilePath]
findWhen pred path = fmap concat (mapM f =<< (liftIO $ listDirectory path))
  where
    f :: FilePath -> B [FilePath]
    f entry = do
      let pe = path </> entry
      d <- liftIO (doesDirectoryExist pe)
      if d then findWhen pred pe
           else do
             pp <- pred pe
             if pp then pure [pe] else pure []

catchAny :: B b -> (Ex.SomeException -> B b) -> B b
catchAny a h = ask >>=
               \e -> lift (Ex.catch (runReaderT a e)
                                    (\ex -> runReaderT (h ex) e))

catchAny_ :: B b -> B b -> B b
catchAny_ a h  = catchAny a (\_ -> h)

traceDir :: B ()
traceDir = ask >>= \e -> lift (traceDir' (e ^. beSettings))

traceDir' :: BootSettings -> IO ()
traceDir' bs = do
  cur <- getCurrentDirectory
  msgD' bs info ("current directory: " <> T.pack cur)


traceRun :: Program -> [Text] -> Text
traceRun p xs = "[" <>
                p ^. pgmName <>
                "]: " <>
                p ^. pgmLocText <>
                " " <>
                T.intercalate " " (map (showT . T.unpack) xs)


traceRun' :: FilePath -> [Text] -> Text
traceRun' p xs = {- "[" <>
                  T.pack p <>
                "]: " <> -}
                T.pack p <>
                " " <>
                T.intercalate " " (map (showT . T.unpack) xs)

requirePgmLoc :: Program -> B FilePath
requirePgmLoc p
  | Just loc <- p ^. pgmLoc = return loc
  | otherwise = do
        {- search in original path, where we configured the programs.
           the shelly path might be local -}
        path <- fromMaybe "" <$> liftIO (Utils.getEnvMay "PATH")
        failWith $ "program " <>
                   p ^. pgmName <>
                   " is required but was not found\n" <>
                   "  name searched for (from boot.yaml or command line): " <>
                   p ^. pgmSearch <>
                   "\n" <>
                   "  searched in PATH:\n" <>
                   T.pack path

pgmLocText :: Getter Program Text
pgmLocText = pgmLoc . to (maybe "<not found>" T.pack)

pgmLocString :: Getter Program String
pgmLocString = pgmLocText . to T.unpack

showT :: Show a => a -> Text
showT = T.pack . show

bool :: Bool -> a -> a -> a
bool b t f = if b then t else f

bj :: Bool -> a -> Maybe a
bj b v = if b then Just v else Nothing

infixl 2 <^>
(<^>) :: MonadReader s m
      => (a -> m b)
      -> Getting a s a
      -> m b
(<^>) m l = m =<< view l

<MSG> add ghcjs-boot --reboot support: rebuild core packages quicker by replaying a log of an earlier boot

<DFF> @@ -59,6 +59,7 @@ data BootSettings = BootSettings { skipRts  :: Bool  -- ^ skip building the rts
                                  , noBuild  :: Bool  -- ^ do not build, only reset the packages
                                  , initTree :: Bool  -- ^ initialize the source tree: configure/build native ghc first
                                  , onlyBasic :: Bool -- ^ only build basic packages: ghc-prim, integer-gmp, base
+                                 , reboot    :: Bool -- ^ reboot using log of old ghcjs invocations
                                  } deriving (Ord, Eq)
 
 bootSettings :: [String] -> BootSettings
@@ -66,6 +67,7 @@ bootSettings args = BootSettings (any (=="--skip-rts") args)
                                  (any (=="--nobuild") args)
                                  (any (=="--init") args)
                                  (any (=="--only-basic") args)
+                                 (any (=="--reboot") args)
 
 main = withSocketsDo $ do
   tmp <- getTemporaryDirectory
@@ -137,28 +139,70 @@ installBootPackages settings = do
   case (mghcjs, mghcjspkg) of
     (Just ghcjs, Just ghcjspkg) -> do
       echo $ "Booting: " <> toTextIgnore ghcjs <> " (" <> toTextIgnore ghcjspkg <> ")"
+      p <- pwd
       ghcjsVer <- T.strip <$> run ghcjs ["--numeric-version"]
-      when (not $ noBuild settings) $ do
-        addWrappers ghcjs ghcjspkg
+      base  <- liftIO getGlobalPackageBase
+      let commandLog = base </> "boot.log"
+      when (not (noBuild settings || reboot settings)) $ do
+        addWrappers ghcjs (toTextIgnore commandLog) ghcjspkg
       initPackageDB
       let corePkgs = corePackages (onlyBasic settings)
-      when (not (skipRts settings || noBuild settings)) $ do
+      when (not (skipRts settings || noBuild settings || reboot settings)) $ do
            echo "Building RTS"
            run_ "make" ["all_rts","-j4"] `catchany_sh` (const (return ()))
-      replacePrimOps
-      when (not $ noBuild settings) $ forM_ corePkgs $ \pkg -> do
-        echo $ "Building package: " <> pkg
-        buildPkg pkg
+           replacePrimOps
+      if reboot settings
+        then do
+          echo "Rebooting"
+          lines <- T.lines <$> readfile commandLog
+          case lines of
+            (wd:cmds) -> do
+              cd (fromText wd)
+              setenv "GHCJS_NO_NATIVE" "1"
+              setenv "GHCJS_FALLBACK_PLAIN" "1"
+              forM_ cmds $ \r ->
+                let args = T.words r
+                in  when (isRebootCmd args) $ do
+                      echo ("replaying: ghcjs " <> r)
+                      (run_ "ghcjs" (T.words r))
+            _ -> error "can't reboot, invalid boot log"
+        else do
+          when (not $ noBuild settings) $ do
+             rm_f commandLog
+             p <- pwd
+             writefile commandLog (toTextIgnore p <> "\n")
+             forM_ corePkgs $ \pkg -> do
+               echo $ "Building package: " <> pkg
+               buildPkg pkg
       installRts
       mapM_ (installPkg ghcjs ghcjspkg) corePkgs
       installFakes
+      cd p
     _ -> echo "Error: ghcjs and ghcjs-pkg must be in the PATH"
 
+-- when rebooting, we replay the log of ghcjs calls
+-- but some of them are for package configuration
+-- testing features etc, filter them out here
+isRebootCmd :: [Text] -> Bool
+isRebootCmd args
+  | any (`elem` args)
+        [ "-M"
+        , "--info"
+        , "--numeric-version"
+        , "--print-libdir"
+        , "--supported-languages"
+        ] = False
+  | any isTmpCSource args = False
+  | otherwise = True
+  where
+    isTmpCSource a = "/tmp/" `T.isPrefixOf` a &&
+                     ".c" `T.isSuffixOf` a
+
 -- add inplace/bin/ghcjs and inplace/bin/ghcjs-pkg wrappers
-addWrappers :: FilePath -> FilePath -> ShIO ()
-addWrappers ghcjs ghcjspkg = do
+addWrappers :: FilePath -> Text -> FilePath -> ShIO ()
+addWrappers ghcjs log ghcjspkg = do
     ghcwrapper <- readfile "inplace/bin/ghc-stage1"
-    writefile "inplace/bin/ghcjs" (fixGhcWrapper ghcwrapper ghcjs)
+    writefile "inplace/bin/ghcjs" (fixGhcWrapper ghcwrapper log ghcjs)
     makeExecutable "inplace/bin/ghcjs"
     pkgwrapper <- readfile "inplace/bin/ghc-pkg"
     writefile "inplace/bin/ghcjs-pkg" (fixPkgWrapper pkgwrapper ghcjspkg)
@@ -168,8 +212,8 @@ addWrappers ghcjs ghcjspkg = do
         p <- getPermissions f
         setPermissions f (p {executable = True})
 
-fixGhcWrapper :: Text -> FilePath -> Text
-fixGhcWrapper wrapper ghcjs = T.unlines . concatMap fixLine . T.lines $ wrapper
+fixGhcWrapper :: Text -> Text -> FilePath -> Text
+fixGhcWrapper wrapper log ghcjs = T.unlines . concatMap fixLine . T.lines $ wrapper
     where
       exec = "executablename="
       fixLine line
@@ -179,6 +223,8 @@ fixGhcWrapper wrapper ghcjs = T.unlines . concatMap fixLine . T.lines $ wrapper
               , "export GHCJS_FALLBACK_PLAIN=1"
               , "export GHCJS_NO_NATIVE=1"
               , "export GHCJS_FAKE_NATIVE=1"
+              , "export GHCJS_LOG_COMMANDLINE=1"
+              , "export GHCJS_LOG_COMMANDLINE_NAME=" <> log
               ]
           | otherwise                             = [line]
 

{'addition_count': 58, 'commit_subject': 'add ghcjs-boot --reboot support: rebuild core packages quicker by replaying a log of an earlier boot', 'deletion_count': 12, 'file_extension': '.hs', 'lang': 'hs', 'license': 'mit', 'repo_name': 'ghcjs/ghcjs'}