/* ### * 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_) * *****************************************************************************************/ 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 '.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() 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 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 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 .jar file tasks.register('buildHelp', JavaExec) { group "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 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("${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") } }