@@ -105,35 +105,148 @@ def build(self) -> Environment:
105105 Raises:
106106 BuildException: If the build fails
107107 """
108+ from ..tool .pixi import Pixi
109+
108110 env_dir = self ._env_dir ()
109111
110- # Check if this is already a pixi project.
111- is_pixi_dir = (
112- (env_dir / "pixi.toml" ).is_file ()
113- or (env_dir / "pyproject.toml" ).is_file ()
114- or (env_dir / ".pixi" ).is_dir ()
112+ # Check for incompatible existing environments
113+ if (env_dir / "conda-meta" ).exists () and not (env_dir / ".pixi" ).exists ():
114+ raise BuildException (
115+ self ,
116+ f"Cannot use PixiBuilder: environment already managed by Mamba/Conda at { env_dir } " ,
117+ )
118+ if (env_dir / "pyvenv.cfg" ).exists ():
119+ raise BuildException (
120+ self ,
121+ f"Cannot use PixiBuilder: environment already managed by uv/venv at { env_dir } " ,
122+ )
123+
124+ pixi = Pixi ()
125+
126+ # Set up progress/output consumers
127+ pixi .set_output_consumer (
128+ lambda msg : [sub (msg ) for sub in self .output_subscribers ]
115129 )
116-
117- if (
118- is_pixi_dir
119- and self .source_content is None
120- and not self .conda_packages
121- and not self .pypi_packages
122- ):
123- # Environment already exists, just use it.
124- return self ._create_environment (env_dir )
125-
126- # Handle source-based build (file or content).
127- if self .source_content is not None :
128- if is_pixi_dir :
129- # Already initialized, just use it.
130- return self ._create_environment (env_dir )
131-
132- # TODO: Implement actual Pixi environment building for new environments
133- raise NotImplementedError (
134- "PixiBuilder.build() is not yet fully implemented. "
135- "Currently only supports wrapping existing environments."
130+ pixi .set_error_consumer (
131+ lambda msg : [sub (msg ) for sub in self .error_subscribers ]
136132 )
133+ pixi .set_download_progress_consumer (
134+ lambda cur , max : [
135+ sub ("Downloading pixi" , cur , max ) for sub in self .progress_subscribers
136+ ]
137+ )
138+
139+ # Pass along intended build configuration
140+ pixi .set_env_vars (self .env_vars_dict )
141+ pixi .set_flags (self .flags_list )
142+
143+ try :
144+ pixi .install ()
145+
146+ # Check if this is already a pixi project
147+ is_pixi_dir = (
148+ (env_dir / "pixi.toml" ).is_file ()
149+ or (env_dir / "pyproject.toml" ).is_file ()
150+ or (env_dir / ".pixi" ).is_dir ()
151+ )
152+
153+ if (
154+ is_pixi_dir
155+ and self .source_content is None
156+ and not self .conda_packages
157+ and not self .pypi_packages
158+ ):
159+ # Environment already exists, just use it
160+ return self ._create_environment (env_dir , pixi )
161+
162+ # Handle source-based build (file or content)
163+ if self .source_content is not None :
164+ # Infer scheme if not explicitly set
165+ if self .scheme is None :
166+ self .scheme = self ._scheme ().name ()
167+
168+ if not env_dir .exists ():
169+ env_dir .mkdir (parents = True , exist_ok = True )
170+
171+ if self .scheme == "pixi.toml" :
172+ # Write pixi.toml to envDir
173+ pixi_toml_file = env_dir / "pixi.toml"
174+ pixi_toml_file .write_text (self .source_content , encoding = "utf-8" )
175+ elif self .scheme == "pyproject.toml" :
176+ # Write pyproject.toml to envDir (Pixi natively supports it)
177+ pyproject_toml_file = env_dir / "pyproject.toml"
178+ pyproject_toml_file .write_text (
179+ self .source_content , encoding = "utf-8"
180+ )
181+ elif self .scheme == "environment.yml" :
182+ # Write environment.yml and import
183+ environment_yaml_file = env_dir / "environment.yml"
184+ environment_yaml_file .write_text (
185+ self .source_content , encoding = "utf-8"
186+ )
187+ # Only run init --import if pixi.toml doesn't exist yet
188+ # (importing creates pixi.toml, so this avoids "pixi.toml already exists" error)
189+ if not (env_dir / "pixi.toml" ).exists ():
190+ pixi .exec (
191+ "init" ,
192+ "--import" ,
193+ str (environment_yaml_file .absolute ()),
194+ str (env_dir .absolute ()),
195+ )
196+
197+ # Add any programmatic channels to augment source file
198+ if self .channels_list :
199+ pixi .add_channels (env_dir , * self .channels_list )
200+ else :
201+ # Programmatic package building
202+ if is_pixi_dir :
203+ # Already initialized, just use it
204+ return self ._create_environment (env_dir , pixi )
205+
206+ if not env_dir .exists ():
207+ env_dir .mkdir (parents = True , exist_ok = True )
208+
209+ pixi .init (env_dir )
210+
211+ # Fail fast for vacuous environments
212+ if not self .conda_packages and not self .pypi_packages :
213+ raise BuildException (
214+ self ,
215+ "Cannot build empty environment programmatically. "
216+ "Either provide a source file via Appose.pixi(source), or add packages via .conda() or .pypi()." ,
217+ )
218+
219+ # Add channels
220+ if self .channels_list :
221+ pixi .add_channels (env_dir , * self .channels_list )
222+
223+ # Add conda packages
224+ if self .conda_packages :
225+ pixi .add_conda_packages (env_dir , * self .conda_packages )
226+
227+ # Add PyPI packages
228+ if self .pypi_packages :
229+ pixi .add_pypi_packages (env_dir , * self .pypi_packages )
230+
231+ # Verify that appose was included when building programmatically
232+ prog_build = bool (self .conda_packages ) or bool (self .pypi_packages )
233+ if prog_build :
234+ import re
235+
236+ has_appose = any (
237+ re .match (r"^appose\b" , pkg ) for pkg in self .conda_packages
238+ ) or any (re .match (r"^appose\b" , pkg ) for pkg in self .pypi_packages )
239+ if not has_appose :
240+ raise BuildException (
241+ self ,
242+ "Appose package must be explicitly included when building programmatically. "
243+ 'Add .conda("appose") or .pypi("appose") to your builder.' ,
244+ )
245+
246+ return self ._create_environment (env_dir , pixi )
247+
248+ except (IOError , KeyboardInterrupt ) as e :
249+ raise BuildException (self , cause = e )
137250
138251 def wrap (self , env_dir : str | Path ) -> Environment :
139252 """
@@ -172,31 +285,34 @@ def wrap(self, env_dir: str | Path) -> Environment:
172285 self .base (env_path )
173286 return self .build ()
174287
175- def _create_environment (self , env_dir : Path ) -> Environment :
288+ def _create_environment (self , env_dir : Path , pixi ) -> Environment :
176289 """
177290 Creates an Environment for the given Pixi directory.
178291
179292 Args:
180293 env_dir: The Pixi environment directory
294+ pixi: The Pixi tool instance
181295
182296 Returns:
183297 Environment configured for this Pixi installation
184298 """
185- base = str (env_dir .absolute ())
299+ # Convert to absolute path for consistency
300+ env_dir_abs = env_dir .absolute ()
301+ base = str (env_dir_abs )
186302
187303 # Check which manifest file exists (pyproject.toml takes precedence)
188- manifest_file = env_dir / "pyproject.toml"
304+ manifest_file = env_dir_abs / "pyproject.toml"
189305 if not manifest_file .exists ():
190- manifest_file = env_dir / "pixi.toml"
306+ manifest_file = env_dir_abs / "pixi.toml"
191307
192- # pixi command - will be found via system PATH or installed pixi
308+ # Use the installed pixi command (full path)
193309 launch_args = [
194- " pixi" ,
310+ pixi . command ,
195311 "run" ,
196312 "--manifest-path" ,
197313 str (manifest_file .absolute ()),
198314 ]
199- bin_paths = [str (env_dir / ".pixi" / "envs" / "default" / "bin" )]
315+ bin_paths = [str (env_dir_abs / ".pixi" / "envs" / "default" / "bin" )]
200316
201317 return self ._create_env (base , bin_paths , launch_args )
202318
0 commit comments