packages feed

cabal-flatpak-0.0: src/Generate.hs

module Generate where

import qualified Flatpak
import Flatpak (ModuleItem(ModuleEmbed))

import qualified Cabal.Plan as Plan
import qualified Shell.Utility.Quote as Quote

import qualified Data.Map as Map
import qualified Data.Text as Text
import Data.Foldable (foldMap)
import Data.Map (Map)
import Data.Bool.HT (if')

import Text.Printf (printf)


archGHC :: Flatpak.Arch -> String
archGHC arch =
   case arch of
      Flatpak.I386 -> "i386"
      Flatpak.X86_64 -> "x86_64"
      Flatpak.ARM -> "armv7"
      Flatpak.AArch64 -> "aarch64"

formatVersion :: Plan.Ver -> String
formatVersion = Text.unpack . Plan.dispVer

ghcDirUrl :: Plan.PkgId -> String
ghcDirUrl (Plan.PkgId (Plan.PkgName compName) compVersion) =
   printf
      "https://downloads.haskell.org/%s/%s/"
      (Text.unpack compName)
      (formatVersion compVersion)

{- |
ghc-8.4.4-x86_64-deb9 is available but fails due to missing libnuma.so.1
-}
ghcNameScheme :: Plan.Ver -> String
ghcNameScheme ghcVer =
   if' (ghcVer >= Plan.Ver [8, 6,1]) "%s-%s-deb9-linux.tar.xz" $
   if' (ghcVer >= Plan.Ver [7,10,3]) "%s-%s-deb8-linux.tar.xz" $
   if' (ghcVer >= Plan.Ver [7, 8,1]) "%s-%s-unknown-linux-deb7.tar.xz" $
   "%s-%s-unknown-linux.tar.bz2"

ghcArchive :: Plan.PkgId -> Flatpak.Arch -> String
ghcArchive compiler@(Plan.PkgId _ compVersion) arch =
   printf (ghcNameScheme compVersion)
      (Text.unpack $ Plan.dispPkgId compiler)
      (archGHC arch)

tinfoPath :: Flatpak.Arch -> String
tinfoPath arch =
   case arch of
      Flatpak.I386 -> "/usr/lib/i386-linux-gnu/libtinfo.so"
      Flatpak.X86_64 -> "/usr/lib/x86_64-linux-gnu/libtinfo.so"
      Flatpak.ARM -> "/lib/arm-linux-gnueabihf/libtinfo.so"
      -- FixMe: What is the correct path?
      Flatpak.AArch64 -> "/lib/arm???-linux-gnueabihf/libtinfo.so"


projectMain :: Flatpak.Cabal -> (String, [Flatpak.Source])
projectMain project =
   (Flatpak.mainPackage project, Flatpak.mainSources project)

matchName :: String -> Plan.Unit -> Bool
matchName expected unit =
   case Plan.uPId unit of
      Plan.PkgId (Plan.PkgName name) _version -> expected == Text.unpack name


type Options = ([String], [String])

projectOptions :: Flatpak.Cabal -> Options
projectOptions project =
   (Flatpak.cabalConfigureOptions project, Flatpak.ghcOptions project)


ghcPkgCleanup :: Plan.PlanJson -> Flatpak.Arch -> [String]
ghcPkgCleanup plan arch =
   let archLinux =
         archGHC arch ++ "-linux-" ++
         Text.unpack (Plan.dispPkgId (Plan.pjCompilerId plan))
   in map (++ archLinux) $
         "/lib/" :
         "/share/doc/" :
         []

ghcCleanup :: Plan.PlanJson -> [String]
ghcCleanup plan =
   let (Plan.PkgId _ ghcVer) = Plan.pjCompilerId plan
       withVersion prg = prg ++ "-" ++ formatVersion ghcVer
   in map ("/" ++) $
         concat [
            map ("bin/" ++) $
               "ghc" : withVersion "ghc" :
               "ghc-pkg" : withVersion "ghc-pkg" :
               "ghci" : withVersion "ghci" :
               "haddock" : withVersion "haddock-ghc" :
               "runghc" : withVersion "runghc" :
               "runhaskell" :
               "hp2ps" :
               "hpc" :
               "hsc2hs" :
               [],
            map ("lib/" ++) $
               "libtinfo.so.5" :
               "debug" :
               withVersion "ghc" :
               [],
            map ("share/" ++) $
               "man" :
               ("doc/" ++ withVersion "ghc") :
               []
            ]

-- | Generate the module to download, compile and install GHC.
ghcModule :: Plan.PlanJson -> Flatpak.Arch -> Plan.Sha256 -> Flatpak.Module
ghcModule plan arch hash =
   let compiler = Plan.pjCompilerId plan
   in Flatpak.Module {
         Flatpak.name =
            Text.unpack (Plan.dispPkgId compiler) ++
               "-" ++ Flatpak.archString arch,
         Flatpak.onlyArches = [arch],
         Flatpak.buildsystem = "simple",
         Flatpak.builddir = False,
         Flatpak.configOpts = [],
         Flatpak.buildCommands =
            "mkdir -p /app/lib" :
            printf "ln -s %s /app/lib/libtinfo.so.5" (tinfoPath arch) :
            "./configure --prefix=/app" :
            "make install" :
            [],
         Flatpak.postInstall = [],
         Flatpak.cleanupModule = ghcCleanup plan,
         Flatpak.sources =
            [Flatpak.Source {
               Flatpak.typ = Flatpak.archive,
               Flatpak.url = ghcDirUrl compiler ++ ghcArchive compiler arch,
               Flatpak.sha256 = Just hash
             }]
      }

packageURLs :: Plan.Unit -> (String, String)
packageURLs pkg =
   let pkgId@(Plan.PkgId (Plan.PkgName name) _version) = Plan.uPId pkg
       pkgName = Text.unpack name
       pkgNameVer = Text.unpack $ Plan.dispPkgId pkgId
   in (printf "https://hackage.haskell.org/package/%s/%s.tar.gz"
         pkgNameVer pkgNameVer,
       printf "https://hackage.haskell.org/package/%s/%s.cabal"
         pkgNameVer pkgName)

packageSources ::
   (String, [Flatpak.Source]) -> Flatpak.SourceType ->
   Plan.Unit -> [Flatpak.Source]
packageSources (mainPkg, mainSrcs) typ pkg =
   let (pkgUrl, cabalUrl) = packageURLs pkg
   in if' (matchName mainPkg pkg) mainSrcs $
      Flatpak.Source {
         Flatpak.typ = typ,
         Flatpak.url = pkgUrl,
         Flatpak.sha256 = Plan.uSha256 pkg
      } :
      Flatpak.Source {
         Flatpak.typ = Flatpak.File,
         Flatpak.url = cabalUrl,
         Flatpak.sha256 = Plan.uCabalSha256 pkg
      } :
      []

ghcOption :: String -> String
ghcOption opt = "--ghc-option=" ++ Quote.minimal opt

{- |
Generate a module to download, build and install a package from Hackage.
Due to the Cabal package "revision" not being part of a package,
we have to download the package,
extract and then overwrite the .cabal file with the latest revision.
-}
modul ::
   (String, [Flatpak.Source]) -> Options -> Map String Flatpak.PackageHook ->
   Plan.Unit -> Flatpak.Module
modul main (cabalCfgOptions, ghcOptions) hookMap pkg =
   let (Plan.PkgId (Plan.PkgName name) _version) = Plan.uPId pkg
       nameStr = Text.unpack name
   in Flatpak.Module {
         Flatpak.name = nameStr,
         Flatpak.onlyArches = [],
         Flatpak.buildsystem = "simple",
         Flatpak.builddir = False,
         Flatpak.configOpts = [],
         Flatpak.buildCommands =
            "echo '#! /usr/bin/env runhaskell' >Setup.txt" :
            "echo '> import Distribution.Simple' >>Setup.txt" :
            "echo '> main = defaultMain' >>Setup.txt" :
            "ln -s Setup.txt Setup.lhs || true" :
            unwords ("runhaskell Setup configure --prefix=/app" :
                     cabalCfgOptions) :
            unwords ("runhaskell Setup build" : map ghcOption ghcOptions) :
            "runhaskell Setup install" :
            [],
         Flatpak.postInstall =
            foldMap Flatpak.postInstallHook $ Map.lookup nameStr hookMap,
         Flatpak.cleanupModule = [],
         Flatpak.sources = packageSources main Flatpak.archive pkg
      }

manifest ::
   Plan.PlanJson ->
   [(Flatpak.Arch, Plan.Sha256)] -> [Plan.Unit] -> Flatpak.Cabal -> Flatpak.T
manifest plan archs units project =
   let base = Flatpak.base project
       hookMap =
         Map.fromList $ map (\hook -> (Flatpak.nameHook hook, hook)) $
         Flatpak.packageHooks project
   in
   base {
      Flatpak.cleanup =
         concatMap (ghcPkgCleanup plan . fst) archs ++ Flatpak.cleanup base,
      Flatpak.modules =
         map (ModuleEmbed . uncurry (ghcModule plan)) archs ++
         Flatpak.modules base ++
         map
            (ModuleEmbed .
             modul (projectMain project) (projectOptions project) hookMap)
            units
   }



cabalHash :: Flatpak.Arch -> Maybe String
cabalHash arch =
   case arch of
      Flatpak.I386 -> Just $
         "b2da736cc27609442b10f77fc1a687aba603a7a33045b722dbf1a0066fade198"
      Flatpak.X86_64 -> Just $
         "6136c189ffccaa39916f9cb5788f757166444a2d0c473b987856a79ecbf0c714"
      _ -> Nothing

cabalInstallModule :: Flatpak.Arch -> Flatpak.Module
cabalInstallModule arch =
   let cabalVerName = "cabal-install-2.4.1.0"
       url =
         printf
            "https://downloads.haskell.org/cabal/%s/%s-%s-unknown-linux.tar.xz"
               cabalVerName cabalVerName (archGHC arch)
   in Flatpak.Module {
         Flatpak.name = cabalVerName ++ "-" ++ Flatpak.archString arch,
         Flatpak.onlyArches = [arch],
         Flatpak.buildsystem = "simple",
         Flatpak.builddir = False,
         Flatpak.configOpts = [],
         Flatpak.buildCommands = ["install cabal /app/bin"],
         Flatpak.postInstall = [],
         Flatpak.cleanupModule = ["/bin/cabal"],
         Flatpak.sources =
            [Flatpak.Source {
               Flatpak.typ =
                  Flatpak.Archive {Flatpak.stripComponents = 0},
               Flatpak.url = url,
               Flatpak.sha256 = Plan.parseSha256 . Text.pack =<< cabalHash arch
             }]
      }

allInOneModule ::
   (String, [Flatpak.Source]) -> Options -> [String] ->
   [Plan.Unit] -> Flatpak.Module
allInOneModule main (cabalCfgOptions, ghcOptions) postInstall pkgs =
   let dispPkgId = Text.unpack . Plan.dispPkgId
   in Flatpak.Module {
         Flatpak.name = "haskell-parts",
         Flatpak.onlyArches = [],
         Flatpak.buildsystem = "simple",
         Flatpak.builddir = False,
         Flatpak.configOpts = [],
         Flatpak.buildCommands =
            (flip concatMap pkgs $ \pkg ->
               let pkgId@(Plan.PkgId (Plan.PkgName name) _version) =
                     Plan.uPId pkg
                   pkgName = Text.unpack name
                   pkgVerName = dispPkgId pkgId
               in printf "gunzip %s.tar.gz" pkgVerName :
                  printf "mkdir %s" pkgVerName :
                  printf "mv %s.cabal %s/" pkgName pkgVerName :
                  printf "tar rf %s.tar %s/%s.cabal"
                    pkgVerName pkgVerName pkgName :
                  printf "gzip %s.tar" pkgVerName :
                  []) ++
            "mkdir .cabal" :
            "touch .cabal/config" :
            unwords
               ("cabal --config-file=.cabal/config install" :
                "--offline --prefix=/app" :
                cabalCfgOptions ++ map ghcOption ghcOptions ++
                map (printf "%s.tar.gz" . dispPkgId . Plan.uPId) pkgs) :
            [],
         Flatpak.postInstall = postInstall,
         Flatpak.cleanupModule = [],
         Flatpak.sources = concatMap (packageSources main Flatpak.File) pkgs
      }

manifestCabalInstall ::
   Plan.PlanJson ->
   [(Flatpak.Arch, Plan.Sha256)] -> [Plan.Unit] -> Flatpak.Cabal -> Flatpak.T
manifestCabalInstall plan archs pkgs project =
   let base = Flatpak.base project in
   base {
      Flatpak.cleanup =
         concatMap (ghcPkgCleanup plan . fst) archs ++ Flatpak.cleanup base,
      Flatpak.modules =
         (map ModuleEmbed $
          concatMap
            (\(arch,ghcHash) ->
               [ghcModule plan arch ghcHash, cabalInstallModule arch])
            archs) ++
         Flatpak.modules base ++
         (ModuleEmbed $
          allInOneModule (projectMain project) (projectOptions project)
            (concatMap Flatpak.postInstallHook $ Flatpak.packageHooks project)
            pkgs) :
         []
   }