11"""Construction of Conan data"""
22
33from pathlib import Path
4- from string import Template
5- from textwrap import dedent
64
7- import libcst as cst
85from pydantic import DirectoryPath
96
107from cppython .plugins .conan .schema import ConanDependency
118
129
13- class RequiresTransformer ( cst . CSTTransformer ) :
14- """Transformer to add or update the `requires` attribute in a ConanFile class. """
10+ class Builder :
11+ """Aids in building the information needed for the Conan plugin """
1512
16- def __init__ (self , dependencies : list [ ConanDependency ] ) -> None :
17- """Initialize the transformer with a list of dependencies. """
18- self .dependencies = dependencies
13+ def __init__ (self ) -> None :
14+ """Initialize the builder """
15+ self ._filename = 'conanfile.py'
1916
20- def _create_requires_assignment ( self ) -> cst . Assign :
21- """Create a `requires` assignment statement."""
22- return cst . Assign (
23- targets = [ cst . AssignTarget ( cst . Name ( value = 'requires' )) ],
24- value = cst . List (
25- [ cst . Element ( cst . SimpleString ( f'" { dependency . requires () } "' )) for dependency in self . dependencies ]
26- ),
27- )
17+ @ staticmethod
18+ def _create_base_conanfile (
19+ base_file : Path ,
20+ dependencies : list [ ConanDependency ],
21+ dependency_groups : dict [ str , list [ ConanDependency ]],
22+ ) -> None :
23+ """Creates a conanfile_base.py with CPPython managed dependencies."""
24+ test_dependencies = dependency_groups . get ( 'test' , [] )
2825
29- def leave_ClassDef (self , original_node : cst .ClassDef , updated_node : cst .ClassDef ) -> cst .BaseStatement :
30- """Modify the class definition to include or update 'requires'.
26+ # Generate requirements method content
27+ requires_lines = []
28+ for dep in dependencies :
29+ requires_lines .append (f' self.requires("{ dep .requires ()} ")' )
30+ requires_content = '\n ' .join (requires_lines ) if requires_lines else ' pass # No requirements'
31+
32+ # Generate build_requirements method content
33+ test_requires_lines = []
34+ for dep in test_dependencies :
35+ test_requires_lines .append (f' self.test_requires("{ dep .requires ()} ")' )
36+ test_requires_content = (
37+ '\n ' .join (test_requires_lines ) if test_requires_lines else ' pass # No test requirements'
38+ )
3139
32- Args:
33- original_node: The original class definition.
34- updated_node: The updated class definition.
40+ content = f'''"""CPPython managed base ConanFile.
3541
36- Returns: The modified class definition.
37- """
38- if self ._is_conanfile_class (original_node ):
39- updated_node = self ._update_requires (updated_node )
40- return updated_node
42+ This file is auto-generated by CPPython. Do not edit manually.
43+ Dependencies are managed through pyproject.toml.
44+ """
4145
42- @staticmethod
43- def _is_conanfile_class (class_node : cst .ClassDef ) -> bool :
44- """Check if the class inherits from ConanFile.
46+ from conan import ConanFile
4547
46- Args:
47- class_node: The class definition to check.
4848
49- Returns: True if the class inherits from ConanFile, False otherwise.
50- """
51- return any ((isinstance (base .value , cst .Name ) and base .value .value == 'ConanFile' ) for base in class_node .bases )
52-
53- def _update_requires (self , updated_node : cst .ClassDef ) -> cst .ClassDef :
54- """Update or add a 'requires' assignment in a ConanFile class definition."""
55- # Check if 'requires' is already defined
56- for body_statement_line in updated_node .body .body :
57- if not isinstance (body_statement_line , cst .SimpleStatementLine ):
58- continue
59- for assignment_statement in body_statement_line .body :
60- if not isinstance (assignment_statement , cst .Assign ):
61- continue
62- for target in assignment_statement .targets :
63- if not isinstance (target .target , cst .Name ) or target .target .value != 'requires' :
64- continue
65- # Replace only the assignment within the SimpleStatementLine
66- return self ._replace_requires (updated_node , body_statement_line , assignment_statement )
67-
68- # Find the last attribute assignment before methods
69- last_attribute = None
70- for body_statement_line in updated_node .body .body :
71- if not isinstance (body_statement_line , cst .SimpleStatementLine ):
72- break
73- if not body_statement_line .body :
74- break
75- if not isinstance (body_statement_line .body [0 ], cst .Assign ):
76- break
77- last_attribute = body_statement_line
78-
79- # Construct a new statement for the 'requires' attribute
80- new_statement = cst .SimpleStatementLine (
81- body = [self ._create_requires_assignment ()],
82- )
49+ class CPPythonBase(ConanFile):
50+ """Base ConanFile with CPPython managed dependencies."""
8351
84- # Insert the new statement after the last attribute assignment
85- if last_attribute is not None :
86- new_body = [item for item in updated_node .body .body ]
87- index = new_body .index (last_attribute )
88- new_body .insert (index + 1 , new_statement )
89- else :
90- new_body = [new_statement ] + [item for item in updated_node .body .body ]
91- return updated_node .with_changes (body = updated_node .body .with_changes (body = new_body ))
92-
93- def _replace_requires (
94- self , updated_node : cst .ClassDef , body_statement_line : cst .SimpleStatementLine , assignment_statement : cst .Assign
95- ) -> cst .ClassDef :
96- """Replace the existing 'requires' assignment with a new one, preserving other statements on the same line."""
97- new_value = cst .List (
98- [cst .Element (cst .SimpleString (f'"{ dependency .requires ()} "' )) for dependency in self .dependencies ]
99- )
100- new_assignment = assignment_statement .with_changes (value = new_value )
101-
102- # Replace only the relevant assignment in the SimpleStatementLine
103- new_body = [
104- new_assignment if statement is assignment_statement else statement for statement in body_statement_line .body
105- ]
106- new_statement_line = body_statement_line .with_changes (body = new_body )
107-
108- # Replace the statement line in the class body
109- return updated_node .with_changes (
110- body = updated_node .body .with_changes (
111- body = [new_statement_line if item is body_statement_line else item for item in updated_node .body .body ]
112- )
113- )
52+ def requirements(self):
53+ """CPPython managed requirements."""
54+ { requires_content }
11455
115-
116- class Builder :
117- """Aids in building the information needed for the Conan plugin"""
118-
119- def __init__ (self ) -> None :
120- """Initialize the builder"""
121- self ._filename = 'conanfile.py'
56+ def build_requirements(self):
57+ """CPPython managed build and test requirements."""
58+ { test_requires_content }
59+ '''
60+ base_file .write_text (content , encoding = 'utf-8' )
12261
12362 @staticmethod
12463 def _create_conanfile (
12564 conan_file : Path ,
126- dependencies : list [ConanDependency ],
127- dependency_groups : dict [str , list [ConanDependency ]],
12865 name : str ,
12966 version : str ,
13067 ) -> None :
131- """Creates a conanfile.py file with the necessary content."""
132- template_string = """
133- import os
134- from conan import ConanFile
135- from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout
136- from conan.tools.files import copy
137-
138- class AutoPackage(ConanFile):
139- name = "${name}"
140- version = "${version}"
141- settings = "os", "compiler", "build_type", "arch"
142- requires = ${dependencies}
143- test_requires = ${test_requires}
144-
145- def layout(self):
146- cmake_layout(self)
147-
148- def generate(self):
149- deps = CMakeDeps(self)
150- deps.generate()
151- tc = CMakeToolchain(self)
152- tc.user_presets_path = None
153- tc.generate()
154-
155- def build(self):
156- cmake = CMake(self)
157- cmake.configure()
158- cmake.build()
159-
160- def package(self):
161- cmake = CMake(self)
162- cmake.install()
163-
164- def package_info(self):
165- # Use native CMake config files to preserve FILE_SET information for C++ modules
166- # This tells CMakeDeps to skip generating files and use the package's native config
167- self.cpp_info.set_property("cmake_find_mode", "none")
168- self.cpp_info.builddirs = ["."]
169-
170- def export_sources(self):
171- copy(self, "CMakeLists.txt", src=self.recipe_folder, dst=self.export_sources_folder)
172- copy(self, "src/*", src=self.recipe_folder, dst=self.export_sources_folder)
173- copy(self, "cmake/*", src=self.recipe_folder, dst=self.export_sources_folder)
174- """
175-
176- template = Template (dedent (template_string ))
177-
178- test_dependencies = dependency_groups .get ('test' , [])
179-
180- values = {
181- 'name' : name ,
182- 'version' : version ,
183- 'dependencies' : [dependency .requires () for dependency in dependencies ],
184- 'test_requires' : [dependency .requires () for dependency in test_dependencies ],
185- }
186-
187- result = template .substitute (values )
188-
189- with open (conan_file , 'w' , encoding = 'utf-8' ) as file :
190- file .write (result )
68+ """Creates a conanfile.py file that inherits from CPPython base."""
69+ class_name = name .replace ('-' , '_' ).title ().replace ('_' , '' )
70+ content = f'''import os
71+ from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout
72+ from conan.tools.files import copy
73+
74+ from conanfile_base import CPPythonBase
75+
76+
77+ class { class_name } Package(CPPythonBase):
78+ """Conan recipe for { name } ."""
79+
80+ name = "{ name } "
81+ version = "{ version } "
82+ settings = "os", "compiler", "build_type", "arch"
83+ exports = "conanfile_base.py"
84+
85+ def requirements(self):
86+ """Declare package dependencies.
87+
88+ CPPython managed dependencies are inherited from CPPythonBase.
89+ Add your custom requirements here.
90+ """
91+ super().requirements() # Get CPPython managed dependencies
92+ # Add your custom requirements here
93+
94+ def build_requirements(self):
95+ """Declare build and test dependencies.
96+
97+ CPPython managed test dependencies are inherited from CPPythonBase.
98+ Add your custom build requirements here.
99+ """
100+ super().build_requirements() # Get CPPython managed test dependencies
101+ # Add your custom build requirements here
102+
103+ def layout(self):
104+ cmake_layout(self)
105+
106+ def generate(self):
107+ deps = CMakeDeps(self)
108+ deps.generate()
109+ tc = CMakeToolchain(self)
110+ tc.user_presets_path = None
111+ tc.generate()
112+
113+ def build(self):
114+ cmake = CMake(self)
115+ cmake.configure()
116+ cmake.build()
117+
118+ def package(self):
119+ cmake = CMake(self)
120+ cmake.install()
121+
122+ def package_info(self):
123+ # Use native CMake config files to preserve FILE_SET information for C++ modules
124+ # This tells CMakeDeps to skip generating files and use the package's native config
125+ self.cpp_info.set_property("cmake_find_mode", "none")
126+ self.cpp_info.builddirs = ["."]
127+
128+ def export_sources(self):
129+ copy(self, "CMakeLists.txt", src=self.recipe_folder, dst=self.export_sources_folder)
130+ copy(self, "src/*", src=self.recipe_folder, dst=self.export_sources_folder)
131+ copy(self, "cmake/*", src=self.recipe_folder, dst=self.export_sources_folder)
132+ '''
133+ conan_file .write_text (content , encoding = 'utf-8' )
191134
192135 def generate_conanfile (
193136 self ,
@@ -197,17 +140,18 @@ def generate_conanfile(
197140 name : str ,
198141 version : str ,
199142 ) -> None :
200- """Generate a conanfile.py file for the project."""
201- conan_file = directory / self ._filename
143+ """Generate conanfile.py and conanfile_base.py for the project.
202144
203- if conan_file .exists ():
204- source_code = conan_file .read_text (encoding = 'utf-8' )
145+ Always generates the base conanfile with managed dependencies.
146+ Only creates conanfile.py if it doesn't exist (never modifies existing files).
147+ """
148+ directory .mkdir (parents = True , exist_ok = True )
205149
206- module = cst . parse_module ( source_code )
207- transformer = RequiresTransformer ( dependencies )
208- modified = module . visit ( transformer )
150+ # Always regenerate the base conanfile with managed dependencies
151+ base_file = directory / 'conanfile_base.py'
152+ self . _create_base_conanfile ( base_file , dependencies , dependency_groups )
209153
210- conan_file . write_text ( modified . code , encoding = 'utf-8' )
211- else :
212- directory . mkdir ( parents = True , exist_ok = True )
213- self ._create_conanfile (conan_file , dependencies , dependency_groups , name , version )
154+ # Only create conanfile.py if it doesn't exist
155+ conan_file = directory / self . _filename
156+ if not conan_file . exists ():
157+ self ._create_conanfile (conan_file , name , version )
0 commit comments