rattler_build_core 0.2.2

The core engine of rattler-build, providing recipe rendering, source fetching, script execution, package building, testing, and publishing
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
//! Configuration for the Rattler-Build tool
//! This is useful when using Rattler-Build as a library

use std::{collections::HashMap, path::PathBuf, sync::Arc};

use clap::ValueEnum;
use rattler::package_cache::PackageCache;
use rattler_conda_types::{ChannelConfig, Platform};
#[cfg(feature = "s3")]
use rattler_networking::s3_middleware;
use rattler_networking::{
    AuthenticationStorage,
    authentication_storage::{self, AuthenticationStorageError},
    mirror_middleware,
};
use rattler_repodata_gateway::Gateway;
use rattler_solve::ChannelPriority;
#[cfg(feature = "s3")]
use thiserror::Error;
use url::Url;

use crate::console_utils::LoggingOutputHandler;

/// The user agent to use for the reqwest client
pub const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);

/// Whether to skip existing packages or not
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum SkipExisting {
    /// Do not skip any packages
    None,
    /// Skip packages that already exist locally
    Local,
    /// Skip packages that already exist in any channel
    All,
}

/// Container for the CLI test strategy
#[derive(Debug, Clone, Copy, ValueEnum, Default)]
pub enum TestStrategy {
    /// Skip the tests
    Skip,
    /// Run the tests only if the build platform is the same as the host platform.
    /// Otherwise, skip the tests. If the target platform is noarch,
    /// the tests are always executed.
    Native,
    /// Always run the tests
    #[default]
    NativeAndEmulated,
}

/// Whether we want to continue building on failure of a package or stop the build
/// entirely
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContinueOnFailure {
    /// Continue building on failure of a package
    Yes,
    /// Stop the build entirely on failure of a package
    #[default]
    No,
}

// This is the key part - implement From<bool> for your type
impl From<bool> for ContinueOnFailure {
    fn from(value: bool) -> Self {
        if value {
            ContinueOnFailure::Yes
        } else {
            ContinueOnFailure::No
        }
    }
}

/// Global configuration for the build
#[derive(Clone)]
pub struct Configuration {
    /// If set to a value, a progress bar will be shown
    pub fancy_log_handler: LoggingOutputHandler,

    /// The HTTP client with S3, mirrors, and auth middleware
    pub client: rattler_build_networking::BaseClient,

    /// The source cache for downloading and caching source code
    pub source_cache: Option<Arc<rattler_build_source_cache::SourceCache>>,

    /// Set this to true if you want to keep the build directory after the build
    /// is done
    pub no_clean: bool,

    /// The strategy to use for running tests
    pub test_strategy: TestStrategy,

    /// Whether to use zstd
    pub use_zstd: bool,

    /// Whether to use bzip2
    pub use_bz2: bool,

    /// Whether to use sharded repodata
    pub use_sharded: bool,

    /// Whether to skip existing packages
    pub skip_existing: SkipExisting,

    /// The noarch platform to use (noarch builds are skipped on other platforms)
    pub noarch_build_platform: Option<Platform>,

    /// The channel configuration to use when parsing channels.
    pub channel_config: ChannelConfig,

    /// How many threads to use for compression (only relevant for `.conda`
    /// archives). This value is not serialized because the number of
    /// threads does not matter for the final result.
    pub compression_threads: Option<u32>,

    /// Concurrency limit for I/O operations
    pub io_concurrency_limit: Option<usize>,

    /// The package cache to use to store packages in.
    pub package_cache: PackageCache,

    /// The repodata gateway to use for querying repodata
    pub repodata_gateway: Gateway,

    /// What channel priority to use in solving
    pub channel_priority: ChannelPriority,

    /// List of hosts for which SSL certificate verification should be skipped
    pub allow_insecure_host: Option<Vec<String>>,

    /// Whether to continue building on failure of a package or stop the build
    pub continue_on_failure: ContinueOnFailure,

    /// Whether to error if the host prefix is detected in binary files
    pub error_prefix_in_binary: bool,

    /// Whether to allow symlinks in packages on Windows (defaults to false)
    pub allow_symlinks_on_windows: bool,

    /// Whether to allow absolute paths in license_file entries (defaults to false)
    pub allow_absolute_license_paths: bool,

    /// Whether the environments are externally managed (e.g. by `pixi-build`).
    /// This is only useful for other libraries that build their own environments and only use Rattler-Build
    /// to execute scripts / bundle up files.
    pub environments_externally_managed: bool,
}

