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 diff --git a/tests/test_reckless.py b/tests/test_reckless.py index 3615b71a5518..e0244fa10659 100644 --- a/tests/test_reckless.py +++ b/tests/test_reckless.py @@ -231,14 +231,12 @@ 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) 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 @@ -247,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): diff --git a/tools/reckless b/tools/reckless index 7b692d391e0d..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' @@ -1175,10 +1179,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-... @@ -1297,6 +1304,36 @@ 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(): + 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) + + # 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 @@ -1309,33 +1346,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) + src = None + 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"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 @@ -1385,6 +1437,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}")