module Rules.SourceDist (sourceDistRules) where

import Base
import Builder
import Context
import Oracles.Setting
import Packages
import Utilities (askWithResources, build)
import Hadrian.Target (target)
import qualified System.Directory.Extra as IO
import Oracles.ModuleFiles (determineBuilder)

sourceDistRules :: Rules ()
sourceDistRules = alternatives $ do
    root <- buildRootRules
    "source-dist" ~> do
        version <- setting ProjectVersion
        need [root -/- "source-dist" -/- ("ghc-" ++ version ++ "-src.tar.xz")]
        need [root -/- "source-dist" -/- ("ghc-" ++ version ++ "-testsuite.tar.xz")]
        need [root -/- "source-dist" -/- ("ghc-" ++ version ++ "-windows-extra-src.tar.xz")]
        putSuccess "| Done"

    -- Ordering of rules is important so that windows-extra-src matches before src
    root -/- "source-dist" -/- "ghc-*-windows-extra-src.tar.xz" %>
      archiveSourceTree prepareWindowsExtraTree
    root -/- "source-dist" -/- "ghc-*-testsuite.tar.xz" %>
      archiveSourceTree prepareTestsuiteTree
    root -/- "source-dist" -/- "ghc-*-src.tar.xz" %>
      archiveSourceTree prepareTree
    "GIT_COMMIT_ID" %> \fname ->
        writeFileChanged fname =<< setting ProjectGitCommitId
    "VERSION" %> \fname ->
        writeFileChanged fname =<< setting ProjectVersion

    -- Rules to download mingw tarballs
    let mingw_tarballs_stamp = "ghc-tarballs/mingw-w64/.mingw-w64.download.stamp"
    ["ghc-tarballs/mingw-w64/*/*.tar.*","ghc-tarballs/mingw-w64/*/SHA256SUMS"]  |%> \_ ->
      need [mingw_tarballs_stamp]
    mingw_tarballs_stamp %> \stamp -> do
      build (target (vanillaContext Stage1 compiler) (Win32Tarballs DownloadTarballs) [] [])
      writeFile' stamp "OK"


archiveSourceTree :: (FilePath -> Action ()) -> FilePath -> Action ()
archiveSourceTree prepare fname = do
  root <- buildRoot
  version <- setting ProjectVersion
  let dropTarXz = dropExtension . dropExtension
      tarName   = takeFileName fname
      dirName   = dropTarXz tarName
      baseName  = "ghc-" ++ version
      treeDir   = dirName -/- baseName
      treePath  = sourceDistRoot -/- treeDir
      sourceDistRoot = root -/- "source-dist"
  removeDirectory treePath
  prepare treePath
  runBuilderWithCmdOptions
      [Cwd $ sourceDistRoot -/- dirName]
      (Tar Create)
      ["chJf", ".." -/- tarName,  baseName]
      ["chJf", ".." -/- tarName] [baseName]


-- | This creates a symlink to the 'source' at 'target'
-- $tar -h$ will eventually copy the source into the tarball
-- This is also how `make sdist` works.
-- 1. It preserves relative symlinks
-- 2. It copies non-empty directories also. This is because git includes
--    directories in its output if they are non empty.
copyFileSourceDist :: FilePath -> FilePath -> Action ()
copyFileSourceDist source target = do
  isSymlink <- liftIO $ IO.pathIsSymbolicLink source
  if isSymlink then do
    link_target <- liftIO $ IO.getSymbolicLinkTarget source
    when (not $ isRelative link_target) $
      error ("source-dist: tried to create non-relative symlink in source dist: " ++ show link_target)
    putProgressInfo =<< renderAction ("Create symlink (" ++ link_target ++ ")") source target
    isDirectory <- liftIO $ IO.doesDirectoryExist source
    when (not isDirectory) $
      need [source]
    let createLink src tgt
          | isDirectory = liftIO $ IO.createDirectoryLink src tgt
          | otherwise = liftIO $ IO.createFileLink src tgt
    let dir = takeDirectory target
    liftIO $ IO.createDirectoryIfMissing True dir
    liftIO $ removeFile_ target
    createLink link_target target
  else do
    isDirectory <- liftIO $ IO.doesDirectoryExist source
    if isDirectory then do
      contents <- liftIO $ IO.listDirectory source
      when (not $ null contents) $ -- Git only includes directories in the output if they are empty
        error ("source-dist: non-empty dir" ++ show source)
      createDirectory target
    else createFileLink source target

prepareTestsuiteTree :: FilePath -> Action ()
prepareTestsuiteTree dest = do
  top <- topDirectory
  let testsuiteFiles = filter testFilter . split (=='\NUL')
      testFilter file = not (null file) && ("testsuite//" ?== file)
  files <- testsuiteFiles <$> askWithResources [] (target (vanillaContext Stage1 compiler) (Git ListFiles) [] [])
  forM_ files $ \source -> do
    let target = dest -/- source
    copyFileSourceDist (top -/- source) target

