From bdb3e651f5ef8ecc020016e5603f3b3d003fb26c Mon Sep 17 00:00:00 2001 From: Alex Myers Date: Thu, 17 Apr 2025 16:41:55 -0500 Subject: [PATCH 01/13] reckless: fix installation from local directories with subpaths This could previously copy the parent directory of a plugin into the installed reckless directory, which was unnecessary. --- tests/test_reckless.py | 1 - tools/reckless | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_reckless.py b/tests/test_reckless.py index 3615b71a5518..4a1e59a93ef7 100644 --- a/tests/test_reckless.py +++ b/tests/test_reckless.py @@ -231,7 +231,6 @@ def test_poetry_install(node_factory): @unittest.skipIf(VALGRIND, "virtual environment triggers memleak detection") -@unittest.skip("Broken") def test_local_dir_install(node_factory): """Test search and install from local directory source.""" n = get_reckless_node(node_factory) diff --git a/tools/reckless b/tools/reckless index 7b692d391e0d..13535aa3dd77 100755 --- a/tools/reckless +++ b/tools/reckless @@ -1175,10 +1175,13 @@ def _install_plugin(src: InstInfo) -> Union[InstInfo, None]: log.debug(f'{clone_path} already exists - deleting') shutil.rmtree(clone_path) if src.srctype == Source.DIRECTORY: + full_source_path = Path(src.source_loc) + if src.subdir: + full_source_path /= src.subdir log.debug(("copying local directory contents from" - f" {src.source_loc}")) + f" {full_source_path}")) create_dir(clone_path) - shutil.copytree(src.source_loc, plugin_path) + shutil.copytree(full_source_path, plugin_path) elif src.srctype in [Source.LOCAL_REPO, Source.GITHUB_REPO, Source.OTHER_URL, Source.GIT_LOCAL_CLONE]: # clone git repository to /tmp/reckless-... From 5ced3247873de69f9a5433c81ec33205565deaf5 Mon Sep 17 00:00:00 2001 From: Alex Myers Date: Thu, 17 Apr 2025 17:13:49 -0500 Subject: [PATCH 02/13] reckless: accept a full local path as source+name This allows installing a local plugin directly without having to modify reckless sources. Changelog-changed: Reckless can be passed a local file or directory for installation. --- tools/reckless | 89 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 24 deletions(-) diff --git a/tools/reckless b/tools/reckless index 13535aa3dd77..1422516b469b 100755 --- a/tools/reckless +++ b/tools/reckless @@ -1300,6 +1300,30 @@ def _install_plugin(src: InstInfo) -> Union[InstInfo, None]: return staged_src +def location_from_name(plugin_name: str) -> (str, str): + """Maybe the location was passed in place of the plugin name. Check + if this looks like a filepath or URL and return that as well as the + plugin name.""" + if not Path(plugin_name).exists(): + # No path included, return the name only. + return (None, plugin_name) + + # Directory containing the plugin? The plugin name should match the dir. + if os.path.isdir(plugin_name): + return (Path(plugin_name).parent, Path(plugin_name).name) + + # Possibly the entrypoint itself was passed? + elif os.path.isfile(plugin_name): + if Path(plugin_name).with_suffix('').name != Path(plugin_name).parent.name or \ + not Path(plugin_name).parent.parent.exists(): + # If the directory is not named for the plugin, we can't infer what + # should be done. + # FIXME: return InstInfo with entrypoint rather than source str. + return (None, plugin_name) + # We have to make inferences as to the naming here. + return (Path(plugin_name).parent.parent, Path(plugin_name).with_suffix('').name) + + def install(plugin_name: str) -> Union[str, None]: """Downloads plugin from source repos, installs and activates plugin. Returns the location of the installed plugin or "None" in the case of @@ -1312,33 +1336,48 @@ def install(plugin_name: str) -> Union[str, None]: else: name = plugin_name commit = None - log.debug(f"Searching for {name}") - if search(name): - global LAST_FOUND - src = LAST_FOUND - src.commit = commit - log.debug(f'Retrieving {src.name} from {src.source_loc}') - try: - installed = _install_plugin(src) - except FileExistsError as err: - log.error(f'File exists: {err.filename}') - return None - LAST_FOUND = None - if not installed: - log.warning(f'{plugin_name}: installation aborted') + # Is the install request specifying a path to the plugin? + direct_location, name = location_from_name(name) + if direct_location: + logging.debug(f"install of {name} requested from {direct_location}") + src = InstInfo(name, direct_location, None) + if not src.get_inst_details(): + src = None + # Treating a local git repo as a directory allows testing + # uncommitted changes. + if src and src.srctype == Source.LOCAL_REPO: + src.srctype = Source.DIRECTORY + if not direct_location or not src: + log.debug(f"direct_location {direct_location}, src: {src}") + log.debug(f"Searching for {name}") + if search(name): + global LAST_FOUND + src = LAST_FOUND + src.commit = commit + log.debug(f'Retrieving {src.name} from {src.source_loc}') + else: return None - # Match case of the containing directory - for dirname in os.listdir(RECKLESS_CONFIG.reckless_dir): - if dirname.lower() == installed.name.lower(): - inst_path = Path(RECKLESS_CONFIG.reckless_dir) - inst_path = inst_path / dirname / installed.entry - RECKLESS_CONFIG.enable_plugin(inst_path) - enable(installed.name) - return f"{installed.source_loc}" - log.error(('dynamic activation failed: ' - f'{installed.name} not found in reckless directory')) + try: + installed = _install_plugin(src) + except FileExistsError as err: + log.error(f'File exists: {err.filename}') return None + LAST_FOUND = None + if not installed: + log.warning(f'{plugin_name}: installation aborted') + return None + + # Match case of the containing directory + for dirname in os.listdir(RECKLESS_CONFIG.reckless_dir): + if dirname.lower() == installed.name.lower(): + inst_path = Path(RECKLESS_CONFIG.reckless_dir) + inst_path = inst_path / dirname / installed.entry + RECKLESS_CONFIG.enable_plugin(inst_path) + enable(installed.name) + return f"{installed.source_loc}" + log.error(('dynamic activation failed: ' + f'{installed.name} not found in reckless directory')) return None @@ -1388,6 +1427,8 @@ def search(plugin_name: str) -> Union[InstInfo, None]: if srctype in [Source.DIRECTORY, Source.LOCAL_REPO, Source.GITHUB_REPO, Source.OTHER_URL]: found = _source_search(plugin_name, source) + if found: + log.debug(f"{found}, {found.srctype}") if not found: continue log.info(f"found {found.name} in source: {found.source_loc}") From 2e204101b6be33c06c3ccc7318af4fe951d2a656 Mon Sep 17 00:00:00 2001 From: Alex Myers Date: Tue, 22 Apr 2025 15:28:58 -0500 Subject: [PATCH 03/13] reckless: handle a direct source in the form of a git repo url --- tools/reckless | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/reckless b/tools/reckless index 1422516b469b..7d13e2b0e18e 100755 --- a/tools/reckless +++ b/tools/reckless @@ -1305,6 +1305,12 @@ def location_from_name(plugin_name: str) -> (str, str): if this looks like a filepath or URL and return that as well as the plugin name.""" if not Path(plugin_name).exists(): + try: + parsed = urlparse(plugin_name) + if parsed.scheme in ['http', 'https']: + return (plugin_name, Path(plugin_name).with_suffix('').name) + except ValueError: + pass # No path included, return the name only. return (None, plugin_name) @@ -1338,6 +1344,7 @@ def install(plugin_name: str) -> Union[str, None]: commit = None # Is the install request specifying a path to the plugin? direct_location, name = location_from_name(name) + src = None if direct_location: logging.debug(f"install of {name} requested from {direct_location}") src = InstInfo(name, direct_location, None) @@ -1348,7 +1355,6 @@ def install(plugin_name: str) -> Union[str, None]: if src and src.srctype == Source.LOCAL_REPO: src.srctype = Source.DIRECTORY if not direct_location or not src: - log.debug(f"direct_location {direct_location}, src: {src}") log.debug(f"Searching for {name}") if search(name): global LAST_FOUND From 3bf30d051df92f865962df80883b5dc3305f6aa3 Mon Sep 17 00:00:00 2001 From: Alex Myers Date: Sun, 4 May 2025 16:51:13 -0500 Subject: [PATCH 04/13] reckless: store absolute paths in metadata --- tools/reckless | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/reckless b/tools/reckless index 7d13e2b0e18e..3bc0984d6908 100755 --- a/tools/reckless +++ b/tools/reckless @@ -1099,12 +1099,16 @@ def add_installation_metadata(installed: InstInfo, updating the plugin.""" install_dir = Path(installed.source_loc) assert install_dir.is_dir() + if urlparse(original_request.source_loc).scheme in ['http', 'https']: + abs_source_path = original_request.source_loc + else: + abs_source_path = Path(original_request.source_loc).resolve() data = ('installation date\n' f'{datetime.date.today().isoformat()}\n' 'installation time\n' f'{int(time.time())}\n' 'original source\n' - f'{original_request.source_loc}\n' + f'{abs_source_path}\n' 'requested commit\n' f'{original_request.commit}\n' 'installed commit\n' From 7309dc83b9f5792a84a4ac2a5c00546798830d49 Mon Sep 17 00:00:00 2001 From: Alex Myers Date: Tue, 13 May 2025 12:06:10 -0500 Subject: [PATCH 05/13] pytest: test reckless direct install --- tests/test_reckless.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/test_reckless.py b/tests/test_reckless.py index 4a1e59a93ef7..e0244fa10659 100644 --- a/tests/test_reckless.py +++ b/tests/test_reckless.py @@ -235,9 +235,8 @@ def test_local_dir_install(node_factory): """Test search and install from local directory source.""" n = get_reckless_node(node_factory) n.start() - r = reckless([f"--network={NETWORK}", "-v", "source", "add", - os.path.join(n.lightning_dir, '..', 'lightningd', 'testplugpass')], - dir=n.lightning_dir) + source_dir = str(Path(n.lightning_dir / '..' / 'lightningd' / 'testplugpass').resolve()) + r = reckless([f"--network={NETWORK}", "-v", "source", "add", source_dir], dir=n.lightning_dir) assert r.returncode == 0 r = reckless([f"--network={NETWORK}", "-v", "install", "testplugpass"], dir=n.lightning_dir) assert r.returncode == 0 @@ -246,6 +245,15 @@ def test_local_dir_install(node_factory): print(plugin_path) assert os.path.exists(plugin_path) + # Retry with a direct install passing the full path to the local source + r = reckless(['uninstall', 'testplugpass', '-v'], dir=n.lightning_dir) + assert not os.path.exists(plugin_path) + r = reckless(['source', 'remove', source_dir], dir=n.lightning_dir) + assert 'plugin source removed' in r.stdout + r = reckless(['install', '-v', source_dir], dir=n.lightning_dir) + assert 'testplugpass enabled' in r.stdout + assert os.path.exists(plugin_path) + @unittest.skipIf(VALGRIND, "virtual environment triggers memleak detection") def test_disable_enable(node_factory): From 4a061a45f78ddf3e57d1bb530284ea703c3dd9e0 Mon Sep 17 00:00:00 2001 From: Alex Myers Date: Tue, 13 May 2025 12:42:08 -0500 Subject: [PATCH 06/13] doc: update reckless documentation for direct install --- doc/lightning-reckless.1.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/lightning-reckless.1.md b/doc/lightning-reckless.1.md index f645ff232f32..c9eaabcaad95 100644 --- a/doc/lightning-reckless.1.md +++ b/doc/lightning-reckless.1.md @@ -16,6 +16,11 @@ lightningd config file. Reckless does all of these by invoking: **reckless** **install**[@*commit/tag*] *plugin\_name* +Alternatively, the source path or URL to the plugin repository can be +passed directly in place of the *plugin\_name*. In either case, the +containing directory or repository should be named for the plugin, don't +pass the plugin's executable/entrypoint directly. + reckless will exit early in the event that: - the plugin is not found in any available source repositories @@ -81,7 +86,7 @@ Available option flags: NOTES ----- -Reckless currently supports python and javascript plugins. +Reckless currently supports python, rust, and javascript plugins. Running the first time will prompt the user that their lightningd's bitcoin config will be appended (or created) to inherit the reckless From 8d82dd024c63f0a7f73c2029a42e217ec2b95081 Mon Sep 17 00:00:00 2001 From: Alex Myers Date: Sun, 4 May 2025 16:21:42 -0500 Subject: [PATCH 07/13] reckless: refactor install remove the duplicative search and extract the enable portion for use next. --- tools/reckless | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/tools/reckless b/tools/reckless index 3bc0984d6908..0c77fcd36468 100755 --- a/tools/reckless +++ b/tools/reckless @@ -1334,6 +1334,20 @@ def location_from_name(plugin_name: str) -> (str, str): return (Path(plugin_name).parent.parent, Path(plugin_name).with_suffix('').name) +def _enable_installed(installed: InstInfo, plugin_name: str) -> Union[str, None]: + """Enable the plugin in the active config file and dynamically activate + if a lightningd rpc is available.""" + if not installed: + log.warning(f'{plugin_name}: installation aborted') + return None + + if enable(installed.name): + return f"{installed.source_loc}" + + log.error(('dynamic activation failed: ' + f'{installed.name} not found in reckless directory')) + return None + def install(plugin_name: str) -> Union[str, None]: """Downloads plugin from source repos, installs and activates plugin. Returns the location of the installed plugin or "None" in the case of @@ -1363,9 +1377,11 @@ def install(plugin_name: str) -> Union[str, None]: if search(name): global LAST_FOUND src = LAST_FOUND + LAST_FOUND = None src.commit = commit log.debug(f'Retrieving {src.name} from {src.source_loc}') else: + LAST_FOUND = None return None try: @@ -1373,22 +1389,8 @@ def install(plugin_name: str) -> Union[str, None]: except FileExistsError as err: log.error(f'File exists: {err.filename}') return None - LAST_FOUND = None - if not installed: - log.warning(f'{plugin_name}: installation aborted') - return None + return _enable_installed(installed, plugin_name) - # Match case of the containing directory - for dirname in os.listdir(RECKLESS_CONFIG.reckless_dir): - if dirname.lower() == installed.name.lower(): - inst_path = Path(RECKLESS_CONFIG.reckless_dir) - inst_path = inst_path / dirname / installed.entry - RECKLESS_CONFIG.enable_plugin(inst_path) - enable(installed.name) - return f"{installed.source_loc}" - log.error(('dynamic activation failed: ' - f'{installed.name} not found in reckless directory')) - return None def uninstall(plugin_name: str) -> str: From 71bf2f07bd4b64da12ce49d1f5f2e3c76adbcd32 Mon Sep 17 00:00:00 2001 From: Alex Myers Date: Mon, 28 Apr 2025 12:35:21 -0500 Subject: [PATCH 08/13] reckless: add update command This updates all reckless-installed plugins with `reckless update` or update individual plugins by passing the plugin names as arguments. The metadata stored with the installed plugin is used to find the plugin from the appropriate source (the same source is used as when originally installed.) Changelog-Added: Reckless: `reckless update` updates all reckless-installed plugins. --- doc/lightning-reckless.1.md | 5 +++ tools/reckless | 79 ++++++++++++++++++++++++++++++++----- 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/doc/lightning-reckless.1.md b/doc/lightning-reckless.1.md index c9eaabcaad95..d8820bd10533 100644 --- a/doc/lightning-reckless.1.md +++ b/doc/lightning-reckless.1.md @@ -58,6 +58,11 @@ Other commands include: **reckless** **source** **rm** *repo\_url* remove a plugin repo for reckless to search. +**reckless** **update** *[plugin\_name]* + install the latest commit of a single plugin, or omit to update all + reckless-installed plugins. Does not automatically update if a plugin + was previously installed by requesting a specific git tag or commit. + OPTIONS ------- diff --git a/tools/reckless b/tools/reckless index 0c77fcd36468..6eb9ce8b9d6e 100755 --- a/tools/reckless +++ b/tools/reckless @@ -1667,6 +1667,57 @@ def list_source(): return sources_from_file() +def update_plugin(plugin_name: str) -> Union[str, None]: + """Check for an installed plugin, if metadata for it exists, update + to the latest available while using the same source.""" + log.info(f"updating {plugin_name}") + metadata_file = Path(RECKLESS_CONFIG.reckless_dir) / plugin_name / '.metadata' + if not metadata_file.exists(): + log.warning(f"no metadata file for {plugin_name}") + return + + metadata = {'installation date': None, + 'installation time': None, + 'original source': None, + 'requested commit': None, + 'installed commit': None, + } + with open(metadata_file, "r") as meta: + metadata_lines = meta.readlines() + for line_no, line in enumerate(metadata_lines): + if line_no > 0 and metadata_lines[line_no - 1].strip() in metadata: + metadata.update({metadata_lines[line_no - 1].strip(): line.strip()}) + for key in metadata: + if metadata[key].lower() == 'none': + metadata[key] = None + log.debug(f'{plugin_name} installation metadata: {str(metadata)}') + + src = InstInfo(plugin_name, + metadata['original source'], None) + uninstall(plugin_name) + try: + installed = _install_plugin(src) + except FileExistsError as err: + log.error(f'File exists: {err.filename}') + return None + return _enable_installed(installed, plugin_name) + + +def update_plugins(plugin_name: str): + """user requested plugin upgrade(s)""" + if plugin_name: + update_plugin(plugin_name) + return + + log.info("updating all plugins") + for plugin in os.listdir(RECKLESS_CONFIG.reckless_dir): + if not (Path(RECKLESS_CONFIG.reckless_dir) / plugin).is_dir(): + continue + if len(plugin) > 0 and plugin[0] == '.': + continue + update_plugin(plugin) + + def report_version() -> str: """return reckless version""" log.info(__VERSION__) @@ -1762,6 +1813,9 @@ if __name__ == '__main__': 'repository') source_rem.add_argument('targets', type=str, nargs='*') source_rem.set_defaults(func=remove_source) + update = cmd1.add_parser('update', help='update plugins to lastest version') + update.add_argument('targets', type=str, nargs='*') + update.set_defaults(func=update_plugins) help_cmd = cmd1.add_parser('help', help='for contextual help, use ' '"reckless -h"') @@ -1772,7 +1826,8 @@ if __name__ == '__main__': help='print version and exit') all_parsers = [parser, install_cmd, uninstall_cmd, search_cmd, enable_cmd, - disable_cmd, list_parse, source_add, source_rem, help_cmd] + disable_cmd, list_parse, source_add, source_rem, help_cmd, + update] for p in all_parsers: # This default depends on the .lightning directory p.add_argument('-d', '--reckless-dir', action=StoreIdempotent, @@ -1853,19 +1908,25 @@ if __name__ == '__main__': if 'GITHUB_API_FALLBACK' in os.environ: GITHUB_API_FALLBACK = os.environ['GITHUB_API_FALLBACK'] - if 'targets' in args: - # FIXME: Catch missing argument + if 'targets' in args: # and len(args.targets) > 0: if args.func.__name__ == 'help_alias': args.func(args.targets) sys.exit(0) + # Catch a missing argument so that we can overload functions. + if len(args.targets) == 0: + args.targets=[None] for target in args.targets: # Accept single item arguments, or a json array - target_list = unpack_json_arg(target) - if target_list: - for tar in target_list: - log.add_result(args.func(tar)) - else: - log.add_result(args.func(target)) + try: + target_list = unpack_json_arg(target) + if target_list: + for tar in target_list: + log.add_result(args.func(tar)) + else: + log.add_result(args.func(target)) + except TypeError: + if len(args.targets) == 1: + log.add_result(args.func(target)) elif 'func' in args: log.add_result(args.func()) From 81ba48fcacc5dc63daa025a1b5f348d670ae4cb1 Mon Sep 17 00:00:00 2001 From: Alex Myers Date: Sun, 4 May 2025 16:24:16 -0500 Subject: [PATCH 09/13] reckless: provide user feedback at info level if enable fails --- tools/reckless | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/reckless b/tools/reckless index 6eb9ce8b9d6e..5ecd859f13e0 100755 --- a/tools/reckless +++ b/tools/reckless @@ -1522,8 +1522,8 @@ def enable(plugin_name: str): log.error(err) return None except RPCError: - log.debug(('lightningd rpc unavailable. ' - 'Skipping dynamic activation.')) + log.info(('lightningd rpc unavailable. ' + 'Skipping dynamic activation.')) RECKLESS_CONFIG.enable_plugin(path) log.info(f'{inst.name} enabled') return 'enabled' From 58173451e1807c4f4d941d14a8896d3a6eb4259e Mon Sep 17 00:00:00 2001 From: Alex Myers Date: Mon, 5 May 2025 10:08:05 -0500 Subject: [PATCH 10/13] reckless: return result from update --- tools/reckless | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tools/reckless b/tools/reckless index 5ecd859f13e0..7d98235eb797 100755 --- a/tools/reckless +++ b/tools/reckless @@ -1674,7 +1674,7 @@ def update_plugin(plugin_name: str) -> Union[str, None]: metadata_file = Path(RECKLESS_CONFIG.reckless_dir) / plugin_name / '.metadata' if not metadata_file.exists(): log.warning(f"no metadata file for {plugin_name}") - return + return None metadata = {'installation date': None, 'installation time': None, @@ -1706,16 +1706,20 @@ def update_plugin(plugin_name: str) -> Union[str, None]: def update_plugins(plugin_name: str): """user requested plugin upgrade(s)""" if plugin_name: - update_plugin(plugin_name) - return + installed = update_plugin(plugin_name) + if not installed: + log.error(f'{plugin_name} update aborted') + return installed log.info("updating all plugins") + update_results = [] for plugin in os.listdir(RECKLESS_CONFIG.reckless_dir): if not (Path(RECKLESS_CONFIG.reckless_dir) / plugin).is_dir(): continue if len(plugin) > 0 and plugin[0] == '.': continue - update_plugin(plugin) + update_results.append(update_plugin(plugin)) + return update_results def report_version() -> str: From 646e00ddd9d80bc26dac2271f93743be3d39036e Mon Sep 17 00:00:00 2001 From: Alex Myers Date: Mon, 5 May 2025 13:48:36 -0500 Subject: [PATCH 11/13] reckless: only proceed with update when appropriate --- tools/reckless | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tools/reckless b/tools/reckless index 7d98235eb797..775078c4c80f 100755 --- a/tools/reckless +++ b/tools/reckless @@ -206,6 +206,36 @@ class InstInfo: return (f'InstInfo({self.name}, {self.source_loc}, {self.git_url}, ' f'{self.entry}, {self.deps}, {self.subdir})') + def get_repo_commit(self) -> Union[str, None]: + """The latest commit from a remote repo or the HEAD of a local repo.""" + if self.srctype in [Source.LOCAL_REPO, Source.GIT_LOCAL_CLONE]: + git = run(['git', 'rev-parse', 'HEAD'], cwd=str(self.source_loc), + stdout=PIPE, stderr=PIPE, text=True, check=False, timeout=10) + if git.returncode != 0: + return None + return git.stdout.splitlines()[0] + + if self.srctype == Source.GITHUB_REPO: + parsed_url = urlparse(self.source_loc) + if 'github.com' not in parsed_url.netloc: + return None + if len(parsed_url.path.split('/')) < 2: + return None + start = 1 + # Maybe we were passed an api.github.com/repo/ url + if 'api' in parsed_url.netloc: + start += 1 + repo_user = parsed_url.path.split('/')[start] + repo_name = parsed_url.path.split('/')[start + 1] + api_url = f'{API_GITHUB_COM}/repos/{repo_user}/{repo_name}/commits?ref=HEAD' + r = urlopen(api_url, timeout=5) + if r.status != 200: + return None + try: + return json.loads(r.read().decode())['0']['sha'] + except: + return None + def get_inst_details(self) -> bool: """Search the source_loc for plugin install details. This may be necessary if a contents api is unavailable. @@ -1694,6 +1724,15 @@ def update_plugin(plugin_name: str) -> Union[str, None]: src = InstInfo(plugin_name, metadata['original source'], None) + if not src.get_inst_details(): + log.error(f'cannot locate {plugin_name} in original source {metadata["original_source"]}') + return None + repo_commit = src.get_repo_commit() + if not repo_commit: + log.debug('source commit not available') + if repo_commit and repo_commit == metadata['installed commit']: + log.debug(f'Installed {plugin_name} is already latest - {repo_commit}') + return None uninstall(plugin_name) try: installed = _install_plugin(src) From aef4791116c85d88aeca122a7cee572ba4f64323 Mon Sep 17 00:00:00 2001 From: Alex Myers Date: Mon, 5 May 2025 16:41:18 -0500 Subject: [PATCH 12/13] reckless: don't return error if update is unnecessary --- tools/reckless | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/tools/reckless b/tools/reckless index 775078c4c80f..4ba0682258b8 100755 --- a/tools/reckless +++ b/tools/reckless @@ -1697,14 +1697,25 @@ def list_source(): return sources_from_file() -def update_plugin(plugin_name: str) -> Union[str, None]: +class UpdateStatus(Enum): + SUCCESS = 0 + LATEST = 1 + UNINSTALLED = 2 + ERROR = 3 + METADATA_MISSING = 4 + + +def update_plugin(plugin_name: str) -> tuple: """Check for an installed plugin, if metadata for it exists, update to the latest available while using the same source.""" log.info(f"updating {plugin_name}") + if not (Path(RECKLESS_CONFIG.reckless_dir) / plugin_name).exists(): + log.error(f'{plugin_name} is not installed') + return (None, UpdateStatus.UNINSTALLED) metadata_file = Path(RECKLESS_CONFIG.reckless_dir) / plugin_name / '.metadata' if not metadata_file.exists(): log.warning(f"no metadata file for {plugin_name}") - return None + return (None, UpdateStatus.METADATA_MISSING) metadata = {'installation date': None, 'installation time': None, @@ -1726,29 +1737,34 @@ def update_plugin(plugin_name: str) -> Union[str, None]: metadata['original source'], None) if not src.get_inst_details(): log.error(f'cannot locate {plugin_name} in original source {metadata["original_source"]}') - return None + return (None, UpdateStatus.ERROR) repo_commit = src.get_repo_commit() if not repo_commit: log.debug('source commit not available') + else: + log.debug(f'source commit: {repo_commit}') if repo_commit and repo_commit == metadata['installed commit']: - log.debug(f'Installed {plugin_name} is already latest - {repo_commit}') - return None + log.info(f'Installed {plugin_name} is already latest @{repo_commit}') + return (None, UpdateStatus.LATEST) uninstall(plugin_name) try: installed = _install_plugin(src) except FileExistsError as err: log.error(f'File exists: {err.filename}') - return None - return _enable_installed(installed, plugin_name) + return (None, UpdateStatus.ERROR) + result = _enable_installed(installed, plugin_name) + if result: + return (result, UpdateStatus.SUCCESS) + return (result, UpdateStatus.ERROR) def update_plugins(plugin_name: str): """user requested plugin upgrade(s)""" if plugin_name: installed = update_plugin(plugin_name) - if not installed: + if not installed[0] and installed[1] != UpdateStatus.LATEST: log.error(f'{plugin_name} update aborted') - return installed + return installed[0] log.info("updating all plugins") update_results = [] @@ -1757,7 +1773,7 @@ def update_plugins(plugin_name: str): continue if len(plugin) > 0 and plugin[0] == '.': continue - update_results.append(update_plugin(plugin)) + update_results.append(update_plugin(plugin)[0]) return update_results From ad744d195141b29da4d20535c6e86658892aaf5f Mon Sep 17 00:00:00 2001 From: Alex Myers Date: Mon, 12 May 2025 17:01:53 -0500 Subject: [PATCH 13/13] reckless: don't update a plugin if a specific tag was previously installed --- tools/reckless | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/reckless b/tools/reckless index 4ba0682258b8..36586d5806e6 100755 --- a/tools/reckless +++ b/tools/reckless @@ -1703,6 +1703,7 @@ class UpdateStatus(Enum): UNINSTALLED = 2 ERROR = 3 METADATA_MISSING = 4 + REFUSING_UPDATE = 5 def update_plugin(plugin_name: str) -> tuple: @@ -1731,7 +1732,10 @@ def update_plugin(plugin_name: str) -> tuple: for key in metadata: if metadata[key].lower() == 'none': metadata[key] = None - log.debug(f'{plugin_name} installation metadata: {str(metadata)}') + log.debug(f'{plugin_name} previous installation metadata: {str(metadata)}') + if metadata['requested commit']: + log.warning(f'refusing to upgrade {plugin_name}@{metadata["requested commit"]} due to previously requested tag/commit') + return (None, UpdateStatus.REFUSING_UPDATE) src = InstInfo(plugin_name, metadata['original source'], None)