Rust components
mkLibrary mkService mkClient mkComponent
mkService
andmkClient
builds binaries.mkLibrary
creates an unpacked rust package (unpacked .crate file) since rust builds everything from source, which can be depended on by other components.mkComponent
is made for building extensions where you can create your own Nedryland component types by passing in thenedrylandType
string.
Specifying Crate Dependencies
In order to be deterministic crates are downloaded and verified ahead of building, and
these crates need to be made available to the build environment. This is done through
buildInputs
and propagatedBuildInputs
. Crates can be be defined using
base.languages.rust.fetchCrate {name = "some-crate-name"; version = "x.y.z"; sha256 = "sha"; deps = [crate]}
where sha256 is the same as you will find on
crates index and deps is a list of these
kind of derivations.
To ease writing of multiple such expressions there's a CLI tool
gen-crate-expression
both in each rust shell and as an app in Nedryglot's
flake. Use gen-crate-expression --help
for usage information.
A set of crates has also been generated and ships with nedryglot under
base.languages.rust.crates
. These are roughly equivalent to the crates available in
Rust Playground (that is, the 100 most downloaded crates on
crates.io). This default crate set can be
overridden by making an extension. This extension overrides the crate set with a set of
one crate:
{ base }:
{
languages.rust = base.languages.rust.overrideAttr {
crates = {
gimli = base.languages.rust.fetchCrate {
name="gimli";
version="0.27.3";
sha256="b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"; deps=[];
};
};
};
}
Specifying Other Dependencies
Specifying dependencies in a Rust component can be done in two ways. First option is to
assign any of the standard mkDerivation
arguments a list with the needed inputs.
# in the component nix file
{ base, lib, hello }:
base.languages.rust.mkClient {
name = "krabba";
src = ./.;
# always depend on hello from the build platform
buildInputs = [ hello ]
}
In the above example, note that we are always depending on the hello
package from the
build platform (see below). If we are not cross-compiling, this is
fine.
To ease cross compilation, a concept called splicing
is used and to take
advantage of that, the same arguments can also be assigned a function with a single
argument pkgs
. This function must return a list with the desired dependencies. pkgs
will be the correct package set for the current platform, making it possible to use the
same dependency specification for multiple platforms.
# in the component nix file
{ base, lib }:
base.languages.rust.mkClient {
name = "krabba";
src = ./.;
buildInputs = pkgs:
# depend on hello for all platforms
[ pkgs.hello ]
# depend on pthreads only for windows
++ lib.optional (pkgs.stdenv.hostPlatform.isWindows) pkgs.windows.pthreads
# and something only for wasi
++ lib.optional (pkgs.stdenv.hostPlatform.isWasi) pkgs.someWasiDependency
}
Cross Compilation
Nedryglot supports adding cross build targets to the rust language in addition
to the provided default target. The default target is always called rust
irrespective of platform. To add more targets either override base.languages.rust
and send in more crossTargets
, which by default is an empty set.
About Platforms
The platform concept is borrowed from tools like GNU autotools and might require some explanation since it is central to cross-compilation and not entirely intuitive.
Build Platform
This is the platform we are currently building on.
Host Platform
This is the platform we are building for. I.e. the platform that the software will run on. If we are not cross compiling, build platform and host platform are always the same.
Target Platform
This only makes sense if the software is compiler-like. For example, a compiler built on Linux for Linux will have Linux as build platform and host platform. However, even though it runs on Linux, it might produce binaries for Windows. I.e. Windows is the target platform.
mkCrossTarget
Nedryglot exposes mkCrossTarget
through base.languages.rust.mkCrossTarget
. This
function allows you to create your own custom cross target with the correct
arguments. It furthermore allows adding attributes to all targets using that target
definition.
# ...
myCrossTarget = base.languages.rust.mkCrossTarget {
name = "🐟-Ƽ";
pkgs = pkgsCross.fiscFive;
# attrs requires a function with one argument,
# containing the attributes from the package using
# the cross target
attrs = pkgAttrs: {
# the target requires the special
# build input "blubb" to produce
# valid builds
buildInputs =
base.languages.rust.combineInputs
(pkgAttrs.buildInputs or [ ])
(pkgs: [ pkgs.blubb ])
};
};
Note also the use of combineInputs
to combine input declarations, irrespective of if
they are functions or lists.
To add cross targets per component:
# in the component nix file.
{ base, pkgsCross }:
(base.languages.rust.override {
crossTargets = {
windows = base.languages.rust.mkCrossTarget {
name = "Windows";
pkgs = pkgsCross.mingwW64;
};
};
}).mkClient {
...
}
To add a cross target for every rust component in the project
# project.nix
nedryland.mkProject {
name = "windows-target-example";
baseExtensions = [ ./rust-target-extension.nix ];
}
The cross targets are exposed as <component>.<target>
which would in the above example
be <component>.windows
. As stated above, the default target is always called rust
.
# rust-target-extension.nix
{ base, pkgsCross }:
{
languages.rust = base.languages.rust.override {
crossTargets = {
windows = base.languages.rust.mkCrossTarget {
name = "Windows";
pkgs = pkgsCross.mingwW64;
};
};
};
}
In the above example, the project-wide meaning of base.languages.rust
is changed to
include cross-builds for Windows. However, it is also possible to create different
variations of the language by assigning to a different attribute,
e.g. base.languages.rustWithWindows
.
When a cross compilation target has been added project-wide, it is still possible to opt out of using it by overriding locally in a component. For example, to only build for the build platform:
# in the component nix file
{ base }:
(base.languages.rust.override {
crossTargets = { };
}).mkClient {
...
}
This will cause the above added windows
target to be removed and this component will
only build for the build platform, accessible on the component target rust
.
Overriding the Default Target
A component might only make sense to build for a cross target. To change the meaning of
the default target (rust
), override the attribute rust
in the cross target set, like
this:
# in the component nix file
{ base }:
(base.languages.rust.override {
crossTargets = {
rust = base.languages.rust.mkCrossTarget {
name = "Windows";
pkgs = pkgsCross.mingwW64;
};
}).mkClient {
...
}
This will cause rust
to correspond to a Windows build of the component. It is also
possible to refer to an existing cross target through crossTargets
# in the component nix file
{ base }:
(base.languages.rust.override {
crossTargets = {
rust = base.languages.rust.crossTargets.windows;
}).mkClient {
...
}
This will yield the same result as the previous example if windows
is already a defined target.
Adding Cross Targets
Using override
on the rust language means that any cross target definitions sent in will
replace the existing ones (with the exception of rust
which is added if it does not
exist in crossTargets
). To add a new cross target without replacing the existing ones,
use overrideCrossTargets
:
# rust-target-extension.nix
{ base, pkgsCross }:
{
languages.rust = base.languages.rust.overrideCrossTargets (prevTargets: {
windows = base.languages.rust.mkCrossTarget {
name = "Windows";
pkgs = pkgsCross.mingwW64;
};
});
}
This will add the windows
target without removing any of the pre-existing targets. Note
that overrideCrossTargets
also expects a function for the first argument, which gives
access to the set of crossTargets
that existed before the call to
overrideCrossTargets
. This makes it easier to create new targets based on existing ones.