Skip to content

Instantly share code, notes, and snippets.

@tadfisher
Last active October 15, 2024 10:00
Show Gist options
  • Save tadfisher/17000caf8653019a9a98fd9b9b921d93 to your computer and use it in GitHub Desktop.
Save tadfisher/17000caf8653019a9a98fd9b9b921d93 to your computer and use it in GitHub Desktop.
Simplified Nix integration with Gradle
{ lib
, stdenv
, jdk
, gradle
, mavenRepo
}:
stdenv.mkDerivation {
pname = "built-with-gradle";
version = "0.0";
nativeBuildInputs = [ gradle ];
JDK_HOME = "${jdk.home}";
buildPhase = ''
runHook preBuild
gradle build \
--offline --no-daemon --no-build-cache --info --full-stacktrace \
--warning-mode=all --parallel --console=plain \
-DnixMavenRepo=file://${mavenRepo}
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out
cp -r build/dist/* $out
runHook postInstall
'';
dontStrip = true;
}
{ pkgs ? import <nixpkgs> { } }:
with pkgs;
lib.makeScope newScope (self: with self; {
gradle = callPackage ./gradle.nix { };
updateLocks = callPackage ./update-locks.nix {
inherit (haskellPackages) xml-to-json;
};
buildMavenRepo = callPackage ./maven-repo.nix { };
mavenRepo = buildMavenRepo {
name = "nix-maven-repo";
repos = [
"https://plugins.gradle.org/m2"
"https://repo1.maven.org/maven2"
];
deps = builtins.fromJSON (builtins.readFile ./deps.json);
};
builtWithGradle = callPackage ./build.nix { };
})
{ gradleGen, fetchurl }:
gradleGen rec {
name = "gradle-7.0-milestone-2";
nativeVersion = "0.22-milestone-10";
src = fetchurl (
url = "https://services.gradle.org/distributions/${name}-bin.zip";
sha256 = "10a2zhr7yhj7a9pi2rn1jaqdf1nxnxxpljvfnnz0v3il4p6v2wd9";
};
}
{ lib
, stdenv
, buildEnv
, fetchurl
, writeTextDir
}:
{ name ? "maven-deps"
, repos ? [ ]
, deps ? [ ]
, extraPaths ? [ ]
}:
with lib;
let
mavenize = sep: replaceStrings ["."] [sep];
fetch =
{ group
, name
, version
, file
, sha256
}:
fetchurl {
name = file;
urls = map (repo: "${repo}/${mavenize "/" group}/${name}/${version}/${file}") repos;
inherit sha256;
meta.platforms = platforms.all;
};
fetchDependency =
{ group
, name
, version
, artifacts
}:
let
fetchArtifact = file: sha256:
fetch { inherit group name version file sha256; };
# Each artifact uses the filename in the Gradle cache, which doesn't
# correspond to the filename in the Maven repo. The mapping of name to URL
# is provided by Gradle module metadata, so we fetch that first. See
# https://github.com/gradle/gradle/blob/master/subprojects/docs/src/docs/design/gradle-module-metadata-latest-specification.md
# for the file format.
isModule = hasSuffix ".module";
moduleArtifacts = filterAttrs (file: _: isModule file) artifacts;
otherArtifacts = filterAttrs (file: _: !isModule file) artifacts;
modules = mapAttrsToList fetchArtifact moduleArtifacts;
replacements = listToAttrs (flatten (map (module:
let
json = builtins.fromJSON (builtins.readFile module);
variants = json.variants or [ ];
files = flatten (map (v: v.files or [ ]) variants);
in
map ({ name, url, ... }: nameValuePair name url) files
) modules));
replaced = mapAttrs' (file: sha256:
nameValuePair (replacements.${file} or file) sha256
) otherArtifacts;
in
if moduleArtifacts == { }
then mapAttrsToList fetchArtifact artifacts
else modules ++ (mapAttrsToList fetchArtifact replaced);
mkDep =
{ group
, name
, version
, artifacts
}@dep:
stdenv.mkDerivation {
pname = "${mavenize "-" group}-${name}";
inherit version;
srcs = fetchDependency dep;
sourceRoot = ".";
phases = "installPhase";
enableParallelBuilding = true;
preferLocalBuild = true;
installPhase = ''
dest=$out/${mavenize "/" group}/${name}/${version}
mkdir -p $dest
for src in $srcs; do
cp $src $dest/$(stripHash $src)
done
'';
};
mkMetadata = deps:
let
modules = groupBy'
(meta: { group, name, version, ... }:
let
isNewer = versionOlder meta.latest version;
isNewerRelease = versionOlder meta.release version;
in {
groupId = group;
artifactId = name;
latest = if isNewer then version else meta.latest;
release = if isNewerRelease then version else meta.release;
versions = meta.versions ++ [ version ];
}
)
{
latest = "";
release = "";
versions = [ ];
}
({ group, name, ... }: "${mavenize "/" group}/${name}/maven-metadata.xml")
deps;
in
attrValues (mapAttrs (path: { groupId, artifactId, latest, release, versions }:
let
versions' = sort versionOlder (unique versions);
in
writeTextDir path ''
<?xml version="1.0" encoding="UTF-8"?>
<metadata modelVersion="1.1">
<groupId>${groupId}</groupId>
<artifactId>${artifactId}</artifactId>
<versioning>
${optionalString (latest != "") "<latest>${latest}</latest>"}
${optionalString (release != "") "<release>${release}</release>"}
<versions>
${concatMapStringsSep "\n " (v: "<version>${v}</version>") versions'}
</versions>
</versioning>
</metadata>
''
) modules);
mkGradleRedirectionPoms = deps:
let
depsMissingPoms = filter ({ artifacts, ... }@dep:
any (f: hasSuffix ".module" f) (attrNames artifacts) &&
!(any (f: hasSuffix ".pom" f) (attrNames artifacts))
) deps;
in
map ({ group, name, version, ... }:
writeTextDir "${mavenize "/" group}/${name}/${version}/${name}-${version}.pom" ''
<project xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
<modelVersion>4.0.0</modelVersion>
<groupId>${group}</groupId>
<artifactId>${name}</artifactId>
<version>${version}</version>
</project>
''
) depsMissingPoms;
in
buildEnv {
inherit name;
paths = map mkDep deps ++ mkMetadata deps ++ mkGradleRedirectionPoms deps ++ extraPaths;
}
val repoProperty = "nixMavenRepo"
val systemRepo: Provider<String> =
providers.systemProperty(repoProperty).forUseAtConfigurationTime()
val gradleRepo: Provider<String> =
providers.gradleProperty(repoProperty).forUseAtConfigurationTime()
val repo: Provider<List<String>> =
systemRepos.orElse(gradleRepos)
pluginManagement.repositories {
if (repo.isPresent) {
clear()
maven(repo.get())
}
}
dependencyResolutionManagement {
if (repo.isPresent) {
clear()
maven(repo.get())
}
}
{ lib
, writeShellScriptBin
, gradle
, jq
, xml-to-json
}:
writeShellScriptBin "update-locks" ''
set -eu -o pipefail
${gradle}/bin/gradle lock --write-locks
${gradle}/bin/gradle --write-verification-metadata sha256 dependencies
${xml-to-json}/bin/xml-to-json -sam -t components gradle/verification-metadata.xml \
| ${jq}/bin/jq '[
.[] | .component |
{ group, name, version,
artifacts: [([.artifact] | flatten | .[] | {(.name): .sha256.value})] | add
}
]' > deps.json
rm gradle/verification-metadata.xml
''
@tmcl
Copy link

tmcl commented Apr 22, 2021

I've used this to create a minimal example: https://github.com/tmcl/minimal-android-nix-example

@yuri-potatoq
Copy link

good solution, thanks. Is there a lib where supports all the maven fetch depedencies nix functions?
To avoid re-write them every project?

@owickstrom
Copy link

Here's a follow-up on this chain of work, supporting Gradle 8 and packaged as a flake: https://github.com/owickstrom/minimal-kotlin-nix-example

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