feat(rust): port crane builder

This commit is contained in:
Yusuf Bera Ertan 2023-07-29 18:37:48 +03:00 committed by DavHau
parent 55b0141465
commit b2ca3da052
10 changed files with 900 additions and 4 deletions

View File

@ -0,0 +1,93 @@
{
lib,
crane,
utils,
vendoring,
# lock data
subsystemAttrs,
packages,
...
}: let
l = lib // builtins;
buildPackage = pname: version: let
replacePaths =
utils.replaceRelativePathsWithAbsolute
subsystemAttrs.relPathReplacements.${pname}.${version};
writeGitVendorEntries = vendoring.writeGitVendorEntries "nix-sources";
# common args we use for both buildDepsOnly and buildPackage
common = {
inherit pname version;
src = utils.getRootSource pname version;
cargoVendorDir = "$TMPDIR/nix-vendor";
installCargoArtifactsMode = "use-zstd";
postUnpack = ''
export CARGO_HOME=$(pwd)/.cargo_home
export cargoVendorDir="$TMPDIR/nix-vendor"
'';
preConfigure = ''
${writeGitVendorEntries}
${replacePaths}
'';
cargoTestProfile = "release";
cargoBuildProfile = "release";
# Make sure cargo only builds & tests the package we want
cargoBuildCommand = "cargo build \${cargoBuildFlags:-} --profile \${cargoBuildProfile} --package ${pname}";
cargoTestCommand = "cargo test \${cargoTestFlags:-} --profile \${cargoTestProfile} --package ${pname}";
};
# The deps-only derivation will use this as a prefix to the `pname`
depsNameSuffix = "-deps";
depsArgs =
common
// {
# we pass cargoLock path to buildDepsOnly
# so that crane's mkDummySrc adds it to the dummy source
inherit (utils) cargoLock;
pnameSuffix = depsNameSuffix;
# Make sure cargo only checks the package we want
cargoCheckCommand = "cargo check \${cargoBuildFlags:-} --profile \${cargoBuildProfile} --package ${pname}";
dream2nixVendorDir = vendoring.vendoredDependencies;
preUnpack = ''
${vendoring.copyVendorDir "$dream2nixVendorDir" common.cargoVendorDir}
'';
# move the vendored dependencies folder to $out for main derivation to use
postInstall = ''
mv $TMPDIR/nix-vendor $out/nix-vendor
'';
};
deps = crane.buildDepsOnly depsArgs;
buildArgs =
common
// {
meta = utils.getMeta pname version;
cargoArtifacts = deps;
# link the vendor dir we used earlier to the correct place
preUnpack = ''
${vendoring.copyVendorDir "$cargoArtifacts/nix-vendor" common.cargoVendorDir}
'';
# write our cargo lock
# note: we don't do this in buildDepsOnly since
# that uses a cargoLock argument instead
preConfigure = ''
${common.preConfigure}
${utils.writeCargoLock}
'';
passthru = {dependencies = deps;};
};
build = crane.buildPackage buildArgs;
in
build;
allPackages =
l.mapAttrs
(name: version: {"${version}" = buildPackage name version;})
packages;
in
allPackages

View File

