1# -*- coding: utf-8 -*-
2"""
3Project and group management for RT-Thread build system.
4
5This module provides classes for managing project groups and their compilation.
6"""
7
8import os
9from typing import List, Dict, Any, Optional
10from dataclasses import dataclass, field
11from SCons.Script import *
12
13
14@dataclass
15class ProjectGroup:
16    """
17    Represents a project group (component).
18
19    This class encapsulates the information from DefineGroup calls.
20    """
21    name: str
22    sources: List[str]
23    dependencies: List[str] = field(default_factory=list)
24    environment: Any = None  # SCons Environment
25
26    # Paths and defines
27    include_paths: List[str] = field(default_factory=list)
28    defines: Dict[str, str] = field(default_factory=dict)
29
30    # Compiler flags
31    cflags: str = ""
32    cxxflags: str = ""
33    asflags: str = ""
34    ldflags: str = ""
35
36    # Local options (only for this group)
37    local_cflags: str = ""
38    local_cxxflags: str = ""
39    local_include_paths: List[str] = field(default_factory=list)
40    local_defines: Dict[str, str] = field(default_factory=dict)
41
42    # Libraries
43    libs: List[str] = field(default_factory=list)
44    lib_paths: List[str] = field(default_factory=list)
45
46    # Build objects
47    objects: List[Any] = field(default_factory=list)
48
49    def build(self, env) -> List:
50        """
51        Build the group and return objects.
52
53        Args:
54            env: SCons Environment
55
56        Returns:
57            List of build objects
58        """
59        if not self.sources:
60            return []
61
62        # Clone environment if we have local options
63        build_env = env
64        if self._has_local_options():
65            build_env = env.Clone()
66            self._apply_local_options(build_env)
67
68        # Apply global options
69        self._apply_global_options(build_env)
70
71        # Build objects
72        self.objects = []
73        for src in self.sources:
74            if isinstance(src, str):
75                # Build single file
76                obj = build_env.Object(src)
77                self.objects.extend(obj if isinstance(obj, list) else [obj])
78            else:
79                # Already a Node
80                self.objects.append(src)
81
82        return self.objects
83
84    def _has_local_options(self) -> bool:
85        """Check if group has local options."""
86        return bool(
87            self.local_cflags or
88            self.local_cxxflags or
89            self.local_include_paths or
90            self.local_defines
91        )
92
93    def _apply_local_options(self, env) -> None:
94        """Apply local options to environment."""
95        if self.local_cflags:
96            env.AppendUnique(CFLAGS=self.local_cflags.split())
97
98        if self.local_cxxflags:
99            env.AppendUnique(CXXFLAGS=self.local_cxxflags.split())
100
101        if self.local_include_paths:
102            paths = [os.path.abspath(p) for p in self.local_include_paths]
103            env.AppendUnique(CPPPATH=paths)
104
105        if self.local_defines:
106            env.AppendUnique(CPPDEFINES=self.local_defines)
107
108    def _apply_global_options(self, env) -> None:
109        """Apply global options to environment."""
110        # These options affect dependent groups too
111        if self.include_paths:
112            paths = [os.path.abspath(p) for p in self.include_paths]
113            env.AppendUnique(CPPPATH=paths)
114
115        if self.defines:
116            env.AppendUnique(CPPDEFINES=self.defines)
117
118        if self.cflags and 'CFLAGS' not in env:
119            env['CFLAGS'] = self.cflags
120
121        if self.cxxflags and 'CXXFLAGS' not in env:
122            env['CXXFLAGS'] = self.cxxflags
123
124        if self.libs:
125            env.AppendUnique(LIBS=self.libs)
126
127        if self.lib_paths:
128            paths = [os.path.abspath(p) for p in self.lib_paths]
129            env.AppendUnique(LIBPATH=paths)
130
131    def get_info(self) -> Dict[str, Any]:
132        """
133        Get group information for project generators.
134
135        Returns:
136            Dictionary with group information
137        """
138        return {
139            'name': self.name,
140            'sources': self.sources,
141            'include_paths': self.include_paths + self.local_include_paths,
142            'defines': {**self.defines, **self.local_defines},
143            'cflags': f"{self.cflags} {self.local_cflags}".strip(),
144            'cxxflags': f"{self.cxxflags} {self.local_cxxflags}".strip(),
145            'libs': self.libs,
146            'lib_paths': self.lib_paths
147        }
148
149
150class ProjectRegistry:
151    """
152    Registry for all project groups.
153
154    This class manages all registered project groups and provides
155    methods for querying and merging them.
156    """
157
158    def __init__(self):
159        self.groups: List[ProjectGroup] = []
160        self._group_index: Dict[str, ProjectGroup] = {}
161
162    def register_group(self, group: ProjectGroup) -> None:
163        """
164        Register a project group.
165
166        Args:
167            group: ProjectGroup instance
168        """
169        self.groups.append(group)
170        self._group_index[group.name] = group
171
172    def get_group(self, name: str) -> Optional[ProjectGroup]:
173        """
174        Get group by name.
175
176        Args:
177            name: Group name
178
179        Returns:
180            ProjectGroup or None
181        """
182        return self._group_index.get(name)
183
184    def get_all_groups(self) -> List[ProjectGroup]:
185        """Get all registered groups."""
186        return self.groups.copy()
187
188    def get_groups_by_dependency(self, dependency: str) -> List[ProjectGroup]:
189        """
190        Get groups that depend on a specific macro.
191
192        Args:
193            dependency: Dependency name
194
195        Returns:
196            List of matching groups
197        """
198        return [g for g in self.groups if dependency in g.dependencies]
199
200    def merge_groups(self, env) -> List:
201        """
202        Merge all groups into a single list of objects.
203
204        Args:
205            env: SCons Environment
206
207        Returns:
208            List of all build objects
209        """
210        all_objects = []
211
212        for group in self.groups:
213            if group.objects:
214                all_objects.extend(group.objects)
215
216        return all_objects
217
218    def get_project_info(self) -> Dict[str, Any]:
219        """
220        Get complete project information for generators.
221
222        Returns:
223            Dictionary with project information
224        """
225        # Collect all unique values
226        all_sources = []
227        all_includes = set()
228        all_defines = {}
229        all_libs = []
230        all_lib_paths = set()
231
232        for group in self.groups:
233            info = group.get_info()
234
235            # Sources
236            all_sources.extend(info['sources'])
237
238            # Include paths
239            all_includes.update(info['include_paths'])
240
241            # Defines
242            all_defines.update(info['defines'])
243
244            # Libraries
245            all_libs.extend(info['libs'])
246            all_lib_paths.update(info['lib_paths'])
247
248        return {
249            'groups': [g.get_info() for g in self.groups],
250            'all_sources': all_sources,
251            'all_includes': sorted(list(all_includes)),
252            'all_defines': all_defines,
253            'all_libs': all_libs,
254            'all_lib_paths': sorted(list(all_lib_paths))
255        }
256
257    def clear(self) -> None:
258        """Clear all registered groups."""
259        self.groups.clear()
260        self._group_index.clear()