Skip to content

Commit ac83556

Browse files
reckless: check for manifest.json in plugin directories
The manifest.json provides a short and long description of the plugin, dependencies, and specifies the entrypoint in case it's not named the same as the plugin. changelog-changed: Reckless uses a manifest in the plugin directory to gain additional details about plugin and installation.
1 parent 19669c8 commit ac83556

File tree

5 files changed

+137
-21
lines changed

5 files changed

+137
-21
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "testplugfail",
3+
"short_description": "a plugin to test reckless installation where the plugin fails to start",
4+
"long_description": "This plugin is one of several used in the reckless blackbox tests.",
5+
"entrypoint": "testplugfail.py",
6+
"requirements": ["python3"]
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "testplugpass",
3+
"short_description": "a plugin to test reckless installation",
4+
"long_description": "This plugin is one of several used in the reckless blackbox tests. This one should success in dependenciy installation, and start up when activated in Core Lightning.",
5+
"entrypoint": "testplugpass.py",
6+
"requirements": ["python3"]
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "testplugpyproj",
3+
"short_description": "a plugin to test reckless installation",
4+
"long_description": "This plugin is one of several used in the reckless blackbox tests. This one should succeed while specifying dependencies in pyproject.toml.",
5+
"entrypoint": "testplugpyproj.py",
6+
"requirements": ["python3"]
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "testpluguv",
3+
"short_description": "a plugin to test reckless installation using uv",
4+
"long_description": "This plugin is one of several used in the reckless blackbox tests. This one specifies dependencies for uv in the pyproject.toml and has a corresponding uv.lock file.",
5+
"entrypoint": "testpluguv.py",
6+
"requirements": ["python3"]
7+
}

tools/reckless

Lines changed: 109 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,20 @@ def remove_dir(directory: str) -> bool:
233233
return False
234234

235235

236+
class GithubRepository():
237+
"""extract the github user account and repository name."""
238+
def __init__(self, url: str):
239+
assert 'github.com/' in url.lower()
240+
url_parts = Path(str(url).lower().partition('github.com/')[2]).parts
241+
assert len(url_parts) >= 2
242+
self.user = url_parts[0]
243+
self.name = url_parts[1].removesuffix('.git')
244+
self.url = url
245+
246+
def __repr__(self):
247+
return '<GithubRepository {self.user}/{self.repo}>'
248+
249+
236250
class Source(Enum):
237251
DIRECTORY = 1
238252
LOCAL_REPO = 2
@@ -262,12 +276,11 @@ class Source(Enum):
262276
@classmethod
263277
def get_github_user_repo(cls, source: str) -> (str, str):
264278
'extract a github username and repository name'
265-
if 'github.com/' not in source.lower():
266-
return None, None
267-
trailing = Path(source.lower().partition('github.com/')[2]).parts
268-
if len(trailing) < 2:
279+
try:
280+
repo = GithubRepository(source)
281+
return repo.user, repo.name
282+
except:
269283
return None, None
270-
return trailing[0], trailing[1].removesuffix('.git')
271284

272285

273286
class SubmoduleSource:
@@ -325,7 +338,7 @@ class SourceDir():
325338
if self.srctype == Source.DIRECTORY:
326339
self.contents = populate_local_dir(self.location)
327340
elif self.srctype in [Source.LOCAL_REPO, Source.GIT_LOCAL_CLONE]:
328-
self.contents = populate_local_repo(self.location, parent_source=self.parent_source)
341+
self.contents = populate_local_repo(self.location, parent=self, parent_source=self.parent_source)
329342
elif self.srctype == Source.GITHUB_REPO:
330343
self.contents = populate_github_repo(self.location)
331344
else:
@@ -351,7 +364,7 @@ class SourceDir():
351364
return None
352365

353366
def __repr__(self):
354-
return f"<SourceDir: {self.name}, {self.location}, {self.relative}>"
367+
return f"<SourceDir: {self.name}, {self.location}, {self.relative}, {self.parent_source}>"
355368