@ -0,0 +1,104 @@
{
lib,
craneSource,
# nixpkgs
cargo,
makeSetupHook,
runCommand,
runCommandLocal,
writeText,
stdenv,
zstd,
jq,
remarshal,
}: let
importLibFile = name: import "${craneSource}/lib/${name}.nix";
makeHook = attrs: name:
makeSetupHook
({inherit name;} // attrs)
"${craneSource}/lib/setupHooks/${name}.sh";
genHooks = names: attrs: lib.genAttrs names (makeHook attrs);
crane = rec {
otherHooks =
genHooks [
"cargoHelperFunctionsHook"
"configureCargoCommonVarsHook"
"configureCargoVendoredDepsHook"
"removeReferencesToVendoredSourcesHook"
]
{};
installHooks =
genHooks [
"inheritCargoArtifactsHook"
"installCargoArtifactsHook"
]
{
substitutions = {
zstd = "${zstd}/bin/zstd";
};
};
installLogHook = genHooks ["installFromCargoBuildLogHook"] {
substitutions = {
cargo = "${cargo}/bin/cargo";
jq = "${jq}/bin/jq";
};
};
# These aren't used by dream2nix
crateNameFromCargoToml = null;
vendorCargoDeps = null;
writeTOML = importLibFile "writeTOML" {
inherit runCommand;
pkgsBuildBuild = {inherit remarshal;};
};
cleanCargoToml = importLibFile "cleanCargoToml" {};
findCargoFiles = importLibFile "findCargoFiles" {
inherit lib;
};
mkDummySrc = importLibFile "mkDummySrc" {
inherit writeText runCommandLocal lib;
inherit writeTOML cleanCargoToml findCargoFiles;
};
mkCargoDerivation = importLibFile "mkCargoDerivation" {
inherit stdenv zstd cargo;
inherit
(installHooks)
inheritCargoArtifactsHook
installCargoArtifactsHook
;
inherit
(otherHooks)
configureCargoCommonVarsHook
configureCargoVendoredDepsHook
cargoHelperFunctionsHook
;
inherit crateNameFromCargoToml vendorCargoDeps;
};
buildDepsOnly = importLibFile "buildDepsOnly" {
inherit lib;
inherit
mkCargoDerivation
crateNameFromCargoToml
vendorCargoDeps
mkDummySrc
;
};
buildPackage = importLibFile "buildPackage" {
inherit lib jq;
inherit (installLogHook) installFromCargoBuildLogHook;
inherit
buildDepsOnly
crateNameFromCargoToml
vendorCargoDeps
mkCargoDerivation
;
inherit (otherHooks) removeReferencesToVendoredSourcesHook;
};
};
in {
inherit (crane) buildPackage buildDepsOnly;
}

View File

@ -0,0 +1,169 @@
{
config,
lib,
extendModules,
...
}: let
l = lib // builtins;
cfg = config.rust-crane;
dreamLock = config.rust-cargo-lock.dreamLock;
fetchDreamLockSources =
import ../../../lib/internal/fetchDreamLockSources.nix
{inherit lib;};
getDreamLockSource = import ../../../lib/internal/getDreamLockSource.nix {inherit lib;};
readDreamLock = import ../../../lib/internal/readDreamLock.nix {inherit lib;};
hashPath = import ../../../lib/internal/hashPath.nix {
inherit lib;
inherit (config.deps) runCommandLocal nix;
};
hashFile = import ../../../lib/internal/hashFile.nix {
inherit lib;
inherit (config.deps) runCommandLocal nix;
};
# fetchers
fetchers = {
git = import ../../../lib/internal/fetchers/git {
inherit hashPath;
inherit (config.deps) fetchgit;
};
crates-io = import ../../../lib/internal/fetchers/crates-io {
inherit hashFile;
inherit (config.deps) fetchurl runCommandLocal;
};
path = import ../../../lib/internal/fetchers/path {
inherit hashPath;
};
};
dreamLockLoaded = readDreamLock {inherit dreamLock;};
dreamLockInterface = dreamLockLoaded.interface;
inherit (dreamLockInterface) defaultPackageName defaultPackageVersion;
fetchedSources' = fetchDreamLockSources {
inherit defaultPackageName defaultPackageVersion;
inherit (dreamLockLoaded.lock) sources;
inherit fetchers;
};
fetchedSources =
fetchedSources'
// {
${defaultPackageName}.${defaultPackageVersion} = config.mkDerivation.src;
};
getSource = getDreamLockSource fetchedSources;
toTOML = import ../../../lib/internal/toTOML.nix {inherit lib;};
utils = import ./utils.nix {
inherit dreamLock getSource lib toTOML;
inherit
(dreamLockInterface)
getSourceSpec
getRoot
subsystemAttrs
packages
;
inherit
(config.deps)
writeText
;
sourceRoot = config.mkDerivation.src;
};
vendoring = import ./vendor.nix {
inherit dreamLock getSource lib;
inherit
(dreamLockInterface)
getSourceSpec
subsystemAttrs
;
inherit
(config.deps)
cargo
jq
moreutils
python3Packages
runCommandLocal
writePython3
;
};
allPackages = import ./build.nix {
inherit lib utils vendoring;
inherit (dreamLockInterface) subsystemAttrs packages;
inherit (config.deps) crane;
};
selectedPackage = allPackages.${config.name}.${config.version};
in {
public = lib.mkForce {
type = "derivation";
inherit config extendModules;
inherit (config) name version;
inherit
(selectedPackage)
drvPath
outPath
outputs
outputName
meta
passthru
;
};
deps = {nixpkgs, ...}:
(l.mapAttrs (_: l.mkDefault) {
cargo = nixpkgs.cargo;
craneSource = config.deps.fetchFromGitHub {
owner = "ipetkov";
repo = "crane";
rev = "v0.12.2";
sha256 = "sha256-looLH5MdY4erLiJw0XwQohGdr0fJL9y6TJY3898RA2U=";
};
crane = import ./crane.nix {
inherit lib;
inherit
(config.deps)
craneSource
stdenv
cargo
jq
zstd
remarshal
makeSetupHook
writeText
runCommand
runCommandLocal
;
};
})
# maybe it would be better to put these under `options.rust-crane.deps` instead of this `deps`
# since it conflicts with a lot of stuff?
// (l.mapAttrs (_: l.mkOverride 999) {
inherit
(nixpkgs)
stdenv
fetchurl
jq
zstd
remarshal
moreutils
python3Packages
makeSetupHook
runCommandLocal
runCommand
writeText
fetchFromGitHub
;
inherit
(nixpkgs.writers)
writePython3
;
});
}