prepareWindowsExtraTree :: FilePath -> Action ()
prepareWindowsExtraTree dest = do
  top <- topDirectory

  files <- lines <$> askWithResources [] (target (vanillaContext Stage1 compiler) (Win32Tarballs ListTarballs) [] [])
  need files
  build (target (vanillaContext Stage1 compiler) (Win32Tarballs VerifyTarballs) [] [])

  createDirectory dest
  liftIO $ IO.createFileLink (top -/- "ghc-tarballs") (dest -/- "ghc-tarballs")

prepareTree :: FilePath -> Action ()
prepareTree dest = do
    out <- askWithResources [] (target (vanillaContext Stage1 compiler) (Git ListFiles) [] [])
    top <- topDirectory
    let files = ["GIT_COMMIT_ID", "VERSION"] ++ getFiles out
    need ["GIT_COMMIT_ID", "VERSION"]
    forM_ files $ \source -> do
      let target = dest -/- source
      copyFileSourceDist (top -/- source) target
    copyAlexHappyFiles
    copyBootFiles
  where

    getFiles = filter treeFilter . split (=='\NUL')
    treeFilter file = not (null file) && not ("testsuite//" ?== file)

    -- Copy files created by running ./boot
    copyBootFiles = do
      top <- topDirectory
      forM_ bootFiles $ \file -> do
        let src_file = top -/- file
            dest_file = top -/- dest -/- file
        createFileLink src_file dest_file
      -- And move ./boot so we can't accidentally call it in CI
      moveFile (dest -/- "boot") (dest -/- "boot.source")

    bootFiles =
      [ pkgPath process -/- "include" -/- "HsProcessConfig.h.in"
      , pkgPath process -/- "configure"
      , pkgPath ghcBignum -/- "configure"
      , pkgPath base -/- "configure"
      , pkgPath base -/- "include" -/- "HsBaseConfig.h.in"
      , pkgPath directory -/- "configure"
      , pkgPath directory -/- "HsDirectoryConfig.h.in"
      , pkgPath time -/- "configure"
      , pkgPath time -/- "lib" -/- "include" -/- "HsTimeConfig.h.in"
      , pkgPath unix -/- "configure"
      , pkgPath unix -/- "include" -/- "HsUnixConfig.h.in"
      , pkgPath terminfo -/- "configure"
      , "configure"
      , "aclocal.m4"
      , "mk" -/- "config.h.in" ]
      -- also include files generated by boot that are only used by the make build system
      ++ concatMap libBootFilesForMake
           [ base
           , ghcBootTh
           , ghcCompact
           , ghcHeap
           , ghcPrim
           , ghci
           , integerGmp
           , libiserv
           , templateHaskell
           , ghcBignum
           , cabal
           , cabalSyntax
           , win32
           , array
           , binary
           , bytestring
           , containers
           , deepseq
           , directory
           , exceptions
           , filepath
           , haskeline
           , hpc
           , mtl
           , parsec
           , pretty
           , process
           , stm
           , terminfo
           , text
           , time
           , transformers
           , unix
           , xhtml ]

    libBootFilesForMake pkg =
      [ pkgPath pkg -/- "ghc.mk"
      , pkgPath pkg -/- "GNUmakefile" ]

    copyAlexHappyFiles =
      forM_ alexHappyFiles $ \(stg, pkg, inp, out) -> do
        let ctx = Context stg pkg vanilla
            srcInputFile = dest -/- pkgPath pkg -/- inp
            generatedFile = dest -/- pkgPath pkg -/- out
            builder =
                case determineBuilder stg inp of
                  Just builder -> builder
                  Nothing -> error $ "Failed to determine builder for " ++ inp

        -- We first make sure that the generated file is... generated.
        build $ target ctx builder [srcInputFile] [generatedFile]

        -- We finally add a ".source" suffix to the input file to
        -- prevent it from being used when building GHC, since the
        -- generated file being there already should prevent
        -- the need for the original input.
        moveFile srcInputFile (srcInputFile <.> "source")

    -- (stage, package, input file, output file)
    alexHappyFiles =
        [ (stage0InTree , compiler,      "GHC/Cmm/Parser.y",   "GHC/Cmm/Parser.hs")
        , (stage0InTree , compiler,      "GHC/Cmm/Lexer.x",    "GHC/Cmm/Lexer.hs")
        , (stage0InTree , compiler,      "GHC/Parser.y",       "GHC/Parser.hs")
        , (stage0InTree , compiler,      "GHC/Parser/Lexer.x", "GHC/Parser/Lexer.hs")
        , (stage0InTree , compiler,      "GHC/Parser/HaddockLex.x", "GHC/Parser/HaddockLex.hs")
        , (stage0InTree , hpcBin,        "HpcParser.y",        "HpcParser.hs")
        , (stage0InTree , genprimopcode, "Parser.y",           "Parser.hs")
        , (stage0InTree , genprimopcode, "Lexer.x",            "Lexer.hs")
        ]
