From 31ee251a5c3e1576793c00a04597d1cdc10e8c3f Mon Sep 17 00:00:00 2001 From: Ryan Kurtz Date: Wed, 11 Dec 2024 12:43:39 -0500 Subject: [PATCH] GP-5138: GhidraDev/PyDev/PyGhidra integration --- Ghidra/Features/Base/certification.manifest | 1 + .../Base/src/main/resources/images/python.png | Bin 0 -> 1347 bytes .../src/main/resources/images/python.png | Bin 73663 -> 0 bytes .../src/main/py/src/pyghidra/launcher.py | 7 +- .../GhidraDev/GhidraDevFeature/feature.xml | 2 +- .../GhidraDevPlugin/.launch/GhidraDev.launch | 35 ++- .../GhidraDevPlugin/META-INF/MANIFEST.MF | 11 +- .../GhidraDev/GhidraDevPlugin/README.md | 41 ++- .../GhidraDev/GhidraDevPlugin/build.gradle | 17 +- .../GhidraDevPlugin/icons/python.png | Bin 0 -> 1347 bytes .../GhidraDev/GhidraDevPlugin/plugin.xml | 54 ++++ .../AbstractPyGhidraLaunchShortcut.java | 107 +++++++ .../launchers/GhidraLaunchDelegate.java | 10 +- .../launchers/PyGhidraGuiLaunchShortcut.java | 33 +++ .../launchers/PyGhidraLaunchDelegate.java | 142 +++++++++ .../PyGhidraProjectPropertyTester.java | 40 +++ .../utils/GhidraLaunchUtils.java | 6 + .../utils/GhidraModuleUtils.java | 10 +- .../utils/GhidraProjectUtils.java | 59 ++-- .../utils/GhidraScriptUtils.java | 13 +- .../utils/PyDevUtils.java | 254 ++++++++++++++-- .../utils/PyDevUtilsInternal.java | 208 ++++++++++++- .../CreateGhidraModuleProjectWizard.java | 16 +- .../CreateGhidraScriptProjectWizard.java | 18 +- .../wizards/ExportGhidraModuleWizard.java | 8 +- .../ImportGhidraModuleSourceWizard.java | 19 +- .../wizards/LinkGhidraWizard.java | 27 +- .../ChooseGhidraInstallationWizardPage.java | 10 +- .../wizards/pages/EnablePythonWizardPage.java | 276 ++++++++++++++---- .../GhidraDev/certification.manifest | 1 + .../src/main/java/LaunchSupport.java | 64 ++-- .../{JavaConfig.java => AppConfig.java} | 97 +++--- .../main/java/ghidra/launch/JavaFinder.java | 20 +- gradle/root/distribution.gradle | 3 + gradle/support/fetchDependencies.gradle | 6 +- 35 files changed, 1300 insertions(+), 315 deletions(-) create mode 100644 Ghidra/Features/Base/src/main/resources/images/python.png delete mode 100644 Ghidra/Features/Jython/src/main/resources/images/python.png create mode 100644 GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/icons/python.png create mode 100644 GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/launchers/AbstractPyGhidraLaunchShortcut.java create mode 100644 GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/launchers/PyGhidraGuiLaunchShortcut.java create mode 100644 GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/launchers/PyGhidraLaunchDelegate.java create mode 100644 GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/testers/PyGhidraProjectPropertyTester.java rename GhidraBuild/LaunchSupport/src/main/java/ghidra/launch/{JavaConfig.java => AppConfig.java} (83%) diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest index 16ac75be9d..a9fd2ec2ca 100644 --- a/Ghidra/Features/Base/certification.manifest +++ b/Ghidra/Features/Base/certification.manifest @@ -898,6 +898,7 @@ src/main/resources/images/pencil16.png||GHIDRA||||END| src/main/resources/images/pin.png||GHIDRA||||END| src/main/resources/images/play_again.png||GHIDRA||||END| src/main/resources/images/preferences-system.png||Tango Icons - Public Domain|||tango|END| +src/main/resources/images/python.png||GHIDRA||||END| src/main/resources/images/question_zero.png||GHIDRA||||END| src/main/resources/images/red-cross.png||GHIDRA||||END| src/main/resources/images/redQuestionMark.png||GHIDRA||||END| diff --git a/Ghidra/Features/Base/src/main/resources/images/python.png b/Ghidra/Features/Base/src/main/resources/images/python.png new file mode 100644 index 0000000000000000000000000000000000000000..d89d2e7d1ce186b385e1f86077aabff68cac7aaa GIT binary patch literal 1347 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|T2doC(|mmy zw18|523AHP24;{FAY@>aVqgWc85q16rQz%#Mh&PMCI*J~Oa>OHnkXO*0v4nJa0`PlBg3pY5xV%QuQiw3qZOUY$~jP%-qzHM1_jnoV;SI3R@+x3M(KRB&@Hb09I0xZL1XF8=&Bv zUzDm~re~mMpk&9TprBw=l#*r@P?Wt5Z@Sn2DRmzV368|&p4rRy77T3YHG z80i}s=>k>g7FXt#Bv$C=6)QswftllyTAW;zSx}OhpQivaH!&%{w8U0P31kr*K-^i9 znTD__uNdkrpa=CqGWv#k2KsQbfm&@qqE`MznW;dVLFU^T+JIG}h(YbK(Fa+M!{-jsD+tMgbAoy2nk1jRdV1c^pzEIn~!jmz94 zv!Z&>ov(gB?{RtSNrV3CozH9T|2$XTx8VC}qjsIko!y@oKl=MU&5G{@LrCQhwKMM) z9(mLJzHhC;d*{4{U2=7QL*mZgjq&FQ*F zQnvEn-kd#^H*x*iO|$YC#U8zUnQ51H&S*#JGP%d^iiN?HGWTmQZ4xcb@C5o_mc+^_y#>0JLY;m!7`Cm($9nV=z}X=u53Id7l=f7F(# zbs~S3e^C~?s>tKYsr2S}_oLYh60{ne=Z7eA*2Jt`qG5Yiymjfy#T&Lnt!7-EUGMs` zzAK5N;-*;=|0D)_paOPt&>^pClxqcl5^c&C8YO`{ix7miOn-=?Sv~&+cn#Zv3=ZVr5ZY?VOl1hYlNheyzGARKIf9HY=xc{_;J06@GKbXiNNF zwmReG`aRnh>a0HHS+M6}P24@#)$jlOQmUO8yY^iA**;Ube|;fki$YnZb8GGJb1*1j z_WU;`c7Mzp-B^pvmXlXPY-gSK+uEQrBiQlazNx$R_HQt>Jy*B=;0N~P_}gdwcitD- zu|a>C+QVlZXV3cnX0>Z!P&G4{(<1B3(eq*3gW0-^HU6tU6t>)Rg;(s<^X{busn?@9 zavmQ1XKB*mSH~L1AHbSg%;a+EBU`86>3P5VXS|*v%~Zkfx@L!rPZgsds3h@p^>bP0 Hl+XkK3}5@n literal 0 HcmV?d00001 diff --git a/Ghidra/Features/Jython/src/main/resources/images/python.png b/Ghidra/Features/Jython/src/main/resources/images/python.png deleted file mode 100644 index 193c227431b5b0ed66eb3dab40ffc448a1177009..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 73663 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 Path: """ diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevFeature/feature.xml b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevFeature/feature.xml index f4a0c661f6..d77986fab7 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevFeature/feature.xml +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevFeature/feature.xml @@ -2,7 +2,7 @@ diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/.launch/GhidraDev.launch b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/.launch/GhidraDev.launch index b78e35f9df..655bd9c454 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/.launch/GhidraDev.launch +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/.launch/GhidraDev.launch @@ -167,7 +167,9 @@ - + + + @@ -176,7 +178,6 @@ - @@ -202,6 +203,8 @@ + + @@ -296,7 +299,7 @@ - + @@ -308,10 +311,11 @@ + + - @@ -414,14 +418,19 @@ - - - - - - - - + + + + + + + + + + + + + @@ -430,7 +439,7 @@ - + diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/META-INF/MANIFEST.MF b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/META-INF/MANIFEST.MF index e6c54306be..54947f8f3c 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/META-INF/MANIFEST.MF +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/META-INF/MANIFEST.MF @@ -3,7 +3,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: GhidraDev Bundle-SymbolicName: ghidra.ghidradev;singleton:=true -Bundle-Version: 4.0.1.qualifier +Bundle-Version: 5.0.0.qualifier Bundle-Activator: ghidradev.Activator Require-Bundle: org.eclipse.ant.core;bundle-version="3.7.200", org.eclipse.buildship.core;bundle-version="3.1.8", @@ -21,10 +21,11 @@ Require-Bundle: org.eclipse.ant.core;bundle-version="3.7.200", org.eclipse.ltk.core.refactoring;bundle-version="3.14.200", org.eclipse.ui;bundle-version="3.205.0", org.eclipse.ui.ide;bundle-version="3.22.0", - com.python.pydev.debug;bundle-version="6.3.1";resolution:=optional, - org.python.pydev;bundle-version="[6.3.1,10.0.0)";resolution:=optional, - org.python.pydev.core;bundle-version="[6.3.1,10.0.0)";resolution:=optional, - org.python.pydev.ast;bundle-version="[6.3.1,10.0.0)";resolution:=optional, + com.python.pydev.debug;bundle-version="9.3.0";resolution:=optional, + org.python.pydev;bundle-version="9.3.0";resolution:=optional, + org.python.pydev.core;bundle-version="9.3.0";resolution:=optional, + org.python.pydev.ast;bundle-version="9.3.0";resolution:=optional, + org.python.pydev.debug;bundle-version="9.3.0";resolution:=optional, org.eclipse.cdt.core;bundle-version="5.9.1";resolution:=optional, org.eclipse.cdt.ui;bundle-version="5.9.0";resolution:=optional Bundle-RequiredExecutionEnvironment: JavaSE-21 diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/README.md b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/README.md index cb16d73996..4fa5b8a0d4 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/README.md +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/README.md @@ -1,7 +1,7 @@ # GhidraDev Eclipse Plugin GhidraDev provides support for developing and debugging Ghidra scripts and modules in Eclipse. -The information provided in this document is effective as of GhidraDev 4.0.0 and is subject to +The information provided in this document is effective as of GhidraDev 5.0.0 and is subject to change with future releases. ## Table of Contents @@ -31,6 +31,9 @@ change with future releases. 12. [Building](#building) ## Change History +__5.0.0:__ +* Added support for PyGhidra. + __4.0.1:__ * New Ghidra module projects now contain a default `README.md` file. * Fixed a bug that prevented an imported module source project from being discovered by Ghidra when @@ -132,7 +135,7 @@ __1.0.1:__ * Ghidra 11.2 or later ## Optional Requirements -* PyDev 6.3.1 - 9.3.0 ([more info](#pydev-support)) +* PyDev 9.3.0 or later ([more info](#pydev-support)) * Gradle - required version(s) specified by linked Ghidra release ([more info](#export-ghidra-module-extension)) @@ -268,7 +271,10 @@ Ghidra from Eclipse independent of a project is not supported. ## PyDev Support GhidraDev is able to integrate with PyDev to conveniently configure Python support into Ghidra -script and module projects. +script and module projects. GhidraDev supports both Jython and PyGhidra Python implementations. + +__NOTE:__ PyDev discontinued Jython 2 support in version 10.0.0. If you want to use GhidraDev with +Jython, you must use __PyDev 9.3.0__. The latest vesions of PyDev support PyGhidra. ### Installing PyDev From Eclipse: @@ -293,13 +299,19 @@ GhidraDev can add Python support to a Ghidra project when: * Creating a new Ghidra script project * Linking a Ghidra installation to an existing Java project -In order for GhidraDev to add in Python support, PyDev must have a Jython interpreter configured. -GhidraDev will present a list of detected Jython interpreters that it found in PyDev's preferences. -If no Jython interpreters were found, one can be added from GhidraDev by clicking the `+` icon. -When the `+` icon is clicked, GhidraDev will attempt to find the Jython interpreter bundled with the -selected Ghidra installation and automatically configure PyDev to use it. If for some reason -GhidraDev was unable to find a Jython interpreter in the Ghidra installation, one will have to be -added manually in the PyDev preferences. +In order for GhidraDev to add in Python support, PyDev must have a PyGhidra or Jython interpreter +configured. GhidraDev will present a list of detected PyGhidra/Jython interpreters that it found in +PyDev's preferences. If no interpreters were found, one can be added from GhidraDev by clicking +the `+` icons. + +When the Jython `+` icon is clicked, GhidraDev will attempt to find the Jython interpreter bundled +with the selected Ghidra installation and automatically configure PyDev to use it. If for some +reason GhidraDev was unable to find a Jython interpreter in the Ghidra installation, one will have +to be added manually in the PyDev preferences. + +When the PyGhidra `+` icon is clicked, GhidraDev will attempt to find the PyGhidra interpreter +that was last used to launch PyGhidra. If it cannot find it, you will have to launch PyGhidra +and try again. ## Upgrading GhidraDev is upgraded differently depending on how it was installed. If GhidraDev was @@ -347,9 +359,6 @@ installation directory. to your Ghidra module project, which automatically happens when the project is created. Simply [relink](#link-ghidra) your Ghidra installation to the project, and your project will pick up any newly discovered Ghidra extensions. -* __Why doesn't GhidraDev support PyDev 10.0 or later?__ - * PyDev dropped support for Python 2 in their 10.0 release. Ghidra currently does not support - Python 3. ## Additional Resources For more information on the GhidraDev plugin and developing for Ghidra in an Eclipse environment, @@ -358,14 +367,14 @@ at `/docs/GhidraClass/Intermediate/Scripting.html`. ## Building GhidraDev is currently built from Eclipse and distributed with Ghidra manually. Ideally we will use -Gradle one day, but we aren't there yet. We do rely on `gradle prepDev` to generate the Eclipse -project and build GhidraDev's dependencies though. +Gradle one day, but we aren't there yet. We do rely on Gradle to generate the Eclipse project and +build GhidraDev's dependencies though. __NOTE:__ Only "Eclipse for RCP and RAP Developers" has the ability to do the below instructions. The following instructions assume that you are using this version of Eclipse. #### Importing GhidraDev Eclipse projects (they are deactivated by default): -1. Run `gradle eclipse -PeclipsePDE` +1. Run `gradle prepGhidraDev eclipse -PeclipsePDE` 2. From Eclipse, `File -> Import -> General -> Existing Projects into Workspace` 3. From the ghidra repo, import `Eclipse GhidraDevFeature` and `Eclipse GhidraDevPlugin` diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/build.gradle b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/build.gradle index e04ec6715b..9b4e22453e 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/build.gradle +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/build.gradle @@ -81,8 +81,8 @@ task pyDevUnpack(type:Copy) { !pyDevDestDir.exists() } - File depsFile = file("${DEPS_DIR}/GhidraDev/PyDev 6.3.1.zip") - File binRepoFile = file("${BIN_REPO}/GhidraBuild/EclipsePlugins/GhidraDev/buildDependencies/PyDev 6.3.1.zip") + File depsFile = file("${DEPS_DIR}/GhidraDev/PyDev 9.3.0.zip") + File binRepoFile = file("${BIN_REPO}/GhidraBuild/EclipsePlugins/GhidraDev/buildDependencies/PyDev 9.3.0.zip") // First check if the file is in the dependencies repo. If not, check in the bin repo. def pyDevZipTree = depsFile.exists() ? zipTree(depsFile) : zipTree(binRepoFile) @@ -115,6 +115,13 @@ task cdtUnpack(type:Copy) { destinationDir cdtDestDir } +task prepGhidraDev { + dependsOn("utilityJar") + dependsOn("launchSupportJar") + dependsOn("pyDevUnpack") + dependsOn("cdtUnpack") +} + // We do not currently build GhidraDev plugin at Ghidra build time so we must // copy the prebuilt zip file from the BIN_REPO rootProject.assembleDistribution { @@ -129,9 +136,3 @@ rootProject.assembleMarkdownToHtml { into "Extensions/Eclipse/GhidraDev/" } } - -// PrepDev dependencies -rootProject.prepDev.dependsOn utilityJar -rootProject.prepDev.dependsOn launchSupportJar -rootProject.prepDev.dependsOn pyDevUnpack -rootProject.prepDev.dependsOn cdtUnpack diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/icons/python.png b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/icons/python.png new file mode 100644 index 0000000000000000000000000000000000000000..d89d2e7d1ce186b385e1f86077aabff68cac7aaa GIT binary patch literal 1347 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|T2doC(|mmy zw18|523AHP24;{FAY@>aVqgWc85q16rQz%#Mh&PMCI*J~Oa>OHnkXO*0v4nJa0`PlBg3pY5xV%QuQiw3qZOUY$~jP%-qzHM1_jnoV;SI3R@+x3M(KRB&@Hb09I0xZL1XF8=&Bv zUzDm~re~mMpk&9TprBw=l#*r@P?Wt5Z@Sn2DRmzV368|&p4rRy77T3YHG z80i}s=>k>g7FXt#Bv$C=6)QswftllyTAW;zSx}OhpQivaH!&%{w8U0P31kr*K-^i9 znTD__uNdkrpa=CqGWv#k2KsQbfm&@qqE`MznW;dVLFU^T+JIG}h(YbK(Fa+M!{-jsD+tMgbAoy2nk1jRdV1c^pzEIn~!jmz94 zv!Z&>ov(gB?{RtSNrV3CozH9T|2$XTx8VC}qjsIko!y@oKl=MU&5G{@LrCQhwKMM) z9(mLJzHhC;d*{4{U2=7QL*mZgjq&FQ*F zQnvEn-kd#^H*x*iO|$YC#U8zUnQ51H&S*#JGP%d^iiN?HGWTmQZ4xcb@C5o_mc+^_y#>0JLY;m!7`Cm($9nV=z}X=u53Id7l=f7F(# zbs~S3e^C~?s>tKYsr2S}_oLYh60{ne=Z7eA*2Jt`qG5Yiymjfy#T&Lnt!7-EUGMs` zzAK5N;-*;=|0D)_paOPt&>^pClxqcl5^c&C8YO`{ix7miOn-=?Sv~&+cn#Zv3=ZVr5ZY?VOl1hYlNheyzGARKIf9HY=xc{_;J06@GKbXiNNF zwmReG`aRnh>a0HHS+M6}P24@#)$jlOQmUO8yY^iA**;Ube|;fki$YnZb8GGJb1*1j z_WU;`c7Mzp-B^pvmXlXPY-gSK+uEQrBiQlazNx$R_HQt>Jy*B=;0N~P_}gdwcitD- zu|a>C+QVlZXV3cnX0>Z!P&G4{(<1B3(eq*3gW0-^HU6tU6t>)Rg;(s<^X{busn?@9 zavmQ1XKB*mSH~L1AHbSg%;a+EBU`86>3P5VXS|*v%~Zkfx@L!rPZgsds3h@p^>bP0 Hl+XkK3}5@n literal 0 HcmV?d00001 diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/plugin.xml b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/plugin.xml index d377ab0ca7..834baa1ee3 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/plugin.xml +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/plugin.xml @@ -378,6 +378,11 @@ id="GhidraHeadlessLaunchConfigurationType" name="Ghidra Headless"> + + @@ -391,6 +396,11 @@ icon="icons/GhidraIcon16_bw.png" id="GhidraHeadlessLaunchConfigurationTypeImage"> + + @@ -408,6 +418,13 @@ name="Ghidra Headless" type="GhidraHeadlessLaunchConfigurationType"> + + @@ -423,6 +440,12 @@ id="GhidraHeadlessLaunchConfigurationTabGroup" type="GhidraHeadlessLaunchConfigurationType"> + + @@ -474,6 +497,30 @@ + + + + + + + + + + + + + + @@ -505,6 +552,13 @@ properties="isGhidraModuleProject" type="java.lang.Object"> + + diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/launchers/AbstractPyGhidraLaunchShortcut.java b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/launchers/AbstractPyGhidraLaunchShortcut.java new file mode 100644 index 0000000000..c24cb497c5 --- /dev/null +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/launchers/AbstractPyGhidraLaunchShortcut.java @@ -0,0 +1,107 @@ +/* ### + * 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 ghidradev.ghidraprojectcreator.launchers; + +import javax.naming.OperationNotSupportedException; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.*; +import org.eclipse.debug.core.*; +import org.eclipse.debug.ui.ILaunchShortcut; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; + +import ghidradev.Activator; +import ghidradev.EclipseMessageUtils; +import ghidradev.ghidraprojectcreator.testers.GhidraProjectPropertyTester; +import ghidradev.ghidraprojectcreator.utils.*; + +/** + * PyGhidra launch shortcut actions. These shortcuts appear when you right click on a + * PyGhidra project or file and select "Run As" or "Debug As". + *

+ * The {@link GhidraProjectPropertyTester} is used to determine whether or not the shortcuts appear. + */ +public abstract class AbstractPyGhidraLaunchShortcut implements ILaunchShortcut { + + private String launchConfigTypeId; + private String launchConfigNameSuffix; + + /** + * Creates a new PyGhidra launch shortcut associated with the given launch configuration type ID. + * + * @param launchConfigTypeId The launch configuration type ID of this PyGhidra launch shortcut. + * @param launchConfigNameSuffix A string to append to the name of the launch configuration. + */ + protected AbstractPyGhidraLaunchShortcut(String launchConfigTypeId, + String launchConfigNameSuffix) { + this.launchConfigTypeId = launchConfigTypeId; + this.launchConfigNameSuffix = launchConfigNameSuffix; + } + + @Override + public void launch(ISelection selection, String mode) { + IProject project = GhidraProjectUtils.getSelectedProject(selection); + if (project != null) { + launch(project, mode); + } + } + + @Override + public void launch(IEditorPart editor, String mode) { + IEditorInput input = editor.getEditorInput(); + IResource resource = input.getAdapter(IResource.class); + if (resource != null) { + launch(resource.getProject(), mode); + } + } + + /** + * Launches the given Python nature in the given mode with a PyGhidra launcher. + * + * @param project The project to launch. + * @param mode The mode to launch in (run/debug). + */ + private void launch(IProject project, String mode) { + ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); + ILaunchConfigurationType launchType = + launchManager.getLaunchConfigurationType(launchConfigTypeId); + String launchConfigName = project.getName() + launchConfigNameSuffix; + try { + ILaunchConfiguration lc = GhidraLaunchUtils.getLaunchConfig(launchConfigName); + ILaunchConfigurationWorkingCopy wc = null; + if (lc == null) { + wc = launchType.newInstance(null, launchConfigName); + wc.setAttribute(PyDevUtils.getAttrProject(), project.getName()); + } + else if (lc.getType().equals(launchType)) { + wc = lc.getWorkingCopy(); + } + else { + throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, + IStatus.ERROR, "Failed to launch. Run configuration with name \"" + + launchConfigName + "\" already exists.", + null)); + } + wc.doSave().launch(mode, null); + } + catch (CoreException | OperationNotSupportedException e) { + EclipseMessageUtils.showErrorDialog(e.getMessage()); + } + } +} diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/launchers/GhidraLaunchDelegate.java b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/launchers/GhidraLaunchDelegate.java index 12beee4473..80d8d5c0b7 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/launchers/GhidraLaunchDelegate.java +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/launchers/GhidraLaunchDelegate.java @@ -33,7 +33,7 @@ import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IPerspectiveDescriptor; import org.eclipse.ui.PlatformUI; -import ghidra.launch.JavaConfig; +import ghidra.launch.AppConfig; import ghidradev.EclipseMessageUtils; import ghidradev.ghidraprojectcreator.utils.*; @@ -62,10 +62,10 @@ public class GhidraLaunchDelegate extends JavaLaunchDelegate { } IFolder ghidraFolder = javaProject.getProject().getFolder(GhidraProjectUtils.GHIDRA_FOLDER_NAME); - JavaConfig javaConfig; + AppConfig appConfig; String ghidraInstallPath = ghidraFolder.getLocation().toOSString(); try { - javaConfig = new JavaConfig(new File(ghidraInstallPath)); + appConfig = new AppConfig(new File(ghidraInstallPath)); } catch (ParseException | IOException e) { EclipseMessageUtils.showErrorDialog( @@ -98,7 +98,7 @@ public class GhidraLaunchDelegate extends JavaLaunchDelegate { } // Set VM arguments - String vmArgs = javaConfig.getLaunchProperties().getVmArgs(); + String vmArgs = appConfig.getLaunchProperties().getVmArgs(); vmArgs += " " + configuration.getAttribute(GhidraLaunchUtils.ATTR_VM_ARGUMENTS, "").trim(); vmArgs += " -Dghidra.external.modules=\"%s%s%s\"".formatted( javaProject.getProject().getLocation(), File.pathSeparator, @@ -171,7 +171,7 @@ public class GhidraLaunchDelegate extends JavaLaunchDelegate { } // Start PyDev debugger - if (PyDevUtils.isSupportedPyDevInstalled()) { + if (PyDevUtils.isSupportedJythonPyDevInstalled()) { try { PyDevUtils.startPyDevRemoteDebugger(); } diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/launchers/PyGhidraGuiLaunchShortcut.java b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/launchers/PyGhidraGuiLaunchShortcut.java new file mode 100644 index 0000000000..ea081e950e --- /dev/null +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/launchers/PyGhidraGuiLaunchShortcut.java @@ -0,0 +1,33 @@ +/* ### + * 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 ghidradev.ghidraprojectcreator.launchers; + +import ghidradev.ghidraprojectcreator.utils.GhidraLaunchUtils; + +/** + * The PyGhidra GUI launch shortcut actions. + * + * @see AbstractGhidraLaunchShortcut + */ +public class PyGhidraGuiLaunchShortcut extends AbstractPyGhidraLaunchShortcut { + + /** + * Creates a new PyGhidra GUI launch shortcut. + */ + public PyGhidraGuiLaunchShortcut() { + super(GhidraLaunchUtils.PYGHIDRA_GUI_LAUNCH, " GUI"); + } +} diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/launchers/PyGhidraLaunchDelegate.java b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/launchers/PyGhidraLaunchDelegate.java new file mode 100644 index 0000000000..0d790422cd --- /dev/null +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/launchers/PyGhidraLaunchDelegate.java @@ -0,0 +1,142 @@ +/* ### + * 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 ghidradev.ghidraprojectcreator.launchers; + +import java.io.File; +import java.io.IOException; +import java.text.ParseException; +import java.util.HashMap; +import java.util.Map; + +import javax.naming.OperationNotSupportedException; + +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.debug.core.*; +import org.eclipse.debug.ui.IDebugUIConstants; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IPerspectiveDescriptor; +import org.eclipse.ui.PlatformUI; +import org.python.pydev.debug.ui.launching.RegularLaunchConfigurationDelegate; + +import ghidra.launch.AppConfig; +import ghidradev.EclipseMessageUtils; +import ghidradev.ghidraprojectcreator.utils.GhidraProjectUtils; +import ghidradev.ghidraprojectcreator.utils.PyDevUtils; + +/** + * The PyGhidra Launch delegate handles the final launch of PyGhidra. + * We can do any extra custom launch behavior here. + */ +public class PyGhidraLaunchDelegate extends RegularLaunchConfigurationDelegate { + + @Override + public void launch(ILaunchConfiguration configuration, String mode, ILaunch launch, + IProgressMonitor monitor) throws CoreException { + + try { + ILaunchConfigurationWorkingCopy wc = configuration.getWorkingCopy(); + + // Get project + String projectName = wc.getAttribute(PyDevUtils.getAttrProject(), ""); + IJavaProject javaProject = GhidraProjectUtils.getGhidraProject(projectName); + if (javaProject == null) { + EclipseMessageUtils.showErrorDialog("Failed to launch project \"" + projectName + + "\".\nDoes not appear to be a Ghidra project."); + return; + } + IProject project = javaProject.getProject(); + + // Get needed application.properties values + String javaComplianceLevel = null; + String ghidraVmErrorMsg = ""; + try { + IFolder ghidraFolder = project.getFolder(GhidraProjectUtils.GHIDRA_FOLDER_NAME); + String ghidraInstallPath = ghidraFolder.getLocation().toOSString(); + AppConfig appConfig = new AppConfig(new File(ghidraInstallPath)); + javaComplianceLevel = appConfig.getCompilerComplianceLevel(); + } + catch (ParseException | IOException e) { + ghidraVmErrorMsg = e.getMessage(); + } + if (javaComplianceLevel == null) { + EclipseMessageUtils + .showErrorDialog("Failed to get JVM compliance level from project \"" + + projectName + "\".\n" + ghidraVmErrorMsg); + return; + } + + // Set program location + wc.setAttribute(PyDevUtils.getAttrLocation(), + "${workspace_loc:%s/Ghidra/Ghidra/Features/PyGhidra/pypkg/src/pyghidra}" + .formatted(project.getName())); + + // Set program arguments + wc.setAttribute(PyDevUtils.getAttrProgramArguments(), "-v -g"); + + // Set Python interpreter + String interpreterName = PyDevUtils.getInterpreterName(project); + wc.setAttribute(PyDevUtils.getAttrInterpreter(), interpreterName); + wc.setAttribute(PyDevUtils.getAttrInterpreterDefault(), interpreterName); + + // Set environment variables + Map env = new HashMap<>(); + //env.put("GHIDRA_INSTALL_DIR", "${project_loc:/%s/Ghidra}".formatted(project.getName())); + env.put("GHIDRA_INSTALL_DIR", + "${resource_loc:/%s/Ghidra}".formatted(project.getName())); + env.put("JAVA_HOME_OVERRIDE", "${ee_home:JavaSE-%s}".formatted(javaComplianceLevel)); + if (mode.equals("debug")) { + env.put("PYGHIDRA_DEBUG", "1"); + handleDebugMode(); + } + wc.setAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES, env); + + super.launch(wc.doSave(), mode, launch, monitor); + } + catch (OperationNotSupportedException e) { + EclipseMessageUtils.showErrorDialog("PyDev error", + "Failed to launch. PyDev version is not supported."); + } + } + + /** + * Handles extra things that should happen when we are launching in debug mode. + */ + private static void handleDebugMode() { + Display.getDefault().asyncExec(() -> { + + // Switch to debug perspective + if (PlatformUI.getWorkbench() != null) { + IPerspectiveDescriptor descriptor = + PlatformUI.getWorkbench().getPerspectiveRegistry().findPerspectiveWithId( + IDebugUIConstants.ID_DEBUG_PERSPECTIVE); + EclipseMessageUtils.getWorkbenchPage().setPerspective(descriptor); + } + + // Start PyDev debugger + try { + PyDevUtils.startPyDevRemoteDebugger(); + } + catch (OperationNotSupportedException e) { + EclipseMessageUtils.error( + "Failed to start the PyDev remote debugger. PyDev version is not supported."); + } + }); + } +} diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/testers/PyGhidraProjectPropertyTester.java b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/testers/PyGhidraProjectPropertyTester.java new file mode 100644 index 0000000000..55c18da027 --- /dev/null +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/testers/PyGhidraProjectPropertyTester.java @@ -0,0 +1,40 @@ +/* ### + * 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 ghidradev.ghidraprojectcreator.testers; + +import javax.naming.OperationNotSupportedException; + +import org.eclipse.core.expressions.PropertyTester; + +import ghidradev.ghidraprojectcreator.utils.GhidraProjectUtils; +import ghidradev.ghidraprojectcreator.utils.PyDevUtils; + +/** + * A {@link PropertyTester} used to determine if a given Eclipse resource is part + * of a PyGhidra project. + */ +public class PyGhidraProjectPropertyTester extends PropertyTester { + + @Override + public boolean test(Object receiver, String property, Object[] args, Object expectedValue) { + try { + return PyDevUtils.isPyGhidraProject(GhidraProjectUtils.getEnclosingProject(receiver)); + } + catch (OperationNotSupportedException e) { + return false; + } + } +} diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/GhidraLaunchUtils.java b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/GhidraLaunchUtils.java index 73791e259e..bd88074cd0 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/GhidraLaunchUtils.java +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/GhidraLaunchUtils.java @@ -49,6 +49,12 @@ public class GhidraLaunchUtils { */ public static final String HEADLESS_LAUNCH = "GhidraHeadlessLaunchConfigurationType"; + /** + * Launch configuration ID for a PyGhidra GUI launch. Must match corresponding value in + * plugin.xml. + */ + public static final String PYGHIDRA_GUI_LAUNCH = "PyGhidraGuiLaunchConfigurationType"; + /** * Program arguments that will get passed to the launched Ghidra. These will be appended * to the required program arguments that are required to launch Ghidra, which are hidden diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/GhidraModuleUtils.java b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/GhidraModuleUtils.java index 97993967d8..8394bb62ac 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/GhidraModuleUtils.java +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/GhidraModuleUtils.java @@ -30,6 +30,7 @@ import org.eclipse.ltk.core.refactoring.*; import ghidra.GhidraApplicationLayout; import ghidra.util.exception.CancelledException; +import ghidradev.ghidraprojectcreator.utils.PyDevUtils.ProjectPythonInterpreter; import utilities.util.FileUtilities; /** @@ -89,7 +90,7 @@ public class GhidraModuleUtils { */ public static IJavaProject createGhidraModuleProject(String projectName, File projectDir, boolean createRunConfig, String runConfigMemory, GhidraApplicationLayout ghidraLayout, - String jythonInterpreterName, IProgressMonitor monitor) + ProjectPythonInterpreter jythonInterpreterName, IProgressMonitor monitor) throws IOException, ParseException, CoreException { // Create empty Ghidra project @@ -227,8 +228,7 @@ public class GhidraModuleUtils { * @param createRunConfig Whether or not to create a new run configuration for the project. * @param runConfigMemory The run configuration's desired memory. Could be null. * @param ghidraLayout The Ghidra layout to link the project to. - * @param jythonInterpreterName The name of the Jython interpreter to use for Python support. - * Could be null if Python support is not wanted. + * @param pythonInterpreter The Python interpreter to use. * @param monitor The progress monitor to use during project creation. * @return The imported project. * @throws IOException If there was a file-related problem with creating the project. @@ -237,13 +237,13 @@ public class GhidraModuleUtils { */ public static IJavaProject importGhidraModuleSource(String projectName, File moduleSourceDir, boolean createRunConfig, String runConfigMemory, GhidraApplicationLayout ghidraLayout, - String jythonInterpreterName, IProgressMonitor monitor) + ProjectPythonInterpreter pythonInterpreter, IProgressMonitor monitor) throws IOException, ParseException, CoreException { // Create empty Ghidra project IJavaProject javaProject = GhidraProjectUtils.createEmptyGhidraProject(projectName, moduleSourceDir, - createRunConfig, runConfigMemory, ghidraLayout, jythonInterpreterName, monitor); + createRunConfig, runConfigMemory, ghidraLayout, pythonInterpreter, monitor); IProject project = javaProject.getProject(); // Set default output location diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/GhidraProjectUtils.java b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/GhidraProjectUtils.java index 059140616f..8899e3f6a7 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/GhidraProjectUtils.java +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/GhidraProjectUtils.java @@ -36,9 +36,10 @@ import org.eclipse.ui.part.FileEditorInput; import generic.jar.ResourceFile; import ghidra.GhidraApplicationLayout; import ghidra.framework.GModule; -import ghidra.launch.JavaConfig; +import ghidra.launch.AppConfig; import ghidradev.Activator; import ghidradev.EclipseMessageUtils; +import ghidradev.ghidraprojectcreator.utils.PyDevUtils.ProjectPythonInterpreter; import utility.module.ModuleUtilities; /** @@ -262,8 +263,7 @@ public class GhidraProjectUtils { * @param createRunConfig Whether or not to create a new run configuration for the project. * @param runConfigMemory The run configuration's desired memory. Could be null. * @param ghidraLayout The Ghidra layout to link the project to. - * @param jythonInterpreterName The name of the Jython interpreter to use for Python support. - * Could be null if Python support is not wanted. + * @param pythonInterpreter The Python interpreter to use. * @param monitor The progress monitor to use during project creation. * @return The created project. * @throws IOException If there was a file-related problem with creating the project. @@ -272,12 +272,12 @@ public class GhidraProjectUtils { */ public static IJavaProject createEmptyGhidraProject(String projectName, File projectDir, boolean createRunConfig, String runConfigMemory, GhidraApplicationLayout ghidraLayout, - String jythonInterpreterName, IProgressMonitor monitor) + ProjectPythonInterpreter pythonInterpreter, IProgressMonitor monitor) throws IOException, ParseException, CoreException { // Get Ghidra's Java configuration - JavaConfig javaConfig = - new JavaConfig(ghidraLayout.getApplicationInstallationDir().getFile(false)); + AppConfig appConfig = + new AppConfig(ghidraLayout.getApplicationInstallationDir().getFile(false)); // Make new Java project IWorkspace workspace = ResourcesPlugin.getWorkspace(); @@ -299,7 +299,7 @@ public class GhidraProjectUtils { javaProject.setRawClasspath(new IClasspathEntry[0], monitor); // Configure Java compiler for the project - configureJavaCompiler(javaProject, javaConfig); + configureJavaCompiler(javaProject, appConfig); // Setup default bin folder IFolder binFolder = project.getFolder("bin/default"); @@ -310,7 +310,7 @@ public class GhidraProjectUtils { monitor); // Link in Ghidra to the project - linkGhidraToProject(javaProject, ghidraLayout, javaConfig, jythonInterpreterName, monitor); + linkGhidraToProject(javaProject, ghidraLayout, appConfig, pythonInterpreter, monitor); // Create run configuration (if necessary) if (createRunConfig) { @@ -338,23 +338,22 @@ public class GhidraProjectUtils { * * @param javaProject The Java project to link. * @param ghidraLayout The Ghidra layout to link the project to. - * @param javaConfig Ghidra's Java configuration. - * @param jythonInterpreterName The name of the Jython interpreter to use for Python support. - * Could be null if Python support is not wanted. + * @param appConfig Ghidra's application configuration. + * @param pythonInterpreter The Python interpreter to use. * @param monitor The progress monitor used during link. * @throws IOException If there was a file-related problem with linking in Ghidra. * @throws CoreException If there was an Eclipse-related problem with linking in Ghidra. */ public static void linkGhidraToProject(IJavaProject javaProject, - GhidraApplicationLayout ghidraLayout, JavaConfig javaConfig, - String jythonInterpreterName, IProgressMonitor monitor) + GhidraApplicationLayout ghidraLayout, AppConfig appConfig, + ProjectPythonInterpreter pythonInterpreter, IProgressMonitor monitor) throws CoreException, IOException { // Gets the Ghidra installation directory to link to from the Ghidra layout File ghidraInstallDir = ghidraLayout.getApplicationInstallationDir().getFile(false); // Get the Java VM used to launch the Ghidra to link to - IVMInstall vm = getGhidraVm(javaConfig); + IVMInstall vm = getGhidraVm(appConfig); IPath vmPath = new Path(JavaRuntime.JRE_CONTAINER).append(vm.getVMInstallType().getId()).append( vm.getName()); @@ -457,15 +456,13 @@ public class GhidraProjectUtils { GhidraModuleUtils.writeAntProperties(javaProject.getProject(), ghidraLayout); // Setup Python for the project - if (PyDevUtils.isSupportedPyDevInstalled()) { - try { - PyDevUtils.setupPythonForProject(javaProject, libraryClasspathEntries, - jythonInterpreterName, monitor); - } - catch (OperationNotSupportedException e) { - EclipseMessageUtils.showErrorDialog("PyDev error", - "Failed to setup Python for the project. PyDev version is not supported."); - } + try { + PyDevUtils.setupPythonForProject(javaProject, libraryClasspathEntries, + pythonInterpreter, monitor); + } + catch (OperationNotSupportedException e) { + EclipseMessageUtils.showErrorDialog("PyDev error", + "Failed to setup Python for the project. PyDev version is not supported."); } } @@ -559,14 +556,14 @@ public class GhidraProjectUtils { /** * Gets the required VM used to build and run the Ghidra defined by the given layout. * - * @param javaConfig Ghidra's Java configuration. + * @param appConfig Ghidra's application configuration. * @return The required VM used to build and run the Ghidra defined by the given layout. * @throws IOException If there was a file-related problem with getting the VM. * @throws CoreException If there was an Eclipse-related problem with creating the project. */ - private static IVMInstall getGhidraVm(JavaConfig javaConfig) throws IOException, CoreException { + private static IVMInstall getGhidraVm(AppConfig appConfig) throws IOException, CoreException { - File requiredJavaHomeDir = javaConfig.getSavedJavaHome(); // safe to assume it's valid + File requiredJavaHomeDir = appConfig.getSavedJavaHome(); // safe to assume it's valid // First look for a matching VM in Eclipse's existing list. // NOTE: Mac has its own VM type, so be sure to check it for VM matches too. @@ -617,19 +614,19 @@ public class GhidraProjectUtils { * Configures the default Java compiler behavior for the given java project. * * @param jp The Java project to configure. - * @param javaConfig Ghidra's Java configuration. + * @param appConfig Ghidra's application configuration. */ - private static void configureJavaCompiler(IJavaProject jp, JavaConfig javaConfig) { + private static void configureJavaCompiler(IJavaProject jp, AppConfig appConfig) { final String WARNING = JavaCore.WARNING; final String IGNORE = JavaCore.IGNORE; final String ERROR = JavaCore.ERROR; // Compliance - jp.setOption(JavaCore.COMPILER_SOURCE, javaConfig.getCompilerComplianceLevel()); - jp.setOption(JavaCore.COMPILER_COMPLIANCE, javaConfig.getCompilerComplianceLevel()); + jp.setOption(JavaCore.COMPILER_SOURCE, appConfig.getCompilerComplianceLevel()); + jp.setOption(JavaCore.COMPILER_COMPLIANCE, appConfig.getCompilerComplianceLevel()); jp.setOption(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, - javaConfig.getCompilerComplianceLevel()); + appConfig.getCompilerComplianceLevel()); // Code style jp.setOption(JavaCore.COMPILER_PB_STATIC_ACCESS_RECEIVER, WARNING); diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/GhidraScriptUtils.java b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/GhidraScriptUtils.java index ed083d01d2..6189d24c54 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/GhidraScriptUtils.java +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/GhidraScriptUtils.java @@ -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. @@ -26,6 +26,7 @@ import org.eclipse.jdt.core.*; import ghidra.GhidraApplicationLayout; import ghidra.framework.GModule; import ghidradev.Activator; +import ghidradev.ghidraprojectcreator.utils.PyDevUtils.ProjectPythonInterpreter; /** * Utility methods for working with Ghidra scripts in Eclipse. @@ -45,8 +46,7 @@ public class GhidraScriptUtils { * @param linkUserScripts Whether or not to link in the user scripts directory. * @param linkSystemScripts Whether or not to link in the system scripts directories. * @param ghidraLayout The Ghidra layout to link the project to. - * @param jythonInterpreterName The name of the Jython interpreter to use for Python support. - * Could be null if Python support is not wanted. + * @param pythonInterpreter The Python interpreter to use. * @param monitor The progress monitor to use during project creation. * @return The created project. * @throws IOException If there was a file-related problem with creating the project. @@ -56,15 +56,14 @@ public class GhidraScriptUtils { public static IJavaProject createGhidraScriptProject(String projectName, File projectDir, boolean createRunConfig, String runConfigMemory, boolean linkUserScripts, boolean linkSystemScripts, GhidraApplicationLayout ghidraLayout, - String jythonInterpreterName, IProgressMonitor monitor) + ProjectPythonInterpreter pythonInterpreter, IProgressMonitor monitor) throws IOException, ParseException, CoreException { List classpathEntries = new ArrayList<>(); // Create empty Ghidra project IJavaProject javaProject = GhidraProjectUtils.createEmptyGhidraProject(projectName, - projectDir, createRunConfig, runConfigMemory, ghidraLayout, jythonInterpreterName, - monitor); + projectDir, createRunConfig, runConfigMemory, ghidraLayout, pythonInterpreter, monitor); // Link each module's ghidra_scripts directory to the project if (linkSystemScripts) { diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/PyDevUtils.java b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/PyDevUtils.java index b452f5311a..59d4e81209 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/PyDevUtils.java +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/PyDevUtils.java @@ -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. @@ -26,6 +26,7 @@ import java.util.stream.Stream; import javax.naming.OperationNotSupportedException; +import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.*; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaProject; @@ -38,23 +39,58 @@ import ghidradev.Activator; */ public class PyDevUtils { - public final static String MIN_SUPPORTED_VERSION = "6.3.1"; - public final static String MAX_SUPPORTED_VERSION = "9.3.0"; + public final static String MIN_SUPPORTED_VERSION = "9.3.0"; + public final static String MAX_JYTHON_SUPPORTED_VERSION = "9.3.0"; /** - * Checks to see if a supported version of PyDev is installed. - * - * @return True if a supported version of PyDev is installed; otherwise, false. + * The various types of supported Python interpreters */ - public static boolean isSupportedPyDevInstalled() { + public static enum ProjectPythonInterpreterType { + NONE, + PYGHIDRA, + JYTHON + } + + /** + * The projects Python interpreter to use + * + * @param name The name of the interpreter + * @param type The {@link ProjectPythonInterpreterType type} of the interpreter + */ + public static record ProjectPythonInterpreter(String name, ProjectPythonInterpreterType type) {} + + /** + * {@return true if a supported version of PyDev is installed for use with PyGhidra; otherwise, + * false} + */ + public static boolean isSupportedPyGhidraPyDevInstalled() { Version min = Version.valueOf(MIN_SUPPORTED_VERSION); - Version max = Version.valueOf(MAX_SUPPORTED_VERSION); + try { + Version version = PyDevUtilsInternal.getPyDevVersion(); + if (version != null) { + return version.compareTo(min) >= 0; + } + } + catch (NoClassDefFoundError e) { + // Fall through to return false + } + + return false; + } + + /** + * {@return true if a supported version of PyDev is installed for use with Jython; otherwise, + * false} + */ + public static boolean isSupportedJythonPyDevInstalled() { + Version min = Version.valueOf(MIN_SUPPORTED_VERSION); + Version max = Version.valueOf(MAX_JYTHON_SUPPORTED_VERSION); try { Version version = PyDevUtilsInternal.getPyDevVersion(); if (version != null) { // Make sure the installed version of PyDev is new enough to support the following // operation. - getJython27InterpreterNames(); + getJythonInterpreterNames(); return version.compareTo(min) >= 0 && version.compareTo(max) <= 0; } } @@ -66,15 +102,53 @@ public class PyDevUtils { } /** - * Gets a list of discovered Jython 2.7 interpreter names. - * - * @return a list of discovered Jython 2.7 interpreter names. + * Gets a list of discovered PyGhidra interpreter names. + * @param requiredFileMatch if not {@code null}, only interpreter names that correspond to the + * given interpreter file will be returned. + * @return a list of discovered PyGhidra interpreter names. * @throws OperationNotSupportedException if PyDev is not installed or it does not support this * operation. */ - public static List getJython27InterpreterNames() throws OperationNotSupportedException { + public static List getPyGhidraInterpreterNames(File requiredFileMatch) + throws OperationNotSupportedException { try { - return PyDevUtilsInternal.getJython27InterpreterNames(); + return PyDevUtilsInternal.getPyGhidraInterpreterNames(requiredFileMatch); + } + catch (NoClassDefFoundError | NoSuchMethodError e) { + throw new OperationNotSupportedException(e.getMessage()); + } + } + + /** + * Gets a list of discovered Jython interpreter names. + * + * @return a list of discovered Jython interpreter names. + * @throws OperationNotSupportedException if PyDev is not installed or it does not support this + * operation. + */ + public static List getJythonInterpreterNames() throws OperationNotSupportedException { + try { + return PyDevUtilsInternal.getJythonInterpreterNames(); + } + catch (NoClassDefFoundError | NoSuchMethodError e) { + throw new OperationNotSupportedException(e.getMessage()); + } + } + + /** + * Adds the given PyGhidra interpreter to PyDev. + * + * @param interpreterName The name of the interpreter to add. + * @param interpreterFile The interpreter file to add. + * @param pypredefDir The pypredef directory to use (could be null if not supported) + * @throws OperationNotSupportedException if PyDev is not installed or it does not support this + * operation. + */ + public static void addPyGhidraInterpreter(String interpreterName, File interpreterFile, + File pypredefDir) throws OperationNotSupportedException { + try { + PyDevUtilsInternal.addPyGhidraInterpreter(interpreterName, interpreterFile, + pypredefDir); } catch (NoClassDefFoundError | NoSuchMethodError e) { throw new OperationNotSupportedException(e.getMessage()); @@ -107,8 +181,7 @@ public class PyDevUtils { * * @param javaProject The Java project to enable Python for. * @param classpathEntries The classpath entries to add to the Python path. - * @param jythonInterpreterName The name of the Jython interpreter to use for Python support. - * If this is null, Python support will be removed from the project. + * @param pythonInterpreter The Python interpreter to use. * @param monitor The progress monitor used during link. * @throws CoreException if there was an Eclipse-related problem with enabling Python for the * project. @@ -116,11 +189,11 @@ public class PyDevUtils { * operation. */ public static void setupPythonForProject(IJavaProject javaProject, - List classpathEntries, String jythonInterpreterName, + List classpathEntries, ProjectPythonInterpreter pythonInterpreter, IProgressMonitor monitor) throws CoreException, OperationNotSupportedException { try { PyDevUtilsInternal.setupPythonForProject(javaProject, classpathEntries, - jythonInterpreterName, monitor); + pythonInterpreter, monitor); } catch (NoClassDefFoundError | NoSuchMethodError e) { throw new OperationNotSupportedException(e.getMessage()); @@ -151,6 +224,15 @@ public class PyDevUtils { return "org.python.pydev.ui.pythonpathconf.interpreterPreferencesPageJython"; } + /** + * Gets the PyDev Python preference page ID. + * + * @return the PyDev Python preference page ID. + */ + public static String getPythonPreferencePageId() { + return "org.python.pydev.ui.pythonpathconf.interpreterPreferencesPagePython"; + } + /** * Gets The PyDev source directory. * @@ -184,4 +266,138 @@ public class PyDevUtils { return null; } + + /** + * Checks to see if the given project is a Python project. + * + * @param project The project to check. + * @return True if the given project is a Python project; otherwise, false. + * @throws OperationNotSupportedException if PyDev is not installed or it does not support this + * operation. + */ + public static boolean isPythonProject(IProject project) throws OperationNotSupportedException { + try { + return PyDevUtilsInternal.isPythonProject(project); + } + catch (NoClassDefFoundError | NoSuchMethodError e) { + throw new OperationNotSupportedException(e.getMessage()); + } + } + + /** + * Checks to see if the given project is a PyGhidra project. + * + * @param project The project to check. + * @return True if the given project is a PyGhidra project; otherwise, false. + * @throws OperationNotSupportedException if PyDev is not installed or it does not support this + * operation. + */ + public static boolean isPyGhidraProject(IProject project) + throws OperationNotSupportedException { + try { + return PyDevUtilsInternal.isPyGhidraProject(project); + } + catch (NoClassDefFoundError | NoSuchMethodError e) { + throw new OperationNotSupportedException(e.getMessage()); + } + } + + /** + * Gets the interpreter name of the given Python project. + * + * @param project The project to get the interpreter name from. + * @return The interpreter name of the given Python project, or null it it's not a Python + * project or doesn't have an interpreter. + * @throws OperationNotSupportedException if PyDev is not installed or it does not support this + * operation. + */ + public static String getInterpreterName(IProject project) + throws OperationNotSupportedException { + try { + return PyDevUtilsInternal.getInterpreterName(project); + } + catch (NoClassDefFoundError | NoSuchMethodError e) { + throw new OperationNotSupportedException(e.getMessage()); + } + } + + /** + * Gets the PyDev "project" attribute. + * + * @return The PyDev "project" attribute. + * @throws OperationNotSupportedException if PyDev is not installed or it does not support this + * operation. + */ + public static String getAttrProject() throws OperationNotSupportedException { + try { + return PyDevUtilsInternal.getAttrProject(); + } + catch (NoClassDefFoundError e) { + throw new OperationNotSupportedException(e.getMessage()); + } + } + + /** + * Gets the PyDev "location" attribute. + * + * @return The PyDev "location" attribute. + * @throws OperationNotSupportedException if PyDev is not installed or it does not support this + * operation. + */ + public static String getAttrLocation() throws OperationNotSupportedException { + try { + return PyDevUtilsInternal.getAttrLocation(); + } + catch (NoClassDefFoundError e) { + throw new OperationNotSupportedException(e.getMessage()); + } + } + + /** + * Gets the PyDev "program arguments" attribute. + * + * @return The PyDev "program arguments" attribute. + * @throws OperationNotSupportedException if PyDev is not installed or it does not support this + * operation. + */ + public static String getAttrProgramArguments() throws OperationNotSupportedException { + try { + return PyDevUtilsInternal.getAttrProgramArguments(); + } + catch (NoClassDefFoundError e) { + throw new OperationNotSupportedException(e.getMessage()); + } + } + + /** + * Gets the PyDev "interpreter" attribute. + * + * @return The PyDev "interpreter" attribute. + * @throws OperationNotSupportedException if PyDev is not installed or it does not support this + * operation. + */ + public static String getAttrInterpreter() throws OperationNotSupportedException { + try { + return PyDevUtilsInternal.getAttrInterpreter(); + } + catch (NoClassDefFoundError e) { + throw new OperationNotSupportedException(e.getMessage()); + } + } + + /** + * Gets the PyDev "interpreter default" attribute. + * + * @return The PyDev "interpreter default" attribute. + * @throws OperationNotSupportedException if PyDev is not installed or it does not support this + * operation. + */ + public static String getAttrInterpreterDefault() throws OperationNotSupportedException { + try { + return PyDevUtilsInternal.getAttrInterpreterDefault(); + } + catch (NoClassDefFoundError e) { + throw new OperationNotSupportedException(e.getMessage()); + } + } } diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/PyDevUtilsInternal.java b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/PyDevUtilsInternal.java index 17a9f6f375..2c061f878e 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/PyDevUtilsInternal.java +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/utils/PyDevUtilsInternal.java @@ -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. @@ -19,6 +19,7 @@ import java.io.File; import java.util.*; import java.util.stream.Collectors; +import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.*; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaProject; @@ -26,11 +27,13 @@ import org.osgi.framework.*; import org.python.pydev.ast.interpreter_managers.InterpreterInfo; import org.python.pydev.ast.interpreter_managers.InterpreterManagersAPI; import org.python.pydev.core.*; +import org.python.pydev.debug.core.Constants; import org.python.pydev.plugin.nature.PythonNature; import com.python.pydev.debug.remote.client_api.PydevRemoteDebuggerServer; import ghidradev.EclipseMessageUtils; +import ghidradev.ghidraprojectcreator.utils.PyDevUtils.ProjectPythonInterpreter; /** * Utility methods for interacting with PyDev. @@ -62,20 +65,50 @@ class PyDevUtilsInternal { } /** - * Gets a list of discovered Jython 2.7 interpreter names. + * Gets a list of discovered PyGhidra interpreter names. * - * @return a list of discovered Jython 2.7 interpreter names. + * @param requiredFileMatch if not {@code null}, only interpreter names that correspond to the + * given interpreter file will be returned. + * @return a list of discovered PyGhidra interpreter names. * @throws NoClassDefFoundError if PyDev is not installed or it does not support this operation. * @throws NoSuchMethodError if PyDev is not installed or it does not support this operation. */ - public static List getJython27InterpreterNames() + public static List getPyGhidraInterpreterNames(File requiredFileMatch) + throws NoClassDefFoundError, NoSuchMethodError { + + List interpreters = new ArrayList<>(); + IInterpreterManager iMan = InterpreterManagersAPI.getPythonInterpreterManager(true); + + for (IInterpreterInfo info : iMan.getInterpreterInfos()) { + ISystemModulesManager modulesManager = info.getModulesManager(); + if (info.getInterpreterType() == IPythonNature.INTERPRETER_TYPE_PYTHON && + !modulesManager.getAllModulesStartingWith("pyghidra.__main__").isEmpty()) { + if (requiredFileMatch == null || + requiredFileMatch.getAbsolutePath().equals(info.getExecutableOrJar())) { + interpreters.add(info.getName()); + } + } + } + + return interpreters; + } + + /** + * Gets a list of discovered Jython interpreter names. + * + * @return a list of discovered Jython interpreter names. + * @throws NoClassDefFoundError if PyDev is not installed or it does not support this operation. + * @throws NoSuchMethodError if PyDev is not installed or it does not support this operation. + */ + public static List getJythonInterpreterNames() throws NoClassDefFoundError, NoSuchMethodError { List interpreters = new ArrayList<>(); IInterpreterManager iMan = InterpreterManagersAPI.getJythonInterpreterManager(true); for (IInterpreterInfo info : iMan.getInterpreterInfos()) { - if (info.getInterpreterType() == IPythonNature.INTERPRETER_TYPE_JYTHON && info.getVersion().equals("2.7")) { + if (info.getInterpreterType() == IPythonNature.INTERPRETER_TYPE_JYTHON && + info.getVersion().equals("2.7")) { interpreters.add(info.getName()); } } @@ -83,6 +116,38 @@ class PyDevUtilsInternal { return interpreters; } + /** + * Adds the given PyGhidra interpreter to PyDev. + * + * @param interpreterName The name of the interpreter to add. + * @param interpreterFile The interpreter to add. + * @param pypredefDir The pypredef directory to use (could be null if not supported) + * @throws NoClassDefFoundError if PyDev is not installed or it does not support this operation. + * @throws NoSuchMethodError if PyDev is not installed or it does not support this operation. + */ + public static void addPyGhidraInterpreter(String interpreterName, File interpreterFile, + File pypredefDir) throws NoClassDefFoundError, NoSuchMethodError { + IProgressMonitor monitor = new NullProgressMonitor(); + IInterpreterManager iMan = InterpreterManagersAPI.getPythonInterpreterManager(true); + IInterpreterInfo[] interpreterInfos = iMan.getInterpreterInfos(); + for (IInterpreterInfo iInfo : interpreterInfos) { + if (iInfo.getName().equals(interpreterName) && + iInfo.getExecutableOrJar().equals(interpreterFile.getAbsolutePath())) { + return; + } + } + IInterpreterInfo iInfo = + iMan.createInterpreterInfo(interpreterFile.getAbsolutePath(), monitor, false); + iInfo.setName(interpreterName); + if (iInfo instanceof InterpreterInfo ii && pypredefDir != null) { + ii.addPredefinedCompletionsPath(pypredefDir.getAbsolutePath()); + } + IInterpreterInfo[] newInterpreterInfos = + Arrays.copyOf(interpreterInfos, interpreterInfos.length + 1); + newInterpreterInfos[interpreterInfos.length] = iInfo; + iMan.setInfos(newInterpreterInfos, null, monitor); + } + /** * Adds the given Jython interpreter to PyDev. * @@ -119,26 +184,38 @@ class PyDevUtilsInternal { * * @param javaProject The Java project to setup Python for. * @param classpathEntries The classpath entries to add to the Python path. - * @param jythonInterpreterName The name of the Jython interpreter to use for Python support. - * If this is null, Python support will be removed from the project. + * @param pythonInterpreter The Python interpreter to use. * @param monitor The progress monitor used during link. * @throws CoreException If there was an Eclipse-related problem with enabling Python for the project. * @throws NoClassDefFoundError if PyDev is not installed or it does not support this operation. * @throws NoSuchMethodError if PyDev is not installed or it does not support this operation. */ public static void setupPythonForProject(IJavaProject javaProject, - List classpathEntries, String jythonInterpreterName, + List classpathEntries, ProjectPythonInterpreter pythonInterpreter, IProgressMonitor monitor) throws CoreException, NoClassDefFoundError, NoSuchMethodError { PythonNature.removeNature(javaProject.getProject(), monitor); - if (jythonInterpreterName != null) { - String libs = classpathEntries.stream().map(e -> e.getPath().toOSString()).collect( - Collectors.joining("|")); - PythonNature.addNature(javaProject.getProject(), monitor, - IPythonNature.JYTHON_VERSION_2_7, null, libs, jythonInterpreterName, null); + String version; + String libs; + switch (pythonInterpreter.type()) { + case PYGHIDRA: + version = IPythonNature.PYTHON_VERSION_INTERPRETER; + libs = null; + break; + case JYTHON: + version = IPythonNature.JYTHON_VERSION_INTERPRETER; + libs = classpathEntries.stream() + .map(e -> e.getPath().toOSString()) + .collect(Collectors.joining("|")); + break; + default: + return; } + + PythonNature.addNature(javaProject.getProject(), monitor, version, null, libs, + pythonInterpreter.name(), null); } /** @@ -151,6 +228,109 @@ class PyDevUtilsInternal { PydevRemoteDebuggerServer.startServer(); } + /** + * Checks to see if the given project is a Python project. + * + * @param project The project to check. + * @return True if the given project is a Python project; otherwise, false. + * @throws NoClassDefFoundError if PyDev is not installed or it does not support this operation. + */ + public static boolean isPythonProject(IProject project) throws NoClassDefFoundError { + try { + return project.hasNature(PythonNature.PYTHON_NATURE_ID); + } + catch (CoreException e) { + return false; + } + } + + /** + * Checks to see if the given project is a PyGhidra project. + * + * @param project The project to check. + * @return True if the given project is a PyGhidra project; otherwise, false. + * @throws NoClassDefFoundError if PyDev is not installed or it does not support this operation. + */ + public static boolean isPyGhidraProject(IProject project) throws NoClassDefFoundError { + return isPythonProject(project) && GhidraProjectUtils.isGhidraProject(project); + } + + /** + * Gets the interpreter name of the given Python project. + * + * @param project The project to get the interpreter name from. + * @return The interpreter name of the given Python project, or null it it's not a Python + * project or doesn't have an interpreter. + * @throws NoClassDefFoundError if PyDev is not installed or it does not support this operation. + * @throws NoSuchMethodError if PyDev is not installed or it does not support this operation. + */ + public static String getInterpreterName(IProject project) + throws NoClassDefFoundError, NoSuchMethodError { + PythonNature nature = PythonNature.getPythonNature(project); + if (nature != null) { + try { + IInterpreterInfo info = nature.getProjectInterpreter(); + if (info != null) { + return info.getName(); + } + } + catch (PythonNatureWithoutProjectException | MisconfigurationException e) { + // Fall through + } + } + return null; + } + + /** + * Gets the PyDev "project" attribute. + * + * @return The PyDev "project" attribute. + * @throws NoClassDefFoundError if PyDev is not installed or it does not support this operation. + */ + public static String getAttrProject() throws NoClassDefFoundError { + return Constants.ATTR_PROJECT; + } + + /** + * Gets the PyDev "location" attribute. + * + * @return The PyDev "location" attribute. + * @throws NoClassDefFoundError if PyDev is not installed or it does not support this operation. + */ + public static String getAttrLocation() throws NoClassDefFoundError { + return Constants.ATTR_LOCATION; + } + + /** + * Gets the PyDev "program arguments" attribute. + * + * @return The PyDev "program arguments" attribute. + * @throws NoClassDefFoundError if PyDev is not installed or it does not support this operation. + */ + public static String getAttrProgramArguments() throws NoClassDefFoundError { + return Constants.ATTR_PROGRAM_ARGUMENTS; + } + + /** + * Gets the PyDev "interpreter" attribute. + * + * @return The PyDev "interpreter" attribute. + * @throws NoClassDefFoundError if PyDev is not installed or it does not support this operation. + */ + public static String getAttrInterpreter() throws NoClassDefFoundError { + return Constants.ATTR_INTERPRETER; + } + + /** + * Gets the PyDev "interpreter default" attribute. + * + * @return The PyDev "interpreter default" attribute. + * @throws NoClassDefFoundError if PyDev is not installed or it does not support this operation. + */ + public static String getAttrInterpreterDefault() throws NoClassDefFoundError { + return Constants.ATTR_INTERPRETER_DEFAULT; + } + private PyDevUtilsInternal() throws NoClassDefFoundError { // Prevent instantiation } diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/CreateGhidraModuleProjectWizard.java b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/CreateGhidraModuleProjectWizard.java index d1df4dde44..0fe96922f9 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/CreateGhidraModuleProjectWizard.java +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/CreateGhidraModuleProjectWizard.java @@ -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. @@ -36,6 +36,7 @@ import ghidra.GhidraApplicationLayout; import ghidradev.EclipseMessageUtils; import ghidradev.ghidraprojectcreator.utils.GhidraModuleUtils; import ghidradev.ghidraprojectcreator.utils.GhidraModuleUtils.ModuleTemplateType; +import ghidradev.ghidraprojectcreator.utils.PyDevUtils.ProjectPythonInterpreter; import ghidradev.ghidraprojectcreator.wizards.pages.*; import utilities.util.FileUtilities; @@ -87,12 +88,12 @@ public class CreateGhidraModuleProjectWizard extends Wizard implements INewWizar boolean createRunConfig = projectPage.shouldCreateRunConfig(); String runConfigMemory = projectPage.getRunConfigMemory(); File projectDir = projectPage.getProjectDir(); - String jythonInterpreterName = pythonPage.getJythonInterpreterName(); + ProjectPythonInterpreter pythonInterpreter = pythonPage.getProjectPythonInterpreter(); Set moduleTemplateTypes = projectConfigPage.getModuleTemplateTypes(); try { getContainer().run(true, false, monitor -> create(ghidraInstallDir, projectName, projectDir, createRunConfig, - runConfigMemory, moduleTemplateTypes, jythonInterpreterName, monitor)); + runConfigMemory, moduleTemplateTypes, pythonInterpreter, monitor)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -115,14 +116,13 @@ public class CreateGhidraModuleProjectWizard extends Wizard implements INewWizar * @param createRunConfig Whether or not to create a new run configuration for the project. * @param runConfigMemory The run configuration's desired memory. Could be null. * @param moduleTemplateTypes The desired module template types. - * @param jythonInterpreterName The name of the Jython interpreter to use for Python support. - * Could be null if Python support is not wanted. + * @param pythonInterpreter The Python interpreter to use. * @param monitor The monitor to use during project creation. * @throws InvocationTargetException if an error occurred during project creation. */ private void create(File ghidraInstallDir, String projectName, File projectDir, boolean createRunConfig, String runConfigMemory, - Set moduleTemplateTypes, String jythonInterpreterName, + Set moduleTemplateTypes, ProjectPythonInterpreter pythonInterpreter, IProgressMonitor monitor) throws InvocationTargetException { try { info("Creating " + projectName + " at " + projectDir); @@ -133,7 +133,7 @@ public class CreateGhidraModuleProjectWizard extends Wizard implements INewWizar IJavaProject javaProject = GhidraModuleUtils.createGhidraModuleProject(projectName, projectDir, - createRunConfig, runConfigMemory, ghidraLayout, jythonInterpreterName, monitor); + createRunConfig, runConfigMemory, ghidraLayout, pythonInterpreter, monitor); monitor.worked(1); IFile sourceFile = GhidraModuleUtils.configureModuleSource(javaProject, diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/CreateGhidraScriptProjectWizard.java b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/CreateGhidraScriptProjectWizard.java index b78887d1b3..0b28ffa1d5 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/CreateGhidraScriptProjectWizard.java +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/CreateGhidraScriptProjectWizard.java @@ -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. @@ -32,6 +32,7 @@ import org.eclipse.ui.IWorkbench; import ghidra.GhidraApplicationLayout; import ghidradev.EclipseMessageUtils; import ghidradev.ghidraprojectcreator.utils.GhidraScriptUtils; +import ghidradev.ghidraprojectcreator.utils.PyDevUtils.ProjectPythonInterpreter; import ghidradev.ghidraprojectcreator.wizards.pages.*; import utilities.util.FileUtilities; @@ -81,11 +82,11 @@ public class CreateGhidraScriptProjectWizard extends Wizard implements INewWizar String runConfigMemory = projectPage.getRunConfigMemory(); boolean linkUserScripts = projectConfigPage.shouldLinkUsersScripts(); boolean linkSystemScripts = projectConfigPage.shouldLinkSystemScripts(); - String jythonInterpreterName = pythonPage.getJythonInterpreterName(); + ProjectPythonInterpreter pythonInterpreter = pythonPage.getProjectPythonInterpreter(); try { getContainer().run(true, false, monitor -> create(ghidraInstallDir, projectName, projectDir, createRunConfig, - runConfigMemory, linkUserScripts, linkSystemScripts, jythonInterpreterName, + runConfigMemory, linkUserScripts, linkSystemScripts, pythonInterpreter, monitor)); } catch (InterruptedException e) { @@ -110,15 +111,14 @@ public class CreateGhidraScriptProjectWizard extends Wizard implements INewWizar * @param runConfigMemory The run configuration's desired memory. Could be null. * @param linkUserScripts Whether or not to link in the user scripts directory. * @param linkSystemScripts Whether or not to link in the system scripts directories. - * @param jythonInterpreterName The name of the Jython interpreter to use for Python support. - * Could be null if Python support is not wanted. + * @param pythonInterpreter The Python interpreter to use. * @param monitor The monitor to use during project creation. * @throws InvocationTargetException if an error occurred during project creation. */ private void create(File ghidraInstallDir, String projectName, File projectDir, boolean createRunConfig, String runConfigMemory, boolean linkUserScripts, - boolean linkSystemScripts, String jythonInterpreterName, IProgressMonitor monitor) - throws InvocationTargetException { + boolean linkSystemScripts, ProjectPythonInterpreter pythonInterpreter, + IProgressMonitor monitor) throws InvocationTargetException { try { info("Creating " + projectName + " at " + projectDir); monitor.beginTask("Creating " + projectName, 2); @@ -128,7 +128,7 @@ public class CreateGhidraScriptProjectWizard extends Wizard implements INewWizar GhidraScriptUtils.createGhidraScriptProject(projectName, projectDir, createRunConfig, runConfigMemory, linkUserScripts, linkSystemScripts, ghidraLayout, - jythonInterpreterName, monitor); + pythonInterpreter, monitor); monitor.worked(1); info("Finished creating " + projectName); diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/ExportGhidraModuleWizard.java b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/ExportGhidraModuleWizard.java index cda82df764..1a54032ea0 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/ExportGhidraModuleWizard.java +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/ExportGhidraModuleWizard.java @@ -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. @@ -40,7 +40,7 @@ import org.eclipse.ui.INewWizard; import org.eclipse.ui.IWorkbench; import ghidra.GhidraApplicationLayout; -import ghidra.launch.JavaConfig; +import ghidra.launch.AppConfig; import ghidradev.EclipseMessageUtils; import ghidradev.ghidraprojectcreator.utils.GhidraProjectUtils; import ghidradev.ghidraprojectcreator.wizards.pages.ChooseGhidraModuleProjectWizardPage; @@ -123,7 +123,7 @@ public class ExportGhidraModuleWizard extends Wizard implements INewWizard { // TODO: It's more correct to get this from the project's classpath, since Ghidra's // saved Java home can change from launch to launch. GhidraApplicationLayout ghidraLayout = new GhidraApplicationLayout(new File(ghidraInstallDirPath)); - File javaHomeDir = new JavaConfig( + File javaHomeDir = new AppConfig( ghidraLayout.getApplicationInstallationDir().getFile(false)).getSavedJavaHome(); if(javaHomeDir == null) { throw new IOException("Failed to get the Java home associated with the project. " + diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/ImportGhidraModuleSourceWizard.java b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/ImportGhidraModuleSourceWizard.java index 1655e08047..6548fb9861 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/ImportGhidraModuleSourceWizard.java +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/ImportGhidraModuleSourceWizard.java @@ -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. @@ -32,6 +32,7 @@ import org.eclipse.ui.IWorkbench; import ghidra.GhidraApplicationLayout; import ghidradev.EclipseMessageUtils; import ghidradev.ghidraprojectcreator.utils.GhidraModuleUtils; +import ghidradev.ghidraprojectcreator.utils.PyDevUtils.ProjectPythonInterpreter; import ghidradev.ghidraprojectcreator.wizards.pages.*; import utilities.util.FileUtilities; @@ -76,11 +77,11 @@ public class ImportGhidraModuleSourceWizard extends Wizard implements IImportWiz String projectName = projectPage.getProjectName(); boolean createRunConfig = projectPage.shouldCreateRunConfig(); String runConfigMemory = projectPage.getRunConfigMemory(); - String jythonInterpreterName = pythonPage.getJythonInterpreterName(); + ProjectPythonInterpreter pythonInterpreter = pythonPage.getProjectPythonInterpreter(); try { getContainer().run(true, false, monitor -> importModuleSource(ghidraInstallDir, projectName, moduleSourceDir, - createRunConfig, runConfigMemory, jythonInterpreterName, monitor)); + createRunConfig, runConfigMemory, pythonInterpreter, monitor)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -102,14 +103,14 @@ public class ImportGhidraModuleSourceWizard extends Wizard implements IImportWiz * @param moduleSourceDir The module source directory to import. * @param createRunConfig Whether or not to create a new run configuration for the project. * @param runConfigMemory The run configuration's desired memory. Could be null. - * @param jythonInterpreterName The name of the Jython interpreter to use for Python support. - * Could be null if Python support is not wanted. + * @param pythonInterpreter The Python interpreter to use. * @param monitor The monitor to use during project creation. * @throws InvocationTargetException if an error occurred during project creation. */ private void importModuleSource(File ghidraInstallDir, String projectName, File moduleSourceDir, - boolean createRunConfig, String runConfigMemory, String jythonInterpreterName, - IProgressMonitor monitor) throws InvocationTargetException { + boolean createRunConfig, String runConfigMemory, + ProjectPythonInterpreter pythonInterpreter, IProgressMonitor monitor) + throws InvocationTargetException { try { info("Importing " + projectName + " at " + moduleSourceDir); monitor.beginTask("Importing " + projectName, 2); @@ -118,7 +119,7 @@ public class ImportGhidraModuleSourceWizard extends Wizard implements IImportWiz monitor.worked(1); GhidraModuleUtils.importGhidraModuleSource(projectName, moduleSourceDir, - createRunConfig, runConfigMemory, ghidraLayout, jythonInterpreterName, monitor); + createRunConfig, runConfigMemory, ghidraLayout, pythonInterpreter, monitor); monitor.worked(1); info("Finished importing " + projectName); diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/LinkGhidraWizard.java b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/LinkGhidraWizard.java index c20ff5df65..06353fb030 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/LinkGhidraWizard.java +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/LinkGhidraWizard.java @@ -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,8 +31,9 @@ import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.wizard.Wizard; import ghidra.GhidraApplicationLayout; -import ghidra.launch.JavaConfig; +import ghidra.launch.AppConfig; import ghidradev.ghidraprojectcreator.utils.GhidraProjectUtils; +import ghidradev.ghidraprojectcreator.utils.PyDevUtils.ProjectPythonInterpreter; import ghidradev.ghidraprojectcreator.wizards.pages.*; /** @@ -63,10 +64,10 @@ public class LinkGhidraWizard extends Wizard { public boolean performFinish() { File ghidraInstallDir = ghidraInstallationPage.getGhidraInstallDir(); IJavaProject javaProject = projectPage.getJavaProject(); - String jythonInterpreterName = pythonPage.getJythonInterpreterName(); + ProjectPythonInterpreter pythonInterpreter = pythonPage.getProjectPythonInterpreter(); try { getContainer().run(true, false, - monitor -> link(ghidraInstallDir, javaProject, jythonInterpreterName, monitor)); + monitor -> link(ghidraInstallDir, javaProject, pythonInterpreter, monitor)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -85,23 +86,23 @@ public class LinkGhidraWizard extends Wizard { * * @param ghidraInstallDir The Ghidra installation directory to use. * @param javaProject The Java project to link. - * @param jythonInterpreterName The name of the Jython interpreter to use for Python support. - * Could be null if Python support is not wanted. + * @param pythonInterpreter The Python interpreter to use. * @param monitor The monitor to use during project link. * @throws InvocationTargetException if an error occurred during link. */ - private void link(File ghidraInstallDir, IJavaProject javaProject, String jythonInterpreterName, - IProgressMonitor monitor) throws InvocationTargetException { + private void link(File ghidraInstallDir, IJavaProject javaProject, + ProjectPythonInterpreter pythonInterpreter, IProgressMonitor monitor) + throws InvocationTargetException { IProject project = javaProject.getProject(); try { info("Linking " + project.getName()); monitor.beginTask("Linking " + project.getName(), 2); GhidraApplicationLayout ghidraLayout = new GhidraApplicationLayout(ghidraInstallDir); - JavaConfig javaConfig = - new JavaConfig(ghidraLayout.getApplicationInstallationDir().getFile(false)); - GhidraProjectUtils.linkGhidraToProject(javaProject, ghidraLayout, javaConfig, - jythonInterpreterName, monitor); + AppConfig appConfig = + new AppConfig(ghidraLayout.getApplicationInstallationDir().getFile(false)); + GhidraProjectUtils.linkGhidraToProject(javaProject, ghidraLayout, appConfig, + pythonInterpreter, monitor); monitor.worked(1); project.refreshLocal(IResource.DEPTH_INFINITE, monitor); diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/pages/ChooseGhidraInstallationWizardPage.java b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/pages/ChooseGhidraInstallationWizardPage.java index 0b9c86d0ab..faaa5d31df 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/pages/ChooseGhidraInstallationWizardPage.java +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/pages/ChooseGhidraInstallationWizardPage.java @@ -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,7 +27,7 @@ import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.*; import org.eclipse.ui.dialogs.PreferencesUtil; -import ghidra.launch.JavaConfig; +import ghidra.launch.AppConfig; import ghidra.launch.JavaFinder.JavaFilter; import ghidradev.ghidraprojectcreator.preferences.GhidraProjectCreatorPreferencePage; import ghidradev.ghidraprojectcreator.preferences.GhidraProjectCreatorPreferences; @@ -108,8 +108,8 @@ public class ChooseGhidraInstallationWizardPage extends WizardPage { File ghidraInstallDir = new File(ghidraInstallDirCombo.getText()); GhidraProjectCreatorPreferencePage.validateGhidraInstallation(ghidraInstallDir); try { - JavaConfig javaConfig = new JavaConfig(ghidraInstallDir); - if (!javaConfig.isSupportedJavaHomeDir(javaConfig.getSavedJavaHome(), + AppConfig appConfig = new AppConfig(ghidraInstallDir); + if (!appConfig.isSupportedJavaHomeDir(appConfig.getSavedJavaHome(), JavaFilter.JDK_ONLY)) { message = "A supported JDK is not associated with this Ghidra " + "installation. Please run this Ghidra and try again."; diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/pages/EnablePythonWizardPage.java b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/pages/EnablePythonWizardPage.java index 1dcf6b71f8..0fbd682131 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/pages/EnablePythonWizardPage.java +++ b/GhidraBuild/EclipsePlugins/GhidraDev/GhidraDevPlugin/src/main/java/ghidradev/ghidraprojectcreator/wizards/pages/EnablePythonWizardPage.java @@ -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. @@ -15,8 +15,7 @@ */ package ghidradev.ghidraprojectcreator.wizards.pages; -import java.io.File; -import java.io.IOException; +import java.io.*; import java.nio.file.Files; import java.util.List; @@ -27,13 +26,15 @@ import org.eclipse.jface.wizard.WizardPage; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; -import org.eclipse.swt.layout.GridData; -import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.layout.*; import org.eclipse.swt.widgets.*; import org.eclipse.ui.dialogs.PreferencesUtil; +import ghidra.launch.AppConfig; import ghidradev.EclipseMessageUtils; import ghidradev.ghidraprojectcreator.utils.PyDevUtils; +import ghidradev.ghidraprojectcreator.utils.PyDevUtils.ProjectPythonInterpreter; +import ghidradev.ghidraprojectcreator.utils.PyDevUtils.ProjectPythonInterpreterType; /** * A wizard page that lets the user enable python for their project. @@ -41,7 +42,11 @@ import ghidradev.ghidraprojectcreator.utils.PyDevUtils; public class EnablePythonWizardPage extends WizardPage { private ChooseGhidraInstallationWizardPage ghidraInstallationPage; - private Button enablePythonCheckboxButton; + private Button pyghidraButton; + private Button jythonButton; + private Button noneButton; + private Combo pyghidraCombo; + private Button addPyGhidraButton; private Combo jythonCombo; private Button addJythonButton; @@ -61,46 +66,107 @@ public class EnablePythonWizardPage extends WizardPage { public void createControl(Composite parent) { Composite container = new Composite(parent, SWT.NULL); - container.setLayout(new GridLayout(3, false)); + container.setLayout(new GridLayout(1, false)); - // Enable Python checkbox. - enablePythonCheckboxButton = new Button(container, SWT.CHECK); - enablePythonCheckboxButton.setText("Enable Python"); - enablePythonCheckboxButton.setToolTipText("Enables Python support using the PyDev " + - "Eclipse plugin. Requires PyDev version " + PyDevUtils.MIN_SUPPORTED_VERSION + - " - " + PyDevUtils.MAX_SUPPORTED_VERSION); - enablePythonCheckboxButton.setSelection(PyDevUtils.isSupportedPyDevInstalled()); - enablePythonCheckboxButton.addSelectionListener(new SelectionListener() { + // Project type selection + SelectionListener projectTypeSelectionListener = new SelectionListener() { @Override public void widgetSelected(SelectionEvent evt) { - validate(); + validate(null); } @Override public void widgetDefaultSelected(SelectionEvent evt) { - validate(); + validate(null); } + }; + Group projectTypeGroup = new Group(container, SWT.SHADOW_ETCHED_OUT); + projectTypeGroup.setLayout(new RowLayout(SWT.HORIZONTAL)); + projectTypeGroup.setText("Project Type"); + pyghidraButton = new Button(projectTypeGroup, SWT.RADIO); + pyghidraButton.setSelection(PyDevUtils.isSupportedPyGhidraPyDevInstalled()); + pyghidraButton.setText("PyGhidra"); + pyghidraButton.setToolTipText("Enables PyGhidra support using the PyDev " + + "Eclipse plugin. Requires PyDev version " + PyDevUtils.MIN_SUPPORTED_VERSION + + " or later."); + pyghidraButton.addSelectionListener(projectTypeSelectionListener); + jythonButton = new Button(projectTypeGroup, SWT.RADIO); + jythonButton.setSelection(false); + jythonButton.setText("Jython"); + jythonButton.setToolTipText("Enables Jython support using the PyDev " + + "Eclipse plugin. Requires PyDev version " + PyDevUtils.MIN_SUPPORTED_VERSION + + " - " + PyDevUtils.MAX_JYTHON_SUPPORTED_VERSION); + jythonButton.addSelectionListener(projectTypeSelectionListener); + noneButton = new Button(projectTypeGroup, SWT.RADIO); + noneButton.setSelection(!PyDevUtils.isSupportedPyGhidraPyDevInstalled()); + noneButton.setText("None"); + noneButton.setToolTipText("Disables Python support for the project."); + noneButton.addSelectionListener(projectTypeSelectionListener); + + Composite interpreterContainer = new Composite(container, SWT.NULL); + interpreterContainer.setLayout(new GridLayout(3, false)); + interpreterContainer.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // PyGhidra interpreter combo box + Label pyGhidraLabel = new Label(interpreterContainer, SWT.NULL); + pyGhidraLabel.setText("PyGhidra interpreter:"); + pyghidraCombo = new Combo(interpreterContainer, SWT.DROP_DOWN | SWT.READ_ONLY); + pyghidraCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + pyghidraCombo.setToolTipText("The wizard requires a Python interpreter to be " + + "selected. Click the + button to add or manage Python interpreters."); + File savedPyGhidraInterpreter = populatePyGhidraCombo(null); + pyghidraCombo.addModifyListener(evt -> validate(null)); + + // PyGhidra interpreter add button + addPyGhidraButton = new Button(interpreterContainer, SWT.BUTTON1); + addPyGhidraButton.setText("+"); + addPyGhidraButton.setToolTipText("Adds/manages PyGhidra interpreters."); + addPyGhidraButton.addListener(SWT.Selection, evt -> { + try { + File ghidraDir = ghidraInstallationPage.getGhidraInstallDir(); + File pyghidraInterpreter = findPyGhidraInterpreter(); + File pypredefDir = new File(ghidraDir, "docs/ghidra_stubs/pypredef"); + if (!pypredefDir.isDirectory()) { + pypredefDir = null; + } + if (EclipseMessageUtils.showQuestionDialog("Python Found", + "PyGhidra was previously launched with: \"" + pyghidraInterpreter + + "\". Would you like to use it as your interpreter?")) { + PyDevUtils.addPyGhidraInterpreter("pyghidra_" + ghidraDir.getName(), + pyghidraInterpreter, pypredefDir); + populatePyGhidraCombo(pyghidraInterpreter); + validate(pyghidraInterpreter); + return; + } + } + catch (Exception e) { + // Fall through to show PyDev's Python preference page + } + + PreferenceDialog dialog = PreferencesUtil.createPreferenceDialogOn(null, + PyDevUtils.getPythonPreferencePageId(), null, null); + dialog.open(); + populatePyGhidraCombo(savedPyGhidraInterpreter); + validate(null); }); - new Label(container, SWT.NONE).setText(""); // filler - new Label(container, SWT.NONE).setText(""); // filler // Jython interpreter combo box - Label jythonLabel = new Label(container, SWT.NULL); + Label jythonLabel = new Label(interpreterContainer, SWT.NULL); jythonLabel.setText("Jython interpreter:"); - jythonCombo = new Combo(container, SWT.DROP_DOWN | SWT.READ_ONLY); + jythonCombo = new Combo(interpreterContainer, SWT.DROP_DOWN | SWT.READ_ONLY); jythonCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); jythonCombo.setToolTipText("The wizard requires a Jython interpreter to be " + "selected. Click the + button to add or manage Jython interpreters."); populateJythonCombo(); - jythonCombo.addModifyListener(evt -> validate()); + jythonCombo.addModifyListener(evt -> validate(null)); // Jython interpreter add button - addJythonButton = new Button(container, SWT.BUTTON1); + addJythonButton = new Button(interpreterContainer, SWT.BUTTON1); addJythonButton.setText("+"); addJythonButton.setToolTipText("Adds/manages Jython interpreters."); addJythonButton.addListener(SWT.Selection, evt -> { try { - if (PyDevUtils.getJython27InterpreterNames().isEmpty()) { + if (PyDevUtils.getJythonInterpreterNames().isEmpty()) { File ghidraDir = ghidraInstallationPage.getGhidraInstallDir(); File jythonFile = findJythonInterpreter(ghidraDir); File jythonLib = findJythonLibrary(ghidraDir); @@ -111,7 +177,7 @@ public class EnablePythonWizardPage extends WizardPage { PyDevUtils.addJythonInterpreter("jython_" + ghidraDir.getName(), jythonFile, jythonLib); populateJythonCombo(); - validate(); + validate(null); return; } } @@ -124,80 +190,142 @@ public class EnablePythonWizardPage extends WizardPage { PyDevUtils.getJythonPreferencePageId(), null, null); dialog.open(); populateJythonCombo(); - validate(); + validate(null); }); - validate(); + validate(savedPyGhidraInterpreter); setControl(container); } - /** - * Checks whether or not Python should be enabled. - * - * @return True if python should be enabled; otherwise, false. - */ - public boolean shouldEnablePython() { - return enablePythonCheckboxButton.getSelection(); + @Override + public void setVisible(boolean visible) { + if (visible) { + validate(populatePyGhidraCombo(null)); + } + super.setVisible(visible); } /** - * Gets the name of the Jython interpreter to use. - * - * @return The name of the Jython interpreter to use. Could be null of Python isn't - * enabled. + * {@return the project Python interpreter to use} */ - public String getJythonInterpreterName() { - if (enablePythonCheckboxButton.getSelection()) { - return jythonCombo.getText(); + public ProjectPythonInterpreter getProjectPythonInterpreter() { + if (pyghidraButton.getSelection()) { + return new ProjectPythonInterpreter(pyghidraCombo.getText(), + ProjectPythonInterpreterType.PYGHIDRA); } - return null; + if (jythonButton.getSelection()) { + return new ProjectPythonInterpreter(jythonCombo.getText(), + ProjectPythonInterpreterType.JYTHON); + } + return new ProjectPythonInterpreter(null, ProjectPythonInterpreterType.NONE); } /** * Validates the fields on the page and updates the page's status. * Should be called every time a field on the page changes. + * + * @param pyghidraInterpreter The Python interpreter used to launch PyGhidra (could be null if + * unknown). */ - private void validate() { + private void validate(File pyghidraInterpreter) { String message = null; - boolean pyDevInstalled = PyDevUtils.isSupportedPyDevInstalled(); - boolean pyDevEnabled = enablePythonCheckboxButton.getSelection(); - boolean comboEnabled = pyDevInstalled && pyDevEnabled; + boolean pyghidraSupported = PyDevUtils.isSupportedPyGhidraPyDevInstalled(); + boolean jythonSupported = PyDevUtils.isSupportedJythonPyDevInstalled(); - if (pyDevEnabled) { - if (!pyDevInstalled) { + if (pyghidraButton.getSelection()) { + if (!pyghidraSupported) { message = "PyDev version " + PyDevUtils.MIN_SUPPORTED_VERSION + - " - " + PyDevUtils.MAX_SUPPORTED_VERSION + " is not installed."; + " or later is not installed."; } else { try { - List interpreters = PyDevUtils.getJython27InterpreterNames(); - if (interpreters.isEmpty()) { - message = "No Jython interpreters found. Click the + button to add one."; + if (pyghidraInterpreter == null) { + pyghidraInterpreter = findPyGhidraInterpreter(); + } + if (pyghidraInterpreter == null) { + message = + "Please first launch PyGhidra to associate the Ghidra installation with a supported version of Python."; + pyghidraSupported = false; + } + else { + List interpreters = + PyDevUtils.getPyGhidraInterpreterNames(pyghidraInterpreter); + if (interpreters.isEmpty()) { + message ="No PyGhidra interpreters found. Click the + button or set project type to \"None\"."; + } } } catch (OperationNotSupportedException e) { - message = "PyDev version is not supported."; - comboEnabled = false; + message = "PyDev version is not supported for Jython."; + pyghidraSupported = false; + } + catch (Exception e) { + message = + "Failed to lookup Python interpreter associated with the Ghidra installation."; + pyghidraSupported = false; + } + } + } + else if (jythonButton.getSelection()) { + if (!jythonSupported) { + message = "PyDev version " + PyDevUtils.MIN_SUPPORTED_VERSION + + " - " + PyDevUtils.MAX_JYTHON_SUPPORTED_VERSION + " is not installed."; + } + else { + try { + List interpreters = PyDevUtils.getJythonInterpreterNames(); + if (interpreters.isEmpty()) { + message = + "No Jython interpreters found. Click the + button or set project type to \"None\"."; + } + } + catch (OperationNotSupportedException e) { + message = "PyDev version is not supported for Jython."; + jythonSupported = false; } } } - jythonCombo.setEnabled(comboEnabled); - addJythonButton.setEnabled(comboEnabled); + pyghidraCombo.setEnabled(pyghidraButton.getSelection() && pyghidraSupported); + addPyGhidraButton.setEnabled(pyghidraButton.getSelection() && pyghidraSupported); + jythonCombo.setEnabled(jythonButton.getSelection() && jythonSupported); + addJythonButton.setEnabled(jythonButton.getSelection() && jythonSupported); setErrorMessage(message); setPageComplete(message == null); } /** - * Populates the Jython combo box with discovered Jython names. + * Populates the PyGhidra combo box with discovered PyGhidra interpreter names. + * + * @param pyghidraInterpreter The Python interpreter used to launch PyGhidra (could be null if + * unknown). + * @return The Python interpreter used to launch PyGhidra (could be null if unknown). + */ + private File populatePyGhidraCombo(File pyghidraInterpreter) { + pyghidraCombo.removeAll(); + try { + if (pyghidraInterpreter == null) { + pyghidraInterpreter = findPyGhidraInterpreter(); + } + PyDevUtils.getPyGhidraInterpreterNames(pyghidraInterpreter).forEach(pyghidraCombo::add); + } + catch (Exception e) { + // Nothing to do. Combo should and will be empty. + } + if (pyghidraCombo.getItemCount() > 0) { + pyghidraCombo.select(0); + } + return pyghidraInterpreter; + } + + /** + * Populates the Jython combo box with discovered Jython interpreter names. */ private void populateJythonCombo() { jythonCombo.removeAll(); try { - for (String jythonName : PyDevUtils.getJython27InterpreterNames()) { - jythonCombo.add(jythonName); - } + PyDevUtils.getJythonInterpreterNames().forEach(jythonCombo::add); } catch (OperationNotSupportedException e) { // Nothing to do. Combo should and will be empty. @@ -207,6 +335,32 @@ public class EnablePythonWizardPage extends WizardPage { } } + /** + * Find's the Python interpreter file that was used to launch PyGhidra. + * + * @return The Python interpreter file that was used to launch PyGhidra, or null if one could + * not be found. + * @throws Exception if there was a problem finding the Python interpreter. + */ + private File findPyGhidraInterpreter() throws Exception { + File ghidraDir = ghidraInstallationPage.getGhidraInstallDir(); + AppConfig appConfig = new AppConfig(ghidraDir); + List cmd = appConfig.getSavedPythonCommand(); + if (cmd == null) { + return null; + } + cmd.add("-c"); + cmd.add("import sys; print(sys.executable)"); + Process p = new ProcessBuilder(cmd).redirectErrorStream(true).start(); + BufferedReader reader = + new BufferedReader(new InputStreamReader(p.getInputStream())); + String pythonExecutable = reader.readLine(); + if (p.waitFor() == 0) { + return new File(pythonExecutable); + } + return null; + } + /** * Find's a Jython interpreter file in the given Ghidra installation directory. * diff --git a/GhidraBuild/EclipsePlugins/GhidraDev/certification.manifest b/GhidraBuild/EclipsePlugins/GhidraDev/certification.manifest index 7ef12ea347..d7d8abed85 100644 --- a/GhidraBuild/EclipsePlugins/GhidraDev/certification.manifest +++ b/GhidraBuild/EclipsePlugins/GhidraDev/certification.manifest @@ -13,6 +13,7 @@ GhidraDevPlugin/icons/GhidraIcon16_bw.png||GHIDRA||||END| GhidraDevPlugin/icons/brick_add.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| GhidraDevPlugin/icons/brick_go.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| GhidraDevPlugin/icons/folder_link.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| +GhidraDevPlugin/icons/python.png||GHIDRA||||END| GhidraDevPlugin/icons/script_add.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| GhidraDevPlugin/icons/script_code_red.png||FAMFAMFAM Icons - CC 2.5|||famfamfam silk icon set|END| GhidraDevPlugin/plugin.xml||GHIDRA||||END| diff --git a/GhidraBuild/LaunchSupport/src/main/java/LaunchSupport.java b/GhidraBuild/LaunchSupport/src/main/java/LaunchSupport.java index a3df9fff0a..342daa08e7 100644 --- a/GhidraBuild/LaunchSupport/src/main/java/LaunchSupport.java +++ b/GhidraBuild/LaunchSupport/src/main/java/LaunchSupport.java @@ -78,20 +78,20 @@ public class LaunchSupport { try { File installDir = new File(installDirPath).getCanonicalFile(); // change relative path to absolute - JavaConfig javaConfig = new JavaConfig(installDir); + AppConfig appConfig = new AppConfig(installDir); JavaFinder javaFinder = JavaFinder.create(); // Pass control to a mode-specific handler switch (mode.toLowerCase()) { case "-java_home": - exitCode = handleJavaHome(javaConfig, javaFinder, JavaFilter.ANY, ask, save); + exitCode = handleJavaHome(appConfig, javaFinder, JavaFilter.ANY, ask, save); break; case "-jdk_home": exitCode = - handleJavaHome(javaConfig, javaFinder, JavaFilter.JDK_ONLY, ask, save); + handleJavaHome(appConfig, javaFinder, JavaFilter.JDK_ONLY, ask, save); break; case "-vmargs": - exitCode = handleVmArgs(javaConfig); + exitCode = handleVmArgs(appConfig); break; default: System.err.println("LaunchSupport received illegal argument: " + mode); @@ -109,7 +109,7 @@ public class LaunchSupport { * Handles figuring out a Java home directory to use for the launch. If it is successfully * determined, an exit code that indicates success is returned. * - * @param javaConfig The Java configuration that defines what we support. + * @param appConfig The appConfig configuration that defines what we support. * @param javaFinder The Java finder. * @param javaFilter A filter used to restrict what kind of Java installations we search for. * @param ask True to interact with the user to they can specify a Java home directory. @@ -120,12 +120,12 @@ public class LaunchSupport { * successfully determined. * @throws IOException if there was a disk-related problem. */ - private static int handleJavaHome(JavaConfig javaConfig, JavaFinder javaFinder, + private static int handleJavaHome(AppConfig appConfig, JavaFinder javaFinder, JavaFilter javaFilter, boolean ask, boolean save) throws IOException { if (ask) { - return askJavaHome(javaConfig, javaFinder, javaFilter); + return askJavaHome(appConfig, javaFinder, javaFilter); } - return findJavaHome(javaConfig, javaFinder, javaFilter, save); + return findJavaHome(appConfig, javaFinder, javaFilter, save); } /** @@ -133,7 +133,7 @@ public class LaunchSupport { * found, its path is printed to STDOUT and an exit code that indicates success is * returned. Otherwise, nothing is printed to STDOUT and an error exit code is returned. * - * @param javaConfig The Java configuration that defines what we support. + * @param appConfig The application configuration that defines what we support. * @param javaFinder The Java finder. * @param javaFilter A filter used to restrict what kind of Java installations we search for. * @param save True if the determined Java home directory should get saved to a file. @@ -141,19 +141,19 @@ public class LaunchSupport { * successfully determined. * @throws IOException if there was a problem saving the java home to disk. */ - private static int findJavaHome(JavaConfig javaConfig, JavaFinder javaFinder, + private static int findJavaHome(AppConfig appConfig, JavaFinder javaFinder, JavaFilter javaFilter, boolean save) throws IOException { File javaHomeDir; - LaunchProperties launchProperties = javaConfig.getLaunchProperties(); + LaunchProperties launchProperties = appConfig.getLaunchProperties(); // PRIORITY 1: JAVA_HOME_OVERRIDE property // If a valid java home override is specified in the launch properties, use that. // Someone presumably wants to force that specific version. javaHomeDir = launchProperties.getJavaHomeOverride(); - if (javaConfig.isSupportedJavaHomeDir(javaHomeDir, javaFilter)) { + if (appConfig.isSupportedJavaHomeDir(javaHomeDir, javaFilter)) { if (save) { - javaConfig.saveJavaHome(javaHomeDir); + appConfig.saveJavaHome(javaHomeDir); } System.out.println(javaHomeDir); return EXIT_SUCCESS; @@ -162,10 +162,10 @@ public class LaunchSupport { // PRIORITY 2: Java on PATH // This program (LaunchSupport) was started with the Java on the PATH. Try to use this one // next because it is most likely the one that is being upgraded on the user's system. - javaHomeDir = javaFinder.findSupportedJavaHomeFromCurrentJavaHome(javaConfig, javaFilter); + javaHomeDir = javaFinder.findSupportedJavaHomeFromCurrentJavaHome(appConfig, javaFilter); if (javaHomeDir != null) { if (save) { - javaConfig.saveJavaHome(javaHomeDir); + appConfig.saveJavaHome(javaHomeDir); } System.out.println(javaHomeDir); return EXIT_SUCCESS; @@ -173,19 +173,19 @@ public class LaunchSupport { // PRIORITY 3: Last used Java // Check to see if a prior launch resulted in that Java being saved. If so, try to use that. - javaHomeDir = javaConfig.getSavedJavaHome(); - if (javaConfig.isSupportedJavaHomeDir(javaHomeDir, javaFilter)) { + javaHomeDir = appConfig.getSavedJavaHome(); + if (appConfig.isSupportedJavaHomeDir(javaHomeDir, javaFilter)) { System.out.println(javaHomeDir); return EXIT_SUCCESS; } // PRIORITY 4: Find all supported Java installations, and use the newest. List javaHomeDirs = - javaFinder.findSupportedJavaHomeFromInstallations(javaConfig, javaFilter); + javaFinder.findSupportedJavaHomeFromInstallations(appConfig, javaFilter); if (!javaHomeDirs.isEmpty()) { javaHomeDir = javaHomeDirs.iterator().next(); if (save) { - javaConfig.saveJavaHome(javaHomeDir); + appConfig.saveJavaHome(javaHomeDir); } System.out.println(javaHomeDir); return EXIT_SUCCESS; @@ -199,7 +199,7 @@ public class LaunchSupport { * If a valid Java home directory was successfully determined, it is saved to the user's * Java home save file, and an exit code that indicates success is returned. * - * @param javaConfig The Java configuration that defines what we support. + * @param appConfig The application configuration that defines what we support. * @param javaFinder The Java finder. * @param javaFilter A filter used to restrict what kind of Java installations we search for. * * @return A suggested exit code based on whether or not a valid Java home directory was @@ -207,13 +207,13 @@ public class LaunchSupport { * @throws IOException if there was a problem interacting with the user, or saving the java * home location to disk. */ - private static int askJavaHome(JavaConfig javaConfig, JavaFinder javaFinder, + private static int askJavaHome(AppConfig appConfig, JavaFinder javaFinder, JavaFilter javaFilter) throws IOException { String javaName = javaFilter.equals(JavaFilter.JDK_ONLY) ? "JDK" : "Java"; String javaRange; - int min = javaConfig.getMinSupportedJava(); - int max = javaConfig.getMaxSupportedJava(); + int min = appConfig.getMinSupportedJava(); + int max = appConfig.getMaxSupportedJava(); if (min == max) { javaRange = min + ""; } @@ -226,7 +226,7 @@ public class LaunchSupport { System.out.println("******************************************************************"); System.out.println( - javaName + " " + javaRange + " (" + javaConfig.getSupportedArchitecture() + + javaName + " " + javaRange + " (" + appConfig.getSupportedArchitecture() + "-bit) could not be found and must be manually chosen!"); System.out.println("******************************************************************"); @@ -254,13 +254,13 @@ public class LaunchSupport { continue; } try { - JavaVersion javaVersion = javaConfig.getJavaVersion(javaHomeDir, javaFilter); - if (javaConfig.isJavaVersionSupported(javaVersion)) { + JavaVersion javaVersion = appConfig.getJavaVersion(javaHomeDir, javaFilter); + if (appConfig.isJavaVersionSupported(javaVersion)) { break; } System.out.println( "Java version " + javaVersion + " is outside of supported range: [" + - javaRange + " " + javaConfig.getSupportedArchitecture() + "-bit]"); + javaRange + " " + appConfig.getSupportedArchitecture() + "-bit]"); } catch (FileNotFoundException e) { System.out.println( @@ -271,7 +271,7 @@ public class LaunchSupport { } } - File javaHomeSaveFile = javaConfig.saveJavaHome(javaHomeDir); + File javaHomeSaveFile = appConfig.saveJavaHome(javaHomeDir); System.out.println("Saved changes to " + javaHomeSaveFile); return EXIT_SUCCESS; } @@ -281,18 +281,18 @@ public class LaunchSupport { * to STDOUT as a new-line delimited string that can be parsed and added to the command line, * and an exit code that indicates success is returned. - * @param javaConfig The Java configuration that defines what we support. + * @param appConfig The appConfig configuration that defines what we support. * @return A suggested exit code based on whether or not the VM arguments were successfully * gotten. */ - private static int handleVmArgs(JavaConfig javaConfig) { - if (javaConfig.getLaunchProperties() == null) { + private static int handleVmArgs(AppConfig appConfig) { + if (appConfig.getLaunchProperties() == null) { System.out.println("Launch properties file was not specified!"); return EXIT_FAILURE; } // Force newline style to make cross-platform parsing consistent - javaConfig.getLaunchProperties().getVmArgList().forEach(e -> System.out.print(e + "\r\n")); + appConfig.getLaunchProperties().getVmArgList().forEach(e -> System.out.print(e + "\r\n")); return EXIT_SUCCESS; } } diff --git a/GhidraBuild/LaunchSupport/src/main/java/ghidra/launch/JavaConfig.java b/GhidraBuild/LaunchSupport/src/main/java/ghidra/launch/AppConfig.java similarity index 83% rename from GhidraBuild/LaunchSupport/src/main/java/ghidra/launch/JavaConfig.java rename to GhidraBuild/LaunchSupport/src/main/java/ghidra/launch/AppConfig.java index d83c6ceb9c..62b42efc8a 100644 --- a/GhidraBuild/LaunchSupport/src/main/java/ghidra/launch/JavaConfig.java +++ b/GhidraBuild/LaunchSupport/src/main/java/ghidra/launch/AppConfig.java @@ -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,21 +17,23 @@ package ghidra.launch; import java.io.*; import java.text.ParseException; -import java.util.Properties; +import java.util.*; import ghidra.launch.JavaFinder.JavaFilter; /** - * Class to determine and represent a required Java configuration, including minimum and maximum - * supported versions, compiler compliance level, etc. + * Class to determine and represent a required application configuration, including minimum and + * maximum supported Java versions, compiler compliance level, etc. */ -public class JavaConfig { +public class AppConfig { private static final String LAUNCH_PROPERTIES_NAME = "launch.properties"; private static final String JAVA_HOME_SAVE_NAME = "java_home.save"; + private static final String PYTHON_COMMAND_SAVE_NAME = "python_command.save"; private LaunchProperties launchProperties; private File javaHomeSaveFile; + private File pythonCommandSaveFile; private String applicationName; // example: Ghidra private String applicationVersion; // example: 9.0.1 @@ -42,43 +44,43 @@ public class JavaConfig { private String compilerComplianceLevel; /** - * Creates a new Java configuration for the given installation. + * Creates a new application configuration for the given installation. * * @param installDir The installation directory. * @throws FileNotFoundException if a required file was not found. * @throws IOException if there was a problem reading a required file. * @throws ParseException if there was a problem parsing a required file. */ - public JavaConfig(File installDir) throws FileNotFoundException, IOException, ParseException { + public AppConfig(File installDir) throws FileNotFoundException, IOException, ParseException { initApplicationProperties(installDir); initLaunchProperties(installDir); - initJavaHomeSaveFile(installDir); + javaHomeSaveFile = getSaveFile(installDir, JAVA_HOME_SAVE_NAME); + pythonCommandSaveFile = getSaveFile(installDir, PYTHON_COMMAND_SAVE_NAME); } /** - * Gets the launch properties associated with this Java configuration. Certain aspects of the - * Java configuration are stored in the launch properties. + * Gets the launch properties associated with this application configuration. * - * @return The launch properties associated with this Java configuration. Could be null if - * this Java configuration does not use launch properties. + * @return The launch properties associated with this application configuration. Could be null + * if this application configuration does not use launch properties. */ public LaunchProperties getLaunchProperties() { return launchProperties; } /** - * Gets the Java configuration's minimum supported major Java version. + * Gets the application configuration's minimum supported major Java version. * - * @return The Java configuration's minimum supported major Java version. + * @return The application configuration's minimum supported major Java version. */ public int getMinSupportedJava() { return minSupportedJava; } /** - * Gets the Java configuration's maximum supported major Java version. + * Gets the application configuration's maximum supported major Java version. * - * @return The Java configuration's maximum supported major Java version. If there is no + * @return The application configuration's maximum supported major Java version. If there is no * restriction, the value will be 0. */ public int getMaxSupportedJava() { @@ -86,20 +88,20 @@ public class JavaConfig { } /** - * Gets the Java configuration's supported Java architecture. All supported Java + * Gets the application configuration's supported Java architecture. All supported Java * configurations must have an architecture of 64. * - * @return The Java configuration's supported Java architecture (64). + * @return The application configuration's supported Java architecture (64). */ public int getSupportedArchitecture() { return 64; } /** - * Gets the Java configuration's compiler compliance level that was used to build the - * associated installation. + * Gets the application configuration's Java compiler compliance level that was used to build + * the associated installation. * - * @return The Java configuration's compiler compliance level. + * @return The application configuration's compiler compliance level. */ public String getCompilerComplianceLevel() { return compilerComplianceLevel; @@ -115,8 +117,11 @@ public class JavaConfig { public File getSavedJavaHome() throws IOException { try (BufferedReader reader = new BufferedReader(new FileReader(javaHomeSaveFile))) { String line = reader.readLine().trim(); - if (line != null && !line.isEmpty()) { - return new File(line); + if (line != null) { + line = line.trim(); + if (!line.isEmpty()) { + return new File(line); + } } } catch (FileNotFoundException e) { @@ -148,12 +153,12 @@ public class JavaConfig { } /** - * Tests to see if the given directory is a supported Java home directory for this Java + * Tests to see if the given directory is a supported Java home directory for this application * configuration. * * @param dir The directory to test. * @param javaFilter A filter used to restrict what kind of Java installations we support. - * @return True if the given directory is a supported Java home directory for this Java + * @return True if the given directory is a supported Java home directory for this application * configuration. */ public boolean isSupportedJavaHomeDir(File dir, JavaFilter javaFilter) { @@ -166,10 +171,10 @@ public class JavaConfig { } /** - * Tests to see if the given Java version is supported by this Java launch configuration. + * Tests to see if the given Java version is supported by this application configuration. * * @param javaVersion The java version to check. - * @return True if the given Java version is supported by this Java launch configuration. + * @return True if the given Java version is supported by this application configuration. */ public boolean isJavaVersionSupported(JavaVersion javaVersion) { if (javaVersion.getArchitecture() != getSupportedArchitecture()) { @@ -233,6 +238,30 @@ public class JavaConfig { return runAndGetJavaVersion(javaExecutable); } + /** + * Gets the Python command from the user's Python command save file. + * + * @return The Python command from the user's Python command save file, or null if the file + * does not exist or is empty. + * @throws IOException if there was a problem reading the Python command save file. + */ + public List getSavedPythonCommand() throws IOException { + List command = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new FileReader(pythonCommandSaveFile))) { + String line = null; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty()) { + command.add(line); + } + } + return command; + } + catch (FileNotFoundException e) { + return null; + } + } + /** * Gets the version of the given Java executable from the output of running "java -version". * @@ -354,13 +383,13 @@ public class JavaConfig { } /** - * Initializes the Java home save file. + * Gets the given "save file". * * @param installDir The Ghidra installation directory. This is the directory that has the * "Ghidra" subdirectory in it. * @throws FileNotFoundException if the user's home directory was not found. */ - private void initJavaHomeSaveFile(File installDir) throws FileNotFoundException { + private File getSaveFile(File installDir, String saveFileName) throws FileNotFoundException { boolean isDev = new File(installDir, "build.gradle").isFile(); String appName = applicationName.replaceAll("\\s", "").toLowerCase(); @@ -385,16 +414,14 @@ public class JavaConfig { // Handle legacy application layout if (applicationLayoutVersion.equals("1")) { userSettingsDir = new File(userHomeDir, "." + appName + "/." + userSettingsDirName); - javaHomeSaveFile = new File(userSettingsDir, JAVA_HOME_SAVE_NAME); - return; + return new File(userSettingsDir, saveFileName); } // Look for XDG environment variable String xdgConfigHomeDirStr = System.getenv("XDG_CONFIG_HOME"); if (xdgConfigHomeDirStr != null && !xdgConfigHomeDirStr.isEmpty()) { userSettingsDir = new File(xdgConfigHomeDirStr, appName + "/" + userSettingsDirName); - javaHomeSaveFile = new File(userSettingsDir, JAVA_HOME_SAVE_NAME); - return; + return new File(userSettingsDir, saveFileName); } // Look in current user settings directory @@ -420,7 +447,7 @@ public class JavaConfig { "Failed to find the user settings directory: Unsupported operating system."); } - javaHomeSaveFile = new File(userSettingsDir, JAVA_HOME_SAVE_NAME); + return new File(userSettingsDir, saveFileName); } /** diff --git a/GhidraBuild/LaunchSupport/src/main/java/ghidra/launch/JavaFinder.java b/GhidraBuild/LaunchSupport/src/main/java/ghidra/launch/JavaFinder.java index b9a2a83feb..400f26b6fd 100644 --- a/GhidraBuild/LaunchSupport/src/main/java/ghidra/launch/JavaFinder.java +++ b/GhidraBuild/LaunchSupport/src/main/java/ghidra/launch/JavaFinder.java @@ -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. @@ -79,11 +79,11 @@ public abstract class JavaFinder { * Returns a list of supported Java home directories from discovered Java installations. * The list is sorted from newest Java version to oldest. * - * @param javaConfig The Java configuration that defines what we support. + * @param appConfig The appConfig configuration that defines what we support. * @param javaFilter A filter used to restrict what kind of Java installations we search for. * @return A sorted list of supported Java home directories from discovered Java installations. */ - public List findSupportedJavaHomeFromInstallations(JavaConfig javaConfig, + public List findSupportedJavaHomeFromInstallations(AppConfig appConfig, JavaFilter javaFilter) { Set potentialJavaHomeSet = new TreeSet<>(); for (File javaRootInstallDir : getJavaRootInstallDirs()) { @@ -107,8 +107,8 @@ public abstract class JavaFinder { for (File potentialJavaHomeDir : potentialJavaHomeSet) { try { JavaVersion javaVersion = - javaConfig.getJavaVersion(potentialJavaHomeDir, javaFilter); - if (javaConfig.isJavaVersionSupported(javaVersion)) { + appConfig.getJavaVersion(potentialJavaHomeDir, javaFilter); + if (appConfig.isJavaVersionSupported(javaVersion)) { javaHomeToVersionMap.put(potentialJavaHomeDir, javaVersion); } } @@ -130,12 +130,12 @@ public abstract class JavaFinder { * Returns the Java home directory corresponding to the current "java.home" system * property (if it supported). * - * @param javaConfig The Java configuration that defines what we support. + * @param appConfig The appConfig configuration that defines what we support. * @param javaFilter A filter used to restrict what kind of Java installations we search for. * @return The Java home directory corresponding to the current "java.home" system property. * Could be null if the current "java.home" is not supported. */ - public File findSupportedJavaHomeFromCurrentJavaHome(JavaConfig javaConfig, + public File findSupportedJavaHomeFromCurrentJavaHome(AppConfig appConfig, JavaFilter javaFilter) { Set potentialJavaHomeSet = new HashSet<>(); String javaHomeProperty = System.getProperty("java.home"); @@ -149,8 +149,8 @@ public abstract class JavaFinder { } for (File potentialJavaHomeDir : potentialJavaHomeSet) { try { - if (javaConfig.isJavaVersionSupported( - javaConfig.getJavaVersion(potentialJavaHomeDir, javaFilter))) { + if (appConfig.isJavaVersionSupported( + appConfig.getJavaVersion(potentialJavaHomeDir, javaFilter))) { return potentialJavaHomeDir; } } diff --git a/gradle/root/distribution.gradle b/gradle/root/distribution.gradle index 06f35ed853..7ddf0673f7 100644 --- a/gradle/root/distribution.gradle +++ b/gradle/root/distribution.gradle @@ -432,6 +432,9 @@ task assembleDistribution (type: Copy) { from ("${ROOT_PROJECT_DIR}/build/typestubs/src") { into 'docs/ghidra_stubs/typestubs' } + from ("${ROOT_PROJECT_DIR}/build/typestubs/pypredef") { + into 'docs/ghidra_stubs/pypredef' + } from (createGhidraStubsWheel) { into 'docs/ghidra_stubs' } diff --git a/gradle/support/fetchDependencies.gradle b/gradle/support/fetchDependencies.gradle index 147a13a21d..8d292cf55a 100644 --- a/gradle/support/fetchDependencies.gradle +++ b/gradle/support/fetchDependencies.gradle @@ -95,9 +95,9 @@ ext.deps = [ destination: file("${DEPS_DIR}/BSim") ], [ - name: "PyDev 6.3.1.zip", - url: "https://sourceforge.net/projects/pydev/files/pydev/PyDev%206.3.1/PyDev%206.3.1.zip", - sha256: "4d81fe9d8afe7665b8ea20844d3f5107f446742927c59973eade4f29809b0699", + name: "PyDev 9.3.0.zip", + url: "https://sourceforge.net/projects/pydev/files/pydev/PyDev%209.3.0/PyDev%209.3.0.zip", + sha256: "45398edf2adb56078a80bc88a919941578f0c0b363efbdd011bfd158a99b112e", destination: file("${DEPS_DIR}/GhidraDev") ], [