hsinstall-3.0: src/app/HSInstall/Opts.hs
{-# LANGUAGE QuasiQuotes #-}
module HSInstall.Opts
( BuildMode (..)
, DebugLog (..)
, ExeFile (..)
, ExeFiles (AllExes, SpecificExes)
, Mode (..)
, Options (..)
, PrefixOpt (..)
, Signing (..)
, Verbose (..)
, parseOpts
, needCleaning
)
where
import Data.List.NonEmpty (NonEmpty, fromList)
import Data.Text.Lazy (Text)
import Data.Version (showVersion)
import Formatting ((%+), format)
import Formatting.ShortFormatters (s)
import Options.Applicative.Dex
import Text.Heredoc (here)
import Paths_hsinstall (version)
newtype CleanSwitch = CleanSwitch Bool
data PrefixOpt = Prefix FilePath | NoPrefixSpecified
data ExeFiles = AllExes | SpecificExes (NonEmpty ExeFile)
newtype ExeFile = ExeFile { v :: FilePath }
data Signing = SigningKeyId String | NoSignature
newtype Verbose = Verbose Bool
newtype DebugLog = DebugLog Bool
-- This is sort-of the "type" of build mode, this type helps with case matching
data BuildMode
= Dist CleanSwitch PrefixOpt ExeFiles
| AppImage CleanSwitch Signing ExeFile
-- This is the "overall" mode, building things vs just dumping the icon
data Mode = Build BuildMode | DumpIcon
data Options = Options Mode Verbose DebugLog
needCleaning :: BuildMode -> Bool
needCleaning (Dist (CleanSwitch True) _ _) = True
needCleaning (AppImage (CleanSwitch True) _ _) = True
needCleaning _ = False
toExeFiles :: [String] -> ExeFiles
toExeFiles [] = AllExes
toExeFiles l = SpecificExes . fromList . map ExeFile $ l
parseOpts :: IO Options
parseOpts = parseOpts' parser
"Pack a haskell project into a deployable directory structure"
footerContent version
parser :: Parser Options
parser = Options
<$> parseMode
<*> ( Verbose <$> switch
( long "verbose"
<> short 'v'
<> help "More verbose output"
)
)
<*> ( DebugLog <$> switch
( long "debug"
<> help "Enable debug logging"
)
)
parseMode :: Parser Mode
parseMode = parseDist <|> parseAppImage <|> parseDumpIcon
parseClean :: Bool -> Parser CleanSwitch
parseClean includeHelp = CleanSwitch <$> switch
( long "clean" <> short 'c' <> helpModifier )
where
helpModifier = if includeHelp
then help "Do stack or cabal 'clean' first"
else idm
parseDist :: Parser Mode
parseDist = Build <$> ( Dist
<$> ( flag' ()
( long "dist"
<> short 'D'
<> help "Deploy Haskell build artifacts below a PREFIX directory"
)
*> parseClean True
)
<*> option ( Prefix <$> str )
( long "prefix"
<> short 'p'
<> metavar "DIR"
<> help "Install prefix directory (Default: hsi-dist/usr)"
<> value NoPrefixSpecified
)
<*> ( toExeFiles <$> many ( argument str
( metavar "EXENAME1 EXENAME2 ..."
<> help "Executables from the cabal file to deploy. Default: all of them"
)
))
)
parseAppImage :: Parser Mode
parseAppImage = Build <$> ( AppImage
<$> ( flag' ()
( long "appimage"
<> short 'A'
<> help "Prepare the AppDir structure and build an AppImage for a single binary"
)
*> parseClean False
)
<*> option ( SigningKeyId <$> str )
( long "sign"
<> short 's'
<> metavar "KEY_ID"
<> help "Sign the AppImage with the specified GPG2 key id"
<> value NoSignature
)
<*> ( ExeFile <$> argument str
( metavar "EXENAME"
<> help "Build an AppImage of this specific executable from the cabal file"
)
)
)
parseDumpIcon :: Parser Mode
parseDumpIcon = DumpIcon
<$ flag' ()
( long "dump-stock-icon"
<> help "Save a default icon, unix-terminal.svg, to the current working directory"
)
footerContent :: Text
footerContent = format content (showVersion version)
where content = [here|OVERVIEW
hsinstall is a tool for installing a Haskell software project into a directory structure for deployment. It builds upon the `stack install` and `cabal install` commands and adds these features:
- Copies the `LICENSE` file into <PREFIX>/share/<PROJECT-NAME>/doc
- Copies the contents of a template directory stucture in your project (named `hsi-tmpl` or `hsi-tmpl.EXE`) into the destination prefix directory. This can contain additional binaries or scripts, resources, documentation, etc. (more on how this works below)
- Optionally builds an AppDir-shaped directory structure for the project and produces an AppImage binary
To use hsinstall, it will be necessary to be in the top-level directory of a Haskell project that builds with either cabal or stack. You'll need to have one or the other of the cabal or stack tools on your path as well. Basically, if you can't build the project, hsinstall can't build it either.
Also note that all cabal commands will be issued as `v2-*` commands. The older `v1-*` commands are deprecated and we don't use them.
If the AppImage features are desired, you must have these tools on your PATH:
linuxdeploy: https://github.com/linuxdeploy/linuxdeploy/releases
linuxdeploy-plugin-appimage: https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases
Note when specifying EXENAME strings, do not use the target notation from stack/cabal (PACKAGE:EXENAME), the EXENAME alone is expected.
hsinstall operates in two main modes
dist (-D|--dist) Pack distributable project binaries and additional files below a PREFIX directory
AppImage (-A|--appimage) Make an AppImage executable of a program and its additional files
DIST DEPLOYMENT
Running `hsinstall -D` on a project will produce this in . :
hsi-dist/
usr/
bin/ <-- All binaries in the project
share/
<PROJECT-NAME>/
doc/
LICENSE
The -p,--prefix switch allows you to set a prefix other than `hsi-dist/usr`. This could be anywhere, like `myproject-2.3` or `/usr/local` or `/opt`
In addition, if an `hsi-tmpl` directory exists, its contents will be copied into the prefix before build and install. See TEMPLATE DIRECTORY below for more info on this.
If present, dist mode will copy the contents of the `hsi-tmpl` template directory into `<PREFIX>`. Here's an explanation of the `hsi-tmpl` directory contents:
hsi-tmpl/
bin/ <-- Put additional binaries and scripts to be deployed here
share/
<PROJECT-NAME>/ <-- Only needed if you have resources
resources/ <-- Put data files your software will need at runtime here
APPIMAGE CREATION
-A|--appimage mode will change the default prefix to `hsi-AppDir.EXE/usr` and only the specified EXE will be installed into `<PREFIX>/bin`, AppImages are intended to be made for exactly ONE BINARY.
If .desktop and .svg files are not found in the template directory, defaults will be created for you and placed in the correct subdirs of the template directory. Check these files into source control for future builds.
Here's an explanation of the `hsi-tmpl.EXE` directory:
hsi-tmpl.EXE/
bin/ <-- Put additional binaries and scripts to be deployed here
share/
applications/ <-- Auto-generated for AppImage
<EXE>.desktop <-- Will be generated by first-time AppImage creation attempt
<PROJECT-NAME>/ <-- Only needed if you have resources
resources/ <-- Put data files your software will need at runtime here
icons/ <-- Auto-generated for AppImage
hicolor/
scalable/
apps/
<EXE>.svg <-- Will be generated by first-time AppImage creation attempt
appimage mode will copy the contents of the `hsi-tmpl.EXE` template directory into `<PREFIX>`
A directory, `hsi-AppDir.EXE` will be created containing everything needed to package into an AppImage. Then the AppImage file itself will be created.
The default `.desktop` file Categories will be populated with 'Utility;'. We recommend adjusting this using the XDG list of registered categories: https://specifications.freedesktop.org/menu-spec/latest/apa.html
If your application is a command-line program, append a line containing this to the end of the default `.desktop` file: 'Terminal=true'
If your application isn't a command-line program, we recommend using a proper icon instead of the hsinstall default, which is a command shell icon. --dump-stock-icon will save the default icon artwork SVG in . which can be used to help creating a custom icon.
For more info on AppImage: https://appimage.org/
If you see failure like the following when using the -s,--sign switch:
[appimage/stderr] [sign] gpgme_op_sign(gpgme_ctx, gpgme_appimage_file_data, gpgme_sig_data, GPGME_SIG_MODE_DETACH): call failed: Inappropriate ioctl for device
What's happening is gpg can't figure out a way to ask for the gpg key passphrase. Try setting the env like this
$ GPG_TTY=$(tty) hsinstall ...
USING DATA FILES AT RUNTIME
In order to locate data files at runtime, including resources, the hsinstall project includes a library to construct the share path relative to the executable. See this source code for help with integrating this into your app: https://codeberg.org/dinofp/hsinstall/src/branch/main/src/lib/HSInstall/Paths.hs
Version|] %+ s %+ " Dino Morelli <dino@ui3.info>"