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
//! Error taxonomy for project-level workflow packaging.
use std::path::PathBuf;
use crate::PackageError;
/// Errors produced while packaging a built Gleam workflow project.
///
/// Every variant carries the offending path, field, or module as structured
/// data — not just formatted text — so callers can map variants to actionable
/// guidance (for example, [`PackagingError::ProjectNotBuilt`] maps to "run
/// `gleam build`").
#[derive(thiserror::Error, Debug)]
pub enum PackagingError {
/// The project root contains no `workflow.toml` packaging descriptor.
#[error("no workflow.toml found in {root}")]
ConfigMissing {
/// Project root that was searched for the descriptor.
root: PathBuf,
},
/// The `workflow.toml` descriptor exists but could not be read.
#[error("failed to read {path}: {source}")]
ConfigRead {
/// Path of the descriptor that could not be read.
path: PathBuf,
/// I/O failure reported while reading the descriptor.
source: std::io::Error,
},
/// The `workflow.toml` descriptor is not valid TOML for the schema.
///
/// The wrapped error carries unknown-key detail: every table rejects
/// unrecognised keys, naming the key and its location.
#[error("failed to parse {path}: {source}")]
ConfigParse {
/// Path of the descriptor that failed to parse.
path: PathBuf,
/// TOML deserialisation failure, including unknown-key detail.
source: toml::de::Error,
},
/// The `workflow.toml` descriptor parsed but failed semantic validation.
#[error("invalid workflow.toml: {field}: {reason}")]
ConfigInvalid {
/// Descriptor field that failed validation, e.g. `workflow[0].entry_module`.
field: String,
/// Human-readable reason the field value was rejected.
reason: String,
},
/// A declared JSON-Schema file could not be read.
#[error("failed to read schema {path}: {source}")]
SchemaRead {
/// Resolved path of the schema file that could not be read.
path: PathBuf,
/// I/O failure reported while reading the schema file.
source: std::io::Error,
},
/// A declared JSON-Schema file is not valid JSON.
#[error("schema {path} is not valid JSON: {source}")]
SchemaParse {
/// Resolved path of the schema file that failed to parse.
path: PathBuf,
/// JSON parsing failure reported by `serde_json`.
source: serde_json::Error,
},
/// The compiled Erlang output required for packaging does not exist.
#[error("project is not built: {missing} does not exist; run `gleam build` first")]
ProjectNotBuilt {
/// Build-output path that was required but absent.
missing: PathBuf,
},
/// The project root contains no `gleam.toml`, so it is not a Gleam project.
#[error("not a Gleam project: {path} not found")]
GleamTomlMissing {
/// Path where `gleam.toml` was expected.
path: PathBuf,
},
/// A Gleam metadata file (`gleam.toml` or the `manifest.toml` lockfile)
/// could not be read.
#[error("failed to read Gleam metadata {path}: {source}")]
GleamMetadataRead {
/// Path of the metadata file that could not be read.
path: PathBuf,
/// I/O failure reported while reading the metadata file.
source: std::io::Error,
},
/// A Gleam metadata file could not be parsed as the expected TOML shape.
#[error("failed to parse Gleam metadata {path}: {source}")]
GleamMetadataParse {
/// Path of the metadata file that failed to parse.
path: PathBuf,
/// TOML deserialisation failure reported while parsing the file.
source: toml::de::Error,
},
/// A production dependency named in `gleam.toml` is absent from the
/// `manifest.toml` lockfile, so the dependency closure cannot be computed.
#[error("dependency `{package}` is in gleam.toml but missing from manifest.toml; rebuild")]
DependencyUnresolved {
/// Gleam package name that could not be resolved in the lockfile.
package: String,
},
/// A compiled `.beam` module (or its containing directory) could not be read.
#[error("failed to read compiled module {path}: {source}")]
BeamRead {
/// Path of the compiled module or module directory that failed to read.
path: PathBuf,
/// I/O failure reported while reading the compiled output.
source: std::io::Error,
},
/// A compiled module filename is not valid UTF-8 and cannot become a
/// logical module name.
#[error("compiled module filename is not valid UTF-8: {path}")]
ModuleNameNotUtf8 {
/// Path whose filename failed UTF-8 decoding.
path: PathBuf,
},
/// Two Gleam packages in the production dependency closure provide the same
/// logical module name.
#[error("module `{module}` is provided by both `{first}` and `{second}`")]
DuplicateModule {
/// Logical module name provided more than once.
module: String,
/// Gleam package that provided the module first.
first: String,
/// Gleam package that provided the module again.
second: String,
},
/// A declared workflow entry module is absent from the compiled output.
#[error("entry module `{module}` not found in compiled output under {searched}")]
EntryModuleNotFound {
/// Entry module declared by `workflow.toml`.
module: String,
/// Compiled-output directory that was searched.
searched: PathBuf,
},
/// A first-party Gleam source file could not be read for inclusion.
#[error("failed to read source file {path}: {source}")]
SourceRead {
/// Path of the source file or directory that could not be read.
path: PathBuf,
/// I/O failure reported while reading the source tree.
source: std::io::Error,
},
/// A `workflow.toml`-declared path is absolute or escapes the project
/// root after lexically folding `.` and `..` components.
///
/// Only descriptor-sourced paths (`output`, `input_schema`,
/// `output_schema`) are confined to the root; the programmatic
/// [`PackageOptions::output_override`](crate::PackageOptions) is the
/// caller's own path and is intentionally exempt.
#[error("invalid workflow.toml: {field}: path {path} is absolute or escapes the project root")]
PathEscapesRoot {
/// Descriptor field that declared the path, e.g. `workflow[0].output`.
field: String,
/// The offending path exactly as declared in the descriptor.
path: PathBuf,
},
/// Two workflows resolve to the same output archive path.
#[error("workflows `{first}` and `{second}` both write to {path}")]
OutputConflict {
/// Entry module of the workflow that claimed the path first.
first: String,
/// Entry module of the workflow that claimed the path again.
second: String,
/// Output path claimed by both workflows.
path: PathBuf,
},
/// An output override was supplied for a project declaring multiple workflows.
#[error("--out is only valid for single-workflow projects ({count} declared)")]
OutputOverrideAmbiguous {
/// Number of workflows the project declares.
count: usize,
},
/// A workflow's `.aion` archive could not be built or written to its
/// resolved output path.
///
/// Unlike the transparent [`PackagingError::Package`] variant, this one
/// names the output path, so "No such file or directory"-style I/O
/// failures identify the file that could not be written.
#[error("failed to write archive {path}: {source}")]
OutputWrite {
/// Resolved output path the archive could not be written to.
path: PathBuf,
/// Package-format failure reported while building or writing.
source: PackageError,
},
/// A package-format failure surfaced while building, writing, or re-loading
/// an archive (reserved module names, write I/O, verify-after-write).
#[error(transparent)]
Package(#[from] PackageError),
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::PackagingError;
fn assert_send_sync<T: Send + Sync + 'static>() {}
#[test]
fn packaging_error_is_send_sync_and_static() {
assert_send_sync::<PackagingError>();
}
#[test]
fn display_messages_name_the_failed_condition() {
assert_eq!(
PackagingError::ConfigMissing {
root: PathBuf::from("/project"),
}
.to_string(),
"no workflow.toml found in /project"
);
assert_eq!(
PackagingError::ConfigInvalid {
field: "workflow[0].timeout_seconds".to_owned(),
reason: "must be at least 1".to_owned(),
}
.to_string(),
"invalid workflow.toml: workflow[0].timeout_seconds: must be at least 1"
);
assert_eq!(
PackagingError::ProjectNotBuilt {
missing: PathBuf::from("/project/build/dev/erlang"),
}
.to_string(),
"project is not built: /project/build/dev/erlang does not exist; \
run `gleam build` first"
);
assert_eq!(
PackagingError::DependencyUnresolved {
package: "gleam_json".to_owned(),
}
.to_string(),
"dependency `gleam_json` is in gleam.toml but missing from manifest.toml; rebuild"
);
assert_eq!(
PackagingError::DuplicateModule {
module: "shared".to_owned(),
first: "pkg_a".to_owned(),
second: "pkg_b".to_owned(),
}
.to_string(),
"module `shared` is provided by both `pkg_a` and `pkg_b`"
);
assert_eq!(
PackagingError::EntryModuleNotFound {
module: "ghost".to_owned(),
searched: PathBuf::from("/project/build/dev/erlang"),
}
.to_string(),
"entry module `ghost` not found in compiled output under /project/build/dev/erlang"
);
assert_eq!(
PackagingError::OutputConflict {
first: "alpha".to_owned(),
second: "beta".to_owned(),
path: PathBuf::from("/project/alpha.aion"),
}
.to_string(),
"workflows `alpha` and `beta` both write to /project/alpha.aion"
);
assert_eq!(
PackagingError::OutputOverrideAmbiguous { count: 3 }.to_string(),
"--out is only valid for single-workflow projects (3 declared)"
);
assert_eq!(
PackagingError::PathEscapesRoot {
field: "workflow[0].output".to_owned(),
path: PathBuf::from("../escape.aion"),
}
.to_string(),
"invalid workflow.toml: workflow[0].output: path ../escape.aion \
is absolute or escapes the project root"
);
let output_write = PackagingError::OutputWrite {
path: PathBuf::from("/project/missing/demo.aion"),
source: crate::PackageError::ArchiveWriteIo {
source: std::io::Error::from(std::io::ErrorKind::NotFound),
},
};
assert!(
output_write
.to_string()
.starts_with("failed to write archive /project/missing/demo.aion: ")
);
assert!(std::error::Error::source(&output_write).is_some());
}
#[test]
fn package_error_converts_transparently() {
let error = PackagingError::from(crate::PackageError::MissingManifest);
assert_eq!(error.to_string(), "missing required manifest.json entry");
assert!(matches!(error, PackagingError::Package(_)));
}
}