## ### # 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. ## import argparse import platform import os import sys import subprocess import sysconfig from pathlib import Path from itertools import chain from typing import List, Dict, Tuple def get_application_properties(install_dir: Path) -> Dict[str, str]: app_properties_path: Path = install_dir / 'Ghidra' / 'application.properties' props: Dict[str, str] = {} with open(app_properties_path, 'r') as f: for line in f: line = line.strip() if not line or line.startswith('#') or line.startswith('!'): continue key, value = line.split('=', 1) if key: props[key] = value return props def get_launch_properties(install_dir: Path, dev: bool) -> List[str]: if dev: launch_properties_path: Path = install_dir / 'Ghidra' / 'RuntimeScripts' / 'Common' / 'support' / 'launch.properties' else: launch_properties_path: Path = install_dir / 'support' / 'launch.properties' props: List[str] = [] with open(launch_properties_path, 'r') as f: for line in f: line = line.strip() if not line or line.startswith('#') or line.startswith('!'): continue props.append(line) return props def get_user_settings_dir(install_dir: Path, dev: bool) -> Path: app_props: Dict[str, str] = get_application_properties(install_dir) app_name: str = app_props['application.name'].replace(' ', '').lower() app_version: str = app_props['application.version'] app_release_name: str = app_props['application.release.name'] versioned_name: str = f'{app_name}_{app_version}_{app_release_name}' if dev: versioned_name += f'_location_{install_dir.parent.name}' # Check for application.settingsdir in launch.properties for launch_prop in get_launch_properties(install_dir, dev): if launch_prop.startswith('VMARGS=-Dapplication.settingsdir='): application_settingsdir = launch_prop[launch_prop.rindex('=')+1:] if application_settingsdir: return Path(application_settingsdir) / app_name / versioned_name # Check for XDG_CONFIG_HOME environment variable xdg_config_home: str = os.environ.get('XDG_CONFIG_HOME') if xdg_config_home: return Path(xdg_config_home) / app_name / versioned_name # Default to platform-specific locations if platform.system() == 'Windows': return Path(os.environ['APPDATA']) / app_name / versioned_name if platform.system() == 'Darwin': return Path.home() / 'Library' / app_name / versioned_name return Path.home() / '.config' / app_name / versioned_name def find_supported_python_exe(install_dir: Path, dev: bool) -> List[str]: python_cmds = [] saved_python_cmd = get_saved_python_cmd(install_dir, dev) if saved_python_cmd is not None: python_cmds.append(saved_python_cmd) print("Last used Python executable: " + str(saved_python_cmd)) props: Dict[str, str] = get_application_properties(install_dir) prop: str = 'application.python.supported' supported: List[str] = [s.strip() for s in props.get(prop, '').split(',')] if '' in supported: raise ValueError(f'Invalid "{prop}" value in application.properties file') python_cmds += list(chain.from_iterable([[f'python{s}'], ['py', f'-{s}']] for s in supported)) python_cmds += [['python3'], ['python'], ['py']] for cmd in python_cmds: try: result = subprocess.run(cmd + ['-c', 'import sys; print("{0}.{1}".format(*sys.version_info))'], capture_output=True, text=True) version = result.stdout.strip() if result.returncode == 0 and version in supported: return cmd except FileNotFoundError: pass return None def in_venv() -> bool: return sys.prefix != sys.base_prefix def is_externally_managed() -> bool: get_default_scheme = 'get_default_scheme' if hasattr(sysconfig, get_default_scheme): # Python 3.10 and later default_scheme = getattr(sysconfig, get_default_scheme) else: # Python 3.9 default_scheme = getattr(sysconfig, f'_{get_default_scheme}') marker: Path = Path(sysconfig.get_path("stdlib", default_scheme())) / 'EXTERNALLY-MANAGED' return marker.is_file() def get_venv_exe(venv_dir: Path) -> List[str]: win_python_cmd: str = str(venv_dir / 'Scripts' / 'python.exe') linux_python_cmd: str = str(venv_dir / 'bin' / 'python3') return [win_python_cmd] if platform.system() == 'Windows' else [linux_python_cmd] def get_ghidra_venv(install_dir: Path, dev: bool) -> Path: return (install_dir / 'build' if dev else get_user_settings_dir(install_dir, dev)) / 'venv' def create_ghidra_venv(python_cmd: List[str], venv_dir: Path) -> None: print(f'Creating Ghidra virtual environment at {venv_dir}...') subprocess.run(python_cmd + ['-m', 'venv', venv_dir.absolute()]) def version_tuple(v: str) -> Tuple[str, ...]: filled = [] for point in v.split("."): filled.append(point.zfill(8)) return tuple(filled) def get_package_version(python_cmd: List[str], package: str) -> str: version = None result = subprocess.run(python_cmd + ['-m', 'pip', 'show', package], capture_output=True, text=True) for line in result.stdout.splitlines(): line = line.strip() print(line) key, value = line.split(':', 1) if key == 'Version': version = value.strip() return version def get_saved_python_cmd(install_dir: Path, dev: bool) -> List[str]: user_settings_dir: Path = get_user_settings_dir(install_dir, dev) save_file: Path = user_settings_dir / 'python_command.save' if not save_file.is_file(): return None ret = [] with open(save_file, 'r') as f: for line in f: ret.append(line.strip()) return ret def save_python_cmd(install_dir: Path, python_cmd: List[str], dev: bool) -> None: user_settings_dir: Path = get_user_settings_dir(install_dir, dev) if not user_settings_dir.is_dir(): user_settings_dir.mkdir(parents=True, exist_ok=True) save_file: Path = user_settings_dir / 'python_command.save' with open(save_file, 'w') as f: f.write('\n'.join(python_cmd) + '\n') def install(install_dir: Path, python_cmd: List[str], pip_args: List[str], offer_venv: bool) -> List[str]: install_choice: str = input('Do you wish to install PyGhidra (y/n)? ') if install_choice.lower() in ('y', 'yes'): if offer_venv: ghidra_venv_choice: str = input('Install into new Ghidra virtual environment (y/n)? ') if ghidra_venv_choice.lower() in ('y', 'yes'): venv_dir = get_ghidra_venv(install_dir, False) create_ghidra_venv(python_cmd, venv_dir) python_cmd = get_venv_exe(venv_dir) print(f'Switching to Ghidra virtual environment: {venv_dir}') elif ghidra_venv_choice.lower() in ('n', 'no'): system_venv_choice: str = input('Install into system environment (y/n)? ') if not system_venv_choice.lower() in ('y', 'yes'): print('Must answer "y" to the prior choices, or launch in an already active virtual environment.') return None else: print('Please answer yes or no.') return None subprocess.check_call(python_cmd + pip_args) return python_cmd elif not install_choice.lower() in ('n', 'no'): print('Please answer yes or no.') return None def upgrade(python_cmd: List[str], pip_args: List[str], dist_dir: Path, current_pyghidra_version: str) -> bool: included_pyghidra: Path = next(dist_dir.glob('pyghidra-*.whl'), None) if included_pyghidra is None: print('Warning: included pyghidra wheel was not found', file=sys.stderr) return included_version = included_pyghidra.name.split('-')[1] current_version = current_pyghidra_version if version_tuple(included_version) > version_tuple(current_version): choice: str = input(f'Do you wish to upgrade PyGhidra {current_version} to {included_version} (y/n)? ') if choice.lower() in ('y', 'yes'): pip_args.append('-U') subprocess.check_call(python_cmd + pip_args) return True else: print('Skipping upgrade') return False def main() -> None: # Parse command line arguments parser = argparse.ArgumentParser(prog=Path(__file__).name) parser.add_argument('install_dir', metavar='', help='Ghidra installation directory') parser.add_argument('--console', action='store_true', help='Force console launch') parser.add_argument('--dev', action='store_true', help='Ghidra development mode') parser.add_argument('-H', '--headless', action='store_true', help='Ghidra headless mode') args, remaining = parser.parse_known_args() # Setup variables install_dir: Path = Path(os.path.normpath(args.install_dir)) pyghidra_dir: Path = install_dir / 'Ghidra' / 'Features' / 'PyGhidra' dist_dir: Path = pyghidra_dir / 'pypkg' / 'dist' venv_dir = get_ghidra_venv(install_dir, args.dev) python_cmd: List[str] = find_supported_python_exe(install_dir, args.dev) if python_cmd is not None: print(f'Using Python command: "{" ".join(python_cmd)}"') else: print('Supported version of Python not found. Check application.properties file.') sys.exit(1) # If headless, force console mode if args.headless: args.console = True if args.dev: # If in dev mode, launch PyGhidra from the source tree using the development virtual environment if not venv_dir.is_dir(): print('Virtual environment not found!') print('Run "gradle prepdev" and try again.') sys.exit(1) python_cmd = get_venv_exe(venv_dir) print(f'Switching to Ghidra virtual environment: {venv_dir}') else: # If in release mode, offer to install or upgrade PyGhidra before launching from user-controlled environment pip_args: List[str] = ['-m', 'pip', 'install', '--no-index', '-f', str(dist_dir), 'pyghidra'] # Setup the proper execution environment: # 1) If we are already in a virtual environment, use that # 2) If the Ghidra user settings virtual environment exists, use that # 3) If we are "externally managed", automatically create/use the Ghidra user settings virtual environment offer_venv: bool = False if in_venv(): # If we are already in a virtual environment, assume that's where the user wants to be python_cmd = get_venv_exe(Path(sys.prefix)) print(f'Using active virtual environment: {sys.prefix}') elif os.path.isdir(venv_dir): # If the Ghidra user settings venv exists, use that python_cmd = get_venv_exe(venv_dir) print(f'Switching to Ghidra virtual environment: {venv_dir}') elif is_externally_managed(): print('Externally managed environment detected!') create_ghidra_venv(python_cmd, venv_dir) python_cmd = get_venv_exe(venv_dir) print(f'Switching to Ghidra virtual environment: {venv_dir}') else: offer_venv = True # If PyGhidra is not installed in the execution environment, offer to install it # If it's already installed, offer to upgrade (if applicable) current_pyghidra_version = get_package_version(python_cmd, 'pyghidra') if current_pyghidra_version is None: python_cmd = install(install_dir, python_cmd, pip_args, offer_venv) if not python_cmd: sys.exit(1) else: upgrade(python_cmd, pip_args, dist_dir, current_pyghidra_version) # Launch PyGhidra save_python_cmd(install_dir, python_cmd, args.dev) py_args: List[str] = python_cmd + ['-m'] if args.headless: py_args += ['pyghidra.ghidra_launch', '--install-dir', str(install_dir), 'ghidra.app.util.headless.AnalyzeHeadless'] else: py_args += ['pyghidra', '-g', '--install-dir', str(install_dir)] if args.console: subprocess.call(py_args + remaining) else: creation_flags = getattr(subprocess, 'CREATE_NO_WINDOW', 0) subprocess.Popen(py_args + remaining, creationflags=creation_flags, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if __name__ == "__main__": main()