···1+# Gradle {#gradle}
2+3+Gradle is a popular build tool for Java/Kotlin. Gradle itself doesn't
4+currently provide tools to make dependency resolution reproducible, so
5+nixpkgs has a proxy designed for intercepting Gradle web requests to
6+record dependencies so they can be restored in a reproducible fashion.
7+8+## Building a Gradle package {#building-a-gradle-package}
9+10+Here's how a typical derivation will look like:
11+12+```nix
13+stdenv.mkDerivation (finalAttrs: {
14+ pname = "pdftk";
15+ version = "3.3.3";
16+17+ src = fetchFromGitLab {
18+ owner = "pdftk-java";
19+ repo = "pdftk";
20+ rev = "v${finalAttrs.version}";
21+ hash = "sha256-ciKotTHSEcITfQYKFZ6sY2LZnXGChBJy0+eno8B3YHY=";
22+ };
23+24+ nativeBuildInputs = [ gradle ];
25+26+ # if the package has dependencies, mitmCache must be set
27+ mitmCache = gradle.fetchDeps {
28+ inherit (finalAttrs) pname;
29+ data = ./deps.json;
30+ };
31+32+ # this is required for using mitm-cache on Darwin
33+ __darwinAllowLocalNetworking = true;
34+35+ gradleFlags = [ "-Dfile.encoding=utf-8" ];
36+37+ # defaults to "assemble"
38+ gradleBuildTask = "shadowJar";
39+40+ # will run the gradleCheckTask (defaults to "test")
41+ doCheck = true;
42+43+ installPhase = ''
44+ mkdir -p $out/{bin,share/pdftk}
45+ cp build/libs/pdftk-all.jar $out/share/pdftk
46+47+ makeWrapper ${jre}/bin/java $out/bin/pdftk \
48+ --add-flags "-jar $out/share/pdftk/pdftk-all.jar"
49+50+ cp ${finalAttrs.src}/pdftk.1 $out/share/man/man1
51+ '';
52+53+ meta.sourceProvenance = with lib.sourceTypes; [
54+ fromSource
55+ binaryBytecode # mitm cache
56+ ];
57+})
58+```
59+60+To update (or initialize) dependencies, run the update script via
61+something like `$(nix-build -A <pname>.mitmCache.updateScript)`
62+(`nix-build` builds the `updateScript`, `$(...)` runs the script at the
63+path printed by `nix-build`).
64+65+If your package can't be evaluated using a simple `pkgs.<pname>`
66+expression (for example, if your package isn't located in nixpkgs, or if
67+you want to override some of its attributes), you will usually have to
68+pass `pkg` instead of `pname` to `gradle.fetchDeps`. There are two ways
69+of doing it.
70+71+The first is to add the derivation arguments required for getting the
72+package. Using the pdftk example above:
73+74+```nix
75+{ lib
76+, stdenv
77+# ...
78+, pdftk
79+}:
80+81+stdenv.mkDerivation (finalAttrs: {
82+ # ...
83+ mitmCache = gradle.fetchDeps {
84+ pkg = pdftk;
85+ data = ./deps.json;
86+ };
87+})
88+```
89+90+This allows you to `override` any arguments of the `pkg` used for
91+the update script (for example, `pkg = pdftk.override { enableSomeFlag =
92+true };`), so this is the preferred way.
93+94+The second is to create a `let` binding for the package, like this:
95+96+```nix
97+let self = stdenv.mkDerivation {
98+ # ...
99+ mitmCache = gradle.fetchDeps {
100+ pkg = self;
101+ data = ./deps.json;
102+ };
103+}; in self
104+```
105+106+This is useful if you can't easily pass the derivation as its own
107+argument, or if your `mkDerivation` call is responsible for building
108+multiple packages.
109+110+In the former case, the update script will stay the same even if the
111+derivation is called with different arguments. In the latter case, the
112+update script will change depending on the derivation arguments. It's up
113+to you to decide which one would work best for your derivation.
114+115+## Update Script {#gradle-update-script}
116+117+The update script does the following:
118+119+- Build the derivation's source via `pkgs.srcOnly`
120+- Enter a `nix-shell` for the derivation in a `bwrap` sandbox (the
121+ sandbox is only used on Linux)
122+- Set the `IN_GRADLE_UPDATE_DEPS` environment variable to `1`
123+- Run the derivation's `unpackPhase`, `patchPhase`, `configurePhase`
124+- Run the derivation's `gradleUpdateScript` (the Gradle setup hook sets
125+ a default value for it, which runs `preBuild`, `preGradleUpdate`
126+ hooks, fetches the dependencies using `gradleUpdateTask`, and finally
127+ runs the `postGradleUpdate` hook)
128+- Finally, store all of the fetched files' hashes in the lockfile. They
129+ may be `.jar`/`.pom` files from Maven repositories, or they may be
130+ files otherwise used for building the package.
131+132+`fetchDeps` takes the following arguments:
133+134+- `attrPath` - the path to the package in nixpkgs (for example,
135+ `"javaPackages.openjfx22"`). Used for update script metadata.
136+- `pname` - an alias for `attrPath` for convenience. This is what you
137+ will generally use instead of `pkg` or `attrPath`.
138+- `pkg` - the package to be used for fetching the dependencies. Defaults
139+ to `getAttrFromPath (splitString "." attrPath) pkgs`.
140+- `bwrapFlags` - allows you to override bwrap flags (only relevant for
141+ downstream, non-nixpkgs projects)
142+- `data` - path to the dependencies lockfile (can be relative to the
143+ package, can be absolute). In nixpkgs, it's discouraged to have the
144+ lockfiles be named anything other `deps.json`, consider creating
145+ subdirectories if your package requires multiple `deps.json` files.
146+147+## Environment {#gradle-environment}
148+149+The Gradle setup hook accepts the following environment variables:
150+151+- `mitmCache` - the MITM proxy cache imported using `gradle.fetchDeps`
152+- `gradleFlags` - command-line flags to be used for every Gradle
153+ invocation (this simply registers a function that uses the necessary
154+ flags).
155+ - You can't use `gradleFlags` for flags that contain spaces, in that
156+ case you must add `gradleFlagsArray+=("-flag with spaces")` to the
157+ derivation's bash code instead.
158+ - If you want to build the package using a specific Java version, you
159+ can pass `"-Dorg.gradle.java.home=${jdk}"` as one of the flags.
160+- `gradleBuildTask` - the Gradle task (or tasks) to be used for building
161+ the package. Defaults to `assemble`.
162+- `gradleCheckTask` - the Gradle task (or tasks) to be used for checking
163+ the package if `doCheck` is set to `true`. Defaults to `test`.
164+- `gradleUpdateTask` - the Gradle task (or tasks) to be used for
165+ fetching all of the package's dependencies in
166+ `mitmCache.updateScript`. Defaults to `nixDownloadDeps`.
167+- `gradleUpdateScript` - the code to run for fetching all of the
168+ package's dependencies in `mitmCache.updateScript`. Defaults to
169+ running the `preBuild` and `preGradleUpdate` hooks, running the
170+ `gradleUpdateTask`, and finally running the `postGradleUpdate` hook.
171+- `gradleInitScript` - path to the `--init-script` to pass to Gradle. By
172+ default, a simple init script that enables reproducible archive
173+ creation is used.
174+ - Note that reproducible archives might break some builds. One example
175+ of an error caused by it is `Could not create task ':jar'. Replacing
176+ an existing task that may have already been used by other plugins is
177+ not supported`. If you get such an error, the easiest "fix" is
178+ disabling reproducible archives altogether by setting
179+ `gradleInitScript` to something like `writeText
180+ "empty-init-script.gradle" ""`
181+- `enableParallelBuilding` / `enableParallelChecking` /
182+ `enableParallelUpdating` - pass `--parallel` to Gradle in the
183+ build/check phase or in the update script. Defaults to true. If the
184+ build fails for mysterious reasons, consider setting this to false.
185+- `dontUseGradleConfigure` / `dontUseGradleBuild` / `dontUseGradleCheck`
186+ \- force disable the Gradle setup hook for certain phases.
187+ - Note that if you disable the configure hook, you may face issues
188+ such as `Failed to load native library 'libnative-platform.so'`,
189+ because the configure hook is responsible for initializing Gradle.
···1+# Gradle Setup Hook
2+3+## Introduction
4+5+Gradle build scripts are written in a DSL, computing the list of Gradle
6+dependencies is a turing-complete task, not just in theory but in
7+practice. Fetching all of the dependencies often requires building some
8+native code, running some commands to check the host platform, or just
9+fetching some files using either JVM code or commands like `curl` or
10+`wget`.
11+12+This practice is widespread and isn't considered a bad practice in the
13+Java world, so all we can do is run Gradle to check what dependencies
14+end up being fetched, and allow derivation authors to apply workarounds
15+so they can run the code necessary for fetching the dependencies our
16+script doesn't fetch.
17+18+"Run Gradle to check what dependencies end up being fetched" isn't a
19+straightforward task. For example, Gradle usually uses Maven
20+repositories, which have features such as "snapshots", a way to always
21+use the latest version of a dependency as opposed to a fixed version.
22+Obviously, this is horrible for reproducibility. Additionally, Gradle
23+doesn't offer a way to export the list of dependency URLs and hashes (it
24+does in a way, but it's far from being complete, and as such is useless
25+for nixpkgs). Even if did, it would be annoying to use considering
26+fetching non-Gradle dependendencies in Gradle scripts is commonplace.
27+28+That's why the setup hook uses mitm-cache, a program designed for
29+intercepting all HTTP requests, recording all the files that were
30+accessed, creating a Nix derivation with all of them, and then allowing
31+the Gradle derivation to access these files.
32+33+## Maven Repositories
34+35+(Reference: [Repository
36+Layout](https://cwiki.apache.org/confluence/display/MAVENOLD/Repository+Layout+-+Final))
37+38+Most of Gradle dependencies are fetched from Maven repositories. For
39+each dependency, Gradle finds the first repo where it can successfully
40+fetch that dependency, and uses that repo for it. Different repos might
41+actually return different files for the same artifact because of e.g.
42+pom normalization. Different repos may be used for the same artifact
43+even across a single package (for example, if two build scripts define
44+repositories in a different order).
45+46+The artifact metadata is specified in a .pom file, and the artifacts
47+themselves are typically .jar files. The URL format is as follows:
48+49+`<repo>/<group-id>/<artifact-id>/<base-version>/<artifact-id>-<version>[-<classifier>].<ext>`
50+51+For example:
52+53+- `https://repo.maven.apache.org/maven2/org/slf4j/slf4j-api/2.0.9/slf4j-api-2.0.9.pom`
54+- `https://oss.sonatype.org/content/groups/public/com/tobiasdiez/easybind/2.2.1-SNAPSHOT/easybind-2.2.1-20230117.075740-16.pom`
55+56+Where:
57+58+- `<repo>` is the repo base (`https://repo.maven.apache.org/maven2`)
59+- `<group-id>` is the group ID with dots replaced with slashes
60+ (`org.slf4j` -> `org/slf4j`)
61+- `<artifact-id>` is the artifact ID (`slf4j-api`)
62+- `<base-version>` is the artifact version (`2.0.9` for normal
63+ artifacts, `2.2.1-SNAPSHOT` for snapshots)
64+- `<version>` is the artifact version - can be either `<base-version>`
65+ or `<version-base>-<timestamp>-<build-num>` (`2.0.9` for normal
66+ artifacts, and either `2.2.1-SNAPSHOT` or `2.2.1-20230117.075740-16`
67+ for snapshots)
68+ - `<version-base>` - `<base-version>` without the `-SNAPSHOT` suffix
69+ - `<timestamp>` - artifact build timestamp in the `YYYYMMDD.HHMMSS`
70+ format (UTC)
71+ - `<build-num>` - a counter that's incremented by 1 for each new
72+ snapshot build
73+- `<classifier>` is an optional classifier for allowing a single .pom to
74+ refer to multiple .jar files. .pom files don't have classifiers, as
75+ they describe metadata.
76+- `<ext>` is the extension. .pom
77+78+Note that the artifact ID can contain `-`, so you can't extract the
79+artifact ID and version from just the file name.
80+81+Additionally, the files in the repository may have associated signature
82+files, formed by appending `.asc` to the filename, and hashsum files,
83+formed by appending `.md5` or `.sha1` to the filename. The signatures
84+are harmless, but the `.md5`/`.sha1` files are rejected.
85+86+The reasoning is as follows - consider two files `a.jar` and `b.jar`,
87+that have the same hash. Gradle will fetch `a.jar.sha1`, find out that
88+it hasn't yet downloaded a file with this hash, and then fetch `a.jar`,
89+and finally download `b.jar.sha1`, locate it in its cache, and then
90+*not* download `b.jar`. This means `b.jar` won't be stored in the MITM
91+cache. Then, consider that on a later invocation, the fetching order
92+changed, whether it was because of a running on different system,
93+changed behavior after a Gradle update, or any other source of
94+nondeterminism - `b.jar` is fetched before `a.jar`. Gradle will first
95+fetch `b.jar.sha1`, not find it in its cache, attempt to fetch `b.jar`,
96+and fail, as the cache doesn't have that file.
97+98+For the same reason, the proxy strips all checksum/etag headers. An
99+alternative would be to make the proxy remember previous checksums and
100+etags, but that would complicate the implementation - however, such a
101+feature can be implemented if necessary. Note that checksum/etag header
102+stripping is hardcoded, but `.md5/.sha1` file rejection is configured
103+via CLI arguments.
104+105+**Caveat**: Gradle .module files also contain file hashes, in md5, sha1,
106+sha256, sha512 formats. It posed no problem as of yet, but it might in
107+the future. If it does pose problems, the deps derivation code can be
108+extended to find all checksums in .module files and copy existing files
109+there if their hash matches.
110+111+## Snapshots
112+113+Snapshots are a way to publish the very latest, unstable version of a
114+dependency that constantly changes. Any project that depends on a
115+snapshot will depend on this rolling version, rather than a fixed
116+version. It's easy to understand why this is a bad idea for reproducible
117+builds. Still, they can be dealt with by the logic in `gradle.fetchDeps`
118+and `gradle.updateDeps`.
119+120+First, as you can see above, while normal artifacts have the same
121+`base-version` and `version`, for snapshots it usually (but not
122+necessarily) differs.
123+124+Second, for figuring out where to download the snapshot, Gradle consults
125+`maven-metadata.xml`. With that in mind...
126+127+## Maven Metadata
128+129+(Reference: [Maven
130+Metadata](https://maven.apache.org/repositories/metadata.html),
131+[Metadata](https://maven.apache.org/ref/3.9.8/maven-repository-metadata/repository-metadata.html)
132+133+Maven metadata files are called `maven-metadata.xml`.
134+135+There are three levels of metadata: "G level", "A level", "V level",
136+representing group, artifact, or version metadata.
137+138+G level metadata is currently unsupported. It's only used for Maven
139+plugins, which Gradle presumably doesn't use.
140+141+A level metadata is used for getting the version list for an artifact.
142+It's an xml with the following items:
143+144+- `<groupId>` - group ID
145+- `<artifactId>` - artifact ID
146+- `<versioning>`
147+ - `<latest>` - the very latest base version (e.g. `2.2.1-SNAPSHOT`)
148+ - `<release>` - the latest non-snapshot version
149+ - `<versions>` - the version list, each in a `<version>` tag
150+ - `<lastUpdated>` - the metadata update timestamp (UTC,
151+ `YYYYMMDDHHMMSS`)
152+153+V level metadata is used for listing the snapshot versions. It has the
154+following items:
155+156+- `<groupId>` - group ID
157+- `<artifactId>` - artifact ID
158+- `<versioning>`
159+ - `<lastUpdated>` - the metadata update timestamp (UTC,
160+ `YYYYMMDDHHMMSS`)
161+ - `<snapshot>` - info about the latest snapshot version
162+ - `<timestamp>` - build timestamp (UTC, `YYYYMMDD.HHMMSS`)
163+ - `<buildNumber>` - build number
164+ - `<snapshotVersions>` - the list of all available snapshot file info,
165+ each info is enclosed in a `<snapshotVersion>`
166+ - `<classifier>` - classifier (optional)
167+ - `<extension>` - file extension
168+ - `<value>` - snapshot version (as opposed to base version)
169+ - `<updated>` - snapshot build timestamp (UTC, `YYYYMMDDHHMMSS`)
170+171+## Lockfile Format
172+173+The mitm-cache lockfile format is described in the [mitm-cache
174+README](https://github.com/chayleaf/mitm-cache#readme).
175+176+The nixpkgs Gradle lockfile format is more complicated:
177+178+```json
179+{
180+ "!comment": "This is a nixpkgs Gradle dependency lockfile. For more details, refer to the Gradle section in the nixpkgs manual.",
181+ "!version": 1,
182+ "https://oss.sonatype.org/content/repositories/snapshots/com/badlogicgames/gdx-controllers": {
183+ "gdx-controllers#gdx-controllers-core/2.2.4-20231021.200112-6/SNAPSHOT": {
184+185+ "jar": "sha256-Gdz2J1IvDJFktUD2XeGNS0SIrOyym19X/+dCbbbe3/U=",
186+ "pom": "sha256-90QW/Mtz1jbDUhKjdJ88ekhulZR2a7eCaEJoswmeny4="
187+ },
188+ "gdx-controllers-core/2.2.4-SNAPSHOT/maven-metadata": {
189+ "xml": {
190+ "groupId": "com.badlogicgames.gdx-controllers"
191+ }
192+ }
193+ },
194+ "https://repo.maven.apache.org/maven2": {
195+ "com/badlogicgames/gdx#gdx-backend-lwjgl3/1.12.1": {
196+ "jar": "sha256-B3OwjHfBoHcJPFlyy4u2WJuRe4ZF/+tKh7gKsDg41o0=",
197+ "module": "sha256-9O7d2ip5+E6OiwN47WWxC8XqSX/mT+b0iDioCRTTyqc=",
198+ "pom": "sha256-IRSihaCUPC2d0QzB0MVDoOWM1DXjcisTYtnaaxR9SRo="
199+ }
200+ }
201+}
202+```
203+204+`!comment` is a human-readable description explaining what the file is,
205+`!version` is the lockfile version (note that while it shares the name
206+with mitm-cache's `!version`, they don't actually have to be in sync and
207+can be bumped separately).
208+209+The other keys are parts of a URL. Each URL is split into three parts.
210+They are joined like this: `<part1>/<part2>.<part3>`.
211+212+Some URLs may have a `#` in them. In that case, the part after `#` is
213+parsed as `#<artifact-id>/<version>[/SNAPSHOT][/<classifier>].<ext>` and
214+expanded into
215+`<artifact-id>/<base-version>/<artifact-id>-<version>[-<classifier>].<ext>`.
216+217+Each URL has a value associated with it. The value may be:
218+219+- an SRI hash (string)
220+- for `maven-metadata.xml` - an attrset containing the parts of the
221+ metadata that can't be generated in Nix code (e.g. `groupId`, which is
222+ challenging to parse from a URL because it's not always possible to
223+ discern where the repo base ends and the group ID begins).
224+225+`compress-deps-json.py` converts the JSON from mitm-cache format into
226+nixpkgs Gradle lockfile format. `fetch.nix` does the opposite.
227+228+## Security Considerations
229+230+Lockfiles won't be human-reviewed. They must be tampering-resistant.
231+That's why it's imperative that nobody can inject their own contents
232+into the lockfiles.
233+234+This is achieved in a very simple way - the `deps.json` only contains
235+the following:
236+237+- `maven-metadata.xml` URLs and small pieces of the contained metadata
238+ (most of it will be generated in Nix, i.e. the area of injection is
239+ minimal, and the parts that aren't generated in Nix are validated).
240+- artifact/other file URLs and associated hashes (Nix will complain if
241+ the hash doesn't match, and Gradle won't even access the URL if it
242+ doesn't match)
243+244+Please be mindful of the above when working on Gradle support for
245+nixpkgs.
···1+import json
2+import sys
3+4+from typing import Dict, Set
5+6+# this compresses MITM URL lists with Gradle-specific optimizations
7+# specifically, it splits each url into up to 3 parts - they will be
8+# concatenated like part1/part2.part3 or part1.part2
9+# part3 is simply always the file extension, but part1 and part2 is
10+# optimized using special heuristics
11+# additionally, if part2 ends with /a/b/{a}-{b}, the all occurences of
12+# /{a}/{b}/ are replaced with #
13+# finally, anything that ends with = is considered SHA256, anything that
14+# starts with http is considered a redirect URL, anything else is
15+# considered text
16+17+with open(sys.argv[1], "rt") as f:
18+ data: dict = json.load(f)
19+20+new_data: Dict[str, Dict[str, Dict[str, dict]]] = {}
21+22+for url, info in data.items():
23+ if url == "!version":
24+ continue
25+ ext, base = map(lambda x: x[::-1], url[::-1].split(".", 1))
26+ if base.endswith(".tar"):
27+ base = base[:-4]
28+ ext = "tar." + ext
29+ # special logic for Maven repos
30+ if ext in ["jar", "pom", "module"]:
31+ comps = base.split("/")
32+ if "-" in comps[-1]:
33+ # convert base/name/ver/name-ver into base#name/ver
34+35+ filename = comps[-1]
36+ name = comps[-3]
37+ basever = comps[-2]
38+ ver = basever
39+ is_snapshot = ver.endswith("-SNAPSHOT")
40+ if is_snapshot:
41+ ver = ver.removesuffix("-SNAPSHOT")
42+ if filename.startswith(f"{name}-{ver}"):
43+ if is_snapshot:
44+ if filename.startswith(f"{name}-{ver}-SNAPSHOT"):
45+ ver += "-SNAPSHOT"
46+ else:
47+ ver += "-".join(
48+ filename.removeprefix(f"{name}-{ver}").split("-")[:3]
49+ )
50+ comp_end = comps[-1].removeprefix(f"{name}-{ver}")
51+ else:
52+ ver, name, comp_end = None, None, None
53+ if name and ver and (not comp_end or comp_end.startswith("-")):
54+ base = "/".join(comps[:-1]) + "/"
55+ base = base.replace(f"/{name}/{basever}/", "#")
56+ base += f"{name}/{ver}"
57+ if is_snapshot:
58+ base += "/SNAPSHOT"
59+ if comp_end:
60+ base += "/" + comp_end[1:]
61+ scheme, rest = base.split("/", 1)
62+ if scheme not in new_data.keys():
63+ new_data[scheme] = {}
64+ if rest not in new_data[scheme].keys():
65+ new_data[scheme][rest] = {}
66+ if "hash" in info.keys():
67+ new_data[scheme][rest][ext] = info["hash"]
68+ elif "text" in info.keys() and ext == "xml":
69+ # nix code in fetch-deps.nix will autogenerate metadata xml files groupId
70+ # is part of the URL, but it can be tricky to parse as we don't know the
71+ # exact repo base, so take it from the xml and pass it to nix
72+ xml = "".join(info["text"].split())
73+ new_data[scheme][rest][ext] = {
74+ "groupId": xml.split("<groupId>")[1].split("</groupId>")[0],
75+ }
76+ if "<release>" in xml:
77+ new_data[scheme][rest][ext]["release"] = xml.split("<release>")[1].split(
78+ "</release>"
79+ )[0]
80+ if "<latest>" in xml:
81+ latest = xml.split("<latest>")[1].split("</latest>")[0]
82+ if latest != new_data[scheme][rest][ext].get("release"):
83+ new_data[scheme][rest][ext]["latest"] = latest
84+ if "<lastUpdated>" in xml:
85+ new_data[scheme][rest][ext]["lastUpdated"] = xml.split("<lastUpdated>")[
86+ 1
87+ ].split("</lastUpdated>")[0]
88+ else:
89+ raise Exception("Unsupported key: " + repr(info))
90+91+# At this point, we have a map by part1 (initially the scheme), part2 (initially a
92+# slash-separated string without the scheme and with potential # substitution as
93+# seen above), extension.
94+# Now, push some segments from "part2" into "part1" like this:
95+# https # part1
96+# domain1/b # part2
97+# domain1/c
98+# domain2/a
99+# domain2/c
100+# ->
101+# https/domain1 # part1
102+# b # part2
103+# c
104+# https/domain2 # part1
105+# a # part2
106+# c
107+# This helps reduce the lockfile size because a Gradle project will usually use lots
108+# of files from a single Maven repo
109+110+data = new_data
111+changed = True
112+while changed:
113+ changed = False
114+ new_data = {}
115+ for part1, info1 in data.items():
116+ starts: Set[str] = set()
117+ # by how many bytes the file size will be increased (roughly)
118+ lose = 0
119+ # by how many bytes the file size will be reduced (roughly)
120+ win = 0
121+ # how many different initial part2 segments there are
122+ count = 0
123+ for part2, info2 in info1.items():
124+ if "/" not in part2:
125+ # can't push a segment from part2 into part1
126+ count = 0
127+ break
128+ st = part2.split("/", 1)[0]
129+ if st not in starts:
130+ lose += len(st) + 1
131+ count += 1
132+ starts.add(st)
133+ win += len(st) + 1
134+ if count == 0:
135+ new_data[part1] = info1
136+ continue
137+ # only allow pushing part2 segments into path1 if *either*:
138+ # - the domain isn't yet part of part1
139+ # - the initial part2 segment is always the same
140+ if count != 1 and "." in part1:
141+ new_data[part1] = info1
142+ continue
143+ # some heuristics that may or may not work well (originally this was
144+ # used when the above if wasn't here, but perhaps it's useless now)
145+ lose += (count - 1) * max(0, len(part1) - 4)
146+ if win > lose or ("." not in part1 and win >= lose):
147+ changed = True
148+ for part2, info2 in info1.items():
149+ st, part3 = part2.split("/", 1)
150+ new_part1 = part1 + "/" + st
151+ if new_part1 not in new_data.keys():
152+ new_data[new_part1] = {}
153+ new_data[new_part1][part3] = info2
154+ else:
155+ new_data[part1] = info1
156+ data = new_data
157+158+new_data["!comment"] = "This is a nixpkgs Gradle dependency lockfile. For more details, refer to the Gradle section in the nixpkgs manual." # type: ignore
159+new_data["!version"] = 1 # type: ignore
160+161+with open(sys.argv[2], "wt") as f:
162+ json.dump(new_data, f, sort_keys=True, indent=1)
163+ f.write("\n")