mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-03 17:59:46 +02:00
545 lines
18 KiB
Groovy
545 lines
18 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) {
|
|
group = "Ghidra Private"
|
|
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) {
|
|
group = "Ghidra Private"
|
|
|
|
// 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")
|
|
it.includeEmptyDirs = false
|
|
|
|
// 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/main/java/'
|
|
}
|
|
|
|
task buildExtension (type: Zip) {
|
|
group = "Ghidra Private"
|
|
|
|
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 '.vscode/**'
|
|
exclude '.classpath'
|
|
exclude '.project'
|
|
exclude '.pydevproject'
|
|
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.get()}.${archiveExtension.get()} in ${destinationDirectory.get()}"
|
|
}
|
|
}
|
|
|
|
/*********************************************************************************
|
|
* 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') {
|
|
group = "Ghidra Private"
|
|
|
|
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) {
|
|
group = "Ghidra Private"
|
|
|
|
File helpRootDir = file('src/main/help/help')
|
|
File outputFile = file("build/help/main/help/${project.name}_JavaHelpSearch")
|
|
|
|
inputs.dir helpRootDir skipWhenEmpty()
|
|
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('buildModuleHelp', JavaExec) {
|
|
group = "Ghidra Private"
|
|
|
|
dependsOn 'indexHelp'
|
|
|
|
File helpRootDir = file('src/main/help/help')
|
|
File outputDir = file('build/help/main/help')
|
|
|
|
onlyIf {
|
|
helpRootDir.exists()
|
|
}
|
|
|
|
if (helpRootDir.exists()) {
|
|
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 depended upon 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
|
|
}
|
|
}
|
|
|
|
// a simple task to alias the old 'buildHelp' task to 'buildModuleHelp' so users that uses of the
|
|
// old command will still work for end users
|
|
tasks.register('buildHelp', JavaExec) {
|
|
group = "Ghidra Private"
|
|
dependsOn 'buildModuleHelp'
|
|
}
|
|
|
|
|
|
// 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 'buildModuleHelp'
|
|
|
|
|
|
/*********************************************************************************
|
|
* 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("${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 (GRADLE_MAX) {
|
|
max = GradleVersion.version("${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")
|
|
}
|
|
}
|
|
|