ghidra/Ghidra/RuntimeScripts/Common/support/buildExtension.gradle

523 lines
17 KiB
Groovy

/* ###
* IP: GHIDRA
*
* Licensed 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
*
* http://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.
*/
import org.apache.tools.ant.filters.ReplaceTokens
defaultTasks 'buildExtension'
apply plugin: 'java-library'
/*****************************************************************************************
*
* Reads the application.properties file and sets properties for the version,
* release name, and distro prefix (ghidira_<version>)
*
*****************************************************************************************/
def ghidraInstallDir = file(buildscript.sourceFile.getAbsolutePath() + "/../..").getCanonicalFile().getAbsolutePath()
def ghidraDir = file(ghidraInstallDir + "/Ghidra").getCanonicalFile().getAbsolutePath()
def ghidraProps = new Properties()
file(ghidraDir + "/application.properties").withReader { reader ->
ghidraProps.load(reader)
project.ext.ghidra_version = ghidraProps.getProperty('application.version')
project.ext.RELEASE_NAME = ghidraProps.getProperty('application.release.name')
project.ext.DISTRO_PREFIX = "ghidra_${ghidra_version}"
project.ext.GRADLE_MIN = ghidraProps.getProperty('application.gradle.min')
project.ext.GRADLE_MAX = ghidraProps.getProperty('application.gradle.max')
}
/***************************************************************************************
* Make sure a supported version of Gradle is being used
***************************************************************************************/
checkGradleVersion()
task copyDependencies(type: Copy) {
from configurations.runtimeClasspath
into "lib"
exclude { fileTreeElement ->
def fileAbsPath = fileTreeElement.getFile().getCanonicalFile().toPath()
// Avoid including Ghidra Jars in lib folder...
def isGhidraJar = fileAbsPath.startsWith(ghidraInstallDir)
// ...and jars already in the destination location
def destLibDir = project.file("lib").getCanonicalFile().toPath()
def isFromDest = fileAbsPath.startsWith(destLibDir)
return isGhidraJar || isFromDest
}
}
compileJava {
sourceCompatibility = ghidraProps.getProperty('application.java.compiler')
targetCompatibility = ghidraProps.getProperty('application.java.compiler')
dependsOn copyDependencies
}
dependencies {
api fileTree(dir: 'lib', include: "*.jar")
api fileTree(dir: ghidraDir + '/Framework', include: "**/*.jar")
api fileTree(dir: ghidraDir + '/Features', include: "**/*.jar")
api fileTree(dir: ghidraDir + '/Debug', include: "**/*.jar")
api fileTree(dir: ghidraDir + '/Processors', include: "**/*.jar")
}
def ZIP_NAME_PREFIX = "${DISTRO_PREFIX}_${RELEASE_NAME}_${getCurrentDate()}"
def DISTRIBUTION_DIR = file("dist")
def pathInZip = "${project.name}"
task zipSource (type: Zip) {
// Define some metadata about the zip (name, location, version, etc....)
it.archiveBaseName = project.name + "-src"
it.archiveExtension = 'zip'
it.destinationDirectory = file(project.projectDir.path + "/build/tmp/src")
// We MUST copy from a directory, and not just grab a list of source files.
// This is the only way to preserve the directory structure.
it.from project.projectDir
it.include 'src/**/*'
}
task buildExtension (type: Zip) {
archiveBaseName = "${ZIP_NAME_PREFIX}_${project.name}"
archiveExtension = 'zip'
destinationDirectory = DISTRIBUTION_DIR
archiveVersion = ''
// Make sure that we don't try to copy the same file with the same path into the
// zip (this can happen!)
duplicatesStrategy 'exclude'
// This filtered property file copy must appear before the general
// copy to ensure that it is prefered over the unmodified file
File propFile = new File(project.projectDir, "extension.properties")
from (propFile) {
String version = "${ghidra_version}"
String name = "${project.name}"
filter (ReplaceTokens, tokens: [extversion: version])
filter (ReplaceTokens, tokens: [extname: name])
into pathInZip
}
from (project.jar) {
into pathInZip + "/lib"
}
from (project.projectDir) {
exclude 'build/**'
exclude '*.gradle'
exclude 'certification.manifest'
exclude 'dist/**'
exclude 'bin/**'
exclude 'src/**'
exclude '.gradle/**'
exclude '.classpath'
exclude '.project'
exclude '.settings/**'
exclude 'developer_scripts'
exclude '.antProperties.xml'
exclude 'gradlew'
exclude 'gradlew.bat'
exclude 'gradle/wrapper/gradle-wrapper.jar'
exclude 'gradle/wrapper/gradle-wrapper.properties'
into pathInZip
}
/////////////////
// SOURCE
/////////////////
from (tasks["zipSource"]) {
into pathInZip + "/lib"
}.dependsOn(zipSource)
/////////////////
// GLOBALS
/////////////////
// First get a list of all files that are under 'src/global'.
FileTree fileTree = project.fileTree('src/global') {
include '**/*'
}
// Now loop over each one, copying it into the zip we're creating. Each will be placed
// at the root level, starting with the first folder AFTER 'src/global/'.
//
// eg: If the file is '/Ghidra/Extensions/Sample/src/global/docs/hello.html', then
// the file in the zip will be at /docs/hello.html
//
fileTree.each { File file ->
String filePath = stripGlobalFilePath(file)
from (file) {
into filePath
}
}
doLast {
println "\nCreated " + archiveBaseName + "." + archiveExtension + " in " + destinationDirectory
}
}
/*********************************************************************************
* Help Build Code
* Note: This code is derived from helpProject.gradle. Required changes should
* be made to that file and then reapplied here.
*********************************************************************************/
sourceSets {
// register help resources to be considered inputs to this project; when these resources change,
// this project will be considered out-of-date
main {
resources {
srcDir 'src/main/help' // help .html files to be copied to the jar
srcDir 'build/help/main' // generated help items (from the indexer); copied to the jar
}
}
}
// Turns the given file into a 'normalized' path using the Java Path API
def normalize(File file) {
def path = null;
try {
path = java.nio.file.Paths.get(file.getAbsolutePath());
}
catch (Exception e) { // InvalidPathException
// we have seen odd strings being placed into the classpath--ignore them
return cpPath;
}
def normalizedPath = path.normalize();
def absolutePath = normalizedPath.toAbsolutePath();
return absolutePath.toString();
}
// Returns the Ghidra module directory for the given file if it is a Ghidra jar file
def getModulePathFromJar(File file) {
String path = normalize(file)
String forwardSlashedPath = path.replaceAll("\\\\", "/")
def jarPattern = ~'.*/(.*)/(?:lib|build/libs)/(.+).jar'
def matcher = jarPattern.matcher(forwardSlashedPath)
if (!matcher.matches()) {
return null
}
def moduleName = matcher.group(1);
def index = forwardSlashedPath.indexOf(moduleName) + moduleName.length()
return forwardSlashedPath.substring(0, index)
}
// This method contains logic for calculating help inputs based on the classpath of the project
// The work is cached, as the inputs may be requested multiple times during a build
ext.helpInputsCache = null
def getHelpInputs(Collection fullClasspath) {
if (ext.helpInputsCache != null) {
return ext.helpInputsCache
}
def results = new HashSet<File>()
fullClasspath.each {
String moduleDirPath = getModulePathFromJar(it)
if (moduleDirPath == null) {
return // continue
}
getHelpInputsFromModule(moduleDirPath, results)
}
// the classpath above does not include my module's contents, so add that manually
def modulePath = file('.').getAbsolutePath()
getHelpInputsFromModule(modulePath, results)
ext.helpInputsCache = results.findAll(File::exists)
return ext.helpInputsCache
}
def getHelpInputsFromModule(String moduleDirPath, Set<File> results) {
// add all desired directories now and filter later those that do not exist
File moduleDir = new File(moduleDirPath)
results.add(new File(moduleDir, 'src/main/resources')) // images
results.add(new File(moduleDir, 'src/main/help')) // html files
File dataDir = new File(moduleDir, 'data') // theme properties files
if (dataDir.exists()) {
FileCollection themeFiles = fileTree(dataDir) {
include '**/*.theme.properties'
}
results.addAll(themeFiles.getFiles())
}
}
// Returns true if the given file is a jar file that contains a '/help/topics' diretory
def hasJarHelp(File file) {
if (!file.exists()) {
return false
}
if (!file.getAbsolutePath().endsWith(".jar")) {
return false
}
def fileSystem = null;
try {
def jarURI = new URI("jar:file://" + file.toURI().getRawPath());
fileSystem = java.nio.file.FileSystems.getFileSystem(jarURI);
}
catch (Exception e) { // FileSystemNotFoundException
// handled below
}
if (fileSystem == null) {
// not yet created; try to create the file system
def jarURI = new URI("jar:file://" + file.toURI().getRawPath());
def env = Map.of("create", "false")
fileSystem = java.nio.file.FileSystems.newFileSystem(jarURI, env);
}
def topicsPath = fileSystem.getPath("/help/topics");
return java.nio.file.Files.exists(topicsPath)
}
tasks.register('cleanHelp') {
File helpOutput = file('build/help/main/help')
doFirst {
delete helpOutput
}
}
// Task for calling the java help indexer, which creates a searchable index of the help contents
tasks.register('indexHelp', JavaExec) {
File helpRootDir = file('src/main/help/help')
File outputFile = file("build/help/main/help/${project.name}_JavaHelpSearch")
inputs.dir helpRootDir
outputs.dir outputFile
classpath = sourceSets.main.runtimeClasspath
mainClass = 'com.sun.java.help.search.Indexer'
doFirst {
// gather up all the help files into a file collection
FileTree helpFiles = fileTree('src/main/help') {
include '**/*.htm'
include '**/*.html'
}
// The index has a config file parameter. The only thing we use in the config file
// is a root directory path that should be stripped off all the help references to
// make them relative instead of absolute
File configFile = file('build/helpconfig')
// create the config file when the task runs and not during configuration.
configFile.parentFile.mkdirs();
configFile.write "IndexRemove ${helpRootDir.absolutePath}" + File.separator + "\n"
// pass the config file we created as an argument to the indexer
args '-c',"$configFile"
// tell the indexer where send its output
args '-db', outputFile.absolutePath
// for each help file that was found, add it as an argument to the indexer
helpFiles.each { File file ->
args "${file.absolutePath}"
}
}
}
// Task for building Ghidra help files
// - depends on the output from the help indexer
// - validates help
// - the files generated will be placed in a diretory usable during development mode and will
// eventually be placed in the <Module>.jar file
tasks.register('buildHelp', JavaExec) {
group "private"
dependsOn 'indexHelp'
File helpRootDir = file('src/main/help/help')
File outputDir = file('build/help/main/help')
inputs.dir helpRootDir
inputs.files({
// Note: this must be done lazily in a closure since the classpath is not ready at
// configuration time.
return getHelpInputs(sourceSets.main.runtimeClasspath.files)
})
outputs.dir outputDir
mainClass = 'help.GHelpBuilder'
args '-n', "${project.name}" // use the module's name for the help file name
args '-o', "${outputDir.absolutePath}" // set the output directory arg
// register the Ghidra installation as an application root so the help system can find modules
systemProperties = [
"ADDITIONAL_APPLICATION_ROOT_DIRS": "${ghidraInstallDir}/Ghidra"
]
// args '-debug' // print debug info
doFirst {
//
// The classpath needs to include:
// 1) the jar of each dependent Module that has already been built
// 2) 'src/main/resources'
//
// Each java project and its dependencies are needed to locate each Ghidra module. Each
// module is scanned to find the theme properties files in the 'data' directories.
classpath += sourceSets.main.runtimeClasspath
classpath += files('src/main/resources')
// To build help, the validator needs any other help content that this module may reference.
// Add each of these dependencies as an argument to the validator.
def helpJars = classpath.findAll(file -> hasJarHelp(file))
helpJars.each {
args "-hp"
args "${it.absolutePath}"
}
// The help dir to process. This needs to be the last argument to the process,
// thus, this is why it is inside of this block
args "${helpRootDir.absolutePath}"
// Sigal that any System.out messages from this Java process should be logged at INFO level.
// To see this output, run gradle with the '-i' option to show INFO messages.
logging.captureStandardOutput LogLevel.INFO
}
}
// include the help into the module's jar
jar {
duplicatesStrategy 'exclude'
from "build/help/main" // include the generated help index files
from "src/main/help" // include the help source files
archiveVersion = ""
}
// build the help whenever this module's jar file is built
jar.dependsOn 'buildHelp'
/*********************************************************************************
* End Help Build Code
*********************************************************************************/
/*********************************************************************************
* Takes the given file and returns a string representing the file path with everything
* up-to and including 'src/global' removed, as well as the filename.
*
* eg: If the file path is '/Ghidra/Configurations/Common/src/global/docs/hello.html',
* the returned string will be at /docs
*
* Note: We have to use 'File.separator' instead of a slash ('/') because of how
* windows/unix handle slashes ('/' vs. '\'). We only need to do this in cases where we're
* using java string manipulation libraries (eg String.replace); Gradle already
* understands how to use the proper slash.
*********************************************************************************/
String stripGlobalFilePath(File file) {
// First strip off everything before 'src/global/ in the file path.
def slashIndex = file.path.indexOf('src' + File.separator + 'global')
String filePath = file.path.substring(slashIndex);
// Now remove 'src/global/' from the string.
filePath = filePath.replace('src' + File.separator + 'global' + File.separator, "");
// Now we need to strip off the filename itself, which we do by finding the last
// instance of a slash ('/') in the string. Unfortunately, groovy doesn't give
// us a "lastIndexOf('/')" or something nice like that, so we reverse the string
// and look for the slash that way, remove the filename, then reverse it back.
//
// Note that it's possible there is no slash (all we have is a filename), meaning
// this file will be placed at the root level.
//
String reverseFilePath = filePath.reverse()
slashIndex = reverseFilePath.indexOf(File.separator)
if (slashIndex != -1) {
reverseFilePath = reverseFilePath.substring(slashIndex)
filePath = reverseFilePath.reverse()
}
else {
filePath = ""
}
return filePath
}
/*********************************************************************************
* Returns the current date formatted as yyyyMMdd.
*********************************************************************************/
def getCurrentDate() {
def date = new Date()
def formattedDate = date.format('yyyyMMdd')
return formattedDate
}
/*********************************************************************************
* Throws a GradleException if the current Gradle version is outside of the supported
* Gradle version range defined in application.properties
*********************************************************************************/
import org.gradle.util.GradleVersion;
def checkGradleVersion() {
GradleVersion min = null;
GradleVersion max = null;
try {
min = GradleVersion.version("${rootProject.GRADLE_MIN}")
}
catch (IllegalArgumentException e) {
String defaultMin = "1.0"
println "Invalid minimum Gradle version specified in application.properties...using ${defaultMin}"
min = GradleVersion.version(defaultMin)
}
try {
if (rootProject.GRADLE_MAX) {
max = GradleVersion.version("${rootProject.GRADLE_MAX}")
}
}
catch (IllegalArgumentException e) {
println "Invalid maximum Gradle version specified in application.properties...ignoring"
}
String gradleRange = "at least ${min}"
if (max) {
gradleRange += " and less than ${max}"
}
if (GradleVersion.current() < min || (max && GradleVersion.current() >= max)) {
throw new GradleException("Requires ${gradleRange}, but was run with $gradle.gradleVersion")
}
}