1
1
mirror of https://github.com/nmattia/snack.git synced 2024-09-11 11:55:36 +03:00

Add more (disabled) tests and refactor

This commit is contained in:
Nicolas Mattia 2018-07-06 15:38:10 +02:00
parent cb521b9776
commit f8bb059954
6 changed files with 268 additions and 227 deletions

136
snack-lib/build.nix Normal file
View File

@ -0,0 +1,136 @@
{ runCommand, lib, singleOut, callPackage, stdenv, rsync, symlinkJoin }:
with (callPackage ./modules.nix { inherit singleOut; });
with (callPackage ./lib.nix {});
with (callPackage ./module-spec.nix { inherit singleOut; });
rec {
# Returns an attribute set where the keys are all the built module names and
# the values are the paths to the object files.
# mainModSpec: a "main" module
buildMain = ghcWith: mainModSpec:
buildModulesRec ghcWith
# XXX: the main modules need special handling regarding the object name
{ "${mainModSpec.moduleName}" =
"${buildModule ghcWith mainModSpec}/Main.o";}
mainModSpec.moduleImports;
# returns a attrset where the keys are the module names and the values are
# the modules' object file path
buildLibrary = ghcWith: modSpecs:
buildModulesRec ghcWith {} modSpecs;
linkMainModule = ghcWith: mod: # main module
let
objAttrs = buildMain ghcWith mod;
objList = lib.attrsets.mapAttrsToList (x: y: y) objAttrs;
deps = allTransitiveDeps [mod];
ghc = ghcWith deps;
ghcOptsArgs = lib.strings.escapeShellArgs mod.moduleGhcOpts;
packageList = map (p: "-package ${p}") deps;
relExePath = "bin/${lib.strings.toLower mod.moduleName}";
drv = runCommand "linker" {}
''
mkdir -p $out/bin
${ghc}/bin/ghc \
${lib.strings.escapeShellArgs packageList} \
${lib.strings.escapeShellArgs objList} \
${ghcOptsArgs} \
-o $out/${relExePath}
'';
in
{
out = drv;
relExePath = relExePath;
};
# Build the given modules (recursively) using the given accumulator to keep
# track of which modules have been built already
# XXX: doesn't work if several modules in the DAG have the same name
buildModulesRec = ghcWith: empty: modSpecs:
foldDAG
{ f = mod:
{ "${mod.moduleName}" =
"${buildModule ghcWith mod}/${moduleToObject mod.moduleName}";
};
elemLabel = mod: mod.moduleName;
elemChildren = mod: mod.moduleImports;
reduce = a: b: a // b;
empty = empty;
}
modSpecs;
buildModule = ghcWith: modSpec:
let
ghc = ghcWith modSpec.moduleDependencies;
exts = modSpec.moduleExtensions;
ghcOpts = modSpec.moduleGhcOpts ++ (map (x: "-X${x}") exts);
ghcOptsArgs = lib.strings.escapeShellArgs ghcOpts;
objectName = modSpec.moduleName;
builtDeps = map (buildModule ghcWith) modSpec.moduleImports;
depsDirs = map (x: x + "/") builtDeps;
base = modSpec.moduleBase;
makeSymtree =
if lib.lists.length depsDirs >= 1
# TODO: symlink instead of copy
then "rsync -r ${lib.strings.escapeShellArgs depsDirs} ."
else "";
makeSymModule =
# TODO: symlink instead of copy
"rsync -r ${singleOutModule base modSpec.moduleName}/ .";
pred = file: path: type:
let
topLevel = (builtins.toString base) + "/";
actual = (lib.strings.removePrefix topLevel path);
expected = file;
in
(expected == actual) ||
(type == "directory" && (lib.strings.hasPrefix actual expected));
extraFiles = builtins.filterSource
(p: t:
lib.lists.length
(
let
topLevel = (builtins.toString base) + "/";
actual = lib.strings.removePrefix topLevel p;
in
lib.filter (expected:
(expected == actual) ||
(t == "directory" && (lib.strings.hasPrefix actual expected))
)
modSpec.moduleFiles
) >= 1
) base;
in stdenv.mkDerivation
{ name = objectName;
src = symlinkJoin
{ name = "extra-files";
paths = [ extraFiles ] ++ modSpec.moduleDirectories;
};
phases =
[ "unpackPhase" "buildPhase" ];
buildPhase =
''
echo "Building module ${modSpec.moduleName}"
mkdir -p $out
echo "Creating dependencies symtree for module ${modSpec.moduleName}"
${makeSymtree}
echo "Creating module symlink for module ${modSpec.moduleName}"
${makeSymModule}
echo "Compiling module ${modSpec.moduleName}"
# Set a tmpdir we have control over, otherwise GHC fails, not sure why
mkdir -p tmp
ghc -tmpdir tmp/ ${moduleToFile modSpec.moduleName} -c \
-outputdir $out \
${ghcOptsArgs} \
2>&1
echo "Done building module ${modSpec.moduleName}"
'';
buildInputs =
[ ghc
rsync
];
};
}

