Skip to content

Instantly share code, notes, and snippets.

@saagarjha
Last active April 13, 2024 12:35
Show Gist options
  • Star 22 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save saagarjha/777909b257dbfa98649476b7f5af41bb to your computer and use it in GitHub Desktop.
Save saagarjha/777909b257dbfa98649476b7f5af41bb to your computer and use it in GitHub Desktop.
Creates a Ghidra.app bundle for macOS
#!/bin/sh
set -eu
create_iconset() {
mkdir -p Ghidra.iconset
cat << EOF > Ghidra.iconset/Contents.json
{
"images":
[
{
"filename": "icon_16x16.png",
"idiom": "mac",
"scale": "1x",
"size": "16x16"
},
{
"filename": "icon_32x32.png",
"idiom": "mac",
"scale": "2x",
"size": "16x16"
},
{
"filename": "icon_32x32.png",
"idiom": "mac",
"scale": "1x",
"size": "32x32"
},
{
"filename": "icon_64x64.png",
"idiom": "mac",
"scale": "2x",
"size": "32x32"
},
{
"filename": "icon_128x128.png",
"idiom": "mac",
"size": "128x128",
"scale": "1x"
},
{
"filename": "icon_256x256.png",
"idiom": "mac",
"scale": "2x",
"size": "128x128"
},
{
"filename": "icon_256x256.png",
"idiom": "mac",
"scale": "1x",
"size": "256x256"
},
{
"filename": "icon_512x512.png",
"idiom": "mac",
"scale": "2x",
"size": "256x256"
},
{
"filename": "icon_512x512.png",
"idiom": "mac",
"scale": "1x",
"size": "512x512"
},
{
"filename": "icon_1024x1024.png",
"idiom": "mac",
"scale": "2x",
"size": "512x512"
}
],
"info":
{
"version": 1,
"author": "xcode"
}
}
EOF
for size in 16 32 64 128 256 512 1024; do
convert "$1" -resize "${size}x${size}" "Ghidra.iconset/icon_${size}x${size}.png"
done
}
if [ $# -ne 1 ]; then
echo "Usage: $0 [path to ghidra folder]" >&2
exit 1
fi
mkdir -p Ghidra.app/Contents/MacOS
cat << EOF | clang -x objective-c -fmodules -framework Foundation -o Ghidra.app/Contents/MacOS/Ghidra -
@import Foundation;
int main() {
execl([NSBundle.mainBundle.resourcePath stringByAppendingString:@"/ghidra/ghidraRun"].UTF8String, NULL);
}
EOF
mkdir -p Ghidra.app/Contents/Resources/
rm -rf Ghidra.app/Contents/Resources/ghidra
cp -R "$(echo "$1" | sed s,/*$,,)" Ghidra.app/Contents/Resources/ghidra
sed "s/bg Ghidra/fg Ghidra/" < "$1/ghidraRun" > Ghidra.app/Contents/Resources/ghidra/ghidraRun
sed "s/apple.laf.useScreenMenuBar=false/apple.laf.useScreenMenuBar=true/" < "$1/support/launch.properties" > Ghidra.app/Contents/Resources/ghidra/support/launch.properties
echo "APPL????" > Ghidra.app/Contents/PkgInfo
jar -x -f Ghidra.app/Contents/Resources/ghidra/Ghidra/Framework/Gui/lib/Gui.jar images/GhidraIcon256.png
if [ "$( (sw_vers -productVersion; echo "11.0") | sort -V | head -n 1)" = "11.0" ]; then
convert \( -size 1024x1024 canvas:none -fill white -draw 'roundRectangle 100,100 924,924 180,180' \) \( +clone -background black -shadow 25x12+0+12 \) +swap -background none -layers flatten -crop 1024x1024+0+0 \( images/GhidraIcon256.png -resize 704x704 -gravity center \) -composite GhidraIcon.png
else
mv images/GhidraIcon256.png GhidraIcon.png
fi
create_iconset GhidraIcon.png
for size in 16 24 32 40 48 64 128 256; do
convert GhidraIcon.png -resize "${size}x${size}" "images/GhidraIcon${size}.png"
jar -u -f Ghidra.app/Contents/Resources/ghidra/Ghidra/Framework/Generic/lib/Generic.jar "images/GhidraIcon${size}.png"
done
iconutil -c icns Ghidra.iconset
cp Ghidra.icns Ghidra.app/Contents/Resources
SetFile -a B Ghidra.app
cat << EOF > Ghidra.app/Contents/Info.plist
<?xml version="1.0" ?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>Ghidra</string>
<key>CFBundleIconFile</key>
<string>Ghidra.icns</string>
<key>CFBundleIdentifier</key>
<string>org.ghidra-sre.Ghidra</string>
<key>CFBundleDisplayName</key>
<string>Ghidra</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Ghidra</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(grep application.version < "$1/Ghidra/application.properties" | sed "s/application.version=//")</string>
<key>CFBundleVersion</key>
<string>$(grep application.version < "$1/Ghidra/application.properties" | sed "s/application.version=//" | sed "s/\.//g")</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>NSHumanReadableCopyright</key>
<string></string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
EOF
mkdir -p docking/widgets/filechooser/
cat << EOF > docking/widgets/filechooser/GhidraFileChooser.java
package docking.widgets.filechooser;
import docking.DialogComponentProvider;
import ghidra.util.filechooser.GhidraFileChooserModel;
import ghidra.util.filechooser.GhidraFileFilter;
import java.awt.Component;
import java.awt.Dialog;
import java.awt.FileDialog;
import java.io.File;
import java.io.FilenameFilter;
import java.util.Arrays;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
public class GhidraFileChooser extends DialogComponentProvider {
private GhidraFileChooserModel model;
private GhidraFileFilter filter;
private FileDialog fileDialog;
private int mode = FILES_AND_DIRECTORIES;
public static final int FILES_ONLY = 0;
public static final int DIRECTORIES_ONLY = 1;
public static final int FILES_AND_DIRECTORIES = 2;
public GhidraFileChooser(Component parent) {
this(new LocalFileChooserModel(), parent);
}
GhidraFileChooser(GhidraFileChooserModel model, Component parent) {
super("File Chooser", true, true, true, false);
this.model = model;
Component root = SwingUtilities.getRoot(parent);
if (root instanceof Dialog) {
fileDialog = new FileDialog((Dialog)root);
} else {
fileDialog = new FileDialog((JFrame)root);
}
}
public void setShowDetails(boolean showDetails) {
}
public void setFileSelectionMode(int mode) {
this.mode = mode;
}
public void setFileSelectionMode(GhidraFileChooserMode mode) {
switch (mode) {
case FILES_ONLY:
this.mode = 0;
break;
case DIRECTORIES_ONLY:
this.mode = 1;
break;
case FILES_AND_DIRECTORIES:
this.mode = 2;
break;
}
}
public boolean isMultiSelectionEnabled() {
return fileDialog.isMultipleMode();
}
public void setMultiSelectionEnabled(boolean b) {
fileDialog.setMultipleMode(b);
}
public void setApproveButtonText(String buttonText) {
}
public void setApproveButtonToolTipText(String tooltipText) {
}
public void setLastDirectoryPreference(String newKey) {
}
public File getSelectedFile() {
show();
String path = fileDialog.getFile();
return path != null ? new File(fileDialog.getDirectory(), path) : null;
}
public List<File> getSelectedFiles() {
show();
return Arrays.asList(fileDialog.getFiles());
}
public File getSelectedFile(boolean show) {
return getSelectedFile();
}
public void setSelectedFile(File file) {
fileDialog.setFile(file != null ? file.getPath() : null);
}
public void show() {
fileDialog.setVisible(true);
}
public void close() {
fileDialog.setVisible(false);
}
public File getCurrentDirectory() {
return new File(fileDialog.getDirectory());
}
public void setCurrentDirectory(File directory) {
fileDialog.setDirectory(directory.getPath());
}
public void rescanCurrentDirectory() {
}
private class _FilenameFilter implements FilenameFilter {
@Override
public boolean accept(File dir, String name) {
File file = new File(dir, name);
switch (mode) {
case DIRECTORIES_ONLY:
if (file.isFile()) {
return false;
}
break;
case FILES_AND_DIRECTORIES:
default:
break;
}
return filter.accept(file, model);
}
}
public void addFileFilter(GhidraFileFilter f) {
}
public void setSelectedFileFilter(GhidraFileFilter filter) {
this.filter = filter;
}
public void setFileFilter(GhidraFileFilter filter) {
this.filter = filter;
}
public boolean wasCancelled() {
return fileDialog.getFile() == null;
}
@Override
public void setTitle(String title) {
fileDialog.setTitle(title);
}
}
EOF
javac -cp "$(find Ghidra.app -regex '.*\.jar' | tr '\n' ':')" docking/widgets/filechooser/GhidraFileChooser.java
cp -R docking Ghidra.app/Contents/Resources/Ghidra/ghidra/patch/
cat << EOF > OpenGhidra.java
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.io.File;
public class OpenGhidra {
public static void main(String[] args) throws Exception {
Runtime.getRuntime().exec(new String[] {"open", "-a", "Ghidra"});
while (true) {
for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) {
if (descriptor.displayName().contains("ghidra.Ghidra")) {
VirtualMachine vm = VirtualMachine.attach(descriptor.id());
for (String arg : args) {
vm.loadAgent(OpenGhidra.class.getProtectionDomain().getCodeSource().getLocation().getPath() + "/OpenGhidra.jar", new File(arg).getAbsolutePath());
}
vm.detach();
return;
}
}
}
}
}
EOF
javac OpenGhidra.java
cp OpenGhidra.class Ghidra.app/Contents/Resources
cat << EOF > OpenGhidraAgent.java
import ghidra.app.services.ProgramManager;
import ghidra.formats.gfilesystem.FileSystemService;
import ghidra.framework.main.AppInfo;
import ghidra.plugin.importer.ImporterUtilities;
import java.awt.Frame;
import java.awt.Menu;
import java.awt.MenuItem;
import java.io.File;
import java.util.Timer;
import java.util.TimerTask;
public class OpenGhidraAgent {
private static boolean checkMenuForReadiness(MenuItem menuItem) {
if (menuItem.getLabel().contains("Import File") && menuItem.isEnabled()) {
return true;
} else if (menuItem instanceof Menu) {
var menu = (Menu)menuItem;
for (int i = 0; i < menu.getItemCount(); ++i) {
if (checkMenuForReadiness(menu.getItem(i))) {
return true;
}
}
}
return false;
}
public static void agentmain(String agentArgs) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
for (var frame : Frame.getFrames()) {
var menuBar = frame.getMenuBar();
if (menuBar == null) {
continue;
}
for (int i = 0; i < menuBar.getMenuCount(); ++i) {
if (checkMenuForReadiness(menuBar.getMenu(i))) {
var file = new File(agentArgs);
var tool = AppInfo.getFrontEndTool();
var manager = tool.getService(ProgramManager.class);
var fsrl = FileSystemService.getInstance().getLocalFSRL(file);
var folder = AppInfo.getActiveProject().getProjectData().getRootFolder();
ImporterUtilities.showImportDialog(tool, manager, fsrl, folder, null);
timer.cancel();
return;
}
}
}
}
}, 0, 100);
}
}
EOF
javac -cp "$(find Ghidra.app -regex '.*\.jar' | tr '\n' ':')" OpenGhidraAgent.java
cat << EOF > manifest
Agent-Class: OpenGhidraAgent
EOF
jar --create --file OpenGhidra.jar --manifest manifest OpenGhidraAgent*.class
cp OpenGhidra.jar Ghidra.app/Contents/Resources
@francesco-plt
Copy link