View File

@ -0,0 +1,81 @@
{
# args
drvs,
name,
# nixpkgs
lib,
libiconv,
mkShell,
...
}: let
l = lib // builtins;
# illegal env names to be removed and not be added to the devshell
illegalEnvNames =
[
"src"
"name"
"pname"
"version"
"args"
"stdenv"
"builder"
"outputs"
"phases"
# cargo artifact and vendoring derivations
# we don't need these in the devshell
"cargoArtifacts"
"dream2nixVendorDir"
"cargoVendorDir"
]
++ (
l.map
(phase: "${phase}Phase")
["configure" "build" "check" "install" "fixup" "unpack"]
)
++ l.flatten (
l.map
(phase: ["pre${phase}" "post${phase}"])
["Configure" "Build" "Check" "Install" "Fixup" "Unpack"]
);
isIllegalEnv = name: l.elem name illegalEnvNames;
getEnvs = drv:
# filter out attrsets, functions and illegal environment vars
l.filterAttrs
(name: env: (env != null) && (! isIllegalEnv name))
(
l.mapAttrs
(
n: v:
if ! (l.isAttrs v || l.isFunction v)
then v
else null
)
drv.drvAttrs
);
combineEnvs = envs:
l.foldl'
(
all: env: let
mergeInputs = name: (all.${name} or []) ++ (env.${name} or []);
in
all
// env
// {
buildInputs = mergeInputs "buildInputs";
nativeBuildInputs = mergeInputs "nativeBuildInputs";
propagatedBuildInputs = mergeInputs "propagatedBuildInputs";
propagatedNativeBuildInputs = mergeInputs "propagatedNativeBuildInputs";
}
)
{}
envs;
_shellEnv = combineEnvs (l.map getEnvs drvs);
shellEnv =
_shellEnv
// {
inherit name;
passthru.env = _shellEnv;
};
in
(mkShell.override {stdenv = (l.head drvs).stdenv;}) shellEnv

View File

@ -0,0 +1,29 @@
{
config,
lib,
...
}: let
l = lib // builtins;
t = l.types;
in {
options.deps = {
cargo = l.mkOption {
type = t.package;
description = "The Cargo package to use";
};
craneSource = l.mkOption {
type = t.path;
};
crane = {
buildPackage = l.mkOption {
type = t.functionTo t.package;
};
buildDepsOnly = l.mkOption {
type = t.functionTo t.package;
};
};
};
options.rust-crane = {
};
}

View File

