Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

shellFor: Refactor for consistency and cross #76349

Merged
merged 3 commits into from Jan 17, 2020

Conversation

Ericson2314
Copy link
Member

Motivation for this change

This makes it work like work-on-multi from Reflex Platform. In
particular, rather than making .env from shellFor, we make .env
the primitive, and shellFor works by combining together the arguments
of all the packages to generic-builder and taking the .env of the
resulting mashup-package.

There are 2 benefits of this:

  1. The dependency logic is deduplicated. generic builder just concatted
    lists, whereas all the envs until now would sieve apart haskell and
    system build inputs. Now, they both decide haskell vs system the same
    way: according to the argument list and without reflection.
    Consistency is good, especially because it mean that if the build
    works, the shell is more likely to work.

  2. Cross is handled better. For native builds, because the
    ghcWithPackages calls would shadow, we through both the regular
    component (lib, exe, test, bench) haskell deps and Setup.hs haskell
    deps in the same ghcWithPackages call. But for cross builds we use
    buildPackages.ghcWithPackages to get the setup deps. This ensures
    everything works correctly.

Things done
  • Tested using sandboxing (nix.useSandbox on NixOS, or option sandbox in nix.conf on non-NixOS linux)
  • Built on platform(s)
    • NixOS
    • macOS
    • other Linux distributions
  • Tested via one or more NixOS test(s) if existing and applicable for the change (look inside nixos/tests)
  • Tested compilation of all pkgs that depend on this change using nix-shell -p nixpkgs-review --run "nixpkgs-review wip"
  • Tested execution of all binary files (usually in ./result/bin/)
  • Determined the impact on package closure size (by running nix path-info -S before and after)
  • Ensured that relevant documentation is up to date
  • Fits CONTRIBUTING.md.
Notify maintainers

cc @ElvishJerricco @infinisil

@cdepillabout
Copy link
Member

cdepillabout commented Dec 24, 2019

Cross is handled better. For native builds, because the ghcWithPackages calls would shadow, we through both the regular component (lib, exe, test, bench) haskell deps and Setup.hs haskell deps in the same ghcWithPackages call. But for cross builds we use buildPackages.ghcWithPackages to get the setup deps. This ensures everything works correctly.

Could you add more documentation in the code about this?

There is a huge lack of documentation about cross-compilation. When I'm making changes to Haskell stuff, I have very little confidence I'm not breaking cross-complation-related things.


Also, do you have any suggestion on how I can test cross-compilation stuff? Should I buy a rasberry-pi raspberry pi and cross compile Haskell stuff for it?


One more thing: would it be possible to write a test for this new shellFor specifically for cross compiling?

@cdepillabout
Copy link
Member

@Ericson2314 I left a lot of comments above, but I should also add that I am very grateful you guys are upstreaming stuff from obsidian :-)

@Ericson2314
Copy link
Member Author

Ericson2314 commented Dec 24, 2019

@Ericson2314 I left a lot of comments above, but I should also add that I am very grateful you guys are upstreaming stuff from obsidian :-)

Hehe thank you, they are all good questions I will be sure we get to all of them and document everything thoroughly before anything is merged.

Upstreaming most of reflex-platform is something I've been waiting years to finally have the time to do, so I'm as excited as you are. :)

@Ericson2314
Copy link
Member Author

We will add some tests in pkgs/test, then this should be good to go.

@@ -22,6 +22,8 @@ with pkgs;
cc-wrapper-libcxx-7 = callPackage ./cc-wrapper { stdenv = llvmPackages_7.libcxxStdenv; };
stdenv-inputs = callPackage ./stdenv-inputs { };

haskell = callPackage ./haskell { };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this to be tested, we also need an entry in pkgs/top-level/release.nix.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh thanks

pkgconfigDepends
setupHaskellDepends
;
} // stdenv.lib.optionalAttrs doCheck {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this needs to be optional given we are calculating regardless of whether doCheck / doBenchmark is on.

