diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a8177e7bc54..4fd97fd0f56 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -249,6 +249,8 @@ default: # GitLab's cache helper restores .gradle as root, but we run as non-root-user (uid 1001), # and Gradle does `chmod 700 .gradle` on startup which requires user ownership. - sudo chown -R 1001:1001 .gradle + # Detect and fix corrupted Gradle cache: "FATAL: unexpected EOF" + - .gitlab/gradle-cache/mitigate_corrupted_gradle_cache.sh "$GRADLE_VERSION" after_script: - *cgroup_info - *container_info diff --git a/.gitlab/gradle-cache/CorruptedGradleCacheMitigator.java b/.gitlab/gradle-cache/CorruptedGradleCacheMitigator.java new file mode 100644 index 00000000000..589e4d8b964 --- /dev/null +++ b/.gitlab/gradle-cache/CorruptedGradleCacheMitigator.java @@ -0,0 +1,136 @@ +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Band-aid for "FATAL: unexpected EOF" during GitLab cache extraction. + *

+ * This removes only the damaged workspaces so Gradle regenerates them. + */ +class CorruptedGradleCacheMitigator { + private static final Path CACHES_DIR = Path.of(".gradle/caches"); + + // Immutable-workspace categories and the directory depth at which their workspaces live. + private static final Map WORKSPACE_CATEGORIES = + Map.of("dependencies-accessors", 1, "groovy-dsl", 1, "kotlin-dsl", 2, "transforms", 1); + + // Gradle temporary workspaces are - and may legitimately lack metadata.bin. + private static final Pattern TEMPORARY_WORKSPACE = + Pattern.compile( + ".*-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"); + + // Gradle's own metadata reader, resolved reflectively. + private static Object metadataStore; + private static Method metadataLoadMethod; + + public static void main(String[] args) throws IOException { + var gradleVersion = args[0]; + + var damaged = new ArrayList(); + try { + loadMetadataReader(); + } catch (Throwable e) { + System.out.println("Gradle metadata reader unavailable; leaving cache unchanged"); + e.printStackTrace(); + return; + } + + try { + for (var workspace : enumerateWorkspaces(gradleVersion)) { + if (isDamaged(workspace)) { + damaged.add(workspace); + } + } + } catch (Throwable e) { + System.out.println("Failed to collect damaged workspaces"); + e.printStackTrace(); + return; + } + + if (!damaged.isEmpty()) { + System.out.println("Damaged Gradle metadata found, removing:"); + + for (var workspace : damaged) { + System.out.println(" - " + workspace); + try { + remove(workspace); + } catch (Throwable e) { + e.printStackTrace(); + } + } + } + } + + private static List enumerateWorkspaces(String gradleVersion) throws IOException { + var versionDir = CACHES_DIR.resolve(gradleVersion); + var workspaces = new ArrayList(); + for (var category : WORKSPACE_CATEGORIES.entrySet()) { + collectAtDepth(versionDir.resolve(category.getKey()), category.getValue(), workspaces); + } + return workspaces; + } + + private static void collectAtDepth(Path dir, int depth, List out) throws IOException { + if (!Files.isDirectory(dir)) { + return; + } + + if (depth == 0) { + out.add(dir); + return; + } + + try (var entries = Files.list(dir)) { + for (var child : entries.filter(Files::isDirectory).collect(Collectors.toList())) { + collectAtDepth(child, depth - 1, out); + } + } + } + + private static void loadMetadataReader() { + try { + var storeClass = Class.forName( + "org.gradle.internal.execution.history.impl.DefaultImmutableWorkspaceMetadataStore"); + metadataStore = storeClass.getDeclaredConstructor().newInstance(); + metadataLoadMethod = storeClass.getMethod("loadWorkspaceMetadata", File.class); + } catch (Throwable e) { + throw new IllegalStateException("Failed to load Gradle metadata reader", e); + } + } + + private static boolean isDamaged(Path workspace) { + if (TEMPORARY_WORKSPACE.matcher(workspace.getFileName().toString()).matches()) { + return false; + } + + if (!Files.isRegularFile(workspace.resolve("metadata.bin"))) { + return true; + } + + // A successful return means Gradle's own reader fully deserialized `metadata.bin`. + try { + metadataLoadMethod.invoke(metadataStore, workspace.toFile()); + return false; + } catch (Throwable e) { + return true; // truncated/unreadable -> remove it + } + } + + private static void remove(Path workspace) { + try (var paths = Files.walk(workspace)) { + for (var path : paths.sorted(Comparator.reverseOrder()).collect(Collectors.toList())) { + Files.deleteIfExists(path); + } + } catch (Throwable e) { + throw new IllegalStateException("Failed to remove: " + workspace, e); + } + } +} diff --git a/.gitlab/gradle-cache/mitigate_corrupted_gradle_cache.sh b/.gitlab/gradle-cache/mitigate_corrupted_gradle_cache.sh new file mode 100755 index 00000000000..0088f9769b2 --- /dev/null +++ b/.gitlab/gradle-cache/mitigate_corrupted_gradle_cache.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -uo pipefail + +gradle_version="${1:?usage: mitigate_corrupted_gradle_cache.sh }" +script_dir="$(cd "$(dirname "$0")" && pwd)" + +java_home="${JAVA_25_HOME:-}" +java_bin="${java_home:+$java_home/bin/}java" +javac_bin="${java_home:+$java_home/bin/}javac" + +gradle_lib="" +for gradle_home in "${GRADLE_USER_HOME:-$PWD/.gradle}" "$HOME/.gradle"; do + [[ -d "$gradle_home/wrapper/dists" ]] || continue + gradle_lib="$( + find "$gradle_home/wrapper/dists" -path "*/gradle-${gradle_version}/lib" -type d -print -quit \ + 2>/dev/null + )" + [[ -n "$gradle_lib" ]] && break +done +if [[ -z "$gradle_lib" ]]; then + echo "Gradle $gradle_version distribution not found; leaving cache unchanged." >&2 + exit 0 +fi + +build_dir="$(mktemp -d)" +trap 'rm -rf "$build_dir"' EXIT + +# -proc:none keeps the compiler from running annotation processors bundled in the Gradle jars. +if ! "$javac_bin" -proc:none -classpath "$gradle_lib/*" -d "$build_dir" \ + "$script_dir/CorruptedGradleCacheMitigator.java"; then + echo "Could not compile CorruptedGradleCacheMitigator; leaving cache unchanged." >&2 + exit 0 +fi + +"$java_bin" -classpath "$build_dir:$gradle_lib/*" \ + CorruptedGradleCacheMitigator "$gradle_version"