/// Get the authentication storage from the given file
pub fn get_auth_store(
    auth_file: Option<PathBuf>,
) -> Result<AuthenticationStorage, AuthenticationStorageError> {
    match auth_file {
        Some(auth_file) => {
            let mut store = AuthenticationStorage::empty();
            store.add_backend(Arc::from(
                authentication_storage::backends::file::FileStorage::from_path(auth_file)?,
            ));
            Ok(store)
        }
        None => rattler_networking::AuthenticationStorage::from_env_and_defaults(),
    }
}

/// Create a reqwest client with the authentication middleware
///
/// * `auth_file` - Optional path to an authentication file
/// * `allow_insecure_host` - Optional list of hosts for which to disable SSL certificate verification
pub fn reqwest_client_from_auth_storage(
    auth_file: Option<PathBuf>,
    #[cfg(feature = "s3")] s3_middleware_config: HashMap<String, s3_middleware::S3Config>,
    mirror_middleware_config: HashMap<Url, Vec<mirror_middleware::Mirror>>,
    allow_insecure_host: Option<Vec<String>>,
) -> Result<rattler_build_networking::BaseClient, AuthenticationStorageError> {
    let auth_storage = get_auth_store(auth_file)?;

    let builder = rattler_build_networking::BaseClient::builder()
        .user_agent(APP_USER_AGENT)
        .timeout(5 * 60)
        .insecure_hosts(allow_insecure_host.unwrap_or_default())
        .with_authentication(auth_storage);
    #[cfg(feature = "s3")]
    let builder = builder.with_s3(s3_middleware_config);
    Ok(builder.with_mirrors(mirror_middleware_config).build())
}

/// A builder for a [`Configuration`].
pub struct ConfigurationBuilder {
    cache_dir: Option<PathBuf>,
    fancy_log_handler: Option<LoggingOutputHandler>,
    client: Option<rattler_build_networking::BaseClient>,
    no_clean: bool,
    no_test: bool,
    test_strategy: TestStrategy,
    use_zstd: bool,
    use_bz2: bool,
    use_sharded: bool,
    skip_existing: SkipExisting,
    noarch_build_platform: Option<Platform>,
    channel_config: Option<ChannelConfig>,
    compression_threads: Option<u32>,
    io_concurrency_limit: Option<usize>,
    channel_priority: ChannelPriority,
    allow_insecure_host: Option<Vec<String>>,
    continue_on_failure: ContinueOnFailure,
    error_prefix_in_binary: bool,
    allow_symlinks_on_windows: bool,
    allow_absolute_license_paths: bool,
    environments_externally_managed: bool,
}

impl Configuration {
    /// Constructs a new builder for the configuration. Using the builder allows
    /// customizing the default configuration.
    pub fn builder() -> ConfigurationBuilder {
        ConfigurationBuilder::new()
    }
}

impl ConfigurationBuilder {
    fn new() -> Self {
        Self {
            cache_dir: None,
            fancy_log_handler: None,
            client: None,
            no_clean: false,
            no_test: false,
            test_strategy: TestStrategy::default(),
            use_zstd: true,
            use_bz2: true,
            use_sharded: true,
            skip_existing: SkipExisting::None,
            noarch_build_platform: None,
            channel_config: None,
            compression_threads: None,
            io_concurrency_limit: None,
            channel_priority: ChannelPriority::Strict,
            allow_insecure_host: None,
            continue_on_failure: ContinueOnFailure::No,
            error_prefix_in_binary: false,
            allow_symlinks_on_windows: false,
            allow_absolute_license_paths: false,
            environments_externally_managed: false,
        }
    }

    /// Set the default cache directory to use for objects that need to be
    /// cached.
    pub fn with_cache_dir(self, cache_dir: PathBuf) -> Self {
        Self {
            cache_dir: Some(cache_dir),
            ..self
        }
    }

    /// Whether to continue building on failure of a package or stop the build
    pub fn with_continue_on_failure(self, continue_on_failure: ContinueOnFailure) -> Self {
        Self {
            continue_on_failure,
            ..self
        }
    }

    /// Whether to error if the host prefix is detected in binary files
    pub fn with_error_prefix_in_binary(self, error_prefix_in_binary: bool) -> Self {
        Self {
            error_prefix_in_binary,
            ..self
        }
    }

    /// Whether to allow symlinks in packages on Windows
    pub fn with_allow_symlinks_on_windows(self, allow_symlinks_on_windows: bool) -> Self {
        Self {
            allow_symlinks_on_windows,
            ..self
        }
    }

    /// Whether to allow absolute paths in license_file entries
    pub fn with_allow_absolute_license_paths(self, allow_absolute_license_paths: bool) -> Self {
        Self {
            allow_absolute_license_paths,
            ..self
        }
    }

