Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f1528f2
IDETECT-4812 - refactor gradle init script
bd-samratmuk Sep 1, 2025
84672c7
IDETECT-4812 - refactor gradle init script
bd-samratmuk Sep 2, 2025
faa7342
IDETECT-4812 - refactor gradle init script
bd-samratmuk Sep 2, 2025
9ff2d45
IDETECT-4812 - refactor gradle init script
bd-samratmuk Sep 2, 2025
94725ad
IDETECT-4812 - refactor gradle init script
bd-samratmuk Sep 2, 2025
3678d0c
IDETECT-4812 - refactor gradle init script
bd-samratmuk Sep 3, 2025
a40e26b
IDETECT-4812 - refactor gradle init script
bd-samratmuk Sep 3, 2025
4614667
Prep doc for 11.0.0 and fix nuget note
cpottsbd Aug 27, 2025
aeba3a2
Update nuget.md
cpottsbd Aug 28, 2025
dac370b
remove deprecated properties and values
dterrybd Sep 2, 2025
457a8a9
add release notes
dterrybd Sep 2, 2025
1f07694
Release 11.0.0-SIGQA3
Sep 2, 2025
52511bb
Using the next snapshot post release 11.0.0-SIGQA4-SNAPSHOT
Sep 2, 2025
fed5e80
add deep license project fields
dterrybd Aug 8, 2025
3d29cdb
update to latest blackduck-common
dterrybd Aug 26, 2025
4982f32
deep license for project creation
dterrybd Aug 26, 2025
6816b4e
allow for project updates
dterrybd Aug 29, 2025
3fd97a9
add basic tests
dterrybd Aug 29, 2025
cec5310
use builder pattern
dterrybd Aug 29, 2025
8b029cb
add release note
dterrybd Sep 2, 2025
b247c92
update release notes
dterrybd Sep 2, 2025
7c3ef46
IDETECT-4799: Ignore the toolchain module when putting together the d…
shantyk Sep 4, 2025
32f78b5
Add release note for IDETECT-4799 (#1532)
shantyk Sep 5, 2025
f829dbd
IDETECT-4751: Normalize IAC scan results file to NFD before upload (#…
shantyk Sep 8, 2025
0a4b538
fix(gradle): Refactored init-script-gradle.ftl to add support for con…
bd-samratmuk Sep 9, 2025
12736f5
Merge branch 'master' into IDETECT-4812a
bd-samratmuk Sep 9, 2025
94913e1
Merge branch 'master' into IDETECT-4812a
bd-samratmuk Sep 11, 2025
4efddb7
fix(gradle): Remove redundant try-catch (IDETECT-4812)
bd-samratmuk Sep 12, 2025
1b0851f
fix(gradle): Edit currentreleasenotes.md (IDETECT-4812)
bd-samratmuk Sep 12, 2025
3e637d4
fix(gradle): throw errors in the catch block allowing the script to e…
bd-samratmuk Sep 15, 2025
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
3 changes: 2 additions & 1 deletion documentation/src/main/markdown/currentreleasenotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@

* (IDETECT-4802) Fix UV Lockfile Detector not generating BDIOs for projects with non-normalized names per Python requirements.
* (IDETECT-4751) Prevent server-side parsing errors by normalizing IAC Scan `results.json` contents before uploading to Black Duck SCA.
* (IDETECT-4799) When constructing the BDIO, ignore the Go toolchain directive, as it is the Go project's build-time configuration setting and not a module dependency.
* (IDETECT-4799) When constructing the BDIO, ignore the Go toolchain directive, as it is the Go project's build-time configuration setting and not a module dependency.
* (IDETECT-4812) Refactored init-script-gradle.ftl to add support for configuration cache in gradle projects.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One tiny tweak, to the file name format.

  • (IDETECT-4812) Refactored init-script-gradle.ftl to add support for configuration cache in Gradle projects.


### Dependency updates

Expand Down
327 changes: 194 additions & 133 deletions src/main/resources/init-script-gradle.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -13,174 +13,235 @@ Set<String> projectNameIncludeFilter = convertStringToSet('${includedProjectName
Set<String> projectPathExcludeFilter = convertStringToSet('${excludedProjectPaths}')
Set<String> projectPathIncludeFilter = convertStringToSet('${includedProjectPaths}')
Boolean rootOnly = Boolean.parseBoolean("${rootOnlyOption}")

gradle.allprojects {
// add a new task to each project to start the process of getting the dependencies
task gatherDependencies(type: DefaultTask) {
// Store project name during configuration phase
def projectName = project.name
doLast {
println "Gathering dependencies for " + project.name
try {
println "Gathering dependencies for " + projectName
} catch (Exception e) {
println "ERROR in gatherDependencies task: " + e.message
e.printStackTrace()
}
}
}
afterEvaluate { project ->
// after a project has been evaluated modify the dependencies task for that project to output to a specific file.
project.tasks.getByName('dependencies') {
ext {
excludedProjectNames = '${excludedProjectNames}'
includedProjectNames = '${includedProjectNames}'
excludedConfigurationNames = '${excludedConfigurationNames}'
includedConfigurationNames = '${includedConfigurationNames}'
outputDirectoryPath = System.getProperty('GRADLEEXTRACTIONDIR')

afterEvaluate { currentProject ->
// Capture all needed project properties during configuration
def projectPath = currentProject.path
def projectName = currentProject.name
def isRootProject = isRoot(currentProject)
def extractionDir = System.getProperty('GRADLEEXTRACTIONDIR')
def projectDir = currentProject.projectDir
def projectDirPath = projectDir.canonicalPath

// Root project properties
def rootProject = currentProject.gradle.rootProject
def rootProjectName = rootProject.name
def rootProjectPath = rootProject.path
def rootProjectGroup = rootProject.group.toString()
def rootProjectVersion = rootProject.version.toString()
def rootProjectDir = rootProject.projectDir
def rootProjectDirPath = rootProjectDir.canonicalPath

// Project metadata
def projectGroup = currentProject.group.toString()
def projectVersion = currentProject.version.toString()
def projectParent = currentProject.parent ? currentProject.parent.toString() : "none"

// Prepare configuration names
def configurationNames = getFilteredConfigurationNames(currentProject,
'${excludedConfigurationNames}', '${includedConfigurationNames}')

def selectedConfigs = []
configurationNames.each { name ->
try {
def config = currentProject.configurations.findByName(name)
if (config) {
selectedConfigs.add(config)
}
} catch (Exception e) {
println "Could not process configuration: " + name
}
doFirst {
generateRootProjectMetaData(project, outputDirectoryPath)
}

if((rootOnly && isRoot(project)) || (!rootOnly && shouldInclude(projectNameExcludeFilter, projectNameIncludeFilter, project.name) && shouldInclude(projectPathExcludeFilter, projectPathIncludeFilter, project.path)) ) {
def dependencyTask = project.tasks.getByName('dependencies')
File projectOutputFile = findProjectOutputFile(project, outputDirectoryPath)
File projectFile = createProjectOutputFile(projectOutputFile)
// Check if the project should be included in results
def shouldIncludeProject = (rootOnly && isRootProject) ||
(!rootOnly && shouldInclude(projectNameExcludeFilter, projectNameIncludeFilter, projectName) &&
shouldInclude(projectPathExcludeFilter, projectPathIncludeFilter, projectPath))

if(dependencyTask.metaClass.respondsTo(dependencyTask, "setConfigurations")) {
println "Updating configurations for task"
// modify the configurations for the dependency task
setConfigurations(filterConfigurations(project, excludedConfigurationNames, includedConfigurationNames))
// Capture output file path during configuration
def projectFilePathConfig = computeProjectFilePath(projectPath, extractionDir)

} else {
println "Could not find method 'setConfigurations'"
}
// Configure the dependencies task during configuration time
def dependenciesTask = currentProject.tasks.getByName('dependencies')

// Set the configurations at configuration time if possible
if (!selectedConfigs.isEmpty()) {
dependenciesTask.configurations = selectedConfigs
Copy link
Preview

Copilot AI Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting task properties directly with Configuration objects can cause serialization issues with configuration cache. Consider using configuration names and resolving configurations during task execution instead.

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

@bd-samratmuk bd-samratmuk Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting configuration object properties during configuration phase is not causing serialization issues as tested with multiple projects. I assume this is what Gradle wants for now.

}

// Set the output file at configuration time if possible
if (shouldIncludeProject) {
// Create output file directly during configuration
File projectFile = new File(projectFilePathConfig)
if (projectFile.exists()) {
projectFile.delete()
}
projectFile.createNewFile()
Comment on lines +86 to +90
Copy link
Preview

Copilot AI Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating and deleting files during the configuration phase can cause issues with configuration cache and parallel builds. File operations should be moved to task execution phase (doFirst/doLast) to avoid configuration cache violations and ensure proper build isolation.

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a known issue. Even though creating file should be part of execution phase but then setting configuration objects like outputFile to that created file, when done should be part of configuration phase. I would like to keep this as it is.


// Set the output file during configuration phase
try {
dependenciesTask.outputFile = projectFile
println "Set output file during configuration to " + projectFile.getAbsolutePath()
} catch (Exception e) {
println "Could not set outputFile property during configuration: " + e.message
e.printStackTrace()
}
}

if(dependencyTask.metaClass.respondsTo(dependencyTask,"setOutputFile")) {
println "Updating output file for task to "+projectFile.getAbsolutePath()
// modify the output file
setOutputFile(projectFile)
} else {
println "Could not find method 'setOutputFile'"
dependenciesTask.doFirst {
try {
if (extractionDir == null) {
throw new IllegalStateException("GRADLEEXTRACTIONDIR system property is not set")
}

Copy link
Preview

Copilot AI Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting the outputFile property with a File object created during configuration phase violates configuration cache requirements. The file creation should be deferred to task execution time.

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already explained in the above comments.

// Create metadata file for root project
if (isRootProject) {
try {
File outputDirectory = new File(extractionDir)
outputDirectory.mkdirs()
File rootOutputFile = new File(outputDirectory, 'rootProjectMetadata.txt')

def rootProjectMetadataPieces = []
rootProjectMetadataPieces.add('DETECT META DATA START')
rootProjectMetadataPieces.add("rootProjectDirectory:" + rootProjectDirPath)
rootProjectMetadataPieces.add("rootProjectPath:" + rootProjectPath)
rootProjectMetadataPieces.add("rootProjectGroup:" + rootProjectGroup)
rootProjectMetadataPieces.add("rootProjectName:" + rootProjectName)
rootProjectMetadataPieces.add("rootProjectVersion:" + rootProjectVersion)
rootProjectMetadataPieces.add('DETECT META DATA END')

rootOutputFile.text = rootProjectMetadataPieces.join('\n')
} catch (Exception e) {
println "ERROR while generating root project metadata: " + e.message
e.printStackTrace()
}
} else {
println "Excluding from results subproject: " + project.path
}
} catch (Exception e) {
println "ERROR in dependencies doFirst: " + e.message
e.printStackTrace()
}
}

doLast {
if((rootOnly && isRoot(project)) || (!rootOnly && shouldInclude(projectNameExcludeFilter, projectNameIncludeFilter, project.name) && shouldInclude(projectPathExcludeFilter, projectPathIncludeFilter, project.path))) {
File projectFile = findProjectOutputFile(project, outputDirectoryPath)
appendProjectMetadata(project, projectFile)
dependenciesTask.doLast {
try {
if(shouldIncludeProject) {
File projectFile = new File(projectFilePathConfig)

// Add metadata at the end of the file
def metaDataPieces = []
metaDataPieces.add('')
metaDataPieces.add('DETECT META DATA START')
metaDataPieces.add("rootProjectDirectory:" + rootProjectDirPath)
metaDataPieces.add("rootProjectGroup:" + rootProjectGroup)
metaDataPieces.add("rootProjectPath:" + rootProjectPath)
metaDataPieces.add("rootProjectName:" + rootProjectName)
metaDataPieces.add("rootProjectVersion:" + rootProjectVersion)
metaDataPieces.add("projectDirectory:" + projectDirPath)
metaDataPieces.add("projectGroup:" + projectGroup)
metaDataPieces.add("projectName:" + projectName)
metaDataPieces.add("projectVersion:" + projectVersion)
metaDataPieces.add("projectPath:" + projectPath)
metaDataPieces.add("projectParent:" + projectParent)
metaDataPieces.add('DETECT META DATA END')
metaDataPieces.add('')

// Append to file
projectFile << metaDataPieces.join('\n')
}
} catch (Exception e) {
println "ERROR in dependencies doLast: " + e.message
e.printStackTrace()
}
}
// this forces the dependencies task to be run which will write the content to the modified output file
project.gatherDependencies.finalizedBy(project.tasks.getByName('dependencies'))
project.gatherDependencies

// This forces the dependencies task to be run
currentProject.gatherDependencies.finalizedBy(currentProject.tasks.getByName('dependencies'))
currentProject.gatherDependencies
}
}

// ## START methods invoked by tasks above
<#-- Do not parse with Freemarker because Groovy variable replacement in template strings is the same as Freemarker template syntax. -->
<#noparse>
def isRoot(Project project) {
Project rootProject = project.gradle.rootProject;
return project.name.equals(rootProject.name)
}

def generateRootProjectMetaData(Project project, String outputDirectoryPath) {
File outputDirectory = createTaskOutputDirectory(outputDirectoryPath)
outputDirectory.mkdirs()

Project rootProject = project.gradle.rootProject;
/* if the current project is the root project then generate the file containing
the meta data for the root project otherwise ignore.
*/
if (project.name.equals(rootProject.name)) {
File rootOutputFile = new File(outputDirectory, 'rootProjectMetadata.txt');
String rootProjectDirectory = rootProject.getProjectDir().getCanonicalPath()
String rootProjectPath = rootProject.path.toString()
String rootProjectGroup = rootProject.group.toString()
String rootProjectName = rootProject.name.toString()
String rootProjectVersionName = rootProject.version.toString()

def rootProjectMetadataPieces = []
rootProjectMetadataPieces.add('DETECT META DATA START')
rootProjectMetadataPieces.add("rootProjectDirectory:${rootProjectDirectory}")
rootProjectMetadataPieces.add("rootProjectPath:${rootProjectPath}")
rootProjectMetadataPieces.add("rootProjectGroup:${rootProjectGroup}")
rootProjectMetadataPieces.add("rootProjectName:${rootProjectName}")
rootProjectMetadataPieces.add("rootProjectVersion:${rootProjectVersionName}")
rootProjectMetadataPieces.add('DETECT META DATA END')
rootOutputFile << rootProjectMetadataPieces.join('\n')
try {
Project rootProject = project.gradle.rootProject;
return project.name.equals(rootProject.name)
} catch (Exception e) {
println "ERROR in isRoot: " + e.message
e.printStackTrace()
return false
}
}

def findProjectOutputFile(Project project, String outputDirectoryPath) {
File outputDirectory = createTaskOutputDirectory(outputDirectoryPath)
String name = project.toString()

int depthCount = 0
for(char c: name.toCharArray()) {
if (c == ':') {
depthCount++
}
}
String depth = String.valueOf(depthCount)
// Get path for project file
def computeProjectFilePath(String projectPath, String outputDirectoryPath) {
try {
File outputDirectory = createTaskOutputDirectory(outputDirectoryPath)
String name = projectPath ?: ""

String nameForFile = name?.replaceAll(/[^\p{IsAlphabetic}\p{Digit}]/, "_")
File outputFile = new File(outputDirectory, "${nameForFile}_depth${depth}_dependencyGraph.txt")

outputFile
}

def filterConfigurations(Project project, String excludedConfigurationNames, String includedConfigurationNames) {
Set<String> configurationExcludeFilter = convertStringToSet(excludedConfigurationNames)
Set<String> configurationIncludeFilter = convertStringToSet(includedConfigurationNames)
Set<Configuration> filteredConfigurationSet = new TreeSet<Configuration>(new Comparator<Configuration>() {
public int compare(Configuration conf1, Configuration conf2) {
return conf1.getName().compareTo(conf2.getName());
int depthCount = 0
for(char c: name.toCharArray()) {
if (c == ':') {
depthCount++
}
}
})
for (Configuration configuration : project.configurations) {
if (shouldInclude(configurationExcludeFilter, configurationIncludeFilter, configuration.name)) {
filteredConfigurationSet.add(configuration)
String depth = String.valueOf(depthCount)
// Special case for root project
if (projectPath == ":") {
return new File(outputDirectory, "root_project_depth${depth}_dependencyGraph.txt").getAbsolutePath()
}
}
// Replace all non-alphanumeric characters with underscores

filteredConfigurationSet
String nameForFile = name?.replaceAll(/[^\p{IsAlphabetic}\p{Digit}]/, "_")
nameForFile = "project_" + nameForFile
return new File(outputDirectory, "${nameForFile}_depth${depth}_dependencyGraph.txt").getAbsolutePath()
} catch (Exception e) {
println "ERROR in computeProjectFilePath: " + e.message
e.printStackTrace()
// Do nothing else
Copy link
Preview

Copilot AI Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The comment '// Do nothing else' after error handling is unclear. Consider either removing the comment or explaining what the expected behavior should be when an error occurs.

Suggested change
println "ERROR in computeProjectFilePath: " + e.message
e.printStackTrace()
// Do nothing else
// Return null to indicate failure
return null

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

@bd-samratmuk bd-samratmuk Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the gradlew executable fails to run, Detect still runs as per the old code, falling back to the gradle project inspector Hence we do nothing.

}
}

def appendProjectMetadata(Project project, File projectOutputFile) {
Project rootProject = project.gradle.rootProject;
String rootProjectGroup = rootProject.group.toString()
String rootProjectName = rootProject.name.toString()
String rootProjectVersionName = rootProject.version.toString()
String rootProjectPath = rootProject.path.toString()
String group = project.group.toString()
String name = project.name.toString()
String version = project.version.toString()
String path = project.path.toString()

def metaDataPieces = []
metaDataPieces.add('')
metaDataPieces.add('DETECT META DATA START')
metaDataPieces.add("rootProjectDirectory:${rootProject.getProjectDir().getCanonicalPath()}")
metaDataPieces.add("rootProjectGroup:${rootProjectGroup}")
metaDataPieces.add("rootProjectPath:${rootProjectPath}")
metaDataPieces.add("rootProjectName:${rootProjectName}")
metaDataPieces.add("rootProjectVersion:${rootProjectVersionName}")
metaDataPieces.add("projectDirectory:${project.getProjectDir().getCanonicalPath()}")
metaDataPieces.add("projectGroup:${group}")
metaDataPieces.add("projectName:${name}")
metaDataPieces.add("projectVersion:${version}")
metaDataPieces.add("projectPath:${path}")
metaDataPieces.add("projectParent:${project.parent}")
metaDataPieces.add('DETECT META DATA END')
metaDataPieces.add('')

projectOutputFile << metaDataPieces.join('\n')
}
// Get only configuration names to avoid serialization issues
def getFilteredConfigurationNames(Project project, String excludedConfigurationNames, String includedConfigurationNames) {
try {
Set<String> configurationExcludeFilter = convertStringToSet(excludedConfigurationNames)
Set<String> configurationIncludeFilter = convertStringToSet(includedConfigurationNames)
Set<String> filteredNames = new TreeSet<String>()

def createProjectOutputFile(File projectFile) {
if (projectFile.exists()) {
projectFile.delete()
}
for (def configuration : project.configurations) {
try {
if (shouldInclude(configurationExcludeFilter, configurationIncludeFilter, configuration.name)) {
filteredNames.add(configuration.name)
}
} catch (Exception e) {
println "ERROR processing configuration " + configuration.name + ": " + e.message
}
}

projectFile.createNewFile()
projectFile
return filteredNames
} catch (Exception e) {
println "ERROR in getFilteredConfigurationNames: " + e.message
e.printStackTrace()
// Do nothing else
Copy link
Preview

Copilot AI Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Same unclear comment as above. The error handling should either return a default value or rethrow the exception rather than silently continuing with undefined behavior.

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already explained in the above comments.

}
}

def createTaskOutputDirectory(String outputDirectoryPath) {
Expand Down Expand Up @@ -219,7 +280,7 @@ def wildCardTokenToRegexToken(String token) {
if(matcher.group(1) != null) {
matcher.appendReplacement(buffer, '.*')
} else if (matcher.group(2) != null) {
matcher.appendReplacement(buffer, ".");
matcher.appendReplacement(buffer, ".");
} else {
matcher.appendReplacement(buffer, '\\\\Q' + matcher.group(0) + '\\\\E')
}
Expand All @@ -228,4 +289,4 @@ def wildCardTokenToRegexToken(String token) {
return buffer.toString()
}
</#noparse>
// ## END methods invoked by tasks above
// ## END methods invoked by tasks above