@ -0,0 +1,48 @@
def normalizeWorkspaceDep:
if ($workspaceDependencies."\(.key)" | type) == "object"
then [.value, $workspaceDependencies."\(.key)"] | add
else [.value, {"version":$workspaceDependencies."\(.key)"}] | add
end
# remove workspace option from the dependency
| del(.workspace)
;
# normalizes workspace inherited dependencies for one list
def mapWorkspaceDepsFor(name):
if has(name)
then
."\(name)" = (
."\(name)"
| to_entries
| map(
if (.value | type) == "object" and .value.workspace == true
then .value = (. | normalizeWorkspaceDep)
else .
end
)
| from_entries
)
else .
end
;
# shorthand for normalizing all the dependencies list
def mapWorkspaceDeps:
mapWorkspaceDepsFor("dependencies")
| mapWorkspaceDepsFor("dev-dependencies")
| mapWorkspaceDepsFor("build-dependencies")
;
# normalize workspace inherited deps
mapWorkspaceDeps
| if has("target")
then
# normalize workspace inherited deps in target specific deps
.target = (
.target
| to_entries
| map(.value = (.value | mapWorkspaceDeps))
| from_entries
)
else .
end

View File

@ -0,0 +1,228 @@
{
dreamLock,
getSourceSpec,
getSource,
getRoot,
sourceRoot,
subsystemAttrs,
packages,
lib,
toTOML,
writeText,
...
}: let
l = lib // builtins;
isInPackages = name: version: (packages.${name} or null) == version;
# a make overridable for rust derivations specifically
makeOverridable = f: origArgs: let
result = f origArgs;
# Creates a functor with the same arguments as f
copyArgs = g: l.setFunctionArgs g (l.functionArgs f);
# Changes the original arguments with (potentially a function that returns) a set of new attributes
overrideWith = newArgs:
origArgs
// (
if l.isFunction newArgs
then newArgs origArgs
else newArgs
);
# Re-call the function but with different arguments
overrideArgs = copyArgs (newArgs: makeOverridable f (overrideWith newArgs));
# Change the result of the function call by applying g to it
overrideResult = g: makeOverridable (copyArgs (args: g (f args))) origArgs;
in
result.derivation
// {
override = args:
overrideArgs {
args =
origArgs.args
// (
if l.isFunction args
then args origArgs.args
else args
);
};
overrideRustToolchain = f: overrideArgs {toolchain = f origArgs.toolchain;};
overrideAttrs = fdrv: overrideResult (x: {derivation = x.derivation.overrideAttrs fdrv;});
};
in rec {
getMeta = pname: version: let
meta = subsystemAttrs.meta.${pname}.${version};
in
meta
// {
license = l.map (name: l.licenses.${name}) meta.license;
};
# Gets the root source for a package
getRootSource = pname: version: let
root = getRoot pname version;
in
getSource root.pname root.version;
# Generates a script that replaces relative path dependency paths with absolute
# ones, if the path dependency isn't in the source dream2nix provides
replaceRelativePathsWithAbsolute = replacements: let
replace =
l.concatStringsSep
" \\\n"
(
l.mapAttrsToList
(
# TODO: this is not great, because it forces us to include the entire
# sourceRoot here, which could possibly cause more rebuilds than necessary
# when source is changed (although this mostly depends on how the project
# repository is structured). doing this properly is pretty complex, but
# it should still be done later.
from: relPath: ''--replace "\"${from}\"" "\"${sourceRoot}/${relPath}\""''
)
replacements
);
in ''
substituteInPlace ./Cargo.toml \
${replace}
'';
mkBuildWithToolchain = mkBuildFunc: let
buildWithToolchain = args:
makeOverridable
(args: {
derivation =
(mkBuildFunc args.toolchain)
(
args.args
// {
passthru =
(args.args.passthru or {})
// {rustToolchain = args.toolchain;};
}
);
})
args;
in
buildWithToolchain;
# Backup original Cargo.lock if it exists and write our own one
writeCargoLock = ''
mv -f Cargo.lock Cargo.lock.orig || echo "no Cargo.lock"
cat ${cargoLock} > Cargo.lock
'';
# The Cargo.lock for this dreamLock.
cargoLock = let
mkPkgEntry = {
name,
version,
...
} @ args: let
# constructs source string for dependency
makeSource = sourceSpec: let
source =
if sourceSpec.type == "crates-io"
then "registry+https://github.com/rust-lang/crates.io-index"
else if sourceSpec.type == "git"
then let
gitSpec =
l.findFirst
(src: src.url == sourceSpec.url && src.sha == sourceSpec.rev)
(throw "no git source: ${sourceSpec.url}#${sourceSpec.rev}")
(subsystemAttrs.gitSources or {});
refPart =
l.optionalString
(gitSpec ? type)
"?${gitSpec.type}=${gitSpec.value}";
in "git+${sourceSpec.url}${refPart}#${sourceSpec.rev}"
else null;
in
source;
# constructs source string for dependency entry
makeDepSource = sourceSpec:
if sourceSpec.type == "crates-io"
then makeSource sourceSpec
else if sourceSpec.type == "git"
then l.concatStringsSep "#" (l.init (l.splitString "#" (makeSource sourceSpec)))
else null;
# removes source type information from the version
normalizeVersion = version: srcType: l.removeSuffix ("$" + srcType) version;
sourceSpec = getSourceSpec name version;
normalizedVersion = normalizeVersion version sourceSpec.type;
source = let
src = makeSource sourceSpec;
in
if src == null
then throw "source type '${sourceSpec.type}' not supported"
else src;
dependencies =
l.map
(
dep: let
depSourceSpec = getSourceSpec dep.name dep.version;
depSource = makeDepSource depSourceSpec;
normalizedDepVersion = normalizeVersion dep.version depSourceSpec.type;
hasMultipleVersions =
l.length (l.attrValues dreamLock.sources.${dep.name}) > 1;
hasDuplicateVersions = dep.version != normalizedDepVersion;
# only put version if there are different versions of the dep
versionString =
l.optionalString hasMultipleVersions " ${normalizedDepVersion}";
# only put source if there are duplicate versions of the dep
# cargo vendor does not support this anyway and so builds will fail
# until https://github.com/rust-lang/cargo/issues/10310 is resolved.
srcString =
l.optionalString hasDuplicateVersions " (${depSource})";
in "${dep.name}${versionString}${srcString}"
)
args.dependencies;
isMainPackage = isInPackages name version;
in
{
name = sourceSpec.pname or name;
version = sourceSpec.version or normalizedVersion;
}
# put dependencies like how cargo expects them
// (
l.optionalAttrs
(l.length dependencies > 0)
{inherit dependencies;}
)
// (
l.optionalAttrs
(sourceSpec.type != "path" && !isMainPackage)
{inherit source;}
)
// (
l.optionalAttrs
(sourceSpec.type == "crates-io" && !isMainPackage)
{checksum = sourceSpec.hash;}
);
package = l.flatten (
l.mapAttrsToList
(
name: versions:
l.mapAttrsToList
(
version: dependencies:
mkPkgEntry {inherit name version dependencies;}
)
versions
)
dreamLock.dependencies
);
lockTOML = toTOML {
# the lockfile we generate is of version 3
version = 3;
inherit package;
};
in
writeText "Cargo.lock" lockTOML;
}