    /// Set the default cache directory to use for objects that need to be
    /// cached.
    pub fn with_opt_cache_dir(self, cache_dir: Option<PathBuf>) -> Self {
        Self { cache_dir, ..self }
    }

    /// Set the logging output handler to use for logging
    pub fn with_logging_output_handler(self, fancy_log_handler: LoggingOutputHandler) -> Self {
        Self {
            fancy_log_handler: Some(fancy_log_handler),
            ..self
        }
    }

    /// Set whether to skip outputs that have already been build.
    pub fn with_skip_existing(self, skip_existing: SkipExisting) -> Self {
        Self {
            skip_existing,
            ..self
        }
    }

    /// Set the channel configuration to use.
    pub fn with_channel_config(self, channel_config: ChannelConfig) -> Self {
        Self {
            channel_config: Some(channel_config),
            ..self
        }
    }

    /// Set the number of threads to use for compression, or `None` to use the
    /// number of cores.
    pub fn with_compression_threads(self, compression_threads: Option<u32>) -> Self {
        Self {
            compression_threads,
            ..self
        }
    }

    /// Set the maximum I/O concurrency during package installation or None to use
    /// a default based on number of cores
    pub fn with_io_concurrency_limit(self, io_concurrency_limit: Option<usize>) -> Self {
        Self {
            io_concurrency_limit,
            ..self
        }
    }

    /// Sets whether to keep the build output or delete it after the build is
    /// done.
    pub fn with_keep_build(self, keep_build: bool) -> Self {
        Self {
            no_clean: keep_build,
            ..self
        }
    }

    /// Sets the request client to use for network requests.
    pub fn with_reqwest_client(self, client: rattler_build_networking::BaseClient) -> Self {
        Self {
            client: Some(client),
            ..self
        }
    }

    /// Sets whether tests should be executed.
    pub fn with_testing(self, testing_enabled: bool) -> Self {
        Self {
            no_test: !testing_enabled,
            ..self
        }
    }

    /// Sets the test strategy to use for running tests.
    pub fn with_test_strategy(self, test_strategy: TestStrategy) -> Self {
        Self {
            test_strategy,
            ..self
        }
    }

    /// Whether downloading repodata as `.zst` files is enabled.
    pub fn with_zstd_repodata_enabled(self, zstd_repodata_enabled: bool) -> Self {
        Self {
            use_zstd: zstd_repodata_enabled,
            ..self
        }
    }

    /// Whether downloading repodata as `.bz2` files is enabled.
    pub fn with_bz2_repodata_enabled(self, bz2_repodata_enabled: bool) -> Self {
        Self {
            use_bz2: bz2_repodata_enabled,
            ..self
        }
    }

    /// Whether downloading sharded repodata is enabled.
    pub fn with_sharded_repodata_enabled(self, sharded_repodata_enabled: bool) -> Self {
        Self {
            use_sharded: sharded_repodata_enabled,
            ..self
        }
    }

    /// Define the noarch platform
    pub fn with_noarch_build_platform(self, noarch_build_platform: Option<Platform>) -> Self {
        Self {
            noarch_build_platform,
            ..self
        }
    }

    /// Sets the channel priority to be used when solving environments
    pub fn with_channel_priority(self, channel_priority: ChannelPriority) -> Self {
        Self {
            channel_priority,
            ..self
        }
    }

    /// Set the list of hosts for which SSL certificate verification should be skipped
    pub fn with_allow_insecure_host(self, allow_insecure_host: Option<Vec<String>>) -> Self {
        Self {
            allow_insecure_host,
            ..self
        }
    }

    /// Set whether the environments are externally managed (e.g. by `pixi-build`).
    /// This is only useful for other libraries that build their own environments and only use rattler
    /// to execute scripts / bundle up files.
    pub fn with_environments_externally_managed(
        self,
        environments_externally_managed: bool,
    ) -> Self {
        Self {
            environments_externally_managed,
            ..self
        }
    }

