|
| 1 | +use std::{str::FromStr, sync::Arc}; |
| 2 | + |
| 3 | +use miette::{Context, IntoDiagnostic}; |
| 4 | +use pixi_build_backend::{ |
| 5 | + protocol::{Protocol, ProtocolInstantiator}, |
| 6 | + utils::TemporaryRenderedRecipe, |
| 7 | +}; |
| 8 | +use pixi_build_types::{ |
| 9 | + procedures::{ |
| 10 | + conda_build::{ |
| 11 | + CondaBuildParams, CondaBuildResult, CondaBuiltPackage, CondaOutputIdentifier, |
| 12 | + }, |
| 13 | + conda_metadata::{CondaMetadataParams, CondaMetadataResult}, |
| 14 | + initialize::{InitializeParams, InitializeResult}, |
| 15 | + negotiate_capabilities::{NegotiateCapabilitiesParams, NegotiateCapabilitiesResult}, |
| 16 | + }, |
| 17 | + CondaPackageMetadata, PlatformAndVirtualPackages, |
| 18 | +}; |
| 19 | +use rattler_build::{ |
| 20 | + build::run_build, |
| 21 | + console_utils::LoggingOutputHandler, |
| 22 | + hash::HashInfo, |
| 23 | + metadata::{Directories, Output}, |
| 24 | + recipe::{parser::BuildString, Jinja}, |
| 25 | + render::resolved_dependencies::DependencyInfo, |
| 26 | + tool_configuration::Configuration, |
| 27 | +}; |
| 28 | +use rattler_conda_types::{ChannelConfig, MatchSpec, PackageName, Platform}; |
| 29 | + |
| 30 | +use crate::{cmake::CMakeBuildBackend, config::CMakeBackendConfig}; |
| 31 | + |
| 32 | +fn input_globs() -> Vec<String> { |
| 33 | + [ |
| 34 | + // Source files |
| 35 | + "**/*.{c,cc,cxx,cpp,h,hpp,hxx}", |
| 36 | + // CMake files |
| 37 | + "**/*.{cmake,cmake.in}", |
| 38 | + "**/CMakeFiles.txt", |
| 39 | + ] |
| 40 | + .iter() |
| 41 | + .map(|s| s.to_string()) |
| 42 | + .collect() |
| 43 | +} |
| 44 | + |
| 45 | +pub struct CMakeBuildBackendInstantiator { |
| 46 | + logging_output_handler: LoggingOutputHandler, |
| 47 | +} |
| 48 | + |
| 49 | +impl CMakeBuildBackendInstantiator { |
| 50 | + pub fn new(logging_output_handler: LoggingOutputHandler) -> Self { |
| 51 | + Self { |
| 52 | + logging_output_handler, |
| 53 | + } |
| 54 | + } |
| 55 | +} |
| 56 | +#[async_trait::async_trait] |
| 57 | +impl Protocol for CMakeBuildBackend { |
| 58 | + async fn get_conda_metadata( |
| 59 | + &self, |
| 60 | + params: CondaMetadataParams, |
| 61 | + ) -> miette::Result<CondaMetadataResult> { |
| 62 | + let channel_config = ChannelConfig { |
| 63 | + channel_alias: params.channel_configuration.base_url, |
| 64 | + root_dir: self.manifest_root.to_path_buf(), |
| 65 | + }; |
| 66 | + let channels = params.channel_base_urls.unwrap_or_default(); |
| 67 | + |
| 68 | + let host_platform = params |
| 69 | + .host_platform |
| 70 | + .as_ref() |
| 71 | + .map(|p| p.platform) |
| 72 | + .unwrap_or(Platform::current()); |
| 73 | + |
| 74 | + // Build the tool configuration |
| 75 | + let tool_config = Arc::new( |
| 76 | + Configuration::builder() |
| 77 | + .with_opt_cache_dir(self.cache_dir.clone()) |
| 78 | + .with_logging_output_handler(self.logging_output_handler.clone()) |
| 79 | + .with_channel_config(channel_config.clone()) |
| 80 | + .with_testing(false) |
| 81 | + .with_keep_build(true) |
| 82 | + .finish(), |
| 83 | + ); |
| 84 | + |
| 85 | + let package_name = PackageName::from_str(&self.project_model.name) |
| 86 | + .into_diagnostic() |
| 87 | + .context("`{name}` is not a valid package name")?; |
| 88 | + |
| 89 | + let directories = Directories::setup( |
| 90 | + package_name.as_normalized(), |
| 91 | + &self.manifest_path, |
| 92 | + ¶ms.work_directory, |
| 93 | + true, |
| 94 | + &chrono::Utc::now(), |
| 95 | + ) |
| 96 | + .into_diagnostic() |
| 97 | + .context("failed to setup build directories")?; |
| 98 | + |
| 99 | + // Create a variant config from the variant configuration in the parameters. |
| 100 | + let variant_combinations = |
| 101 | + self.compute_variants(params.variant_configuration, host_platform)?; |
| 102 | + |
| 103 | + // Construct the different outputs |
| 104 | + let mut packages = Vec::new(); |
| 105 | + for variant in variant_combinations { |
| 106 | + // TODO: Determine how and if we can determine this from the manifest. |
| 107 | + let recipe = self.recipe(host_platform, &channel_config, &variant)?; |
| 108 | + let output = Output { |
| 109 | + build_configuration: self.build_configuration( |
| 110 | + &recipe, |
| 111 | + channels.clone(), |
| 112 | + params.build_platform.clone(), |
| 113 | + params.host_platform.clone(), |
| 114 | + variant, |
| 115 | + directories.clone(), |
| 116 | + )?, |
| 117 | + recipe, |
| 118 | + finalized_dependencies: None, |
| 119 | + finalized_cache_dependencies: None, |
| 120 | + finalized_cache_sources: None, |
| 121 | + finalized_sources: None, |
| 122 | + build_summary: Arc::default(), |
| 123 | + system_tools: Default::default(), |
| 124 | + extra_meta: None, |
| 125 | + }; |
| 126 | + |
| 127 | + let temp_recipe = TemporaryRenderedRecipe::from_output(&output)?; |
| 128 | + let tool_config = tool_config.clone(); |
| 129 | + let output = temp_recipe |
| 130 | + .within_context_async(move || async move { |
| 131 | + output |
| 132 | + .resolve_dependencies(&tool_config) |
| 133 | + .await |
| 134 | + .into_diagnostic() |
| 135 | + }) |
| 136 | + .await?; |
| 137 | + |
| 138 | + let selector_config = output.build_configuration.selector_config(); |
| 139 | + |
| 140 | + let jinja = Jinja::new(selector_config.clone()).with_context(&output.recipe.context); |
| 141 | + |
| 142 | + let hash = HashInfo::from_variant(output.variant(), output.recipe.build().noarch()); |
| 143 | + let build_string = output.recipe.build().string().resolve( |
| 144 | + &hash, |
| 145 | + output.recipe.build().number(), |
| 146 | + &jinja, |
| 147 | + ); |
| 148 | + |
| 149 | + let finalized_deps = &output |
| 150 | + .finalized_dependencies |
| 151 | + .as_ref() |
| 152 | + .expect("dependencies should be resolved at this point") |
| 153 | + .run; |
| 154 | + |
| 155 | + packages.push(CondaPackageMetadata { |
| 156 | + name: output.name().clone(), |
| 157 | + version: output.version().clone().into(), |
| 158 | + build: build_string.to_string(), |
| 159 | + build_number: output.recipe.build.number, |
| 160 | + subdir: output.build_configuration.target_platform, |
| 161 | + depends: finalized_deps |
| 162 | + .depends |
| 163 | + .iter() |
| 164 | + .map(DependencyInfo::spec) |
| 165 | + .map(MatchSpec::to_string) |
| 166 | + .collect(), |
| 167 | + constraints: finalized_deps |
| 168 | + .constraints |
| 169 | + .iter() |
| 170 | + .map(DependencyInfo::spec) |
| 171 | + .map(MatchSpec::to_string) |
| 172 | + .collect(), |
| 173 | + license: output.recipe.about.license.map(|l| l.to_string()), |
| 174 | + license_family: output.recipe.about.license_family, |
| 175 | + noarch: output.recipe.build.noarch, |
| 176 | + }); |
| 177 | + } |
| 178 | + |
| 179 | + Ok(CondaMetadataResult { |
| 180 | + packages, |
| 181 | + input_globs: None, |
| 182 | + }) |
| 183 | + } |
| 184 | + |
| 185 | + async fn build_conda(&self, params: CondaBuildParams) -> miette::Result<CondaBuildResult> { |
| 186 | + let channel_config = ChannelConfig { |
| 187 | + channel_alias: params.channel_configuration.base_url, |
| 188 | + root_dir: self.manifest_root.to_path_buf(), |
| 189 | + }; |
| 190 | + let channels = params.channel_base_urls.unwrap_or_default(); |
| 191 | + let host_platform = params |
| 192 | + .host_platform |
| 193 | + .as_ref() |
| 194 | + .map(|p| p.platform) |
| 195 | + .unwrap_or_else(Platform::current); |
| 196 | + |
| 197 | + let package_name = PackageName::from_str(&self.project_model.name) |
| 198 | + .into_diagnostic() |
| 199 | + .context("`{name}` is not a valid package name")?; |
| 200 | + |
| 201 | + let directories = Directories::setup( |
| 202 | + package_name.as_normalized(), |
| 203 | + &self.manifest_path, |
| 204 | + ¶ms.work_directory, |
| 205 | + true, |
| 206 | + &chrono::Utc::now(), |
| 207 | + ) |
| 208 | + .into_diagnostic() |
| 209 | + .context("failed to setup build directories")?; |
| 210 | + |
| 211 | + // Recompute all the variant combinations |
| 212 | + let variant_combinations = |
| 213 | + self.compute_variants(params.variant_configuration, host_platform)?; |
| 214 | + |
| 215 | + // Compute outputs for each variant |
| 216 | + let mut outputs = Vec::with_capacity(variant_combinations.len()); |
| 217 | + for variant in variant_combinations { |
| 218 | + let recipe = self.recipe(host_platform, &channel_config, &variant)?; |
| 219 | + let build_configuration = self.build_configuration( |
| 220 | + &recipe, |
| 221 | + channels.clone(), |
| 222 | + params.host_platform.clone(), |
| 223 | + Some(PlatformAndVirtualPackages { |
| 224 | + platform: host_platform, |
| 225 | + virtual_packages: params.build_platform_virtual_packages.clone(), |
| 226 | + }), |
| 227 | + variant, |
| 228 | + directories.clone(), |
| 229 | + )?; |
| 230 | + |
| 231 | + let mut output = Output { |
| 232 | + build_configuration, |
| 233 | + recipe, |
| 234 | + finalized_dependencies: None, |
| 235 | + finalized_cache_dependencies: None, |
| 236 | + finalized_cache_sources: None, |
| 237 | + finalized_sources: None, |
| 238 | + build_summary: Arc::default(), |
| 239 | + system_tools: Default::default(), |
| 240 | + extra_meta: None, |
| 241 | + }; |
| 242 | + |
| 243 | + // Resolve the build string |
| 244 | + let selector_config = output.build_configuration.selector_config(); |
| 245 | + let jinja = Jinja::new(selector_config.clone()).with_context(&output.recipe.context); |
| 246 | + let hash = HashInfo::from_variant(output.variant(), output.recipe.build().noarch()); |
| 247 | + let build_string = output |
| 248 | + .recipe |
| 249 | + .build() |
| 250 | + .string() |
| 251 | + .resolve(&hash, output.recipe.build().number(), &jinja) |
| 252 | + .into_owned(); |
| 253 | + output.recipe.build.string = BuildString::Resolved(build_string); |
| 254 | + |
| 255 | + outputs.push(output); |
| 256 | + } |
| 257 | + |
| 258 | + // Setup tool configuration |
| 259 | + let tool_config = Arc::new( |
| 260 | + Configuration::builder() |
| 261 | + .with_opt_cache_dir(self.cache_dir.clone()) |
| 262 | + .with_logging_output_handler(self.logging_output_handler.clone()) |
| 263 | + .with_channel_config(channel_config.clone()) |
| 264 | + .with_testing(false) |
| 265 | + .with_keep_build(true) |
| 266 | + .finish(), |
| 267 | + ); |
| 268 | + |
| 269 | + // Determine the outputs to build |
| 270 | + let selected_outputs = if let Some(output_identifiers) = params.outputs { |
| 271 | + output_identifiers |
| 272 | + .into_iter() |
| 273 | + .filter_map(|iden| { |
| 274 | + let pos = outputs.iter().position(|output| { |
| 275 | + let CondaOutputIdentifier { |
| 276 | + name, |
| 277 | + version, |
| 278 | + build, |
| 279 | + subdir, |
| 280 | + } = &iden; |
| 281 | + name.as_ref() |
| 282 | + .map_or(true, |n| output.name().as_normalized() == n) |
| 283 | + && version |
| 284 | + .as_ref() |
| 285 | + .map_or(true, |v| output.version().to_string() == *v) |
| 286 | + && build |
| 287 | + .as_ref() |
| 288 | + .map_or(true, |b| output.build_string() == b.as_str()) |
| 289 | + && subdir |
| 290 | + .as_ref() |
| 291 | + .map_or(true, |s| output.target_platform().as_str() == s) |
| 292 | + })?; |
| 293 | + Some(outputs.remove(pos)) |
| 294 | + }) |
| 295 | + .collect() |
| 296 | + } else { |
| 297 | + outputs |
| 298 | + }; |
| 299 | + |
| 300 | + let mut packages = Vec::with_capacity(selected_outputs.len()); |
| 301 | + for output in selected_outputs { |
| 302 | + let temp_recipe = TemporaryRenderedRecipe::from_output(&output)?; |
| 303 | + let build_string = output |
| 304 | + .recipe |
| 305 | + .build |
| 306 | + .string |
| 307 | + .as_resolved() |
| 308 | + .expect("build string must have already been resolved") |
| 309 | + .to_string(); |
| 310 | + let tool_config = tool_config.clone(); |
| 311 | + let (output, package) = temp_recipe |
| 312 | + .within_context_async(move || async move { run_build(output, &tool_config).await }) |
| 313 | + .await?; |
| 314 | + let built_package = CondaBuiltPackage { |
| 315 | + output_file: package, |
| 316 | + input_globs: input_globs(), |
| 317 | + name: output.name().as_normalized().to_string(), |
| 318 | + version: output.version().to_string(), |
| 319 | + build: build_string.to_string(), |
| 320 | + subdir: output.target_platform().to_string(), |
| 321 | + }; |
| 322 | + packages.push(built_package); |
| 323 | + } |
| 324 | + |
| 325 | + Ok(CondaBuildResult { packages }) |
| 326 | + } |
| 327 | +} |
| 328 | + |
| 329 | +#[async_trait::async_trait] |
| 330 | +impl ProtocolInstantiator for CMakeBuildBackendInstantiator { |
| 331 | + type ProtocolEndpoint = CMakeBuildBackend; |
| 332 | + |
| 333 | + async fn initialize( |
| 334 | + &self, |
| 335 | + params: InitializeParams, |
| 336 | + ) -> miette::Result<(Self::ProtocolEndpoint, InitializeResult)> { |
| 337 | + let project_model = params |
| 338 | + .project_model |
| 339 | + .ok_or_else(|| miette::miette!("project model is required"))?; |
| 340 | + |
| 341 | + let project_model = project_model |
| 342 | + .into_v1() |
| 343 | + .ok_or_else(|| miette::miette!("project model v1 is required"))?; |
| 344 | + |
| 345 | + let config = if let Some(config) = params.configuration { |
| 346 | + serde_json::from_value(config) |
| 347 | + .into_diagnostic() |
| 348 | + .context("failed to parse configuration")? |
| 349 | + } else { |
| 350 | + CMakeBackendConfig::default() |
| 351 | + }; |
| 352 | + |
| 353 | + let instance = CMakeBuildBackend::new( |
| 354 | + params.manifest_path.as_path(), |
| 355 | + project_model, |
| 356 | + config, |
| 357 | + self.logging_output_handler.clone(), |
| 358 | + params.cache_directory, |
| 359 | + )?; |
| 360 | + |
| 361 | + Ok((instance, InitializeResult {})) |
| 362 | + } |
| 363 | + |
| 364 | + async fn negotiate_capabilities( |
| 365 | + params: NegotiateCapabilitiesParams, |
| 366 | + ) -> miette::Result<NegotiateCapabilitiesResult> { |
| 367 | + let capabilities = Self::ProtocolEndpoint::capabilities(¶ms.capabilities); |
| 368 | + Ok(NegotiateCapabilitiesResult { capabilities }) |
| 369 | + } |
| 370 | +} |
0 commit comments