    {-# LANGUAGE OverloadedStrings #-}

    -----------------------------------------------------------------------------
    -- |
    -- Module      :  Distribution.Simple.Program.Ar
    -- Copyright   :  Duncan Coutts 2009
    --
    -- Maintainer  :  cabal-devel@haskell.org
    -- Portability :  portable
    --
    -- This module provides an library interface to the @ar@ program.

    module Distribution.Simple.Program.Ar (
        createArLibArchive,
        multiStageProgramInvocation
      ) where

    import Control.Monad (when, unless)
    import qualified Data.ByteString as BS
    import qualified Data.ByteString.Char8 as BS8
    import Data.Char (isSpace)
    import Distribution.Compat.CopyFile (filesEqual)
    import Distribution.Simple.LocalBuildInfo (LocalBuildInfo(..))
    import Distribution.Simple.Program
             ( arProgram, requireProgram )
    import Distribution.Simple.Program.Run
             ( programInvocation, multiStageProgramInvocation
             , runProgramInvocation )
    import qualified Distribution.Simple.Program.Strip as Strip
             ( stripLib )
    import Distribution.Simple.Utils
             ( dieWithLocation, withTempDirectory )
    import Distribution.System
             ( Arch(..), OS(..), Platform(..) )
    import Distribution.Verbosity
             ( Verbosity, deafening, verbose )
    import System.Directory (doesFileExist, renameFile)
    import System.FilePath ((</>), splitFileName)
    import System.IO
             ( Handle, IOMode(ReadWriteMode), SeekMode(AbsoluteSeek)
             , hFileSize, hSeek, withBinaryFile )

    -- | Call @ar@ to create a library archive from a bunch of object files.
    --
    createArLibArchive :: Verbosity -> LocalBuildInfo
                       -> FilePath -> [FilePath] -> IO ()
    createArLibArchive verbosity lbi targetPath files = do
      (ar, _) <- requireProgram verbosity arProgram progConf

      let (targetDir, targetName) = splitFileName targetPath
      withTempDirectory verbosity targetDir "objs" $ \ tmpDir -> do
      let tmpPath = tmpDir </> targetName

      -- The args to use with "ar" are actually rather subtle and system-dependent.
      -- In particular we have the following issues:
      --
      --  -- On OS X, "ar q" does not make an archive index. Archives with no
      --     index cannot be used.
      --
      --  -- GNU "ar r" will not let us add duplicate objects, only "ar q" lets us
      --     do that. We have duplicates because of modules like "A.M" and "B.M"
      --     both make an object file "M.o" and ar does not consider the directory.
      --
      -- Our solution is to use "ar r" in the simple case when one call is enough.
      -- When we need to call ar multiple times we use "ar q" and for the last
      -- call on OSX we use "ar qs" so that it'll make the index.

      let simpleArgs  = case hostOS of
                 OSX -> ["-r", "-s"]
                 _   -> ["-r"]

          initialArgs = ["-q"]
          finalArgs   = case hostOS of
                 OSX -> ["-q", "-s"]
                 _   -> ["-q"]

          extraArgs   = verbosityOpts verbosity ++ [tmpPath]

          simple  = programInvocation ar (simpleArgs  ++ extraArgs)
          initial = programInvocation ar (initialArgs ++ extraArgs)
          middle  = initial
          final   = programInvocation ar (finalArgs   ++ extraArgs)

      sequence_
            [ runProgramInvocation verbosity inv
            | inv <- multiStageProgramInvocation
                       simple (initial, middle, final) files ]

      when stripLib $ Strip.stripLib verbosity platform progConf tmpPath
      unless (hostArch == Arm) $ -- See #1537
        wipeMetadata tmpPath
      equal <- filesEqual tmpPath targetPath
      unless equal $ renameFile tmpPath targetPath

      where
        progConf = withPrograms lbi
        stripLib = stripLibs    lbi
        platform@(Platform hostArch hostOS) = hostPlatform lbi
        verbosityOpts v | v >= deafening = ["-v"]
                        | v >= verbose   = []
                        | otherwise      = ["-c"]

    -- | @ar@ by default includes various metadata for each object file in their
    -- respective headers, so the output can differ for the same inputs, making
    -- it difficult to avoid re-linking. GNU @ar@(1) has a deterministic mode
    -- (@-D@) flag that always writes zero for the mtime, UID and GID, and 0644
    -- for the file mode. However detecting whether @-D@ is supported seems
    -- rather harder than just re-implementing this feature.
    wipeMetadata :: FilePath -> IO ()
    wipeMetadata path = do
        -- Check for existence first (ReadWriteMode would create one otherwise)
        exists <- doesFileExist path
        unless exists $ wipeError "Temporary file disappeared"
        withBinaryFile path ReadWriteMode $ \ h -> hFileSize h >>= wipeArchive h

      where
        wipeError msg = dieWithLocation path Nothing $
            "Distribution.Simple.Program.Ar.wipeMetadata: " ++ msg
        archLF = "!<arch>\x0a" -- global magic, 8 bytes
        x60LF = "\x60\x0a" -- header magic, 2 bytes
        metadata = BS.concat
            [ "0           " -- mtime, 12 bytes
            , "0     " -- UID, 6 bytes
            , "0     " -- GID, 6 bytes
            , "0644    " -- mode, 8 bytes
            ]
        headerSize :: Int
        headerSize = 60

        -- http://en.wikipedia.org/wiki/Ar_(Unix)#File_format_details
        wipeArchive :: Handle -> Integer -> IO ()
        wipeArchive h archiveSize = do
            global <- BS.hGet h (BS.length archLF)
            unless (global == archLF) $ wipeError "Bad global header"
            wipeHeader (toInteger $ BS.length archLF)

          where
            wipeHeader :: Integer -> IO ()
            wipeHeader offset = case compare offset archiveSize of
                EQ -> return ()
                GT -> wipeError (atOffset "Archive truncated")
                LT -> do
                    header <- BS.hGet h headerSize
                    unless (BS.length header == headerSize) $
                        wipeError (atOffset "Short header")
                    let magic = BS.drop 58 header
                    unless (magic == x60LF) . wipeError . atOffset $
                        "Bad magic " ++ show magic ++ " in header"

                    let name = BS.take 16 header
                    let size = BS.take 10 $ BS.drop 48 header
                    objSize <- case reads (BS8.unpack size) of
                        [(n, s)] | all isSpace s -> return n
                        _ -> wipeError (atOffset "Bad file size in header")

                    let replacement = BS.concat [ name, metadata, size, magic ]
                    unless (BS.length replacement == headerSize) $
                        wipeError (atOffset "Something has gone terribly wrong")
                    hSeek h AbsoluteSeek offset
                    BS.hPut h replacement

                    let nextHeader = offset + toInteger headerSize +
                            -- Odd objects are padded with an extra '\x0a'
                            if odd objSize then objSize + 1 else objSize
                    hSeek h AbsoluteSeek nextHeader
                    wipeHeader nextHeader

              where
                atOffset msg = msg ++ " at offset " ++ show offset