diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 896149e..91fa305 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -34,7 +34,7 @@ application { "-Dslf4j.internal.verbosity=ERROR" ) applicationDistribution - .from(configurations.named("share")) + .from(configurations.named("share"), files("../gradle.nix")) .into("share") .rename("plugin.*\\.jar", "plugin.jar") } diff --git a/app/src/dist/share/gradle.nix b/app/src/dist/share/gradle.nix deleted file mode 100644 index 99d1c31..0000000 --- a/app/src/dist/share/gradle.nix +++ /dev/null @@ -1,290 +0,0 @@ -# This file is generated by gradle2nix. -# -# Example usage (e.g. in default.nix): -# -# with (import {}); -# let -# buildGradle = callPackage ./gradle.nix {}; -# in -# buildGradle { -# lockFile = ./gradle.lock; -# -# src = ./.; -# -# gradleFlags = [ "installDist" ]; -# -# installPhase = '' -# mkdir -p $out -# cp -r app/build/install/myproject $out -# ''; -# } - -{ lib -, stdenv -, buildEnv -, fetchs3 -, fetchurl -, gradle -, maven -, runCommandLocal -, symlinkJoin -, writeText -, writeTextDir -}: - -{ - # Path to the lockfile generated by gradle2nix (e.g. gradle.lock). - lockFile -, pname ? "project" -, version ? null -, enableParallelBuilding ? true -# Arguments to Gradle used to build the project in buildPhase. -, gradleFlags ? [ "build" ] -# Enable debugging for the Gradle build; this will cause Gradle to run a debug server -# and wait for a JVM debugging client to attach. -, enableDebug ? false -# Additional code to run in the Gradle init script (init.gradle). -, extraInit ? "" -# Override the default JDK used to run Gradle itself. -, buildJdk ? null -# Override functions which fetch dependency artifacts. -# Keys in this set are URL schemes such as "https" or "s3". -# Values are functions which take a dependency in the form -# `{ urls, hash }` and fetch into the Nix store. For example: -# -# { -# s3 = { name, urls, hash }: fetchs3 { -# s3url = builtins.head urls; -# # 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"; -# }; -# }; -# } -, fetchers ? { } -, ... } @ args: - -let - inherit (builtins) - attrValues concatStringsSep elemAt filter fromJSON getAttr hasAttr head length match - removeAttrs replaceStrings sort; - - inherit (lib) - assertMsg concatMapStringsSep findFirst foldl' groupBy' hasSuffix hasPrefix last mapAttrs - mapAttrsToList optionalAttrs optionalString readFile removeSuffix unique versionAtLeast - versionOlder; - - inherit (lib.strings) sanitizeDerivationName; - - lockedDeps = fromJSON (readFile lockFile); - - toCoordinates = id: - let - coords = builtins.split ":" id; - in rec { - group = elemAt coords 0; - module = elemAt coords 2; - version = elemAt coords 4; - versionParts = parseVersion version; - }; - - parseVersion = version: - let - parts = builtins.split ":" version; - base = elemAt parts 0; - in - { - inherit base; - exact = base; - } - // optionalAttrs (length parts >= 2) ( - let - snapshot = elemAt parts 2; - exact = replaceStrings [ "-SNAPSHOT" ] [ "-${snapshot}" ] base; - parts = builtins.split "-" timestamp; - timestamp = findFirst (match "[0-9]{8}\.[0-9]{6}") parts; - buildNumber = let lastPart = last parts; in if match "[0-9]+" lastPart then lastPart else null; - in - { inherit snapshot exact timestamp buildNumber; } - ); - - fetchers' = { - http = fetchurl; - https = fetchurl; - } // fetchers; - - # Fetch urls using the scheme for the first entry only; there isn't a - # straightforward way to tell Nix to try multiple fetchers in turn - # and short-circuit on the first successful fetch. - fetch = name: { url, hash }: - let - scheme = head (builtins.match "([a-z0-9+.-]+)://.*" url); - fetch' = getAttr scheme fetchers'; - in - fetch' { inherit url hash; }; - - mkModule = id: artifacts: - let - coords = toCoordinates id; - modulePath = "${replaceStrings ["."] ["/"] coords.group}/${coords.module}/${coords.version}"; - in - stdenv.mkDerivation { - pname = sanitizeDerivationName "${coords.group}-${coords.module}"; - version = coords.versionParts.exact; - - srcs = mapAttrsToList fetch artifacts; - - dontPatch = true; - dontConfigure = true; - dontBuild = true; - dontFixup = true; - dontInstall = true; - - preUnpack = '' - mkdir -p "$out/${modulePath}" - ''; - - unpackCmd = '' - cp "$curSrc" "$out/${modulePath}/$(stripHash "$curSrc")" - ''; - - sourceRoot = "."; - - preferLocalBuild = true; - allowSubstitutes = false; - }; - - offlineRepo = symlinkJoin { - name = if version != null then "${pname}-${version}-gradle-repo" else "${pname}-gradle-repo"; - paths = mapAttrsToList mkModule lockedDeps; - }; - - initScript = - let - inSettings = pred: script: - optionalString pred ( - if versionAtLeast gradle.version "6.0" then '' - gradle.beforeSettings { - ${script} - } - '' else '' - gradle.settingsEvaluated { - ${script} - } - '' - ); - in - writeText "init.gradle" '' - static def offlineRepo(RepositoryHandler repositories) { - repositories.clear() - repositories.mavenLocal { - url 'file:${offlineRepo}' - metadataSources { - gradleMetadata() - mavenPom() - artifact() - } - } - } - - ${inSettings (versionAtLeast gradle.version "6.0") '' - offlineRepo(it.buildscript.repositories) - ''} - - ${inSettings true '' - offlineRepo(it.pluginManagement.repositories) - ''} - - gradle.projectsLoaded { - allprojects { - buildscript { - offlineRepo(repositories) - } - } - } - - ${if versionAtLeast gradle.version "6.8" - then '' - gradle.beforeSettings { - it.dependencyResolutionManagement { - offlineRepo(repositories) - repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) - } - } - '' - else '' - gradle.projectsLoaded { - allprojects { - offlineRepo(repositories) - } - } - '' - } - - ${extraInit} - ''; - - buildProject = flags: '' - gradle --offline --no-daemon --no-build-cache \ - --info --full-stacktrace --warning-mode=all \ - --no-configuration-cache \ - -Dmaven.repo.local=${offlineRepo} \ - ${optionalString enableParallelBuilding "--parallel"} \ - ${optionalString enableDebug "-Dorg.gradle.debug=true"} \ - ${optionalString (buildJdk != null) "-Dorg.gradle.java.home=${buildJdk.home}"} \ - --init-script ${initScript} \ - ${concatStringsSep " " flags} - ''; - -in stdenv.mkDerivation ({ - - dontStrip = true; - - nativeBuildInputs = (args.nativeBuildInputs or []) ++ [ gradle ]; - - buildPhase = args.buildPhase or '' - runHook preBuild - - ( - set -eux - - ${optionalString (versionOlder gradle.version "8.0") '' - # Work around https://github.com/gradle/gradle/issues/1055 - TMPHOME="$(mktemp -d)" - mkdir -p "$TMPHOME/init.d" - export GRADLE_USER_HOME="$TMPHOME" - cp ${initScript} $TMPHOME/ - ''} - - gradle --offline --no-daemon --no-build-cache \ - --info --full-stacktrace --warning-mode=all \ - --no-configuration-cache --console=plain \ - -Dmaven.repo.local=${offlineRepo} \ - ${optionalString enableParallelBuilding "--parallel"} \ - ${optionalString enableDebug "-Dorg.gradle.debug=true"} \ - ${optionalString (buildJdk != null) "-Dorg.gradle.java.home=${buildJdk.home}"} \ - --init-script ${initScript} \ - ${concatStringsSep " " gradleFlags} - ) - - runHook postBuild - ''; - - passthru = (args.passthru or {}) // { - inherit offlineRepo; - }; - -} // (removeAttrs args [ - "nativeBuildInputs" - "passthru" - "lockFile" - "gradleFlags" - "gradle" - "enableDebug" - "extraInit" - "buildJdk" - "fetchers" -])) diff --git a/default.nix b/default.nix index 6e568fb..4a79742 100644 --- a/default.nix +++ b/default.nix @@ -33,8 +33,18 @@ let ''; passthru = { + build = buildGradle; plugin = "${gradle2nix}/share/plugin.jar"; }; + + meta = with lib; { + inherit (gradle.meta) platforms; + description = "Wrap Gradle builds with Nix"; + homepage = "https://github.com/tadfisher/gradle2nix"; + license = licenses.asl20; + maintainers = with maintainers; [ tadfisher ]; + mainProgram = "gradle2nix"; + }; }; in gradle2nix diff --git a/flake.lock b/flake.lock index cda6dde..3b7699d 100644 --- a/flake.lock +++ b/flake.lock @@ -1,19 +1,5 @@ { "nodes": { - "flake-compat": { - "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", - "revCount": 57, - "type": "tarball", - "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz" - }, - "original": { - "type": "tarball", - "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz" - } - }, "flake-utils": { "inputs": { "systems": "systems" @@ -50,7 +36,6 @@ }, "root": { "inputs": { - "flake-compat": "flake-compat", "flake-utils": "flake-utils", "nixpkgs": "nixpkgs" } diff --git a/flake.nix b/flake.nix index fac545a..1ccebb5 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,6 @@ description = "Wrap Gradle builds with Nix"; inputs = { - flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"; flake-utils.url = "github:numtide/flake-utils"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable-small"; }; @@ -13,7 +12,7 @@ pkgs = nixpkgs.legacyPackages.${system}; in { - packages.default = pkgs.callPackage ./gradle2nix.nix {}; + packages.default = pkgs.callPackage ./default.nix {}; apps.default = { type = "app"; diff --git a/gradle.nix b/gradle.nix index 99d1c31..509a3e5 100644 --- a/gradle.nix +++ b/gradle.nix @@ -32,16 +32,20 @@ , writeTextDir }: +let defaultGradle = gradle; in + { # Path to the lockfile generated by gradle2nix (e.g. gradle.lock). - lockFile + lockFile ? null , pname ? "project" , version ? null , enableParallelBuilding ? true +# The Gradle package to use. Default is 'pkgs.gradle'. +, gradle ? defaultGradle # Arguments to Gradle used to build the project in buildPhase. , gradleFlags ? [ "build" ] -# Enable debugging for the Gradle build; this will cause Gradle to run a debug server -# and wait for a JVM debugging client to attach. +# Enable debugging for the Gradle build; this will cause Gradle to run +# a debug server and wait for a JVM debugging client to attach. , enableDebug ? false # Additional code to run in the Gradle init script (init.gradle). , extraInit ? "" @@ -65,52 +69,79 @@ # }; # } , fetchers ? { } +# Overlays for dependencies in the offline Maven repository. +# +# Acceps an attrset of dependencies (usually parsed from 'lockFile'), and produces an attrset +# containing dependencies to merge into the final set. +# +# The attrset is of the form: +# +# { +# "${group}:${module}:${version}" = ; +# # ... +# } +# +# A dependency derivation unpacks multiple source files into a single Maven-style directory named +# "${out}/${groupPath}/${module}/${version}/", where 'groupPath' is the dependency group ID with dot +# characters ('.') replaced by the path separator ('/'). +# +# Examples: +# +# 1. Add or replace a dependency with a single JAR file: +# +# (_: { +# "com.squareup.okio:okio:3.9.0" = fetchurl { +# url = "https://repo.maven.apache.org/maven2/com/squareup/okio/okio/3.9.0/okio-3.9.0.jar"; +# hash = "..."; +# downloadToTemmp = true; +# postFetch = "install -Dt $out/com/squareup/okio/okio/3.9.0/ $downloadedFile" +# }; +# }) +# +# 2. Remove a dependency entirely: +# +# # This works because the result is filtered for values that are derivations. +# (_: { +# "org.apache.log4j:core:2.23.1" = null; +# }) +, overlays ? [] , ... } @ args: let inherit (builtins) - attrValues concatStringsSep elemAt filter fromJSON getAttr hasAttr head length match - removeAttrs replaceStrings sort; + attrValues concatStringsSep elemAt filter fromJSON getAttr head length mapAttrs removeAttrs + replaceStrings; inherit (lib) - assertMsg concatMapStringsSep findFirst foldl' groupBy' hasSuffix hasPrefix last mapAttrs - mapAttrsToList optionalAttrs optionalString readFile removeSuffix unique versionAtLeast - versionOlder; + mapAttrsToList optionalString readFile versionAtLeast versionOlder; inherit (lib.strings) sanitizeDerivationName; - lockedDeps = fromJSON (readFile lockFile); - toCoordinates = id: let coords = builtins.split ":" id; + + parseVersion = version: + let + parts = builtins.split ":" version; + base = elemAt parts 0; + in + if length parts >= 2 + then + let + snapshot = elemAt parts 2; + in + replaceStrings [ "-SNAPSHOT" ] [ "-${snapshot}" ] base + else + base; + in rec { group = elemAt coords 0; module = elemAt coords 2; version = elemAt coords 4; - versionParts = parseVersion version; + uniqueVersion = parseVersion version; }; - parseVersion = version: - let - parts = builtins.split ":" version; - base = elemAt parts 0; - in - { - inherit base; - exact = base; - } - // optionalAttrs (length parts >= 2) ( - let - snapshot = elemAt parts 2; - exact = replaceStrings [ "-SNAPSHOT" ] [ "-${snapshot}" ] base; - parts = builtins.split "-" timestamp; - timestamp = findFirst (match "[0-9]{8}\.[0-9]{6}") parts; - buildNumber = let lastPart = last parts; in if match "[0-9]+" lastPart then lastPart else null; - in - { inherit snapshot exact timestamp buildNumber; } - ); - fetchers' = { http = fetchurl; https = fetchurl; @@ -133,7 +164,7 @@ let in stdenv.mkDerivation { pname = sanitizeDerivationName "${coords.group}-${coords.module}"; - version = coords.versionParts.exact; + version = coords.uniqueVersion; srcs = mapAttrsToList fetch artifacts; @@ -157,9 +188,30 @@ let allowSubstitutes = false; }; + # Intermediate dependency spec. + # + # We want to allow overriding dependencies via the 'dependencies' function, + # so we pass an intermediate set that maps each Maven coordinate to the + # derivation created with 'mkModule'. This allows users extra flexibility + # to do things like patching native libraries with patchelf or replacing + # artifacts entirely. + lockedDependencies = final: if lockFile == null then {} else + let + lockedDependencySpecs = fromJSON (readFile lockFile); + in mapAttrs mkModule lockedDependencySpecs; + + + finalDependencies = + let + composedExtension = lib.composeManyExtensions overlays; + extended = lib.extends composedExtension lockedDependencies; + fixed = lib.fix extended; + in + filter lib.isDerivation (attrValues fixed); + offlineRepo = symlinkJoin { name = if version != null then "${pname}-${version}-gradle-repo" else "${pname}-gradle-repo"; - paths = mapAttrsToList mkModule lockedDeps; + paths = finalDependencies; }; initScript = @@ -227,64 +279,53 @@ let ${extraInit} ''; - buildProject = flags: '' - gradle --offline --no-daemon --no-build-cache \ - --info --full-stacktrace --warning-mode=all \ - --no-configuration-cache \ - -Dmaven.repo.local=${offlineRepo} \ - ${optionalString enableParallelBuilding "--parallel"} \ - ${optionalString enableDebug "-Dorg.gradle.debug=true"} \ - ${optionalString (buildJdk != null) "-Dorg.gradle.java.home=${buildJdk.home}"} \ - --init-script ${initScript} \ - ${concatStringsSep " " flags} - ''; -in stdenv.mkDerivation ({ + buildGradle = stdenv.mkDerivation (finalAttrs: { - dontStrip = true; + inherit buildJdk enableParallelBuilding enableDebug gradle gradleFlags pname version; - nativeBuildInputs = (args.nativeBuildInputs or []) ++ [ gradle ]; + dontStrip = true; - buildPhase = args.buildPhase or '' - runHook preBuild + nativeBuildInputs = [ finalAttrs.gradle ] + ++ lib.optional (finalAttrs.buildJdk != null) finalAttrs.buildJdk; - ( - set -eux + buildPhase = '' + runHook preBuild - ${optionalString (versionOlder gradle.version "8.0") '' - # Work around https://github.com/gradle/gradle/issues/1055 - TMPHOME="$(mktemp -d)" - mkdir -p "$TMPHOME/init.d" - export GRADLE_USER_HOME="$TMPHOME" - cp ${initScript} $TMPHOME/ - ''} + ( + set -eux - gradle --offline --no-daemon --no-build-cache \ - --info --full-stacktrace --warning-mode=all \ - --no-configuration-cache --console=plain \ - -Dmaven.repo.local=${offlineRepo} \ - ${optionalString enableParallelBuilding "--parallel"} \ - ${optionalString enableDebug "-Dorg.gradle.debug=true"} \ - ${optionalString (buildJdk != null) "-Dorg.gradle.java.home=${buildJdk.home}"} \ - --init-script ${initScript} \ - ${concatStringsSep " " gradleFlags} - ) + ${optionalString (versionOlder finalAttrs.gradle.version "8.0") '' + # Work around https://github.com/gradle/gradle/issues/1055 + TMPHOME="$(mktemp -d)" + mkdir -p "$TMPHOME/init.d" + export GRADLE_USER_HOME="$TMPHOME" + cp ${initScript} $TMPHOME/ + ''} - runHook postBuild - ''; + gradle --offline --no-daemon --no-build-cache \ + --info --full-stacktrace --warning-mode=all \ + --no-configuration-cache --console=plain \ + -Dmaven.repo.local=${offlineRepo} \ + ${optionalString finalAttrs.enableParallelBuilding "--parallel"} \ + ${optionalString finalAttrs.enableDebug "-Dorg.gradle.debug=true"} \ + ${optionalString (finalAttrs.buildJdk != null) "-Dorg.gradle.java.home=${finalAttrs.buildJdk.home}"} \ + --init-script ${initScript} \ + ${concatStringsSep " " finalAttrs.gradleFlags} + ) - passthru = (args.passthru or {}) // { - inherit offlineRepo; - }; + runHook postBuild + ''; -} // (removeAttrs args [ - "nativeBuildInputs" - "passthru" - "lockFile" - "gradleFlags" - "gradle" - "enableDebug" - "extraInit" - "buildJdk" - "fetchers" -])) + passthru = { + inherit offlineRepo; + }; + } // removeAttrs args [ + "lockFile" + "extraInit" + "fetchers" + "overlays" + ]); + +in +buildGradle diff --git a/gradle2nix.nix b/gradle2nix.nix deleted file mode 100644 index 428d913..0000000 --- a/gradle2nix.nix +++ /dev/null @@ -1,46 +0,0 @@ -{ lib -, callPackage -, gradle -}: - -let - buildGradle = callPackage ./gradle.nix { - inherit gradle; - }; -in -buildGradle { - pname = "gradle2nix"; - version = "2.0.0"; - lockFile = ./gradle.lock; - - src = lib.cleanSourceWith { - filter = lib.cleanSourceFilter; - src = lib.cleanSourceWith { - filter = path: type: let baseName = builtins.baseNameOf path; in !( - (type == "directory" && ( - baseName == "build" || - baseName == ".idea" || - baseName == ".gradle" - )) || - (lib.hasSuffix ".iml" baseName) - ); - src = ./.; - }; - }; - - gradleFlags = [ "installDist" ]; - - installPhase = '' - mkdir -p $out - cp -r app/build/install/gradle2nix/* $out/ - ''; - - meta = with lib; { - inherit (gradle.meta) platforms; - description = "Wrap Gradle builds with Nix"; - homepage = "https://github.com/tadfisher/gradle2nix"; - license = licenses.asl20; - maintainers = with maintainers; [ tadfisher ]; - mainProgram = "gradle2nix"; - }; -}