Can you give some instruction on how to correctly use this script? I would like to create a .app wrapper with a custom icon

@saagarjha
Copy link
Author

$ ./CreateGhidraApp.sh <path-to-ghidra-folder>

This will create a Ghidra.app in the current folder, as well as a bunch of transient junk, so I recommend running this in /tmp or something. If you want a custom icon, just replace the lines in the middle of the script that generate the app icon catalog.

@francesco-plt
Copy link

Thank you, worked like a charm

@CyrilHu
Copy link

CyrilHu commented Nov 25, 2021

./CreateGhidraApp.sh: line 105: convert: command not found

resolved by brew install imagemagick, it needs download almost 350MB

brew install imagemagick
$ brew install imagemagick
==> Installing dependencies for imagemagick: libpng, freetype, fontconfig, jbig2dec, jpeg, libidn, libtiff, little-cms2, openjpeg, ghostscript, brotli, giflib, imath, openexr, webp, jpeg-xl, libvmaf, aom, libde265, libffi, pcre, gdbm, mpdecimal, xz, python@3.9, glib, docbook, docbook-xsl, gnu-getopt, xmlto, shared-mime-info, x265, libheif, liblqr, libomp, m4 and libtool
==> Installing imagemagick
==> Pouring imagemagick--7.1.0-16.monterey.bottle.tar.gz
🍺  /usr/local/Cellar/imagemagick/7.1.0-16: 801 files, 28.3MB

