push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

View File

@@ -0,0 +1,245 @@
# Gradle Setup Hook
## Introduction
Gradle build scripts are written in a DSL, computing the list of Gradle
dependencies is a Turing-complete task, not just in theory but also in
practice. Fetching all of the dependencies often requires building some
native code, running some commands to check the host platform, or just
fetching some files using either JVM code or commands like `curl` or
`wget`.
This practice is widespread and isn't considered a bad practice in the
Java world, so all we can do is run Gradle to check what dependencies
end up being fetched, and allow derivation authors to apply workarounds
so they can run the code necessary for fetching the dependencies our
script doesn't fetch.
"Run Gradle to check what dependencies end up being fetched" isn't a
straightforward task. For example, Gradle usually uses Maven
repositories, which have features such as "snapshots", a way to always
use the latest version of a dependency as opposed to a fixed version.
Obviously, this is horrible for reproducibility. Additionally, Gradle
doesn't offer a way to export the list of dependency URLs and hashes (it
does in a way, but it's far from being complete, and as such is useless
for Nixpkgs). Even if it did, it would be annoying to use considering
fetching non-Gradle dependencies in Gradle scripts is commonplace.
That's why the setup hook uses mitm-cache, a program designed for
intercepting all HTTP requests, recording all the files that were
accessed, creating a Nix derivation with all of them, and then allowing
the Gradle derivation to access these files.
## Maven Repositories
(Reference: [Repository
Layout](https://cwiki.apache.org/confluence/display/MAVENOLD/Repository+Layout+-+Final))
Most of Gradle dependencies are fetched from Maven repositories. For
each dependency, Gradle finds the first repo where it can successfully
fetch that dependency, and uses that repo for it. Different repos might
actually return different files for the same artifact because of e.g.
pom normalization. Different repos may be used for the same artifact
even across a single package (for example, if two build scripts define
repositories in a different order).
The artifact metadata is specified in a .pom file, and the artifacts
themselves are typically .jar files. The URL format is as follows:
`<repo>/<group-id>/<artifact-id>/<base-version>/<artifact-id>-<version>[-<classifier>].<ext>`
For example:
- `https://repo.maven.apache.org/maven2/org/slf4j/slf4j-api/2.0.9/slf4j-api-2.0.9.pom`
- `https://oss.sonatype.org/content/groups/public/com/tobiasdiez/easybind/2.2.1-SNAPSHOT/easybind-2.2.1-20230117.075740-16.pom`
Where:
- `<repo>` is the repo base (`https://repo.maven.apache.org/maven2`)
- `<group-id>` is the group ID with dots replaced with slashes
(`org.slf4j` -> `org/slf4j`)
- `<artifact-id>` is the artifact ID (`slf4j-api`)
- `<base-version>` is the artifact version (`2.0.9` for normal
artifacts, `2.2.1-SNAPSHOT` for snapshots)
- `<version>` is the artifact version - can be either `<base-version>`
or `<version-base>-<timestamp>-<build-num>` (`2.0.9` for normal
artifacts, and either `2.2.1-SNAPSHOT` or `2.2.1-20230117.075740-16`
for snapshots)
- `<version-base>` - `<base-version>` without the `-SNAPSHOT` suffix
- `<timestamp>` - artifact build timestamp in the `YYYYMMDD.HHMMSS`
format (UTC)
- `<build-num>` - a counter that's incremented by 1 for each new
snapshot build
- `<classifier>` is an optional classifier for allowing a single .pom to
refer to multiple .jar files. .pom files don't have classifiers, as
they describe metadata.
- `<ext>` is the extension. .pom
Note that the artifact ID can contain `-`, so you can't extract the
artifact ID and version from just the file name.
Additionally, the files in the repository may have associated signature
files, formed by appending `.asc` to the filename, and hashsum files,
formed by appending `.md5` or `.sha1` to the filename. The signatures
are harmless, but the `.md5`/`.sha1` files are rejected.
The reasoning is as follows - consider two files `a.jar` and `b.jar`,
that have the same hash. Gradle will fetch `a.jar.sha1`, find out that
it hasn't yet downloaded a file with this hash, and then fetch `a.jar`,
and finally download `b.jar.sha1`, locate it in its cache, and then
*not* download `b.jar`. This means `b.jar` won't be stored in the MITM
cache. Then, consider that on a later invocation, the fetching order
changed, whether it was because of running on a different system,
changed behavior after a Gradle update, or any other source of
nondeterminism - `b.jar` is fetched before `a.jar`. Gradle will first
fetch `b.jar.sha1`, not find it in its cache, attempt to fetch `b.jar`,
and fail, as the cache doesn't have that file.
For the same reason, the proxy strips all checksum/etag headers. An
alternative would be to make the proxy remember previous checksums and
etags, but that would complicate the implementation - however, such a
feature can be implemented if necessary. Note that checksum/etag header
stripping is hardcoded, but `.md5/.sha1` file rejection is configured
via CLI arguments.
**Caveat**: Gradle .module files also contain file hashes, in md5, sha1,
sha256, sha512 formats. It has posed no problem as of yet, but it might in
the future. If it does pose problems, the deps derivation code can be
extended to find all checksums in .module files and copy existing files
there if their hash matches.
## Snapshots
Snapshots are a way to publish the very latest, unstable version of a
dependency that constantly changes. Any project that depends on a
snapshot will depend on this rolling version, rather than a fixed
version. It's easy to understand why this is a bad idea for reproducible
builds. Still, they can be dealt with by the logic in `gradle.fetchDeps`
and `gradle.updateDeps`.
First, as you can see above, while normal artifacts have the same
`base-version` and `version`, for snapshots it usually (but not
necessarily) differs.
Second, for figuring out where to download the snapshot, Gradle consults
`maven-metadata.xml`. With that in mind...
## Maven Metadata
(Reference: [Maven
Metadata](https://maven.apache.org/repositories/metadata.html),
[Metadata](https://maven.apache.org/ref/3.9.8/maven-repository-metadata/repository-metadata.html)
Maven metadata files are called `maven-metadata.xml`.
There are three levels of metadata: "G level", "A level", "V level",
representing group, artifact, or version metadata.
G level metadata is currently unsupported. It's only used for Maven
plugins, which Gradle presumably doesn't use.
A level metadata is used for getting the version list for an artifact.
It's an xml with the following items:
- `<groupId>` - group ID
- `<artifactId>` - artifact ID
- `<versioning>`
- `<latest>` - the very latest base version (e.g. `2.2.1-SNAPSHOT`)
- `<release>` - the latest non-snapshot version
- `<versions>` - the version list, each in a `<version>` tag
- `<lastUpdated>` - the metadata update timestamp (UTC,
`YYYYMMDDHHMMSS`)
V level metadata is used for listing the snapshot versions. It has the
following items:
- `<groupId>` - group ID
- `<artifactId>` - artifact ID
- `<versioning>`
- `<lastUpdated>` - the metadata update timestamp (UTC,
`YYYYMMDDHHMMSS`)
- `<snapshot>` - info about the latest snapshot version
- `<timestamp>` - build timestamp (UTC, `YYYYMMDD.HHMMSS`)
- `<buildNumber>` - build number
- `<snapshotVersions>` - the list of all available snapshot file info,
each info is enclosed in a `<snapshotVersion>`
- `<classifier>` - classifier (optional)
- `<extension>` - file extension
- `<value>` - snapshot version (as opposed to base version)
- `<updated>` - snapshot build timestamp (UTC, `YYYYMMDDHHMMSS`)
## Lockfile Format
The mitm-cache lockfile format is described in the [mitm-cache
README](https://github.com/chayleaf/mitm-cache#readme).
The Nixpkgs Gradle lockfile format is more complicated:
```json
{
"!comment": "This is a Nixpkgs Gradle dependency lockfile. For more details, refer to the Gradle section in the Nixpkgs manual.",
"!version": 1,
"https://oss.sonatype.org/content/repositories/snapshots/com/badlogicgames/gdx-controllers": {
"gdx-controllers#gdx-controllers-core/2.2.4-20231021.200112-6/SNAPSHOT": {
"jar": "sha256-Gdz2J1IvDJFktUD2XeGNS0SIrOyym19X/+dCbbbe3/U=",
"pom": "sha256-90QW/Mtz1jbDUhKjdJ88ekhulZR2a7eCaEJoswmeny4="
},
"gdx-controllers-core/2.2.4-SNAPSHOT/maven-metadata": {
"xml": {
"groupId": "com.badlogicgames.gdx-controllers"
}
}
},
"https://repo.maven.apache.org/maven2": {
"com/badlogicgames/gdx#gdx-backend-lwjgl3/1.12.1": {
"jar": "sha256-B3OwjHfBoHcJPFlyy4u2WJuRe4ZF/+tKh7gKsDg41o0=",
"module": "sha256-9O7d2ip5+E6OiwN47WWxC8XqSX/mT+b0iDioCRTTyqc=",
"pom": "sha256-IRSihaCUPC2d0QzB0MVDoOWM1DXjcisTYtnaaxR9SRo="
}
}
}
```
`!comment` is a human-readable description explaining what the file is,
`!version` is the lockfile version (note that while it shares the name
with mitm-cache's `!version`, they don't actually have to be in sync and
can be bumped separately).
The other keys are parts of a URL. Each URL is split into three parts.
They are joined like this: `<part1>/<part2>.<part3>`.
Some URLs may have a `#` in them. In that case, the part after `#` is
parsed as `#<artifact-id>/<version>[/SNAPSHOT][/<classifier>].<ext>` and
expanded into
`<artifact-id>/<base-version>/<artifact-id>-<version>[-<classifier>].<ext>`.
Each URL has a value associated with it. The value may be:
- an SRI hash (string)
- for `maven-metadata.xml` - an attrset containing the parts of the
metadata that can't be generated in Nix code (e.g. `groupId`, which is
challenging to parse from a URL because it's not always possible to
discern where the repo base ends and the group ID begins).
`compress-deps-json.py` converts the JSON from mitm-cache format into
Nixpkgs Gradle lockfile format. `fetch.nix` does the opposite.
## Security Considerations
Lockfiles won't be human-reviewed. They must be tampering-resistant.
That's why it's imperative that nobody can inject their own contents
into the lockfiles.
This is achieved in a very simple way - the `deps.json` only contains
the following:
- `maven-metadata.xml` URLs and small pieces of the contained metadata
(most of it will be generated in Nix, i.e. the area of injection is
minimal, and the parts that aren't generated in Nix are validated).
- artifact/other file URLs and associated hashes (Nix will complain if
the hash doesn't match, and Gradle won't even access the URL if it
doesn't match)
Please be mindful of the above when working on Gradle support for
Nixpkgs.

View File

@@ -0,0 +1,163 @@
import json
import sys
from typing import Dict, Set
# this compresses MITM URL lists with Gradle-specific optimizations
# specifically, it splits each url into up to 3 parts - they will be
# concatenated like part1/part2.part3 or part1.part2
# part3 is simply always the file extension, but part1 and part2 is
# optimized using special heuristics
# additionally, if part2 ends with /a/b/{a}-{b}, the all occurences of
# /{a}/{b}/ are replaced with #
# finally, anything that ends with = is considered SHA256, anything that
# starts with http is considered a redirect URL, anything else is
# considered text
with open(sys.argv[1], "rt") as f:
data: dict = json.load(f)
new_data: Dict[str, Dict[str, Dict[str, dict]]] = {}
for url, info in data.items():
if url == "!version":
continue
ext, base = map(lambda x: x[::-1], url[::-1].split(".", 1))
if base.endswith(".tar"):
base = base[:-4]
ext = "tar." + ext
# special logic for Maven repos
if ext in ["jar", "pom", "module"]:
comps = base.split("/")
if "-" in comps[-1]:
# convert base/name/ver/name-ver into base#name/ver
filename = comps[-1]
name = comps[-3]
basever = comps[-2]
ver = basever
is_snapshot = ver.endswith("-SNAPSHOT")
if is_snapshot:
ver = ver.removesuffix("-SNAPSHOT")
if filename.startswith(f"{name}-{ver}"):
if is_snapshot:
if filename.startswith(f"{name}-{ver}-SNAPSHOT"):
ver += "-SNAPSHOT"
else:
ver += "-".join(
filename.removeprefix(f"{name}-{ver}").split("-")[:3]
)
comp_end = comps[-1].removeprefix(f"{name}-{ver}")
else:
ver, name, comp_end = None, None, None
if name and ver and (not comp_end or comp_end.startswith("-")):
base = "/".join(comps[:-1]) + "/"
base = base.replace(f"/{name}/{basever}/", "#")
base += f"{name}/{ver}"
if is_snapshot:
base += "/SNAPSHOT"
if comp_end:
base += "/" + comp_end[1:]
scheme, rest = base.split("/", 1)
if scheme not in new_data.keys():
new_data[scheme] = {}
if rest not in new_data[scheme].keys():
new_data[scheme][rest] = {}
if "hash" in info.keys():
new_data[scheme][rest][ext] = info["hash"]
elif "text" in info.keys() and ext == "xml":
# nix code in fetch-deps.nix will autogenerate metadata xml files groupId
# is part of the URL, but it can be tricky to parse as we don't know the
# exact repo base, so take it from the xml and pass it to nix
xml = "".join(info["text"].split())
new_data[scheme][rest][ext] = {
"groupId": xml.split("<groupId>")[1].split("</groupId>")[0],
}
if "<release>" in xml:
new_data[scheme][rest][ext]["release"] = xml.split("<release>")[1].split(
"</release>"
)[0]
if "<latest>" in xml:
latest = xml.split("<latest>")[1].split("</latest>")[0]
if latest != new_data[scheme][rest][ext].get("release"):
new_data[scheme][rest][ext]["latest"] = latest
if "<lastUpdated>" in xml:
new_data[scheme][rest][ext]["lastUpdated"] = xml.split("<lastUpdated>")[
1
].split("</lastUpdated>")[0]
else:
raise Exception("Unsupported key: " + repr(info))
# At this point, we have a map by part1 (initially the scheme), part2 (initially a
# slash-separated string without the scheme and with potential # substitution as
# seen above), extension.
# Now, push some segments from "part2" into "part1" like this:
# https # part1
# domain1/b # part2
# domain1/c
# domain2/a
# domain2/c
# ->
# https/domain1 # part1
# b # part2
# c
# https/domain2 # part1
# a # part2
# c
# This helps reduce the lockfile size because a Gradle project will usually use lots
# of files from a single Maven repo
data = new_data
changed = True
while changed:
changed = False
new_data = {}
for part1, info1 in data.items():
starts: Set[str] = set()
# by how many bytes the file size will be increased (roughly)
lose = 0
# by how many bytes the file size will be reduced (roughly)
win = 0
# how many different initial part2 segments there are
count = 0
for part2, info2 in info1.items():
if "/" not in part2:
# can't push a segment from part2 into part1
count = 0
break
st = part2.split("/", 1)[0]
if st not in starts:
lose += len(st) + 1
count += 1
starts.add(st)
win += len(st) + 1
if count == 0:
new_data[part1] = info1
continue
# only allow pushing part2 segments into path1 if *either*:
# - the domain isn't yet part of part1
# - the initial part2 segment is always the same
if count != 1 and "." in part1:
new_data[part1] = info1
continue
# some heuristics that may or may not work well (originally this was
# used when the above if wasn't here, but perhaps it's useless now)
lose += (count - 1) * max(0, len(part1) - 4)
if win > lose or ("." not in part1 and win >= lose):
changed = True
for part2, info2 in info1.items():
st, part3 = part2.split("/", 1)
new_part1 = part1 + "/" + st
if new_part1 not in new_data.keys():
new_data[new_part1] = {}
new_data[new_part1][part3] = info2
else:
new_data[part1] = info1
data = new_data
new_data["!comment"] = "This is a nixpkgs Gradle dependency lockfile. For more details, refer to the Gradle section in the nixpkgs manual." # type: ignore
new_data["!version"] = 1 # type: ignore
with open(sys.argv[2], "wt") as f:
json.dump(new_data, f, sort_keys=True, indent=1)
f.write("\n")

View File

@@ -0,0 +1,349 @@
{
callPackage,
jdk11,
jdk17,
jdk21,
}:
let
wrapGradle =
{
lib,
callPackage,
mitm-cache,
replaceVars,
symlinkJoin,
concatTextFile,
makeSetupHook,
nix-update-script,
# This is the "current" version of gradle in nixpkgs.
# Used to define the update script.
gradle-unwrapped,
runCommand,
}:
this-gradle-unwrapped:
lib.makeOverridable (
args:
let
gradle = this-gradle-unwrapped.override args;
in
symlinkJoin {
pname = "gradle";
inherit (gradle) version;
paths = [
(makeSetupHook { name = "gradle-setup-hook"; } (concatTextFile {
name = "setup-hook.sh";
files = [
(mitm-cache.setupHook)
(replaceVars ./setup-hook.sh {
# jdk used for keytool
inherit (gradle) jdk;
init_script = "${./init-build.gradle}";
})
];
}))
gradle
mitm-cache
];
passthru = {
fetchDeps = callPackage ./fetch-deps.nix { inherit mitm-cache; };
inherit (gradle) jdk;
unwrapped = gradle;
tests = {
toolchains =
let
javaVersion = lib.getVersion jdk11;
javaMajorVersion = lib.versions.major javaVersion;
in
runCommand "detects-toolchains-from-nix-env"
{
# Use JDKs that are not the default for any of the gradle versions
nativeBuildInputs = [
(gradle.override {
javaToolchains = [
jdk11
];
})
];
src = ./tests/toolchains;
}
''
cp -a $src/* .
substituteInPlace ./build.gradle --replace-fail '@JAVA_VERSION@' '${javaMajorVersion}'
env GRADLE_USER_HOME=$TMPDIR/gradle org.gradle.native.dir=$TMPDIR/native \
gradle run --no-daemon --quiet --console plain > $out
actual="$(<$out)"
if [[ "${javaVersion}" != "$actual"* ]]; then
echo "Error: Expected '${javaVersion}', to start with '$actual'" >&2
exit 1
fi
'';
}
// gradle.tests;
}
// lib.optionalAttrs (this-gradle-unwrapped == gradle-unwrapped) {
updateScript = nix-update-script {
extraArgs = [
"--url=https://github.com/gradle/gradle"
# Gradles .0 releases are tagged as `vX.Y.0`, but the actual
# release version omits the `.0`, so well wanto to only capture
# the version up but not including the the trailing `.0`.
"--version-regex=^v(\\d+\\.\\d+(?:\\.[1-9]\\d?)?)(\\.0)?$"
];
};
};
meta = gradle.meta // {
# prefer normal gradle/mitm-cache over this wrapper, this wrapper only provides the setup hook
# and passthru
priority = (gradle.meta.priority or lib.meta.defaultPriority) + 1;
};
}
) { };
gen =
{
version,
hash,
# The default JDK/JRE that will be used for derived Gradle packages.
# A current LTS version of a JDK is a good choice.
defaultJava,
# The platforms supported by this Gradle package.
# Gradle Native-Platform ships some binaries that
# are compatible only with specific platforms.
# As of 2022-04 this affects platform compatibility
# of multiple Gradle releases, so this is used as default.
# See https://github.com/gradle/native-platform#supported-platforms
platforms ? [
"aarch64-darwin"
"aarch64-linux"
"i686-windows"
"x86_64-cygwin"
"x86_64-darwin"
"x86_64-linux"
"x86_64-windows"
],
# Extra attributes to be merged into the resulting derivation's
# meta attribute.
meta ? { },
}@genArgs:
{
lib,
stdenv,
fetchurl,
callPackage,
makeWrapper,
unzip,
ncurses5,
ncurses6,
udev,
testers,
runCommand,
writeText,
autoPatchelfHook,
buildPackages,
# The JDK/JRE used for running Gradle.
java ? defaultJava,
# Additional JDK/JREs to be registered as toolchains.
# See https://docs.gradle.org/current/userguide/toolchains.html
javaToolchains ? [ ],
}:
stdenv.mkDerivation (finalAttrs: {
pname = "gradle";
inherit version;
src = fetchurl {
inherit hash;
url = "https://services.gradle.org/distributions/gradle-${version}-bin.zip";
};
dontBuild = true;
nativeBuildInputs = [
makeWrapper
unzip
]
++ lib.optionals stdenv.hostPlatform.isLinux [
autoPatchelfHook
];
buildInputs = [
stdenv.cc.cc
ncurses5
ncurses6
];
# We only need to patchelf some libs embedded in JARs.
dontAutoPatchelf = true;
installPhase =
with builtins;
let
# set toolchains via installations.path property in gradle.properties.
# See https://docs.gradle.org/current/userguide/toolchains.html#sec:custom_loc
toolchainPaths = "org.gradle.java.installations.paths=${concatStringsSep "," javaToolchains}";
jnaLibraryPath = if stdenv.hostPlatform.isLinux then lib.makeLibraryPath [ udev ] else "";
jnaFlag =
if stdenv.hostPlatform.isLinux then "--add-flags \"-Djna.library.path=${jnaLibraryPath}\"" else "";
in
''
mkdir -pv $out/lib/gradle/
cp -rv lib/ $out/lib/gradle/
gradle_launcher_jar=$(echo $out/lib/gradle/lib/gradle-launcher-*.jar)
test -f $gradle_launcher_jar
makeWrapper ${java}/bin/java $out/bin/gradle \
--set JAVA_HOME ${java} \
${jnaFlag} \
--add-flags "-classpath $gradle_launcher_jar org.gradle.launcher.GradleMain"
echo "${toolchainPaths}" > $out/lib/gradle/gradle.properties
'';
dontFixup = !stdenv.hostPlatform.isLinux;
fixupPhase =
let
arch = if stdenv.hostPlatform.is64bit then "amd64" else "i386";
newFileEvents = toString (lib.versionAtLeast version "8.12");
in
''
# get the correct jar executable for cross
export PATH="${buildPackages.jdk}/bin:$PATH"
. ${./patching.sh}
nativeVersion="$(extractVersion native-platform $out/lib/gradle/lib/native-platform-*.jar)"
for variant in "" "-ncurses5" "-ncurses6"; do
autoPatchelfInJar \
$out/lib/gradle/lib/native-platform-linux-${arch}$variant-''${nativeVersion}.jar \
"${lib.getLib stdenv.cc.cc}/lib64:${
lib.makeLibraryPath [
stdenv.cc.cc
ncurses5
ncurses6
]
}"
done
# The file-events library _seems_ to follow the native-platform version, but
# we wont assume that.
if [ -n "${newFileEvents}" ]; then
fileEventsVersion="$(extractVersion gradle-fileevents $out/lib/gradle/lib/gradle-fileevents-*.jar)"
autoPatchelfInJar \
$out/lib/gradle/lib/gradle-fileevents-''${fileEventsVersion}.jar \
"${lib.getLib stdenv.cc.cc}/lib64:${lib.makeLibraryPath [ stdenv.cc.cc ]}"
else
fileEventsVersion="$(extractVersion file-events $out/lib/gradle/lib/file-events-*.jar)"
autoPatchelfInJar \
$out/lib/gradle/lib/file-events-linux-${arch}-''${fileEventsVersion}.jar \
"${lib.getLib stdenv.cc.cc}/lib64:${lib.makeLibraryPath [ stdenv.cc.cc ]}"
fi
# The scanner doesn't pick up the runtime dependency in the jar.
# Manually add a reference where it will be found.
mkdir $out/nix-support
echo ${stdenv.cc.cc} > $out/nix-support/manual-runtime-dependencies
# Gradle will refuse to start without _both_ 5 and 6 versions of ncurses.
echo ${ncurses5} >> $out/nix-support/manual-runtime-dependencies
echo ${ncurses6} >> $out/nix-support/manual-runtime-dependencies
${lib.optionalString stdenv.hostPlatform.isLinux "echo ${udev} >> $out/nix-support/manual-runtime-dependencies"}
'';
passthru.tests = {
version = testers.testVersion {
package = finalAttrs.finalPackage;
command = ''
env GRADLE_USER_HOME=$TMPDIR/gradle org.gradle.native.dir=$TMPDIR/native \
gradle --version
'';
};
java-application = testers.testEqualContents {
assertion = "can build and run a trivial Java application";
expected = writeText "expected" "hello\n";
actual =
runCommand "actual"
{
nativeBuildInputs = [ finalAttrs.finalPackage ];
src = ./tests/java-application;
}
''
cp -a $src/* .
env GRADLE_USER_HOME=$TMPDIR/gradle org.gradle.native.dir=$TMPDIR/native \
gradle run --no-daemon --quiet --console plain > $out
'';
};
};
passthru.jdk = defaultJava;
passthru.wrapped = callPackage wrapGradle { } (gen' genArgs);
meta =
with lib;
{
inherit platforms;
description = "Enterprise-grade build system";
longDescription = ''
Gradle is a build system which offers you ease, power and freedom.
You can choose the balance for yourself. It has powerful multi-project
build support. It has a layer on top of Ivy that provides a
build-by-convention integration for Ivy. It gives you always the choice
between the flexibility of Ant and the convenience of a
build-by-convention behavior.
'';
homepage = "https://www.gradle.org/";
changelog = "https://docs.gradle.org/${version}/release-notes.html";
downloadPage = "https://gradle.org/next-steps/?version=${version}";
sourceProvenance = with sourceTypes; [
binaryBytecode
binaryNativeCode
];
license = licenses.asl20;
maintainers = with maintainers; [
britter
liff
lorenzleutgeb
];
teams = [ lib.teams.java ];
mainProgram = "gradle";
}
// meta;
});
# Calls the generated Gradle package with default arguments.
gen' = args: callPackage (gen args) { };
in
rec {
# NOTE: Default JDKs that are hardcoded below must be LTS versions
# and respect the compatibility matrix at
# https://docs.gradle.org/current/userguide/compatibility.html
gradle_9 = gen' {
version = "9.1.0";
hash = "sha256-oX3dhaJran9d23H/iwX8UQTAICxuZHgkKXkMkzaGyAY=";
defaultJava = jdk21;
};
gradle_8 = gen' {
version = "8.14.3";
hash = "sha256-vXEQIhNJMGCVbsIp2Ua+7lcVjb2J0OYrkbyg+ixfNTE=";
defaultJava = jdk21;
};
gradle_7 = gen' {
version = "7.6.6";
hash = "sha256-Zz2XdvMDvHBI/DMp0jLW6/EFGweJO9nRFhb62ahnO+A=";
defaultJava = jdk17;
};
# Default version of Gradle in nixpkgs.
gradle = gradle_8;
}

View File

@@ -0,0 +1,277 @@
{
mitm-cache,
lib,
pkgs,
stdenv,
callPackage,
}:
let
getPkg = attrPath: lib.getAttrFromPath (lib.splitString "." (toString attrPath)) pkgs;
in
# the derivation to fetch/update deps for
{
pkg ? getPkg attrPath,
pname ? null,
attrPath ? pname,
# bwrap flags for the update script (this will be put in bash as-is)
# this is relevant for downstream users
bwrapFlags ? "--ro-bind \"$PWD\" \"$PWD\"",
# deps path (relative to the package directory, or absolute)
data,
# redirect stdout to stderr to allow the update script to be used with update script combinators
silent ? true,
useBwrap ? stdenv.hostPlatform.isLinux,
}@attrs:
let
data' =
removeAttrs
(
if builtins.isPath data then
lib.importJSON data
else if builtins.isString data then
lib.importJSON "${dirOf pkg.meta.position}/${data}"
else
data
)
[
"!comment"
"!version"
];
parseArtifactUrl =
url:
let
extension = lib.last (lib.splitString "." url);
splitUrl = lib.splitString "/" url;
artifactId = builtins.elemAt splitUrl (builtins.length splitUrl - 3);
baseVer = builtins.elemAt splitUrl (builtins.length splitUrl - 2);
filename = builtins.elemAt splitUrl (builtins.length splitUrl - 1);
filenameNoExt = lib.removeSuffix ".${extension}" filename;
verCls = lib.removePrefix "${artifactId}-" filenameNoExt;
in
rec {
inherit
artifactId
baseVer
filename
extension
;
isSnapshot = lib.hasSuffix "-SNAPSHOT" baseVer;
version =
if isSnapshot && !lib.hasPrefix "SNAPSHOT" verCls then
builtins.concatStringsSep "-" (lib.take 3 (lib.splitString "-" verCls))
else
baseVer;
classifier = if verCls == version then null else lib.removePrefix "${version}-" verCls;
# for snapshots
timestamp = builtins.elemAt (lib.splitString "-" version) 1;
buildNum = builtins.elemAt (lib.splitString "-" version) 2;
};
parseMetadataUrl =
url:
let
xmlBase = lib.removeSuffix "/maven-metadata.xml" url;
vMeta = lib.hasSuffix "-SNAPSHOT" xmlBase;
splitBase = lib.splitString "/" xmlBase;
in
if vMeta then
{
vMeta = true;
baseVer = builtins.elemAt splitBase (builtins.length splitBase - 1);
artifactId = builtins.elemAt splitBase (builtins.length splitBase - 2);
}
else
{
vMeta = false;
baseVer = null;
artifactId = builtins.elemAt splitBase (builtins.length splitBase - 1);
};
extractHashArtifact =
afterHash:
let
nameVer = builtins.match "([^/]*)/([^/]*)(/SNAPSHOT)?(/.*)?" afterHash;
artifactId = builtins.elemAt nameVer 0;
version = builtins.elemAt nameVer 1;
isSnapshot = builtins.elemAt nameVer 2 != null;
cls = builtins.elemAt nameVer 3;
in
rec {
inherit artifactId version isSnapshot;
baseVer =
if !isSnapshot then
version
else
builtins.head (builtins.match "(.*)-([^-]*)-([^-]*)" version) + "-SNAPSHOT";
classifier = if cls == null then null else lib.removePrefix "/" cls;
clsSuf = if classifier == null then "" else "-${classifier}";
};
# replace base#name/ver with base/name/ver/name-ver
decompressNameVer =
prefix:
let
splitHash = lib.splitString "#" (builtins.concatStringsSep "/" prefix);
inherit (extractHashArtifact (lib.last splitHash))
artifactId
baseVer
version
clsSuf
;
in
if builtins.length splitHash == 1 then
builtins.head splitHash
else
builtins.concatStringsSep "/${artifactId}/${baseVer}/" (
lib.init splitHash ++ [ "${artifactId}-${version}${clsSuf}" ]
);
# `visit` all elements in attrs and merge into a set
# attrs will be passed as parent1, parent1 will be passed as parent2
visitAttrs =
parent1: prefix: attrs:
builtins.foldl' (a: b: a // b) { } (lib.mapAttrsToList (visit parent1 attrs prefix) attrs);
# convert a compressed deps.json into an uncompressed json used for mitm-cache.fetch
visit =
parent2: parent1: prefix: k: v:
# groupId being present means this is a metadata xml "leaf" and we shouldn't descend further
if builtins.isAttrs v && !v ? groupId then
visitAttrs parent1 (prefix ++ [ k ]) v
else
let
url = "${decompressNameVer prefix}.${k}";
in
{
${url} =
if builtins.isString v then
{ hash = v; }
else
{
text =
let
xmlBase = lib.removeSuffix "/maven-metadata.xml" url;
meta = parseMetadataUrl url // v;
inherit (meta)
groupId
vMeta
artifactId
baseVer
;
fileList = builtins.filter (x: lib.hasPrefix xmlBase x && x != url) (builtins.attrNames finalData);
jarPomList = map parseArtifactUrl fileList;
sortByVersion = a: b: (builtins.compareVersions a.version b.version) < 0;
sortedJarPomList = lib.sort sortByVersion jarPomList;
uniqueVersionFiles = map ({ i, x }: x) (
builtins.filter (
{ i, x }: i == 0 || (builtins.elemAt sortedJarPomList (i - 1)).version != x.version
) (lib.imap0 (i: x: { inherit i x; }) sortedJarPomList)
);
uniqueVersions' = map (x: x.version) uniqueVersionFiles;
releaseVersions = map (x: x.version) (builtins.filter (x: !x.isSnapshot) uniqueVersionFiles);
latestVer = v.latest or v.release or (lib.last uniqueVersions');
releaseVer = v.release or (lib.last releaseVersions);
# The very latest version isn't necessarily used by Gradle, so it may not be present in the MITM data.
# In order to generate better metadata xml, if the latest version is known but wasn't fetched by Gradle,
# add it anyway.
uniqueVersions =
uniqueVersions'
++ lib.optional (!builtins.elem releaseVer uniqueVersions') releaseVer
++ lib.optional (!builtins.elem latestVer uniqueVersions' && releaseVer != latestVer) latestVer;
lastUpdated =
v.lastUpdated
or (if vMeta then builtins.replaceStrings [ "." ] [ "" ] snapshotTs else "20240101123456");
# the following are only used for snapshots
snapshotTsAndNum = lib.splitString "-" latestVer;
snapshotTs = builtins.elemAt snapshotTsAndNum 1;
snapshotNum = lib.last snapshotTsAndNum;
indent = x: s: builtins.concatStringsSep "\n" (map (s: x + s) (lib.splitString "\n" s));
containsSpecialXmlChars = s: builtins.match ''.*[<>"'&].*'' s != null;
in
# make sure all user-provided data is safe
assert lib.hasInfix "${builtins.replaceStrings [ "." ] [ "/" ] groupId}/${artifactId}" url;
assert !containsSpecialXmlChars groupId;
assert !containsSpecialXmlChars lastUpdated;
if vMeta then
''
<?xml version="1.0" encoding="UTF-8"?>
<metadata modelVersion="1.1.0">
<groupId>${groupId}</groupId>
<artifactId>${artifactId}</artifactId>
<version>${baseVer}</version>
<versioning>
<snapshot>
<timestamp>${snapshotTs}</timestamp>
<buildNumber>${snapshotNum}</buildNumber>
</snapshot>
<lastUpdated>${lastUpdated}</lastUpdated>
<snapshotVersions>
${builtins.concatStringsSep "\n" (
map (
x:
indent " " ''
<snapshotVersion>${
lib.optionalString (x.classifier != null) "\n <classifier>${x.classifier}</classifier>"
}
<extension>${x.extension}</extension>
<value>${x.version}</value>
<updated>${builtins.replaceStrings [ "." ] [ "" ] x.timestamp}</updated>
</snapshotVersion>''
) sortedJarPomList
)}
</snapshotVersions>
</versioning>
</metadata>
''
else
assert !containsSpecialXmlChars latestVer;
assert !containsSpecialXmlChars releaseVer;
''
<?xml version="1.0" encoding="UTF-8"?>
<metadata modelVersion="1.1.0">
<groupId>${groupId}</groupId>
<artifactId>${artifactId}</artifactId>
<versioning>
<latest>${latestVer}</latest>
<release>${releaseVer}</release>
<versions>
${builtins.concatStringsSep "\n" (map (x: " <version>${x}</version>") uniqueVersions)}
</versions>
<lastUpdated>${lastUpdated}</lastUpdated>
</versioning>
</metadata>
'';
};
};
finalData = visitAttrs { } [ ] data';
in
mitm-cache.fetch {
name = "${pkg.pname or pkg.name}-deps";
data = finalData // {
"!version" = 1;
};
passthru = lib.optionalAttrs (!builtins.isAttrs data) {
updateScript = callPackage ./update-deps.nix { } {
inherit
pkg
pname
attrPath
bwrapFlags
data
silent
useBwrap
;
};
};
}

View File

@@ -0,0 +1,8 @@
gradle.projectsLoaded {
rootProject.allprojects {
tasks.withType(AbstractArchiveTask) {
preserveFileTimestamps = false
reproducibleFileOrder = true
}
}
}

View File

@@ -0,0 +1,10 @@
gradle.projectsLoaded {
rootProject.allprojects {
task nixDownloadDeps {
doLast {
configurations.findAll{it.canBeResolved}.each{it.resolve()}
buildscript.configurations.findAll{it.canBeResolved}.each{it.resolve()}
}
}
}
}

View File

@@ -0,0 +1,29 @@
extractVersion() {
local jar version
local prefix="$1"
shift
local candidates="$@"
jar="$(basename -a $candidates | sort | head -n1)"
version="${jar#$prefix-}"
echo "${version%.jar}"
}
autoPatchelfInJar() {
local file="$1" rpath="$2"
local work
work="$(mktemp -dt patching.XXXXXXXXXX)"
pushd "$work"
jar xf "$file"
rm "$file"
autoPatchelf -- .
jar cf "$file" .
popd
}

View File

@@ -0,0 +1,72 @@
gradleConfigureHook() {
if [ -z "${GRADLE_USER_HOME-}" ]; then
GRADLE_USER_HOME="$(mktemp -d)"
fi
export GRADLE_USER_HOME
export TERM=dumb
gradleFlagsArray+=(--no-daemon --console plain --init-script "${gradleInitScript:-@init_script@}")
if [ -n "${MITM_CACHE_CA-}" ]; then
if [ -z "${MITM_CACHE_KEYSTORE-}" ]; then
MITM_CACHE_KEYSTORE="$MITM_CACHE_CERT_DIR/keystore"
MITM_CACHE_KS_PWD="$(head -c10 /dev/random | base32)"
echo y | @jdk@/bin/keytool -importcert -file "$MITM_CACHE_CA" -alias alias -keystore "$MITM_CACHE_KEYSTORE" -storepass "$MITM_CACHE_KS_PWD"
fi
gradleFlagsArray+=(-Dhttp.proxyHost="$MITM_CACHE_HOST" -Dhttp.proxyPort="$MITM_CACHE_PORT")
gradleFlagsArray+=(-Dhttps.proxyHost="$MITM_CACHE_HOST" -Dhttps.proxyPort="$MITM_CACHE_PORT")
gradleFlagsArray+=(-Djavax.net.ssl.trustStore="$MITM_CACHE_KEYSTORE" -Djavax.net.ssl.trustStorePassword="$MITM_CACHE_KS_PWD")
else
gradleFlagsArray+=(--offline)
fi
if ! [[ -v enableParallelBuilding ]]; then
enableParallelBuilding=1
fi
if ! [[ -v enableParallelChecking ]]; then
enableParallelChecking=1
fi
if ! [[ -v enableParallelUpdating ]]; then
enableParallelUpdating=1
fi
}
gradle() {
local flagsArray=()
concatTo flagsArray gradleFlags gradleFlagsArray
command gradle "${flagsArray[@]}" "$@"
}
gradleBuildPhase() {
runHook preBuild
gradle ${enableParallelBuilding:+--parallel} ${gradleBuildTask:-assemble}
runHook postBuild
}
gradleCheckPhase() {
runHook preCheck
gradle ${enableParallelChecking:+--parallel} ${gradleCheckTask:-test}
runHook postCheck
}
gradleUpdateScript() {
runHook preBuild
runHook preGradleUpdate
gradle ${enableParallelUpdating:+--parallel} ${gradleUpdateTask:-nixDownloadDeps}
runHook postGradleUpdate
}
if [ -z "${dontUseGradleConfigure-}" ]; then
preConfigureHooks+=(gradleConfigureHook)
fi
if [ -z "${dontUseGradleBuild-}" ] && [ -z "${buildPhase-}" ]; then
buildPhase=gradleBuildPhase
fi
if [ -z "${dontUseGradleCheck-}" ] && [ -z "${checkPhase-}" ]; then
checkPhase=gradleCheckPhase
fi

View File

@@ -0,0 +1,7 @@
plugins {
id('application')
}
application {
mainClass = 'Main'
}

View File

@@ -0,0 +1,5 @@
public class Main {
public static void main(String[] args) {
System.out.println("hello");
}
}

View File

@@ -0,0 +1,11 @@
plugins {
id('application')
}
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(@JAVA_VERSION@))
}
application {
mainClass = 'Main'
}

View File

@@ -0,0 +1,5 @@
public class Main {
public static void main(String[] args) {
System.out.println(System.getProperty("java.version"));
}
}

View File

@@ -0,0 +1,157 @@
{
lib,
runtimeShell,
srcOnly,
stdenvNoCC,
writeTextFile,
writeShellScript,
path,
bubblewrap,
coreutils,
curl,
jq,
mitm-cache,
nix,
openssl,
procps,
python3,
}:
lib.makeOverridable (
{
pkg,
pname,
attrPath,
bwrapFlags,
data,
silent,
useBwrap,
}:
let
keep = [
"MITM_CACHE_HOST"
"MITM_CACHE_PORT"
"MITM_CACHE_ADDRESS"
"MITM_CACHE_CA"
"MITM_CACHE_CERT_DIR"
];
gradleScript = writeShellScript "gradle-commands.sh" ''
set -eo pipefail
export http_proxy="$MITM_CACHE_ADDRESS"
export https_proxy="$MITM_CACHE_ADDRESS"
export SSL_CERT_FILE="$MITM_CACHE_CA"
export NIX_SSL_CERT_FILE="$MITM_CACHE_CA"
export GRADLE_USER_HOME="$(${coreutils}/bin/mktemp -d)"
export IN_GRADLE_UPDATE_DEPS=1
trap "${coreutils}/bin/rm -rf '$GRADLE_USER_HOME'" SIGINT SIGTERM ERR EXIT
cd "$(${coreutils}/bin/mktemp -d)"
${coreutils}/bin/mkdir out
export out="$PWD/out"
trap "${coreutils}/bin/rm -rf '$PWD'" SIGINT SIGTERM ERR EXIT
source "$stdenv/setup"
phases="''${prePhases[*]:-} unpackPhase patchPhase ''${preConfigurePhases[*]:-} configurePhase gradleUpdateScript" genericBuild
'';
source = srcOnly (
pkg.overrideAttrs (old: {
mitmCache = "";
gradleInitScript = ./init-deps.gradle;
stdenv = old.stdenv or stdenvNoCC;
})
);
sourceDrvPath = builtins.unsafeDiscardOutputDependency source.drvPath;
nixShellKeep = lib.concatMapStringsSep " " (x: "--keep ${x}") keep;
in
writeTextFile {
name = "fetch-deps.sh";
executable = true;
# see pkgs/common-updater/combinators.nix
derivationArgs.passthru = {
supportedFeatures = lib.optional silent "silent";
}
// lib.optionalAttrs (attrPath != null) { inherit attrPath; };
text = ''
#!${runtimeShell}
set -eo pipefail
export PATH="${
lib.makeBinPath (
[
coreutils
curl
jq
mitm-cache
openssl
procps
python3.pkgs.ephemeral-port-reserve
]
++ lib.optional useBwrap bubblewrap
)
}:$PATH"
outPath="${
# if this is an absolute path in nix store, use path relative to the store path
if lib.hasPrefix "${builtins.storeDir}/" (toString data) then
builtins.concatStringsSep "/" (
lib.drop 1 (lib.splitString "/" (lib.removePrefix "${builtins.storeDir}/" (toString data)))
)
# if this is an absolute path anywhere else, just use that path
else if lib.hasPrefix "/" (toString data) then
toString data
# otherwise, use a path relative to the package
else
"${dirOf pkg.meta.position}/${data}"
}"
pushd "$(mktemp -d)" >/dev/null
MITM_CACHE_DIR="$PWD"
trap "rm -rf '$MITM_CACHE_DIR'" SIGINT SIGTERM ERR EXIT
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -sha256 -days 1 -out ca.cer -subj "/C=AL/ST=a/L=a/O=a/OU=a/CN=example.org"
export MITM_CACHE_HOST=127.0.0.1
export MITM_CACHE_PORT="''${mitmCachePort:-$(ephemeral-port-reserve "$MITM_CACHE_HOST")}"
export MITM_CACHE_ADDRESS="$MITM_CACHE_HOST:$MITM_CACHE_PORT"
# forget all redirects - this makes the lockfiles predictable
# not only does this strip CDN URLs, but it also improves security - since the redirects aren't
# stored in the lockfile, a malicious actor can't change the redirect URL stored in the lockfile
mitm-cache \
-l"$MITM_CACHE_ADDRESS" \
record \
--reject '\.(md5|sha(1|256|512:?):?)$' \
--forget-redirects-from '.*' \
--record-text '/maven-metadata\.xml$' >/dev/null 2>/dev/null &
MITM_CACHE_PID="$!"
# wait for mitm-cache to fully start
for i in {0..20}; do
kill -0 "$MITM_CACHE_PID" 2>/dev/null || (echo "Failed to start mitm-cache" && exit 1)
curl -so/dev/null "$MITM_CACHE_ADDRESS" && break
[[ "$i" -eq 20 ]] && (echo "Failed to start mitm-cache" && exit 1)
sleep 0.5
done
trap "kill '$MITM_CACHE_PID'" SIGINT SIGTERM ERR EXIT
export MITM_CACHE_CERT_DIR="$PWD"
export MITM_CACHE_CA="$MITM_CACHE_CERT_DIR/ca.cer"
popd >/dev/null
useBwrap="''${USE_BWRAP:-${toString useBwrap}}"
if [ -n "$useBwrap" ]; then
# bwrap isn't necessary, it's only used to prevent messy build scripts from touching ~
bwrap \
--unshare-all --share-net --clearenv --chdir / --setenv HOME /homeless-shelter \
--tmpfs /home --bind /tmp /tmp --ro-bind /nix /nix --ro-bind /run /run --proc /proc --dev /dev \
--ro-bind ${toString path} ${toString path} --bind "$MITM_CACHE_CERT_DIR" "$MITM_CACHE_CERT_DIR" \
${builtins.concatStringsSep " " (map (x: "--setenv ${x} \"\$${x}\"") keep)} \
--setenv NIX_BUILD_SHELL ${runtimeShell} ${bwrapFlags} ''${BWRAP_FLAGS:-} \
-- ${nix}/bin/nix-shell --pure --run ${gradleScript} ${nixShellKeep} ${sourceDrvPath}
else
NIX_BUILD_SHELL=${runtimeShell} nix-shell --pure --run ${gradleScript} ${nixShellKeep} ${sourceDrvPath}
fi${lib.optionalString silent " >&2"}
kill -s SIGINT "$MITM_CACHE_PID"
for i in {0..20}; do
# check for valid json
if jq -e 1 "$MITM_CACHE_DIR/out.json" >/dev/null 2>/dev/null; then
exec ${python3.interpreter} ${./compress-deps-json.py} "$MITM_CACHE_DIR/out.json" "$outPath"
fi
sleep 1
done
exit 1
'';
}
)