From ecefcc15dd9b0c5e60a80c524adfc9144538456e Mon Sep 17 00:00:00 2001 From: Nicolas Mattia Date: Tue, 29 Jan 2019 22:15:09 +0100 Subject: [PATCH] Major build interface refactor --- bin/Snack.hs | 40 ++----- nix/overlay.nix | 3 +- snack-lib/default.nix | 240 +++++++++++++++---------------------- snack-lib/hpack.nix | 9 +- snack-lib/package-spec.nix | 7 +- tests/library-2/test | 2 +- tests/library-3/test | 2 +- tests/swp/test | 2 +- tests/trans-imp/test | 2 +- 9 files changed, 124 insertions(+), 183 deletions(-) diff --git a/bin/Snack.hs b/bin/Snack.hs index 52f89e1..5850e34 100644 --- a/bin/Snack.hs +++ b/bin/Snack.hs @@ -18,6 +18,7 @@ import Control.Monad.IO.Class import Data.Aeson (FromJSON, (.:)) import Data.FileEmbed (embedStringFile) import Data.List (intercalate) +import Data.Maybe (mapMaybe) import Data.Semigroup ((<>)) import Data.String.Interpolate import Shelly (Sh) @@ -283,7 +284,6 @@ newtype ModuleName = ModuleName T.Text data BuildResult = BuiltLibrary LibraryBuild | BuiltExecutable ExecutableBuild - | BuiltMulti MultiBuild | BuiltGhci GhciBuild deriving Show @@ -291,7 +291,6 @@ instance Aeson.FromJSON BuildResult where parseJSON v = BuiltLibrary <$> (guardBuildType "library" v) <|> BuiltExecutable <$> (guardBuildType "executable" v) - <|> BuiltMulti <$> (guardBuildType "multi" v) <|> BuiltGhci <$> (guardBuildType "ghci" v) where guardBuildType :: FromJSON a => T.Text -> Aeson.Value -> Aeson.Parser a @@ -304,10 +303,7 @@ newtype GhciBuild = GhciBuild { ghciExePath :: NixPath } deriving stock Show - -instance FromJSON GhciBuild where - parseJSON = Aeson.withObject "ghci build" $ \o -> - GhciBuild <$> o .: "ghci_path" + deriving newtype FromJSON -- The kinds of builds: library, executable, or a mix of both (currently only -- for HPack) @@ -322,17 +318,6 @@ instance FromJSON ExecutableBuild where parseJSON = Aeson.withObject "executable build" $ \o -> ExecutableBuild <$> o .: "exe_path" -newtype MultiBuild = MultiBuild - { executableBuilds :: Map.Map T.Text ExecutableBuild - } - deriving stock Show - -instance Aeson.FromJSON MultiBuild where - parseJSON = Aeson.withObject "multi build" $ \o -> - MultiBuild <$> o .: "executables" - ---- Type-helpers for passing arguments to Nix - data NixArg = NixArg { argType :: NixArgType , argName :: T.Text @@ -418,7 +403,7 @@ nixBuild snackCfg extraNixArgs nixExpr = : [ argName narg , argValue narg ] nixCfg = snackNixCfg snackCfg -snackBuild :: SnackConfig -> PackageFile -> Sh BuildResult +snackBuild :: SnackConfig -> PackageFile -> Sh [BuildResult] snackBuild snackCfg packageFile = do NixPath out <- nixBuild snackCfg [ NixArg @@ -441,8 +426,9 @@ snackGhci snackCfg packageFile = do ] $ NixExpr "snack.inferGhci packageFile" liftIO (BS.readFile (T.unpack out)) >>= decodeOrFail >>= \case - BuiltGhci g -> pure g - b -> throwIO $ userError $ "Expected GHCi build, got " <> show b + -- TODO: shouldn't be dropping the tail + (BuiltGhci g):_ -> pure g + bs -> throwIO $ userError $ "Expected GHCi build, got " <> show bs runCommand :: SnackConfig -> PackageFile -> Command -> IO () runCommand snackCfg packageFile = \case @@ -455,13 +441,13 @@ runCommand snackCfg packageFile = \case noTest :: IO a noTest = fail "There is no test command for test suites" -runBuildResult :: [String] -> BuildResult -> IO () -runBuildResult args = \case - BuiltExecutable (ExecutableBuild p) -> runExe p args - BuiltMulti b - | [ExecutableBuild exe] <- Map.elems (executableBuilds b) -> - runExe exe args - b -> fail $ "Unexpected build type: " <> show b +runBuildResult :: [String] -> [BuildResult] -> IO () +runBuildResult args res = + case mapMaybe (\case {BuiltExecutable e -> Just e; _ -> Nothing}) res of + [ExecutableBuild p] -> runExe p args + -- TODO: be more graceful here + -- TODO: allow 'snack run my-exe -- ...' + _ -> fail $ "Expected exactly one executable, got: " <> show res runExe :: NixPath -> [String] -> IO () runExe (NixPath fp) args = executeFile (T.unpack fp) True args Nothing diff --git a/nix/overlay.nix b/nix/overlay.nix index 89210ea..1078e29 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -1,5 +1,4 @@ _: pkgs: rec { snack-lib = pkgs.callPackage ../snack-lib/default.nix { }; - snack-exe = - (snack-lib.buildAsExecutable (snack-lib.snackSpec ../bin/package.nix)).out; + snack-exe = snack-lib.executable ../bin/package.nix; } diff --git a/snack-lib/default.nix b/snack-lib/default.nix index b3a670b..94f0c1d 100644 --- a/snack-lib/default.nix +++ b/snack-lib/default.nix @@ -19,24 +19,106 @@ with (callPackage ./package-spec.nix {}); with (callPackage ./hpack.nix {}); let + + # Derivation that creates a binary in a 'bin' folder. + executable = packageFile: + let + specs = specsFromPackageFile packageFile; + spec = + if pkgs.lib.length specs == 1 + then pkgs.lib.head specs + else abort "'executable' can only be called on a single executable"; + exe = + if spec.packageIsExe + then buildAsExecutable spec + else abort "'executable' called on a library"; + in exe.out; + + + # Build a package spec as resp. a library and an executable + + buildAsLibrary = pkgSpec: + buildLibrary ghcWith (libraryModSpecs pkgSpec); + + buildAsExecutable = pkgSpec: + let drv = linkMainModule ghcWith (executableMainModSpec pkgSpec); + in + { out = drv.out; + exe_path = "${drv.out}/${drv.relExePath}"; + }; + ghcWith = deps: ghcWithPackages (ps: map (p: ps.${p}) deps); - # Assumes the package description describes an executable - withMainModSpec = pkgDescr: act: + specsFromPackageFile = packageFile: let - mainModName = pkgDescr.packageMain; - mainModSpec = + basename = builtins.baseNameOf packageFile; + components = pkgs.lib.strings.splitString "." basename; + ext = + if pkgs.lib.length components <= 1 + then abort ("File " ++ packageFile ++ " does not have an extension") + else pkgs.lib.last components; + fromNix = [(mkPackageSpec (import packageFile))]; + fromHPack = let - fld = moduleSpecFold' modSpecs; - modSpecs = foldDAG fld [mainModName]; - in modSpecs.${mainModName}; - drv = linkMainModule ghcWith mainModSpec; - in - { out = drv.out; - outPath = "${drv.out}"; - exePath = "${drv.out}/${drv.relExePath}"; - }; + descrs = pkgDescrsFromHPack packageFile; + executables = + builtins.map mkPackageSpec descrs.executables; + library = withAttr descrs "library" null + (comp: if builtins.isNull comp then null else mkPackageSpec comp); + in executables ++ (if builtins.isNull library then [] else [ library ]); + specs = + if ext == "nix" then fromNix + else if ext == "yaml" then fromHPack + else if ext == "yml" then fromHPack + else abort ("Unknown extension " ++ ext ++ " of file " ++ packageFile); + in specs; + + # Normal build (libs, exes) + + inferBuild = packageFile: + mkPackages (specsFromPackageFile packageFile); + + mkPackages = pkgSpecs: writeText "build.json" + ( builtins.toJSON + ( builtins.map + (pkgSpec: + if pkgSpec.packageIsExe + then + { build_type = "executable"; + result = buildAsExecutable pkgSpec; + } + else + { build_type = "library"; + result = buildAsLibrary pkgSpec; + } + ) pkgSpecs + ) + ); + + # GHCi build (libs, exes) + + inferGhci = packageFile: + mkPackagesGhci (specsFromPackageFile packageFile); + + mkPackagesGhci = pkgSpecs: writeText "hpack-ghci-json" + ( builtins.toJSON ( + builtins.map + (pkgSpec: + let + drv = + if pkgSpec.packageIsExe + then ghciWithMain ghcWith (executableMainModSpec pkgSpec) + else ghciWithModules ghcWith (libraryModSpecs pkgSpec) + ; + in + { build_type = "ghci"; # TODO: need to record the name somewhere + result = "${drv.out}/bin/ghci-with-files"; + } + ) pkgSpecs + )); + + # How to build resp. libraries and executables libraryModSpecs = pkgSpec: let @@ -58,143 +140,13 @@ let in modSpecs.${mainModName}; in mainModSpec; - buildAsLibrary = pkgSpec: - buildLibrary ghcWith (libraryModSpecs pkgSpec); - - buildAsExecutable = pkgSpec: - let drv = linkMainModule ghcWith (executableMainModSpec pkgSpec); - in - { out = drv.out; - exe_path = "${drv.out}/${drv.relExePath}"; - }; - - # TODO: deduplicate extensions + update README with --package-file - inferBuild = packageFile: - let - basename = builtins.baseNameOf packageFile; - components = pkgs.lib.strings.splitString "." basename; - ext = - if pkgs.lib.length components <= 1 - then abort ("File " ++ packageFile ++ " does not have an extension") - else pkgs.lib.last components; - build = - if ext == "nix" then inferSnackBuild - else if ext == "yaml" then inferHPackBuild - else if ext == "yml" then inferHPackBuild - else abort ("Unknown extension " ++ ext ++ " of file " ++ packageFile); - in build packageFile; - - inferGhci = packageFile: - let - basename = builtins.baseNameOf packageFile; - components = pkgs.lib.strings.splitString "." basename; - ext = - if pkgs.lib.length components <= 1 - then abort ("File " ++ packageFile ++ " does not have an extension") - else pkgs.lib.last components; - ghci = - if ext == "nix" then inferSnackGhci - else if ext == "yaml" then inferHPackGhci - else if ext == "yml" then inferHPackGhci - else abort ("Unknown extension " ++ ext ++ " of file " ++ packageFile); - in ghci packageFile; - - inferSnackBuild = packageNix: mkPackage (import packageNix); - - inferSnackGhci = packageNix: writeText "snack-ghci-json" - ( builtins.toJSON ( - let - pkgSpec = mkPackageSpec (import packageNix); - drv = - if builtins.isNull pkgSpec.packageMain - then ghciWithModules ghcWith (libraryModSpecs pkgSpec) - else ghciWithMain ghcWith (executableMainModSpec pkgSpec); - in - { build_type = "ghci"; - result = { - "ghci_path" = "${drv.out}/bin/ghci-with-files"; - }; - } - )); - - inferHPackBuild = packageYaml: writeText "hpack-build-json" - ( builtins.toJSON ( - let pkgSpecs = hpackSpecs packageYaml; - in - { build_type = "multi"; - result = - { library = - if builtins.isNull pkgSpecs.library - then null - else buildAsLibrary (pkgSpecs.library); - executables = lib.attrsets.mapAttrs (k: v: buildAsExecutable v) pkgSpecs.executables; - }; - } - )); - - inferHPackGhci = packageYaml: writeText "hpack-ghci-json" - ( builtins.toJSON ( - let - pkgSpecs = hpackSpecs packageYaml; - pkgSpec = mkPackageSpec (import packageNix); - drv = - let exeSpecs = builtins.attrValues pkgSpecs.executables; - in - if lib.lists.length exeSpecs == 1 - then ghciWithMain ghcWith (executableMainModSpec (lib.lists.head exeSpecs)) - else - if builtins.isNull pkgSpecs.library - then abort "GHCi: needs either a single executable or a library" - else ghciWithModules ghcWith (libraryModSpecs pkgSpecs.library); - in - { build_type = "ghci"; - result = { - "ghci_path" = "${drv.out}/bin/ghci-with-files"; - }; - } - )); - - snackSpec = packageNix: mkPackageSpec (import packageNix); - hpackSpecs = packageYaml: - let - descrs = pkgDescrsFromHPack packageYaml; - in - { library = withAttr descrs "library" null - (comp: if builtins.isNull comp then null else mkPackageSpec comp); - executables = - lib.attrsets.mapAttrs (k: v: mkPackageSpec v) descrs.executables; - }; - - mkPackage = snackNixExpr: writeText "snack-build-json" - ( builtins.toJSON ( - let - pkgSpec = mkPackageSpec snackNixExpr; - in - if builtins.isNull pkgSpec.packageMain - then - { "build_type" = "library"; - "result" = buildAsLibrary pkgSpec; - } - else - { "build_type" = "executable"; - "result" = buildAsExecutable pkgSpec; - } - )); - in { inherit inferBuild inferGhci - inferSnackBuild - inferSnackGhci - inferHPackBuild - inferHPackGhci - packageYaml buildAsExecutable buildAsLibrary - snackSpec - hpackSpecs - mkPackage + executable ; } diff --git a/snack-lib/hpack.nix b/snack-lib/hpack.nix index 8f83aae..8c4993b 100644 --- a/snack-lib/hpack.nix +++ b/snack-lib/hpack.nix @@ -52,9 +52,9 @@ in ); exes = - withAttr package "executables" {} (lib.mapAttrs (k: v: mkExe v)) // - withAttr package "executable" {} (comp: { ${package.name} = mkExe comp; }); - mkExe = component: + withAttr package "executables" [] (lib.mapAttrsToList (k: v: mkExe k v)) ++ + withAttr package "executable" [] (comp: [(mkExe package.name comp)] ); + mkExe = nn: component: let depOrPack = lib.lists.partition @@ -62,8 +62,9 @@ in (optAttr component "dependencies" []); in { main = fileToModule component.main; + name = nn; src = - let + let base = builtins.dirOf packageYaml; source-dirs = optAttr component "source-dirs" "."; in diff --git a/snack-lib/package-spec.nix b/snack-lib/package-spec.nix index adb269f..4aa2674 100644 --- a/snack-lib/package-spec.nix +++ b/snack-lib/package-spec.nix @@ -8,7 +8,8 @@ rec { mkPackageSpec = packageDescr@ - { src + { src ? [] + , name ? null , main ? null , ghcOpts ? [] , dependencies ? [] @@ -17,7 +18,9 @@ rec { , extra-directories ? [] , packages ? [] }: - { packageMain = main; + { packageIsExe = ! builtins.isNull main; + packageName = name; + packageMain = main; packageSourceDirs = if builtins.isList src then src diff --git a/tests/library-2/test b/tests/library-2/test index b5104c9..a468184 100755 --- a/tests/library-2/test +++ b/tests/library-2/test @@ -6,7 +6,7 @@ set -euo pipefail test() { TMP_FILE=$(mktemp) - cat $($SNACK build) | jq -M '.result | keys' > $TMP_FILE + cat $($SNACK build) | jq -M '.[0] | .result | keys' > $TMP_FILE diff golden.jq $TMP_FILE diff --git a/tests/library-3/test b/tests/library-3/test index b5104c9..a468184 100755 --- a/tests/library-3/test +++ b/tests/library-3/test @@ -6,7 +6,7 @@ set -euo pipefail test() { TMP_FILE=$(mktemp) - cat $($SNACK build) | jq -M '.result | keys' > $TMP_FILE + cat $($SNACK build) | jq -M '.[0] | .result | keys' > $TMP_FILE diff golden.jq $TMP_FILE diff --git a/tests/swp/test b/tests/swp/test index 232f3ab..0be4476 100755 --- a/tests/swp/test +++ b/tests/swp/test @@ -6,7 +6,7 @@ set -euo pipefail test() { TMP_FILE=$(mktemp) - cat $($SNACK build) | jq -M '.result | keys' > $TMP_FILE + cat $($SNACK build) | jq -M '.[0] | .result | keys' > $TMP_FILE diff golden.jq $TMP_FILE } diff --git a/tests/trans-imp/test b/tests/trans-imp/test index 3c7be34..b963211 100755 --- a/tests/trans-imp/test +++ b/tests/trans-imp/test @@ -6,7 +6,7 @@ set -euo pipefail test() { TMP_FILE=$(mktemp) - cat $($SNACK build) | jq -M '.result | keys' > $TMP_FILE + cat $($SNACK build) | jq -M '.[0] | .result | keys' > $TMP_FILE diff golden.jq $TMP_FILE