Merge remote-tracking branch

'origin/GP-5819-dragonmacher-help-build-update--SQUASHED' (Closes #8320)
This commit is contained in:
Ryan Kurtz
2025-07-21 13:21:42 -04:00
23 changed files with 427 additions and 155 deletions

View File

@@ -123,7 +123,7 @@ task buildJavacc {
description = " Compiles the JavaCC files\n"
}
// Note: this must happen before the standard buildHelp for Base
// Note: this must happen before the standard buildModuleHelp for Base
tasks.register('generateExtraHelpFiles') {
group = 'private'
@@ -193,7 +193,7 @@ def createTipsHelpFile(input, output) {
compileJava.dependsOn buildJavacc
rootProject.prepDev.dependsOn buildJavacc
// 'indexHelp' is defined in the buildHelp.gradle 'script plugin'
// 'indexHelp' is defined in the helpProject.gradle 'script plugin'
indexHelp.dependsOn generateExtraHelpFiles
zipSourceSubproject.dependsOn buildCPPParser

View File

@@ -3,7 +3,10 @@
<tocroot>
<tocref id="Ghidra Functionality">
<tocref id="Scripting">
<tocdef id="Jython Interpreter" sortgroup="z" text="Jython Interpreter" target="help/topics/Jython/interpreter.html" />
<!-- The sort group places this entry at the bottom of all other entries in the Program
Annotation section. 'z' puts it at the end. The rest makes it unique. -->
<tocdef id="Jython Interpreter" sortgroup="z_Python_Jython" text="Jython Interpreter" target="help/topics/Jython/interpreter.html" />
</tocref>
</tocref>
</tocroot>

View File

@@ -50,7 +50,10 @@
<tocroot>
<tocref id="Program Annotation">
<tocdef id="PDB" sortgroup="s" text="PDB" target="help/topics/Pdb/PDB.htm" >
<!-- The sort group places this entry at the bottom of all other entries in the Program
Annotation section. 'z' puts it at the end. The rest makes it unique. -->
<tocdef id="PDB" sortgroup="z_PDB" text="PDB" target="help/topics/Pdb/PDB.htm" >
<tocdef id="LoadPDB" sortgroup="a" text="Load PDB File" target="help/topics/Pdb/LoadPDB.html" />
<tocdef id="README_PDB" sortgroup="b" text="PDB Parser (README_PDB)" target="external:docs/README_PDB.html" />
</tocdef>

View File

@@ -3,7 +3,10 @@
<tocroot>
<tocref id="Ghidra Functionality">
<tocref id="Scripting">
<tocdef id="PyGhidra Interpreter" sortgroup="z" text="PyGhidra Interpreter" target="help/topics/PyGhidra/interpreter.html" />
<!-- The sort group places this entry at the bottom of all other entries in the Program
Annotation section. 'z' puts it at the end. The rest makes it unique. -->
<tocdef id="PyGhidra Interpreter" sortgroup="z_Python_PyGhidra" text="PyGhidra Interpreter" target="help/topics/PyGhidra/interpreter.html" />
</tocref>
</tocref>
</tocroot>

View File

@@ -50,7 +50,10 @@
<tocroot>
<tocref id="Program Annotation">
<tocdef id="SARIF" sortgroup="s" text="SARIF" target="help/topics/Sarif/SARIF.htm" >
<!-- The sort group places this entry at the bottom of all other entries in the Program
Annotation section. 'z' puts it at the end. The rest makes it unique. -->
<tocdef id="SARIF" sortgroup="z_Sarif" text="SARIF" target="help/topics/Sarif/SARIF.htm" >
</tocdef>
</tocref>
</tocroot>

View File

@@ -4,9 +4,9 @@
* 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.
@@ -31,3 +31,85 @@ dependencies {
//api name:'jh2.with.debug'
api 'javax.help:javahelp:2.0.05'
}
/*
* Get all help tasks from all projects
*/
def getAllHelpTasks() {
List list = new ArrayList()
rootProject.allprojects {
tasks.each {
if (it.name == 'buildModuleHelp') {
list.add(it)
}
}
}
return list
}
/*
* A task at the Ghidra tool top-level build that will trigger all 'buildModuleHelp' tasks to run,
* this building help. Additionally, this task will perform any needed validation after all help
* modules are built.
*/
tasks.register('buildHelp') {
description 'Build all Ghidra help'
dependsOn { getAllHelpTasks() } // all 'buildHelpModule' tasks
dependsOn 'validateHelpTocFiles' // the validate TOC task to run after all help is built
}
/*
* Gathers all generated help TOC files and validates them.
*/
tasks.register('validateHelpTocFiles', JavaExec) {
group = "private"
dependsOn { getAllHelpTasks() } // all 'buildHelpModule' tasks to provide TOC inputs
// we need our Help java files compiled
dependsOn jar
mainClass = 'help.GHelpTocValidator'
// to allow remote debugging of the help build jvm
// jvmArgs '-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=13001'
// print debug info
// args '-debug'
doFirst {
// get the Help module jar file, since it has classes we use
classpath += jar.outputs.files
// get the classpath items used by the Help module, since we use those classes
classpath += configurations.runtimeClasspath
// add each Module-help.jar file created by 'buildModuleHelp' to the application inputs
rootProject.allprojects.forEach {
// <module>/build/libs/Module-help.jar
def helpLibDir = it.file('build/libs')
if (helpLibDir.isDirectory()) {
helpLibDir.listFiles().each {
String name = it.getName()
if (name.endsWith('-help.jar')) {
args "${it.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
}
}

View File

@@ -15,6 +15,8 @@
*/
package help;
import static help.GHelpMsg.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
@@ -96,7 +98,7 @@ public class GHelpBuilder {
if (results.failed()) {
String message = "Found invalid help:\n" + results.getMessage();
if (ignoreInvalid) {
printErrorMessage(message);
error(message);
}
else {
exitWithError(message, null);
@@ -267,15 +269,6 @@ public class GHelpBuilder {
}
}
private static void flush() {
System.out.flush();
System.out.println();
System.out.flush();
System.err.flush();
System.err.println();
System.err.flush();
}
private static void debug(String string) {
if (debugEnabled) {
flush();
@@ -290,7 +283,7 @@ public class GHelpBuilder {
if (opt.equals(OUTPUT_DIRECTORY_OPTION)) {
i++;
if (i >= args.length) {
errorMessage(OUTPUT_DIRECTORY_OPTION + " requires an argument");
error(OUTPUT_DIRECTORY_OPTION + " requires an argument");
printUsage();
System.exit(1);
}
@@ -299,7 +292,7 @@ public class GHelpBuilder {
else if (opt.equals(MODULE_NAME_OPTION)) {
i++;
if (i >= args.length) {
errorMessage(MODULE_NAME_OPTION + " requires an argument");
error(MODULE_NAME_OPTION + " requires an argument");
printUsage();
System.exit(1);
}
@@ -308,7 +301,7 @@ public class GHelpBuilder {
else if (opt.equals(HELP_PATHS_OPTION)) {
i++;
if (i >= args.length) {
errorMessage(HELP_PATHS_OPTION + " requires an argument");
error(HELP_PATHS_OPTION + " requires an argument");
printUsage();
System.exit(1);
}
@@ -322,7 +315,7 @@ public class GHelpBuilder {
else if (opt.equals(HELP_PATHS_GENERATED_OPTION)) {
i++;
if (i >= args.length) {
errorMessage(HELP_PATHS_GENERATED_OPTION + " requires an argument");
error(HELP_PATHS_GENERATED_OPTION + " requires an argument");
printUsage();
System.exit(1);
}
@@ -340,7 +333,7 @@ public class GHelpBuilder {
ignoreInvalid = true;
}
else if (opt.startsWith("-")) {
errorMessage("Unknown option " + opt);
error("Unknown option " + opt);
printUsage();
System.exit(1);
}
@@ -353,17 +346,17 @@ public class GHelpBuilder {
HelpBuildUtils.debug = debugEnabled;
if (helpInputDirectories.size() == 0) {
errorMessage("Must specify at least one input directory");
error("Must specify at least one input directory");
printUsage();
System.exit(1);
}
if (outputDirectoryName == null) {
errorMessage("Missing output directory: " + OUTPUT_DIRECTORY_OPTION + " [output]");
error("Missing output directory: " + OUTPUT_DIRECTORY_OPTION + " [output]");
printUsage();
System.exit(1);
}
if (moduleName == null) {
errorMessage("Missing module name: " + MODULE_NAME_OPTION + " [name]");
error("Missing module name: " + MODULE_NAME_OPTION + " [name]");
printUsage();
System.exit(1);
}
@@ -385,35 +378,7 @@ public class GHelpBuilder {
buffy.append(" ").append(IGNORE_INVALID_SWITCH).append("\n");
buffy.append(" to continue despite broken links and anchors\n");
errorMessage(buffy.toString());
}
private static void printErrorMessage(String message) {
// this prevents error messages getting interspersed with output messages
flush();
errorMessage(message);
}
private static void errorMessage(String message) {
errorMessage(message, null);
}
private static void errorMessage(String message, Throwable t) {
try {
// give the output thread a chance to finish it's output (this is a workaround for
// the Eclipse editor, and its use of two threads in its console).
Thread.sleep(250);
}
catch (InterruptedException e) {
// don't care; we tried
}
System.err.println("[" + GHelpBuilder.class.getSimpleName() + "] " + message);
if (t != null) {
t.printStackTrace();
}
flush();
error(buffy.toString());
}
//==================================================================================================

View File

@@ -0,0 +1,62 @@
/* ###
* 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.
*/
package help;
import utilities.util.reflection.ReflectionUtilities;
/**
* Contains helpful methods for emitting messages to the console.
*/
public class GHelpMsg {
public static void error(String message) {
error(message, null);
}
public static void error(String message, Throwable t) {
flush();
try {
// give the output thread a chance to finish it's output (this is a workaround for
// the Eclipse editor, and its use of two threads in its console).
Thread.sleep(250);
}
catch (InterruptedException e) {
// don't care; we tried
}
String caller = ReflectionUtilities.getClassNameOlderThan(GHelpMsg.class);
int index = caller.lastIndexOf('.');
caller = caller.substring(index + 1);
System.err.println("[" + caller + "] " + message);
if (t != null) {
t.printStackTrace();
}
flush();
}
public static void flush() {
System.out.flush();
System.out.println();
System.out.flush();
System.err.flush();
System.err.println();
System.err.flush();
}
}

View File

@@ -0,0 +1,85 @@
/* ###
* 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.
*/
package help;
import static help.GHelpMsg.*;
import java.io.File;
import java.util.*;
import generic.application.GenericApplicationLayout;
import ghidra.framework.Application;
import ghidra.framework.ApplicationConfiguration;
import help.validator.LinkDatabase;
import help.validator.location.HelpModuleCollection;
/**
* Checks for errors in source TOC files, such as conflicting sort groups. This validator is meant
* to be used when all system TOC files have been built. Individual module TOC files are validated
* for correctness with their dependencies when they are built. This class is needed to validate
* all TOC files, including for leaf modules that don't have each other as dependencies.
*/
public class GHelpTocValidator {
private static final String DEBUG_SWITCH = "-debug";
private Collection<File> helpInputDirectories = new LinkedHashSet<>();
public static void main(String[] args) throws Exception {
GHelpTocValidator validator = new GHelpTocValidator();
ApplicationConfiguration config = new ApplicationConfiguration();
Application.initializeApplication(new GenericApplicationLayout("Help TOC Validator", "0.1"),
config);
validator.validate(args);
}
private void validate(String[] args) {
parseArguments(args);
List<File> allHelp = new ArrayList<>(helpInputDirectories);
HelpModuleCollection help = HelpModuleCollection.fromFiles(allHelp);
LinkDatabase linkDatabase = new LinkDatabase(help);
linkDatabase.validateAllTOCs();
}
private void parseArguments(String[] args) {
boolean debugEnabled = false;
for (String opt : args) {
if (opt.equals(DEBUG_SWITCH)) {
debugEnabled = true;
}
else if (opt.startsWith("-")) {
error("Unknown option " + opt);
System.exit(1);
}
else {
// It must just be an input
helpInputDirectories.add(new File(opt));
}
}
HelpBuildUtils.debug = debugEnabled;
if (helpInputDirectories.size() == 0) {
error("Must specify at least one help jar file");
System.exit(1);
}
}
}

View File

@@ -428,10 +428,10 @@ public class HelpBuildUtils {
return false;
}
private static final Path DEFAULT_FS_ROOT;
private static final Path DEFAULT_ROOT_DIR;
static {
try {
DEFAULT_FS_ROOT = Paths.get(".").toRealPath().getRoot();
DEFAULT_ROOT_DIR = Paths.get(".").toRealPath().getRoot();
}
catch (IOException e) {
throw new RuntimeException(
@@ -439,7 +439,7 @@ public class HelpBuildUtils {
}
}
private static Path toFSGivenRoot(Path root, Path path) {
private static Path relativeToRoot(Path root, Path path) {
if (path.getNameCount() == 0) {
if (path.isAbsolute()) {
return root;
@@ -459,12 +459,12 @@ public class HelpBuildUtils {
return temp;
}
public static Path toDefaultFS(Path path) {
return toFSGivenRoot(DEFAULT_FS_ROOT, path);
public static Path relativeToWorkingDir(Path path) {
return relativeToRoot(DEFAULT_ROOT_DIR, path);
}
public static Path toFS(Path targetFS, Path path) {
return toFSGivenRoot(targetFS.toAbsolutePath().getRoot(), path);
public static Path relativeTo(Path targetFS, Path path) {
return relativeToRoot(targetFS.toAbsolutePath().getRoot(), path);
}
public static Path createReferencePath(URI fileURI) {

View File

@@ -17,7 +17,8 @@ package help;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.Collection;
import java.util.Date;
import ghidra.util.exception.AssertException;
import help.validator.LinkDatabase;
@@ -135,9 +136,7 @@ public class JavaHelpFilesBuilder {
out.println("<map version=\"1.0\">");
Collection<AnchorDefinition> anchors = help.getAllAnchorDefinitions();
Iterator<AnchorDefinition> iterator = anchors.iterator();
while (iterator.hasNext()) {
AnchorDefinition a = iterator.next();
for (AnchorDefinition a : anchors) {
String anchorTarget = a.getHelpPath();
//

View File

@@ -52,6 +52,7 @@ public class OverlayHelpTree {
}
private void addExternalTOCItem(TOCItem item) {
TOCItem parent = item.getParent();
String parentID = parent == null ? null : parent.getIDAttribute();
if (parentID == null) {
@@ -129,6 +130,20 @@ public class OverlayHelpTree {
// printTreeForID(writer, sourceFileID);
}
public void validateAllTOCs() {
initializeTree();
doValidateAllTOCs(rootNode);
}
private void doValidateAllTOCs(OverlayNode node) {
node.validateChildrenSortGroups();
Set<OverlayNode> children = node.children;
for (OverlayNode child : children) {
doValidateAllTOCs(child);
}
}
void printTreeForID(PrintWriter writer, String sourceFileID) {
initializeTree();
@@ -149,7 +164,7 @@ public class OverlayHelpTree {
private void printContents(String sourceFileID, PrintWriter writer) {
if (rootNode == null) {
// assume not TOC contents; empty TOC file
// assume no TOC contents; empty TOC file
return;
}
@@ -185,6 +200,7 @@ public class OverlayHelpTree {
}
private void buildChildren(OverlayNode node) {
String definitionID = node.getDefinitionID();
Set<TOCItem> children = parentToChildrenMap.remove(definitionID);
if (children == null) {

View File

@@ -1,13 +1,12 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.
@@ -16,12 +15,12 @@
*/
package help.validator;
import help.validator.model.AnchorDefinition;
import help.validator.model.HelpTopic;
import java.nio.file.Path;
import java.util.List;
import help.validator.model.AnchorDefinition;
import help.validator.model.HelpTopic;
public class DuplicateAnchorCollectionByHelpTopic implements DuplicateAnchorCollection,
Comparable<DuplicateAnchorCollectionByHelpTopic> {
@@ -35,7 +34,7 @@ public class DuplicateAnchorCollectionByHelpTopic implements DuplicateAnchorColl
@Override
public String toString() {
return "Duplicate anchors for topic\n\ttopic file: " + topic.getTopicFile() + "\n" +
return "Duplicate anchors for topic\n\ttopic dir: " + topic.getTopicDir() + "\n" +
getAnchorsAsString();
}
@@ -49,8 +48,8 @@ public class DuplicateAnchorCollectionByHelpTopic implements DuplicateAnchorColl
@Override
public int compareTo(DuplicateAnchorCollectionByHelpTopic o) {
Path topicFile1 = topic.getTopicFile();
Path topicFile2 = o.topic.getTopicFile();
return topicFile1.compareTo(topicFile2);
Path topicDir1 = topic.getTopicDir();
Path topicDir2 = o.topic.getTopicDir();
return topicDir1.compareTo(topicDir2);
}
}

View File

@@ -204,7 +204,7 @@ public class JavaHelpValidator {
Path dirPath = Paths.get(dir.getAbsolutePath());
Path imagePath = Paths.get(imgSrc);
Path imageFileFS = HelpBuildUtils.toFS(dirPath, imagePath);
Path imageFileFS = HelpBuildUtils.relativeTo(dirPath, imagePath);
Path toCheck = dirPath.resolve(imageFileFS);
if (Files.exists(toCheck)) {
return toCheck;
@@ -214,7 +214,7 @@ public class JavaHelpValidator {
private Path makePath(Path helpDir, Path imagePath) {
Path imageFileFS = HelpBuildUtils.toFS(helpDir, imagePath);
Path imageFileFS = HelpBuildUtils.relativeTo(helpDir, imagePath);
imageFileFS = removeRedundantHelp(helpDir, imageFileFS);
Path toCheck = helpDir.resolve(imageFileFS);
if (Files.exists(toCheck)) {

View File

@@ -204,4 +204,8 @@ public class LinkDatabase {
public void generateTOCOutputFile(Path outputFile, GhidraTOCFile file) throws IOException {
printableTree.printTreeForID(outputFile, file.getFile().toUri().toString());
}
public void validateAllTOCs() {
printableTree.validateAllTOCs();
}
}

View File

@@ -124,6 +124,10 @@ public class HelpModuleCollection implements TOCItemProvider {
}
public GhidraTOCFile getSourceTOCFile() {
if (inputHelp == null) {
// this collection of help modules is only external inputs (e.g., jar files)
return null;
}
return inputHelp.getSourceTOCFile();
}
@@ -258,6 +262,11 @@ public class HelpModuleCollection implements TOCItemProvider {
@Override
public Map<String, TOCItemDefinition> getTocDefinitionsByID() {
if (inputHelp == null) {
// this collection of help modules is only external inputs (e.g., jar files)
return Map.of();
}
Map<String, TOCItemDefinition> map = new HashMap<>();
GhidraTOCFile TOC = inputHelp.getSourceTOCFile();
map.putAll(TOC.getTOCDefinitionByIDMapping());
@@ -267,7 +276,6 @@ public class HelpModuleCollection implements TOCItemProvider {
@Override
public Map<String, TOCItemExternal> getExternalTocItemsById() {
Map<String, TOCItemExternal> map = new HashMap<>();
if (externalHelpSets.isEmpty()) {
return map;
}
@@ -325,6 +333,11 @@ public class HelpModuleCollection implements TOCItemProvider {
* @return the items
*/
public Collection<TOCItem> getInputTOCItems() {
if (inputHelp == null) {
// this collection of help modules is only external inputs (e.g., jar files)
return List.of();
}
Collection<TOCItem> items = new ArrayList<>();
GhidraTOCFile TOC = inputHelp.getSourceTOCFile();
items.addAll(TOC.getAllTOCItems());
@@ -332,6 +345,11 @@ public class HelpModuleCollection implements TOCItemProvider {
}
public Collection<HREF> getTOC_HREFs() {
if (inputHelp == null) {
// this collection of help modules is only external inputs (e.g., jar files)
return List.of();
}
Collection<HREF> definitions = new ArrayList<>();
GhidraTOCFile TOC = inputHelp.getSourceTOCFile();
definitions.addAll(getTOC_HREFs(TOC));

View File

@@ -4,9 +4,9 @@
* 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.
@@ -17,17 +17,11 @@ package help.validator.model;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.*;
import ghidra.util.exception.AssertException;
import help.HelpBuildUtils;
import help.PathKey;
import help.validator.AnchorManager;
import help.validator.HTMLFileParser;
import help.validator.ReferenceTagProcessor;
import help.validator.TagProcessor;
import help.*;
import help.validator.*;
import help.validator.location.HelpModuleLocation;
public class HelpFile {
@@ -80,7 +74,7 @@ public class HelpFile {
return anchorManager.getDuplicateAnchorsByID();
}
public AnchorDefinition getAnchorDefinition(Path helpPath) {
public AnchorDefinition getAnchorDefinition(Path helpPath) {
Map<PathKey, AnchorDefinition> anchorsByHelpPath = anchorManager.getAnchorsByHelpPath();
AnchorDefinition def = anchorsByHelpPath.get(new PathKey(helpPath));
return def;
@@ -124,9 +118,9 @@ public class HelpFile {
HTMLFileParser.scanHtmlFile(file, tagProcessor);
}
catch (IOException e) {
System.err.println("Exception parsing file: " + file.toUri() + "\n");
System.err.println(e.getMessage());
e.printStackTrace();
String msg =
"Exception parsing file: %s\n%s".formatted(file.toUri(), e.getMessage());
GHelpMsg.error(msg, e);
}
}
else {

View File

@@ -4,9 +4,9 @@
* 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.
@@ -20,16 +20,19 @@ import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import help.GHelpMsg;
import help.HelpBuildUtils;
import help.validator.location.DirectoryHelpModuleLocation;
import help.validator.location.HelpModuleLocation;
public class HelpTopic implements Comparable<HelpTopic> {
private final HelpModuleLocation help;
private final Path topicFile;
private final Path topicDir;
// topics/TopicName
private final Path relativePath;
private Map<Path, HelpFile> helpFiles = new LinkedHashMap<>();
private Map<Path, HelpFile> helpFiles;
public static HelpTopic fromHTMLFile(Path topicFile) {
@@ -44,65 +47,80 @@ public class HelpTopic implements Comparable<HelpTopic> {
return helpTopic;
}
public HelpTopic(HelpModuleLocation help, Path topicFile) {
public HelpTopic(HelpModuleLocation help, Path topicDir) {
this.help = help;
this.topicFile = topicFile;
this.topicDir = topicDir;
Path helpDir = help.getHelpLocation();
Path unknownFSRelativePath = helpDir.relativize(topicFile); // may or may not be jar paths
this.relativePath = HelpBuildUtils.toDefaultFS(unknownFSRelativePath);
loadHelpFiles(topicFile);
// topic file: /help/topics/TopicName
// relative: topics/TopicName
Path relativeTopicPath = helpDir.relativize(topicDir); // may or may not be jar paths
this.relativePath = HelpBuildUtils.relativeToWorkingDir(relativeTopicPath);
}
public Path getTopicFile() {
return topicFile;
public Path getTopicDir() {
return topicDir;
}
private void loadHelpFiles(final Path dir) {
final PathMatcher matcher =
dir.getFileSystem().getPathMatcher("glob:**/*.{[Hh][Tt][Mm],[Hh][Tt][Mm][Ll]}");
private void lazyLoad() {
if (helpFiles != null) {
return;
}
// Ex:
// jar: /help/topics/FooPlugin
final Path dirDefaultFS = HelpBuildUtils.toDefaultFS(dir);
helpFiles = new LinkedHashMap<>();
loadHelpFiles(topicDir);
}
private void loadHelpFiles(Path dir) {
FileSystem fs = dir.getFileSystem();
PathMatcher matcher =
fs.getPathMatcher("glob:**/*.{[Hh][Tt][Mm],[Hh][Tt][Mm][Ll]}");
// Ex: /help/topics/FooPlugin
Path dirDefaultFS = HelpBuildUtils.relativeToWorkingDir(dir);
try {
Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
if (matcher.matches(file)) {
// Ex:
// jar: /help/topics/FooPlugin/Foo.html
Path fileDefaultFS = HelpBuildUtils.toDefaultFS(file);
// Ex: jar: Foo.html
Path relFilePath = dirDefaultFS.relativize(fileDefaultFS);
// Ex: jar: topics/FooPlugin/Foo.html
relFilePath = relativePath.resolve(relFilePath);
helpFiles.put(relFilePath, new HelpFile(help, file));
mapHelpFile(dirDefaultFS, file);
}
return FileVisitResult.CONTINUE;
}
});
}
catch (IOException e) {
System.err.println("Error loading help files: " + dir.toUri());
e.printStackTrace(System.err);
GHelpMsg.error("Error loading help files: " + dir.toUri(), e);
}
}
private void mapHelpFile(Path dirDefaultFS, Path file) {
// Ex: /help/topics/FooPlugin/Foo.html
Path fileDefaultFS = HelpBuildUtils.relativeToWorkingDir(file);
// Ex: Foo.html
Path relFilePath = dirDefaultFS.relativize(fileDefaultFS);
// Ex: topics/FooPlugin/Foo.html
relFilePath = relativePath.resolve(relFilePath);
helpFiles.put(relFilePath, new HelpFile(help, file));
}
void addHelpFile(Path relPath, HelpFile helpFile) {
lazyLoad();
helpFiles.put(relPath, helpFile);
}
public Collection<HREF> getAllHREFs() {
// Don't need to validate hrefs already in a .jar
if (topicFile.getFileSystem() != FileSystems.getDefault()) {
if (topicDir.getFileSystem() != FileSystems.getDefault()) {
return Collections.emptyList();
}
lazyLoad();
List<HREF> list = new ArrayList<>();
for (HelpFile helpFile : helpFiles.values()) {
list.addAll(helpFile.getAllHREFs());
@@ -112,9 +130,11 @@ public class HelpTopic implements Comparable<HelpTopic> {
public Collection<IMG> getAllIMGs() {
// Don't need to validate imgs already in a .jar
if (topicFile.getFileSystem() != FileSystems.getDefault()) {
if (topicDir.getFileSystem() != FileSystems.getDefault()) {
return Collections.emptyList();
}
lazyLoad();
List<IMG> list = new ArrayList<>();
for (HelpFile helpFile : helpFiles.values()) {
list.addAll(helpFile.getAllIMGs());
@@ -124,6 +144,7 @@ public class HelpTopic implements Comparable<HelpTopic> {
public Collection<AnchorDefinition> getAllAnchorDefinitions() {
// The current module may refer to anchors in pre-built modules.
lazyLoad();
List<AnchorDefinition> list = new ArrayList<>();
for (HelpFile helpFile : helpFiles.values()) {
list.addAll(helpFile.getAllAnchorDefinitions());
@@ -132,9 +153,14 @@ public class HelpTopic implements Comparable<HelpTopic> {
}
public Collection<HelpFile> getHelpFiles() {
lazyLoad();
return helpFiles.values();
}
/**
* Returns the relative path, which is {@code topics/TopicName}
* @return the path
*/
Path getRelativePath() {
return relativePath;
}
@@ -144,16 +170,16 @@ public class HelpTopic implements Comparable<HelpTopic> {
}
public String getName() {
return topicFile.getFileName().toString();
return topicDir.getFileName().toString();
}
@Override
public int compareTo(HelpTopic o) {
return topicFile.compareTo(o.topicFile);
return topicDir.compareTo(o.topicDir);
}
@Override
public String toString() {
return topicFile.toString();
return topicDir.toString();
}
}

View File

@@ -4,9 +4,9 @@
* 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.
@@ -85,6 +85,7 @@ return buildy.toString();*/
//@formatter:off
return "<"+GhidraTOCFile.TOC_ITEM_DEFINITION +
" id=\"" + getIDAttribute() + "\" text=\"" + getTextAttribute() + "\" " +
"\n\t\tsortgroup=\"" + getSortPreference() + "\"" +
"\n\t\ttarget=\"" + getTargetAttribute() + "\" />" +
"\n\t\t[source file=\"" + getSourceFile() + "\" (line:" + getLineNumber() + ")]";
//@formatter:on

View File

@@ -1,13 +1,12 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* 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.
@@ -16,10 +15,10 @@
*/
package help.validator.model;
import help.validator.LinkDatabase;
import java.nio.file.Path;
import help.validator.LinkDatabase;
public class TOCItemExternal extends TOCItem {
public TOCItemExternal(TOCItem parentItem, Path tocFile, String ID, String text, String target,
@@ -41,7 +40,8 @@ public class TOCItemExternal extends TOCItem {
}
@Override
public String generateTOCItemTag(LinkDatabase linkDatabase, boolean isInlineTag, int indentLevel) {
public String generateTOCItemTag(LinkDatabase linkDatabase, boolean isInlineTag,
int indentLevel) {
return super.generateTOCItemTag(linkDatabase, isInlineTag, indentLevel);
}
@@ -49,6 +49,7 @@ public class TOCItemExternal extends TOCItem {
public String toString() {
//@formatter:off
return "<tocitem id=\"" + getIDAttribute() + "\"\n\t\t" +
"sort=\"" + getSortPreference() + "\"\n\t\t" +
"text=\"" + getTextAttribute() + "\"\n\t\t" +
"target=\"" + getTargetAttribute() + "\"" +
"/>\n\t" +

View File

@@ -363,7 +363,7 @@ tasks.register('indexHelp', JavaExec) {
// - 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) {
tasks.register('buildModuleHelp', JavaExec) {
group = "Ghidra Private"
dependsOn 'indexHelp'
@@ -406,7 +406,7 @@ tasks.register('buildHelp', JavaExec) {
//
// The classpath needs to include:
// 1) the jar of each dependent Module that has already been built
// 1) the jar of each depended upon Module that has already been built
// 2) 'src/main/resources'
//
@@ -434,6 +434,12 @@ tasks.register('buildHelp', JavaExec) {
}
}
// 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
@@ -445,7 +451,7 @@ jar {
}
// build the help whenever this module's jar file is built
jar.dependsOn 'buildHelp'
jar.dependsOn 'buildModuleHelp'
/*********************************************************************************

View File

@@ -197,14 +197,14 @@ def getModuleResourcesDirs(Collection<File> fullClasspath) {
.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.
// Locates 'buildModuleHelp' 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'))
return myProjects.collect(p -> p.tasks.findByPath('buildModuleHelp'))
.findAll(t -> t != null)
}
@@ -313,6 +313,8 @@ tasks.register('buildGlobalMarkdown') {
}
}
// Task for building Ghidra help files
// - depends on the output from the help indexer
// - validates help
@@ -376,7 +378,7 @@ tasks.register('buildHelpFiles', JavaExec) {
//
// 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
// 1) The jar path of each depended upon 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
@@ -395,7 +397,7 @@ tasks.register('buildHelpFiles', JavaExec) {
// 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
// The dependency file is <Module>-help.jar from the 'buildModuleHelp' tasks upon which
// we depend.
def buildHelpTasks = getDependentProjectHelpTasks(sourceSets.main.runtimeClasspath.files)
buildHelpTasks.each {
@@ -423,7 +425,7 @@ tasks.register('buildHelpFiles', JavaExec) {
* 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) {
tasks.register('buildModuleHelp', Jar) {
group = rootProject.GHIDRA_GROUP
description = " Builds the help for this module. [gradle/helpProject.gradle]\n"
@@ -473,11 +475,11 @@ jar {
}
// build the help whenever this module's jar file is built
processResources.dependsOn buildHelp
jar.dependsOn buildHelp
processResources.dependsOn buildModuleHelp
jar.dependsOn buildModuleHelp
// 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
rootProject.prepDev.dependsOn buildModuleHelp

View File

@@ -4,9 +4,9 @@
* 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.
@@ -27,8 +27,8 @@ GHIDRA GRADLE
be run against a specific module. For example:
from root project, "gradle buildHelp" builds help for all modules.
from root project, "gradle :Base:buildHelp" builds help for the "Base" module
from the Base project dir, "gradle buildHelp" builds help for the "Base" module
from root project, "gradle :Base:buildModuleHelp" builds help for the "Base" module
from the Base project dir, "gradle buildModuleHelp" builds help for the "Base" module
Primary gradle tasks for Ghidra