    /// Construct a [`Configuration`] from the builder.
    pub fn finish(self) -> Configuration {
        let cache_dir = self.cache_dir.unwrap_or_else(|| {
            rattler_cache::default_cache_dir().expect("failed to determine default cache directory")
        });
        let client = self.client.unwrap_or_default();
        let package_cache = PackageCache::new(cache_dir.join(rattler_cache::PACKAGE_CACHE_DIR));
        let channel_config = self.channel_config.unwrap_or_else(|| {
            ChannelConfig::default_with_root_dir(
                std::env::current_dir().unwrap_or_else(|_err| PathBuf::from("/")),
            )
        });
        let repodata_gateway = Gateway::builder()
            .with_cache_dir(cache_dir.join(rattler_cache::REPODATA_CACHE_DIR))
            .with_package_cache(package_cache.clone())
            .with_client(client.get_client().clone())
            .with_channel_config(rattler_repodata_gateway::ChannelConfig {
                default: rattler_repodata_gateway::SourceConfig {
                    zstd_enabled: self.use_zstd,
                    bz2_enabled: self.use_bz2,
                    sharded_enabled: self.use_sharded,
                    cache_action: Default::default(),
                },
                per_channel: Default::default(),
            })
            .finish();

        let test_strategy = match self.no_test {
            true => TestStrategy::Skip,
            false => self.test_strategy,
        };

        Configuration {
            fancy_log_handler: self.fancy_log_handler.unwrap_or_default(),
            client,
            source_cache: None, // Built lazily on first use
            no_clean: self.no_clean,
            test_strategy,
            use_zstd: self.use_zstd,
            use_bz2: self.use_bz2,
            use_sharded: self.use_sharded,
            skip_existing: self.skip_existing,
            noarch_build_platform: self.noarch_build_platform,
            channel_config,
            compression_threads: self.compression_threads,
            io_concurrency_limit: self.io_concurrency_limit,
            package_cache,
            repodata_gateway,
            channel_priority: self.channel_priority,
            allow_insecure_host: self.allow_insecure_host,
            continue_on_failure: self.continue_on_failure,
            error_prefix_in_binary: self.error_prefix_in_binary,
            allow_symlinks_on_windows: self.allow_symlinks_on_windows,
            allow_absolute_license_paths: self.allow_absolute_license_paths,
            environments_externally_managed: self.environments_externally_managed,
        }
    }
}

/// Error type for S3 credential resolution
#[cfg(feature = "s3")]
#[derive(Debug, Error)]
pub enum S3CredentialError {
    /// Failed to get authentication storage
    #[error("Failed to get authentication storage: {0}")]
    AuthStorageError(#[from] AuthenticationStorageError),

    /// Failed to load credentials from AWS SDK
    #[error("Failed to load credentials from AWS SDK: {0}")]
    SdkError(#[from] rattler_s3::FromSDKError),

    /// No credentials found
    #[error(
        "No S3 credentials found for bucket '{bucket}'. Please configure credentials via `rattler auth` or AWS environment variables."
    )]
    NoCredentials {
        /// The bucket name
        bucket: String,
    },
}

/// Resolve S3 credentials for the given bucket URL.
///
/// This function tries to resolve S3 credentials in the following order:
/// 1. If S3 config is present for the bucket in the configuration, use it with auth storage
/// 2. Fall back to AWS SDK default credential chain
///
/// # Arguments
/// * `s3_config` - S3 middleware configuration (from rattler config)
/// * `auth_file` - Optional path to an authentication file
/// * `bucket_url` - The S3 bucket URL to resolve credentials for
#[cfg(feature = "s3")]
pub async fn resolve_s3_credentials(
    s3_config: &HashMap<String, s3_middleware::S3Config>,
    auth_file: Option<PathBuf>,
    bucket_url: &Url,
) -> Result<rattler_s3::ResolvedS3Credentials, S3CredentialError> {
    let bucket_name = bucket_url.host_str().unwrap_or_default();

    // Check if we have custom S3 config for this bucket
    if let Some(config) = s3_config.get(bucket_name)
        && let s3_middleware::S3Config::Custom {
            endpoint_url,
            region,
            force_path_style,
        } = config
    {
        // Create S3Credentials from the config
        let s3_creds = rattler_s3::S3Credentials {
            endpoint_url: endpoint_url.clone(),
            region: region.clone(),
            addressing_style: if *force_path_style {
                rattler_s3::S3AddressingStyle::Path
            } else {
                rattler_s3::S3AddressingStyle::VirtualHost
            },
            access_key_id: None,
            secret_access_key: None,
            session_token: None,
        };

        // Try to resolve with auth storage
        let auth_storage = get_auth_store(auth_file.clone())?;
        if let Some(resolved) = s3_creds.resolve(bucket_url, &auth_storage) {
            tracing::debug!(
                "Resolved S3 credentials for bucket '{}' from config + auth storage",
                bucket_name
            );
            return Ok(resolved);
        }
    }

    // Fall back to AWS SDK default credential chain
    tracing::debug!(
        "Using AWS SDK default credential chain for bucket '{}'",
        bucket_name
    );
    let resolved = rattler_s3::ResolvedS3Credentials::from_sdk().await?;
    Ok(resolved)
}