on iko/ghcjs-patch-master help sign in
builder: Add nix-shell development environments
20226faRodney Lorrimar
builder/comp-builder.nix+47−11
@@ -1,4 +1,4 @@
1−{ stdenv, buildPackages, ghc, lib, pkgconfig, writeText, runCommand, haskellLib, nonReinstallablePkgs }:
1+{ stdenv, buildPackages, ghc, lib, pkgconfig, writeText, runCommand, haskellLib, nonReinstallablePkgs, withPackage }:
2
2
3{ componentId
3{ componentId
4, component
4, component
@@ -16,6 +16,7 @@
16, preBuild ? null, postBuild ? null
16, preBuild ? null, postBuild ? null
17, preCheck ? null, postCheck ? null
17, preCheck ? null, postCheck ? null
18, preInstall ? null, postInstall ? null
18, preInstall ? null, postInstall ? null
19+, shellHook ? null
19
20
20, doCheck ? component.doCheck || haskellLib.isTest componentId
21, doCheck ? component.doCheck || haskellLib.isTest componentId
21, doCrossCheck ? component.doCrossCheck || false
22, doCrossCheck ? component.doCrossCheck || false
@@ -46,18 +47,30 @@ let
46 in map ({val,...}: val) closure;
47 in map ({val,...}: val) closure;
47
48
48 exactDep = pdbArg: p: ''
49 exactDep = pdbArg: p: ''
49− if id=$(${ghc.targetPrefix}ghc-pkg -v0 ${pdbArg} field ${p} id --simple-output); then
50+ if id=$(target-pkg ${pdbArg} field ${p} id --simple-output); then
50 echo "--dependency=${p}=$id" >> $out/configure-flags
51 echo "--dependency=${p}=$id" >> $out/configure-flags
51 fi
52 fi
52− if ver=$(${ghc.targetPrefix}ghc-pkg -v0 ${pdbArg} field ${p} version --simple-output); then
53+ if ver=$(target-pkg ${pdbArg} field ${p} version --simple-output); then
53 echo "constraint: ${p} == $ver" >> $out/cabal.config
54 echo "constraint: ${p} == $ver" >> $out/cabal.config
54 echo "constraint: ${p} installed" >> $out/cabal.config
55 echo "constraint: ${p} installed" >> $out/cabal.config
55 fi
56 fi
56 '';
57 '';
57
58
59+ envDep = pdbArg: p: ''
60+ if id=$(target-pkg ${pdbArg} field ${p} id --simple-output); then
61+ echo "package-id $id" >> $out/ghc-environment
62+ fi
63+ '';
64+
58 configFiles = runCommand "${fullName}-config" { nativeBuildInputs = [ghc]; } (''
65 configFiles = runCommand "${fullName}-config" { nativeBuildInputs = [ghc]; } (''
59 mkdir -p $out
66 mkdir -p $out
67+
68+ # Calls ghc-pkg for the target platform
69+ target-pkg() {
60− ${ghc.targetPrefix}ghc-pkg -v0 init $out/package.conf.d
70+ ${ghc.targetPrefix}ghc-pkg "$@"
71+ }
72+
73+ target-pkg init $out/package.conf.d
61
74
62 ${lib.concatStringsSep "\n" (lib.mapAttrsToList flagsAndConfig {
75 ${lib.concatStringsSep "\n" (lib.mapAttrsToList flagsAndConfig {
63 "extra-lib-dirs" = map (p: "${lib.getLib p}/lib") component.libs;
76 "extra-lib-dirs" = map (p: "${lib.getLib p}/lib") component.libs;
@@ -69,11 +82,11 @@ let
69 # Note: we need to use --global-package-db with ghc-pkg to prevent it
82 # Note: we need to use --global-package-db with ghc-pkg to prevent it
70 # from looking into the implicit global package db when registering the package.
83 # from looking into the implicit global package db when registering the package.
71 ${lib.concatMapStringsSep "\n" (p: ''
84 ${lib.concatMapStringsSep "\n" (p: ''
72− ${ghc.targetPrefix}ghc-pkg -v0 describe ${p} | ${ghc.targetPrefix}ghc-pkg -v0 --force --global-package-db $out/package.conf.d register - || true
85+ target-pkg describe ${p} | target-pkg --force --global-package-db $out/package.conf.d register - || true
73 '') nonReinstallablePkgs}
86 '') nonReinstallablePkgs}
74
87
75 ${lib.concatMapStringsSep "\n" (p: ''
88 ${lib.concatMapStringsSep "\n" (p: ''
76− ${ghc.targetPrefix}ghc-pkg -v0 --package-db ${p}/package.conf.d dump | ${ghc.targetPrefix}ghc-pkg -v0 --force --package-db $out/package.conf.d register -
89+ target-pkg --package-db ${p}/package.conf.d dump | target-pkg --force --package-db $out/package.conf.d register -
77 '') flatDepends}
90 '') flatDepends}
78
91
79 # Note: we pass `clear` first to ensure that we never consult the implicit global package db.
92 # Note: we pass `clear` first to ensure that we never consult the implicit global package db.
@@ -81,6 +94,14 @@ let
81
94
82 echo ${lib.concatStringsSep " " (lib.mapAttrsToList (fname: val: "--flags=${lib.optionalString (!val) "-" + fname}") flags)} >> $out/configure-flags
95 echo ${lib.concatStringsSep " " (lib.mapAttrsToList (fname: val: "--flags=${lib.optionalString (!val) "-" + fname}") flags)} >> $out/configure-flags
83
96
97+ # Provide a GHC environment file
98+ cat > $out/ghc-environment <<EOF
99+ clear-package-db
100+ package-db $out/package.conf.d
101+ EOF
102+ ${lib.concatMapStringsSep "\n" (p: envDep "--package-db ${p.components.library}/package.conf.d" p.identifier.name) component.depends}
103+ ${lib.concatMapStringsSep "\n" (envDep "") (lib.remove "ghc" nonReinstallablePkgs)}
104+
84 '' + lib.optionalString component.doExactConfig ''
105 '' + lib.optionalString component.doExactConfig ''
85 echo "--exact-configuration" >> $out/configure-flags
106 echo "--exact-configuration" >> $out/configure-flags
86 echo "allow-newer: ${package.identifier.name}:*" >> $out/cabal.config
107 echo "allow-newer: ${package.identifier.name}:*" >> $out/cabal.config
@@ -117,9 +138,9 @@ let
117 sed -i "s,dynamic-library-dirs: .*,dynamic-library-dirs: $dynamicLinksDir," $f
138 sed -i "s,dynamic-library-dirs: .*,dynamic-library-dirs: $dynamicLinksDir," $f
118 done
139 done
119 '' + ''
140 '' + ''
120− ${ghc.targetPrefix}ghc-pkg -v0 --package-db $out/package.conf.d recache
141+ target-pkg --package-db $out/package.conf.d recache
121 '' + ''
142 '' + ''
122− ${ghc.targetPrefix}ghc-pkg -v0 --package-db $out/package.conf.d check
143+ target-pkg --package-db $out/package.conf.d check
123 '');
144 '');
124
145
125 finalConfigureFlags = lib.concatStringsSep " " (
146 finalConfigureFlags = lib.concatStringsSep " " (
@@ -147,6 +168,16 @@ let
147 ++ component.configureFlags
168 ++ component.configureFlags
148 );
169 );
149
170
171+ executableToolDepends = lib.concatMap (c: if c.isHaskell or false
172+ then builtins.attrValues (c.components.exes or {})
173+ else [c]) component.build-tools;
174+
175+ # Unfortunately, we need to wrap ghc commands for cabal builds to
176+ # work in the nix-shell. See ../doc/removing-with-package-wrapper.md.
177+ shellWrappers = withPackage {
178+ inherit package configFiles;
179+ };
180+
150in stdenv.mkDerivation ({
181in stdenv.mkDerivation ({
151 name = fullName;
182 name = fullName;
152
183
@@ -156,6 +187,7 @@ in stdenv.mkDerivation ({
156 inherit (package) identifier;
187 inherit (package) identifier;
157 config = component;
188 config = component;
158 inherit configFiles;
189 inherit configFiles;
190+ env = shellWrappers;
159 };
191 };
160
192
161 meta = {
193 meta = {
@@ -169,6 +201,7 @@ in stdenv.mkDerivation ({
169 };
201 };
170
202
171 CABAL_CONFIG = configFiles + /cabal.config;
203 CABAL_CONFIG = configFiles + /cabal.config;
204+ GHC_ENVIRONMENT = configFiles + /ghc-environment;
172 LANG = "en_US.UTF-8"; # GHC needs the locale configured during the Haddock phase.
205 LANG = "en_US.UTF-8"; # GHC needs the locale configured during the Haddock phase.
173 LC_ALL = "en_US.UTF-8";
206 LC_ALL = "en_US.UTF-8";
174
207
@@ -181,9 +214,7 @@ in stdenv.mkDerivation ({
181 nativeBuildInputs =
214 nativeBuildInputs =
182 [ghc]
215 [ghc]
183 ++ lib.optional (component.pkgconfig != []) pkgconfig
216 ++ lib.optional (component.pkgconfig != []) pkgconfig
184− ++ lib.concatMap (c: if c.isHaskell or false
217+ ++ executableToolDepends;
185− then builtins.attrValues (c.components.exes or {})
186− else [c]) component.build-tools;
187
218
188 SETUP_HS = setup + /bin/Setup;
219 SETUP_HS = setup + /bin/Setup;
189
220
@@ -235,6 +266,11 @@ in stdenv.mkDerivation ({
235 ''}
266 ''}
236 runHook postInstall
267 runHook postInstall
237 '';
268 '';
269+
270+ shellHook = ''
271+ export PATH="${shellWrappers}/bin:$PATH"
272+ ${toString shellHook}
273+ '';
238}
274}
239# patches can (if they like) depend on the version and revision of the package.
275# patches can (if they like) depend on the version and revision of the package.
240// lib.optionalAttrs (patches != []) { patches = map (p: if builtins.isFunction p then p { inherit (package.identifier) version; inherit revision; } else p) patches; }
276// lib.optionalAttrs (patches != []) { patches = map (p: if builtins.isFunction p then p { inherit (package.identifier) version; inherit revision; } else p) patches; }
builder/default.nix+5−2
@@ -1,4 +1,4 @@
1−{ pkgs, buildPackages, stdenv, lib, haskellLib, ghc, buildGHC, fetchurl, writeText, runCommand, pkgconfig, nonReinstallablePkgs }:
1+{ pkgs, buildPackages, stdenv, lib, haskellLib, ghc, buildGHC, fetchurl, writeText, runCommand, pkgconfig, nonReinstallablePkgs, withPackage }:
2
2
3{ flags
3{ flags
4, package
4, package
@@ -22,6 +22,8 @@
22, preInstall
22, preInstall
23, postInstall
23, postInstall
24
24
25+, shellHook
26+
25, ...
27, ...
26}@config:
28}@config:
27
29
@@ -67,11 +69,12 @@ let
67 '';
69 '';
68 };
70 };
69
71
70− comp-builder = haskellLib.weakCallPackage pkgs ./comp-builder.nix { inherit ghc haskellLib nonReinstallablePkgs; };
72+ comp-builder = haskellLib.weakCallPackage pkgs ./comp-builder.nix { inherit ghc haskellLib nonReinstallablePkgs withPackage; };
71
73
72 buildComp = componentId: component: comp-builder {
74 buildComp = componentId: component: comp-builder {
73 inherit componentId component package name src flags setup cabalFile patches revision
75 inherit componentId component package name src flags setup cabalFile patches revision
74 preUnpack postUnpack preConfigure postConfigure preBuild postBuild preCheck postCheck preInstall postInstall
76 preUnpack postUnpack preConfigure postConfigure preBuild postBuild preCheck postCheck preInstall postInstall
77+ shellHook
75 ;
78 ;
76 };
79 };
77
80
builder/with-package-wrapper.nixnew file+77
@@ -0,0 +1,77 @@
1+# This is a simplified version of the ghcWithPackages wrapper in
2+# nixpkgs, adapted to work with the package database of a single
3+# component.
4+{ lib, stdenv, ghc, runCommand, lndir, makeWrapper
5+}:
6+
7+{ package
8+, configFiles
9+, postBuild ? ""
10+}:
11+
12+let
13+ isGhcjs = ghc.isGhcjs or false;
14+ ghcCommand' = if isGhcjs then "ghcjs" else "ghc";
15+ ghcCommand = "${ghc.targetPrefix}${ghcCommand'}";
16+ ghcCommandCaps= lib.toUpper ghcCommand';
17+ libDir = "$out/lib/${ghcCommand}-${ghc.version}";
18+ docDir = "$out/share/doc/ghc/html";
19+ packageCfgDir = "${libDir}/package.conf.d";
20+
21+in runCommand "${ghc.name}-with-${package.identifier.name}" {
22+ preferLocalBuild = true;
23+ passthru = {
24+ inherit (ghc) version meta;
25+ baseGhc = ghc;
26+ inherit package;
27+ };
28+} (
29+ ''
30+ . ${makeWrapper}/nix-support/setup-hook
31+
32+ # Start with a ghc...
33+ mkdir -p $out/bin
34+ ${lndir}/bin/lndir -silent ${ghc} $out
35+
36+ # ...and replace package database with the one from target package config.
37+ rm -rf ${libDir}
38+ mkdir -p ${libDir}
39+ ln -s ${configFiles}/package.conf.d ${packageCfgDir}
40+
41+ # Wrap compiler executables with correct env variables.
42+ # The NIX_ variables are used by the patched Paths_ghc module.
43+ # The GHC_ENVIRONMENT variable forces ghc to use the build
44+ # dependencies of the component.
45+
46+ for prg in ${ghcCommand} ${ghcCommand}i ${ghcCommand}-${ghc.version} ${ghcCommand}i-${ghc.version} runghc runhaskell; do
47+ if [[ -x "${ghc}/bin/$prg" ]]; then
48+ rm -f $out/bin/$prg
49+ makeWrapper ${ghc}/bin/$prg $out/bin/$prg \
50+ --set "NIX_${ghcCommandCaps}" "$out/bin/${ghcCommand}" \
51+ --set "NIX_${ghcCommandCaps}PKG" "$out/bin/${ghcCommand}-pkg" \
52+ --set "NIX_${ghcCommandCaps}_DOCDIR" "${docDir}" \
53+ --set "NIX_${ghcCommandCaps}_LIBDIR" "${libDir}" \
54+ --set "${ghcCommandCaps}_ENVIRONMENT" "${configFiles}/ghc-environment"
55+ fi
56+ done
57+
58+ # Point ghc-pkg to the package database of the component using the
59+ # GHC_PACKAGE_PATH variable.
60+
61+ for prg in ${ghcCommand}-pkg ${ghcCommand}-pkg-${ghc.version}; do
62+ if [[ -x "${ghc}/bin/$prg" ]]; then
63+ rm -f $out/bin/$prg
64+ makeWrapper ${ghc}/bin/$prg $out/bin/$prg \
65+ --set "${ghcCommandCaps}_PACKAGE_PATH" "${configFiles}/package.conf.d"
66+ fi
67+ done
68+
69+ # fixme: check if this is needed
70+ # haddock was referring to the base ghc, https://github.com/NixOS/nixpkgs/issues/36976
71+ if [[ -x "${ghc}/bin/haddock" ]]; then
72+ rm -f $out/bin/haddock
73+ makeWrapper ${ghc}/bin/haddock $out/bin/haddock \
74+ --set "NIX_${ghcCommandCaps}_LIBDIR" "${libDir}"
75+ fi
76+ ''
77+)
doc/removing-with-package-wrapper.mdnew file+66
@@ -0,0 +1,66 @@
1+# `ghcWithPackages` wrapper removal
2+
3+The current [Nixpkgs Haskell infrastructure][nixpkgs-haskell] and `haskell.nix` both
4+provide a `ghcWithPackages` derivation which contains shell script
5+wrappers that wrap `ghc` and `ghc-pkg`.
6+
7+In the Nixpkgs Haskell infrastructure, the wrapper scripts are used
8+for building Haskell packages. However, in `haskell.nix`, the wrappers
9+are only used for development environments.
10+
11+The wrapper scripts provide a `ghc` command that "knows" about the
12+package set and has all Haskell package dependencies available to it.
13+
14+We would like to remove the wrapper scripts, but it's currently not
15+possible to configure all build tools using environment variables
16+alone.
17+
18+## Plain `ghc`
19+
20+When using `ghc` or `ghci` by itself, the `GHC_ENVIRONMENT` variable
21+can point to a configuration file containing an exact package
22+set. This works quite well.
23+
24+## `ghc-pkg`
25+
26+The package tool `ghc-pkg` does not recognize `GHC_ENVIRONMENT`, but
27+does recognize a `GHC_PACKAGE_PATH` pointing to a `package.conf.d`.
28+
29+This works well. However, the `cabal` command will refuse to start if
30+`GHC_PACKAGE_PATH` is set.
31+
32+## `Setup.hs`
33+
34+When invoking `Setup.hs configure`, the package database is provided
35+with the `--package-db` argument and exact dependencies in the package
36+set can be provided as `--dependency` arguments.
37+
38+The `haskell.nix` component builder uses `Setup.hs` with these
39+command-line options to build Haskell packages.
40+
41+## `cabal new-build`
42+
43+Cabal-install will observe the `CABAL_CONFIG` environment variable,
44+which points to a cabal config file. This config file can provide a
45+`package-db` value, but it can't specify exact versions of packages.
46+
47+Cabal is designed to solve dependencies, not simply take the package
48+set which is given to it.
49+
50+Therefore, `cabal` does not use `GHC_ENVIRONMENT`, but instead creates
51+its own environment file. It will not accept `--dependency` arguments.
52+
53+As far as I know, the best way to force `cabal` to take a pre-computed
54+package set is to use a `new-freeze` file. However there is no
55+environment variable (or config file entry) which can specify a path
56+to a freeze file.
57+
58+Specifying a `package-db` path in the cabal config file is not enough
59+for it to successfully resolve dependencies.
60+
61+As mentioned before, `cabal` does not work when `GHC_PACKAGE_PATH` is
62+set. The best way to work around this is to wrap `ghc` and `ghc-pkg`
63+in shell scripts.
64+
65+
66+[nixpkgs-haskell]: https://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure
modules/component-driver.nix+9
@@ -6,7 +6,16 @@ let
6 ghc = config.ghc.package;
6 ghc = config.ghc.package;
7 buildGHC = buildModules.config.ghc.package;
7 buildGHC = buildModules.config.ghc.package;
8 inherit (config) nonReinstallablePkgs;
8 inherit (config) nonReinstallablePkgs;
9+ inherit withPackage;
9 };
10 };
11+
12+ withPackage = import ../builder/with-package-wrapper.nix {
13+ inherit lib;
14+ inherit (pkgs) stdenv runCommand makeWrapper;
15+ inherit (pkgs.xorg) lndir;
16+ ghc = config.ghc.package;
17+ };
18+
10in
19in
11
20
12{
21{
modules/package.nix+4
@@ -270,6 +270,10 @@ in {
270 type = nullOr string;
270 type = nullOr string;
271 default = null;
271 default = null;
272 };
272 };
273+ shellHook = mkOption {
274+ type = nullOr string;
275+ default = null;
276+ };
273 doCheck = mkOption {
277 doCheck = mkOption {
274 type = bool;
278 type = bool;
275 default = false;
279 default = false;