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
use std::io;
use camino::Utf8PathBuf;
use thiserror::Error;
/// Failure while discovering, generating, building, or validating an Apple artifact.
#[derive(Debug, Error)]
pub enum AppleError {
/// Project icon or splash asset validation failed.
#[error(transparent)]
Assets(#[from] rustferry_core::AssetError),
/// Deterministic platform asset generation or cache validation failed.
#[error(transparent)]
AssetPipeline(#[from] rustferry_codegen::AssetPipelineError),
/// A request value cannot safely enter a generated project or process argument.
#[error("invalid iOS build request: {0}")]
InvalidRequest(String),
/// `ferry.toml` failed semantic validation.
#[error("invalid RustFerry configuration: {0}")]
InvalidConfig(String),
/// iOS builds were requested on a non-macOS host.
#[error(
"iOS builds require macOS; detected host `{host}`. Run this build on a Mac with full Xcode installed"
)]
UnsupportedHost {
/// Detected host operating system.
host: String,
},
/// A required Apple or Rust executable was not found.
#[error("Apple build tool `{tool}` was not found; searched: {searched:?}. {fix}")]
ToolMissing {
/// Human-readable executable name.
tool: String,
/// Candidate paths inspected.
searched: Vec<Utf8PathBuf>,
/// Concrete remediation.
fix: String,
},
/// The selected developer directory is not a full Xcode installation.
#[error("full Xcode was not found; developer directory: {developer_dir:?}. {fix}")]
XcodeMissing {
/// Selected developer directory, when one was found.
developer_dir: Option<Utf8PathBuf>,
/// Concrete remediation.
fix: String,
},
/// Xcode does not expose an iPhone Simulator SDK.
#[error("an iPhone Simulator SDK was not found through xcrun. {fix}")]
SimulatorSdkMissing {
/// Concrete remediation.
fix: String,
},
/// The required Rust target is absent.
#[error(
"Rust target `{target}` is not installed. Run `rustup target add {target}`, then `cargo ferry doctor`"
)]
RustTargetMissing {
/// Missing Rust target triple.
target: String,
},
/// An external command could not be started.
#[error("could not start `{program}` during {stage}: {source}")]
CommandSpawn {
/// Build or validation stage.
stage: String,
/// Executable path.
program: Utf8PathBuf,
/// Operating-system error.
#[source]
source: io::Error,
},
/// An external command exceeded its deadline.
#[error("`{program}` timed out during {stage}; log: {log:?}")]
CommandTimedOut {
/// Build or validation stage.
stage: String,
/// Executable path.
program: Utf8PathBuf,
/// Redacted diagnostic log, when requested.
log: Option<Utf8PathBuf>,
},
/// An external command exceeded the per-stream output memory limit.
#[error(
"`{program}` produced more than {limit_bytes} bytes on {stream} during {stage}; its process tree was stopped; log: {log:?}"
)]
ProcessOutputTooLarge {
/// Build or validation stage.
stage: String,
/// Executable path.
program: Utf8PathBuf,
/// Stream that crossed the limit.
stream: String,
/// Maximum retained bytes for each stream.
limit_bytes: usize,
/// Diagnostic log containing only the retained prefixes.
log: Option<Utf8PathBuf>,
},
/// Cancellation interrupted an external build tool.
#[error("`{program}` was interrupted during {stage}")]
CommandInterrupted {
/// Build or validation stage.
stage: String,
/// Executable path.
program: Utf8PathBuf,
},
/// An external command returned a non-zero status.
#[error("`{program}` failed during {stage} with status {status}; {summary}; log: {log:?}")]
CommandFailed {
/// Build or validation stage.
stage: String,
/// Executable path.
program: Utf8PathBuf,
/// Exit code or signal description.
status: String,
/// Short stderr/stdout excerpt.
summary: String,
/// Redacted diagnostic log, when requested.
log: Option<Box<Utf8PathBuf>>,
},
/// A filesystem operation failed.
#[error("could not {operation} `{path}`: {source}")]
Io {
/// Operation being attempted.
operation: &'static str,
/// Affected path.
path: Utf8PathBuf,
/// Operating-system error.
#[source]
source: io::Error,
},
/// A generated path escaped or traversed a protected boundary.
#[error("unsafe generated path `{path}`: {reason}")]
UnsafeGeneratedPath {
/// Rejected path.
path: Utf8PathBuf,
/// Exact invariant that failed.
reason: String,
},
/// Independent artifact validation rejected an app or extension bundle.
#[error("iOS artifact validation failed for `{path}`: {reason}")]
InvalidArtifact {
/// Rejected artifact path.
path: Utf8PathBuf,
/// Exact failed invariant.
reason: String,
},
/// A platform path cannot be represented by this crate's UTF-8 API.
#[error("path is not valid UTF-8: {0:?}")]
NonUtf8Path(std::path::PathBuf),
}
pub(crate) fn io_error(
operation: &'static str,
path: impl Into<Utf8PathBuf>,
source: io::Error,
) -> AppleError {
AppleError::Io {
operation,
path: path.into(),
source,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn command_failure_stays_compact_and_preserves_its_diagnostic() {
assert!(std::mem::size_of::<AppleError>() < 128);
let error = AppleError::CommandFailed {
stage: "archive application".to_owned(),
program: Utf8PathBuf::from("xcodebuild"),
status: "65".to_owned(),
summary: "archive failed".to_owned(),
log: Some(Box::new(Utf8PathBuf::from("archive.log"))),
};
assert_eq!(
error.to_string(),
"`xcodebuild` failed during archive application with status 65; archive failed; log: Some(\"archive.log\")"
);
}
}