packages feed

cabal-flatpak (empty) → 0.0

raw patch · 11 files changed

+1137/−0 lines, 11 filesdep +aesondep +aeson-prettydep +basesetup-changed

Dependencies added: aeson, aeson-pretty, base, bytestring, cabal-plan, containers, http-client, http-client-tls, http-types, optparse-applicative, pathtype, shell-utility, text, utility-ht

Files

+ Changes.md view
@@ -0,0 +1,5 @@+# Change log for the `cabal-flatpak` package++## 0.0++ * First experimental version inspired by `stackpak`.
+ LICENSE view
@@ -0,0 +1,31 @@+Copyright (c) 2019, Henning Thielemann++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * The names of contributors may not be used to endorse or promote+      products derived from this software without specific prior+      written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Makefile view
@@ -0,0 +1,20 @@+FLATPAK = $$FLATPAKBUILD++%.html:	%.md+	pandoc --output $@ $<++dist-newstyle/cache/plan.json:+	cabal new-build --dry-run++flatpak.json:	flatpak.cabal.json dist-newstyle/cache/plan.json+	cabal run -- --cabal-install --arch x86_64 --arch i386 $< $@++repo-%:	flatpak.json+	flatpak-builder --force-clean --arch=$* --repo=$(FLATPAK)/repository --state-dir=$(FLATPAK)/builder/ $(FLATPAK)/build/cabal-flatpak $<+	touch $@++cabal-flatpak.%.flatpak:	repo-%+	flatpak build-bundle --arch=$* $(FLATPAK)/repository $@ com.github.thielema.cabal-flatpak \+	  --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo++flatpak-all:	cabal-flatpak.x86_64.flatpak cabal-flatpak.i386.flatpak
+ ReadMe.md view
@@ -0,0 +1,172 @@+## Scope++This program generates a large Flatpak manifest in JSON format+with the versions of all transitively imported Haskell packages.+This is required by Flathub+since this platform downloads all source files for you+and lets you build only in a sandbox without access to the internet.+The detailed version information can also be useful for you+for reproducible builds.+However, for maintaining your own Flatpak repository+or for distributing Flatpak bundles+it is not strictly necessary to have such a verbose manifest.+You may get results quicker by writing you own manifest+which simply runs `cabal-install` or `stack`.+`stack` should give you reproducible builds+but its package database contains only a subset of Hackage.+`cabal-install` gives you access to more Haskell packages,+but the build may fail at a later point due to lax version bounds+in dependent packages.+++## Usage++### Create a Flatpak manifest++The usage is a bit cumbersome,+because for Flathub you may only build manifests+for released versions of your package,+whereas you certainly want to manage manifests+in the working copy of your repository.++First let `cabal` build a `plan.json` for your released package:++~~~~+$ cd /tmp+$ cabal unpack my-package-0.1.2+$ cd my-package-0.1.2+$ cabal new-build --dry-run --disable-tests --disable-benchmarks+~~~~++`cabal-flatpak` reads `plan.json` via the `cabal-plan` library.++Now create a `flatpak.cabal.json` file in your working copy.+It contains a custom JSON object with information needed by `cabal-flatpak`+and also a template for the generated manifest.+For an example see the configuration file for `cabal-flatpak`.++~~~~+$ cd /path/to/my-package+$ cabal-flatpak --directory=/tmp/my-package-0.1.2 flatpak.cabal.json flatpak.json+~~~~++The `--arch` option allows you to put build information+for one or more architectures into one manifest.+Usually, only `ghc` and `cabal-install` binaries depend on the architecture+and the whole lot of Haskell packages can be build with the same commands.++There are two build modes:+One builds all modules individually using plain `Cabal`,+the other one builds all modules in one go using `cabal-install`.+You can enable the second mode using the `--cabal-install` option.+These are the differences:++* `Cabal` mode needs less dependencies.+  `cabal-install` needs a pre-built binary for your architecture.++* `Cabal` mode can pass options to the build of specific packages.+  `cabal-install` can only pass options to all packages at once.+  If `-fbuildExamples` means something different to different Haskell packages,+  then this will fail.+  Even in `cabal-install` mode we need to preprocess each package+  and in this stage we could alter flag switches by patching Cabal files.+  We could also build packages with requested flags in separate stages.+  Currently, we don't try any of these strategies.++* `cabal-install` enables parallel builds.+  This builds significantly faster.+  `Cabal` mode on the other hand allows Flatpak to cache build results+  of individual packages.+  This can accelerate re-builds.+  However, Flatpak does not know the dependency graph+  and thus simply rebuilds anything after a module that must be rebuilt.++Unfortunately, you cannot simply switch between the two modes+simply by adding or removing the `--cabal-install` option.+The reason is that you must maintain the data for the main package+manually in the `"main-sources"` object+and `Cabal` requires the source tarball unpacked+whereas `cabal-install` needs it packed.+Thus `Cabal` needs `"type": "archive"`+and `cabal-install` needs `"type": "file"`.++For some packages you need to build dependencies on external C packages.+`cabal-flatpak` cannot generate according build instructions for you.+However, you can re-use build instructions you found useful.+Flatpak-builder supports this itself.+In the `"modules"` list you cannot only put module JSON objects,+but also plain strings.+Such a string is interpreted as path to a separate file+containing a Flatpak module JSON object.+I add such JSON files to the FFI packages I maintain.+++### Build the Flatpak package++You may refer to the `Makefile` that is shipped with `cabal-flatpak`+for how to eventually build the Flatpak package.+The command line is:++~~~~+$ flatpak-builder --force-clean --repo=$FLATPAK/repository --state-dir=$FLATPAK/builder/ $FLATPAK/build/my-package $<+~~~~++Flatpak consumes pretty much storage+thus I set $FLATPAK to a directory on a separate harddisk partition.+The `--repo` option points to your Flatpak repository.+This is where `flatpak-builder` stores the compiled package.+The Flatpak repository contains all versions of all your Flatpak packages.+If another user has access to it,+she can easily install and update Flatpak packages.+The `--state-dir` option points to a directory+that caches all downloads and build artifacts of Flatpak.+You may share it between different projects.+The `DIRECTORY` argument names the path+to where Flatpak builds your project.+It may not be shared between projects,+but you can safely delete it after a build+and the `--force-clean` option triggers exactly this+when you re-build your project.+The directories specified by `--state-dir` and `DIRECTORY`+must reside on the same file system.++You may also extract a single package file+for a certain version of your package from the repository.+This can be handy for a one-time install+but disallows the user to easily get updates of your program.++~~~~+$ flatpak build-bundle $FLATPAK/repository \+    my-package-0.1.2.flatpak com.my_domain.my-package \+    --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo+~~~~+++## Known issues++### Cabal revisions++Cabal package description files can be updated on Hackage+without altering the package version.+Every update increases the Cabal file revision.+Unfortunately, `plan.json` does not contain the revision number,+it just refers to the currently most recent Cabal file for each Haskell package.+This is a fundamental problem since Flatpak builds must be reproducible.++We have currently no solution for that problem.+If someone updates the Cabal file of one of your dependencies,+then the Flatpak build will break with an error due to a SHA256 hash mismatch.+In this case you should find out the original revision+and add that to the Hackage path in the Flatpak manifest.+E.g. if the path to a Cabal file is say+`https://hackage.haskell.org/package/cabal-flatpak-0.0/cabal-flatpak.cabal`+then the path to revision 1 is:+`https://hackage.haskell.org/package/cabal-flatpak-0.0/revision/1.cabal`.++We could try to solve the problem by downloading Cabal file revisions+from Hackage for every imported package+until we find the one with a matching SHA256 hash.+We could try to do this offline and scan Cabal's local package database+using a program like:+https://github.com/haskell-hvr/hackage-index .+None of these ideas is implemented, yet.
+ Setup.lhs view
@@ -0,0 +1,3 @@+#! /usr/bin/env runhaskell+> import Distribution.Simple+> main = defaultMain
+ cabal-flatpak.cabal view
@@ -0,0 +1,59 @@+Name:           cabal-flatpak+Version:        0.0+License:        BSD3+License-File:   LICENSE+Author:         Henning Thielemann <haskell@henning-thielemann.de>+Maintainer:     Henning Thielemann <haskell@henning-thielemann.de>+Homepage:       http://hub.darcs.net/thielema/cabal-flatpak/+Category:       Distribution+Synopsis:       Generate a FlatPak manifest from a Cabal package description+Description:+  Generate a FlatPak manifest from a Cabal package description.+  .+  Inspired by Richard Szibele's stackpak:+  <https://gitlab.com/rszibele/stackpak#readme>+  .+  See also David Lettier's post on Flatpak for Haskell:+  <https://medium.com/@lettier/how-to-flatpak-a-haskell-app-into-flathub-86ef6d69e94d>+Tested-With:    GHC==8.6.5+Cabal-Version:  >=1.6+Build-Type:     Simple+Extra-Source-Files:+  Makefile+  ReadMe.md+  Changes.md+  flatpak.cabal.json++Source-Repository this+  Tag:         0.0+  Type:        darcs+  Location:    http://hub.darcs.net/thielema/cabal-flatpak/++Source-Repository head+  Type:        darcs+  Location:    http://hub.darcs.net/thielema/cabal-flatpak/++Executable cabal-flatpak+  Build-Depends:+    cabal-plan >=0.5 && <0.6,+    http-client-tls >=0.3.5.3 && <0.4,+    http-client >=0.5.12 && <0.7,+    http-types >=0.10 && <0.13,+    aeson-pretty >=0.8.7 && <0.9,+    aeson >=1.2 && <1.5,+    shell-utility >=0.0 && <0.1,+    optparse-applicative >=0.11 && <0.16,+    pathtype >=0.8 && <0.9,+    text >=1.2 && <1.3,+    bytestring >=0.9 && <0.11,+    containers >=0.4.2 && <0.7,+    utility-ht >=0.0.12 && <0.1,+    base >=4.5 && <5++  GHC-Options:    -Wall+  Hs-Source-Dirs: src+  Main-Is:        Main.hs+  Other-Modules:+    Generate+    Flatpak+    Option
+ flatpak.cabal.json view
@@ -0,0 +1,31 @@+{+	"base":+	{+		"app-id": "com.github.thielema.cabal-flatpak",+		"runtime": "org.freedesktop.Platform",+		"runtime-version": "18.08",+		"sdk": "org.freedesktop.Sdk",+		"command": "cabal-flatpak",+		"finish-args": [+			"--share=network",+			"--filesystem=host"+		],+		"cleanup": [],+		"cleanup-commands": [],+		"modules": []+	},++	"main-package": "cabal-flatpak",+	"main-sources": [+		{+			"url": "https://hackage.haskell.org/package/cabal-flatpak-0.0/candidate/cabal-flatpak-0.0.tar.gz",+			"type": "file",+			"sha256": "112e0dee2ffa9e71855ac5350ce26121867434c507c4e660734a885511683f1c"+		},+		{+			"url": "https://hackage.haskell.org/package/cabal-flatpak-0.0/candidate/cabal-flatpak.cabal",+			"type": "file",+			"sha256": "f4d54935b16f55a406a3844990990347e5ddfdfd147ac647791aebda244d64be"+		}+	]+}
+ src/Flatpak.hs view
@@ -0,0 +1,283 @@+module Flatpak (+   SourceType(..), archive, Source(..),+   Module(..), ModuleItem(ModuleEmbed),+   Arch(..), archString, T(..),+   PackageHook(..), Cabal(..),+   ) where++import qualified Shell.Utility.ParseArgument as ParseArg++import qualified Cabal.Plan as Plan++import qualified Data.Aeson as Json+import qualified Data.Text as Text+import qualified Data.Monoid.HT as Mn+import Data.Aeson.Types (Parser, Pair)+import Data.Aeson (FromJSON, ToJSON, parseJSON, toJSON, withObject)+import Data.Tuple.HT (mapFst)+import Data.Monoid (mempty, (<>))++import Text.Printf (printf)++import Control.Monad (mzero)+import Control.Applicative (pure, (<*>), (<$>), (<|>))+++infixr 8 .:, .:?, .:*++(.:) :: FromJSON a => Json.Object -> String -> Parser a+o .: field  =  o Json..: Text.pack field++(.:?) :: FromJSON a => Json.Object -> (String, a) -> Parser a+o .:? (field, deflt)  =  o Json..:? Text.pack field Json..!= deflt++(.:*) :: FromJSON a => Json.Object -> String -> Parser [a]+o .:* field  =  o .:? (field, [])+++infixr 8 .=, .=*++(.=) :: ToJSON a => String -> (object -> a) -> object -> [Pair]+(field .= a) obj  =  [Text.pack field Json..= a obj]++(.=*) :: (ToJSON a) => String -> (object -> [a]) -> object -> [Pair]+(field .=* a) obj  =  Mn.when (not $ null $ a obj) ((field .= a) obj)++object :: (object -> [Pair]) -> object -> Json.Value+object fields = Json.object . fields+++parseEnum ::+   (Enum a, Bounded a) => (a -> String) -> String -> Json.Value -> Parser a+parseEnum fmt nm =+   Json.withText nm $ \txt ->+      case Text.unpack txt of+         str ->+            case ParseArg.enumMaybe fmt str of+               Just x -> return x+               Nothing -> fail $ printf "unknown %s type: %s" nm str+++data SourceType =+      Archive {stripComponents :: Int} |+      Git | Bzr | File | Dir | Script | Shell | Patch | ExtraData+   deriving (Show)++archive :: SourceType+archive = Archive {stripComponents = 1}++pairsFromSourceType :: SourceType -> [Pair]+pairsFromSourceType t =+   let simple str = [("type", Json.toJSON str)]+   in map (mapFst Text.pack) $+      case t of+         Archive {stripComponents = strip} ->+            simple "archive" +++            Mn.when (strip/=1) [("strip-components", Json.toJSON strip)]+         Git       -> simple "git"+         Bzr       -> simple "bzr"+         File      -> simple "file"+         Dir       -> simple "dir"+         Script    -> simple "script"+         Shell     -> simple "shell"+         Patch     -> simple "patch"+         ExtraData -> simple "extra-data"++parseSourceType :: Json.Object -> String -> Parser SourceType+parseSourceType obj t =+   case t of+      "archive"    -> fmap Archive $ obj .:? ("strip-components", 1)+      "git"        -> return Git+      "bzr"        -> return Bzr+      "file"       -> return File+      "dir"        -> return Dir+      "script"     -> return Script+      "shell"      -> return Shell+      "patch"      -> return Patch+      "extra-data" -> return ExtraData+      _ -> mzero++data Source =+   Source {+      typ :: SourceType,+      url :: String,+      sha256 :: Maybe Plan.Sha256+   } deriving (Show)++instance FromJSON Source where+   parseJSON =+      withObject "Source" $ \o -> pure Source+         <*> (parseSourceType o =<< o .: "type")+         <*> o .: "url"+         <*> o .: "sha256"++instance ToJSON Source where+   toJSON = object $+      (pairsFromSourceType . typ) <>+      ("url" .= url) <>+      ("sha256" .= sha256) <>+      mempty+++data Arch = I386 | X86_64 | ARM | AArch64+   deriving (Show, Enum, Bounded)++archString :: Arch -> String+archString arch =+   case arch of+      I386 -> "i386"+      X86_64 -> "x86_64"+      ARM -> "arm"+      AArch64 -> "aarch64"++instance FromJSON Arch where+   parseJSON = parseEnum archString "arch"++instance ToJSON Arch where+   toJSON = Json.toJSON . archString+++data Module =+   Module {+      name :: String,+      onlyArches :: [Arch],+      buildsystem :: String,+      builddir :: Bool,+      configOpts :: [String],+      buildCommands :: [String],+      postInstall :: [String],+      cleanupModule :: [String],+      sources :: [Source]+   } deriving (Show)++instance FromJSON Module where+   parseJSON =+      withObject "Module" $ \o -> pure Module+         <*> o .:  "name"+         <*> o .:* "only-arches"+         <*> o .:  "buildsystem"+         <*> o .:  "builddir"+         <*> o .:* "config-opts"+         <*> o .:* "build-commands"+         <*> o .:* "post-install"+         <*> o .:* "cleanup"+         <*> o .:* "sources"++instance ToJSON Module where+   toJSON = object $+      ("name" .= name) <>+      ("only-arches" .=* onlyArches) <>+      ("buildsystem" .= buildsystem) <>+      ("builddir" .= builddir) <>+      ("config-opts" .=* configOpts) <>+      ("build-commands" .=* buildCommands) <>+      ("post-install" .=* postInstall) <>+      ("cleanup" .=* cleanupModule) <>+      ("sources" .=* sources) <>+      mempty+++data ModuleItem = ModuleEmbed Module | ModuleInclude String+   deriving (Show)++instance FromJSON ModuleItem where+   parseJSON v =+      ModuleInclude <$> parseJSON v+      <|>+      ModuleEmbed <$> parseJSON v++instance ToJSON ModuleItem where+   toJSON m =+      case m of+         ModuleEmbed modu -> toJSON modu+         ModuleInclude path -> toJSON path+++data T =+   Cons {+      appId :: String,+      runtime :: String,+      runtimeVersion :: String,+      sdk :: String,+      command :: String,+      finishArgs :: [String],+      cleanup :: [String],+      cleanupCommands :: [String],+      modules :: [ModuleItem]+   } deriving (Show)++instance FromJSON T where+   parseJSON =+      withObject "Flatpak" $ \o -> pure Cons+         <*> o .:  "app-id"+         <*> o .:  "runtime"+         <*> o .:  "runtime-version"+         <*> o .:  "sdk"+         <*> o .:  "command"+         <*> o .:  "finish-args"+         <*> o .:* "cleanup"+         <*> o .:* "cleanup-commands"+         <*> o .:  "modules"++instance ToJSON T where+   toJSON = object $+      ("app-id" .= appId) <>+      ("runtime" .= runtime) <>+      ("runtime-version" .= runtimeVersion) <>+      ("sdk" .= sdk) <>+      ("command" .= command) <>+      ("finish-args" .= finishArgs) <>+      ("cleanup" .=* cleanup) <>+      ("cleanup-commands" .=* cleanupCommands) <>+      ("modules" .= modules) <>+      mempty++++data PackageHook =+   PackageHook {+      nameHook :: String,+      postInstallHook :: [String]+   } deriving (Show)++instance FromJSON PackageHook where+   parseJSON =+      withObject "PackageHook" $ \o -> pure PackageHook+         <*> o .:  "name"+         <*> o .:* "post-install"++instance ToJSON PackageHook where+   toJSON = object $+      ("name" .= nameHook) <>+      ("post-install" .=* postInstallHook) <>+      mempty+++data Cabal =+   Cabal {+      base :: T,+      mainPackage :: String,+      mainSources :: [Source],+      packageHooks :: [PackageHook],+      cabalConfigureOptions, ghcOptions :: [String]+   } deriving (Show)++instance FromJSON Cabal where+   parseJSON =+      withObject "Cabal" $ \o -> pure Cabal+         <*> o .:  "base"+         <*> o .:  "main-package"+         <*> o .:  "main-sources"+         <*> o .:* "package-hooks"+         <*> o .:* "cabal-configure-options"+         <*> o .:* "ghc-options"++instance ToJSON Cabal where+   toJSON = object $+      ("base" .= base) <>+      ("main-package" .= mainPackage) <>+      ("main-sources" .= mainSources) <>+      ("package-hooks" .= packageHooks) <>+      ("cabal-configure-options" .=* cabalConfigureOptions) <>+      ("ghc-options" .=* ghcOptions) <>+      mempty
+ src/Generate.hs view
@@ -0,0 +1,324 @@+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) :+         []+   }
+ src/Main.hs view
@@ -0,0 +1,142 @@+module Main where++import qualified Flatpak+import qualified Generate++import qualified Cabal.Plan as Plan++import qualified System.Path.Directory as PathDir+import qualified System.Path as Path++import qualified Option+import qualified Options.Applicative as OP+import qualified Shell.Utility.ParseArgument as ParseArg+import qualified Shell.Utility.Verbosity as Verbosity+import qualified Shell.Utility.Log as Log++import qualified Network.HTTP.Client as Http+import Network.HTTP.Client.TLS (tlsManagerSettings)+import Network.HTTP.Types.Status (statusCode)++import Control.Monad (when)++import qualified Data.Aeson.Encode.Pretty as JsonPretty+import qualified Data.Aeson as Json+import qualified Data.ByteString.Lazy.Char8 as BL+import qualified Data.ByteString.Char8 as B+import qualified Data.Text.Encoding as TextEnc+import qualified Data.Text as Text+import qualified Data.Traversable as Trav+import qualified Data.Graph as Graph+import qualified Data.Map as Map+import qualified Data.Set as Set+import qualified Data.List.HT as ListHT+import Data.Tuple.HT (mapFst, snd3)+import Data.Map (Map)+import Data.Set (Set)++import Text.Printf (printf)+++topSort :: Ord a => Map a (Set a) -> [a]+topSort m =+   reverse $+   (\(graph, lookupVertex, _) ->+      map (snd3 . lookupVertex) $ Graph.topSort graph) $+   Graph.graphFromEdges $ Map.elems $+   Map.mapWithKey (\k v -> ((), k, Set.toList v)) m++httpsGet :: String -> IO BL.ByteString+httpsGet url = do+   response <- do+      request <- Http.parseRequest url+      manager <- Http.newManager tlsManagerSettings+      Http.httpLbs request manager+   case statusCode $ Http.responseStatus response of+      200  -> return $ Http.responseBody response+      code -> fail $ "HTTPS get failed with status code: " ++ show code+++main :: IO ()+main = do+   opt <- OP.execParser Option.info+   dir <-+      maybe (fmap Path.toAbsRel PathDir.getCurrentDirectory) return $+      Option.projectDir opt+   plan <-+      Plan.findAndDecodePlanJson $+      Plan.ProjectRelativeToDir $ Path.toString dir+   projectJson <-+      either fail return . Json.eitherDecode+      =<< (BL.readFile $ Path.toString $ Option.input opt)++   {-+   Only cabal >= 2.4.1.0 provides the SHA 256 hashes.+   see: https://hackage.haskell.org/package/cabal-plan-0.5.0.0/docs/Cabal-Plan.html#v:uCabalSha256+   -}+   let minVersion = Plan.Ver [2,4,1,0]+       curVersion = Plan.pjCabalVersion plan+   when (curVersion < minVersion) $ Log.warn Verbosity.normal $+      printf+         ("plan.json is from Cabal-%s, " +++          "but only version %s and higher provide SHA256 hashes for Cabal files.")+         (Generate.formatVersion curVersion)+         (Generate.formatVersion minVersion)++   let matchExe name comp =+         case comp of Plan.CompNameExe nameExe -> name == nameExe; _ -> False+   let mainExe = Text.pack $ Flatpak.command $ Flatpak.base projectJson+   let units =+         filter+            (\unit ->+               if Generate.matchName (Flatpak.mainPackage projectJson) unit+                  then any (matchExe mainExe) $ Map.keys $ Plan.uComps unit+                  else Map.member Plan.CompNameLib $ Plan.uComps unit) $+         filter ((Plan.UnitTypeBuiltin /=) . Plan.uType) $+         map (Plan.pjUnits plan Map.!) $ topSort $ Plan.planJsonIdGraph plan++   archs <-+      case Option.archs opt of+         [] ->+            let archStr = Text.unpack $ Plan.pjArch plan+            in maybe+                  (fail $ printf "unsupported architecture: %s" archStr)+                  (return . (:[])) $+               ParseArg.enumMaybe Generate.archGHC archStr+         archs -> return archs++   let compiler = Plan.pjCompilerId plan+   let sha256Url = Generate.ghcDirUrl compiler ++ "SHA256SUMS"+   ghcSha256s <- httpsGet sha256Url+   let (ghcSha256Map, ghcSha256Corrupts) =+         mapFst Map.fromList $+         ListHT.partitionMaybe+            (\line ->+               case BL.words line of+                  [hash,archive] -> Just (archive,hash)+                  _ -> Nothing)+            (BL.lines ghcSha256s)+   when (not $ null ghcSha256Corrupts) $ fail $ unlines $+      (printf "corrupt lines in %s:" sha256Url) :+      map BL.unpack ghcSha256Corrupts+   archHashes <-+      Trav.forM archs $ \arch -> do+         let ghcName = Generate.ghcArchive compiler arch+         hashStr <-+            maybe+               (fail $+                printf "could not find SHA256 checksum for %s in %s"+                  ghcName sha256Url) return $+            Map.lookup (BL.pack $ "./"++ghcName) ghcSha256Map+         sha256 <-+            maybe+               (fail $ "could not parse SHA256 checksum " ++ BL.unpack hashStr)+               return $+            Plan.parseSha256 $+            TextEnc.decodeLatin1 $ B.concat $ BL.toChunks hashStr+         return (arch, sha256)++   BL.writeFile (Path.toString $ Option.output opt) $ JsonPretty.encodePretty $+      if Option.cabalInstall opt+         then Generate.manifestCabalInstall plan archHashes units projectJson+         else Generate.manifest plan archHashes units projectJson
+ src/Option.hs view
@@ -0,0 +1,67 @@+module Option where++import qualified Flatpak++import qualified System.Path.PartClass as PathC+import qualified System.Path as Path++import qualified Shell.Utility.ParseArgument as ParseArg+import qualified Options.Applicative as OP+import Options.Applicative (help, metavar, argument, option, long, value, flag)++import Control.Applicative (pure, (<*>))++import Data.Monoid ((<>))+++data T =+   Cons {+      cabalInstall :: Bool,+      archs :: [Flatpak.Arch],+      projectDir :: Maybe Path.AbsRelDir,+      input, output :: Path.AbsRelFile+   } deriving (Show)+++path :: (PathC.FileDir fd) => OP.ReadM (Path.AbsRel fd)+path = OP.eitherReader Path.parse++arch :: OP.ReadM Flatpak.Arch+arch =+   OP.eitherReader $ \str ->+      maybe (Left $ "unknown architecture: " ++ str) Right $+      ParseArg.enumMaybe Flatpak.archString str+++parseFlags :: OP.Parser T+parseFlags =+   pure Cons+    <*> flag False True+            (long "cabal-install"+          <> help "Build with one cabal-install (default: build module-wise using Cabal)")+    <*> OP.many+         (option arch+            (metavar "ARCH"+          <> long "arch"+          <> help "Architecture to build for"))+    <*> option (fmap Just path)+            (metavar "DIR"+          <> value Nothing+          <> long "directory"+          <> help "Cabal package directory (default: PWD)")+    <*> argument path+            (metavar "INPUT"+          <> help "base FlatPak manifest")+    <*> argument path+            (metavar "OUTPUT"+          <> help "full FlatPak manifest to generate")+++desc :: OP.InfoMod a+desc =+   OP.fullDesc+   <>+   OP.progDesc "Generate a FlatPak manifest from a Cabal package description"++info :: OP.ParserInfo T+info = OP.info (OP.helper <*> parseFlags) desc