View File

@ -20,230 +20,11 @@ with (callPackage ./module-spec.nix { inherit singleOut; });
with (callPackage ./package-spec.nix { inherit singleOut; });
with (callPackage ./hpack.nix { inherit singleOut; });
with (callPackage ./lib.nix {});
with (callPackage ./build.nix { inherit singleOut; });
with (callPackage ./ghci.nix { inherit singleOut; });
let
buildModule = ghcWith: modSpec:
let
ghc = ghcWith modSpec.moduleDependencies;
exts = modSpec.moduleExtensions;
ghcOpts = modSpec.moduleGhcOpts ++ (map (x: "-X${x}") exts);
ghcOptsArgs = lib.strings.escapeShellArgs ghcOpts;
objectName = modSpec.moduleName;
builtDeps = map (buildModule ghcWith) modSpec.moduleImports;
depsDirs = map (x: x + "/") builtDeps;
base = modSpec.moduleBase;
makeSymtree =
if lib.lists.length depsDirs >= 1
# TODO: symlink instead of copy
then "rsync -r ${lib.strings.escapeShellArgs depsDirs} ."
else "";
makeSymModule =
# TODO: symlink instead of copy
"rsync -r ${singleOutModule base modSpec.moduleName}/ .";
pred = file: path: type:
let
topLevel = (builtins.toString base) + "/";
actual = (lib.strings.removePrefix topLevel path);
expected = file;
in
(expected == actual) ||
(type == "directory" && (lib.strings.hasPrefix actual expected));
extraFiles = builtins.filterSource
(p: t:
lib.lists.length
(
let
topLevel = (builtins.toString base) + "/";
actual = lib.strings.removePrefix topLevel p;
in
lib.filter (expected:
(expected == actual) ||
(t == "directory" && (lib.strings.hasPrefix actual expected))
)
modSpec.moduleFiles
) >= 1
) base;
in stdenv.mkDerivation
{ name = objectName;
src = symlinkJoin
{ name = "extra-files";
paths = [ extraFiles ] ++ modSpec.moduleDirectories;
};
phases =
[ "unpackPhase" "buildPhase" ];
buildPhase =
''
echo "Building module ${modSpec.moduleName}"
mkdir -p $out
echo "Creating dependencies symtree for module ${modSpec.moduleName}"
${makeSymtree}
echo "Creating module symlink for module ${modSpec.moduleName}"
${makeSymModule}
echo "Compiling module ${modSpec.moduleName}"
# Set a tmpdir we have control over, otherwise GHC fails, not sure why
mkdir -p tmp
ghc -tmpdir tmp/ ${moduleToFile modSpec.moduleName} -c \
-outputdir $out \
${ghcOptsArgs} \
2>&1
echo "Done building module ${modSpec.moduleName}"
'';
buildInputs =
[ ghc
rsync
];
};
# Returns an attribute set where the keys are all the built module names and
# the values are the paths to the object files.
# mainModSpec: a "main" module
buildMain = ghcWith: mainModSpec:
buildModulesRec ghcWith
# XXX: the main modules need special handling regarding the object name
{ "${mainModSpec.moduleName}" =
"${buildModule ghcWith mainModSpec}/Main.o";}
mainModSpec.moduleImports;
# returns a attrset where the keys are the module names and the values are
# the modules' object file path
buildLibrary = ghcWith: modSpecs:
buildModulesRec ghcWith {} modSpecs;
# Build the given modules (recursively) using the given accumulator to keep
# track of which modules have been built already
# XXX: doesn't work if several modules in the DAG have the same name
buildModulesRec = ghcWith: empty: modSpecs:
foldDAG
{ f = mod:
{ "${mod.moduleName}" =
"${buildModule ghcWith mod}/${moduleToObject mod.moduleName}";
};
elemLabel = mod: mod.moduleName;
elemChildren = mod: mod.moduleImports;
reduce = a: b: a // b;
empty = empty;
}
modSpecs;
linkMainModule = ghcWith: mod: # main module
let
objAttrs = buildMain ghcWith mod;
objList = lib.attrsets.mapAttrsToList (x: y: y) objAttrs;
deps = allTransitiveDeps [mod];
ghc = ghcWith deps;
ghcOptsArgs = lib.strings.escapeShellArgs mod.moduleGhcOpts;
packageList = map (p: "-package ${p}") deps;
relExePath = "bin/${lib.strings.toLower mod.moduleName}";
drv = runCommand "linker" {}
''
mkdir -p $out/bin
${ghc}/bin/ghc \
${lib.strings.escapeShellArgs packageList} \
${lib.strings.escapeShellArgs objList} \
${ghcOptsArgs} \
-o $out/${relExePath}
'';
in
{
out = drv;
relExePath = relExePath;
};
# Write a new ghci executable that loads all the modules defined in the
# module spec
ghciWithMain = ghcWith: mainModSpec:
let
imports = allTransitiveImports [mainModSpec];
modSpecs = [mainModSpec] ++ imports;
in ghciWithModules ghcWith modSpecs;
ghciWithModules = ghcWith: modSpecs:
let
ghcOpts = allTransitiveGhcOpts modSpecs
++ (map (x: "-X${x}") (allTransitiveExtensions modSpecs));
ghc = ghcWith (allTransitiveDeps modSpecs);
ghciArgs = lib.strings.escapeShellArgs
(ghcOpts ++ absoluteModuleFiles);
absoluteModuleFiles =
map
(mod:
builtins.toString (mod.moduleBase) +
"/${moduleToFile mod.moduleName}"
)
modSpecs;
dirs = allTransitiveDirectories modSpecs;
newGhc =
symlinkJoin
{ name = "ghci";
paths = [ ghc ];
postBuild =
''
wrapProgram "$out/bin/ghci" \
--add-flags "${ghciArgs}"
'';
buildInputs = [makeWrapper];
};
in
# This symlinks the extra dirs to $PWD for GHCi to work
writeScriptBin "ghci-with-files"
''
#!/usr/bin/env bash
set -euo pipefail
TRAPS=""
for i in ${lib.strings.escapeShellArgs dirs}; do
if [ "$i" != "$PWD" ]; then
for j in $(find "$i" ! -path "$i"); do
file=$(basename $j)
echo "Temporarily symlinking $j to $file..."
ln -s $j $file
TRAPS="rm $file ; $TRAPS"
trap "$TRAPS" EXIT
echo "done."
done
fi
done
${newGhc}/bin/ghci
'';
# Takes a package spec and returns (modSpecs -> Fold)
modSpecFoldFromPackageSpec = pkgSpec:
let
baseByModuleName = modName:
let res = pkgSpecByModuleName pkgSpec null modName;
in if res == null then null else res.packageBase;
depsByModuleName = modName:
(pkgSpecByModuleName
pkgSpec
(abort "asking dependencies for external module: ${modName}")
modName).packageDependencies
modName
;
extsByModuleName = modName:
(pkgSpecByModuleName
pkgSpec
(abort "asking extensions for external module: ${modName}")
modName).packageExtensions;
ghcOptsByModuleName = modName:
(pkgSpecByModuleName
pkgSpec
(abort "asking ghc options for external module: ${modName}")
modName).packageGhcOpts;
in
moduleSpecFold
{ baseByModuleName = baseByModuleName;
filesByModuleName = pkgSpec.packageExtraFiles;
dirsByModuleName = pkgSpec.packageExtraDirectories;
depsByModuleName = depsByModuleName;
extsByModuleName = extsByModuleName;
ghcOptsByModuleName = ghcOptsByModuleName;
};
# TODO: "executable" is a bad name
executable = pkgDescr:
let