@@ -1,4 +1,4 @@
{ lib, stdenv, ghc, llvmPackages, packages, symlinkJoin, makeWrapper
{ lib, stdenv, ghc, llvmPackages, packages, buildEnv, makeWrapper
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one was reverted. I think this might just be a github diff bug, but we should verify this change isn’t getting into master accidentally.

#77523 corrects it in the 19.03/19.09/master merge-base.

@matthewbauer matthewbauer changed the title WIP: shellFor: Refactor for consistency and cross shellFor: Refactor for consistency and cross Jan 16, 2020
@matthewbauer
Copy link
Member

matthewbauer commented Jan 16, 2020

Real diff is:

diff --git a/pkgs/development/haskell-modules/generic-builder.nix b/pkgs/development/haskell-modules/generic-builder.nix
index 5410fccf0bb..4c552d64d9a 100644
--- a/pkgs/development/haskell-modules/generic-builder.nix
+++ b/pkgs/development/haskell-modules/generic-builder.nix
@@ -1,5 +1,6 @@
 { stdenv, buildPackages, buildHaskellPackages, ghc
-, jailbreak-cabal, hscolour, cpphs, nodejs, shellFor
+, jailbreak-cabal, hscolour, cpphs, nodejs
+, ghcWithHoogle, ghcWithPackages
 }:
 
 let
@@ -206,21 +207,28 @@ let
                         optionals doCheck testPkgconfigDepends ++ optionals doBenchmark benchmarkPkgconfigDepends;
 
   depsBuildBuild = [ nativeGhc ];
-  nativeBuildInputs = [ ghc removeReferencesTo ] ++ optional (allPkgconfigDepends != []) pkgconfig ++
-                      setupHaskellDepends ++
-                      buildTools ++ libraryToolDepends ++ executableToolDepends ++
-                      optionals doCheck testToolDepends ++
-                      optionals doBenchmark benchmarkToolDepends;
+  collectedToolDepends =
+    buildTools ++ libraryToolDepends ++ executableToolDepends ++
+    optionals doCheck testToolDepends ++
+    optionals doBenchmark benchmarkToolDepends;
+  nativeBuildInputs =
+    [ ghc removeReferencesTo ] ++ optional (allPkgconfigDepends != []) pkgconfig ++
+    setupHaskellDepends ++ collectedToolDepends;
   propagatedBuildInputs = buildDepends ++ libraryHaskellDepends ++ executableHaskellDepends ++ libraryFrameworkDepends;
-  otherBuildInputs = extraLibraries ++ librarySystemDepends ++ executableSystemDepends ++ executableFrameworkDepends ++
-                     allPkgconfigDepends ++
-                     optionals doCheck (testDepends ++ testHaskellDepends ++ testSystemDepends ++ testFrameworkDepends) ++
-                     optionals doBenchmark (benchmarkDepends ++ benchmarkHaskellDepends ++ benchmarkSystemDepends ++ benchmarkFrameworkDepends);
-
-
-  allBuildInputs = propagatedBuildInputs ++ otherBuildInputs ++ depsBuildBuild ++ nativeBuildInputs;
-  isHaskellPartition =
-    stdenv.lib.partition isHaskellPkg allBuildInputs;
+  otherBuildInputsHaskell =
+    optionals doCheck (testDepends ++ testHaskellDepends) ++
+    optionals doBenchmark (benchmarkDepends ++ benchmarkHaskellDepends);
+  otherBuildInputsSystem =
+    extraLibraries ++ librarySystemDepends ++ executableSystemDepends ++ executableFrameworkDepends ++
+    allPkgconfigDepends ++
+    optionals doCheck (testSystemDepends ++ testFrameworkDepends) ++
+    optionals doBenchmark (benchmarkSystemDepends ++ benchmarkFrameworkDepends);
+  # TODO next rebuild just define as `otherBuildInputsHaskell ++ otherBuildInputsSystem`
+  otherBuildInputs =
+    extraLibraries ++ librarySystemDepends ++ executableSystemDepends ++ executableFrameworkDepends ++
+    allPkgconfigDepends ++
+    optionals doCheck (testDepends ++ testHaskellDepends ++ testSystemDepends ++ testFrameworkDepends) ++
+    optionals doBenchmark (benchmarkDepends ++ benchmarkHaskellDepends ++ benchmarkSystemDepends ++ benchmarkFrameworkDepends);
 
   setupCommand = "./Setup";
 
@@ -462,17 +470,61 @@ stdenv.mkDerivation ({
     runHook postInstall
   '';
 
-  passthru = passthru // {
+  passthru = passthru // rec {
 
     inherit pname version;
 
     compiler = ghc;
 
+    # All this information is intended just for `shellFor`.  It should be
+    # considered unstable and indeed we knew how to keep it private we would.
+    getCabalDeps = {
+      inherit
+        buildDepends
+        buildTools
+        executableFrameworkDepends
+        executableHaskellDepends
+        executablePkgconfigDepends
+        executableSystemDepends
+        executableToolDepends
+        extraLibraries
+        libraryFrameworkDepends
+        libraryHaskellDepends
+        libraryPkgconfigDepends
+        librarySystemDepends
+        libraryToolDepends
+        pkgconfigDepends
+        setupHaskellDepends
+        ;
+    } // stdenv.lib.optionalAttrs doCheck {
+      inherit
+        testDepends
+        testFrameworkDepends
+        testHaskellDepends
+        testPkgconfigDepends
+        testSystemDepends
+        testToolDepends
+        ;
+    } // stdenv.lib.optionalAttrs doBenchmark {
+      inherit
+        benchmarkDepends
+        benchmarkFrameworkDepends
+        benchmarkHaskellDepends
+        benchmarkPkgconfigDepends
+        benchmarkSystemDepends
+        benchmarkToolDepends
+        ;
+    };
 
-    getBuildInputs = {
+    # Attributes for the old definition of `shellFor`. Should be removed but
+    # this predates the warning at the top of `getCabalDeps`.
+    getBuildInputs = rec {
       inherit propagatedBuildInputs otherBuildInputs allPkgconfigDepends;
       haskellBuildInputs = isHaskellPartition.right;
       systemBuildInputs = isHaskellPartition.wrong;
+      isHaskellPartition = stdenv.lib.partition
+        isHaskellPkg
+        (propagatedBuildInputs ++ otherBuildInputs ++ depsBuildBuild ++ nativeBuildInputs);
     };
 
     isHaskellLibrary = isLibrary;
@@ -485,10 +537,64 @@ stdenv.mkDerivation ({
     # TODO: fetch the self from the fixpoint instead
     haddockDir = self: if doHaddock then "${docdir self.doc}/html" else null;
 
-    env = shellFor {
-      packages = p: [ drv ];
-      inherit shellHook;
-    };
+    # Creates a derivation containing all of the necessary dependencies for building the
+    # parent derivation. The attribute set that it takes as input can be viewed as:
+    #
+    #    { withHoogle }
+    #
+    # The derivation that it builds contains no outpaths because it is meant for use
+    # as an environment
+    #
+    #   # Example use
+    #   # Creates a shell with all of the dependencies required to build the "hello" package,
+    #   # and with python:
+    #
+    #   > nix-shell -E 'with (import <nixpkgs> {}); \
+    #   >    haskell.packages.ghc865.hello.envFunc { buildInputs = [ python ]; }'
+    envFunc = { withHoogle ? false }:
+      let
+        name = "ghc-shell-for-${drv.name}";
+
+        withPackages = if withHoogle then ghcWithHoogle else ghcWithPackages;
+
+        # We use the `ghcWithPackages` function from `buildHaskellPackages` if we
+        # want a shell for the sake of cross compiling a package. In the native case
+        # we don't use this at all, and instead put the setupDepends in the main
+        # `ghcWithPackages`. This way we don't have two wrapper scripts called `ghc`
+        # shadowing each other on the PATH.
+        ghcEnvForBuild =
+          assert isCross;
+          buildHaskellPackages.ghcWithPackages (_: setupHaskellDepends);
+
+        ghcEnv = withPackages (_:
+          otherBuildInputsHaskell ++
+          propagatedBuildInputs ++
+          stdenv.lib.optionals (!isCross) setupHaskellDepends);
+
+        ghcCommandCaps = stdenv.lib.toUpper ghcCommand';
+      in stdenv.mkDerivation ({
+        inherit name shellHook;
+
+        depsBuildBuild = stdenv.lib.optional isCross ghcEnvForBuild;
+        nativeBuildInputs =
+          [ ghcEnv ] ++ optional (allPkgconfigDepends != []) pkgconfig ++
+          collectedToolDepends;
+        buildInputs =
+          otherBuildInputsSystem;
+        phases = ["installPhase"];
+        installPhase = "echo $nativeBuildInputs $buildInputs > $out";
+        LANG = "en_US.UTF-8";
+        LOCALE_ARCHIVE = stdenv.lib.optionalString (stdenv.hostPlatform.libc == "glibc") "${buildPackages.glibcLocales}/lib/locale/locale-archive";
+        "NIX_${ghcCommandCaps}" = "${ghcEnv}/bin/${ghcCommand}";
+        "NIX_${ghcCommandCaps}PKG" = "${ghcEnv}/bin/${ghcCommand}-pkg";
+        # TODO: is this still valid?
+        "NIX_${ghcCommandCaps}_DOCDIR" = "${ghcEnv}/share/doc/ghc/html";
+        "NIX_${ghcCommandCaps}_LIBDIR" = if ghc.isHaLVM or false
+          then "${ghcEnv}/lib/HaLVM-${ghc.version}"
+          else "${ghcEnv}/lib/${ghcCommand}-${ghc.version}";
+      });
+
+  env = envFunc { };
 
   };
 
diff --git a/pkgs/development/haskell-modules/make-package-set.nix b/pkgs/development/haskell-modules/make-package-set.nix
index e2d01c5798f..9ba25e09db9 100644
--- a/pkgs/development/haskell-modules/make-package-set.nix
+++ b/pkgs/development/haskell-modules/make-package-set.nix
@@ -38,12 +38,12 @@ let
   inherit (stdenv) buildPlatform hostPlatform;
 
   inherit (stdenv.lib) fix' extends makeOverridable;
-  inherit (haskellLib) overrideCabal getBuildInputs;
+  inherit (haskellLib) overrideCabal;
 
   mkDerivationImpl = pkgs.callPackage ./generic-builder.nix {
     inherit stdenv;
     nodejs = buildPackages.nodejs-slim;
-    inherit (self) buildHaskellPackages ghc shellFor;
+    inherit (self) buildHaskellPackages ghc ghcWithHoogle ghcWithPackages;
     inherit (self.buildHaskellPackages) jailbreak-cabal;
     hscolour = overrideCabal self.buildHaskellPackages.hscolour (drv: {
       isLibrary = false;
@@ -258,6 +258,8 @@ in package-set { inherit pkgs stdenv callPackage; } self // {
     # packages themselves. Using nix-shell on this derivation will
     # give you an environment suitable for developing the listed
     # packages with an incremental tool like cabal-install.
+    # In addition to the "packages" arg and "withHoogle" arg, anything that
+    # can be passed into stdenv.mkDerivation can be included in the input attrset
     #
     #     # default.nix
     #     with import <nixpkgs> {};
@@ -268,9 +270,11 @@ in package-set { inherit pkgs stdenv callPackage; } self // {
     #     })
     #
     #     # shell.nix
+    #     let pkgs = import <nixpkgs> {} in
     #     (import ./.).shellFor {
     #       packages = p: [p.frontend p.backend p.common];
     #       withHoogle = true;
+    #       buildInputs = [ pkgs.python ];
     #     }
     #
     #     -- cabal.project
@@ -280,49 +284,41 @@ in package-set { inherit pkgs stdenv callPackage; } self // {
     #       common/
     #
     #     bash$ nix-shell --run "cabal new-build all"
+    #     bash$ nix-shell --run "python"
     shellFor = { packages, withHoogle ? false, ... } @ args:
       let
-        selected = packages self;
-
-        packageInputs = map getBuildInputs selected;
-
-        name = if pkgs.lib.length selected == 1
-          then "ghc-shell-for-${(pkgs.lib.head selected).name}"
-          else "ghc-shell-for-packages";
-
-        # If `packages = [ a b ]` and `a` depends on `b`, don't build `b`,
-        # because cabal will end up ignoring that built version, assuming
-        # new-style commands.
-        haskellInputs = pkgs.lib.filter
-          (input: pkgs.lib.all (p: input.outPath != p.outPath) selected)
-          (pkgs.lib.concatMap (p: p.haskellBuildInputs) packageInputs);
-        systemInputs = pkgs.lib.concatMap (p: p.systemBuildInputs) packageInputs;
-
-        withPackages = if withHoogle then self.ghcWithHoogle else self.ghcWithPackages;
-        ghcEnv = withPackages (p: haskellInputs);
-        nativeBuildInputs = pkgs.lib.concatMap (p: p.nativeBuildInputs) selected;
-
-        ghcCommand' = if ghc.isGhcjs or false then "ghcjs" else "ghc";
-        ghcCommand = "${ghc.targetPrefix}${ghcCommand'}";
-        ghcCommandCaps= pkgs.lib.toUpper ghcCommand';
-
-        mkDrvArgs = builtins.removeAttrs args ["packages" "withHoogle"];
-      in pkgs.stdenv.mkDerivation (mkDrvArgs // {
-        name = mkDrvArgs.name or name;
-
-        buildInputs = systemInputs ++ mkDrvArgs.buildInputs or [];
-        nativeBuildInputs = [ ghcEnv ] ++ nativeBuildInputs ++ mkDrvArgs.nativeBuildInputs or [];
-        phases = ["installPhase"];
-        installPhase = "echo $nativeBuildInputs $buildInputs > $out";
-        LANG = "en_US.UTF-8";
-        LOCALE_ARCHIVE = pkgs.lib.optionalString (stdenv.hostPlatform.libc == "glibc") "${buildPackages.glibcLocales}/lib/locale/locale-archive";
-        "NIX_${ghcCommandCaps}" = "${ghcEnv}/bin/${ghcCommand}";
-        "NIX_${ghcCommandCaps}PKG" = "${ghcEnv}/bin/${ghcCommand}-pkg";
-        # TODO: is this still valid?
-        "NIX_${ghcCommandCaps}_DOCDIR" = "${ghcEnv}/share/doc/ghc/html";
-        "NIX_${ghcCommandCaps}_LIBDIR" = if ghc.isHaLVM or false
-          then "${ghcEnv}/lib/HaLVM-${ghc.version}"
-          else "${ghcEnv}/lib/${ghcCommand}-${ghc.version}";
+        combinedPackageFor = packages:
+          let
+            selected = packages self;
+
+            pname = if pkgs.lib.length selected == 1
+              then (pkgs.lib.head selected).name
+              else "packages";
+
+            # If `packages = [ a b ]` and `a` depends on `b`, don't build `b`,
+            # because cabal will end up ignoring that built version, assuming
+            # new-style commands.
+            combinedPackages = pkgs.lib.filter
+              (input: pkgs.lib.all (p: input.outPath or null != p.outPath) selected);
+
+            # Returns an attrset containing a combined list packages' inputs for each
+            # stage of the build process
+            packageInputs = pkgs.lib.zipAttrsWith
+              (_: pkgs.lib.concatMap combinedPackages)
+              (map (p: p.getCabalDeps) selected);
+
+            genericBuilderArgs = {
+              inherit pname;
+              version = "0";
+              license = null;
+            } // packageInputs;
+
+          in self.mkDerivation genericBuilderArgs;
+
+        envFuncArgs = builtins.removeAttrs args [ "packages" ];
+      in (combinedPackageFor packages).env.overrideAttrs (old: envFuncArgs // {
+        nativeBuildInputs = old.nativeBuildInputs ++ envFuncArgs.nativeBuildInputs or [];
+        buildInputs = old.buildInputs ++ envFuncArgs.buildInputs or [];
       });
 
     ghc = ghc // {
diff --git a/pkgs/test/default.nix b/pkgs/test/default.nix
index eb0711b8885..f62d208d22d 100644
--- a/pkgs/test/default.nix
+++ b/pkgs/test/default.nix
@@ -26,6 +26,8 @@ with pkgs;
   cc-wrapper-libcxx-9 = callPackage ./cc-wrapper { stdenv = llvmPackages_9.libcxxStdenv; };
   stdenv-inputs = callPackage ./stdenv-inputs { };
 
+  haskell-shellFor = callPackage ./haskell-shellFor { };
+
   cc-multilib-gcc = callPackage ./cc-wrapper/multilib.nix { stdenv = gccMultiStdenv; };
   cc-multilib-clang = callPackage ./cc-wrapper/multilib.nix { stdenv = clangMultiStdenv; };
 

@@ -22,6 +22,8 @@ with pkgs;
cc-wrapper-libcxx-7 = callPackage ./cc-wrapper { stdenv = llvmPackages_7.libcxxStdenv; };
stdenv-inputs = callPackage ./stdenv-inputs { };

haskell-shellFor = callPackage ./haskell-shellFor { };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this file doesn't exist yet in this PR? Was this an oversight? Or is it still being written?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jmininger did we forget to commit??

jmininger and others added 2 commits January 17, 2020 10:46
This makes it work like work-on-multi from Reflex Platform. In
particular, rather than making `.env` from `shellFor`, we make `.env`
the primitive, and `shellFor` works by combining together the arguments
of all the packages to `generic-builder` and taking the `.env` of the
resulting mashup-package.

There are 2 benefits of this:

1. The dependency logic is deduplicated. generic builder just concatted
   lists, whereas all the envs until now would sieve apart haskell and
   system build inputs. Now, they both decide haskell vs system the same
   way: according to the argument list and without reflection.
   Consistency is good, especially because it mean that if the build
   works, the shell is more likely to work.

2. Cross is handled better. For native builds, because the
   `ghcWithPackages` calls would shadow, we through both the regular
   component (lib, exe, test, bench) haskell deps and Setup.hs haskell
   deps in the same `ghcWithPackages` call. But for cross builds we use
   `buildPackages.ghcWithPackages` to get the setup deps. This ensures
   everything works correctly.
We have just a few, and these are regular jobs not must-pass. The tests
that were must-pass are left as is.
@pjones
Copy link
Contributor

pjones commented Feb 11, 2020

@jmininger @Ericson2314

FYI: In make-package-set.nix, the comments for shellFor still mention using withHoogle, and the function itself still takes a withHoogle argument, although it never uses it.

I had some code using shellFor that silently stopped producing a hoogle database and it took me awhile to figure out what was going on. Switching to envFun did the trick. Thanks.

@teto
Copy link
Member

teto commented Feb 17, 2020

I went to comment just on that. withHoogle ceased working with shellFor. I've moved on to envFun too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants