Files
haskell.nix/lib/cabal-project-parser.nix
Maxim Koltsov a30665693a Fixes re source-repository-package (#1715)
* Fix regex to match source-repository-package tag

Regex to tell ref from rev was wrong

* Fix fetchGit call when rev and sha256 unspecified

* Update expected output for unit test

* Auto retry concurrency check that keeps failing

Co-authored-by: Hamish Mackenzie <Hamish.K.Mackenzie@gmail.com>
2022-09-30 13:33:56 +13:00

220 lines
9.3 KiB
Nix

{ pkgs }:
let
span = pred: list:
let n = pkgs.lib.lists.foldr (x: acc: if pred x then acc + 1 else 0) 0 list;
in { fst = pkgs.lib.lists.take n list; snd = pkgs.lib.lists.drop n list; };
# Look for a index-state: field in the cabal.project file
parseIndexState = rawCabalProject:
let
indexState = pkgs.lib.lists.concatLists (
pkgs.lib.lists.filter (l: l != null)
(builtins.map (l: builtins.match "^index-state: *(.*)" l)
(pkgs.lib.splitString "\n" rawCabalProject)));
in
pkgs.lib.lists.head (indexState ++ [ null ]);
# Remove the indentation of the first line from all the lines
unindent = blockLines: (pkgs.lib.foldl' ({matchString, out}: s:
let
m = builtins.match matchString s;
in
if m != null
then {
matchString = "(${builtins.head m})(.*)";
out = out ++ [(builtins.elemAt m 1)];
}
else {
inherit matchString out;
}
) { matchString = "( *)(.*)"; out = []; } blockLines).out;
# Strip comments except `--sha256`
stripComments = blockLines: pkgs.lib.filter (s: s != "") (
pkgs.lib.foldl' (out: s:
let
m = builtins.match "(.*)--.*" s;
in
if !(pkgs.lib.hasPrefix "--sha256" s) && m != null
then out ++ [(builtins.head m)]
else out ++ [s]
) [] blockLines
);
# Parse lines of a source-repository-package block
parseBlockLines = blockLines: (pkgs.lib.foldl' ({name, attrs}: s:
let
# Look for a new attribute name
pair = builtins.match "([^ :]*): *(.*)" s;
# Function to build the next parse state when the attribute name is known
nextState = name: value: {
inherit name;
# Support multi line attributes by appending the value to the named attribute
attrs = attrs // {
${name} =
if attrs ? ${name}
then attrs.${name} + " " + value
else value;
};
};
in
if pair != null
then
# First line of a new attribute
nextState (builtins.head pair) (builtins.elemAt pair 1)
else
if name != null
then nextState name s # Append another line to the current attribute
else __trace "Expected attribute but found `${s}`" { inherit name attrs; }
) { name = null; attrs = {}; } (stripComments (unindent blockLines))).attrs;
# Gets data for all the repositories to fetch
# A comment like
# --shar256: 003lm3pm0000hbfmii7xcdd9v20000flxf7gdl2pyxia7p014i8z
# will be trated like a field and returned here
# (used in call-cabal-project-to-nix.nix to create a fixed-output derivation)
extractSourceRepoPackageData = cabalProjectFileName: sha256map: repo:
let
refOrRev =
if builtins.match "[0-9a-f]{40}" repo.tag != null
then "rev"
else "ref";
in {
url = repo.location;
"${refOrRev}" = repo.tag;
sha256 = repo."--sha256" or (
if sha256map != null
then sha256map."${repo.location}"."${repo.tag}"
else null);
subdirs = if repo ? subdir
then pkgs.lib.filter (x: x != "") (pkgs.lib.splitString " " repo.subdir)
else ["."];
};
# Parse a source-repository-package and return data of `type: git` repositories
# See tests/unit.nix for examples of input and output.
parseSourceRepositoryPackageBlock = cabalProjectFileName: sha256map: block:
let
x = span (pkgs.lib.strings.hasPrefix " ") (pkgs.lib.splitString "\n" block);
attrs = parseBlockLines x.fst;
in
if attrs."type" or "" != "git"
then {
sourceRepo = [];
otherText = "\nsource-repository-package\n" + block;
}
else {
sourceRepo = extractSourceRepoPackageData cabalProjectFileName sha256map attrs;
otherText = pkgs.lib.strings.concatStringsSep "\n" x.snd;
};
parseSourceRepositoryPackages = cabalProjectFileName: sha256map: source-repo-override: projectFile:
let
blocks = pkgs.lib.splitString "\nsource-repository-package\n" ("\n" + projectFile);
initialText = pkgs.lib.lists.take 1 blocks;
repoBlocks = builtins.map (parseSourceRepositoryPackageBlock cabalProjectFileName sha256map) (pkgs.lib.lists.drop 1 blocks);
overrideSourceRepo = sourceRepo: (source-repo-override.${sourceRepo.url} or (pkgs.lib.id)) sourceRepo;
in {
sourceRepos = pkgs.lib.lists.map (block: overrideSourceRepo block.sourceRepo) repoBlocks;
otherText = pkgs.lib.strings.concatStringsSep "\n" (
initialText
++ (builtins.map (x: x.otherText) repoBlocks));
};
# Parse and replace repository
# This works in a similar way to the `source-repository-package` but we are
# able to simply replace the `repository` blocks with local `file:/nix/store` ones.
# This works because `cabal configure` does not include any of the `/nix/sore/`
# paths in the `plan.json` (so materialized plan-nix will still work as expeced).
# See tests/unit.nix for examples of input and output.
parseRepositoryBlock = evalPackages: cabalProjectFileName: sha256map: inputMap: cabal-install: nix-tools: block:
let
lines = pkgs.lib.splitString "\n" block;
# The first line will contain the repository name.
x = span (pkgs.lib.strings.hasPrefix " ") (__tail lines);
attrs = parseBlockLines x.fst;
sha256 = attrs."--sha256" or (
if sha256map != null
then sha256map."${attrs.url}"
else null);
in rec {
# This is `some-name` from the `repository some-name` line in the `cabal.project` file.
name = __head lines;
# The $HOME/.cabal/packages/${name} after running `cabal v2-update` to download the repository
repoContents = if inputMap ? ${attrs.url}
# If there is an input use it to make `file:` url and create a suitable `.cabal/packages/${name}` directory
then evalPackages.runCommand name ({
nativeBuildInputs = [ cabal-install ] ++ evalPackages.haskell-nix.cabal-issue-8352-workaround;
preferLocalBuild = true;
}) ''
HOME=$(mktemp -d)
mkdir -p $HOME/.cabal/packages/${name}
cat <<EOF > $HOME/.cabal/config
repository ${name}
url: file:${inputMap.${attrs.url}}
${pkgs.lib.optionalString (attrs ? secure) "secure: ${attrs.secure}"}
${pkgs.lib.optionalString (attrs ? root-keys) "root-keys: ${attrs.root-keys}"}
${pkgs.lib.optionalString (attrs ? key-threshold) "key-threshold: ${attrs.key-threshold}"}
EOF
cabal v2-update ${name}
cp -r $HOME/.cabal/packages/${name} $out
''
else evalPackages.runCommand name ({
nativeBuildInputs = [ cabal-install evalPackages.curl nix-tools ] ++ evalPackages.haskell-nix.cabal-issue-8352-workaround;
LOCALE_ARCHIVE = pkgs.lib.optionalString (evalPackages.stdenv.buildPlatform.libc == "glibc") "${evalPackages.glibcLocales}/lib/locale/locale-archive";
LANG = "en_US.UTF-8";
preferLocalBuild = true;
} // pkgs.lib.optionalAttrs (sha256 != null) {
outputHashMode = "recursive";
outputHashAlgo = "sha256";
outputHash = sha256;
}) ''
HOME=$(mktemp -d)
mkdir -p $HOME/.cabal/packages/${name}
cat <<EOF > $HOME/.cabal/config
repository ${name}
url: ${attrs.url}
${pkgs.lib.optionalString (attrs ? secure) "secure: ${attrs.secure}"}
${pkgs.lib.optionalString (attrs ? root-keys) "root-keys: ${attrs.root-keys}"}
${pkgs.lib.optionalString (attrs ? key-threshold) "key-threshold: ${attrs.key-threshold}"}
EOF
export SSL_CERT_FILE=${evalPackages.cacert}/etc/ssl/certs/ca-bundle.crt
cabal v2-update ${name}
cp -r $HOME/.cabal/packages/${name} $out
'';
# Output of hackage-to-nix
hackage = import (
evalPackages.runCommand ("hackage-to-nix-" + name) {
nativeBuildInputs = [ cabal-install evalPackages.curl nix-tools ];
LOCALE_ARCHIVE = pkgs.lib.optionalString (evalPackages.stdenv.buildPlatform.libc == "glibc") "${evalPackages.glibcLocales}/lib/locale/locale-archive";
LANG = "en_US.UTF-8";
preferLocalBuild = true;
} ''
mkdir -p $out
hackage-to-nix $out ${repoContents}/01-index.tar ${attrs.url}
'');
# Directory to `lndir` when constructing a suitable $HOME/.cabal dir
repo = {
${name} = repoContents;
};
};
parseRepositories = evalPackages: cabalProjectFileName: sha256map: inputMap: cabal-install: nix-tools: projectFile:
let
# This will leave the name of repository in the first line of each block
blocks = pkgs.lib.splitString "\nrepository " ("\n" + projectFile);
repoBlocks = builtins.map (parseRepositoryBlock evalPackages cabalProjectFileName sha256map inputMap cabal-install nix-tools) (pkgs.lib.lists.drop 1 blocks);
in {
extra-hackages = pkgs.lib.lists.map (block: block.hackage) repoBlocks;
repos = pkgs.lib.lists.foldl' (x: block: x // block.repo) {} repoBlocks;
};
in {
inherit parseIndexState parseSourceRepositoryPackages parseRepositories
# These are only exposed for tests
parseSourceRepositoryPackageBlock parseRepositoryBlock;
}