@@ -19,14 +19,17 @@ class Host:
1919 BASE_DIR : str = os .getcwd ()
2020 """Base directory for the project (current working directory)."""
2121
22- MODULES_DIR : str = os .path .join (BASE_DIR , 'build' , 'modules' )
23- """Directory where . bite.py modules are located."""
22+ MODULES_DIR : str = os .path .join ('build' , 'modules' )
23+ """Directory where the bite modules are located."""
2424
25- DOTNET_DIR : str = os . path . join ( BASE_DIR , '.dotnet' )
25+ DOTNET_DIR : str = '.dotnet'
2626 """Directory where the .NET SDK is or will be installed."""
2727
2828 SOLUTION_PATH : Optional [str ] = None
2929 """Optional override for the solution path."""
30+
31+ BITE_PROJ_PATH : str = 'bite.proj'
32+ """Path to the bite.core file."""
3033
3134 DEFAULT_ARGS : List [str ] = ['--nologo' ]
3235 """Default arguments to pass to dotnet CLI commands."""
@@ -41,32 +44,41 @@ class Host:
4144
4245 DOTNET_COMMANDS : List [Dict [str , str ]] = [
4346 {'name' : 'restore' , 'help' : 'Restore NuGet packages for the solution' },
44- {'name' : 'clean' , 'help' : 'Clean the solution' },
45- {'name' : 'build' , 'help' : 'Build the solution' },
46- {'name' : 'test' , 'help' : 'Test the solution' }
47+ {'name' : 'build' , 'help' : 'Build the solution (default)' },
48+ {'name' : 'pack' , 'help' : 'Pack the solution' },
49+ {'name' : 'clean' , 'help' : 'Clean the solution' },
50+ {'name' : 'test' , 'help' : 'Test the solution' },
4751 ]
4852 """List of default dotnet commands and their help descriptions."""
4953
5054 def __init__ (
5155 self ,
5256 app : str ,
5357 description : Optional [str ] = None ,
54- epilog : Optional [str ] = None ,
55- usage : Optional [str ] = None ,
5658 ) -> None :
5759 """
5860 Initialize the Host instance and prepare CLI argument parser configuration.
5961
6062 Args:
6163 app: The name of the application.
6264 description: Description of the application.
63- usage: Usage string for the argument parser.
64- epilog: Text to display at the end of the help message.
6565 """
6666 self .name : str = app
6767 self .description = description
68- self .argparser_usage = usage
69- self .argparser_epilog = epilog
68+ self .argparser_usage = f'{ self .name } command [options]'
69+ self .argparser_epilog = "Any unrecognized options will be passed to the command handler."
70+
71+ # Only join with BASE_DIR if the path is relative
72+ if not os .path .isabs (self .MODULES_DIR ):
73+ self .MODULES_DIR = os .path .join (self .BASE_DIR , self .MODULES_DIR )
74+ if not os .path .isabs (self .DOTNET_DIR ):
75+ self .DOTNET_DIR = os .path .join (self .BASE_DIR , self .DOTNET_DIR )
76+ if self .SOLUTION_PATH and not os .path .isabs (self .SOLUTION_PATH ):
77+ self .SOLUTION_PATH = os .path .join (self .BASE_DIR , self .SOLUTION_PATH )
78+ if not os .path .isabs (self .BITE_PROJ_PATH ):
79+ self .BITE_PROJ_PATH = os .path .join (self .BASE_DIR , self .BITE_PROJ_PATH )
80+
81+ self .DEFAULT_ARGS .append (f'/p:BiteModulesPath={ self .msbuild_path (self .MODULES_DIR )} ' )
7082
7183 try :
7284 self .global_json : Optional [GlobalJson ] = GlobalJson (os .path .join (self .BASE_DIR , 'global.json' ))
@@ -107,21 +119,32 @@ def get_argparser(self) -> argparse.ArgumentParser:
107119 required = False , # Allow default
108120 metavar = '' , # This hides the {restore,clean,...} line in help
109121 )
122+
110123 subparsers .required = False # Explicitly allow no subcommand
111124 parser .set_defaults (command = 'build' , func = self ._handle_dotnet_command )
112125 self ._subparsers_action = subparsers
113126
114127 # Register built-in dotnet commands
115128 for cmd in self .DOTNET_COMMANDS :
116- sub = subparsers .add_parser (cmd ['name' ], help = cmd ['help' ])
129+ sub = subparsers .add_parser (
130+ cmd ['name' ],
131+ help = cmd ['help' ],
132+ epilog = self .argparser_epilog + ' (Dotnet CLI)' ,
133+ usage = self .argparser_usage .replace ('command' , cmd ['name' ]),
134+ )
117135 sub .set_defaults (func = self ._handle_dotnet_command )
118136 self .handlers [cmd ['name' ]] = self ._handle_dotnet_command
119137
120- # Bite command
121- bite_parser = subparsers .add_parser ('bite' , help = 'Run a custom bite target' )
122- bite_parser .add_argument ('target' , nargs = '?' , default = 'help' , help = 'Bite target to run' )
123- bite_parser .set_defaults (func = self ._handle_bite )
124- self .handlers ['bite' ] = self ._handle_bite
138+ if os .path .isfile (self .BITE_PROJ_PATH ):
139+ bite_parser = subparsers .add_parser (
140+ 'bite' ,
141+ help = 'Run a bite.core target' ,
142+ epilog = self .argparser_epilog + ' (MSBuild)' ,
143+ usage = self .argparser_usage .replace ('command' , 'bite' ) + ' [target]' ,
144+ )
145+ bite_parser .add_argument ('target' , nargs = '?' , default = 'help' , help = 'bite.core target to run, default is "help"' )
146+ bite_parser .set_defaults (func = self ._handle_bite )
147+ self .handlers ['bite' ] = self ._handle_bite
125148
126149 self .argparser = parser
127150 return self .argparser
@@ -139,7 +162,7 @@ def add_command(
139162
140163 Args:
141164 name: The command name (e.g., 'deploy').
142- handler: The function to handle the command (called with parsed args).
165+ handler: The function to handle the command (called with parsed args and unknown args ).
143166 description: Optional description for the command.
144167 help: Help string for the command.
145168 arguments: List of dicts with keys for add_argument (optional).
@@ -148,7 +171,7 @@ def add_command(
148171 subparsers = self ._subparsers_action
149172 if subparsers is None :
150173 raise RuntimeError ("Subparsers not found in parser" )
151- sub = subparsers .add_parser (name , help = help , description = description or help )
174+ sub = subparsers .add_parser (name , help = help , description = description or help , epilog = self . argparser_epilog )
152175 if arguments :
153176 for arg in arguments :
154177 sub .add_argument (* arg .get ('args' , ()), ** arg .get ('kwargs' , {}))
@@ -170,7 +193,7 @@ def _handle_bite(self, args: argparse.Namespace, *extras: str) -> None:
170193 Passes any extra arguments to msbuild.
171194 """
172195 target = getattr (args , 'target' , 'help' )
173- self .msbuild (target , * extras )
196+ self .run_bite (target , * extras )
174197
175198 def register_handler (self , command : str , handler : Callable [[argparse .Namespace ], None ]) -> None :
176199 """
@@ -317,17 +340,31 @@ def run(self, command: str, *args: str) -> None:
317340 cmd = ['dotnet' , command ] + [self .solution ] + self .DEFAULT_ARGS + list (args )
318341 subprocess .call (cmd )
319342
320- def msbuild (self , target : str , * args : str ) -> None :
343+ def run_bite (self , target : str , * args : str ) -> None :
321344 """
322- Run msbuild with a specific target using dotnet CLI .
345+ Run bite.core with the specified target.
323346
324347 Args:
325- target: The msbuild target to run.
348+ target: The bite.core target to run.
326349 *args: Additional arguments to pass to msbuild.
327350 """
328- cmd = ['dotnet' , 'msbuild' ] + self .DEFAULT_ARGS + [f'-t:{ target } ' , 'bite.proj' ] + list (args )
351+ cmd = ['dotnet' , 'msbuild' ] + self .DEFAULT_ARGS + [f'-t:{ target } ' , self . BITE_PROJ_PATH ] + list (args )
329352 subprocess .call (cmd )
330353
354+ @staticmethod
355+ def msbuild_path (path : str ) -> str :
356+ """
357+ Convert a Python path string to an MSBuild-acceptable path for directory properties.
358+ Ensures absolute path, uses backslashes, and ends with a backslash.
359+ """
360+ abs_path = os .path .abspath (path )
361+ msbuild_path = abs_path
362+ if not msbuild_path .endswith ('\\ ' ):
363+ msbuild_path += '\\ '
364+ if ' ' in msbuild_path :
365+ msbuild_path = f'"{ msbuild_path } "'
366+ return msbuild_path
367+
331368 def load_modules (self ) -> Dict [str , Any ]:
332369 """
333370 Load all .bite.py modules from the modules directory.
@@ -342,8 +379,10 @@ def load_modules(self) -> Dict[str, Any]:
342379 spec = importlib .util .spec_from_file_location (name , path )
343380 if spec is None or spec .loader is None :
344381 continue
382+
345383 mod = importlib .util .module_from_spec (spec )
346384 spec .loader .exec_module (mod )
385+
347386 if hasattr (mod , 'load' ):
348387 mods [name ] = mod .load (self )
349388 return mods
0 commit comments