port buildRustPackage to drv-parts

This commit is contained in:
DavHau 2023-06-01 14:53:29 +02:00
parent 8843ecf03d
commit 9e2129d053
7 changed files with 818 additions and 5 deletions

View File

@ -1,6 +1,7 @@
{
pkgs,
utils,
runCommandLocal,
fetchurl,
hashFile,
...
}: {
inputs = ["pname" "version"];
@ -17,18 +18,18 @@
url = "https://crates.io/api/v1/crates/${pname}/${version}/download";
in {
calcHash = algo:
utils.hashFile algo (b.fetchurl {
hashFile algo (b.fetchurl {
inherit url;
});
fetched = hash: let
fetched = pkgs.fetchurl {
fetched = fetchurl {
inherit url;
sha256 = hash;
name = "download-${pname}-${version}";
};
in
pkgs.runCommandLocal "unpack-${pname}-${version}" {}
runCommandLocal "unpack-${pname}-${version}" {}
''
mkdir -p $out
tar --strip-components 1 -xzf ${fetched} -C $out

145
lib/internal/toTOML.nix Normal file
View File

@ -0,0 +1,145 @@
{lib, ...}: let
inherit
(lib)
length
elemAt
concatMap
concatLists
concatStringsSep
concatMapStringsSep
mapAttrsToList
foldl
isDerivation
;
inherit
(builtins)
abort
match
typeOf
;
quoteKey = k:
if match "[a-zA-Z]+" k == []
then k
else quoteString k;
quoteString = builtins.toJSON;
outputValInner = v: let
ty = tomlTy v;
in
if ty == "set"
then let
vals =
mapAttrsToList
(k': v': "${quoteKey k'} = ${outputValInner v'}")
v;
valsStr = concatStringsSep ", " vals;
in "{ ${valsStr} }"
else outputVal v;
outputVal = v: let
ty = tomlTy v;
in
if (ty == "bool" || ty == "int")
then builtins.toJSON v
else if ty == "string"
then quoteString v
else if ty == "list" || ty == "list_of_attrs"
then let
vals = map quoteString v;
valsStr = concatStringsSep ", " vals;
in "[ ${valsStr} ]"
else if ty == "set"
then abort "unsupported set for not-inner value"
else abort "Not implemented: type ${ty}";
outputKeyValInner = k: v: let
ty = tomlTy v;
in
if ty == "set"
then let
vals =
mapAttrsToList
(k': v': "${quoteKey k'} = ${outputValInner v'}")
v;
valsStr = concatStringsSep ", " vals;
in ["${quoteKey k} = { ${valsStr} }"]
else outputKeyVal k v;
# Returns a list of strings; one string per line
outputKeyVal = k: v: let
ty = tomlTy v;
in
if ty == "bool" || ty == "int"
then ["${quoteKey k} = ${outputValInner v}"]
else if ty == "string"
then ["${quoteKey k} = ${quoteString v}"]
else if ty == "list_of_attrs"
then
concatMap (
inner:
["[[${k}]]"] ++ (concatLists (mapAttrsToList outputKeyValInner inner))
)
v
else if ty == "list"
then let
vals = map quoteString v;
valsStr = concatStringsSep ", " vals;
in ["${quoteKey k} = [ ${valsStr} ]"]
else if ty == "set"
then ["[${k}]"] ++ (concatLists (mapAttrsToList outputKeyValInner v))
else abort "Not implemented: type ${ty} for key ${k}";
tomlTy = x:
if typeOf x == "string"
then "string"
else if typeOf x == "bool"
then "bool"
else if typeOf x == "int"
then "int"
else if typeOf x == "float"
then "float"
else if typeOf x == "set"
then
if isDerivation x
then "string"
else "set"
else if typeOf x == "list"
then
if length x == 0
then "list"
else let
ty = typeOf (elemAt x 0);
in
#assert (all (v: typeOf v == ty) x);
if ty == "set"
then "list_of_attrs"
else "list"
else abort "Not implemented: toml type for ${typeOf x}";
toTOML = attrs:
assert (typeOf attrs == "set"); let
byTy =
foldl
(
acc: x: let
ty = tomlTy x.v;
in
acc // {"${ty}" = (acc.${ty} or []) ++ [x];}
)
{} (mapAttrsToList (k: v: {inherit k v;}) attrs);
in
concatMapStringsSep "\n"
(kv: concatStringsSep "\n" (outputKeyVal kv.k kv.v))
(
(byTy.string or [])
++ (byTy.int or [])
++ (byTy.float or [])
++ (byTy.list or [])
++ (byTy.list_of_attrs or [])
++ (byTy.set or [])
);
in
toTOML

View File

@ -0,0 +1,213 @@
{
config,
lib,
...
} @ topArgs: let
l = lib // builtins;
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;
};
http = import ../../../lib/internal/fetchers/http {
inherit hashFile lib;
inherit (config.deps.stdenv) mkDerivation;
inherit (config.deps) fetchurl;
};
crates-io = import ../../../lib/internal/fetchers/crates-io {
inherit hashFile;
inherit (config.deps) fetchurl runCommandLocal;
};
};
dreamLockLoaded =
readDreamLock {inherit (config.rust-cargo-lock) dreamLock;};
dreamLockInterface = dreamLockLoaded.interface;
fetchedSources' = fetchDreamLockSources {
inherit (dreamLockInterface) defaultPackageName defaultPackageVersion;
inherit (dreamLockLoaded.lock) sources;
inherit fetchers;
};
fetchedSources =
fetchedSources'
// {
${defaultPackageName}.${defaultPackageVersion} = config.mkDerivation.src;
};
# name: version: -> store-path
getSource = getDreamLockSource fetchedSources;
inherit
(dreamLockInterface)
getDependencies # name: version: -> [ {name=; version=; } ]
# Attributes
subsystemAttrs # attrset
packageVersions
defaultPackageName
defaultPackageVersion
;
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
;
};
buildWithToolchain =
utils.mkBuildWithToolchain
(toolchain: (config.deps.makeRustPlatform toolchain).buildRustPackage);
defaultToolchain = {
inherit (config.deps) cargo rustc;
};
buildPackage = pname: version: let
src = utils.getRootSource pname version;
replacePaths =
utils.replaceRelativePathsWithAbsolute
subsystemAttrs.relPathReplacements.${pname}.${version};
writeGitVendorEntries = vendoring.writeGitVendorEntries "vendored-sources";
cargoBuildFlags = "--package ${pname}";
buildArgs = {
inherit pname version src;
meta = utils.getMeta pname version;
cargoBuildFlags = cargoBuildFlags;
cargoTestFlags = cargoBuildFlags;
cargoVendorDir = "../nix-vendor";
dream2nixVendorDir = vendoring.vendoredDependencies;
postUnpack = ''
${vendoring.copyVendorDir "$dream2nixVendorDir" "./nix-vendor"}
export CARGO_HOME=$(pwd)/.cargo_home
'';
preConfigure = ''
mkdir -p $CARGO_HOME
if [ -f ../.cargo/config ]; then
mv ../.cargo/config $CARGO_HOME/config.toml
fi
${writeGitVendorEntries}
${replacePaths}
${utils.writeCargoLock}
'';
};
in
buildWithToolchain {
toolchain = defaultToolchain;
args = buildArgs;
};
allPackages =
l.mapAttrs
(name: version: {"${version}" = buildPackage name version;})
dreamLockInterface.packages;
mkShellForDrvs = drvs:
import ./devshell.nix {
inherit drvs lib;
inherit (config.deps) mkShell;
name = "devshell";
};
pkgShells =
l.mapAttrs
(
name: version: let
pkg = allPackages.${name}.${version};
in
mkShellForDrvs [pkg]
)
dreamLockInterface.packages;
allPackagesList =
l.mapAttrsToList
(name: version: allPackages.${name}.${version})
dreamLockInterface.packages;
packages = allPackages;
devShells =
pkgShells
// {
default = mkShellForDrvs allPackagesList;
};
in {
imports = [
# dream2nix.modules.drv-parts.mkDerivation
];
public = lib.mkForce packages.${dreamLockInterface.defaultPackageName}.${dreamLockInterface.defaultPackageVersion};
deps = {nixpkgs, ...}: {
inherit
(nixpkgs)
cargo
fetchurl
jq
makeRustPlatform
moreutils
python3Packages
runCommandLocal
rustc
;
inherit
(nixpkgs.writers)
writePython3
;
};
}

View File

@ -0,0 +1,79 @@
{
# args
drvs,
name,
# nixpkgs
lib,
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,227 @@
{
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,147 @@
{
lib,
getSource,
getSourceSpec,
subsystemAttrs,
dreamLock,
# pkgs
cargo,
jq,
moreutils,
python3Packages,
runCommandLocal,
writePython3,
} @ 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,6 +7,7 @@
in {
imports = [
../../drv-parts/rust-cargo-lock
../../drv-parts/buildRustPackage
];
deps = {nixpkgs, ...}: {