356369
def __eq__(self, compared):
357370
if isinstance(compared, str):
@@ -576,7 +589,8 @@ def populate_local_repo(path: str, parent=None, parent_source=None) -> list:
576589
parentdir.name)
577590
else:
578591
relative_path = parentdir.name
579-
child = SourceDir(p, srctype=Source.LOCAL_REPO,
592+
child = SourceDir(p, srctype=parent.srctype,
593+
parent_source=parent_source,
580594
relative=relative_path)
581595
# ls-tree lists every file in the repo with full path.
582596
# No need to populate each directory individually.
@@ -611,8 +625,13 @@ def populate_local_repo(path: str, parent=None, parent_source=None) -> list:
611625
relative_path = filepath
612626
elif basedir.relative:
613627
relative_path = str(Path(basedir.relative) / filepath)
614-
assert relative_path
615-
submodule_dir = SourceDir(filepath, srctype=Source.LOCAL_REPO,
628+
else:
629+
relative_path = filepath
630+
if parent:
631+
srctype = parent.srctype
632+
else:
633+
srctype = Source.LOCAL_REPO
634+
submodule_dir = SourceDir(filepath, srctype=srctype,
616635
relative=relative_path,
617636
parent_source=parent_source)
618637
populate_local_repo(Path(path) / filepath, parent=submodule_dir,
@@ -710,7 +729,7 @@ def populate_github_repo(url: str) -> list:
710729
return contents
711730

712731

713-
def copy_remote_git_source(github_source: InstInfo, verbose: bool=True):
732+
def copy_remote_git_source(github_source: InstInfo, verbose: bool=True) -> SourceDir:
714733
"""clone or fetch & checkout a local copy of a remote git repo"""
715734
user, repo = Source.get_github_user_repo(github_source.source_loc)
716735
if not user or not repo:
@@ -2067,17 +2086,80 @@ def listinstalled():
20672086
return plugins
20682087

20692088

2070-
def find_plugin_candidates(source: SourceDir, depth=2) -> list:
2089+
def have_files(source: SourceDir):
2090+
"""Do we have direct access to the files in this directory?"""
2091+
if source.srctype in [Source.DIRECTORY, Source.LOCAL_REPO,
2092+
Source.GIT_LOCAL_CLONE]:
2093+
return True
2094+
log.info(f'no files in {source.name} ({source.srctype})')
2095+
return False
2096+
2097+
2098+
def fetch_manifest(source: SourceDir) -> dict:
2099+
"""read and ingest a manifest from the provided source."""
2100+
log.info(f'ingesting manifest from {source.name}: {source.location}/manifest.json ({source.srctype})')
2101+
# local_path = RECKLESS_DIR / '.remote_sources' / user
2102+
if source.srctype is Source.GIT_LOCAL_CLONE:
2103+
try:
2104+
repo = GithubRepository(source.parent_source.original_source)
2105+
path = RECKLESS_DIR / '.remote_sources' / repo.user / repo.name
2106+
if source.relative:
2107+
path = path / source.relative
2108+
path = path / 'manifest.json'
2109+
if not path.exists():
2110+
return None
2111+
with open(path, 'r+') as manifest_file:
2112+
try:
2113+
manifest = json.loads(manifest_file.read())
2114+
return manifest
2115+
except json.decoder.JSONDecodeError:
2116+
log.warning(f'{source.name} contains malformed manifest ({source.parent_source.original_source})')
2117+
return None
2118+
except AssertionError:
2119+
log.info(f'could not parse github source {source.parent_source.original_source}')
2120+
return None
2121+
else:
2122+
log.info(f'oops! {source.srctype}')
2123+
return None
2124+
2125+
2126+
def find_plugin_candidates(source: Union[LoadedSource, SourceDir], depth=2) -> list:
20712127
"""Filter through a source and return any candidates that appear to be
20722128
installable plugins with the registered installers."""
2129+
if isinstance(source, LoadedSource):
2130+
if source.local_clone:
2131+
return find_plugin_candidates(source.local_clone)
2132+
return find_plugin_candidates(source.content)
2133+
20732134
candidates = []
20742135
assert isinstance(source, SourceDir)
20752136
if not source.contents and not source.prepopulated:
20762137
source.populate()
2138+
for s in source.contents:
2139+
if isinstance(s, SourceDir):
2140+
assert s.srctype == source.srctype, f'source dir {s.name}, {s.srctype} did not inherit {source.srctype} from {source.name}'
2141+
assert s.parent_source == source.parent_source, f'source dir {s.name} did not inherit parent {source.parent_source} from {source.name}'
20772142

20782143
guess = InstInfo(source.name, source.location, None, source_dir=source)
2144+
guess.srctype = source.srctype
2145+
manifest = None
20792146
if guess.get_inst_details():
2080-
candidates.append(source.name)
2147+
guess.srctype = source.srctype
2148+
guess.source_dir.srctype = source.srctype
2149+
if guess.source_dir.find('manifest.json'):
2150+
# FIXME: Handle github source case
2151+
if have_files(guess.source_dir):
2152+
manifest = fetch_manifest(guess.source_dir)
2153+
2154+
if manifest:
2155+
candidate = manifest
2156+
else:
2157+
candidate = {'name': source.name,
2158+
'short_description': None,
2159+
'long_description': None,
2160+
'entrypoint': guess.entry,
2161+
'requirements': []}
2162+
candidates.append(candidate)
20812163
if depth <= 1:
20822164
return candidates
20832165

@@ -2106,21 +2188,27 @@ def available_plugins() -> list:
21062188
source.original_source,
21072189
source_dir=source.content),
21082190
verbose=False)
2191+
clone.srctype = Source.GIT_LOCAL_CLONE
2192+
clone.parent_source = source
21092193
if not clone:
21102194
log.warning(f"could not clone github source {source.original_source}")
21112195
continue
21122196
source.local_clone = clone
21132197
source.local_clone.parent_source = source
21142198

2115-
if source.local_clone:
2116-
candidates.extend(find_plugin_candidates(source.local_clone))
2117-
else:
2118-
candidates.extend(find_plugin_candidates(source.content))
2199+
candidates.extend(find_plugin_candidates(source))
2200+
2201+
# json output requested
2202+
if log.capture:
2203+
return candidates
2204+
2205+
for c in candidates:
2206+
log.info(c['name'])
2207+
if c['short_description']:
2208+
log.info(f' description: {c["short_description"]}')
2209+
if c['requirements']:
2210+
log.info(f' requirements: {c["short_description"]}')
21192211

2120-
# Order and deduplicate results
2121-
candidates = list(set(candidates))
2122-
candidates.sort()
2123-
log.info(' '.join(candidates))
21242212
return candidates
21252213

21262214

0 commit comments

Comments
 (0)