feat(gui): add button to install desktop file directly from jadx-gui (PR #2404)

* fix: missing/incorrect .desktop file values

- correct comment
- Add StartupWMClass

* feat: add button to install desktop file from jadx-gui (#1392)

* fix: use POSIX compliant realpath instead of readlink

* fix: get XDG executable from PATH
This commit is contained in:
Hidoni
2025-02-02 01:03:42 +02:00
committed by GitHub
parent b18604071a
commit afdd2d8b39
16 changed files with 244 additions and 3 deletions
+2 -1
View File
@@ -1,9 +1,10 @@
[Desktop Entry] [Desktop Entry]
Name=JADX GUI Name=JADX GUI
Comment=Dex to Java compiler Comment=Dex to Java decompiler
Icon=jadx Icon=jadx
Exec=jadx-gui %f Exec=jadx-gui %f
Terminal=false Terminal=false
Type=Application Type=Application
Categories=Development;Java; Categories=Development;Java;
Keywords=Java;Decompiler; Keywords=Java;Decompiler;
StartupWMClass=jadx-gui-JadxGUI
@@ -231,6 +231,16 @@ public class FileUtils {
} }
} }
public static Path createTempFileNonPrefixed(String fileName) {
try {
Path path = Files.createFile(tempRootDir.resolve(fileName));
path.toFile().deleteOnExit();
return path;
} catch (Exception e) {
throw new JadxRuntimeException("Failed to create non-prefixed temp file: " + fileName, e);
}
}
public static void copyStream(InputStream input, OutputStream output) throws IOException { public static void copyStream(InputStream input, OutputStream output) throws IOException {
byte[] buffer = new byte[READ_BUFFER_SIZE]; byte[] buffer = new byte[READ_BUFFER_SIZE];
while (true) { while (true) {
@@ -266,6 +276,11 @@ public class FileUtils {
StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
} }
public static void writeFile(Path file, InputStream is) throws IOException {
FileUtils.makeDirsForFile(file);
Files.copy(is, file, StandardCopyOption.REPLACE_EXISTING);
}
public static String readFile(Path textFile) throws IOException { public static String readFile(Path textFile) throws IOException {
return Files.readString(textFile); return Files.readString(textFile);
} }
+10 -2
View File
@@ -110,11 +110,19 @@ project.components.withType(AdhocComponentWithVariants::class.java).forEach { c
tasks.startShadowScripts { tasks.startShadowScripts {
doLast { doLast {
val newContent = val newWindowsScriptContent =
windowsScript.readText() windowsScript.readText()
.replace("java.exe", "javaw.exe") .replace("java.exe", "javaw.exe")
.replace("\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS%", "start \"jadx-gui\" /B \"%JAVA_EXE%\" %DEFAULT_JVM_OPTS%") .replace("\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS%", "start \"jadx-gui\" /B \"%JAVA_EXE%\" %DEFAULT_JVM_OPTS%")
windowsScript.writeText(newContent) // Add launch script path as a property
val newUnixScriptContent =
unixScript.readText()
.replace(
Regex("DEFAULT_JVM_OPTS=.+", RegexOption.MULTILINE),
{ result -> result.value + "\" \\\"-Djadx.launchScript.path=\$(realpath $0)\\\"\"" },
)
windowsScript.writeText(newWindowsScriptContent)
unixScript.writeText(newUnixScriptContent)
} }
} }
@@ -153,11 +153,13 @@ import jadx.gui.ui.treenodes.StartPageNode;
import jadx.gui.ui.treenodes.SummaryNode; import jadx.gui.ui.treenodes.SummaryNode;
import jadx.gui.update.JadxUpdate; import jadx.gui.update.JadxUpdate;
import jadx.gui.utils.CacheObject; import jadx.gui.utils.CacheObject;
import jadx.gui.utils.DesktopEntryUtils;
import jadx.gui.utils.FontUtils; import jadx.gui.utils.FontUtils;
import jadx.gui.utils.ILoadListener; import jadx.gui.utils.ILoadListener;
import jadx.gui.utils.LafManager; import jadx.gui.utils.LafManager;
import jadx.gui.utils.Link; import jadx.gui.utils.Link;
import jadx.gui.utils.NLS; import jadx.gui.utils.NLS;
import jadx.gui.utils.SystemInfo;
import jadx.gui.utils.UiUtils; import jadx.gui.utils.UiUtils;
import jadx.gui.utils.dbg.UIWatchDog; import jadx.gui.utils.dbg.UIWatchDog;
import jadx.gui.utils.fileswatcher.LiveReloadWorker; import jadx.gui.utils.fileswatcher.LiveReloadWorker;
@@ -1186,6 +1188,9 @@ public class MainWindow extends JFrame implements ExportProjectDialog.ExportProj
JMenu help = new JadxMenu(NLS.str("menu.help"), shortcutsController); JMenu help = new JadxMenu(NLS.str("menu.help"), shortcutsController);
help.setMnemonic(KeyEvent.VK_H); help.setMnemonic(KeyEvent.VK_H);
help.add(showLogAction); help.add(showLogAction);
if (SystemInfo.IS_UNIX && !SystemInfo.IS_MAC) {
help.add(new JadxGuiAction(ActionModel.CREATE_DESKTOP_ENTRY, this::createDesktopEntry));
}
if (Jadx.isDevVersion()) { if (Jadx.isDevVersion()) {
help.add(new AbstractAction("Show sample error report") { help.add(new AbstractAction("Show sample error report") {
@Override @Override
@@ -1724,6 +1729,16 @@ public class MainWindow extends JFrame implements ExportProjectDialog.ExportProj
pluginsMenu.add(item); pluginsMenu.add(item);
} }
private void createDesktopEntry() {
if (DesktopEntryUtils.createDesktopEntry()) {
JOptionPane.showMessageDialog(this, NLS.str("message.desktop_entry_creation_success"),
NLS.str("message.success_title"), JOptionPane.INFORMATION_MESSAGE);
} else {
JOptionPane.showMessageDialog(this, NLS.str("message.desktop_entry_creation_error"),
NLS.str("message.errorTitle"), JOptionPane.ERROR_MESSAGE);
}
}
public RenameMappingsGui getRenameMappings() { public RenameMappingsGui getRenameMappings() {
return renameMappings; return renameMappings;
} }
@@ -65,6 +65,7 @@ public enum ActionModel {
Shortcut.keyboard(KeyEvent.VK_D, UiUtils.ctrlButton() | KeyEvent.ALT_DOWN_MASK)), Shortcut.keyboard(KeyEvent.VK_D, UiUtils.ctrlButton() | KeyEvent.ALT_DOWN_MASK)),
SHOW_LOG(MENU_TOOLBAR, "menu.log", "menu.log", "ui/logVerbose", SHOW_LOG(MENU_TOOLBAR, "menu.log", "menu.log", "ui/logVerbose",
Shortcut.keyboard(KeyEvent.VK_L, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)), Shortcut.keyboard(KeyEvent.VK_L, UiUtils.ctrlButton() | KeyEvent.SHIFT_DOWN_MASK)),
CREATE_DESKTOP_ENTRY(MENU_TOOLBAR, "menu.create_desktop_entry", "menu.create_desktop_entry", null, Shortcut.none()),
BACK(MENU_TOOLBAR, "nav.back", "nav.back", "ui/left", BACK(MENU_TOOLBAR, "nav.back", "nav.back", "ui/left",
Shortcut.keyboard(KeyEvent.VK_ESCAPE)), Shortcut.keyboard(KeyEvent.VK_ESCAPE)),
BACK_V(MENU_TOOLBAR, "nav.back", "nav.back", "ui/left", BACK_V(MENU_TOOLBAR, "nav.back", "nav.back", "ui/left",
@@ -0,0 +1,155 @@
package jadx.gui.utils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Objects;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.export.TemplateFile;
import jadx.core.utils.files.FileUtils;
public class DesktopEntryUtils {
private static final Logger LOG = LoggerFactory.getLogger(DesktopEntryUtils.class);
private static final Map<Integer, String> SIZE_TO_LOGO_MAP = Map.of(
16, "jadx-logo-16px.png",
32, "jadx-logo-32px.png",
48, "jadx-logo-48px.png",
252, "jadx-logo.png",
256, "jadx-logo.png");
private static final Path XDG_DESKTOP_MENU_COMMAND_PATH = findExecutablePath("xdg-desktop-menu");
private static final Path XDG_ICON_RESOURCE_COMMAND_PATH = findExecutablePath("xdg-icon-resource");
public static boolean createDesktopEntry() {
if (XDG_DESKTOP_MENU_COMMAND_PATH == null) {
LOG.error("xdg-desktop-menu was not found in $PATH");
return false;
}
if (XDG_ICON_RESOURCE_COMMAND_PATH == null) {
LOG.error("xdg-icon-resource was not found in $PATH");
return false;
}
Path desktopTempFile = FileUtils.createTempFileNonPrefixed("jadx-gui.desktop");
Path iconTempFolder = FileUtils.createTempDir("logos");
LOG.debug("Creating desktop with temp files: {}, {}", desktopTempFile, iconTempFolder);
try {
return createDesktopEntry(desktopTempFile, iconTempFolder);
} finally {
try {
FileUtils.deleteFileIfExists(desktopTempFile);
FileUtils.deleteDirIfExists(iconTempFolder);
} catch (IOException e) {
LOG.error("Failed to clean up temp files", e);
}
}
}
private static boolean createDesktopEntry(Path desktopTempFile, Path iconTempFolder) {
String launchScriptPath = getLaunchScriptPath();
if (launchScriptPath == null) {
return false;
}
for (Map.Entry<Integer, String> entry : SIZE_TO_LOGO_MAP.entrySet()) {
Path path = iconTempFolder.resolve(entry.getKey() + ".png");
if (!writeLogoFile(entry.getValue(), path)) {
return false;
}
if (!installIcon(entry.getKey(), path)) {
return false;
}
}
if (!writeDesktopFile(launchScriptPath, desktopTempFile)) {
return false;
}
return installDesktopEntry(desktopTempFile);
}
private static boolean installDesktopEntry(Path desktopTempFile) {
try {
ProcessBuilder desktopFileInstallCommand = new ProcessBuilder(Objects.requireNonNull(XDG_DESKTOP_MENU_COMMAND_PATH).toString(),
"install", desktopTempFile.toString());
Process process = desktopFileInstallCommand.start();
int statusCode = process.waitFor();
if (statusCode != 0) {
LOG.error("Got error code {} while installing desktop file", statusCode);
return false;
}
} catch (Exception e) {
LOG.error("Failed to install desktop file", e);
return false;
}
LOG.info("Successfully installed desktop file");
return true;
}
private static boolean installIcon(int size, Path iconPath) {
try {
ProcessBuilder iconInstallCommand =
new ProcessBuilder(Objects.requireNonNull(XDG_ICON_RESOURCE_COMMAND_PATH).toString(), "install", "--novendor", "--size",
String.valueOf(size), iconPath.toString(),
"jadx");
Process process = iconInstallCommand.start();
int statusCode = process.waitFor();
if (statusCode != 0) {
LOG.error("Got error code {} while installing icon of size {}", statusCode, size);
return false;
}
} catch (Exception e) {
LOG.error("Failed to install icon of size {}", size, e);
return false;
}
LOG.info("Successfully installed icon of size {}", size);
return true;
}
private static Path findExecutablePath(String executableName) {
for (String pathDirectory : System.getenv("PATH").split(File.pathSeparator)) {
Path path = Paths.get(pathDirectory, executableName);
if (path.toFile().isFile() && path.toFile().canExecute()) {
return path;
}
}
return null;
}
private static boolean writeDesktopFile(String launchScriptPath, Path desktopFilePath) {
try {
TemplateFile tmpl = TemplateFile.fromResources("/files/jadx-gui.desktop.tmpl");
tmpl.add("launchScriptPath", launchScriptPath);
FileUtils.writeFile(desktopFilePath, tmpl.build());
} catch (Exception e) {
LOG.error("Failed to save .desktop file at: {}", desktopFilePath, e);
return false;
}
LOG.debug("Wrote .desktop file to {}", desktopFilePath);
return true;
}
private static boolean writeLogoFile(String logoFile, Path logoPath) {
try (InputStream is = DesktopEntryUtils.class.getResourceAsStream("/logos/" + logoFile)) {
FileUtils.writeFile(logoPath, is);
} catch (Exception e) {
LOG.error("Failed to write logo file at: {}", logoPath, e);
return false;
}
LOG.debug("Wrote logo file to: {}", logoPath);
return true;
}
public static @Nullable String getLaunchScriptPath() {
String launchScriptPath = System.getProperty("jadx.launchScript.path");
if (launchScriptPath.isEmpty()) {
LOG.error(
"The jadx.launchScript.path property is not set. Please launch JADX with the bundled launch script or set it to the appropriate value yourself.");
return null;
}
LOG.debug("JADX launch script path: {}", launchScriptPath);
return launchScriptPath;
}
}
@@ -0,0 +1,10 @@
[Desktop Entry]
Name=JADX GUI
Comment=Dex to Java decompiler
Icon=jadx
Exec={{launchScriptPath}} %f
Terminal=false
Type=Application
Categories=Development;Java;
Keywords=Java;Decompiler;
StartupWMClass=jadx-gui-JadxGUI
@@ -24,6 +24,7 @@ menu.tools=Tools
#menu.reset_cache=Reset code cache #menu.reset_cache=Reset code cache
menu.deobfuscation=Deobfuskierung menu.deobfuscation=Deobfuskierung
menu.log=Log-Anzeige menu.log=Log-Anzeige
#menu.create_desktop_entry=Create Desktop Entry
menu.help=Hilfe menu.help=Hilfe
menu.about=Über menu.about=Über
#menu.quark=Quark Engine #menu.quark=Quark Engine
@@ -113,6 +114,9 @@ message.indexingClassesSkipped=<html>Jadx hat nur noch wenig Speicherplatz. Dahe
#message.enter_new_name=Enter new name #message.enter_new_name=Enter new name
#message.could_not_rename=Can't rename the file #message.could_not_rename=Can't rename the file
#message.confirm_remove_script=Do you really want to remove script? #message.confirm_remove_script=Do you really want to remove script?
#message.desktop_entry_creation_error=Failed to create desktop entry (check log for details).
#message.desktop_entry_creation_success=Desktop entry created successfully!
#message.success_title=Success
heapUsage.text=JADX-Speicherauslastung: %.2f GB von %.2f GB heapUsage.text=JADX-Speicherauslastung: %.2f GB von %.2f GB
@@ -24,6 +24,7 @@ menu.decompile_all=Decompile all classes
menu.reset_cache=Reset code cache menu.reset_cache=Reset code cache
menu.deobfuscation=Deobfuscation menu.deobfuscation=Deobfuscation
menu.log=Log Viewer menu.log=Log Viewer
menu.create_desktop_entry=Create Desktop Entry
menu.help=Help menu.help=Help
menu.about=About menu.about=About
menu.quark=Quark Engine menu.quark=Quark Engine
@@ -113,6 +114,9 @@ message.indexingClassesSkipped=<html>Jadx is running low on memory. Therefore %d
message.enter_new_name=Enter new name message.enter_new_name=Enter new name
message.could_not_rename=Can't rename the file message.could_not_rename=Can't rename the file
message.confirm_remove_script=Do you really want to remove script? message.confirm_remove_script=Do you really want to remove script?
message.desktop_entry_creation_error=Failed to create desktop entry (check log for details).
message.desktop_entry_creation_success=Desktop entry created successfully!
message.success_title=Success
heapUsage.text=JADX memory usage: %.2f GB of %.2f GB heapUsage.text=JADX memory usage: %.2f GB of %.2f GB
@@ -24,6 +24,7 @@ menu.tools=Herramientas
#menu.reset_cache=Reset code cache #menu.reset_cache=Reset code cache
menu.deobfuscation=Desofuscación menu.deobfuscation=Desofuscación
menu.log=Visor log menu.log=Visor log
#menu.create_desktop_entry=Create Desktop Entry
menu.help=Ayuda menu.help=Ayuda
menu.about=Acerca de... menu.about=Acerca de...
#menu.quark=Quark Engine #menu.quark=Quark Engine
@@ -113,6 +114,9 @@ nav.forward=Adelante
#message.enter_new_name=Enter new name #message.enter_new_name=Enter new name
#message.could_not_rename=Can't rename the file #message.could_not_rename=Can't rename the file
#message.confirm_remove_script=Do you really want to remove script? #message.confirm_remove_script=Do you really want to remove script?
#message.desktop_entry_creation_error=Failed to create desktop entry (check log for details).
#message.desktop_entry_creation_success=Desktop entry created successfully!
#message.success_title=Success
#heapUsage.text=JADX memory usage: %.2f GB of %.2f GB #heapUsage.text=JADX memory usage: %.2f GB of %.2f GB
@@ -24,6 +24,7 @@ menu.decompile_all=Deskompilasi semua kelas
menu.reset_cache=Reset cache kode menu.reset_cache=Reset cache kode
menu.deobfuscation=Deobfikasi menu.deobfuscation=Deobfikasi
menu.log=Pemantau Log menu.log=Pemantau Log
#menu.create_desktop_entry=Create Desktop Entry
menu.help=Bantuan menu.help=Bantuan
menu.about=Tentang menu.about=Tentang
menu.quark=Mesin Quark menu.quark=Mesin Quark
@@ -113,6 +114,9 @@ message.indexingClassesSkipped=<html>JADX kekurangan memori. Oleh karena itu %d
#message.enter_new_name=Enter new name #message.enter_new_name=Enter new name
#message.could_not_rename=Can't rename the file #message.could_not_rename=Can't rename the file
#message.confirm_remove_script=Do you really want to remove script? #message.confirm_remove_script=Do you really want to remove script?
#message.desktop_entry_creation_error=Failed to create desktop entry (check log for details).
#message.desktop_entry_creation_success=Desktop entry created successfully!
#message.success_title=Success
heapUsage.text=Penggunaan memori JADX: %.2f GB dari %.2f GB heapUsage.text=Penggunaan memori JADX: %.2f GB dari %.2f GB
@@ -24,6 +24,7 @@ menu.tools=도구
#menu.reset_cache=Reset code cache #menu.reset_cache=Reset code cache
menu.deobfuscation=난독화 해제 menu.deobfuscation=난독화 해제
menu.log=로그 뷰어 menu.log=로그 뷰어
#menu.create_desktop_entry=Create Desktop Entry
menu.help=도움말 menu.help=도움말
menu.about=정보 menu.about=정보
#menu.quark=Quark Engine #menu.quark=Quark Engine
@@ -113,6 +114,9 @@ message.indexingClassesSkipped=<html>Jadx의 메모리가 부족합니다. 따
#message.enter_new_name=Enter new name #message.enter_new_name=Enter new name
#message.could_not_rename=Can't rename the file #message.could_not_rename=Can't rename the file
#message.confirm_remove_script=Do you really want to remove script? #message.confirm_remove_script=Do you really want to remove script?
#message.desktop_entry_creation_error=Failed to create desktop entry (check log for details).
#message.desktop_entry_creation_success=Desktop entry created successfully!
#message.success_title=Success
heapUsage.text=JADX 메모리 사용량 : %.2f GB / %.2f GB heapUsage.text=JADX 메모리 사용량 : %.2f GB / %.2f GB
@@ -24,6 +24,7 @@ menu.tools=Ferramentas
#menu.reset_cache=Reset code cache #menu.reset_cache=Reset code cache
menu.deobfuscation=Desofuscar menu.deobfuscation=Desofuscar
menu.log=Visualizador de log menu.log=Visualizador de log
#menu.create_desktop_entry=Create Desktop Entry
menu.help=Ajuda menu.help=Ajuda
menu.about=Sobre menu.about=Sobre
#menu.quark=Quark Engine #menu.quark=Quark Engine
@@ -113,6 +114,9 @@ message.indexingClassesSkipped=<html>Jadx está rodando com pouca memória. Por
#message.enter_new_name=Enter new name #message.enter_new_name=Enter new name
#message.could_not_rename=Can't rename the file #message.could_not_rename=Can't rename the file
#message.confirm_remove_script=Do you really want to remove script? #message.confirm_remove_script=Do you really want to remove script?
#message.desktop_entry_creation_error=Failed to create desktop entry (check log for details).
#message.desktop_entry_creation_success=Desktop entry created successfully!
#message.success_title=Success
heapUsage.text=Uso de memória do JADX: %.2f GB of %.2f GB heapUsage.text=Uso de memória do JADX: %.2f GB of %.2f GB
@@ -24,6 +24,7 @@ menu.decompile_all=Декомпилировать все
menu.reset_cache=Сбросить кэш menu.reset_cache=Сбросить кэш
menu.deobfuscation=Деобфускация menu.deobfuscation=Деобфускация
menu.log=Просмотр логов menu.log=Просмотр логов
#menu.create_desktop_entry=Create Desktop Entry
menu.help=Помощь menu.help=Помощь
menu.about=О программе menu.about=О программе
#menu.quark=Quark Engine #menu.quark=Quark Engine
@@ -113,6 +114,9 @@ message.indexingClassesSkipped=<html>JaDX запущен с малым коли
#message.enter_new_name=Enter new name #message.enter_new_name=Enter new name
#message.could_not_rename=Can't rename the file #message.could_not_rename=Can't rename the file
#message.confirm_remove_script=Do you really want to remove script? #message.confirm_remove_script=Do you really want to remove script?
#message.desktop_entry_creation_error=Failed to create desktop entry (check log for details).
#message.desktop_entry_creation_success=Desktop entry created successfully!
#message.success_title=Success
heapUsage.text=JADX использует: %.2f ГБ из %.2f ГБ heapUsage.text=JADX использует: %.2f ГБ из %.2f ГБ
@@ -24,6 +24,7 @@ menu.decompile_all=反编译所有类
menu.reset_cache=重置代码缓存 menu.reset_cache=重置代码缓存
menu.deobfuscation=反混淆 menu.deobfuscation=反混淆
menu.log=日志查看器 menu.log=日志查看器
#menu.create_desktop_entry=Create Desktop Entry
menu.help=帮助 menu.help=帮助
menu.about=关于 menu.about=关于
menu.quark=Quark 引擎 menu.quark=Quark 引擎
@@ -113,6 +114,9 @@ message.indexingClassesSkipped=<html>Jadx 的内存不足。因此,%d 个类
message.enter_new_name=输入新名称 message.enter_new_name=输入新名称
message.could_not_rename=无法重命名文件 message.could_not_rename=无法重命名文件
message.confirm_remove_script=您真的要删除脚本吗? message.confirm_remove_script=您真的要删除脚本吗?
#message.desktop_entry_creation_error=Failed to create desktop entry (check log for details).
#message.desktop_entry_creation_success=Desktop entry created successfully!
#message.success_title=Success
heapUsage.text=JADX 内存使用率:%.2f GB / %.2f GB heapUsage.text=JADX 内存使用率:%.2f GB / %.2f GB
@@ -24,6 +24,7 @@ menu.decompile_all=反編譯所有類別
menu.reset_cache=重設程式碼快取 menu.reset_cache=重設程式碼快取
menu.deobfuscation=去模糊化 menu.deobfuscation=去模糊化
menu.log=記錄檔檢視器 menu.log=記錄檔檢視器
#menu.create_desktop_entry=Create Desktop Entry
menu.help=幫助 menu.help=幫助
menu.about=關於 menu.about=關於
menu.quark= Quark 引擎 menu.quark= Quark 引擎
@@ -113,6 +114,9 @@ message.indexingClassesSkipped=<html>Jadx 的記憶體不足。故 %d 個類別
#message.enter_new_name=Enter new name #message.enter_new_name=Enter new name
#message.could_not_rename=Can't rename the file #message.could_not_rename=Can't rename the file
#message.confirm_remove_script=Do you really want to remove script? #message.confirm_remove_script=Do you really want to remove script?
#message.desktop_entry_creation_error=Failed to create desktop entry (check log for details).
#message.desktop_entry_creation_success=Desktop entry created successfully!
#message.success_title=Success
heapUsage.text=JADX 記憶體使用率:%.2f GB / %.2f GB heapUsage.text=JADX 記憶體使用率:%.2f GB / %.2f GB