/* ### * 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. */ /***************************************************************************************** This file is a "mix-in" gradle script that individual gradle projects should include if they have content for the Ghidra help system. A gradle project can include help support by adding the following to its build.gradle file. apply from: "$rootProject.projectDir/gradle/helpProject.gradle" Note: This code is copied into buildExtension.gradle. All changes to this file should be made to that file. Help Build System Notes This file contains custom glue coded needed to adapt the structure of Ghidra's help to the Gradle build system. 'Building help' is defined as validating help content, generating all necessary help files used by the Java Help system, and then placing the help and generated content in a place to be consumed by Ghidra, which differs for development mode and production mode. Validating the help content consists of ensuring: hyperlinks point to valid destinations and image references point to existing images. (This is done to find broken help links at build time.) This file supports building help to work in development mode using Eclipse as well as when performing a build of Ghidra. Generated help content is written to: 'build/help/main/help/' Developent Mode The Eclipse projects are setup so that 'build/help/main' is part of the classpath. This triggers Eclipse to copy help resources to the respective project's 'bin' directory, which makes the help content available at runtime in Eclipse. In this setup no jar files are used at runtime. Production Mode In production mode the contents of 'build/help/main' are added to the final output of the Gradle 'jar' task, which will be .jar. Gradle Building During the help build process we place the contents of 'build/help/main' inside of an artifact named -help.jar. This allows us to depend on these artifacts from the projects we depend upon. Specifically, the 'buildHelpFiles' task depends upon the -help.jar artifact from all dependent Modules. To get Gradle's incremental building to work correctly, the following list of inputs is declared for validating and building the help content: 1) Java files in the Help module used to build help - the help building code 2) all dependency data/*.theme.properties - for images used by help via theme IDs 3) all dependency src/main/help folder - for help content 4) all dependency src/main/resources - for images used by help 5) This module's equivalent inputs for 2-3 In order to correctly find these inputs, we use the main runtime classpath to find Ghidra Modules. These modules are then scanned to find these inputs for each module. The final collection of all these items for all dependent Modules is added to the set of task inputs for building help. Thus, when any of the input above change, the help is considered out-of-date and will be rebuilt. The help build code is called via a JavaExec call. This call will pass arguments to the process for any dependent Module's help. This dependent help will be inside of the -help.jar artifact described above. This file contains code to locate those artifacts so they can be passed into the help build java process. *****************************************************************************************/ // The help modules must be configured first so that we can reference its runtime classpath configurations { // This represents the Help module jar file. This is required for building help. helpModule // This is used by the indexHelp task to configure the jar file dependency helpIndex } dependencies { helpIndex "javax.help:javahelp:2.0.05" // signal that we depend on the Help.jar for our Java build files helpModule project(':Help') } 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 } } } /***************************************************************************************** Utility Methods *****************************************************************************************/ // 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) } // Parses the classpath looking for all Module jar file paths, using those to locate the module // that contains that jar file. // Note: In development mode, the .jar file on the classpath may not actually yet be built. // In that case, we can still use that path to locate the module. def getMyModules(Collection fullClasspath) { return fullClasspath.collect(file -> getModulePathFromJar(file)) .findAll(path -> path != null) .collect(path -> new File(path)) } // 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() Collection modules = getMyModules(fullClasspath) modules.each { m -> getHelpInputsFromModule(m.getAbsolutePath(), 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()) } } def getModuleResourcesDirs(Collection fullClasspath) { def modules = getMyModules(fullClasspath) return modules.collect(m -> new File(m, 'src/main/resources')) .findAll(dir -> dir.exists()) } // Locatates 'buildHelp' tasks in projects that this project depends on. The output of the tasks // is the module's help jar, which is only used to build help and not in the final release. The // jar file names follow this format: -help.jar. def getDependentProjectHelpTasks(Collection fullClasspath) { def myModules = getMyModules(fullClasspath) def myProjects = filterProjectsBy(myModules) return myProjects.collect(p -> p.tasks.findByPath('buildHelp')) .findAll(t -> t != null) } // Only projects matching the given collection of modules are returned def filterProjectsBy(Collection modules) { return modules.collect(m -> m.getName()) .collect(name -> rootProject.findProject(name)) .findAll(p -> p != null) } /***************************************************************************************** Tasks *****************************************************************************************/ 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) { group "private" description "indexes the helps files for this module. [gradle/helpProject.gradle]" File helpRootDir = file('src/main/help/help') File outputFile = file("build/help/main/help/${project.name}_JavaHelpSearch") onlyIf ("There is no help root directory") { helpRootDir.exists() } inputs.dir helpRootDir outputs.dir outputFile classpath = configurations.helpIndex 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' } if (helpFiles.isEmpty()) { // must have help to index throw new GradleException("No help files found") } // The index tool has a config file parameter, which allows you to pass arguments via a file // instead of the command line. This is useful when dealing with file paths. 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. We generate this config // file below. 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 // debug // args '-verbose' // 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 Markdown in src/global/docs to HTML // - the files generated will be placed in a build directory usable during development mode tasks.register('buildGlobalMarkdown') { group "private" dependsOn ':MarkdownSupport:classes' FileTree markdownFiles = this.project.fileTree('src/global/docs') { include '*.md' } onlyIf ("There are no markdown files") { !markdownFiles.isEmpty() } inputs.files markdownFiles doFirst { markdownFiles.each { f -> def htmlName = f.name[0..-3] + "html" javaexec { classpath = project(':MarkdownSupport').sourceSets.main.runtimeClasspath mainClass = 'ghidra.markdown.MarkdownToHtml' args f args file("build/src/global/docs/${htmlName}") } } } } // Task for building Ghidra help files // - depends on the output from the help indexer // - validates help // - the files generated will be placed in a directory usable during development mode and will // eventually be placed in: // - the .jar file in production mode, or // - the -help.jar file in development mode tasks.register('buildHelpFiles', JavaExec) { group "private" dependsOn 'indexHelp' // Depend on all -help.jar files for our dependency modules. These jar files must be // built before we run, since the files will be passed to the help builder. Use a closure to // ensure that the classpath is ready when needed. dependsOn({ getDependentProjectHelpTasks(sourceSets.main.runtimeClasspath.files) }) File helpRootDir = file('src/main/help/help') File outputDir = file('build/help/main/help') onlyIf ("There is no help root directory") { helpRootDir.exists() } // // Inputs (used for incremental building): // 1) Java files in the Help module used to build help // 2) all dependency data/**/*.theme.properties - for images used by help via theme IDs // 3) all dependency src/main/help folder - for help content // 4) all dependency src/main/resources - for images used by help // 5) This module's equivalent inputs for 2-3 // // 1) Java files in the Help module used to build help inputs.files(configurations.helpModule) // 2-5) from above 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 // to allow remote debugging of the help build jvm // jvmArgs '-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=13001' // print debug info // args '-debug' doFirst { // // The classpath needs to include items used by internal Java code to validate help // resources: // 1) The jar path of each dependent Module. The jar file will be on the 'main' runtime // classpath, but may not yet exist. Regardless, the Java code will use the path to // locate the module for that path. // 2) Each module's 'src/main/resources' dir (this is needed when the jar files from 1 // above have not been built) // 3) This module's 'src/main/resources' dir // // 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(getModuleResourcesDirs(sourceSets.main.runtimeClasspath.files)) 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. // The dependency file is the -help.jar file from the 'buildHelp' tasks upon which // we depend. def buildHelpTasks = getDependentProjectHelpTasks(sourceSets.main.runtimeClasspath.files) buildHelpTasks.each { def jarFiles = it.outputs.files jarFiles.each { helpJar -> args "-hp" args "${helpJar.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 } } /* * This task creates a jar file that is used by dependent modules only at build time. The name of * this jar is -help.jar. This is in contrast to each module's jar which itself contains * all help needed in production. The module's jar filename is .jar. */ tasks.register('buildHelp', Jar) { group rootProject.GHIDRA_GROUP description " Builds the help for this module. [gradle/helpProject.gradle]\n" dependsOn tasks.named('buildHelpFiles') dependsOn tasks.named('buildGlobalMarkdown') duplicatesStrategy 'exclude' from "build/help/main" // include the generated help and index files from "src/main/help" // include the help source files destinationDirectory = file("build/libs") archiveBaseName = project.name + '-help' } // Task for finding unused images that are not referenced from Ghidra help files tasks.register('findUnusedHelp', JavaExec) { group "private" description " Finds unused help images for this module. [gradle/helpProject.gradle]\n" File helpRootDir = file('src/main/help/help') inputs.dir helpRootDir inputs.files(configurations.helpModule) mainClass = 'help.validator.UnusedHelpImageFileFinder' // args '-debug' // print debug info doFirst { classpath sourceSets.main.runtimeClasspath // the current help dir to process args "-hp" args "${helpRootDir.absolutePath}" } } // include the help into the module's jar jar { duplicatesStrategy 'exclude' from "build/help/main" // include the generated help and index files from "src/main/help" // include the help source files } // build the help whenever this module's jar file is built processResources.dependsOn buildHelp jar.dependsOn buildHelp // make sure generated help directories exist during prepdev so that the directories are created and // eclipse doesn't complain about missing src directories. rootProject.prepDev.dependsOn buildHelp