diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f557f9..bc977e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,3 +53,12 @@ jobs: ACTIONS_ALLOW_UNSECURE_COMMANDS: true run: | PATH=~/.local/bin/:$PATH stack test --system-ghc + # Inspired by: + # https://nix.dev/tutorials/continuous-integration-github-actions.html + nix: + name: nix-build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2.3.4 + - uses: cachix/install-nix-action@v12 + - run: nix-build --no-out-link diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..ee80e9f --- /dev/null +++ b/default.nix @@ -0,0 +1,68 @@ +# Inspired by https://discourse.nixos.org/t/nix-haskell-development-2020/6170/16 +let + pinnedPkgs = import (builtins.fetchTarball { + # Descriptive name to make the store path easier to identify + name = "nixos-20.09-2020-12-13"; + # Current commit from https://github.com/NixOS/nixpkgs/tree/nixos-20.09 + url = "https://github.com/nixos/nixpkgs/archive/65c9cc79f1d179713c227bf447fb0dac384cdcda.tar.gz"; + # Hash obtained using `nix-prefetch-url --unpack ` + sha256 = "0whxlm098vas4ngq6hm3xa4mdd2yblxcl5x5ny216zajp08yp1wf"; + }) {}; + + packageName = "consul-haskell"; +in +{ + pkgs ? pinnedPkgs, + # compiler: + # If given as the default `null` uses the default `pkgs.haskellPackages`; + # if given as a string (e.g. `ghc865`), uses + # `pkgs.haskell.packages.${compiler}`. + compiler ? null, +}: +let + explicitSource = + (import ./explicitSource.nix { lib = pkgs.lib; }).explicitSource; + + src = explicitSource ./. { + name = "consul-haskell"; + includeDirs = [ + ./src + ./tests + ]; + includeFiles = [ + ./consul-haskell.cabal + ./Setup.hs + ./LICENSE + ./README.md + ]; + pathComponentExcludes = [ "build" "gen" ]; + }; + + originalHaskellpackages = + if compiler == null + then pkgs.haskellPackages + else pkgs.haskell.packages.${compiler}; + + # Create a Haskell package set with our package in it, + # and add the required native packages to its dependencies. + myHaskellPackages = originalHaskellpackages.override { + overrides = hself: hsuper: { + "${packageName}" = + pkgs.haskell.lib.overrideCabal + (hself.callCabal2nix "${packageName}" src {}) + (drv: { + # The test suite starts a consul server; add it to PATH. + preCheck = '' + export PATH="${pkgs.consul}/bin:$PATH" + ''; + }); + }; + }; + + + exe = pkgs.haskell.lib.justStaticExecutables (myHaskellPackages."${packageName}"); + +in + { + inherit exe; + } diff --git a/explicitSource.nix b/explicitSource.nix new file mode 100644 index 0000000..dc20e6f --- /dev/null +++ b/explicitSource.nix @@ -0,0 +1,215 @@ +# `explicitSource` function that nh2 is upstreaming +# in https://github.com/NixOS/nixpkgs/pull/56985 +# and dogfooding here. +{ lib }: rec { + inherit (lib) cleanSourceWith; + + # Splits a filesystem path into its components. + splitPath = path: lib.splitString "/" (toString path); + + # Turns a list of path components into a tree, e.g. + # + # ["a" "b" "c1"] + # ["a" "b" "c2"] + # ["a" "b" "c3"] + # ["a" "x" ] + # + # becomes: + # + # { a = { b = { c1 = null; c2 = null; c3 = null }; x = null; }; } + pathComponentsToTree = paths: with lib; + foldl (tree: path: recursiveUpdate tree (setAttrByPath path null)) {} paths; + + # Returns true if and only if any prefix of the given `path` leads to a leaf + # (`null`) in the given `tree` (nested attrset). + # That is: "If we go down the tree by the given path, do we hit a leaf?" + # + # Example: For the tree `tree` + # + # a + # b-leaf + # c-leaf + # d + # e-leaf + # f-leaf + # + # represented as attrset + # + # { a = { b = null; c = null; d = { e = null; }; }; f = null; } + # + # we have (string quotes omitted for readability): + # + # isPrefixOfLeafPath [a] tree == false + # isPrefixOfLeafPath [x] tree == false + # isPrefixOfLeafPath [a b] tree == true + # isPrefixOfLeafPath [a b c] tree == true + # isPrefixOfLeafPath [a b c x] tree == true + # isPrefixOfLeafPath [a d] tree == false + isPrefixOfLeafPath = path: tree: + if tree == null + then true + else + if path == [] + then false + else + let + component = builtins.head path; + restPath = builtins.tail path; + in + if !(builtins.hasAttr component tree) + then false + else + let + subtree = builtins.getAttr component tree; + in + isPrefixOfLeafPath restPath subtree; + + + + # See `explicitSource` for an example of this this filter. + # + # You can use this filter standalone (with `builtins.filterSource` + # or better, `builtins.path` with explicitly given `name`) when + # you want to combine it with other filters. + explicitSourceFilter = + { + # List of dirs under which all recursively contained files are taken in + # (unless a file is filtered by other arguments). + # Dirs that match explicitly are immediately taken in. + includeDirs ? [], + # Explicit list of files that should be taken in. + includeFiles ? [], + # Exclude dotfiles/dirs by default (unless they are matched explicitly)? + excludeHidden ? true, + # If any of the path components given here appears anywhere in the path, + # (e.g. X in `.../X/...`), the path is excluded (unless matched explicitly). + # Example: `pathComponentExcludes = [ "gen" "build" ]`. + pathComponentExcludes ? [], + + # Debugging + + # Enable this to enable a `builtins.trace` output that prints which files + # were matched as source inputs. + # Output looks like: + # trace: myproject: include regular /home/user/myproject/Setup.hs + # trace: myproject: skip regular /home/user/myproject/myproject.nix + # trace: myproject: skip directory /home/user/myproject/dist + # trace: myproject: include directory /home/user/myproject/images + # trace: myproject: include regular /home/user/myproject/images/image.svg + # trace: myproject: include directory /home/user/myproject/src + # trace: myproject: include directory /home/user/myproject/src/MyDir + # trace: myproject: include regular /home/user/myproject/src/MyDir/File1.hs + # trace: myproject: include regular /home/user/myproject/src/MyDir/File2.hs + debugTraceEnable ? false, + # Set this to prefix the trace output with some arbitrary string. + # Useful if you enable `debugTraceEnable` in multiple places and want + # to distinguish them. + debugTracePrefix ? "", + + # For debugging + name, + }: with lib; + let + # Pre-processing done once, across all files passed in. + + # Turns a list into a "set" (map where all values are null). + keySet = list: genAttrs list (name: null); + + # For fast non-O(n) lookup, we turn `includeDirs` and `includeFiles` into + # string-keyed attrsets first. + includeDirsSet = keySet (map toString includeDirs); + srcFilesSet = keySet (map toString includeFiles); + + # We also turn `includeDirs` into a directory-prefix-tree so that we can + # check whether a given path is under one of the `includeDirs` in sub-O(n). + includeDirsTree = pathComponentsToTree (map splitPath includeDirs); + in + # The actual filter function with per-file processing + fullPath: type: + let + fileName = baseNameOf (toString fullPath); + + components = splitPath fullPath; + + isExplicitSrcFile = hasAttr fullPath srcFilesSet; + isExplicitSrcDir = type == "directory" && hasAttr fullPath includeDirsSet; + # The below is equivalent to + # any (srcDir: hasPrefix (toString srcDir + "/") fullPath) includeDirs; + # but faster than O(n) where n is the number of `includeDirs` entries. + isUnderSomeSrcDir = isPrefixOfLeafPath components includeDirsTree; + + isHidden = excludeHidden && hasPrefix "." fileName; + + hasExcludedComponentInPath = any (c: elem c pathComponentExcludes) components; + + isSourceInput = + isExplicitSrcFile || + isExplicitSrcDir || + (isUnderSomeSrcDir && !isHidden && !hasExcludedComponentInPath); + + tracing = + let + prefix = if debugTracePrefix == "" then "" else debugTracePrefix + ": "; + action = if isSourceInput then "include" else "skip "; + # Pad type (e.g. "regular", "symlink") to be as wide as + # the widest ("directory") for aligned output. + width = max (stringLength "directory") (stringLength type); + formattedType = substring 0 width (type + " "); + in + debug.traceIf + debugTraceEnable + "${prefix}${action} ${formattedType} ${fullPath}"; + in + tracing isSourceInput; + + # A general-purpose, explicit source code importer suitable for most + # packaging needs. + # + # See `explicitSourceFilter` for an explanation of the filter arguments. + # + # Example usage: + # + # src = lib.explicitSource ./. { + # name = "mypackage"; + # includeDirs = [ + # ./src + # ./app + # ./images + # ]; + # includeFiles = [ + # ./mypackage.cabal + # ./Setup.hs + # ]; + # pathComponentExcludes = [ "build" "gen" ]; + # }; + # + # Note that `includeDirs = [ ./. ]` is also permitted. + # + # But consider that in most cases you will not want to include files + # that are not relevant for the build, such as `.nix` files, so that + # input hashes do not change unnecessarily. + # + # If you want to combine it with other filters, use `explicitSourceFilter` + # directly instead. + explicitSource = topPath: filterArgs@{ + # Recommended, to be identifiable in downloads and `nix-store -qR`. + # "-src" will be automatically appended. + name ? "filtered", + # Other `explicitSourceFilter` arguments. + ... + }: + cleanSourceWith { + # The `-src` suffix makes it easy to distinguish source store paths + # from built output store paths in the nix store. + # + # Requiring an explicit name prevents the basename of the directory + # making it into the store path when `./.` is used, thus preventing + # impure builds in situations where ./. is a directory with random + # names, as is common e.g. when cloning source repositories under + # multiple names; see https://github.com/NixOS/nix/issues/1305 + # and https://github.com/NixOS/nixpkgs/pull/67996. + name = name + "-src"; + src = topPath; + filter = explicitSourceFilter filterArgs; + }; +}