GP-3430 - Updated the gradle buildHelp task to better handle its inputs up-do-date state

This commit is contained in:
dragonmacher 2023-06-06 15:25:25 -04:00
parent 0d71657d05
commit a7668c7f85
32 changed files with 690 additions and 266 deletions

View File

@ -127,7 +127,7 @@ if (System.env.LLVM_HOME) {
}
}
} else {
println "Debugger-swig-lldb:buildNatives skipped - LLVM_HOME not defined"
logger.debug('Debugger-swig-lldb:buildNatives skipped - LLVM_HOME not defined')
}
task checkLLVM {

View File

@ -31,10 +31,6 @@ dependencies {
api project(':Decompiler')
api project(':ProposedUtils')
helpPath project(path: ':Base', configuration: 'helpPath')
helpPath project(path: ':Decompiler', configuration: 'helpPath')
helpPath project(path: ':ProgramDiff', configuration: 'helpPath')
testImplementation project(path: ':Base', configuration: 'testArtifacts')
testImplementation project(path: ':Framework-AsyncComm', configuration: 'testArtifacts')
testImplementation project(path: ':Framework-Debugging', configuration: 'testArtifacts')

View File

@ -23,7 +23,6 @@ eclipse.project.name = 'Xtra MachineLearning'
dependencies {
api project(':Base')
helpPath project(path: ":Base", configuration: 'helpPath')
api "com.oracle.labs.olcut:olcut-config-protobuf:5.2.0" //{exclude group: "com.google.protobuf", module: "protobuf-java"}
api ("com.oracle.labs.olcut:olcut-core:5.2.0") {exclude group: "org.jline"}

View File

@ -61,7 +61,6 @@ dependencies {
testImplementation project(path: ':Project', configuration: 'testArtifacts')
testImplementation project(path: ':SoftwareModeling', configuration: 'testArtifacts')
testImplementation project(path: ':DB', configuration: 'testArtifacts')
helpPath project(path: ':Docking', configuration: 'helpPath') // this module's help has links to Base help files
javacc 'net.java.dev.javacc:javacc:5.0'
}
@ -125,22 +124,29 @@ task buildJavacc {
}
// Note: this must happen before the standard buildHelp for Base
task generateExtraHelpFiles {
tasks.register('generateExtraHelpFiles') {
group = 'private'
description " Creates any extra help files for Base not covered by the standard build help system"
def rawTipsFile = file('src/main/resources/ghidra/app/plugin/core/totd/tips.txt')
def rawTipsFile = file('src/main/resources/ghidra/app/plugin/core/totd/tips.txt')
inputs.file(rawTipsFile)
def htmlTipsFile = file('src/main/help/help/topics/Misc/Tips.htm')
def htmlTipsFile = file('build/help/main/help/topics/Misc/Tips.htm')
outputs.file(htmlTipsFile)
doLast {
doLast {
createTipsHelpFile(rawTipsFile, htmlTipsFile)
}
}
// Base's help includes the file generated by the 'generateExtraHelpFiles' task. Signal that we
// depend on that task and it's output file.
tasks.named('buildHelp') {
dependsOn(tasks.named('generateExtraHelpFiles'))
inputs.files tasks.named('generateExtraHelpFiles').get().outputs
}
def createTipsHelpFile(input, output) {
// transform original contents - wrap each line in <li> tags
def buffy = new StringBuilder()
@ -169,7 +175,7 @@ def createTipsHelpFile(input, output) {
output.text = htmlContent
println '\n\n\nwrote file ' + output + '\n\n\n'
logger.info '\n\n\nwrote file ' + output + '\n\n\n'
}

View File

@ -24,8 +24,5 @@ eclipse.project.name = 'Features BytePatterns'
dependencies {
api project(':Base')
api project(':Utility')
helpPath project(path: ":Base", configuration: 'helpPath')
api project(':Utility')
}

View File

@ -26,6 +26,4 @@ dependencies {
api project(':Base')
testImplementation project(path: ':SoftwareModeling', configuration: 'testArtifacts')
helpPath project(path: ':Base', configuration: 'helpPath') // this module's help has links to Base help files
}

View File

@ -32,8 +32,6 @@ dependencies {
// include Base src/test/resources when running decompiler integration tests (uses defaultTools)
integrationTestImplementation project(path: ':Base', configuration: 'testArtifacts')
integrationTestImplementation project(path: ':SoftwareModeling', configuration: 'testArtifacts')
helpPath project(path: ":Base", configuration: 'helpPath')
}
// Include buildable native source in distribution

View File

@ -47,8 +47,6 @@ dependencies {
api 'net.sf.sevenzipjbinding:sevenzipjbinding:16.02-2.01'
runtimeOnly 'net.sf.sevenzipjbinding:sevenzipjbinding-all-platforms:16.02-2.01'
helpPath project(path: ":Base", configuration: 'helpPath')
// include code from src/test/slow in Base
testImplementation project(path: ':Base', configuration: 'integrationTestArtifacts')
}

View File

@ -22,15 +22,9 @@ apply plugin: 'eclipse'
eclipse.project.name = 'Features Graph FunctionGraph'
dependencies {
api project(":Base")
api project(":GraphServices")
helpPath project(path: ":Base", configuration: 'helpPath')
}

View File

@ -770,10 +770,6 @@
mouse scroll wheel. Disabling this option restores the original function graph scroll wheel
behavior of zooming when scrolled.</P>
<P>The <B>Start Fully Zoomed Out</B> option causes the initial graph to zoom out far enough
that the entire graph is displayed. When this option is off a new graph rendering will zoom
all the way in (no scaling) to the active vertex.</P>
<P>The <B>Update Vertex Colors When Grouping</B> option signals to the graph to make the
color of the grouped vertex be that of the vertices being grouped.</P>

View File

@ -26,9 +26,7 @@ eclipse.project.name = 'Features FunctionID'
dependencies {
api project(":Base")
api project(":DB")
api project(":SoftwareModeling")
helpPath project(path: ":Base", configuration: 'helpPath')
api project(":SoftwareModeling")
}
// All *.fidb files located in the dependencies/fid directory OR the

View File

@ -26,14 +26,7 @@ eclipse.project.name = 'Features Graph FunctionCalls'
// Note: this module's name is 'GraphFunctionCalls'
dependencies {
api project(":Base")
helpPath project(path: ":Base", configuration: 'helpPath')
// This is needed now because we like to the help of the FunctionGraph. If and when that
// help is extracted to a higher-level help page, like 'Graphing', then this link should be
// removed
helpPath project(path: ":FunctionGraph", configuration: 'helpPath')
// These have abstract test classes and stubs needed by this module
testImplementation project(path: ':Project', configuration: 'testArtifacts')
testImplementation project(path: ':SoftwareModeling', configuration: 'testArtifacts')

View File

@ -226,20 +226,88 @@
<H2><A name="Satellite_View"></A>Satellite View</H2>
<BLOCKQUOTE>
<P>The Satellite View works exactly as the
<A href="help/topics/FunctionGraphPlugin/Function_Graph.html#Satellite_View">
Function Graph's Satellite View</A>.
</P>
</BLOCKQUOTE>
<P>The Satellite View provides an overview of the graph. From this view you may also perform
basic adjustment of the overall graph location. In addition to the complete graph, the
satellite view contains a <B>lens</B> (the white rectangle) that indicates how much of the
current graph fits into the primary view.</P>
<P>When you single left mouse click in the satellite view the graph is centered around the
corresponding point in the primary view. Alternatively, you may drag the lens of the
satellite view to the desired location by performing a mouse drag operation on the lens.</P>
<P>You may hide the satellite view by right-clicking anywhere in the Primary View and
deselecting the <B>Display Satellite View</B> toggle button from the popup menu.</P>
<BLOCKQUOTE>
<P><IMG src="help/shared/tip.png" alt="" border="0"> If the Primary View is painting
sluggishly, then hiding the Satellite View cause the Primary View to be more
responsive.</P>
</BLOCKQUOTE>
<H3><A name="Satellite_View_Dock"></A>Detached Satellite</H3>
<BLOCKQUOTE>
<P>The Satellite View is attached, or <B>docked</B>, to the Primary View by default.
However, you can detach, or undock, the Satellite View, which will put the view into a
Component Provider, which itself can be moved, resized and docked anywhere in the Tool you
wish.</P>
<P>To undock the Satellite View, right-click in the graph and deselect the <B>Dock
Satellite View</B> menu item.</P>
<P>To re-dock the Satellite View, right-click in the graph and select the <B>Dock Satellite
View</B> menu item.</P>
</BLOCKQUOTE>
<BLOCKQUOTE>
<P><IMG src="help/shared/tip.png" alt="" border="0"> To reshow the Satellite View if it is
hidden, whether docked or undocked, you can press the <IMG src=
"images/network-wireless.png" alt="" border="1"> button. This button is in the lower-right
hand corner of the graph and is only visible if the Satellite View is hidden or
undocked.</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<H2><A name="Options"></A>Options</H2>
<BLOCKQUOTE>
<P>The Function Call Graph options are currently a subset of the
<A href="help/topics/FunctionGraphPlugin/Function_Graph.html#Options">
Function Graph's Options</A>.
</P>
</BLOCKQUOTE>
<P>The <B>Scroll Wheel Pans</B> option signals to move the graph vertical when scrolling the
mouse scroll wheel. Disabling this option restores the original function graph scroll wheel
behavior of zooming when scrolled.</P>
<P>The <B>Use Animation</B> option signals to the graph whether to animate mutative graph
operations and navigations.</P>
<P><A name="Layout_Compressing"></A>The <B>Use Condensed Layout</B> option signals to the
graph to bring vertices as close together as possible when laying out the graph. Using this
option to fit as many vertices on the screen as possible. Disable this option to make the
overall layout of the graph more aesthetic.</P>
<P>The <B>Use Mouse-relative Zoom</B> option signals zoom the graph to and from the mouse
location when zooming from the middle-mouse. The default for this option is off, which
triggers zoom to work from the center of the graph, regardless of the mouse location.</P>
<P>The <B>View Settings</B> option describes how the graph will be zoomed when it is first
loaded. The values are:</P>
<UL>
<LI><B>Start Fully Zoomed Out</B> - always start fully zoomed out so that the entire
graph can be seen.</LI>
<LI><B>Start Fully Zoomed In</B> - always start fully zoomed in on the vertex containing
the current location.</LI>
<LI><B>Remember User Settings</B> - keep the zoom level where the user previously left
it.</LI>
</UL>
<BR>
<BR>
<P>There are various edge color and highlight color options available to change. The
highlight colors are those to be used when the flow animations take place.</P>
</BLOCKQUOTE>
</BLOCKQUOTE>

View File

@ -36,8 +36,5 @@ dependencies {
api ("org.jgrapht:jgrapht-io:1.5.1") { exclude group: "org.antlr", module: "antlr4-runtime" }
runtimeOnly "org.jheaps:jheaps:0.13"
helpPath project(path: ":Base", configuration: 'helpPath')
}

View File

@ -33,8 +33,7 @@ dependencies {
// Demangler Analyzer needs to find MicrosoftDemangler
api project(":MicrosoftDemangler")
helpPath project(path: ':Base', configuration: 'helpPath') // this module's help has links to Base help files
testImplementation project(path: ':Base', configuration: 'testArtifacts')
testImplementation project(path: ':SoftwareModeling', configuration: 'testArtifacts')
}

View File

@ -25,6 +25,4 @@ eclipse.project.name = 'Features ProgramDiff'
dependencies {
api project(":Base")
helpPath project(path: ":Base", configuration: 'helpPath')
}

View File

@ -25,9 +25,6 @@ eclipse.project.name = 'Features Graph ProgramGraph'
dependencies {
api project(":Base")
helpPath project(path: ":Base", configuration: 'helpPath')
helpPath project(path: ":GraphServices", configuration: 'helpPath')
api project(":GraphServices")
}

View File

@ -495,8 +495,7 @@
<P>These are the display options for graphs that are types of "Program Graphs" such as
Call graphs, Block graphs, etc. These types of graphs
use program elements as vertices and reference types as edges. See
<A href="help/topics/GraphServices/GraphDisplay.htm#Graph_Type_Display_Options">Graph Type Display Options</A> for
general help on graph type display options.</P>
<B>Graph Type Display Options</B> for general help on graph type display options.</P>
</BLOCKQUOTE>
<P class="providedbyplugin">Provided by: <I>Program Graph Plugin</I></P>

View File

@ -33,8 +33,7 @@ configurations {
dependencies {
api project(':Base')
helpPath project(path: ":Base", configuration: "helpPath")
jython JYTHON
jython JYTHON
api JYTHON
}

View File

@ -25,5 +25,4 @@ eclipse.project.name = 'Features SourceCodeLookup'
dependencies {
api project(":Base")
api project(":Decompiler")
helpPath project(path: ":Decompiler", configuration: 'helpPath')
}

View File

@ -27,8 +27,6 @@ project.ext.excludeFromParallelIntegrationTests = true
dependencies {
api project(":Base")
helpPath project(path: ":Base", configuration: "helpPath")
testImplementation project(path: ':Project', configuration: 'testArtifacts')
testImplementation project(path: ':SoftwareModeling', configuration: 'testArtifacts')
}

View File

@ -30,6 +30,4 @@ dependencies {
// Only include this debug version of the jh library if necessary.
//api name:'jh2.with.debug'
api 'javax.help:javahelp:2.0.05'
}

View File

@ -357,17 +357,6 @@ public class GHelpBuilder {
errorMessage(buffy.toString());
}
private static void warningMessage(String... message) {
StringBuilder buffy = new StringBuilder();
buffy.append("\n");
buffy.append(" !!!!! WARNING !!!!!\n");
for (String string : message) {
buffy.append('\t').append('\t').append(string).append('\n');
}
buffy.append("\n");
errorMessage(buffy.toString());
}
private static void printErrorMessage(String message) {
// this prevents error messages getting interspersed with output messages
flush();

View File

@ -68,7 +68,11 @@ public class HelpBuildUtils {
return new DirectoryHelpModuleLocation(file);
}
else if (file.isFile()) {
return new JarHelpModuleLocation(file);
JarHelpModuleLocation jarLocation = JarHelpModuleLocation.fromFile(file);
if (jarLocation == null) {
HelpBuildUtils.debug("Jar file does not contain help: " + file);
}
return jarLocation;
}
throw new IllegalArgumentException(
"Don't know how to create a help module location for file: " + file);

View File

@ -15,16 +15,16 @@
*/
package help;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import ghidra.util.exception.AssertException;
import help.validator.LinkDatabase;
import help.validator.location.HelpModuleCollection;
import help.validator.model.AnchorDefinition;
import help.validator.model.GhidraTOCFile;
import java.io.*;
import java.nio.file.*;
import java.util.*;
/**
* This class:
* <ul>
@ -129,7 +129,8 @@ public class JavaHelpFilesBuilder {
PrintWriter out = new LogFileWriter(mapFile);
try {
out.println("<?xml version='1.0' encoding='ISO-8859-1' ?>");
out.println("<!doctype MAP public \"-//Sun Microsystems Inc.//DTD JavaHelp Map Version 1.0//EN\">");
out.println(
"<!doctype MAP public \"-//Sun Microsystems Inc.//DTD JavaHelp Map Version 1.0//EN\">");
out.println("<!-- Auto-generated on " + (new Date()).toString() + " : Do Not Edit -->");
out.println("<map version=\"1.0\">");
@ -166,8 +167,8 @@ public class JavaHelpFilesBuilder {
}
if (!parent.endsWith("help")) {
throw new AssertException("Map file expected in a directory name 'help'. "
+ "Update the map file generation code.");
throw new AssertException("Map file expected in a directory name 'help'. " +
"Update the map file generation code.");
}
if (!anchorTarget.startsWith("help")) {

View File

@ -92,8 +92,11 @@ public class UnusedHelpImageFileFinder {
}
SortedSet<Path> set =
new TreeSet<>((f1, f2) -> f1.toUri().toString().toLowerCase().compareTo(
f2.toUri().toString().toLowerCase()));
new TreeSet<>((f1, f2) -> f1.toUri()
.toString()
.toLowerCase()
.compareTo(
f2.toUri().toString().toLowerCase()));
for (Path file : imageFiles) {
IMG img = fileToIMGMap.get(file);
if (img == null && !isExcludedImageFile(file)) {
@ -173,7 +176,10 @@ public class UnusedHelpImageFileFinder {
}
// Create the help directory
helpCollections.add(HelpBuildUtils.toLocation(helpDirectoryFile));
HelpModuleLocation location = HelpBuildUtils.toLocation(helpDirectoryFile);
if (location != null) {
helpCollections.add(location);
}
}
return helpCollections;

View File

@ -20,15 +20,16 @@ import java.net.*;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import javax.help.HelpSet;
import javax.help.Map.ID;
import javax.help.TOCView;
import javax.swing.tree.DefaultMutableTreeNode;
import help.CustomTOCView.CustomTreeItemDecorator;
import help.HelpBuildUtils;
import help.TOCItemProvider;
import help.CustomTOCView.CustomTreeItemDecorator;
import help.validator.model.*;
/**
@ -98,7 +99,10 @@ public class HelpModuleCollection implements TOCItemProvider {
}
private HelpModuleCollection(Collection<HelpModuleLocation> locations) {
helpLocations = new LinkedHashSet<>(locations);
helpLocations = locations.stream()
.filter(l -> l != null)
.collect(Collectors.toCollection(LinkedHashSet::new));
loadTOCs();

View File

@ -22,6 +22,7 @@ import java.util.*;
import javax.help.HelpSet;
import ghidra.util.exception.AssertException;
import help.HelpBuildUtils;
import help.validator.model.*;
public abstract class HelpModuleLocation {
@ -47,13 +48,16 @@ public abstract class HelpModuleLocation {
public abstract HelpSet loadHelpSet();
/** Returns true if this help location represents a source of input files to generate help output */
/**
* Returns true if this help location represents a source of input files to generate help output
* @return true if this help location represents a source of input files to generate help output
*/
public abstract boolean isHelpInputSource();
protected void loadHelpTopics() {
Path helpTopicsDir = helpDir.resolve("topics");
if (!Files.exists(helpTopicsDir)) {
throw new AssertException("No topics found in help dir: " + helpDir);
HelpBuildUtils.debug("No topics found in help dir: " + this);
}
try (DirectoryStream<Path> ds = Files.newDirectoryStream(helpTopicsDir);) {
@ -65,7 +69,7 @@ public abstract class HelpModuleLocation {
}
catch (IOException e) {
// I suppose there aren't any
throw new AssertException("No topics found in help dir: " + helpDir);
throw new AssertException("No topics found in help dir: " + this);
}
}
@ -193,7 +197,7 @@ public abstract class HelpModuleLocation {
}
List<AnchorDefinition> list = map.get(name);
if (list == null) {
list = new ArrayList<AnchorDefinition>();
list = new ArrayList<>();
map.put(name, list);
}
list.add(anchorDefinition);

View File

@ -21,7 +21,6 @@ import java.net.*;
import java.nio.file.*;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import javax.help.HelpSet;
import javax.help.HelpSetException;
@ -32,17 +31,29 @@ import help.validator.model.GhidraTOCFile;
public class JarHelpModuleLocation extends HelpModuleLocation {
/*
* format of 'helpDir':
* jar:file:///.../ghidra-prep/Ghidra/Features/Base/build/libs/Base.jar!/help
*/
private static final Pattern JAR_FILENAME_PATTERN = Pattern.compile(".*/(\\w*)\\.jar!/.*");
private static Map<String, String> env = new HashMap<String, String>();
private static Map<String, String> env = new HashMap<>();
static {
env.put("create", "false");
}
public static JarHelpModuleLocation fromFile(File jar) {
FileSystem fs = getOrCreateJarFS(jar);
Path helpRootPath = fs.getPath("/help");
if (!Files.exists(helpRootPath)) {
return null;
}
Path topicsPath = helpRootPath.resolve("topics");
if (!Files.exists(topicsPath)) {
// all help locations must have a topics directory; we can get here if the jar contains
// a package with the name 'help'
return null;
}
return new JarHelpModuleLocation(jar, helpRootPath);
}
private static FileSystem getOrCreateJarFS(File jar) {
URI jarURI;
try {
@ -64,8 +75,8 @@ public class JarHelpModuleLocation extends HelpModuleLocation {
}
}
public JarHelpModuleLocation(File file) {
super(getOrCreateJarFS(file).getPath("/help"));
private JarHelpModuleLocation(File jar, Path path) {
super(path);
}
@Override
@ -101,9 +112,11 @@ public class JarHelpModuleLocation extends HelpModuleLocation {
}
private String getModuleName(File jarFile) {
String name = jarFile.getName();
int dotIndex = name.indexOf('.');
int dotIndex = name.indexOf("-help.jar"); // dev mode
if (dotIndex == -1) {
dotIndex = name.indexOf(".jar"); // production mode
}
return name.substring(0, dotIndex);
}

View File

@ -17,8 +17,6 @@ package help.validator.location;
import java.nio.file.Path;
import help.validator.location.HelpModuleLocation;
public abstract class HelpModuleLocationTestDouble extends HelpModuleLocation {
// this class exists to open up the package-level constructor

View File

@ -42,14 +42,6 @@ file(ghidraDir + "/application.properties").withReader { reader ->
***************************************************************************************/
checkGradleVersion()
configurations {
helpPath
}
artifacts {
helpPath jar
}
task copyDependencies(type: Copy) {
from configurations.runtimeClasspath
into "lib"
@ -76,7 +68,6 @@ dependencies {
api fileTree(dir: ghidraDir + '/Features', include: "**/*.jar")
api fileTree(dir: ghidraDir + '/Debug', include: "**/*.jar")
api fileTree(dir: ghidraDir + '/Processors', include: "**/*.jar")
helpPath fileTree(dir: ghidraDir + '/Features/Base', include: "**/Base.jar")
}
def ZIP_NAME_PREFIX = "${DISTRO_PREFIX}_${RELEASE_NAME}_${getCurrentDate()}"
@ -180,73 +171,211 @@ task buildExtension (type: Zip) {
}
}
// task for calling the java help indexer
task indexHelp(type: JavaExec) {
File helpRootDir = file('src/main/help') // this the root dir for the help source
File outputFile = file("build/help/main/help/${project.name}_JavaHelpSearch")
onlyIf {helpRootDir.exists()}
/*********************************************************************************
* Help Build Code
* Note: This code is derived from helpProject.gradle. Required changes should
* be made to that file and then reapplied here.
*********************************************************************************/
dependsOn configurations.helpPath
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 // this modules runtime classpath (contains jhall.jar)
mainClass = 'com.sun.java.help.search.Indexer' // main class to call
// tell the indexer where send its output
classpath = sourceSets.main.runtimeClasspath
args '-db', outputFile.absolutePath
mainClass = 'com.sun.java.help.search.Indexer'
// 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') // this is the config file that we will create
// create the config file when the task runs and not during configuration.
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"
// pass the config file we created as an argument to the indexer
args '-c',"$configFile"
// gather up all the help files into a file collection
FileTree helpFiles = fileTree('src/main/help') {
include '**/*.htm'
include '**/*.html'
}
doFirst {
// 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}"
}
}
group "private"
description "indexes the helps files for this module. [gradle/helpProject.gradle]"
}
// task for building Ghidra help files - depends on the output from the help indexer
task buildHelp(type: JavaExec, dependsOn: indexHelp) {
File helpRootDir = file('src/main/help/help') // help root where topics dir lives
File outputDir = file('build/help/main/help') // dir where we want the help files to be generated
onlyIf {helpRootDir.exists()}
// 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
classpath = sourceSets.main.runtimeClasspath // this modules runtime classpath (contains jhall.jar)
mainClass = 'help.GHelpBuilder' // program to run to build help files.
mainClass = 'help.GHelpBuilder'
args '-n', "${project.name}" // use the modules name as the base for the help file name
args '-n', "${project.name}" // use the module's name for the help file name
args '-o', "${outputDir.absolutePath}" // set the output directory arg
@ -254,21 +383,48 @@ task buildHelp(type: JavaExec, dependsOn: indexHelp) {
systemProperties = [
"ADDITIONAL_APPLICATION_ROOT_DIRS": "${ghidraInstallDir}/Ghidra"
]
// args '-debug' // print debug info
doFirst {
configurations.helpPath.each {
//
// 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}"
}
args "${helpRootDir.absolutePath}" // tell the help builder what help dir to process
// 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
}
description " Builds the help for this module. [gradle/helpProject.gradle]\n"
}
// 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
from "src/main/help" // include the help source files
archiveVersion = ""
}
@ -276,6 +432,10 @@ jar {
jar.dependsOn 'buildHelp'
/*********************************************************************************
* End Help Build Code
*********************************************************************************/
/*********************************************************************************
* Takes the given file and returns a string representing the file path with everything

View File

@ -18,54 +18,216 @@
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"
Clients can register dependencies on other help modules by using the
'helpPath' configuration, like so:
apply from: "$rootProject.projectDir/gradle/helpProject.gradle"
helpPath project(path: ":Base", configuration: 'helpPath')
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 example will put into the 'helpPath' configuration the project path of
'Base', using the value of it's 'helpPath' configuration. This brings us to the next
point--'helpPath' gets updated when the jar file is built to include the path of
the generated jar file. This allows future clients of a given help module to get
the jar file path of that help module's output.
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 <Module>.jar.
Gradle Building
During the help build process we place the contents of 'build/help/main' inside of an
artifact named <Module>-help.jar. This allows us to depend on these artifacts from the
projects we depend upon. Specifically, the 'buildHelpFiles' task depends upon the
<Module>-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
<Module>-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 {
// The Help Build System takes optional paths to resolve dependencies. Build files
// that use this 'script plugin' may put project paths into this variable.
helpPath
}
artifacts {
// The helpPath is updated to include the jar file output of the help build.
helpPath jar
}
sourceSets {
helpIndex {
java {
}
}
main {
resources {
srcDir 'src/main/help'
srcDir 'build/help/main'
}
}
// 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 {
helpIndexImplementation "javax.help:javahelp:2.0.05"
helpIndexImplementation project(':Help')
helpIndex "javax.help:javahelp:2.0.05"
// signal that we depend on the Help.jar for our Java build files
helpModule project(':Help')
}
// Task for calling the java help indexer, which creates a searchable index of the
// help contents.
task indexHelp(type: JavaExec) {
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 <Module>.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<File> 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<File> fullClasspath) {
if (ext.helpInputsCache != null) {
return ext.helpInputsCache
}
def results = new HashSet<File>()
Collection<File> 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<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())
}
}
def getModuleResourcesDirs(Collection<File> 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: <Module>-help.jar.
def getDependentProjectHelpTasks(Collection<File> 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<File> 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]"
@ -73,44 +235,42 @@ task indexHelp(type: JavaExec) {
File helpRootDir = file('src/main/help/help')
File outputFile = file("build/help/main/help/${project.name}_JavaHelpSearch")
dependsOn configurations.helpPath
dependsOn configurations.runtimeClasspath
inputs.dir helpRootDir
outputs.dir outputFile
classpath = sourceSets.helpIndex.runtimeClasspath
classpath = configurations.helpIndex
mainClass = 'com.sun.java.help.search.Indexer'
// tell the indexer where send its output
args '-db', outputFile.absolutePath
// 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')
// gather up all the help files into a file collection
FileTree helpFiles = fileTree('src/main/help') {
include '**/*.htm'
include '**/*.html'
}
// pass the config file we created as an argument to the indexer
args '-c',"$configFile"
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
// for each help file that was found, add it as an argument to the indexer
helpFiles.each { File file ->
@ -119,31 +279,49 @@ task indexHelp(type: JavaExec) {
}
}
task cleanHelp {
File helpOutput = file('build/help/main/help')
doFirst {
delete helpOutput
}
}
// Task for building Ghidra help files
// - depends on the output from the help indexer
task buildHelp(type: JavaExec, dependsOn: indexHelp) {
group rootProject.GHIDRA_GROUP
description " Builds the help for this module. [gradle/helpProject.gradle]\n"
// - 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 in production mode, or
// - the <Module>-help.jar file in development mode
tasks.register('buildHelpFiles', JavaExec) {
outputs.upToDateWhen {false} // TODO remove this. we should be able to get the inputs right
// the issue is if docs get changed in a dependant module,
// this modules needs to know to rebuild
group "private"
dependsOn 'indexHelp'
// Depend on all <Module>-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')
inputs.dir file('src/main/help');
File resourcesDir = file('src/main/resources')
if (resourcesDir.exists()) {
inputs.dir resourcesDir
}
//
// 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'
@ -157,52 +335,93 @@ task buildHelp(type: JavaExec, dependsOn: indexHelp) {
doFirst {
// this module's runtime classpath (contains jhall.jar)
classpath project(':Help').sourceSets.main.runtimeClasspath
// include the classpath for the project using this Help gradle script plugin
classpath += project.sourceSets.main.runtimeClasspath
// since this runs BEFORE resources are copied, include the resources source dir in
// the classpath so the help validator can find them
classpath += files('src/main/resources')
//
// 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
//
configurations.helpPath.each {
args "-hp"
args "${it.absolutePath}"
}
// 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 <Module>-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 <Module>-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 <Module>.jar.
*/
tasks.register('buildHelp', Jar) {
group rootProject.GHIDRA_GROUP
description " Builds the help for this module. [gradle/helpProject.gradle]\n"
dependsOn tasks.named('buildHelpFiles')
duplicatesStrategy 'exclude'
from "build/help/main" // include the generated help and index files from
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
task findUnusedHelp(type: JavaExec) {
group rootProject.GHIDRA_GROUP
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')
File outputDir = file('build/help/main/help')
dependsOn configurations.helpPath
inputs.dir helpRootDir
inputs.files(configurations.helpModule)
mainClass = 'help.validator.UnusedHelpImageFileFinder'
// args '-debug' // print debug info
doFirst {
classpath project(':Help').sourceSets.main.runtimeClasspath
classpath sourceSets.main.runtimeClasspath
// the current help dir to process
args "-hp"
args "${helpRootDir.absolutePath}"
}
}
@ -210,7 +429,7 @@ task findUnusedHelp(type: JavaExec) {
// include the help into the module's jar
jar {
duplicatesStrategy 'exclude'
from "build/help/main" // include the generated help index files
from "build/help/main" // include the generated help and index files
from "src/main/help" // include the help source files
}
@ -222,3 +441,4 @@ 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