@saagarjha
Copy link
Author

This script uses ImageMagick to resize the app icon. You can either install it, or replace that part with your own resizing code.

@hmne
Copy link

hmne commented Jan 18, 2023

i git error the script making .app in .app

cp: Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra/Ghidra.app/Contents/Resources/ghidra: name too long (not copied)

@saagarjha
Copy link
Author

Can you share how you invoked the script?

@hmne
Copy link

hmne commented Jan 22, 2023

Can you share how you invoked the script?

first i did compile ghidra from source so the folder stretcher something like this ~/ghidra/build/dist/
in side the dist folder there is zip after unzip the file i got ghidra folder so the folder stretcher ~/ghidra/build/dist/ghidra/

in side this folder ghidraRun etc

i did download the script to this folder and did chmod +x to script
and i did run the script ./CreateGhidraApp.sh ~/ghidra/build/dist/ghidra/
and i git this error name to long file not copied

PS: i did moved the folder to other location like home or downloads same error

Screen Shot 2023-01-22 at 4 15 08 PM

Screen Shot 2023-01-22 at 4 15 25 PM

@saagarjha
Copy link
Author

Oh, you're trying to run the script from within the ghidra folder. Don't do that.

@hmne
Copy link

hmne commented Jan 22, 2023

Oh, you're trying to run the script from within the ghidra folder. Don't do that.

