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
//! What a dog's manifest says it needs. `shep adopt` and `shep enable` warn
//! of what is unmet and go on; `shep install` plans the missing dogs and
//! refuses when the index cannot supply one.
use shep_core::dogs::manifest::Manifest;
use crate::dog_index::{AvailableDog, DogSourceKind};
use crate::exit::ExitCode;
use crate::output::Streams;
/// [`emit_notice`](crate::output::emit_notice) code for a dependency dog that
/// is not adopted; not a failure.
const DOG_DEPENDENCY_NOTICE: &str = "dog_dependency_missing";
/// [`emit_notice`](crate::output::emit_notice) code for a dog that needs a
/// newer shep than this one; not a failure.
const DOG_SHEP_TOO_OLD_NOTICE: &str = "dog_needs_newer_shep";
/// Whether the dog `name` is built in or in `adopted`.
fn is_present(name: &str, adopted: &[String]) -> bool {
crate::dog::BUILT_IN_DOGS.contains(&name) || adopted.iter().any(|dog| dog == name)
}
/// Why `shep install` cannot supply a dog's dependencies.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum Refusal {
/// The index has no dog called `dependency`.
NotIndexed {
dependent: String,
dependency: String,
},
/// The index lists `dependency` but approves no version shep can build.
NotInstallable {
dependent: String,
dependency: String,
},
/// `range`, as the manifest declares it, is not a version range.
BadRange {
dependent: String,
dependency: String,
range: String,
},
/// The index approves `version` of `dependency`, outside `range`.
Unmet {
dependent: String,
dependency: String,
range: String,
version: String,
},
/// `dependency` is already being installed, so it and `dependent` wait
/// on each other. `trail` is the dogs being installed, outermost first.
Cycle {
dependent: String,
dependency: String,
trail: Vec<String>,
},
}
impl Refusal {
/// The exit code `shep install` refuses with.
pub(super) fn code(&self) -> ExitCode {
match self {
Self::NotIndexed { .. } => ExitCode::NotFound,
_ => ExitCode::Failure,
}
}
}
impl core::fmt::Display for Refusal {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NotIndexed {
dependent,
dependency,
} => write!(
f,
"`{dependent}` depends on `{dependency}`, which the dog index does not carry; \
adopt it with `shep adopt <path>` first"
),
Self::NotInstallable {
dependent,
dependency,
} => write!(
f,
"`{dependent}` depends on `{dependency}`, which the dog index lists but approves \
no version shep can install; adopt it with `shep adopt <path>` first"
),
Self::BadRange {
dependent,
dependency,
range,
} => write!(
f,
"`{dependent}` depends on `{dependency}` at {range:?}, which is not a version range"
),
Self::Unmet {
dependent,
dependency,
range,
version,
} => write!(
f,
"`{dependent}` needs `{dependency}` {range}, and the dog index approves \
{version}"
),
Self::Cycle {
dependent,
dependency,
trail,
} => write!(
f,
"`{dependent}` depends on `{dependency}`, which is already being installed \
({}): a dependency cycle",
trail
.iter()
.map(String::as_str)
.chain([dependency.as_str()])
.collect::<Vec<_>>()
.join(" -> ")
),
}
}
}
impl core::error::Error for Refusal {}
/// The dogs `manifest` needs that are not yet present, as index rows in
/// manifest order, for `shep install` to install first.
///
/// `dependent` is the dog being installed and `trail` every dog being
/// installed, outermost first, ending in `dependent`. A dependency adopted already is
/// skipped whatever its version, as at enable.
///
/// # Errors
/// The [`Refusal`] for the first missing dependency the index does not carry
/// or cannot supply in the declared range, or that is already in `trail`.
pub(super) fn missing<'a>(
manifest: &Manifest,
dependent: &str,
trail: &[String],
adopted: &[String],
index: &'a [AvailableDog],
) -> Result<Vec<&'a AvailableDog>, Refusal> {
let mut wanted: Vec<&AvailableDog> = Vec::new();
for dependency in &manifest.depends.dogs {
let name = dependency.name.as_str();
if is_present(name, adopted) || wanted.iter().any(|dog| dog.adopt_as == name) {
continue;
}
let (dependent, dependency_name) = (dependent.to_owned(), name.to_owned());
if trail.iter().any(|above| above == name) {
return Err(Refusal::Cycle {
dependent,
dependency: dependency_name,
trail: trail.to_vec(),
});
}
let Some(row) = index.iter().find(|dog| dog.adopt_as == name) else {
return Err(Refusal::NotIndexed {
dependent,
dependency: dependency_name,
});
};
let DogSourceKind::Cargo {
version: Some(version),
..
} = &row.source
else {
return Err(Refusal::NotInstallable {
dependent,
dependency: dependency_name,
});
};
let range =
semver::VersionReq::parse(&dependency.version).map_err(|_| Refusal::BadRange {
dependent: dependent.clone(),
dependency: dependency_name.clone(),
range: dependency.version.clone(),
})?;
let met = semver::Version::parse(version).is_ok_and(|version| range.matches(&version));
if !met {
return Err(Refusal::Unmet {
dependent,
dependency: dependency_name,
range: dependency.version.clone(),
version: version.clone(),
});
}
wanted.push(row);
}
Ok(wanted)
}
/// Warns of each dog `manifest` depends on that is neither built in nor in
/// `adopted`, and of a minimum shep newer than this build.
///
/// A dependency is checked by name: which version of it is installed is not
/// read. A minimum that is not a version is not judged.
pub(super) fn warn_of_unmet_depends(
streams: &mut Streams<'_>,
name: &str,
manifest: &Manifest,
adopted: &[String],
) {
for dependency in &manifest.depends.dogs {
if !is_present(&dependency.name, adopted) {
let message = format!(
"`{name}` depends on `{dep}`, which is not adopted, so `{name}` may not work \
until you run `shep adopt` for `{dep}`",
dep = dependency.name
);
streams.aside(DOG_DEPENDENCY_NOTICE, &message);
}
}
let Some(minimum) = &manifest.depends.shep else {
return;
};
let this = env!("CARGO_PKG_VERSION");
let too_old = match (
semver::Version::parse(minimum),
semver::Version::parse(this),
) {
(Ok(minimum), Ok(this)) => this < minimum,
_ => false,
};
if too_old {
let message = format!(
"`{name}` needs shep {minimum} or newer, and this is shep {this}, so `{name}` may \
not work until you upgrade shep"
);
streams.aside(DOG_SHEP_TOO_OLD_NOTICE, &message);
}
}