|
17 | 17 | use anyhow::Result; |
18 | 18 | use serde::Serialize; |
19 | 19 | use std::collections::BTreeMap; |
| 20 | +use std::fmt; |
| 21 | +use std::str::FromStr; |
20 | 22 |
|
21 | 23 | use super::types::FrontMatter; |
22 | 24 |
|
@@ -284,6 +286,147 @@ pub trait CompilerExtension { |
284 | 286 | fn required_pipeline_vars(&self) -> Vec<PipelineEnvMapping> { |
285 | 287 | vec![] |
286 | 288 | } |
| 289 | + |
| 290 | + /// AWF volume mounts this extension requires inside the chroot. |
| 291 | + /// |
| 292 | + /// AWF replaces `$HOME` with an empty directory overlay for security, |
| 293 | + /// only mounting specific known subdirectories. Extensions that install |
| 294 | + /// toolchains under `$HOME` (e.g., elan for Lean 4) must declare mounts |
| 295 | + /// here so the toolchain is accessible inside the chroot. |
| 296 | + /// |
| 297 | + /// Shell variables like `$HOME` are expanded at runtime by bash, not at |
| 298 | + /// compile time. AWF auto-adjusts container paths for chroot by prefixing |
| 299 | + /// `/host`. |
| 300 | + fn required_awf_mounts(&self) -> Vec<AwfMount> { |
| 301 | + vec![] |
| 302 | + } |
| 303 | +} |
| 304 | + |
| 305 | +/// Mount access mode for an AWF bind mount. |
| 306 | +/// |
| 307 | +/// Maps to the Docker bind-mount mode string: `ro` (read-only) or `rw` |
| 308 | +/// (read-write, the Docker default when no mode is specified). |
| 309 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 310 | +pub enum AwfMountMode { |
| 311 | + /// Read-only mount (`ro`). The process inside the container cannot write |
| 312 | + /// to this path. |
| 313 | + ReadOnly, |
| 314 | + /// Read-write mount (`rw`). The container can write to this path. |
| 315 | + ReadWrite, |
| 316 | +} |
| 317 | + |
| 318 | +impl fmt::Display for AwfMountMode { |
| 319 | + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 320 | + match self { |
| 321 | + Self::ReadOnly => f.write_str("ro"), |
| 322 | + Self::ReadWrite => f.write_str("rw"), |
| 323 | + } |
| 324 | + } |
| 325 | +} |
| 326 | + |
| 327 | +impl FromStr for AwfMountMode { |
| 328 | + type Err = anyhow::Error; |
| 329 | + |
| 330 | + fn from_str(s: &str) -> Result<Self, Self::Err> { |
| 331 | + match s { |
| 332 | + "ro" => Ok(Self::ReadOnly), |
| 333 | + "rw" => Ok(Self::ReadWrite), |
| 334 | + other => anyhow::bail!( |
| 335 | + "Unknown AWF mount mode '{}': expected 'ro' or 'rw'", |
| 336 | + other |
| 337 | + ), |
| 338 | + } |
| 339 | + } |
| 340 | +} |
| 341 | + |
| 342 | +impl serde::Serialize for AwfMountMode { |
| 343 | + fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> { |
| 344 | + serializer.serialize_str(&self.to_string()) |
| 345 | + } |
| 346 | +} |
| 347 | + |
| 348 | +impl<'de> serde::Deserialize<'de> for AwfMountMode { |
| 349 | + fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> { |
| 350 | + let s = String::deserialize(deserializer)?; |
| 351 | + s.parse().map_err(serde::de::Error::custom) |
| 352 | + } |
| 353 | +} |
| 354 | + |
| 355 | +/// An AWF `--mount` specification in Docker bind-mount format. |
| 356 | +/// |
| 357 | +/// The format is `host_path:container_path[:mode]` |
| 358 | +/// (e.g. `"$HOME/.elan:$HOME/.elan:ro"`). |
| 359 | +/// |
| 360 | +/// Serializes and deserializes as the Docker format string so it round-trips |
| 361 | +/// cleanly through YAML/JSON configuration. |
| 362 | +#[derive(Debug, Clone, PartialEq, Eq)] |
| 363 | +pub struct AwfMount { |
| 364 | + /// Host path to bind-mount into the container. |
| 365 | + pub host_path: String, |
| 366 | + /// Corresponding path inside the container. |
| 367 | + pub container_path: String, |
| 368 | + /// Mount access mode. Defaults to [`AwfMountMode::ReadOnly`] when not |
| 369 | + /// specified in the input — the secure default for AWF chroot mounts. |
| 370 | + pub mode: AwfMountMode, |
| 371 | +} |
| 372 | + |
| 373 | +impl AwfMount { |
| 374 | + /// Creates an `AwfMount` with the given host path, container path, and |
| 375 | + /// access mode. |
| 376 | + pub fn new( |
| 377 | + host_path: impl Into<String>, |
| 378 | + container_path: impl Into<String>, |
| 379 | + mode: AwfMountMode, |
| 380 | + ) -> Self { |
| 381 | + Self { |
| 382 | + host_path: host_path.into(), |
| 383 | + container_path: container_path.into(), |
| 384 | + mode, |
| 385 | + } |
| 386 | + } |
| 387 | +} |
| 388 | + |
| 389 | +impl fmt::Display for AwfMount { |
| 390 | + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 391 | + write!(f, "{}:{}:{}", self.host_path, self.container_path, self.mode) |
| 392 | + } |
| 393 | +} |
| 394 | + |
| 395 | +impl FromStr for AwfMount { |
| 396 | + type Err = anyhow::Error; |
| 397 | + |
| 398 | + fn from_str(s: &str) -> Result<Self, Self::Err> { |
| 399 | + let parts: Vec<&str> = s.splitn(3, ':').collect(); |
| 400 | + match parts.as_slice() { |
| 401 | + [host, container] => Ok(Self { |
| 402 | + host_path: (*host).to_string(), |
| 403 | + container_path: (*container).to_string(), |
| 404 | + mode: AwfMountMode::ReadOnly, |
| 405 | + }), |
| 406 | + [host, container, mode_str] => Ok(Self { |
| 407 | + host_path: (*host).to_string(), |
| 408 | + container_path: (*container).to_string(), |
| 409 | + mode: mode_str.parse()?, |
| 410 | + }), |
| 411 | + _ => anyhow::bail!( |
| 412 | + "Invalid AWF mount spec '{}': expected 'host:container[:mode]'", |
| 413 | + s |
| 414 | + ), |
| 415 | + } |
| 416 | + } |
| 417 | +} |
| 418 | + |
| 419 | +impl serde::Serialize for AwfMount { |
| 420 | + fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> { |
| 421 | + serializer.serialize_str(&self.to_string()) |
| 422 | + } |
| 423 | +} |
| 424 | + |
| 425 | +impl<'de> serde::Deserialize<'de> for AwfMount { |
| 426 | + fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> { |
| 427 | + let s = String::deserialize(deserializer)?; |
| 428 | + s.parse().map_err(serde::de::Error::custom) |
| 429 | + } |
287 | 430 | } |
288 | 431 |
|
289 | 432 | /// Maps a container environment variable to a pipeline variable. |
@@ -358,6 +501,9 @@ macro_rules! extension_enum { |
358 | 501 | fn required_pipeline_vars(&self) -> Vec<PipelineEnvMapping> { |
359 | 502 | match self { $( $Enum::$Variant(e) => e.required_pipeline_vars(), )+ } |
360 | 503 | } |
| 504 | + fn required_awf_mounts(&self) -> Vec<AwfMount> { |
| 505 | + match self { $( $Enum::$Variant(e) => e.required_awf_mounts(), )+ } |
| 506 | + } |
361 | 507 | } |
362 | 508 | }; |
363 | 509 | } |
|
0 commit comments