44import os
55import platform
66import subprocess
7- from typing import Optional , Dict , Any , List , Callable
87import urllib .request
8+ from typing import Optional , Dict , Any , List , Callable
99
1010from .global_json import GlobalJson
1111
@@ -17,19 +17,19 @@ class Host:
1717 """
1818
1919 BASE_DIR : str = os .getcwd ()
20- """Base directory for the project (current working directory) ."""
20+ """Absolute path to the project root ."""
2121
2222 MODULES_DIR : str = os .path .join ('build' , 'modules' )
23- """Directory where the bite modules are located ."""
23+ """Relative or absolute path to the bite modules directory ."""
2424
2525 DOTNET_DIR : str = '.dotnet'
26- """Directory where the .NET SDK is or will be installed ."""
26+ """Relative or absolute path to the .NET SDK directory ."""
2727
2828 SOLUTION_PATH : Optional [str ] = None
29- """Optional override for the solution path ."""
30-
29+ """Optional override for the solution (.sln) file ."""
30+
3131 BITE_PROJ_PATH : str = 'bite.proj'
32- """Path to the bite.core file."""
32+ """Relative or absolute path to the bite.proj file."""
3333
3434 DEFAULT_ARGS : List [str ] = ['--nologo' ]
3535 """Default arguments to pass to dotnet CLI commands."""
@@ -68,7 +68,7 @@ def __init__(
6868 self .argparser_usage = f'{ self .name } command [options]'
6969 self .argparser_epilog = "Any unrecognized options will be passed to the command handler."
7070
71- # Only join with BASE_DIR if the path is relative
71+ # Normalize paths: only join with BASE_DIR if relative
7272 if not os .path .isabs (self .MODULES_DIR ):
7373 self .MODULES_DIR = os .path .join (self .BASE_DIR , self .MODULES_DIR )
7474 if not os .path .isabs (self .DOTNET_DIR ):
@@ -93,15 +93,17 @@ def __init__(
9393 self .requested_sdk : Optional [str ] = self ._resolve_requested_sdk ()
9494 self .solution : str = self .SOLUTION_PATH or self .detect_solution ()
9595 self .argparser : Optional [argparse .ArgumentParser ] = None
96- self .handlers : Dict [str , Callable [[argparse .Namespace ], None ]] = {}
96+ self .handlers : Dict [str , Callable [[argparse .Namespace , List [ str ] ], None ]] = {}
9797 self ._subparsers_action : Optional [argparse ._SubParsersAction ] = None
9898
99+ # --- CLI and Command Registration ---
100+
99101 def get_argparser (self ) -> argparse .ArgumentParser :
100102 """
101103 Get or create the argument parser for the CLI, using subparsers for each command.
102104
103105 Returns:
104- An argparse.ArgumentParser instance for parsing command-line arguments .
106+ argparse.ArgumentParser: The argument parser instance .
105107 """
106108 if self .argparser is not None :
107109 return self .argparser
@@ -113,14 +115,12 @@ def get_argparser(self) -> argparse.ArgumentParser:
113115 usage = self .argparser_usage ,
114116 add_help = True
115117 )
116- # Hide the metavar for the subparser argument, but keep help for each command
117118 subparsers = parser .add_subparsers (
118119 dest = 'command' ,
119- required = False , # Allow default
120- metavar = '' , # This hides the {restore,clean,...} line in help
120+ required = False ,
121+ metavar = '' ,
121122 )
122-
123- subparsers .required = False # Explicitly allow no subcommand
123+ subparsers .required = False
124124 parser .set_defaults (command = 'build' , func = self ._handle_dotnet_command )
125125 self ._subparsers_action = subparsers
126126
@@ -135,6 +135,7 @@ def get_argparser(self) -> argparse.ArgumentParser:
135135 sub .set_defaults (func = self ._handle_dotnet_command )
136136 self .handlers [cmd ['name' ]] = self ._handle_dotnet_command
137137
138+ # Register bite command only if bite.proj exists
138139 if os .path .isfile (self .BITE_PROJ_PATH ):
139140 bite_parser = subparsers .add_parser (
140141 'bite' ,
@@ -152,7 +153,7 @@ def get_argparser(self) -> argparse.ArgumentParser:
152153 def add_command (
153154 self ,
154155 name : str ,
155- handler : Callable [[argparse .Namespace ], None ],
156+ handler : Callable [[argparse .Namespace , List [ str ] ], None ],
156157 description : Optional [str ] = None ,
157158 help : str = "" ,
158159 arguments : Optional [List [Dict [str , Any ]]] = None ,
@@ -175,50 +176,83 @@ def add_command(
175176 if arguments :
176177 for arg in arguments :
177178 sub .add_argument (* arg .get ('args' , ()), ** arg .get ('kwargs' , {}))
178- # Always allow extra arguments for custom commands
179- sub .add_argument ('extras' , nargs = argparse .REMAINDER , help = 'Extra arguments' )
180179 sub .set_defaults (func = handler )
181180 self .handlers [name ] = handler
182181
183- def _handle_dotnet_command (self , args : argparse .Namespace , * extras : str ) -> None :
182+ def register_handler (self , command : str , handler : Callable [[argparse .Namespace , List [str ]], None ]) -> None :
183+ """
184+ Register a custom handler for a command.
185+
186+ Args:
187+ command: The command name.
188+ handler: The function to handle the command.
189+ """
190+ self .handlers [command ] = handler
191+
192+ def dispatch (self , args : argparse .Namespace , unknown_args : Optional [List [str ]] = None ) -> None :
193+ """
194+ Dispatch the parsed arguments to the appropriate handler.
195+
196+ Args:
197+ args: The parsed argparse.Namespace.
198+ unknown_args: List of unknown arguments, if any.
199+ """
200+ extras = unknown_args or []
201+ if hasattr (args , 'func' ):
202+ args .func (args , extras )
203+ else :
204+ self .get_argparser ().print_help ()
205+
206+ # --- Dotnet/MSBuild Command Handlers ---
207+
208+ def _handle_dotnet_command (self , args : argparse .Namespace , extras : List [str ]) -> None :
184209 """
185- Handle standard dotnet commands: restore, clean, build, test.
210+ Handle standard dotnet commands: restore, clean, build, test, pack .
186211 Passes any extra arguments to the dotnet CLI.
187212 """
188213 self .run (args .command , * extras )
189214
190- def _handle_bite (self , args : argparse .Namespace , * extras : str ) -> None :
215+ def _handle_bite (self , args : argparse .Namespace , extras : List [ str ] ) -> None :
191216 """
192217 Handle the 'bite' command, running a custom msbuild target.
193218 Passes any extra arguments to msbuild.
194219 """
195220 target = getattr (args , 'target' , 'help' )
196221 self .run_bite (target , * extras )
197222
198- def register_handler (self , command : str , handler : Callable [[argparse .Namespace ], None ]) -> None :
223+ # --- Dotnet/MSBuild Execution ---
224+
225+ def run (self , command : str , * args : str ) -> None :
199226 """
200- Register a custom handler for a command .
227+ Run a dotnet command with the solution file as an argument .
201228
202229 Args:
203- command: The command name .
204- handler: The function to handle the command.
230+ command: The dotnet CLI command to run (e.g., 'build', 'restore') .
231+ *args: Additional arguments to pass to the command.
205232 """
206- self .handlers [command ] = handler
233+ cmd = ['dotnet' , command ] + [self .solution ] + self .DEFAULT_ARGS + list (args )
234+ try :
235+ subprocess .check_call (cmd )
236+ except subprocess .CalledProcessError as e :
237+ print (f"Error: dotnet { command } failed with exit code { e .returncode } " )
238+ raise
207239
208- def dispatch (self , args : argparse . Namespace , unknown_args : Optional [ List [ str ]] = None ) -> None :
240+ def run_bite (self , target : str , * args : str ) -> None :
209241 """
210- Dispatch the parsed arguments to the appropriate handler .
242+ Run bite.core with the specified target .
211243
212244 Args:
213- args : The parsed argparse.Namespace .
214- unknown_args: List of unknown arguments, if any .
245+ target : The bite.core target to run .
246+ *args: Additional arguments to pass to msbuild .
215247 """
216- extras = unknown_args or []
248+ cmd = ['dotnet' , 'msbuild' ] + self .DEFAULT_ARGS + [f'-t:{ target } ' , self .BITE_PROJ_PATH ] + list (args )
249+ try :
250+ subprocess .check_call (cmd )
251+ except subprocess .CalledProcessError as e :
252+ print (f"Error: msbuild target '{ target } ' failed with exit code { e .returncode } " )
253+ raise
217254
218- if hasattr (args , 'func' ):
219- args .func (args , * extras )
220- else :
221- self .get_argparser ().print_help ()
255+ # --- SDK Installation ---
222256
223257 def _set_environment_variables (self ) -> None :
224258 """
@@ -249,15 +283,13 @@ def _install_sdk_windows(self) -> None:
249283 """
250284 if self .requested_sdk is None :
251285 raise RuntimeError ("No .NET SDK install required" )
252-
286+
253287 installer = os .path .join (self .DOTNET_DIR , 'dotnet-install.ps1' )
254288 url = 'https://dot.net/v1/dotnet-install.ps1'
255289
256- # Download the PowerShell script using urllib
257290 with urllib .request .urlopen (url ) as response , open (installer , 'wb' ) as out_file :
258291 out_file .write (response .read ())
259292
260- # Run the installer script
261293 subprocess .check_call ([
262294 'powershell' , '-NoProfile' , '-ExecutionPolicy' , 'Bypass' ,
263295 installer ,
@@ -271,30 +303,29 @@ def _install_sdk_unix(self) -> None:
271303 """
272304 if self .requested_sdk is None :
273305 raise RuntimeError ("No .NET SDK install required" )
274-
306+
275307 installer = os .path .join (self .DOTNET_DIR , 'dotnet-install.sh' )
276308 url = 'https://dot.net/v1/dotnet-install.sh'
277309
278- # Download the Bash script using urllib
279310 with urllib .request .urlopen (url ) as response , open (installer , 'wb' ) as out_file :
280311 out_file .write (response .read ())
281312
282- # Make the script executable
283313 os .chmod (installer , 0o755 )
284314
285- # Run the installer script
286315 subprocess .check_call ([
287316 'bash' , installer ,
288317 '--version' , self .requested_sdk ,
289318 '--install-dir' , self .DOTNET_DIR
290319 ])
291320
321+ # --- Solution/SDK Detection ---
322+
292323 def detect_solution (self ) -> str :
293324 """
294325 Find the single .sln file in BASE_DIR and return its full path.
295326
296327 Returns:
297- The full path to the solution file.
328+ str: The full path to the solution file.
298329
299330 Raises:
300331 RuntimeError: If zero or multiple .sln files are found.
@@ -311,7 +342,7 @@ def _resolve_requested_sdk(self) -> Optional[str]:
311342 Determine if the required SDK is installed.
312343
313344 Returns:
314- The required version if installation is needed, otherwise None.
345+ Optional[str]: The required version if installation is needed, otherwise None.
315346 """
316347 required = (
317348 self .global_json .version
@@ -329,27 +360,7 @@ def _resolve_requested_sdk(self) -> Optional[str]:
329360 except (subprocess .CalledProcessError , FileNotFoundError ):
330361 return required
331362
332- def run (self , command : str , * args : str ) -> None :
333- """
334- Run a dotnet command with the solution file as an argument.
335-
336- Args:
337- command: The dotnet CLI command to run (e.g., 'build', 'restore').
338- *args: Additional arguments to pass to the command.
339- """
340- cmd = ['dotnet' , command ] + [self .solution ] + self .DEFAULT_ARGS + list (args )
341- subprocess .call (cmd )
342-
343- def run_bite (self , target : str , * args : str ) -> None :
344- """
345- Run bite.core with the specified target.
346-
347- Args:
348- target: The bite.core target to run.
349- *args: Additional arguments to pass to msbuild.
350- """
351- cmd = ['dotnet' , 'msbuild' ] + self .DEFAULT_ARGS + [f'-t:{ target } ' , self .BITE_PROJ_PATH ] + list (args )
352- subprocess .call (cmd )
363+ # --- Utility ---
353364
354365 @staticmethod
355366 def msbuild_path (path : str ) -> str :
@@ -370,7 +381,7 @@ def load_modules(self) -> Dict[str, Any]:
370381 Load all .bite.py modules from the modules directory.
371382
372383 Returns:
373- A dictionary mapping plugin names to loaded module objects.
384+ Dict[str, Any]: Mapping of plugin names to loaded module objects.
374385 """
375386 mods : Dict [str , Any ] = {}
376387 pattern = os .path .join (self .MODULES_DIR , '**' , '*.bite.py' )
@@ -379,10 +390,15 @@ def load_modules(self) -> Dict[str, Any]:
379390 spec = importlib .util .spec_from_file_location (name , path )
380391 if spec is None or spec .loader is None :
381392 continue
382-
383393 mod = importlib .util .module_from_spec (spec )
384- spec .loader .exec_module (mod )
385-
394+ try :
395+ spec .loader .exec_module (mod )
396+ except Exception as e :
397+ print (f"Failed to load module { name } from { path } : { e } " )
398+ continue
386399 if hasattr (mod , 'load' ):
387- mods [name ] = mod .load (self )
400+ try :
401+ mods [name ] = mod .load (self )
402+ except Exception as e :
403+ print (f"Module '{ name } ' failed to initialize: { e } " )
388404 return mods
0 commit comments