View File

@ -0,0 +1,145 @@
{
lib,
getSource,
getSourceSpec,
subsystemAttrs,
dreamLock,
moreutils,
writePython3,
python3Packages,
runCommandLocal,
...
} @ args: let
l = lib // builtins;
allDependencies =
l.flatten
(
l.mapAttrsToList
(
name: versions:
l.map (version: {inherit name version;}) (l.attrNames versions)
)
dreamLock.dependencies
);
in rec {
# Generates a shell script that writes git vendor entries to .cargo/config.
# `replaceWith` is the name of the vendored source(s) to use.
writeGitVendorEntries = replaceWith: let
makeEntry = source: ''
[source."${source.url}${l.optionalString (source ? type) "?${source.type}=${source.value}"}"]
replace-with = "${replaceWith}"
git = "${source.url}"
${l.optionalString (source ? type) "${source.type} = \"${source.value}\""}
'';
entries = l.map makeEntry subsystemAttrs.gitSources;
in ''
echo "Writing git vendor entries to $CARGO_HOME/config.toml"
mkdir -p $CARGO_HOME && touch $CARGO_HOME/config.toml
cat >> $CARGO_HOME/config.toml <<EOF
${l.concatStringsSep "\n" entries}
EOF
'';
# Vendors the dependencies passed as Cargo expects them
vendorDependencies = deps: let
makeSource = dep: let
path = getSource dep.name dep.version;
spec = getSourceSpec dep.name dep.version;
normalizeVersion = version: l.removeSuffix ("$" + spec.type) version;
in {
inherit path spec dep;
name = "${dep.name}-${normalizeVersion dep.version}";
};
sources = l.map makeSource deps;
findCrateSource = source: let
cargo = "${args.cargo}/bin/cargo";
jq = "${args.jq}/bin/jq";
sponge = "${moreutils}/bin/sponge";
writeConvertScript = from: to:
writePython3
"${from}-to-${to}.py"
{libraries = [python3Packages.toml];}
''
import toml
import json
import sys
t = ${from}.loads(sys.stdin.read())
sys.stdout.write(${to}.dumps(t))
'';
tomlToJson = writeConvertScript "toml" "json";
jsonToToml = writeConvertScript "json" "toml";
pkg = source.dep;
in ''
# If the target package is in a workspace, or if it's the top-level
# crate, we should find the crate path using `cargo metadata`.
crateCargoTOML=$(${cargo} metadata --format-version 1 --no-deps --manifest-path $tree/Cargo.toml | \
${jq} -r '.packages[] | select(.name == "${pkg.name}") | .manifest_path')
# If the repository is not a workspace the package might be in a subdirectory.
if [[ -z $crateCargoTOML ]]; then
for manifest in $(find $tree -name "Cargo.toml"); do
echo Looking at $manifest
crateCargoTOML=$(${cargo} metadata --format-version 1 --no-deps --manifest-path "$manifest" | ${jq} -r '.packages[] | select(.name == "${pkg.name}") | .manifest_path' || :)
if [[ ! -z $crateCargoTOML ]]; then
break
fi
done
if [[ -z $crateCargoTOML ]]; then
>&2 echo "Cannot find path for crate '${pkg.name}-${pkg.version}' in the tree in: $tree"
exit 1
fi
else
# we need to patch dependencies with `workspace = true` (workspace inheritance)
workspaceDependencies="$(cat "$tree/Cargo.toml" | ${tomlToJson} | ${jq} -cr '.workspace.dependencies')"
if [[ "$workspaceDependencies" != "null" ]]; then
tree="$(pwd)/${pkg.name}-${pkg.version}"
cp -prd --no-preserve=mode,ownership "$(dirname $crateCargoTOML)" "$tree"
crateCargoTOML="$tree/Cargo.toml"
cat "$crateCargoTOML" \
| ${tomlToJson} \
| ${jq} -cr --argjson workspaceDependencies "$workspaceDependencies" \
--from-file ${./patch-workspace-deps.jq} \
| ${jsonToToml} \
| ${sponge} "$crateCargoTOML"
fi
fi
echo Found crate ${pkg.name} at $crateCargoTOML
tree="$(dirname $crateCargoTOML)"
'';
makeScript = source: let
isGit = source.spec.type == "git";
isPath = source.spec.type == "path";
in
l.optionalString (!isPath) ''
tree="${source.path}"
${l.optionalString isGit (findCrateSource source)}
echo Vendoring crate ${source.name}
if [ -d $out/${source.name} ]; then
echo Crate is already vendored
echo Crates with duplicate versions cannot be vendored as Cargo does not support this behaviour
exit 1
else
cp -prd "$tree" $out/${source.name}
chmod u+w $out/${source.name}
${l.optionalString isGit "printf '{\"files\":{},\"package\":null}' > $out/${source.name}/.cargo-checksum.json"}
fi
'';
in
runCommandLocal "vendor" {} ''
mkdir -p $out
${
l.concatMapStringsSep "\n"
makeScript
sources
}
'';
# All dependencies in the Cargo.lock file, vendored
vendoredDependencies = vendorDependencies allDependencies;
copyVendorDir = from: to: ''cp -rs --no-preserve=mode,ownership ${from} ${to}'';
}

View File

@ -7,12 +7,11 @@
in {
imports = [
../../drv-parts/rust-cargo-lock
../../drv-parts/buildRustPackage
../../drv-parts/rust-crane
];
deps = {nixpkgs, ...}: {
inherit (nixpkgs) fetchFromGitHub;
inherit (nixpkgs) stdenv;
};
name = l.mkForce "ripgrep";

View File

@ -8,7 +8,7 @@
in {
imports = [
dream2nix.modules.drv-parts.rust-cargo-lock
dream2nix.modules.drv-parts.buildRustPackage
dream2nix.modules.drv-parts.rust-crane
];
mkDerivation = {
@ -23,5 +23,5 @@ in {
};
name = "app";
version = "1.0.0";
version = "0.1.0";
}