65
snack-lib/ghci.nix Normal file
View File

@ -0,0 +1,65 @@
{ makeWrapper, symlinkJoin, lib, callPackage, singleOut, writeScriptBin }:
with (callPackage ./module-spec.nix { inherit singleOut; });
with (callPackage ./modules.nix { inherit singleOut; });
rec {
# Write a new ghci executable that loads all the modules defined in the
# module spec
ghciWithMain = ghcWith: mainModSpec:
let
imports = allTransitiveImports [mainModSpec];
modSpecs = [mainModSpec] ++ imports;
in ghciWithModules ghcWith modSpecs;
ghciWithModules = ghcWith: modSpecs:
let
ghcOpts = allTransitiveGhcOpts modSpecs
++ (map (x: "-X${x}") (allTransitiveExtensions modSpecs));
ghc = ghcWith (allTransitiveDeps modSpecs);
ghciArgs = lib.strings.escapeShellArgs
(ghcOpts ++ absoluteModuleFiles);
absoluteModuleFiles =
map
(mod:
builtins.toString (mod.moduleBase) +
"/${moduleToFile mod.moduleName}"
)
modSpecs;
dirs = allTransitiveDirectories modSpecs;
newGhc =
symlinkJoin
{ name = "ghci";
paths = [ ghc ];
postBuild =
''
wrapProgram "$out/bin/ghci" \
--add-flags "${ghciArgs}"
'';
buildInputs = [makeWrapper];
};
in
# This symlinks the extra dirs to $PWD for GHCi to work
writeScriptBin "ghci-with-files"
''
#!/usr/bin/env bash
set -euo pipefail
TRAPS=""
for i in ${lib.strings.escapeShellArgs dirs}; do
if [ "$i" != "$PWD" ]; then
for j in $(find "$i" ! -path "$i"); do
file=$(basename $j)
echo "Temporarily symlinking $j to $file..."
ln -s $j $file
TRAPS="rm $file ; $TRAPS"
trap "$TRAPS" EXIT
echo "done."
done
fi
done
${newGhc}/bin/ghci
'';
}

