hix-0.7.0: lib/Hix/Bootstrap.hs
module Hix.Bootstrap where
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Reader (ask)
import qualified Data.Text as Text
import Distribution.Compiler (PerCompilerFlavor (PerCompilerFlavor))
import qualified Distribution.PackageDescription as Cabal
import Distribution.PackageDescription (
BuildInfo,
GenericPackageDescription,
PackageDescription,
UnqualComponentName,
buildType,
licenseFiles,
unPackageName,
unUnqualComponentName,
)
import Distribution.Pretty (pretty)
import Distribution.Simple (Dependency (Dependency), depVerRange)
import Distribution.Types.PackageDescription (license)
import qualified Distribution.Verbosity as Cabal
import Exon (exon)
import Path (Abs, Dir, File, Path, Rel, parent, parseRelFile, relfile, toFilePath, (</>))
import System.FilePattern.Directory (getDirectoryFilesIgnore)
import Hix.Compat (readGenericPackageDescription)
import qualified Hix.Data.BootstrapProjectConfig
import Hix.Data.BootstrapProjectConfig (BootstrapProjectConfig)
import qualified Hix.Data.Monad (AppResources (cwd))
import Hix.Data.Monad (M (M))
import qualified Hix.Data.NewProjectConfig
import Hix.Data.NixExpr (Expr (ExprAttrs, ExprLit, ExprPrefix, ExprString), ExprAttr (ExprAttr, ExprAttrNil))
import Hix.Data.PackageName (PackageName (PackageName))
import qualified Hix.Data.ProjectFile
import Hix.Data.ProjectFile (ProjectFile (ProjectFile), createFile)
import Hix.Error (pathText, tryIO)
import Hix.Monad (AppResources (AppResources), noteBootstrap)
import Hix.NixExpr (mkAttrs, multi, multiOrSingle, nonEmptyAttrs, renderRootExpr, single, singleOpt)
import qualified Hix.Prelude
import Hix.Prelude (Prelude, findPrelude)
data CabalInfo =
CabalInfo {
path :: Path Rel Dir,
info :: GenericPackageDescription
}
deriving stock (Eq, Show, Generic)
data ComponentType =
Library
|
Executable Text
|
Benchmark Text
|
Test Text
deriving stock (Eq, Show, Generic)
data PreludeWithVersion =
PreludeWithVersion {
prelude :: Prelude,
dep :: Maybe Dependency
}
deriving stock (Eq, Show, Generic)
data HixComponent =
HixComponent {
special :: ComponentType,
known :: [ExprAttr],
prelude :: Maybe PreludeWithVersion
}
deriving stock (Eq, Show, Generic)
data HixPackage =
HixPackage {
name :: PackageName,
src :: Path Rel Dir,
known :: [ExprAttr],
meta :: [ExprAttr],
description :: ExprAttr,
components :: [HixComponent]
}
deriving stock (Eq, Show, Generic)
readCabal ::
Path Abs Dir ->
Path Rel File ->
M CabalInfo
readCabal cwd path = do
info <- liftIO (readGenericPackageDescription Cabal.verbose (toFilePath (cwd </> path)))
pure CabalInfo {path = dir, info}
where
dir = parent path
-- TODO extract version and put it in a file
knownPackageKeys :: PackageDescription -> [ExprAttr]
knownPackageKeys =
mkAttrs [
single "author" (.author),
single "build-type" (pretty . buildType),
single "copyright" (.copyright),
single "license" (pretty . license),
singleOpt "license-file" (fmap pretty . head . licenseFiles),
single "version" (.package.pkgVersion)
]
metaPackageKeys :: PackageDescription -> [ExprAttr]
metaPackageKeys =
mkAttrs [
single "maintainer" (.maintainer),
single "homepage" (.homepage),
single "synopsis" (.synopsis)
]
ghcFlavour :: PerCompilerFlavor a -> a
ghcFlavour (PerCompilerFlavor a _) = a
notDefaultGhcOption :: String -> Bool
notDefaultGhcOption = \case
"-threaded" -> False
"-rtsopts" -> False
"-with-rtsopts=-N" -> False
_ -> True
knownComponentKeys :: Maybe Prelude -> BuildInfo -> (Maybe PreludeWithVersion, [ExprAttr])
knownComponentKeys prelude info =
(preludeWithVersion, vals)
where
vals =
mkAttrs [
multi "dependencies" (fmap pretty . const deps),
multi "default-extensions" (fmap pretty . (.defaultExtensions)),
multiOrSingle "source-dirs" (fmap pretty . (.hsSourceDirs)),
singleOpt "language" (fmap pretty . (.defaultLanguage)),
multi "ghc-options" (fmap (ExprString . toText) . filter notDefaultGhcOption . ghcFlavour . (.options)),
misc
] info
misc e =
nonEmptyAttrs "component" (mkAttrs [
multi "other-modules" (fmap pretty . (.otherModules))
] e)
(preludeWithVersion, deps)
| Just p <- prelude =
let (v, res) = foldl (amendPrelude p) (Nothing, []) info.targetBuildDepends
in (Just (PreludeWithVersion p v), res)
| otherwise =
(Nothing, filter notBase info.targetBuildDepends)
amendPrelude p (Nothing, ds) dep@(Dependency (Cabal.unPackageName -> dname) _ _) | dname == p.preludePackage =
(Just dep, ds)
amendPrelude _ (v, ds) d = (v, d : ds)
notBase :: Cabal.Dependency -> Bool
notBase = \case
Cabal.Dependency "base" _ _ -> False
_ -> True
convertComponent :: ComponentType -> BuildInfo -> [ExprAttr] -> HixComponent
convertComponent special info extra =
HixComponent {known = knownCommon <> extra, ..}
where
(prelude, knownCommon) = knownComponentKeys preludeBasic info
preludeBasic = findPrelude info.mixins
convertLibrary :: Cabal.Library -> HixComponent
convertLibrary lib =
convertComponent Library lib.libBuildInfo extra
where
extra = mkAttrs [
multi "reexported-modules" (fmap pretty . (.reexportedModules))
] lib
convertExecutable :: UnqualComponentName -> Cabal.Executable -> HixComponent
convertExecutable name exe =
convertComponent (Executable (toText (unUnqualComponentName name))) exe.buildInfo []
convertTestsuite :: UnqualComponentName -> Cabal.TestSuite -> HixComponent
convertTestsuite name test =
convertComponent (Test (toText (unUnqualComponentName name))) test.testBuildInfo []
convertBenchmark :: UnqualComponentName -> Cabal.Benchmark -> HixComponent
convertBenchmark name bench =
convertComponent (Benchmark (toText (unUnqualComponentName name))) bench.benchmarkBuildInfo []
convert :: CabalInfo -> HixPackage
convert cinfo =
HixPackage {
name = PackageName (toText (unPackageName pkg.package.pkgName)),
src = cinfo.path,
known = knownPackageKeys pkg,
meta = metaPackageKeys pkg,
description = single "description" (.description) pkg,
components
}
where
components =
maybeToList (convertLibrary . (.condTreeData) <$> info.condLibrary)
<>
(uncurry convertExecutable . second (.condTreeData) <$> info.condExecutables)
<>
(uncurry convertTestsuite . second (.condTreeData) <$> info.condTestSuites)
<>
(uncurry convertBenchmark . second (.condTreeData) <$> info.condBenchmarks)
pkg = info.packageDescription
info = cinfo.info
renderComponent :: HixComponent -> ExprAttr
renderComponent HixComponent {..} =
ExprAttr key cabalConfig
where
cabalConfig = ExprAttrs (enable <> foldMap preludeAttrs prelude <> known)
preludeAttrs p =
[ExprAttr "prelude" (ExprAttrs [
ExprAttr "package" (preludePackageAttrs p),
ExprAttr "module" (ExprString (toText p.prelude.preludeModule))
])]
preludePackageAttrs p
| Just dep <- p.dep =
ExprAttrs [
ExprAttr "name" (ExprString (toText p.prelude.preludePackage)),
ExprAttr "version" (ExprString (show (pretty (depVerRange dep))))
]
| otherwise = ExprString (toText p.prelude.preludePackage)
key = case special of
Library -> "library"
Executable name -> [exon|executables.#{name}|]
Test name -> [exon|tests.#{name}|]
Benchmark name -> [exon|benchmarks.#{name}|]
enable = case special of
Library -> [ExprAttr "enable" (ExprLit "true")]
_ -> []
flakePackage :: HixPackage -> ExprAttr
flakePackage pkg =
ExprAttr (coerce pkg.name) attrs
where
attrs = ExprAttrs (src : pkg.description : ExprAttr "cabal" cabalConfig : comps)
src = ExprAttr "src" (ExprLit [exon|./#{Text.dropWhileEnd ('/' ==) (pathText pkg.src)}|])
cabalConfig = ExprAttrs (pkg.known <> [ExprAttr "meta" (ExprAttrs pkg.meta) | not (null pkg.meta)])
comps = renderComponent <$> pkg.components
mainPackage :: [HixPackage] -> ExprAttr
mainPackage = \case
pkg : _ : _ -> ExprAttr "main" (ExprString (coerce pkg.name))
_ -> ExprAttrNil
flake :: BootstrapProjectConfig -> [HixPackage] -> Expr
flake conf pkgs =
ExprAttrs [
ExprAttr "description" (ExprString "A Haskell project"),
ExprAttr "inputs.hix.url" (ExprString conf.hixUrl.unHixUrl),
ExprAttr "outputs" (ExprPrefix "{hix, ...}: hix.lib.flake" (ExprAttrs [
ExprAttr "packages" (ExprAttrs (flakePackage <$> pkgs)),
mainPackage pkgs
]))
]
bootstrapFiles :: BootstrapProjectConfig -> M [ProjectFile]
bootstrapFiles conf = do
AppResources {cwd} <- M ask
cabals <- paths =<< M (lift (tryIO (getDirectoryFilesIgnore (toFilePath cwd) ["**/*.cabal"] ["dist-newstyle/**"])))
pkgs <- fmap convert <$> traverse (readCabal cwd) cabals
pure [
ProjectFile {path = [relfile|flake.nix|], content = renderRootExpr (flake conf pkgs)}
]
where
paths = traverse (noteBootstrap "File path error" . parseRelFile)
bootstrapProject :: BootstrapProjectConfig -> M ()
bootstrapProject conf =
traverse_ createFile =<< bootstrapFiles conf