Skip to content

Commit

Permalink
Overhaul Workplace Shell & Overrides
Browse files Browse the repository at this point in the history
A bit of history.  The workspace shell started off as a quick solution to get
overrides to express some of their side effects within the user's development
shell.  Because it didn't work closely with the actual mechanisms of mkShell,
the chosen mechanism, from overrides to dev shell, was use of propagated
inputs.  This and some cargo culting had resulted in overrides mimicking other
overrides, lots of use of propagatedNativeBuildInputs.

First, workspace shell's overhaul is given the actual package set instead of the
noBuild set that is used when running tests.  (the tests themselves may need
similar overhaul).  The new shell has one simple job:  Make everything required
to build every crate, except rust crates themselves, expressed in environment
and available from the store.  This has to be done for the entire rust crate
DAG.

Second, once the workspace shell could correctly express dependencies and shell
hooks etc without the abuse of propagated inputs, the overrides themselves could
be made much more sensible, much more consistent with the rest of Nix.

substractLists works fine for derivation filtering

The issue that lead to using attrSets was using the wrong crate outputs to
filter on.  After that was fixed, it was no longer necessary to construct
attrSets by keys.  If complexity of a dependency set (n^2 because of lib.unique)
becomes too high, the attrSet based implementation should become favored again.
This won't really show up to users for n < 1000 where n is the immediate
dependencies of crates that are not crates themselves.

Apple frameworks must be propagated?  Missing a setupHook?
  • Loading branch information
psionic-k committed Oct 4, 2022
1 parent f9450d9 commit 2ae2f57
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 60 deletions.
2 changes: 1 addition & 1 deletion overlay/make-package-set/internal.nix
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ lib.fix' (self:

in packageFunWith { mkRustCrate = mkRustCrate'; buildRustPackages = buildRustPackages'; } // {
inherit rustPackages callPackage pkgs rustToolchain noBuild;
workspaceShell = workspaceShell { inherit pkgs noBuild rustToolchain; };
workspaceShell = workspaceShell { inherit pkgs rustPackages rustToolchain; };
mkRustCrate = mkRustCrate';
buildRustPackages = buildRustPackages';
__splicedPackages = defaultScope;
Expand Down
4 changes: 4 additions & 0 deletions overlay/mkcrate.nix
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ let
inherit src version meta NIX_DEBUG;
name = "crate-${name}-${version}${optionalString (compileMode != "build") "-${compileMode}"}";

# Adding libiconv is a convenience hack. It really isn't needed by every
# derivation and should instead be added / propagated where appropriate, but
# until someone decides to investigate the actual dependencies, it remains
# here instead of in overrides.
buildInputs = runtimeDependencies ++ lib.optionals stdenv.hostPlatform.isDarwin [ pkgs.libiconv ];
propagatedBuildInputs = lib.unique (concatMap (drv: drv.propagatedBuildInputs) runtimeDependencies);
nativeBuildInputs = [ rustToolchain ] ++ buildtimeDependencies;
Expand Down
118 changes: 72 additions & 46 deletions overlay/overrides.nix
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
{ rustLib, lib, pkgs, buildPackages }:

let
inherit (rustLib) makeOverride nullOverride;

# The bindings in this let expression are used below in the recursive set to
# create overrides. They are not themselves overrides. See the list of `all`
# overrides and their definitions below.

# For example, openssl builds to separate directories in nixpkgs while the
# Rust crate expects all of the output to be in one directory. We use a
# symlink join to create an output of the sum of two of openssl's normal
# outputs. This is one example of a nixpkg requiring some slight finesse to
# be used as a buildInput for a Rust crate derivation.

envize = s: builtins.replaceStrings ["-"] ["_"] (lib.toUpper s);

patchOpenssl = pkgs:
Expand Down Expand Up @@ -44,25 +56,8 @@ let
libkrb5 = pkgs.libkrb5.override { inherit openssl; };
};

propagateEnv = name: envs: buildPackages.stdenv.mkDerivation {
name = "${name}-propagate-env";
setupHook = buildPackages.writeText "exports.sh" ''
${name}-setup-env() {
${lib.concatMapStringsSep
"\n"
({ name, value }: "export ${name}=${lib.escapeShellArg value}")
envs}
}
addEnvHooks "$hostOffset" ${name}-setup-env
'';
phases = "installPhase fixupPhase";
installPhase = "mkdir -p $out";
preferLocalBuild = true;
allowSubstitutes = false;
};

