Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -489,12 +489,12 @@ Some common gotchas with Java build reproducibility problems:
not ship property files with dynamic timestamps.
4. Gradle builds are not reproducible by default because Gradle does not guarantee file ordering. Grails configures all
tasks that create archive files to ensure file ordering is reproducible. This configuration can be found in
`gradle/java-config.gradle`
the `CompilePlugin` in the `build-logic` project.
5. Grails makes heavy use of Groovy AST transforms. These transforms often lookup methods on a class, and the methods
returned by Java will vary by operating system. Any code using reflection to lookup methods or fields must sort the
results to ensure reproducibility.
6. Javadoc / Groovydoc will by default write out dates in it's documentation headers. Grails configures these settings
in `gradle/java-config.gradle`.
in the `CompilePlugin` in the `build-logic` project.

After a build is made reproducible on the same machine, the next step is to ensure it's reproducible in GitHub actions.
OS differences will exist and even exist between different Docker Runtimes. To help test reproducible builds, we found
Expand Down
12 changes: 10 additions & 2 deletions build-logic/plugins/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,21 @@ dependencies {

gradlePlugin {
plugins {
publishPlugin {
register('compilePlugin') {
id = 'org.apache.grails.buildsrc.compile'
implementationClass = 'org.apache.grails.buildsrc.CompilePlugin'
}
register('publishPlugin') {
id = 'org.apache.grails.buildsrc.publish'
implementationClass = 'org.apache.grails.buildsrc.PublishPlugin'
}
sbomPlugin {
register('sbomPlugin') {
id = 'org.apache.grails.buildsrc.sbom'
implementationClass = 'org.apache.grails.buildsrc.SbomPlugin'
}
register('sharedPropertyPlugin') {
id = 'org.apache.grails.buildsrc.properties'
implementationClass = 'org.apache.grails.buildsrc.SharedPropertyPlugin'
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* 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.
*/

package org.apache.grails.buildsrc

import java.nio.charset.StandardCharsets
import java.util.concurrent.atomic.AtomicBoolean

import groovy.transform.CompileStatic

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.compile.GroovyCompile
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.javadoc.Javadoc
import org.gradle.external.javadoc.StandardJavadocDocletOptions

import static org.apache.grails.buildsrc.GradleUtils.lookupPropertyByType

@CompileStatic
class CompilePlugin implements Plugin<Project> {

@Override
void apply(Project project) {
def initialized = new AtomicBoolean(false)
project.plugins.withId('java') { // java (applied when groovy is applied) or java-library
if (initialized.compareAndSet(false, true)) {
configureCompile(project)
}
}
}

private static void configureCompile(Project project) {
configureJavaVersion(project)
configureJars(project)
configureCompiler(project)
configureReproducible(project)
}

private static void configureJavaVersion(Project project) {
project.tasks.withType(JavaCompile).configureEach {
it.options.release.set(lookupPropertyByType(project, 'javaVersion', Integer))
}
}

private static void configureJars(Project project) {
project.extensions.configure(JavaPluginExtension) {
it.withJavadocJar()
it.withSourcesJar()
}

// 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.
project.tasks.withType(Jar).configureEach { Jar jar ->
if (lookupPropertyByType(project, 'skipJavaComponent', Boolean)) {
jar.enabled = false
return
}

jar.manifest.attributes(
'Implementation-Title': 'Apache Grails',
'Implementation-Version': lookupPropertyByType(project, 'grailsVersion', String),
'Implementation-Vendor': 'grails.apache.org'
)
// Explicitly fail since duplicates indicate a double configuration that needs fixed
jar.duplicatesStrategy = DuplicatesStrategy.FAIL
}
}

private static void configureCompiler(Project project) {
project.tasks.withType(JavaCompile).configureEach {
// Preserve method parameter names in Groovy/Java classes for IDE parameter hints & bean reflection metadata.
it.options.compilerArgs.add('-parameters')
// encoding needs to be the same since it's different across platforms
it.options.encoding = StandardCharsets.UTF_8.name()
it.options.fork = true
it.options.forkOptions.jvmArgs = ['-Xms128M', '-Xmx2G']
}

project.plugins.withId('groovy') {
project.tasks.withType(GroovyCompile).configureEach {
// encoding needs to be the same since it's different across platforms
it.groovyOptions.encoding = StandardCharsets.UTF_8.name()
// Preserve method parameter names in Groovy/Java classes for IDE parameter hints & bean reflection metadata.
it.groovyOptions.parameters = true
// encoding needs to be the same since it's different across platforms
it.options.encoding = StandardCharsets.UTF_8.name()
it.options.fork = true
it.options.forkOptions.jvmArgs = ['-Xms128M', '-Xmx2G']
}
}
}

private static void configureReproducible(Project project) {
project.tasks.withType(Javadoc).configureEach { Javadoc it ->
def options = it.options as StandardJavadocDocletOptions
options.noTimestamp = true
options.bottom = "Generated ${lookupPropertyByType(project, 'formattedBuildDate', String)} (UTC)"
}

// Any jar, zip, or archive should be reproducible
// No longer needed after https://github.com/gradle/gradle/issues/30871
project.tasks.withType(AbstractArchiveTask).configureEach {
it.preserveFileTimestamps = false // to prevent timestamp mismatches
it.reproducibleFileOrder = true // to keep the same ordering
// to avoid platform specific defaults, set the permissions consistently
it.filePermissions { permissions ->
permissions.unix(0644)
}
it.dirPermissions { permissions ->
permissions.unix(0755)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,42 @@ import org.gradle.api.file.Directory
class GradleUtils {

static Directory findRootGrailsCoreDir(Project project) {
def rootLayout = project.rootProject.layout
// .github / .git related directories are purged from source releases, so use the .asf.yaml as an indicator of
// the parent directory
if (rootLayout.projectDirectory.file('.asf.yaml').asFile.exists()) {
return rootLayout.projectDirectory
}
findAsfRootDir(project.layout.projectDirectory)
}

// we currently only nest 1 project level deep
rootLayout.projectDirectory.dir('../')
static Directory findAsfRootDir(Directory currentDirectory) {
def asfFile = currentDirectory.file('.asf.yaml').asFile
asfFile.exists() ? currentDirectory : findAsfRootDir(currentDirectory.dir('../'))
}

static <T> T lookupProperty(Project project, String name, T defaultValue = null) {
project.findProperty(name) as T ?: defaultValue
T v = lookupPropertyByType(project, name, defaultValue?.class) as T
return v == null ? defaultValue : v
}

static <T> T lookupPropertyByType(Project project, String name, Class<T> type) {
// a cast exception will occur without this
if (type && (type == Integer || type == int.class)) {
def v = findProperty(project, name)
return v == null ? null : Integer.valueOf(v as String) as T
}

findProperty(project, name) as T
}

static Object findProperty(Project project, String name) {
def property = project.findProperty(name)
if (property != null) {
return property
}

def ext = project.extensions.extraProperties
if (ext.has(name)) {
return ext.get(name)
}

null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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.
*/

package org.apache.grails.buildsrc

import groovy.transform.CompileStatic

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.file.Directory
import org.gradle.api.plugins.ExtraPropertiesExtension

import static org.apache.grails.buildsrc.GradleUtils.findRootGrailsCoreDir

/**
* Gradle can't share properties across buildSrc or composite projects. This plugin ensures that properties not defined
* in this project, but are in the root grails-core project, are accessible in this project. This plugin must be applied
* prior to the access of any property for it to work properly
*/
@CompileStatic
class SharedPropertyPlugin implements Plugin<Project> {

@Override
void apply(Project project) {
populateParentProperties(
project.layout.projectDirectory,
findRootGrailsCoreDir(project),
project.extensions.extraProperties,
project
)
}

void populateParentProperties(Directory projectDirectory, Directory rootDirectory, ExtraPropertiesExtension ext, Project project) {
if (!rootDirectory) {
throw new IllegalStateException('Could not locate the root directory to populate up to')
}

if (projectDirectory.file('gradle.properties').asFile.exists()) {
def propertyPath = rootDirectory.asFile.relativePath(projectDirectory.asFile)
project.logger.info('Using properties from grails-core/{}gradle.properties', propertyPath ? "${propertyPath}/" : '')
projectDirectory.file('gradle.properties').asFile.withInputStream {
Properties rootProperties = new Properties()
rootProperties.load(it)

for (String key : rootProperties.stringPropertyNames()) {
if (!ext.has(key)) {
ext.set(key, rootProperties.getProperty(key))
}
}

if (rootProperties.containsKey('projectVersion')) {
ext.set('grailsVersion', rootProperties.getProperty('projectVersion'))
}
}
}

if (projectDirectory.asFile.absolutePath != rootDirectory.asFile.absolutePath) {
populateParentProperties(projectDirectory.dir('..'), rootDirectory, ext, project)
}
}
}
2 changes: 0 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ ext {
DateTimeFormatter.ISO_INSTANT.format(buildInstant) :
DateTimeFormatter.ISO_DATE.format(LocalDate.ofInstant(buildInstant as Instant, ZoneOffset.UTC))
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)
excludeUnusedTransDeps = findProperty('excludeUnusedTransDeps')
Expand Down
16 changes: 5 additions & 11 deletions buildSrc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,10 @@

plugins {
id 'groovy-gradle-plugin'
id 'org.apache.grails.buildsrc.properties'
}

file('../gradle.properties').withInputStream {
Properties props = new Properties()
props.load(it)
project.ext.gradleProperties = props
}

compileJava.options.release = gradleProperties.javaVersion.toString().toInteger()
compileJava.options.release = javaVersion.toString().toInteger()

repositories {
// mavenLocal()
Expand Down Expand Up @@ -61,14 +56,13 @@ repositories {
}

dependencies {
implementation platform("org.apache.grails:grails-gradle-bom:${gradleProperties.projectVersion}")
implementation platform("org.apache.grails:grails-gradle-bom:$projectVersion")
implementation 'org.apache.grails.gradle:grails-publish'
implementation 'cloud.wondrify:asset-pipeline-gradle'
implementation 'org.apache.grails:grails-docs-core'
implementation 'org.apache.grails:grails-gradle-plugins'
implementation 'org.asciidoctor:asciidoctor-gradle-jvm'
implementation 'org.springframework.boot:spring-boot-gradle-plugin'
implementation "org.nosphere.apache.rat:org.nosphere.apache.rat.gradle.plugin:${gradleProperties.apacheRatVersion}"
implementation "org.cyclonedx.bom:org.cyclonedx.bom.gradle.plugin:${gradleProperties.gradleCycloneDxPluginVersion}"
implementation "org.gradle.crypto.checksum:org.gradle.crypto.checksum.gradle.plugin:${gradleProperties.gradleChecksumPluginVersion}"
implementation "org.nosphere.apache.rat:org.nosphere.apache.rat.gradle.plugin:$apacheRatVersion"
implementation "org.cyclonedx.bom:org.cyclonedx.bom.gradle.plugin:$gradleCycloneDxPluginVersion"
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/

package grails.doc.macros

import org.radeox.macro.BaseMacro
import org.radeox.macro.parameter.MacroParameter

class HiddenMacro extends BaseMacro {
String getName() { "hidden" }

void execute(Writer out, MacroParameter params) {
out << '<div class="hidden-block">' << params.content << '</div>'
pluginManagement {
includeBuild('../build-logic') {
name = 'build-logic-root'
}
}

includeBuild('../build-logic') {
name = 'build-logic-root'
}
Loading
Loading