View File

@ -5,6 +5,7 @@
}:
with (callPackage ./modules.nix { inherit singleOut; });
with (callPackage ./package-spec.nix { inherit singleOut; });
with (callPackage ./lib.nix {});
rec {
@ -99,4 +100,39 @@ rec {
}
modSpecs
);
# Takes a package spec and returns (modSpecs -> Fold)
modSpecFoldFromPackageSpec = pkgSpec:
let
baseByModuleName = modName:
let res = pkgSpecByModuleName pkgSpec null modName;
in if res == null then null else res.packageBase;
depsByModuleName = modName:
(pkgSpecByModuleName
pkgSpec
(abort "asking dependencies for external module: ${modName}")
modName).packageDependencies
modName
;
extsByModuleName = modName:
(pkgSpecByModuleName
pkgSpec
(abort "asking extensions for external module: ${modName}")
modName).packageExtensions;
ghcOptsByModuleName = modName:
(pkgSpecByModuleName
pkgSpec
(abort "asking ghc options for external module: ${modName}")
modName).packageGhcOpts;
in
moduleSpecFold
{ baseByModuleName = baseByModuleName;
filesByModuleName = pkgSpec.packageExtraFiles;
dirsByModuleName = pkgSpec.packageExtraDirectories;
depsByModuleName = depsByModuleName;
extsByModuleName = extsByModuleName;
ghcOptsByModuleName = ghcOptsByModuleName;
};
}

View File

@ -0,0 +1,6 @@
name: snack-packages-test
dependencies:
- conduit
executable:
main: Foo.hs
source-dirs: src

View File

@ -3,12 +3,29 @@
set -euo pipefail
snack build
snack run | diff golden -
TMP_FILE=$(mktemp)
test() {
$SNACK build
$SNACK run | diff golden -
capture_io "$TMP_FILE" main | snack ghci
TMP_FILE=$(mktemp)
diff golden $TMP_FILE
rm $TMP_FILE
capture_io "$TMP_FILE" main | snack ghci
diff golden $TMP_FILE
rm $TMP_FILE
}
SNACK="snack" test
SNACK="snack -s ./snack.nix" test
#SNACK="snack --package-yaml ./package.yaml" test
#snack build
#snack run | diff golden -
#TMP_FILE=$(mktemp)
#capture_io "$TMP_FILE" main | snack ghci
#diff golden $TMP_FILE
#rm $TMP_FILE