mirror of
https://github.com/srid/haskell-flake.git
synced 2024-09-11 05:55:25 +03:00
Autodetect "packages" based on cabal.project
(and package.yaml
) (#110)
This commit is contained in:
parent
da1d8bbda4
commit
56d8d9787a
@ -5,7 +5,7 @@
|
||||
- New features
|
||||
- #63: Add `config.haskellProjects.${name}.outputs` containing all flake outputs for that project.
|
||||
- #102 In addition, `outputs` contains `finalPackages` and `localPackages`.
|
||||
- #49 & #91: The `packages` option now autodiscovers the top-level `.cabal` file (in addition to looking inside sub-directories) as its default value.
|
||||
- #49 & #91 & #110: The `packages` option now autodiscovers the top-level `.cabal` file, `package.yaml` or it discovers multiple packages if they are specified in the `cabal.project` file.
|
||||
- #69: The default flake template creates `flake.nix` only, while the `#example` one creates the full Haskell project template.
|
||||
- #92: Add `devShell.mkShellArgs` to pass custom arguments to `mkShell`
|
||||
- #111: Add `devShell.extraLibraries` to add custom Haskell libraries to the devshell.
|
||||
|
@ -1,41 +0,0 @@
|
||||
{ lib, ... }:
|
||||
|
||||
self:
|
||||
# We look for a single *.cabal in project root as well as
|
||||
# multiple */*.cabal. Otherwise, error out.
|
||||
#
|
||||
# In future, we could just read `cabal.project`. See #76.
|
||||
let
|
||||
# Like pkgs.haskell.lib.haskellPathsInDir' but with a few differences
|
||||
# - Allows top-level .cabal files
|
||||
haskellPathsInDir' = path:
|
||||
lib.filterAttrs (k: v: v != null) (lib.mapAttrs'
|
||||
(k: v:
|
||||
if v == "regular" && lib.strings.hasSuffix ".cabal" k
|
||||
then lib.nameValuePair (lib.strings.removeSuffix ".cabal" k) path
|
||||
else
|
||||
if v == "directory" && builtins.pathExists (path + "/${k}/${k}.cabal")
|
||||
then lib.nameValuePair k (path + "/${k}")
|
||||
else lib.nameValuePair k null
|
||||
)
|
||||
(builtins.readDir path));
|
||||
errorNoDefault = msg:
|
||||
builtins.throw ''
|
||||
haskell-flake: A default value for `packages` cannot be auto-detected:
|
||||
|
||||
${msg}
|
||||
You must manually specify the `packages` option.
|
||||
'';
|
||||
cabalPaths =
|
||||
let
|
||||
cabalPaths = haskellPathsInDir' self;
|
||||
in
|
||||
if cabalPaths == { }
|
||||
then
|
||||
errorNoDefault ''
|
||||
No .cabal file found in project root or its sub-directories.
|
||||
''
|
||||
else cabalPaths;
|
||||
in
|
||||
cabalPaths
|
||||
|
10
nix/find-haskell-paths/README.md
Normal file
10
nix/find-haskell-paths/README.md
Normal file
@ -0,0 +1,10 @@
|
||||
# `find-haskell-paths`
|
||||
|
||||
`find-haskell-paths` is a superior alternative to nixpkgs' [`haskellPathsInDir`](https://github.com/NixOS/nixpkgs/blob/f991762ea1345d850c06cd9947700f3b08a12616/lib/filesystem.nix#L18).
|
||||
|
||||
- It locates packages based on the "packages" field of `cabal.project` file if it exists (otherwise it returns the top-level package).
|
||||
- It supports `hpack`, thus works with `package.yaml` even if no `.cabal` file exists.
|
||||
|
||||
## Limitations
|
||||
|
||||
- Glob patterns in `cabal.project` are not supported yet. Feel free to open a PR improving the parser to support them.
|
79
nix/find-haskell-paths/default.nix
Normal file
79
nix/find-haskell-paths/default.nix
Normal file
@ -0,0 +1,79 @@
|
||||
{ pkgs
|
||||
, lib
|
||||
, throwError ? msg: builtins.throw msg
|
||||
, ...
|
||||
}:
|
||||
|
||||
let
|
||||
parser = import ./parser.nix { inherit pkgs lib; };
|
||||
traversal = rec {
|
||||
findSingleCabalFile = path:
|
||||
let
|
||||
cabalFiles = lib.filter (lib.strings.hasSuffix ".cabal") (builtins.attrNames (builtins.readDir path));
|
||||
num = builtins.length cabalFiles;
|
||||
in
|
||||
if num == 0
|
||||
then null
|
||||
else if num == 1
|
||||
then builtins.head cabalFiles
|
||||
else throwError "Expected a single .cabal file, but found multiple: ${builtins.toJSON cabalFiles}";
|
||||
findSinglePackageYamlFile = path:
|
||||
let f = path + "/package.yaml";
|
||||
in if builtins.pathExists f then f else null;
|
||||
getCabalName = cabalFile:
|
||||
lib.strings.removeSuffix ".cabal" cabalFile;
|
||||
getPackageYamlName = fp:
|
||||
let
|
||||
name = parser.parsePackageYamlName (builtins.readFile fp);
|
||||
in
|
||||
if name.type == "success"
|
||||
then name.value
|
||||
else throwError ("Failed to parse ${fp}: ${builtins.toJSON name}");
|
||||
findHaskellPackageNameOfDirectory = path:
|
||||
let
|
||||
cabalFile = findSingleCabalFile path;
|
||||
packageYamlFile = findSinglePackageYamlFile path;
|
||||
in
|
||||
if cabalFile != null
|
||||
then
|
||||
getCabalName cabalFile
|
||||
else if packageYamlFile != null
|
||||
then
|
||||
getPackageYamlName packageYamlFile
|
||||
else
|
||||
throwError "Neither a .cabal file nor a package.yaml found under ${path}";
|
||||
};
|
||||
in
|
||||
projectRoot:
|
||||
let
|
||||
cabalProjectFile = projectRoot + "/cabal.project";
|
||||
packageDirs =
|
||||
if builtins.pathExists cabalProjectFile
|
||||
then
|
||||
let
|
||||
res = parser.parseCabalProjectPackages (builtins.readFile cabalProjectFile);
|
||||
isSelfPath = path:
|
||||
path == "." || path == "./" || path == "./.";
|
||||
in
|
||||
if res.type == "success"
|
||||
then
|
||||
map
|
||||
(path:
|
||||
if isSelfPath path
|
||||
then projectRoot
|
||||
else if lib.strings.hasInfix "*" path
|
||||
then throwError "Found a path with glob (${path}) in ${cabalProjectFile}, which is not supported"
|
||||
else if lib.strings.hasSuffix ".cabal" path
|
||||
then throwError "Expected a directory but ${path} (in ${cabalProjectFile}) is a .cabal filepath"
|
||||
else "${projectRoot}/${path}"
|
||||
)
|
||||
res.value
|
||||
else throwError ("Failed to parse ${cabalProjectFile}: ${builtins.toJSON res}")
|
||||
else
|
||||
[ projectRoot ];
|
||||
in
|
||||
lib.listToAttrs
|
||||
(map
|
||||
(path:
|
||||
lib.nameValuePair (traversal.findHaskellPackageNameOfDirectory path) path)
|
||||
packageDirs)
|
47
nix/find-haskell-paths/parser.nix
Normal file
47
nix/find-haskell-paths/parser.nix
Normal file
@ -0,0 +1,47 @@
|
||||
# Sufficiently basic parsers for `cabal.project` and `package.yaml` formats
|
||||
#
|
||||
# "sufficiently" because we care only about 'packages' from `cabal.project` and
|
||||
# 'name' from `package.yaml`.
|
||||
{ pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
nix-parsec = builtins.fetchGit {
|
||||
url = "https://github.com/kanwren/nix-parsec.git";
|
||||
ref = "master";
|
||||
rev = "1bf25dd9c5de1257a1c67de3c81c96d05e8beb5e";
|
||||
shallow = true;
|
||||
};
|
||||
inherit (import nix-parsec) parsec;
|
||||
in
|
||||
{
|
||||
# Extract the "packages" list from a cabal.project file.
|
||||
#
|
||||
# Globs are not supported yet. Values must be refer to a directory, not file.
|
||||
parseCabalProjectPackages = cabalProjectFile:
|
||||
let
|
||||
spaces1 = parsec.skipWhile1 (c: c == " " || c == "\t");
|
||||
newline = parsec.string "\n";
|
||||
path = parsec.fmap lib.concatStrings (parsec.many1 (parsec.anyCharBut "\n"));
|
||||
key = parsec.string "packages:\n";
|
||||
val =
|
||||
(parsec.many1
|
||||
(parsec.between spaces1 newline path)
|
||||
);
|
||||
parser = parsec.skipThen
|
||||
key
|
||||
val;
|
||||
in
|
||||
parsec.runParser parser cabalProjectFile;
|
||||
|
||||
# Extract the "name" field from a package.yaml file.
|
||||
parsePackageYamlName = packageYamlFile:
|
||||
let
|
||||
spaces1 = parsec.skipWhile1 (c: c == " " || c == "\t");
|
||||
key = parsec.string "name:";
|
||||
val = parsec.fmap lib.concatStrings (parsec.many1 (parsec.anyCharBut "\n"));
|
||||
parser = parsec.skipThen
|
||||
(parsec.skipThen key spaces1)
|
||||
val;
|
||||
in
|
||||
parsec.runParser parser packageYamlFile;
|
||||
}
|
47
nix/find-haskell-paths/parser_tests.nix
Normal file
47
nix/find-haskell-paths/parser_tests.nix
Normal file
@ -0,0 +1,47 @@
|
||||
{ pkgs ? import <nixpkgs> { }, lib ? pkgs.lib, ... }:
|
||||
|
||||
let
|
||||
parser = pkgs.callPackage ./parser.nix { };
|
||||
cabalProjectTests =
|
||||
let
|
||||
eval = s:
|
||||
let res = parser.parseCabalProjectPackages s; in
|
||||
if res.type == "success" then res.value else res;
|
||||
in
|
||||
{
|
||||
testSimple = {
|
||||
expr = eval ''
|
||||
packages:
|
||||
foo
|
||||
bar
|
||||
'';
|
||||
expected = [ "foo" "bar" ];
|
||||
};
|
||||
};
|
||||
packageYamlTests =
|
||||
let
|
||||
eval = s:
|
||||
let res = parser.parsePackageYamlName s; in
|
||||
if res.type == "success" then res.value else res;
|
||||
in
|
||||
{
|
||||
testSimple = {
|
||||
expr = eval ''
|
||||
name: foo
|
||||
'';
|
||||
expected = "foo";
|
||||
};
|
||||
};
|
||||
# Like lib.runTests, but actually fails if any test fails.
|
||||
runTestsFailing = tests:
|
||||
let
|
||||
res = lib.runTests tests;
|
||||
in
|
||||
if res == builtins.trace "All tests passed" [ ]
|
||||
then res
|
||||
else builtins.throw "Some tests failed: ${builtins.toJSON res}" res;
|
||||
in
|
||||
{
|
||||
"cabal.project" = runTestsFailing cabalProjectTests;
|
||||
"package.yaml" = runTestsFailing packageYamlTests;
|
||||
}
|
@ -40,7 +40,10 @@ in
|
||||
options = {
|
||||
root = mkOption {
|
||||
type = path;
|
||||
description = "Path to Haskell package where the `.cabal` file lives";
|
||||
description = ''
|
||||
The directory path under which the Haskell package's `.cabal`
|
||||
file or `package.yaml` resides.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
@ -147,8 +150,19 @@ in
|
||||
specialArgs = { inherit pkgs self; };
|
||||
modules = [
|
||||
./haskell-project.nix
|
||||
{
|
||||
({ config, ... }: {
|
||||
options = {
|
||||
projectRoot = mkOption {
|
||||
type = types.path;
|
||||
description = ''
|
||||
Path to the root of the project directory.
|
||||
|
||||
Chaning this affects certain functionality, like where to
|
||||
look for the 'cabal.project' file.
|
||||
'';
|
||||
default = self;
|
||||
defaultText = "Top-level directory of the flake";
|
||||
};
|
||||
basePackages = mkOption {
|
||||
type = types.attrsOf raw;
|
||||
description = ''
|
||||
@ -199,17 +213,33 @@ in
|
||||
packages = mkOption {
|
||||
type = types.lazyAttrsOf packageSubmodule;
|
||||
description = ''
|
||||
Attrset of local packages in the project repository.
|
||||
Set of local packages in the project repository.
|
||||
|
||||
Autodiscovered by default by looking for `.cabal` files in
|
||||
top-level or sub-directories.
|
||||
If you have a `cabal.project` file (under `projectRoot`),
|
||||
those packages are automatically discovered. Otherwise, a
|
||||
top-level .cabal or package.yaml file is used to discover
|
||||
the single local project.
|
||||
|
||||
haskell-flake currently supports a limited range of syntax
|
||||
for `cabal.project`. Specifically it requires an explicit
|
||||
list of package directories under the "packages" option.
|
||||
'';
|
||||
default =
|
||||
let find-cabal-paths = import ./find-cabal-paths.nix { inherit lib; };
|
||||
let
|
||||
find-haskell-paths = import ./find-haskell-paths {
|
||||
inherit pkgs lib;
|
||||
throwError = msg: builtins.throw ''
|
||||
haskell-flake: A default value for `packages` cannot be auto-determined:
|
||||
|
||||
${msg}
|
||||
|
||||
Please specify the `packages` option manually or change your project configuration.
|
||||
'';
|
||||
};
|
||||
in
|
||||
lib.mapAttrs
|
||||
(_: value: { root = value; })
|
||||
(find-cabal-paths self);
|
||||
(find-haskell-paths config.projectRoot);
|
||||
defaultText = lib.literalMD "autodiscovered by reading `self` files.";
|
||||
};
|
||||
devShell = mkOption {
|
||||
@ -230,7 +260,7 @@ in
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
})
|
||||
];
|
||||
};
|
||||
in
|
||||
|
33
runtest.sh
33
runtest.sh
@ -12,32 +12,41 @@ else
|
||||
}
|
||||
fi
|
||||
|
||||
|
||||
# Waiting on github.com/nixbuild/nix-quick-install-action to support 2.13+
|
||||
# We use newer Nix for:
|
||||
# - https://github.com/NixOS/nix/issues/7263
|
||||
# - https://github.com/NixOS/nix/issues/7026
|
||||
NIX="nix run github:nixos/nix/2.14.1 --"
|
||||
${NIX} --version
|
||||
|
||||
# Before anything, run the main haskell-flake tests
|
||||
logHeader "Testing find-haskell-paths' parser"
|
||||
${NIX} eval -I nixpkgs=flake:github:nixos/nixpkgs/bb31220cca6d044baa6dc2715b07497a2a7c4bc7 \
|
||||
--impure --expr 'import ./nix/find-haskell-paths/parser_tests.nix {}'
|
||||
|
||||
|
||||
FLAKE=$(pwd)
|
||||
|
||||
# A Nix bug causes incorrect self when in a sub-flake.
|
||||
# https://github.com/NixOS/nix/issues/7263
|
||||
# Workaround: copy ./test somewhere outside of this Git repo.
|
||||
TESTDIR=$(mktemp -d)
|
||||
trap 'rm -fr "$TESTDIR"' EXIT
|
||||
cp -r ./test/* "$TESTDIR"
|
||||
cd "$TESTDIR"
|
||||
pwd
|
||||
pushd ./test
|
||||
|
||||
# First, build the flake
|
||||
logHeader "Testing nix build"
|
||||
nix build --override-input haskell-flake path:${FLAKE}
|
||||
${NIX} build --override-input haskell-flake path:${FLAKE}
|
||||
# Run the devshell test script in a nix develop shell.
|
||||
logHeader "Testing nix devshell"
|
||||
nix develop --override-input haskell-flake path:${FLAKE} -c ./test.sh
|
||||
${NIX} develop --override-input haskell-flake path:${FLAKE} -c ./test.sh
|
||||
# Test non-devshell features:
|
||||
# Checks
|
||||
logHeader "Testing nix flake checks"
|
||||
nix --option sandbox false \
|
||||
${NIX} --option sandbox false \
|
||||
build --override-input haskell-flake path:${FLAKE} -L .#check
|
||||
|
||||
popd
|
||||
|
||||
logHeader "Testing docs"
|
||||
nix build --override-input haskell-flake path:${FLAKE} \
|
||||
--option log-lines 1000 --show-trace \
|
||||
github:hercules-ci/flake.parts-website#checks.${SYSTEM}.linkcheck
|
||||
"github:hercules-ci/flake.parts-website#checks.${SYSTEM}.linkcheck"
|
||||
|
||||
logHeader "All tests passed!"
|
Loading…
Reference in New Issue
Block a user