This is an automated email from the ASF dual-hosted git repository. jdaugherty pushed a commit to branch 7.0.x in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 0baef55ba86eb1e38bc84e615ffb5aef20e93c9a Author: James Daugherty <[email protected]> AuthorDate: Thu Apr 24 10:41:12 2025 -0400 Revert "Revert "reproducible builds"" This reverts commit e5d40dedbc209bf1d7e1f32449b9cfe07a66d3b6. --- .github/workflows/gradle.yml | 16 ++++++++++++++ .github/workflows/release.yml | 6 ++++++ build.gradle | 21 +++++++++--------- etc/bin/reproducible-build.sh | 38 +++++++++++++++++++++++++++++++++ gradle/java-config.gradle | 25 ++++++++++++++++++---- grails-gradle/gradle/java-config.gradle | 27 +++++++++++++++++------ 6 files changed, 112 insertions(+), 21 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index d1868e0bfb..c2c47e7976 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -39,6 +39,8 @@ jobs: os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: + - name: 'Ensure Common Build Date' # to ensure a reproducible build + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" - name: "📥 Checkout repository" uses: actions/checkout@v4 - name: "☕️ Setup JDK" @@ -64,6 +66,8 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: + - name: 'Ensure Common Build Date' # to ensure a reproducible build + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" - name: "📥 Checkout repository" uses: actions/checkout@v4 - name: "☕️ Setup JDK" @@ -94,6 +98,8 @@ jobs: java: [ '17', '21', '23' ] runs-on: ubuntu-latest steps: + - name: 'Ensure Common Build Date' # to ensure a reproducible build + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" - name: "📥 Checkout repository" uses: actions/checkout@v4 - name: "☕️ Setup JDK" @@ -120,6 +126,8 @@ jobs: java: [ 17, 21 ] mongodb-version: [ '5.0', '6.0', '7.0', '8.0' ] steps: + - name: 'Ensure Common Build Date' # to ensure a reproducible build + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" - name: "📥 Checkout the repository" uses: actions/checkout@v4 - name: "☕️ Setup JDK" @@ -149,6 +157,8 @@ jobs: matrix: java: [ 17, 21 ] steps: + - name: 'Ensure Common Build Date' # to ensure a reproducible build + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" - name: "📥 Checkout the repository" uses: actions/checkout@v4 - name: "☕️ Setup JDK" @@ -172,6 +182,8 @@ jobs: contents: read # limit to read access runs-on: ubuntu-latest steps: + - name: 'Ensure Common Build Date' # to ensure a reproducible build + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" - name: "📥 Checkout repository" uses: actions/checkout@v4 - name: "☕️ Setup JDK" @@ -200,6 +212,8 @@ jobs: contents: read # limit to read access runs-on: ubuntu-latest steps: + - name: 'Ensure Common Build Date' # to ensure a reproducible build + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" - name: "📥 Checkout repository" uses: actions/checkout@v4 - name: "☕️ Setup JDK" @@ -227,6 +241,8 @@ jobs: permissions: contents: write steps: + - name: 'Ensure Common Build Date' # to ensure a reproducible build + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" - name: "📥 Checkout the repository" uses: actions/checkout@v4 - name: "🔀 Store current branch name" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 445c4e9338..8a5c058a3d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,8 @@ jobs: release_version: ${{ steps.release_version.outputs.value }} target_branch: ${{ steps.extract_branch.outputs.value }} steps: + - name: 'Ensure Common Build Date' # to ensure a reproducible build + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" - name: "📥 Checkout repository" uses: actions/checkout@v4 - name: "☕️ Setup JDK" @@ -114,6 +116,8 @@ jobs: permissions: contents: read # limit to read access steps: + - name: 'Ensure Common Build Date' # to ensure a reproducible build + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" - name: "📥 Checkout repository" uses: actions/checkout@v4 with: @@ -159,6 +163,8 @@ jobs: needs: publish runs-on: ubuntu-24.04 steps: + - name: 'Ensure Common Build Date' # to ensure a reproducible build + run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> "$GITHUB_ENV" - name: "📥 Checkout repository" uses: actions/checkout@v4 with: diff --git a/build.gradle b/build.gradle index 84e134cd51..e01425fdb1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,7 @@ +import java.time.Instant +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter + /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -18,6 +22,13 @@ */ ext { + buildInstant = java.util.Optional.ofNullable(System.getenv("SOURCE_DATE_EPOCH")) + .map(Long::parseLong) + .map(Instant::ofEpochSecond) + .orElseGet(Instant::now) + formattedBuildDate = DateTimeFormatter.ISO_INSTANT.format(buildInstant) + buildDate = (buildInstant as Instant).atZone(ZoneOffset.UTC) // for reproducible builds + grailsVersion = projectVersion isCiBuild = System.getenv().get('CI') as Boolean configuredTestParallel = findProperty('maxTestParallel') as Integer ?: (isCiBuild ? 3 : Runtime.runtime.availableProcessors() * 3 / 4 as int ?: 1) @@ -74,16 +85,6 @@ subprojects { } } - // This is added to prevent a remote cache misses, because the project JAR include a manifest - // file with Built-By and Created-By properties which might be different for CI vs Local. - normalization { - runtimeClasspath { - metaInf { - ignoreAttribute('Built-By') - ignoreAttribute('Created-By') - } - } - } apply from: rootProject.layout.projectDirectory.file('gradle/dependency-licenses.gradle') } diff --git a/etc/bin/reproducible-build.sh b/etc/bin/reproducible-build.sh new file mode 100755 index 0000000000..23e6ba3385 --- /dev/null +++ b/etc/bin/reproducible-build.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +cd "$SCRIPT_DIR/../.." + +git clean -xdf +./gradlew build --rerun-tasks -PskipTests +FIRST_BUILD=$(sha256sum build/libs/*) + +git clean -xdf +./gradlew build --rerun-tasks -PskipTests +SECOND_BUILD=$(sha256sum build/libs/*) + +cd $SCRIPT_DIR +echo $FIRST_BUILD > first.txt +echo $SECOND_BUILD > second.txt + +diff -u first.txt second.txt \ No newline at end of file diff --git a/gradle/java-config.gradle b/gradle/java-config.gradle index d0aafc7fb2..b18be0d0d7 100644 --- a/gradle/java-config.gradle +++ b/gradle/java-config.gradle @@ -25,22 +25,39 @@ extensions.configure(JavaPluginExtension) { it.withJavadocJar() } +tasks.withType(Javadoc).configureEach { Javadoc it -> + it.options.noTimestamp true // prevent the file header with the date + it.options.bottom "Generated ${formattedBuildDate} (UTC)" +} + +// JavaCompile is not configured because we put java files inside of the groovy source sets + tasks.withType(GroovyCompile).configureEach { - groovyOptions.encoding = 'UTF-8' + groovyOptions.encoding = 'UTF-8' // encoding needs to be the same since it's different across platforms // Preserve method parameter names in Groovy classes for IDE parameter hints. groovyOptions.parameters = true - options.encoding = 'UTF-8' + options.encoding = 'UTF-8' // encoding needs to be the same since it's different across platforms options.fork = true options.forkOptions.jvmArgs = ['-Xms128M', '-Xmx1G'] } +// Grails determines the grails version via the META-INF/MANIFEST.MF file +// Note: we exclude attributes such as Built-By, Build-Jdk, Created-By to ensure the build is reproducible. tasks.withType(Jar).configureEach { manifest.attributes( - 'Built-By': System.properties['user.name'], - 'Created-By': System.properties['java.vm.version'] + ' (' + System.properties['java.vm.vendor'] + ')', 'Implementation-Title': 'Grails', 'Implementation-Version': grailsVersion, 'Implementation-Vendor': 'grails.org' ) duplicatesStrategy = DuplicatesStrategy.INCLUDE } + +// Any jar, zip, or archive should be reproducible +// No longer needed after https://github.com/gradle/gradle/issues/30871 +tasks.withType(AbstractArchiveTask).configureEach { + preserveFileTimestamps = false // to prevent timestamp mismatches + reproducibleFileOrder = true // to keep the same ordering + dirMode = 0755 // To avoid platform specific defaults + fileMode = 0644 // to avoid platform specific defaults +} + diff --git a/grails-gradle/gradle/java-config.gradle b/grails-gradle/gradle/java-config.gradle index 7996788a18..b762684f70 100644 --- a/grails-gradle/gradle/java-config.gradle +++ b/grails-gradle/gradle/java-config.gradle @@ -25,25 +25,38 @@ extensions.configure(JavaPluginExtension) { it.withJavadocJar() } -tasks.withType(JavaCompile).configureEach { - options.deprecation = true - options.debug = true +tasks.withType(Javadoc).configureEach { Javadoc it -> + it.options.noTimestamp true // prevent the file header with the date + it.options.bottom "Generated ${formattedBuildDate} (UTC)" } +// JavaCompile is not configured because we put java files inside of the groovy source sets + tasks.withType(GroovyCompile).configureEach { - groovyOptions.encoding = 'UTF-8' - options.encoding = 'UTF-8' + groovyOptions.encoding = 'UTF-8' // encoding needs to be the same since it's different across platforms + // Preserve method parameter names in Groovy classes for IDE parameter hints. + groovyOptions.parameters = true + options.encoding = 'UTF-8' // encoding needs to be the same since it's different across platforms options.fork = true options.forkOptions.jvmArgs = ['-Xms128M', '-Xmx1G'] } +// Grails determines the grails version via the META-INF/MANIFEST.MF file +// Note: we exclude attributes such as Built-By, Build-Jdk, Created-By to ensure the build is reproducible. tasks.withType(Jar).configureEach { manifest.attributes( - 'Built-By': System.properties['user.name'], - 'Created-By': System.properties['java.vm.version'] + ' (' + System.properties['java.vm.vendor'] + ')', 'Implementation-Title': 'Grails', 'Implementation-Version': projectVersion, 'Implementation-Vendor': 'grails.org' ) duplicatesStrategy = DuplicatesStrategy.INCLUDE } + +// Any jar, zip, or archive should be reproducible +// No longer needed after https://github.com/gradle/gradle/issues/30871 +tasks.withType(AbstractArchiveTask).configureEach { + preserveFileTimestamps = false // to prevent timestamp mismatches + reproducibleFileOrder = true // to keep the same ordering + dirMode = 0755 // To avoid platform specific defaults + fileMode = 0644 // to avoid platform specific defaults +}
