mirror of
https://github.com/nmattia/snack.git
synced 2024-11-24 12:16:17 +03:00
Major build interface refactor
This commit is contained in:
parent
88f6a41029
commit
ecefcc15dd
40
bin/Snack.hs
40
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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user