From 9fddd2a54eaa640888c656cdfb490690ec6f519b Mon Sep 17 00:00:00 2001 From: 0cyn Date: Tue, 14 Apr 2026 15:45:28 -0400 Subject: Plugin Manifest V2 Support --- python/__init__.py | 1 - python/pluginmanager.py | 52 ++++++++++++++++++++++++++++++--------------- python/scriptingprovider.py | 18 +++++++++------- 3 files changed, 45 insertions(+), 26 deletions(-) (limited to 'python') diff --git a/python/__init__.py b/python/__init__.py index 8790ac8f..cafea2e8 100644 --- a/python/__init__.py +++ b/python/__init__.py @@ -263,7 +263,6 @@ def _init_plugins(): if _enable_default_log and is_headless_init_once and min_level in LogLevel.__members__ and not core_ui_enabled( ) and sys.stderr.isatty(): log_to_stderr(LogLevel[min_level]) - core.BNInitRepoPlugins() if core.BNIsLicenseValidated(): _plugin_init = True else: diff --git a/python/pluginmanager.py b/python/pluginmanager.py index 27360dbe..5e32a79f 100644 --- a/python/pluginmanager.py +++ b/python/pluginmanager.py @@ -29,9 +29,9 @@ from . import deprecation from .enums import PluginType -class RepoPlugin: +class Extension: """ - ``RepoPlugin`` is mostly read-only, however you can install/uninstall enable/disable plugins. RepoPlugins are + ``Extension`` is mostly read-only, however you can install/uninstall enable/disable plugins. Extensions are created by parsing the plugins.json in a plugin repository. """ def __init__(self, handle: 'core.BNRepoPluginHandle'): @@ -70,10 +70,10 @@ class RepoPlugin: """Boolean True if the plugin is installed, False otherwise""" return core.BNPluginIsInstalled(self.handle) - def install(self) -> bool: + def install(self, version_id=None) -> bool: """Attempt to install the given plugin""" self.install_dependencies() - return core.BNPluginInstall(self.handle) + return core.BNPluginInstall(self.handle, version_id) def uninstall(self) -> bool: """Attempt to uninstall the given plugin""" @@ -83,7 +83,7 @@ class RepoPlugin: def installed(self, state: bool): if state: self.install_dependencies() - core.BNPluginInstall(self.handle) + core.BNPluginInstall(self.handle, None) else: core.BNPluginUninstall(self.handle) @@ -126,14 +126,16 @@ class RepoPlugin: core.BNFreePluginPlatforms(platforms, count.value) @property + @deprecation.deprecated(deprecated_in="5.3", details='Use :py:attr:`current_version` in combination with :py:attr:`versions` instead.') def description(self) -> Optional[str]: """String short description of the plugin""" return core.BNPluginGetDescription(self.handle) @property + @deprecation.deprecated(deprecated_in="5.3", details='This field will be removed.') def license_text(self) -> Optional[str]: """String complete license text for the given plugin""" - return core.BNPluginGetLicenseText(self.handle) + return '' @property def long_description(self) -> Optional[str]: @@ -185,6 +187,7 @@ class RepoPlugin: return core.BNPluginGetProjectUrl(self.handle) @property + @deprecation.deprecated(deprecated_in="5.3", details='Use :py:attr:`current_version` in combination with :py:attr:`versions` instead.') def package_url(self) -> Optional[str]: """String URL of the plugin's zip file""" return core.BNPluginGetPackageUrl(self.handle) @@ -200,9 +203,17 @@ class RepoPlugin: return core.BNPluginGetAuthor(self.handle) @property + @deprecation.deprecated(deprecated_in="5.3", details='Use :py:attr:`current_version` in combination with :py:attr:`versions` instead.') def version(self) -> Optional[str]: """String version of the plugin""" - return core.BNPluginGetVersion(self.handle) + version: core.BNPluginVersion = core.BNPluginGetCurrentVersion(self.handle) + try: + version_string = version.versionString + except AttributeError: + version_string = "" + finally: + core.BNPluginFreeVersion(version) + return version_string @property def install_platforms(self) -> List[str]: @@ -259,6 +270,7 @@ class RepoPlugin: return core.BNPluginAreDependenciesBeingInstalled(self.handle) @property + @deprecation.deprecated(deprecated_in="5.3", details='This field will be removed.') def project_data(self) -> Dict: """Gets a json object of the project data field""" data = core.BNPluginGetProjectData(self.handle) @@ -266,11 +278,18 @@ class RepoPlugin: return json.loads(data) @property + @deprecation.deprecated(deprecated_in="5.3", details='Use :py:attr:`versions` in combination with :py:attr:`current_version` to check for updates instead.') def last_update(self) -> date: """Returns a datetime object representing the plugins last update""" return datetime.fromtimestamp(core.BNPluginGetLastUpdate(self.handle)) +@deprecation.deprecated(deprecated_in="5.3", details='Use :py:class:`binaryninja.Extension` instead.') +class RepoPlugin(Extension): + def __init__(self, handle: 'core.BNRepoPluginHandle'): + super().__init__(handle) + + class Repository: """ ``Repository`` is a read-only class. Use RepositoryManager to Enable/Disable/Install/Uninstall plugins. @@ -313,8 +332,8 @@ class Repository: return result @property - def plugins(self) -> List[RepoPlugin]: - """List of RepoPlugin objects contained within this repository""" + def plugins(self) -> List[Extension]: + """List of Extension objects contained within this repository""" pluginlist = [] count = ctypes.c_ulonglong(0) result = core.BNRepositoryGetPlugins(self.handle, count) @@ -323,7 +342,7 @@ class Repository: for i in range(count.value): plugin_ref = core.BNNewPluginReference(result[i]) assert plugin_ref is not None, "core.BNNewPluginReference returned None" - pluginlist.append(RepoPlugin(plugin_ref)) + pluginlist.append(Extension(plugin_ref)) return pluginlist finally: core.BNFreeRepositoryPluginList(result) @@ -337,7 +356,6 @@ class RepositoryManager: """ def __init__(self): binaryninja._init_plugins() - self.handle = core.BNGetRepositoryManager() def __getitem__(self, repo_path: str) -> Repository: for repo in self.repositories: @@ -347,14 +365,14 @@ class RepositoryManager: def check_for_updates(self) -> bool: """Check for updates for all managed Repository objects""" - return core.BNRepositoryManagerCheckForUpdates(self.handle) + return core.BNRepositoryManagerCheckForUpdates() @property def repositories(self) -> List[Repository]: """List of Repository objects being managed""" result = [] count = ctypes.c_ulonglong(0) - repos = core.BNRepositoryManagerGetRepositories(self.handle, count) + repos = core.BNRepositoryManagerGetRepositories(count) assert repos is not None, "core.BNRepositoryManagerGetRepositories returned None" try: for i in range(count.value): @@ -366,8 +384,8 @@ class RepositoryManager: core.BNFreeRepositoryManagerRepositoriesList(repos) @property - def plugins(self) -> Dict[str, List[RepoPlugin]]: - """List of all RepoPlugins in each repository""" + def plugins(self) -> Dict[str, List[Extension]]: + """List of all Extensions in each repository""" plugin_list = {} for repo in self.repositories: plugin_list[repo.path] = repo.plugins @@ -376,7 +394,7 @@ class RepositoryManager: @property def default_repository(self) -> Repository: """Gets the default Repository""" - repo_handle = core.BNRepositoryManagerGetDefaultRepository(self.handle) + repo_handle = core.BNRepositoryManagerGetDefaultRepository() assert repo_handle is not None, "core.BNRepositoryManagerGetDefaultRepository returned None" repo_handle_ref = core.BNNewRepositoryReference(repo_handle) assert repo_handle_ref is not None, "core.BNNewRepositoryReference returned None" @@ -406,4 +424,4 @@ class RepositoryManager: if not isinstance(url, str) or not isinstance(repopath, str): raise ValueError("Expected url or repopath to be of type str.") - return core.BNRepositoryManagerAddRepository(self.handle, url, repopath) + return core.BNRepositoryManagerAddRepository(url, repopath) diff --git a/python/scriptingprovider.py b/python/scriptingprovider.py index dbeef1cb..3b6718af 100644 --- a/python/scriptingprovider.py +++ b/python/scriptingprovider.py @@ -53,6 +53,7 @@ from .pluginmanager import RepositoryManager from .enums import ScriptingProviderExecuteResult, ScriptingProviderInputReadyState from .settings import Settings from .enums import SettingsScope +import json _WARNING_REGEX = re.compile(r'^\S+:\d+: \w+Warning: ') @@ -1086,12 +1087,8 @@ class PythonScriptingProvider(ScriptingProvider): plugin = repo[module] if not force and self.apiName not in plugin.api: - raise ValueError(f"Plugin API name is not {self.name}") + raise ValueError(f"Plugin '{plugin.name}' API name '{plugin.api}' is not {self.name}") - if not force and core.core_platform not in plugin.install_platforms: - raise ValueError( - f"Current platform {core.core_platform} isn't in list of valid platforms for this plugin {plugin.install_platforms}" - ) if not plugin.installed: plugin.installed = True @@ -1218,8 +1215,13 @@ class PythonScriptingProvider(ScriptingProvider): def _install_modules(self, ctx, _modules: bytes) -> bool: # This callback should not be called directly - modules = _modules.decode("utf-8") - if len(modules.strip()) == 0: + dependencies_json = json.loads(_modules.decode("utf-8")) + modules = "" + if "pip" in dependencies_json: + if len(dependencies_json["pip"].strip()) == 0: + return True + modules = [line.split('#', 1)[0].strip() for line in dependencies_json["pip"].split('\n') if line.split('#', 1)[0].strip()] + if len(modules) == 0: return True python_lib = settings.Settings().get_string("python.interpreter") python_bin_override = settings.Settings().get_string("python.binaryOverride") @@ -1267,7 +1269,7 @@ class PythonScriptingProvider(ScriptingProvider): ) / f"python{sys.version_info.major}{sys.version_info.minor}" / "site-packages" site_package_dir.mkdir(parents=True, exist_ok=True) args.extend(["--target", str(site_package_dir)]) - args.extend(list(filter(len, modules.split("\n")))) + args.extend(list(filter(len, modules))) logger.log_info(f"Running pip {args}") status, result = self._run_args(args, env=python_env) if status: -- cgit v1.3.1