Skip to content

Commit 3d2cd8d

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 3d2cd8d

5 files changed

Lines changed: 141 additions & 21 deletions

File tree

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: 113 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,84 @@ 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.debug(f'ingesting manifest from {source.name}: {source.location}/manifest.json ({source.srctype})')
2101+
# local_path = RECKLESS_DIR / '.remote_sources' / user
2102+
if source.srctype not in [Source.GIT_LOCAL_CLONE, Source.LOCAL_REPO, Source.DIRECTORY]:
2103+
log.info(f'oops! {source.srctype}')
2104+
return None
2105+
if source.srctype == Source.GIT_LOCAL_CLONE:
2106+
try:
2107+
repo = GithubRepository(source.parent_source.original_source)
2108+
path = RECKLESS_DIR / '.remote_sources' / repo.user / repo.name
2109+
except AssertionError:
2110+
log.info(f'could not parse github source {source.parent_source.original_source}')
2111+
return None
2112+
elif source.srctype in [Source.DIRECTORY, Source.LOCAL_REPO]:
2113+
path = Path(source.location)
2114+
else:
2115+
raise Exception(f"cannot access manifest in {source.srctype}: {source}")
2116+
if source.relative:
2117+
path = path / source.relative
2118+
path = path / 'manifest.json'
2119+
if not path.exists():
2120+
return None
2121+
with open(path, 'r+') as manifest_file:
2122+
try:
2123+
manifest = json.loads(manifest_file.read())
2124+
return manifest
2125+
except json.decoder.JSONDecodeError:
2126+
log.warning(f'{source.name} contains malformed manifest ({source.parent_source.original_source})')
2127+
return None
2128+
2129+
2130+
def find_plugin_candidates(source: Union[LoadedSource, SourceDir], depth=2) -> list:
20712131
"""Filter through a source and return any candidates that appear to be
20722132
installable plugins with the registered installers."""
2133+
if isinstance(source, LoadedSource):
2134+
if source.local_clone:
2135+
return find_plugin_candidates(source.local_clone)
2136+
return find_plugin_candidates(source.content)
2137+
20732138
candidates = []
20742139
assert isinstance(source, SourceDir)
20752140
if not source.contents and not source.prepopulated:
20762141
source.populate()
2142+
for s in source.contents:
2143+
if isinstance(s, SourceDir):
2144+
assert s.srctype == source.srctype, f'source dir {s.name}, {s.srctype} did not inherit {source.srctype} from {source.name}'
2145+
assert s.parent_source == source.parent_source, f'source dir {s.name} did not inherit parent {source.parent_source} from {source.name}'
20772146

20782147
guess = InstInfo(source.name, source.location, None, source_dir=source)
2148+
guess.srctype = source.srctype
2149+
manifest = None
20792150
if guess.get_inst_details():
2080-
candidates.append(source.name)
2151+
guess.srctype = source.srctype
2152+
guess.source_dir.srctype = source.srctype
2153+
if guess.source_dir.find('manifest.json'):
2154+
# FIXME: Handle github source case
2155+
if have_files(guess.source_dir):
2156+
manifest = fetch_manifest(guess.source_dir)
2157+
2158+
if manifest:
2159+
candidate = manifest
2160+
else:
2161+
candidate = {'name': source.name,
2162+
'short_description': None,
2163+
'long_description': None,
2164+
'entrypoint': guess.entry,
2165+
'requirements': []}
2166+
candidates.append(candidate)
20812167
if depth <= 1:
20822168
return candidates
20832169

@@ -2106,21 +2192,27 @@ def available_plugins() -> list:
21062192
source.original_source,
21072193
source_dir=source.content),
21082194
verbose=False)
2195+
clone.srctype = Source.GIT_LOCAL_CLONE
2196+
clone.parent_source = source
21092197
if not clone:
21102198
log.warning(f"could not clone github source {source.original_source}")
21112199
continue
21122200
source.local_clone = clone
21132201
source.local_clone.parent_source = source
21142202

2115-
if source.local_clone:
2116-
candidates.extend(find_plugin_candidates(source.local_clone))
2117-
else:
2118-
candidates.extend(find_plugin_candidates(source.content))
2203+
candidates.extend(find_plugin_candidates(source))
2204+
2205+
# json output requested
2206+
if log.capture:
2207+
return candidates
2208+
2209+
for c in candidates:
2210+
log.info(c['name'])
2211+
if c['short_description']:
2212+
log.info(f'\tdescription: {c["short_description"]}')
2213+
if c['requirements']:
2214+
log.info(f'\trequirements: {c["requirements"]}')
21192215

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

21262218

0 commit comments

Comments
 (0)