#+STARTUP: inlineimages

[[./assets/gradle2nix.png]]

Generate [[https://nixos.org/nix/][Nix]] expressions which build
[[https://gradle.org/][Gradle]]-based projects.

** Table of contents

#+BEGIN_QUOTE
- [[#why][Why?]]
- [[#installation][Installation]]
  - [[#flake][Flake]]
- [[#usage][Usage]]
  - [[#for-packagers][For packagers]]
  - [[#specifying-the-gradle-installation][Specifying the Gradle installation]]
- [[#reference][Reference]]
  - [[#buildgradlepackage][buildGradlePackage]]
  - [[#buildmavenrepo][buildMavenRepo]]
  - [[#gradlesetuphook][gradleSetupHook]]
- [[#contributing][Contributing]]
- [[#license][License]]
#+END_QUOTE

** Why?

Nix is an OS-agnostic package manager, a language-agnostic build
system, and a bespoke programming language. One of its unique features
is that it is purely functional; a "package" is a function which
accepts inputs (source code, configuration, etc) and produces an
output (binaries, a Java JAR, documentation, really anything).

One benefit of a functional build system is
[[https://reproducible-builds.org/][reproducibility]]. If you
specify your inputs precisely, and take care not to introduce
impurities—such as files retrieved over a network without tracking
their content—you will receive, byte-for-byte, the exact output as
someone else running the same function over the same inputs.

Gradle is not a functional build system. Most Gradle-based projects
will produce highly variable outputs depending on a host of impure
inputs, including:

- The JVM hosting the build
- The Gradle installation running the build
- Any usage of dynamic version constraints for dependencies
- SNAPSHOT dependencies
- Environment variables and command-line options
- Artifacts cached on the system hosting the build

=gradle2nix= helps to solve this problem by leveraging Nix to
control the most common inputs to a Gradle build. When run on a
project, it will record all dependencies for both the build
environment (including =plugins= and =buildscript= blocks) and the
project, and provide a Nix expression to run the build given these
dependencies. The build itself is then run in a sandbox, where only
content-tracked network requests are allowed to fetch dependencies,
and a local Maven repository is created on-the-fly to host the
dependency artifacts somewhere Gradle can resolve them without a
network.

This tool is useful for both development and packaging. You can use
=gradle2nix= to:

- Create isolated and reproducible development environments that
  work anywhere Nix itself can run.
- Reduce or eliminate flakiness and maintenance headaches from CI/CD
  pipelines.
- Distribute a recipe which can reliably build a Gradle project in
  repositories such as the [[https://nixos.org/nixpkgs/][Nix Package
  Collection]].

** Installation

A [[./gradle.nix][Nix expression]] (generated by =gradle2nix=
itself) is provided for convenience. The following expression will
fetch and build the latest version of this package:

#+begin_src nix
import (fetchTarball "https://github.com/tadfisher/gradle2nix/archive/master.tar.gz")  {}
#+end_src

If this expression is in, say, =gradle2nix.nix=, =gradle2nix= can be
built and placed in =./result= with the following:

#+begin_example
nix build -f gradle2nix.nix
#+end_example

You can also use the following one-liners to build or install
=gradle2nix= in your user profile:

#+begin_example
# Build and place in ./result/
nix build -f "https://github.com/tadfisher/gradle2nix/archive/master.tar.gz"

# Build and install in the user profile
nix-env -if "https://github.com/tadfisher/gradle2nix/archive/master.tar.gz"
#+end_example

=gradle2nix= is not yet packaged in =nixpkgs= itself, but work is
[[https://github.com/NixOS/nixpkgs/pull/77422][in progress]].

The [[./nix/build-gradle-package.nix][buildGradlePackage]] function
is provided via the =gradle2nix.passthru.buildGradlePackage=
attribute.

#+begin_src nix
{ pkgs ? import <nixpkgs> {} }:

let
  gradle2nix = import (fetchTarball "https://github.com/tadfisher/gradle2nix/archive/master.tar.gz")  {}
in
gradle2nix.buildGradlePackage {
  pname = "my-package";
  version = "1.0";
  lockFile = ./gradle.lock;
  gradleInstallFlags = [ "installDist" ];
  # ...
}
#+end_src

*** Flake

A [[./flake.nix][flake.nix]] is provided for those using
[[https://nixos.wiki/wiki/Flakes][Nix flakes]]. For example, the
following will build and run =gradle2nix= with the arguments
provided after =--=:

#+begin_example
nix run github:tadfisher/gradle2nix -- --help
#+end_example

The [[./nix/build-gradle-package.nix][buildGradlePackage]] function
is provided via the =builders= output.

#+begin_src nix
{
  inputs.gradle2nix.url = "github:tadfisher/gradle2nix";

  outputs = { self, gradle2nix }: {

    packages.x86_64-linux.default = gradle2nix.builders.x86_64-linux.buildGradlePackage {
      pname = "my-package";
      version = "1.0";
      lockFile = ./gradle.lock;
      gradleInstallFlags = [ "installDist" ];
      # ...
    };
  };
}
#+end_src

** Usage

#+begin_example
Usage: gradle2nix [<options>] [<args>]...

Gradle installation:

  Where to find Gradle. By default, use the project's wrapper.

  --gradle-dist=<uri>       Gradle distribution URI
  --gradle-home=<dir>       Gradle home path (e.g. `nix eval --raw nixpkgs#gradle.outPath`/lib/gradle)
  --gradle-wrapper=<value>  Gradle wrapper version

Options:
  -t, --task=<task>              Gradle tasks to run (default: [resolveAllArtifacts])
  -a, --artifacts=<artifacts>    Comma-separated list of artifacts to download (artifacts:
                                 doxygen,javadoc,samples,sources,usermanual) (default: [])
  -p, --project=<path>           Path to the project root (default: Current directory)
  -o, --out-dir=<dir>            Path to write generated files (default: <project>)
  -l, --lock-file=<filename>     Name of the generated lock file (default: gradle.lock)
  -j, --gradle-jdk=<dir>         JDK home to use for launching Gradle (e.g. `nix eval --raw
                                 nixpkgs#openjdk.home`)
  --log=(debug|info|warn|error)  Print messages with this priority or higher (default: info)
  --dump-events                  Dump Gradle event logs to the output directory
  --stacktrace                   Print a stack trace on error
  -h, --help                     Show this message and exit

Arguments:
  <args>  Extra arguments to pass to Gradle
#+end_example

Simply running =gradle2nix= in the root directory of a project
should be enough for most projects. This will produce a lock file,
by default called =gradle.lock=, which contains the pinned
dependencies for the project. An example of a build expression using
this lock file can be found in this project's
[[./default.nix][default.nix]].

*** For packagers

If you're creating a Nix package for an existing Gradle project, you
can reduce the number of pinned dependencies by passing one or more
=--task= arguments. This will only pin the dependencies that were
resolved as part of the build, instead of the default behavior where
all possible dependencies are pinned.

For example, if the package produces its build output via the
=:app:installDist= task, use the following:

#+begin_example
gradle2nix -t :app:installDist
#+end_example

/Note:/ This may be *required* if the build resolves configurations
at execution time.

*** Specifying the Gradle installation

By default, if the project has configured the Gradle wrapper, it will
be used; otherwise, the version of Gradle used to build gradle2nix
will be used. You can override this to use any of the following:

#+begin_example
# Gradle distribution URL:
gradle2nix --gradle-dist='https://services.gradle.org/distributions/gradle-8.7-bin.zip'

# Path to a local Gradle installation:
gradle2nix --gradle-home=`nix eval nixpkgs#gradle.outPath`/lib/gradle

# A specific wrapper version:
gradle2nix --gradle-wrapper=8.7
#+end_example

** Reference

*** =buildGradlePackage=

This function is a convenience wrapper around =stdenv.mkDerivation=
that simplifies building Gradle projects with the lock files
produced by =gradle2nix=. It performs the following:

1. Applies [[#gradlesetuphook][gradleSetupHook]], overriding the
  required =gradle= package if specified.
2. Builds the offline Maven repository with
  [[#buildmavenrepo][buildMavenRepo]].
3. Sets the JDK used to run Gradle if specified.
4. Applies the offline repo to the Gradle build using an
  initialization script.

- Source:
  [[./nix/build-gradle-package.nix][build-gradle-package.nix]]
- Location:
  - Nix :: =gradle2nix.passthru.buildGradlePackage=
  - Flake :: =builders.${system}.buildGradlePackage=

**** Arguments to =buildGradlePackage=

- =lockFile= :: Path to the lock file generated by =gradle2nix=
  (e.g. =gradle.lock=).
- =gradle= :: The Gradle package to use. Default is
  =pkgs.gradle=.
- =buildJdk= :: Override the default JDK used to run Gradle itself.
- =fetchers= :: Override functions which fetch dependency
  artifacts.

  See [[#fetchers][detailed documentation]] below.
- =overrides= :: Override artifacts in the offline Maven repository.

  See [[#overrides][detailed documentation]] below.

In addition, this function accepts:
- All arguments to =stdenv.mkDerivation=.
- Arguments specific to =gradleSetupHook= (see
  [[#gradlesetuphook][below]]).

*** =buildMavenRepo=

This function takes a lock file and produces a derivation which
downloads all dependencies into a Maven local repository. The
derivation provides a passthru =gradleInitScript= attribute, which
is a Gradle initialization script that can be applied using =gradle
--init-script== or placed in =$GRADLE_USER_HOME/init.d=. The init
script replaces all repositories referenced in the project with the
local repository.

- Source: [[./nix/build-maven-repo.nix][build-maven-repo.nix]]
- Location:
  - Nix :: =gradle2nix.passthru.buildMavenRepo=
  - Flake :: =builders.${system}.buildMavenRepo=

**** Arguments to =buildMavenRepo=
- =lockFile= :: Path to the lock file generated by gradle2nix (e.g.
  =gradle.lock=).
- =fetchers= :: Override functions which fetch dependency
  artifacts.

  See [[#fetchers][detailed documentation]] below.
- =overrides= :: Override artifacts in the offline Maven repository.

  See [[#overrides][detailed documentation]] below.

*** =gradleSetupHook=

A
[[https://nixos.org/manual/nixpkgs/unstable/#ssec-setup-hooks][setup
hook]] to simplify building Gradle packages. Overrides the default
configure, build, check, and install phases.

To use, add =gradleSetupHook= to a derivation's =nativeBuildInputs=.

- Source: [[./nix/setup-hook.sh][setup-hook.sh]]
- Location:
  - Nix :: =gradle2nix.passthru.gradleSetupHook=
  - Flake :: =packages.${system}.gradleSetupHook=

**** Variables controlling =gradleSetupHook=
- =gradleInitScript= :: Path to an
  [[https://docs.gradle.org/current/userguide/init_scripts.html][initialization
  script]] used by =gradle= during all phases.
- =gradleFlags= :: Controls the arguments passed to =gradle= during
  all phases.
- =gradleBuildFlags= :: Controls the arguments passed to =gradle=
  during the build phase. The build phase is skipped if this is
  unset.
- =gradleCheckFlags= :: Controls the arguments passed to =gradle=
  during the check phase. The check phase is skipped if this is
  unset.
- =gradleInstallFlags= :: Controls the arguments passed to =gradle=
  during the install phase. This install phase is skipped if this is
  unset.
- =dontUseGradleConfigure= :: When set to true, don't use the
  predefined =gradleConfigurePhase=. This will also disable the use
  of =gradleInitScript=.
- =dontUseGradleCheck= :: When set to true, don't use the predefined
  =gradleCheckPhase=.
- =dontUseGradleInstall= :: When set to true, don't use the
  predefined =gradleInstallPhase=.

**** Honored variables

The following variables commonly used by =stdenv.mkDerivation= are
honored by =gradleSetupHook=.

- =enableParallelBuilding=
- =enableParallelChecking=
- =enableParallelInstalling=

*** Common arguments
**** =fetchers=

Names in this set are URL schemes such as "https" or "s3". Values
are functions which take an artifact in the form ={ url, hash }=
and fetches it into the Nix store. For example:

#+begin_src nix
{
  s3 = { name, url, hash }: fetchs3 {
    s3url = url;
    # TODO This doesn't work without patching fetchs3 to accept SRI hashes
    inherit name hash;
    region = "us-west-2";
    credentials = {
      access_key_id = "foo";
      secret_access_key = "bar";
    };
  };
}
#+end_src

**** =overrides=

This is an attrset of the form:

#+begin_src nix
{
  "${group}:${module}:${version}" = {
    "${filename}" = <override function>;
  }
}
#+end_src

The override function takes the original derivation from 'fetchers'
(e.g. the result of 'fetchurl') and produces a new derivation to
replace it.

- Replace a dependency's JAR artifact:

  #+begin_src nix
  {
    "com.squareup.okio:okio:3.9.0"."okio-3.9.0.jar" = _: fetchurl {
      url = "https://repo.maven.apache.org/maven2/com/squareup/okio/okio/3.9.0/okio-3.9.0.jar";
      hash = "...";
      downloadToTemp = true;
      postFetch = "install -Dt $out/com/squareup/okio/okio/3.9.0/ $downloadedFile"
    };
  }
  #+end_src

- Patch a JAR containing native binaries:

  #+begin_src nix
  {
    "com.android.tools.build:aapt2:8.5.0-rc02-11315950" = {
      "aapt2-8.5.0-rc02-11315950-linux.jar" = src: runCommandCC src.name {
        nativeBuildInputs = [ jdk autoPatchelfHook ];
        dontAutoPatchelf = true;
      } ''
        cp ${src} aapt2.jar
        jar xf aapt2.jar aapt2
        chmod +x aapt2
        autoPatchelf aapt2
        jar uf aapt2.jar aapt2
        cp aapt2.jar $out
      '';
    };
  }
  #+end_src

** Contributing

Bug reports and feature requests are encouraged.

[[https://github.com/tadfisher/gradle2nix/issues/new][Create an
issue]]

Code contributions are also encouraged. Please review the test cases
in the [[./fixtures][fixtures]] directory and create a new one to
reproduce any fixes or test new features. See the
[[./app/src/test/kotlin/org/nixos/gradle2nix/GoldenTest.kt][existing
tests]] for examples of testing with these fixtures.

** License

=gradle2nix= is licensed under the [[./COPYING][Apache License 2.0]].