in rec {
patches = { inherit patchOpenssl patchCurl patchPostgresql joinOpenssl propagateEnv; };
patches = { inherit patchOpenssl patchCurl patchPostgresql joinOpenssl;};

# Don't forget to add new overrides here.
all = [
Expand Down Expand Up @@ -92,6 +87,7 @@ in rec {
overrideArgs = old: { rustcLinkFlags = old.rustcLinkFlags or [ ] ++ [ "--cap-lints" "warn" ]; };
};

# Every crate that depends on the cc crate (usually build scripts) will have the xcbuild
cc = if pkgs.stdenv.hostPlatform.isDarwin
then makeOverride {
name = "cc";
Expand All @@ -106,7 +102,9 @@ in rec {
curl-sys = makeOverride {
name = "curl-sys";
overrideAttrs = drv: {
propagatedBuildInputs = drv.propagatedBuildInputs or [ ] ++ [ (patchCurl pkgs) ];
propagatedBuildInputs = drv.propagatedBuildInputs or [ ] ++ [
(patchCurl pkgs)
];
};
};

Expand Down Expand Up @@ -135,7 +133,7 @@ in rec {
libdbus-sys = pkgs.rustBuilder.rustLib.makeOverride {
name = "libdbus-sys";
overrideAttrs = drv: {
propagatedBuildInputs = drv.propagatedBuildInputs or [ ] ++ [
buildInputs = drv.buildInputs or [ ] ++ [
pkgs.dbus
];
};
Expand All @@ -144,7 +142,7 @@ in rec {
libudev-sys = pkgs.rustBuilder.rustLib.makeOverride {
name = "libudev-sys";
overrideAttrs = drv: {
propagatedBuildInputs = drv.propagatedBuildInputs or [ ] ++ [
buildInputs = drv.buildInputs or [ ] ++ [
pkgs.udev
];
buildPhase = ''
Expand All @@ -163,6 +161,8 @@ in rec {
propagatedBuildInputs = drv.propagatedBuildInputs or [ ] ++ [
pkgs.darwin.apple_sdk.frameworks.Security
pkgs.darwin.apple_sdk.frameworks.CoreFoundation
];
buildInputs = drv.buildInputs or [ ] ++ [
pkgs.libgit2
];
preferLocalBuild = true;
Expand All @@ -174,76 +174,102 @@ in rec {
libssh2-sys = makeOverride {
name = "libssh2-sys";
overrideAttrs = drv: {
propagatedNativeBuildInputs = drv.propagatedNativeBuildInputs or [ ] ++ [ pkgs.openssl.dev pkgs.zlib.dev ];
buildInputs = drv.buildInputs or [ ] ++ [ pkgs.openssl.dev pkgs.zlib.dev ];
};
};

libsqlite3-sys = pkgs.rustBuilder.rustLib.makeOverride {
name = "libsqlite3-sys";
overrideAttrs = drv: {
propagatedNativeBuildInputs = drv.propagatedNativeBuildInputs or [ ] ++ [ pkgs.sqlite ];
buildInputs = drv.buildInputs or [ ] ++ [ pkgs.sqlite ];
};
};

openssl-sys = makeOverride {
name = "openssl-sys";
overrideAttrs = drv: {
propagatedBuildInputs = drv.propagatedBuildInputs or [ ] ++ [
(propagateEnv "openssl-sys" [
{ name = "RUSTFLAGS"; value = "--cfg ossl111 --cfg ossl110 --cfg ossl101";}
{ name = "${envize (pkgs.rustBuilder.rustLib.rustTriple pkgs.stdenv.buildPlatform)}_OPENSSL_DIR"; value = joinOpenssl (patchOpenssl pkgs.buildPackages); }
{ name = "${envize (pkgs.rustBuilder.rustLib.rustTriple pkgs.stdenv.hostPlatform)}_OPENSSL_DIR"; value = joinOpenssl (patchOpenssl pkgs); }
{ name = "OPENSSL_NO_VENDOR"; value = "1";} # fixed 0.9.60
])
];
# The setup hook will set the variables both for building openssl-sys and
# in dependent derivations. This mechanism will also set the variable
# inside our development shell. Because the setupHook does not add the
# joinOpenssl derivation as a dependnecy, we have to include it in
# nativeBuildInputs as well or the variable will point to a path not
# visible to the derivation at build time.
buildInputs = drv.buildInputs or [ ] ++ [(joinOpenssl (patchOpenssl pkgs.buildPackages))];

shellHook = drv.shellHook or "" + ''
export ${envize (pkgs.rustBuilder.rustLib.rustTriple pkgs.stdenv.buildPlatform)}_OPENSSL_DIR=${lib.escapeShellArg (joinOpenssl (patchOpenssl pkgs.buildPackages))}
export ${envize (pkgs.rustBuilder.rustLib.rustTriple pkgs.stdenv.hostPlatform)}_OPENSSL_DIR=${lib.escapeShellArg (joinOpenssl (patchOpenssl pkgs))}
export OPENSSL_NO_VENDOR=1 # fixed 0.9.60
export RUSTFLAGS="''${RUSTFLAGS:-} --cfg ossl111 --cfg ossl110 --cfg ossl101"
'';

# setupHook is also a means of injecting the build environment for a dependency
# setupHook = buildPackages.writeText "openssl-sys-setup-env.sh" ''
# openssl-sys-setup-env() {
# export ${envize (pkgs.rustBuilder.rustLib.rustTriple pkgs.stdenv.buildPlatform)}_OPENSSL_DIR=${lib.escapeShellArg (joinOpenssl (patchOpenssl pkgs.buildPackages))}
# export ${envize (pkgs.rustBuilder.rustLib.rustTriple pkgs.stdenv.hostPlatform)}_OPENSSL_DIR=${lib.escapeShellArg (joinOpenssl (patchOpenssl pkgs))}
# export OPENSSL_NO_VENDOR=1 # fixed 0.9.60
# export RUSTFLAGS="''${RUSTFLAGS:-} --cfg ossl111 --cfg ossl110 --cfg ossl101"
# }
# addEnvHooks "$hostOffset" openssl-sys-setup-env
# '';
};
};

pkg-config = makeOverride {
name = "pkg-config";
overrideAttrs = drv: {
propagatedBuildInputs = drv.propagatedBuildInputs or [ ] ++ [
# Every crate that depends on the pkg-config crate also gets pkg-config and this environment
propagatedNativeBuildInputs = drv.propagatedNativeBuildInputs or [ ] ++ [
pkgs.pkg-config
(propagateEnv "pkg-config" [
{ name = "PKG_CONFIG_ALLOW_CROSS"; value = "1"; }
])
];
shellHook = drv.shellHook or "" + ''
export PGK_CONFIG_ALLOW_CROSS=1
'';
};
};

pq-sys =
let
binEcho = s: "${pkgs.buildPackages.writeShellScriptBin "bin-echo" "echo ${s}"}/bin/bin-echo";
fake_pg_config = binEcho "${(patchPostgresql pkgs.buildPackages).lib}/lib";
in
makeOverride {
name = "pq-sys";
overrideAttrs = drv: {
# We can't use the host `pg_config` here, as it might not run on build platform. `pq-sys` only needs
# to know the `lib` directory for `libpq`, so just create a fake binary that gives it exactly that.
propagatedBuildInputs = drv.propagatedBuildInputs or [ ] ++ [
(propagateEnv "pq-sys" [
{ name = "PG_CONFIG_${envize pkgs.stdenv.buildPlatform.config}"; value = binEcho "${(patchPostgresql pkgs.buildPackages).lib}/lib"; }
{ name = "PG_CONFIG_${envize pkgs.stdenv.hostPlatform.config}"; value = binEcho "${(patchPostgresql pkgs).lib}/lib"; }
])
nativeBuildInputs = drv.nativeBuildInputs or [ ] ++ [
fake_pg_config
];
shellHook = drv.shellHook + ''
PG_CONFIG_${envize pkgs.stdenv.buildPlatform.config}="${fake_pg_config}"
'';
};
};

# Note that protobuff is from buildPackages and runs at the crate's
# build-time, so it's a nativeBuildInput. Every crate that depends on
# prost-build might need protoc at runtime, so it's propagated.
prost-build = makeOverride {
name = "prost-build";
overrideAttrs = drv: {
propagatedBuildInputs = drv.propagatedBuildInputs or [ ] ++ [
(propagateEnv "prost-build" [
{ name = "PROTOC"; value = "${pkgs.buildPackages.buildPackages.protobuf}/bin/protoc"; }
])
setupHook = buildPackages.writeText "prost-build-setup-env.sh" ''
prost-build-setup-env () {
PROTOC="${pkgs.buildPackages.buildPackages.protobuf}/bin/protoc"
}
addEnvHooks "$hostOffset" prost-build-setup-env
'';
propagatedNativeBuildInputs = drv.propagatedNativeBuildInputs or [ ] ++ [
pkgs.buildPackages.buildPackages.protobuf
];
};
};

protoc = makeOverride {
name = "protoc";
overrideAttrs = drv: {
propagatedBuildInputs = drv.propagatedBuildInputs or [ ] ++ [ pkgs.buildPackages.buildPackages.protobuf ];
propagatedNativeBuildInputs = drv.propagatedNativeBuildInputs or [ ] ++ [ pkgs.buildPackages.buildPackages.protobuf ];
};
};

Expand Down Expand Up @@ -287,7 +313,7 @@ in rec {
zmq-sys = makeOverride {
name = "zmq-sys";
overrideAttrs = drv: {
propagatedBuildInputs = drv.propagatedBuildInputs or [ ] ++ [ pkgs.zeromq ];
buildInputs = drv.buildInputs or [ ] ++ [ pkgs.zeromq ];
};
};
}
108 changes: 95 additions & 13 deletions overlay/workspace-shell.nix
Original file line number Diff line number Diff line change
@@ -1,16 +1,98 @@
{ pkgs, noBuild, rustToolchain }:
args@{
inputsFrom ? [],
nativeBuildInputs ? [],
...
}:

pkgs.mkShell (args // {
# `noBuild` is a special crate set used to create a development shell
# containing all native dependencies provided by the overrides above.
# `cargo build` within the shell should just work.
inputsFrom = (pkgs.lib.mapAttrsToList (_: pkg: pkg { }) noBuild.workspace) ++ inputsFrom;
nativeBuildInputs = [ rustToolchain ] ++ (with pkgs; [cacert]) ++ nativeBuildInputs;
{ pkgs, rustPackages, rustToolchain }:
# The resulting function is a decoration of mkShell. Usually mkShell is only
# given an inputsFrom that is a single package or packages without overlap in
# dependencies. However, we seek to provide a shell that is complete for every
# rust crate in the entire workspace. However, if we naively pass all rust
# crates to inputsFrom, many dependencies will be duplicated via propagation or
# multiple inclusion. Therefore, instead create the dependency sets from the
# rust crates are collected and de-duplicated before passing them to the normal
# mkShell. This is also done for the shellHook, just slightly differently. The
# source is really similar to nixpkgs mkShell that it decorates, so study one
# and understand both.
{ name ? "nix-shell"
, # a list of packages to add to the shell environment
packages ? [ ]
, # propagate all the inputs from the given derivations
inputsFrom ? [ ]
, buildInputs ? [ ]
, nativeBuildInputs ? [ ]
, propagatedBuildInputs ? [ ]
, propagatedNativeBuildInputs ? [ ]
, ...
}@attrs:
let
lib = pkgs.lib;

# mkShell in the end will pass through arguments that it doesn't explicitly
# handle onward to mkDerivation. Arguments removed at this point will be
# consumed and propagated by this function.
rest = builtins.removeAttrs attrs [
"buildInputs"
"nativeBuildInputs"
"propagatedBuildInputs"
"propagatedNativeBuildInputs"
"shellHook"
];

# The crate functions from which we will gather the inputs must be called to
# yield finished crate derivations. It is important to note that they will be
# called with their default arguments. Augmentation of the crates which may
# affect their dependencies in the user's flake will result in inconsistency,
# so if such behavior exists or is added by the user, a mechanism must be
# introduced to propagate these effects on dependencies into the shell.

# TODO note that if package set is created from nixpkgs for another platform,
# it will be inappropriate for creating a workspace shell. Flakes written for
# cross compilation but still get their workspaceShell from a package set for
# the build platform.

# TODO This is fragile because any path in the entire set that is not a crate
# function will cause failure. Recent changes to overlay/make-package-set or
# overlay/default, even changes to Nix itself could add a path. Replace this
# with some construct with no possibility of non-crate contamination.
crateFunctions = builtins.removeAttrs rustPackages
["workspace"
"workspaceShell"
"cargo2nixVersion"
"rustToolchain"
"rustPackages"
"pkgs"
"noBuild"
"mkRustCrate"
"callPackage"
"buildRustPackages"
"__unfix__"
"__splicedPackages"];

# Note that out paths must match between the crate and the crates dependencies
# (crates depend on crate.out) or else you will get multiple inclusion and
# crates themselves in the shell depencnedies. Cargo can build rust deps, so
# they are not needed in the development shell, nor will they be used by cargo
# outside of nix builds.
crates = map (pkg: (pkg { }).out) (pkgs.lib.collect builtins.isFunction crateFunctions);

# This function will extract the attr "name" from all crates, augment this
# list with the "name" from @attrs, remove explicit overlap with inputsFrom,
# and finally de-duplicate with unique.
mergeCrateInputs = name:
(lib.unique
(lib.subtractLists (packages ++ inputsFrom ++ crates)
((attrs.${name} or [ ]) ++ (lib.flatten (lib.catAttrs name crates)))));

in pkgs.mkShell (rest // {

# TODO investigate if cacert is needed or had been omitted in previous implementation
buildInputs = mergeCrateInputs "buildInputs";
nativeBuildInputs = (mergeCrateInputs "nativeBuildInputs") ++ [ rustToolchain ] ++ (with pkgs; [cacert]);
propagatedBuildInputs = mergeCrateInputs "propagatedBuildInputs";
propagatedNativeBuildInputs = mergeCrateInputs "propagatedNativeBuildInputs";

# Create a composite shellHook from the user passed shellHook and the rust
# crates' shellHooks. This hook will be merged by mkShell with the shellHooks
# in inputsFrom
shellHook = lib.concatStringsSep "\n" (lib.unique (lib.catAttrs "shellHook"
(lib.reverseList crates ++ [ attrs ])));

# Configures tools like Rust Analyzer to locate the correct rust-src
RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library";
})

0 comments on commit 2ae2f57

Please sign in to comment.