zlayer_builder/error.rs
1//! Builder error types
2//!
3//! This module defines all error types for the Dockerfile builder subsystem,
4//! covering parsing, context handling, build execution, and caching operations.
5
6use std::path::PathBuf;
7use thiserror::Error;
8
9/// Build-specific errors
10#[derive(Debug, Error)]
11pub enum BuildError {
12 /// Dockerfile parsing failed
13 #[error("Dockerfile parse error at line {line}: {message}")]
14 DockerfileParse {
15 /// The underlying parsing error message
16 message: String,
17 /// Line number where the error occurred (1-indexed)
18 line: usize,
19 },
20
21 /// Failed to read build context
22 #[error("Failed to read build context at '{path}': {source}")]
23 ContextRead {
24 /// Path that could not be read
25 path: PathBuf,
26 /// Underlying IO error
27 source: std::io::Error,
28 },
29
30 /// Path escape attempt detected (security violation)
31 #[error("Path escape attempt: '{path}' escapes build context")]
32 PathEscape {
33 /// The offending path
34 path: PathBuf,
35 },
36
37 /// File was ignored by .dockerignore
38 #[error("File '{path}' is ignored by .dockerignore")]
39 FileIgnored {
40 /// The ignored file path
41 path: PathBuf,
42 },
43
44 /// Referenced stage not found
45 #[error("Stage '{name}' not found in Dockerfile")]
46 StageNotFound {
47 /// The stage name or index that was referenced
48 name: String,
49 },
50
51 /// RUN instruction failed
52 #[error("RUN command failed with exit code {exit_code}: {command}")]
53 RunFailed {
54 /// The command that failed
55 command: String,
56 /// Exit code returned by the command
57 exit_code: i32,
58 },
59
60 /// Failed to create layer
61 #[error("Failed to create layer: {message}")]
62 LayerCreate {
63 /// Underlying error description
64 message: String,
65 },
66
67 /// Cache operation failed
68 #[error("Cache error: {message}")]
69 CacheError {
70 /// Underlying cache error
71 message: String,
72 },
73
74 /// Registry operation failed
75 #[error("Registry error: {message}")]
76 RegistryError {
77 /// Underlying registry error
78 message: String,
79 },
80
81 /// A cross-architecture build was requested but the host has no
82 /// `binfmt_misc` / qemu-user-static handler registered for the target
83 /// architecture, so buildah cannot execute foreign-arch `RUN` steps.
84 #[error("cross-arch build for '{platform}' needs qemu-user-static + binfmt_misc registered on the build host (no /proc/sys/fs/binfmt_misc/qemu-{qemu_arch}); install qemu-user-static and run e.g. `docker run --privileged --rm tonistiigi/binfmt --install all`")]
85 BinfmtNotRegistered {
86 /// The platform string that was requested (e.g. `linux/arm64`).
87 platform: String,
88 /// The qemu-user-static handler arch name expected under
89 /// `/proc/sys/fs/binfmt_misc/qemu-<arch>` (e.g. `aarch64`).
90 qemu_arch: String,
91 },
92
93 /// IO error
94 #[error("IO error: {0}")]
95 IoError(#[from] std::io::Error),
96
97 /// Variable expansion failed
98 #[error("Variable expansion failed: {0}")]
99 VariableExpansion(String),
100
101 /// Invalid instruction
102 #[error("Invalid instruction '{instruction}': {reason}")]
103 InvalidInstruction {
104 /// The instruction that was invalid
105 instruction: String,
106 /// Reason why it was invalid
107 reason: String,
108 },
109
110 /// Buildah command execution failed
111 #[error("Buildah execution failed: {command} (exit code {exit_code}): {stderr}")]
112 BuildahExecution {
113 /// The buildah command that failed
114 command: String,
115 /// Exit code from buildah
116 exit_code: i32,
117 /// Standard error output
118 stderr: String,
119 },
120
121 /// Build context too large
122 #[error("Build context too large: {size} bytes (max: {max} bytes)")]
123 ContextTooLarge {
124 /// Actual size in bytes
125 size: u64,
126 /// Maximum allowed size
127 max: u64,
128 },
129
130 /// Base image not found
131 #[error("Base image not found: {image}")]
132 BaseImageNotFound {
133 /// The image reference that was not found
134 image: String,
135 },
136
137 /// Circular dependency in multi-stage build
138 #[error("Circular dependency detected in multi-stage build: {stages:?}")]
139 CircularDependency {
140 /// The stages involved in the cycle
141 stages: Vec<String>,
142 },
143
144 /// Buildah binary not found or installation failed
145 #[error("Buildah not found: {message}")]
146 BuildahNotFound {
147 /// Details about the failure
148 message: String,
149 },
150
151 /// `ZImagefile` YAML deserialization failed
152 #[error("ZImagefile parse error: {message}")]
153 ZImagefileParse {
154 /// The underlying YAML parse error message
155 message: String,
156 },
157
158 /// `ZImagefile` semantic validation failed
159 #[error("ZImagefile validation error: {message}")]
160 ZImagefileValidation {
161 /// Description of what validation rule was violated
162 message: String,
163 },
164
165 /// Pipeline validation or execution error
166 #[error("Pipeline error: {message}")]
167 PipelineError {
168 /// Description of the pipeline error
169 message: String,
170 },
171
172 /// WASM build failed
173 #[error("WASM build error: {0}")]
174 WasmBuild(#[from] crate::wasm_builder::WasmBuildError),
175
176 /// Operation not supported by this backend
177 #[error("Operation '{operation}' is not supported by this backend")]
178 NotSupported {
179 /// The operation that was attempted
180 operation: String,
181 },
182
183 /// A code path that is reserved for a future phase / task and is
184 /// intentionally not yet wired up. Constructed by
185 /// [`BuildError::not_yet_implemented`] so call sites can give a precise
186 /// reason (typically referencing the follow-up task that delivers it,
187 /// e.g. "RUN execution lands in Phase 4 task 4.B").
188 ///
189 /// This is distinct from [`BuildError::NotSupported`], which signals a
190 /// permanent capability gap of the chosen backend; `NotYetImplemented`
191 /// signals "tracked work, coming in a later task — do not silently
192 /// no-op".
193 #[error("not yet implemented: {0}")]
194 NotYetImplemented(String),
195
196 /// A macOS sandbox build step cannot be performed natively because it
197 /// genuinely needs a Linux environment — a Linux toolchain / libc /
198 /// kernel headers, a Linux-only package with no Homebrew bottle, or a
199 /// Linux ELF binary that cannot execute on Darwin.
200 ///
201 /// Distinct from [`BuildError::RunFailed`] (a command that actually ran
202 /// and exited non-zero — i.e. a *user* error) and from
203 /// [`BuildError::NotSupported`] (a permanent backend capability gap):
204 /// `SandboxNeedsLinux` signals "this is faithfully buildable, just not
205 /// here — retry on the buildd-in-VZ Linux path". The build front-end keys
206 /// its sandbox -> VZ-Linux fallback off this variant, so it must NOT be
207 /// raised for a broken command and must NOT be swallowed as a silent
208 /// success.
209 #[error("sandbox build needs a Linux environment for '{command}': {reason}")]
210 SandboxNeedsLinux {
211 /// The RUN command (or build step) that requires Linux.
212 command: String,
213 /// Why Linux is required: the unresolved package/formula, the
214 /// detected Linux ELF binary, or the toolchain dependency.
215 reason: String,
216 },
217
218 /// A specific RUN step in a Dockerfile failed with a non-zero exit
219 /// code. Carries the step index (0-based, counted across the active
220 /// stage's instruction list), the exit code surfaced by the
221 /// guest process, and a stderr tail to anchor diagnostics.
222 ///
223 /// Distinct from [`BuildError::RunFailed`] in that the latter is
224 /// emitted by the buildah/HCS backends working through the
225 /// `BuildBackend` trait, whereas this variant is emitted by the
226 /// Phase 4 `WindowsBuilder` path which carries a richer per-step
227 /// context (the step index and a stderr tail) than the buildah
228 /// path can produce.
229 #[error("RUN step {step_index} failed with exit code {exit_code}: {stderr_tail}")]
230 RunStepFailed {
231 /// Zero-based step index within the active stage's instruction
232 /// list (the value Phase 4 errors use to anchor a diagnostic).
233 step_index: usize,
234 /// Exit code reported by the guest process.
235 exit_code: i32,
236 /// Last fragment of stderr captured during the RUN step (or a
237 /// synthesised message when pipe capture has not yet been
238 /// wired). Surfaced verbatim in the error display so users get
239 /// the failing command in their build log.
240 stderr_tail: String,
241 },
242
243 /// `HcsExportLayer` / wclayer-side IO failed while capturing the
244 /// post-RUN scratch diff. Distinct from [`BuildError::IoError`] so
245 /// the WCOW builder can surface a layer-export-specific message
246 /// (the underlying failure is almost always either an
247 /// `HcsExportLayer` HRESULT or a tar/gzip walk error).
248 #[error("layer export failed: {source}")]
249 LayerExportFailed {
250 /// Underlying IO error (often wraps an HCS HRESULT).
251 #[source]
252 source: std::io::Error,
253 },
254
255 /// Chocolatey resolver could not produce a Windows equivalent for a
256 /// Linux package name encountered in a RUN instruction. The package
257 /// is named so the user can edit the Dockerfile (or contribute the
258 /// mapping to `RepoSources`).
259 #[error(
260 "no Chocolatey mapping for Linux package '{package}' in source distro '{source_distro}'"
261 )]
262 ChocoResolutionFailed {
263 /// The Linux package name that did not resolve.
264 package: String,
265 /// The `RepoSources`-style source distro key that was queried
266 /// (e.g. `"debian-12"`, `"ubuntu-22.04"`).
267 source_distro: String,
268 },
269
270 /// COPY/ADD source path contained a `..` component which is forbidden
271 /// because it would escape the build context. Surfaced before any
272 /// filesystem access so a malicious Dockerfile cannot reach files
273 /// outside the build directory even via a TOCTOU window.
274 #[error("COPY/ADD source path '{src}' contains '..' which is forbidden")]
275 PathTraversal {
276 /// The offending source path as it appeared in the Dockerfile.
277 src: String,
278 },
279
280 /// ADD `<URL> <dest>` failed to download the remote resource. Carries
281 /// the URL for diagnostics; the underlying network/protocol failure is
282 /// chained as the `source` so users get the precise cause (connection
283 /// refused, 404, TLS error, etc.).
284 #[error("ADD failed to fetch '{url}': {source}")]
285 HttpFetchFailed {
286 /// The URL the builder attempted to fetch.
287 url: String,
288 /// Underlying reqwest failure.
289 #[source]
290 source: reqwest::Error,
291 },
292
293 /// ADD auto-extraction of a tarball failed. Carries the chained IO
294 /// error from the `tar` / `flate2` / `bzip2` / `xz2` pipeline so the
295 /// user can see which entry tripped (path traversal in the archive,
296 /// disk full, etc.).
297 #[error("ADD failed to extract tarball: {source}")]
298 TarExtractFailed {
299 /// Underlying tar/decompressor IO error.
300 #[source]
301 source: std::io::Error,
302 },
303
304 /// The WCOW builder could not resolve an `os.version` for the emitted
305 /// OCI image config. The Windows runtime refuses to launch a
306 /// container whose `os.version` does not exactly match the host
307 /// kernel's build, so emitting a manifest without one would produce
308 /// an image nothing can run. Surfaces when the base manifest's
309 /// `os.version` is missing AND the user did not pass
310 /// `WindowsBuildConfig::os_version_override`.
311 #[error(
312 "os.version could not be resolved from base manifest or override; \
313 set WindowsBuildConfig::os_version_override or pull a base image \
314 whose manifest carries an os.version field"
315 )]
316 OsVersionUnresolved,
317
318 /// Computing a sha256 digest over a layer blob (or an in-memory
319 /// manifest blob) failed because the underlying IO read failed.
320 /// Carries the chained IO error so callers can see which file
321 /// tripped.
322 #[error("layer digest computation failed: {source}")]
323 LayerDigestComputationFailed {
324 /// Underlying IO error.
325 #[source]
326 source: std::io::Error,
327 },
328
329 /// JSON serialisation of the emitted OCI image config or manifest
330 /// blob failed. In practice this only happens if a value carried on
331 /// the [`crate::windows_builder::OciImageConfig`] is itself
332 /// unserialisable, which is a programmer error in this crate.
333 #[error("failed to serialise manifest/image config: {source}")]
334 SerializeManifestFailed {
335 /// Underlying `serde_json` error.
336 #[source]
337 source: serde_json::Error,
338 },
339
340 /// Top-level wrapper for any failure during the WCOW
341 /// [`crate::windows_builder::WindowsBuilder::push`] flow. Returned when
342 /// the push could not be completed but the failure is not better
343 /// described by [`BuildError::BlobUploadFailed`] or
344 /// [`BuildError::ManifestPutFailed`] — for example, when reading a
345 /// local layer blob off disk fails before it ever reaches the wire.
346 #[error("push of image {tag:?} failed: {reason}")]
347 PushFailed {
348 /// Target image tag the push was destined for, e.g.
349 /// `"ghcr.io/zorpxinc/zlayer-test:wcow-0.1"`.
350 tag: String,
351 /// Human-readable failure reason from the upstream layer.
352 reason: String,
353 },
354
355 /// A single layer blob upload PUT/PATCH chain failed. Carries the
356 /// blob's content-addressable digest so the operator can correlate
357 /// against registry-side logs.
358 #[error("failed to upload blob {digest} for tag {tag:?}: {reason}")]
359 BlobUploadFailed {
360 /// `sha256:...` digest of the blob that failed to upload.
361 digest: String,
362 /// Target image tag.
363 tag: String,
364 /// Human-readable failure reason from the upstream layer.
365 reason: String,
366 },
367
368 /// The final `PUT /v2/<name>/manifests/<tag>` request failed.
369 /// Distinct from [`BuildError::BlobUploadFailed`] because the manifest
370 /// PUT is the last write that makes the push observable from the
371 /// registry — a failure here means the layers and config blob may be
372 /// staged but the image is not yet visible under `tag`.
373 #[error("failed to PUT manifest for tag {tag:?}: {reason}")]
374 ManifestPutFailed {
375 /// Target image tag the manifest PUT targeted.
376 tag: String,
377 /// Human-readable failure reason from the upstream layer.
378 reason: String,
379 },
380}
381
382impl BuildError {
383 /// Create a `DockerfileParse` error from a message and line number
384 pub fn parse_error(msg: impl Into<String>, line: usize) -> Self {
385 Self::DockerfileParse {
386 message: msg.into(),
387 line,
388 }
389 }
390
391 /// Create a `ContextRead` error from a path and IO error
392 pub fn context_read(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
393 Self::ContextRead {
394 path: path.into(),
395 source,
396 }
397 }
398
399 /// Create a `PathEscape` error
400 pub fn path_escape(path: impl Into<PathBuf>) -> Self {
401 Self::PathEscape { path: path.into() }
402 }
403
404 /// Create a `StageNotFound` error
405 pub fn stage_not_found(name: impl Into<String>) -> Self {
406 Self::StageNotFound { name: name.into() }
407 }
408
409 /// Create a `RunFailed` error
410 pub fn run_failed(command: impl Into<String>, exit_code: i32) -> Self {
411 Self::RunFailed {
412 command: command.into(),
413 exit_code,
414 }
415 }
416
417 /// Create a `LayerCreate` error
418 pub fn layer_create(msg: impl Into<String>) -> Self {
419 Self::LayerCreate {
420 message: msg.into(),
421 }
422 }
423
424 /// Create a `CacheError`
425 pub fn cache_error(msg: impl Into<String>) -> Self {
426 Self::CacheError {
427 message: msg.into(),
428 }
429 }
430
431 /// Create a `RegistryError`
432 pub fn registry_error(msg: impl Into<String>) -> Self {
433 Self::RegistryError {
434 message: msg.into(),
435 }
436 }
437
438 /// Create an `InvalidInstruction` error
439 pub fn invalid_instruction(instruction: impl Into<String>, reason: impl Into<String>) -> Self {
440 Self::InvalidInstruction {
441 instruction: instruction.into(),
442 reason: reason.into(),
443 }
444 }
445
446 /// Create a `BuildahExecution` error
447 pub fn buildah_execution(
448 command: impl Into<String>,
449 exit_code: i32,
450 stderr: impl Into<String>,
451 ) -> Self {
452 Self::BuildahExecution {
453 command: command.into(),
454 exit_code,
455 stderr: stderr.into(),
456 }
457 }
458
459 /// Create a `BuildahNotFound` error
460 pub fn buildah_not_found(message: impl Into<String>) -> Self {
461 Self::BuildahNotFound {
462 message: message.into(),
463 }
464 }
465
466 /// Create a `ZImagefileParse` error
467 pub fn zimagefile_parse(message: impl Into<String>) -> Self {
468 Self::ZImagefileParse {
469 message: message.into(),
470 }
471 }
472
473 /// Create a `ZImagefileValidation` error
474 pub fn zimagefile_validation(message: impl Into<String>) -> Self {
475 Self::ZImagefileValidation {
476 message: message.into(),
477 }
478 }
479
480 /// Create a `PipelineError`
481 pub fn pipeline_error(message: impl Into<String>) -> Self {
482 Self::PipelineError {
483 message: message.into(),
484 }
485 }
486
487 /// Create a `NotYetImplemented` error. The `msg` should name the
488 /// follow-up task or phase that delivers the missing behavior
489 /// (e.g. `"RUN execution lands in Phase 4 task 4.B"`).
490 pub fn not_yet_implemented(msg: impl Into<String>) -> Self {
491 Self::NotYetImplemented(msg.into())
492 }
493
494 /// Create a `SandboxNeedsLinux` error signalling the build front-end to
495 /// fall back to the buildd-in-VZ Linux path.
496 pub fn sandbox_needs_linux(command: impl Into<String>, reason: impl Into<String>) -> Self {
497 Self::SandboxNeedsLinux {
498 command: command.into(),
499 reason: reason.into(),
500 }
501 }
502}
503
504/// Convert a `zlayer-toolchain` error into a `BuildError`.
505///
506/// The toolchain crate (the macOS keg provisioner — source build + prebuilt
507/// fetch) is a leaf crate, extracted from this crate to break the
508/// builder<->agent build cycle. Its errors flow back through `?` at the builder
509/// call sites (e.g. `ensure_toolchain(..).await?` when materializing a keg into
510/// a build rootfs), so they need a `From` bridge. The `IoError` arm is
511/// preserved structurally; everything else folds into `RegistryError` carrying
512/// the rendered message.
513impl From<zlayer_toolchain::ToolchainError> for BuildError {
514 fn from(err: zlayer_toolchain::ToolchainError) -> Self {
515 match err {
516 zlayer_toolchain::ToolchainError::IoError(io) => BuildError::IoError(io),
517 other => BuildError::RegistryError {
518 message: other.to_string(),
519 },
520 }
521 }
522}
523
524/// Result type alias for build operations
525pub type Result<T, E = BuildError> = std::result::Result<T, E>;
526
527#[cfg(test)]
528mod tests {
529 use super::*;
530
531 #[test]
532 fn test_error_display() {
533 let err = BuildError::parse_error("unexpected token", 42);
534 assert!(err.to_string().contains("line 42"));
535 assert!(err.to_string().contains("unexpected token"));
536 }
537
538 #[test]
539 fn test_path_escape_error() {
540 let err = BuildError::path_escape("/etc/passwd");
541 assert!(err.to_string().contains("/etc/passwd"));
542 assert!(err.to_string().contains("escape"));
543 }
544
545 #[test]
546 fn test_run_failed_error() {
547 let err = BuildError::run_failed("apt-get install foo", 127);
548 assert!(err.to_string().contains("exit code 127"));
549 assert!(err.to_string().contains("apt-get install foo"));
550 }
551}