Skip to content

Reckless direct install #8241

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion doc/lightning-reckless.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 11 additions & 4 deletions tests/test_reckless.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
108 changes: 81 additions & 27 deletions tools/reckless
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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-...
Expand Down Expand Up @@ -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
Expand All @@ -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


Expand Down Expand Up @@ -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}")
Expand Down
Loading