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 +5/−0
- LICENSE +31/−0
- Makefile +20/−0
- ReadMe.md +172/−0
- Setup.lhs +3/−0
- cabal-flatpak.cabal +59/−0
- flatpak.cabal.json +31/−0
- src/Flatpak.hs +283/−0
- src/Generate.hs +324/−0
- src/Main.hs +142/−0
- src/Option.hs +67/−0
+ 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