Skip to content

Commit ffdffc6

Browse files
committed
Add ability to list bite.core targets
Signed-off-by: Alireza Poodineh <[email protected]>
1 parent e1bcb7b commit ffdffc6

File tree

3 files changed

+277
-8
lines changed

3 files changed

+277
-8
lines changed

tools/pybite/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from .host import Host
22
from .global_json import GlobalJson
3+
from .msbuild import MSBuildFile
34

45
__all__ = [
56
'Host',
67
'GlobalJson'
8+
'MSBuildFile'
79
]

tools/pybite/host.py

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import Optional, Dict, Any, List, Callable
99

1010
from .global_json import GlobalJson
11+
from .msbuild import MSBuildFile, MSBuildTarget
1112

1213

1314
class Host:
@@ -144,9 +145,10 @@ def get_argparser(self) -> argparse.ArgumentParser:
144145
usage=self.argparser_usage.replace('command', 'bite') + ' [target]',
145146
)
146147
bite_parser.add_argument('target', nargs='?', default='help', help='bite.core target to run, default is "help"')
148+
bite_parser.add_argument('--list', '-l', action='store_true', help='List available targets')
147149
self.register_handler('bite', self._handle_bite)
148150

149-
dotnet_parser = subparsers.add_parser(
151+
subparsers.add_parser(
150152
'dotnet',
151153
help='Run a dotnet command',
152154
epilog=self.argparser_epilog + ' (Dotnet CLI)',
@@ -232,43 +234,79 @@ def _handle_bite(self, args: argparse.Namespace, extras: List[str]) -> None:
232234
Handle the 'bite' command, running a custom msbuild target.
233235
Passes any extra arguments to msbuild.
234236
"""
237+
list_targets = getattr(args, 'list', False)
238+
if list_targets:
239+
dependant_targets: List[MSBuildTarget] = []
240+
targets = self._get_bite_core_targets()
241+
print("Available independent targets:")
242+
for target in targets:
243+
if getattr(target, 'AfterTargets', None) is None and getattr(target, 'BeforeTargets', None) is None:
244+
print(f" {target.Name}")
245+
else:
246+
dependant_targets.append(target)
247+
if dependant_targets:
248+
print("\nAvailable automated targets:")
249+
for target in dependant_targets:
250+
print(f" {target.Name}", end=' ')
251+
if getattr(target, 'AfterTargets', None):
252+
print(f"(after '{target.AfterTargets}')", end=' ')
253+
if getattr(target, 'BeforeTargets', None):
254+
print(f"(before '{target.BeforeTargets}')", end=' ')
255+
print()
256+
return
235257
target = getattr(args, 'target', 'help')
236258
self.run_bite(target, *extras)
237259

238260
# --- Dotnet/MSBuild Execution ---
239261

240-
def run(self, command: str, *args: str) -> None:
262+
def run(self, command: str, *args: str, capture_output: bool = False) -> Optional[subprocess.CompletedProcess]:
241263
"""
242264
Run a dotnet command.
243265
244266
Args:
245267
command: The dotnet CLI command to run.
246268
*args: Additional arguments to pass to the command.
269+
capture_output: If True, capture and return the output.
270+
271+
Returns:
272+
subprocess.CompletedProcess if capture_output is True, otherwise None.
247273
"""
248274
cmd = ['dotnet', command] + list(args)
249-
subprocess.call(cmd)
275+
if capture_output:
276+
return subprocess.run(cmd, capture_output=True, text=True)
277+
else:
278+
subprocess.call(cmd)
279+
return None
250280

251-
def run_builtin(self, command: str, *args: str) -> None:
281+
def run_builtin(self, command: str, *args: str, capture_output: bool = False) -> Optional[subprocess.CompletedProcess]:
252282
"""
253283
Run a built-in dotnet command with the solution file and default arguments.
254284
255285
Args:
256286
command: The dotnet command to run (e.g., 'build', 'restore').
257287
*args: Additional arguments to pass to the dotnet cli.
288+
capture_output: If True, capture and return the output.
289+
290+
Returns:
291+
subprocess.CompletedProcess if capture_output is True, otherwise None.
258292
"""
259293
cmd = [self.solution] + self.DEFAULT_ARGS + list(args)
260-
self.run(command, *cmd)
294+
return self.run(command, *cmd, capture_output=capture_output)
261295

262-
def run_bite(self, target: str, *args: str) -> None:
296+
def run_bite(self, target: str, *args: str, capture_output: bool = False) -> Optional[subprocess.CompletedProcess]:
263297
"""
264298
Run bite.core with the specified target and default arguments.
265299
266300
Args:
267301
target: The bite.core target to run.
268302
*args: Additional arguments to pass to msbuild.
303+
capture_output: If True, capture and return the output.
304+
305+
Returns:
306+
subprocess.CompletedProcess if capture_output is True, otherwise None.
269307
"""
270308
cmd = self.DEFAULT_ARGS + [f'-t:{target}', self.BITE_PROJ_PATH] + list(args)
271-
self.run('msbuild', *cmd)
309+
return self.run('msbuild', *cmd, capture_output=capture_output)
272310

273311
# --- SDK Installation ---
274312

@@ -384,7 +422,12 @@ def _resolve_requested_sdk(self) -> Optional[str]:
384422
def msbuild_path(path: str) -> str:
385423
"""
386424
Convert a Python path string to an MSBuild-acceptable path for directory properties.
387-
Ensures absolute path, uses backslashes, and ends with a backslash.
425+
426+
Args:
427+
path (str): The path to convert.
428+
429+
Returns:
430+
str: The MSBuild-compatible absolute path, quoted if it contains spaces, and ending with a backslash.
388431
"""
389432
abs_path = os.path.abspath(path)
390433
msbuild_path = abs_path
@@ -394,6 +437,36 @@ def msbuild_path(path: str) -> str:
394437
msbuild_path = f'"{msbuild_path}"'
395438
return msbuild_path
396439

440+
def _get_bite_core_targets(self) -> List[MSBuildTarget]:
441+
"""
442+
Retrieve all available MSBuild targets from bite.core or .bite.targets files.
443+
444+
Returns:
445+
List[MSBuildTarget]: List of discovered MSBuildTarget objects.
446+
"""
447+
targets: List[MSBuildTarget] = []
448+
449+
# Try to get targets from bite.core msbuild output
450+
output = self.run_bite('help', '-pp', capture_output=True)
451+
if output and output.stdout:
452+
try:
453+
# Parse the XML output directly if possible
454+
targets = MSBuildFile(xml_string=output.stdout).get_targets()
455+
return targets
456+
except Exception:
457+
pass
458+
459+
# Fallback: scan all .bite.targets files in the modules directory
460+
pattern = os.path.join(self.MODULES_DIR, '**', '*.bite.targets')
461+
for path in glob.glob(pattern, recursive=True):
462+
try:
463+
obj = MSBuildFile(path)
464+
targets.extend(obj.get_targets())
465+
except Exception:
466+
continue
467+
468+
return targets
469+
397470
def load_modules(self) -> Dict[str, Any]:
398471
"""
399472
Load all .bite.py modules from the modules directory.

tools/pybite/msbuild.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import xml.etree.ElementTree as ET
2+
from typing import Optional, Dict, List
3+
4+
class MSBuildElement:
5+
"""
6+
Base wrapper for an XML element in an MSBuild file.
7+
Provides attribute and child element lookup.
8+
"""
9+
def __init__(self, element: ET.Element):
10+
self._element = element
11+
12+
def __getattr__(self, name: str):
13+
# Try attribute first, then child element
14+
if name in self._element.attrib:
15+
return self._element.attrib[name]
16+
child = self._element.find(name)
17+
if child is not None:
18+
return child.text
19+
raise AttributeError(f"{self.__class__.__name__} has no attribute '{name}'")
20+
21+
@property
22+
def tag(self) -> str:
23+
"""Returns the XML tag name."""
24+
return self._element.tag
25+
26+
@property
27+
def attrib(self) -> Dict[str, str]:
28+
"""Returns the XML attributes as a dict."""
29+
return self._element.attrib
30+
31+
@property
32+
def text(self) -> Optional[str]:
33+
"""Returns the text content of the element."""
34+
return self._element.text
35+
36+
class MSBuildTarget(MSBuildElement):
37+
"""
38+
Represents an MSBuild <Target> element.
39+
Exposes common attributes and child elements as properties.
40+
"""
41+
@property
42+
def Name(self) -> Optional[str]:
43+
"""The Name attribute of the target."""
44+
return self._element.attrib.get("Name")
45+
46+
@property
47+
def DependsOnTargets(self) -> Optional[str]:
48+
"""The DependsOnTargets attribute."""
49+
return self._element.attrib.get("DependsOnTargets")
50+
51+
@property
52+
def Inputs(self) -> Optional[str]:
53+
"""The Inputs attribute."""
54+
return self._element.attrib.get("Inputs")
55+
56+
@property
57+
def Outputs(self) -> Optional[str]:
58+
"""The Outputs attribute."""
59+
return self._element.attrib.get("Outputs")
60+
61+
@property
62+
def Condition(self) -> Optional[str]:
63+
"""The Condition attribute."""
64+
return self._element.attrib.get("Condition")
65+
66+
@property
67+
def BeforeTargets(self) -> Optional[str]:
68+
"""The BeforeTargets attribute."""
69+
return self._element.attrib.get("BeforeTargets")
70+
71+
@property
72+
def AfterTargets(self) -> Optional[str]:
73+
"""The AfterTargets attribute."""
74+
return self._element.attrib.get("AfterTargets")
75+
76+
@property
77+
def Returns(self) -> Optional[str]:
78+
"""The Returns attribute."""
79+
return self._element.attrib.get("Returns")
80+
81+
@property
82+
def KeepDuplicateOutputs(self) -> Optional[str]:
83+
"""The KeepDuplicateOutputs attribute."""
84+
return self._element.attrib.get("KeepDuplicateOutputs")
85+
86+
@property
87+
def Tasks(self) -> List[MSBuildElement]:
88+
"""
89+
Returns a list of child task elements (all direct children that are not PropertyGroup/ItemGroup/OnError).
90+
"""
91+
return [MSBuildElement(e) for e in self._element if e.tag not in ("PropertyGroup", "ItemGroup", "OnError")]
92+
93+
@property
94+
def OnError(self) -> List[MSBuildElement]:
95+
"""
96+
Returns a list of <OnError> child elements.
97+
"""
98+
return [MSBuildElement(e) for e in self._element.findall("OnError")]
99+
100+
class MSBuildProperty(MSBuildElement):
101+
"""
102+
Represents a property element in MSBuild.
103+
"""
104+
@property
105+
def name(self) -> str:
106+
"""The property name (tag)."""
107+
return self._element.tag
108+
109+
@property
110+
def value(self) -> Optional[str]:
111+
"""The property value (text)."""
112+
return self._element.text
113+
114+
class MSBuildItem(MSBuildElement):
115+
"""
116+
Represents an item element in MSBuild.
117+
"""
118+
@property
119+
def Include(self) -> Optional[str]:
120+
"""The Include attribute."""
121+
return self._element.attrib.get("Include")
122+
123+
@property
124+
def Remove(self) -> Optional[str]:
125+
"""The Remove attribute."""
126+
return self._element.attrib.get("Remove")
127+
128+
@property
129+
def Update(self) -> Optional[str]:
130+
"""The Update attribute."""
131+
return self._element.attrib.get("Update")
132+
133+
@property
134+
def name(self) -> str:
135+
"""The item name (tag)."""
136+
return self._element.tag
137+
138+
class MSBuildFile:
139+
"""
140+
Represents an MSBuild file and provides access to its structure.
141+
"""
142+
def __init__(self, filepath: Optional[str] = None, xml_string: Optional[str] = None):
143+
"""
144+
Initialize the MSBuildProject from a file or XML string.
145+
146+
:param filepath: Path to the MSBuild XML file.
147+
:param xml_string: XML content as a string.
148+
"""
149+
if filepath:
150+
self.tree = ET.parse(filepath)
151+
self.root = self.tree.getroot()
152+
elif xml_string:
153+
self.root = ET.fromstring(xml_string)
154+
self.tree = ET.ElementTree(self.root)
155+
else:
156+
raise ValueError("Either filepath or xml_string must be provided.")
157+
158+
def get_property_groups(self) -> List[ET.Element]:
159+
"""
160+
Returns a list of <PropertyGroup> elements.
161+
"""
162+
return self.root.findall(".//PropertyGroup")
163+
164+
def get_item_groups(self) -> List[ET.Element]:
165+
"""
166+
Returns a list of <ItemGroup> elements.
167+
"""
168+
return self.root.findall(".//ItemGroup")
169+
170+
def get_properties(self) -> Dict[str, MSBuildProperty]:
171+
"""
172+
Returns a dict of all properties in all <PropertyGroup>s as MSBuildProperty objects.
173+
"""
174+
props: Dict[str, MSBuildProperty] = {}
175+
for pg in self.get_property_groups():
176+
for prop in pg:
177+
props[prop.tag] = MSBuildProperty(prop)
178+
return props
179+
180+
def get_items(self) -> Dict[str, List[MSBuildItem]]:
181+
"""
182+
Returns a dict of item type to list of MSBuildItem objects in all <ItemGroup>s.
183+
"""
184+
items: Dict[str, List[MSBuildItem]] = {}
185+
for ig in self.get_item_groups():
186+
for item in ig:
187+
items.setdefault(item.tag, []).append(MSBuildItem(item))
188+
return items
189+
190+
def get_targets(self) -> List[MSBuildTarget]:
191+
"""
192+
Returns a list of MSBuildTarget objects for <Target> elements.
193+
"""
194+
return [MSBuildTarget(e) for e in self.root.findall(".//Target")]

0 commit comments

Comments
 (0)