ok after moving script to another folder i got another error

./CreateGhidraApp.sh ~/ghidra/build/dist/ghidra
convert: unable to open image 'images/GhidraIcon256.png': No such file or directory @ error/blob.c/OpenBlob/3570.
convert: image sequence is required `-composite' @ error/mogrify.c/MogrifyImageList/7996.
convert: no images defined `GhidraIcon.png' @ error/convert.c/ConvertImageCommand/3342.

i did found the fix only copy folder images form this location ~/ghidra/Ghidra/Framework/Gui/src/main/resources/

to same folder next to script

@saagarjha
Copy link
Author

Does that file exist?

@saagarjha
Copy link
Author

@hmne this should be fixed now

@The-Dumb-Dino
Copy link

Works fine, used this script both on my intel and arm mac but both of them failed to give the application a icon, set one myself anyways but would be nice if its fixed at some point. Thanks

@saagarjha
Copy link
Author

It should be doing that. Do you have ImageMagick installed?

@mologie
Copy link

mologie commented Dec 23, 2023

This is amazing, it's still working nicely with Ghidra 11. I have installed Ghidra via Homebrew and made the script run ln -s $(realpath "$1") ... instead of embedding Ghidra, and that also works. One will just have to re-run the script after upgrading with Homebrew.

Thank you!

@blacktop
Copy link

blacktop commented Dec 24, 2023

@al3xtjames
Copy link

This seems to break the filepicker in the File > Install Extensions menu (click the plus icon):

'void docking.widgets.filechooser.GhidraFileChooser.setLastDirectoryPreference(java.lang.String)'
java.lang.NoSuchMethodError: 'void docking.widgets.filechooser.GhidraFileChooser.setLastDirectoryPreference(java.lang.String)'
	at ghidra.framework.project.extensions.ExtensionTableProvider$1.actionPerformed(ExtensionTableProvider.java:144)
	at docking.menu.DialogToolbarButton.lambda$doActionPerformed$0(DialogToolbarButton.java:67)
	at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318)
	at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:771)
	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:722)
	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:716)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
	at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:741)
	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:117)
	at java.desktop/java.awt.WaitDispatchSupport$2.run(WaitDispatchSupport.java:191)
	at java.desktop/java.awt.WaitDispatchSupport$4.run(WaitDispatchSupport.java:236)
	at java.desktop/java.awt.WaitDispatchSupport$4.run(WaitDispatchSupport.java:234)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:318)
	at java.desktop/java.awt.WaitDispatchSupport.enter(WaitDispatchSupport.java:234)
	at java.desktop/java.awt.Dialog.show(Dialog.java:1080)
	at java.desktop/java.awt.Component.show(Component.java:1728)
	at java.desktop/java.awt.Component.setVisible(Component.java:1675)
	at java.desktop/java.awt.Window.setVisible(Window.java:1036)
	at java.desktop/java.awt.Dialog.setVisible(Dialog.java:1016)
	at docking.DockingDialog.setVisible(DockingDialog.java:351)
	at docking.DockingWindowManager.lambda$doShowDialog$6(DockingWindowManager.java:1807)
	at ghidra.util.Swing.doRun(Swing.java:292)
	at ghidra.util.Swing.runNow(Swing.java:208)
	at ghidra.util.Swing.runNow(Swing.java:163)
	at docking.DockingWindowManager.doShowDialog(DockingWindowManager.java:1811)
	at docking.DockingWindowManager.showDialog(DockingWindowManager.java:1760)
	at docking.AbstractDockingTool.showDialog(AbstractDockingTool.java:158)
	at ghidra.framework.plugintool.PluginTool.showExtensions(PluginTool.java:341)
	at ghidra.framework.main.FrontEndTool$2.actionPerformed(FrontEndTool.java:641)
	at docking.DockingActionProxy.actionPerformed(DockingActionProxy.java:47)
	at docking.MenuBarMenuHandler.lambda$processMenuAction$0(MenuBarMenuHandler.java:60)
	at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318)
	at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:771)
	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:722)
	at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:716)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
	at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:741)
	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
	at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)

---------------------------------------------------
Build Date: 2023-Dec-22 0936 EST
Ghidra Version: 11.0
Java Home: /Users/alex/Library/Java/JavaVirtualMachines/corretto-17.0.6/Contents/Home
JVM Version: Amazon.com Inc. 17.0.6
OS: Mac OS X 14.2.1 x86_64

@saagarjha
Copy link
Author

@al3xtjames try now

@al3xtjames
Copy link

Thanks for the fix, it works now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment