@@ -157,6 +157,9 @@ class Installer:
157157 self .manager = manager # dependency manager (if required)
158158 self .dependency_file = None
159159 self .dependency_call = None
160+ # extra check routine to see if a source is installable by this Installer
161+ self .check = None
162+
160163
161164 def __repr__ (self ):
162165 return (f'<Installer { self .name } : '
@@ -175,7 +178,7 @@ class Installer:
175178 return False
176179 return True
177180
178- def installable (self ) -> bool :
181+ def installable (self , source ) -> bool :
179182 '''Validate the necessary compiler and package manager executables are
180183 available to install. If these are defined, they are considered
181184 mandatory even though the user may have the requisite packages already
@@ -184,6 +187,8 @@ class Installer:
184187 return False
185188 if self .manager and not shutil .which (self .manager ):
186189 return False
190+ if self .check :
191+ return self .check (source )
187192 return True
188193
189194 def add_entrypoint (self , entry : str ):
@@ -484,9 +489,14 @@ class InstInfo:
484489 found_entry = sub .find (g , ftype = SourceFile )
485490 if found_entry :
486491 break
487- # FIXME: handle a list of dependencies
488- found_dep = sub .find (inst .dependency_file ,
489- ftype = SourceFile )
492+
493+ if inst .dependency_file :
494+ # FIXME: handle a list of dependencies
495+ found_dep = sub .find (inst .dependency_file ,
496+ ftype = SourceFile )
497+ else :
498+ found_dep = None
499+
490500 if found_entry :
491501 # Success!
492502 if found_dep :
@@ -1031,6 +1041,47 @@ def install_to_python_virtual_environment(cloned_plugin: InstInfo):
10311041 return cloned_plugin
10321042
10331043
1044+ def have_files (source : SourceDir ):
1045+ """Do we have direct access to the files in this directory?"""
1046+ if source .srctype in [Source .DIRECTORY , Source .LOCAL_REPO ,
1047+ Source .GIT_LOCAL_CLONE ]:
1048+ return True
1049+ log .info (f'no files in { source .name } ({ source .srctype } )' )
1050+ return False
1051+
1052+
1053+ def fetch_manifest (source : SourceDir ) -> dict :
1054+ """read and ingest a manifest from the provided source."""
1055+ log .debug (f'ingesting manifest from { source .name } : { source .location } /manifest.json ({ source .srctype } )' )
1056+ # local_path = RECKLESS_DIR / '.remote_sources' / user
1057+ if source .srctype not in [Source .GIT_LOCAL_CLONE , Source .LOCAL_REPO , Source .DIRECTORY ]:
1058+ log .info (f'oops! { source .srctype } ' )
1059+ return None
1060+ if source .srctype == Source .GIT_LOCAL_CLONE :
1061+ try :
1062+ repo = GithubRepository (source .parent_source .original_source )
1063+ path = RECKLESS_DIR / '.remote_sources' / repo .user / repo .name
1064+ except AssertionError :
1065+ log .info (f'could not parse github source { source .parent_source .original_source } ' )
1066+ return None
1067+ elif source .srctype in [Source .DIRECTORY , Source .LOCAL_REPO ]:
1068+ path = Path (source .location )
1069+ else :
1070+ raise Exception (f"cannot access manifest in { source .srctype } : { source } " )
1071+ if source .relative :
1072+ path = path / source .relative
1073+ path = path / 'manifest.json'
1074+ if not path .exists ():
1075+ return None
1076+ with open (path , 'r+' ) as manifest_file :
1077+ try :
1078+ manifest = json .loads (manifest_file .read ())
1079+ return manifest
1080+ except json .decoder .JSONDecodeError :
1081+ log .warning (f'{ source .name } contains malformed manifest ({ source .parent_source .original_source } )' )
1082+ return None
1083+
1084+
10341085def cargo_installation (cloned_plugin : InstInfo ):
10351086 call = ['cargo' , 'build' , '--release' , '-vv' ]
10361087 # FIXME: the symlinked Cargo.toml allows the installer to identify a valid
@@ -1144,6 +1195,60 @@ def install_python_uv_legacy(cloned_plugin: InstInfo):
11441195 return cloned_plugin
11451196
11461197
1198+ def open_source_entrypoint (source : InstInfo ) -> str :
1199+ if source .srctype not in [Source .GIT_LOCAL_CLONE , Source .LOCAL_REPO , Source .DIRECTORY ]:
1200+ log .info (f'oops! { source .srctype } ' )
1201+ return None
1202+ assert source .entry
1203+ file = Path (source .source_loc )
1204+ # if source.subdir:
1205+ # file /= source.subdir
1206+ file /= source .entry
1207+ log .debug (f'checking entry file { str (file )} ' )
1208+ if file .exists ():
1209+ # FIXME: check file encoding
1210+ try :
1211+ with open (file , 'r' ) as f :
1212+ return f .read ()
1213+ except UnicodeDecodeError :
1214+ log .debug ('failed to read source file' )
1215+ return None
1216+ else :
1217+ log .debug ('could not find source file' )
1218+
1219+ return None
1220+
1221+ def check_for_shebang (source : InstInfo ) -> bool :
1222+ # TODO: match name, open file, look for shebang
1223+ # FIXME: Just testing that the plugin runs right now
1224+ log .debug (f'checking for shebang in { source } ' )
1225+ if source .source_dir :
1226+ source .get_inst_details ()
1227+ log .debug (f'given source dir { source .source_dir } ' )
1228+ log .debug (f'given entry { source .entry } ' )
1229+ if have_files (source .source_dir ):
1230+ log .debug ('Have access to files!' )
1231+ entrypoint_file = open_source_entrypoint (source )
1232+ log .debug (f"first line: { entrypoint_file .split ('\n ' )[0 ]} " )
1233+ if entrypoint_file .split ('\n ' )[0 ].startswith ('#!' ):
1234+ log .debug ('have shebang' )
1235+ # Calling the python interpreter will not manage dependencies.
1236+ # Leave this to another python installer.
1237+ for interpreter in ['bin/python' , 'env python' ]:
1238+ if interpreter in entrypoint_file .split ('\n ' )[0 ]:
1239+ return False
1240+ return True
1241+
1242+
1243+ # Open entrypoint file
1244+ # check for shebang
1245+ raise Exception ('Keep going!' )
1246+ if source .name == 'testplugshebang' :
1247+ raise Exception ("hold up!" )
1248+ return True
1249+ return False
1250+
1251+
11471252python3venv = Installer ('python3venv' , exe = 'python3' ,
11481253 manager = 'pip' , entry = '{name}.py' )
11491254python3venv .add_entrypoint ('{name}' )
@@ -1186,7 +1291,12 @@ rust_cargo = Installer('rust', manager='cargo', entry='Cargo.toml')
11861291rust_cargo .add_dependency_file ('Cargo.toml' )
11871292rust_cargo .dependency_call = cargo_installation
11881293
1189- INSTALLERS = [pythonuv , pythonuvlegacy , python3venv , poetryvenv ,
1294+ shebang = Installer ('shebang' , entry = '{name}.py' )
1295+ shebang .add_entrypoint ('{name}' )
1296+ # An extra installable check to see if a #! is present in the file
1297+ shebang .check = check_for_shebang
1298+
1299+ INSTALLERS = [shebang , pythonuv , pythonuvlegacy , python3venv , poetryvenv ,
11901300 pyprojectViaPip , nodejs , rust_cargo ]
11911301
11921302
@@ -1376,6 +1486,8 @@ def _checkout_commit(orig_src: InstInfo,
13761486def _install_plugin (src : InstInfo ) -> Union [InstInfo , None ]:
13771487 """make sure the repo exists and clone it."""
13781488 log .debug (f'Install requested from { src } .' )
1489+ if src .source_dir and src .source_dir .parent_source :
1490+ log .debug (f'source has parent { src .source_dir .parent_source } ' )
13791491 if RECKLESS_CONFIG is None :
13801492 log .error ('reckless install directory unavailable' )
13811493 return None
@@ -1411,6 +1523,8 @@ def _install_plugin(src: InstInfo) -> Union[InstInfo, None]:
14111523 # FIXME: Validate path was cloned successfully.
14121524 # Depending on how we accessed the original source, there may be install
14131525 # details missing. Searching the cloned repo makes sure we have it.
1526+ # FIXME: This could be cloned to .remotesources and the global sources
1527+ # could then be updated with this new LoadedSource to save on additional cloning.
14141528 clone = LoadedSource (plugin_path )
14151529 clone .content .populate ()
14161530 # Make sure we don't try to fetch again!
@@ -1426,14 +1540,29 @@ def _install_plugin(src: InstInfo) -> Union[InstInfo, None]:
14261540 if not plugin_path :
14271541 return None
14281542
1543+ # FIXME: replace src wholesale
1544+ # We have a hunch it's in this directory/source, so link it here.
1545+ inst_check_src = copy .copy (src )
1546+ if not inst_check_src .source_dir :
1547+ inst_check_src .source_loc = plugin_path
1548+ inst_check_src .source_dir = clone .content
1549+ inst_check_src .source_dir .parent_source = clone
1550+
1551+ if src .srctype == Source .GITHUB_REPO :
1552+ inst_check_src .srctype = Source .GIT_LOCAL_CLONE
1553+ else :
1554+ inst_check_src .srctype = clone .type
1555+
14291556 # Find a suitable installer
14301557 INSTALLER = None
14311558 for inst_method in INSTALLERS :
1432- if not (inst_method .installable () and inst_method .executable ()):
1559+ if not (inst_method .installable (inst_check_src ) and inst_method .executable ()):
14331560 continue
14341561 if inst_method .dependency_file is not None :
14351562 if inst_method .dependency_file not in os .listdir (plugin_path ):
14361563 continue
1564+ if inst_method .check and not inst_method .check (inst_check_src ):
1565+ continue
14371566 log .debug (f"using installer { inst_method .name } " )
14381567 INSTALLER = inst_method
14391568 break
@@ -1685,6 +1814,8 @@ def search(plugin_name: str) -> Union[InstInfo, None]:
16851814 if not found :
16861815 continue
16871816 log .info (f"found { found .name } in source: { found .source_loc } " )
1817+ # FIXME: remove debugging
1818+ log .debug (f"source type: { found .srctype } " )
16881819 log .debug (f"entry: { found .entry } " )
16891820 if found .subdir :
16901821 log .debug (f'sub-directory: { found .subdir } ' )
@@ -2086,47 +2217,6 @@ def listinstalled():
20862217 return plugins
20872218
20882219
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-
21302220def find_plugin_candidates (source : Union [LoadedSource , SourceDir ], depth = 2 ) -> list :
21312221 """Filter through a source and return any candidates that appear to be
21322222 installable plugins with the registered installers."""
0 commit comments