1use std::collections::{BTreeMap, BTreeSet};
6use std::fmt;
7
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11use crate::config::Host;
12use crate::digest::DigestWriter;
13use crate::errors::{ConfigError, StoreError};
14use crate::jigsaw::digest::host_facts_digest;
15use crate::jigsaw::releases::{HasHostTag, SeriesReleases, SeriesReleasesError};
16use crate::jigsaw::tags::{OtpLine, OtpLineError};
17use crate::model::names::{
18 ApplicationName, CommitSha, DependencyName, ExtractorVersion, HostFactsDigest, HostName,
19 PlanCacheKey, ProjectName, TagName, vocabulary,
20};
21use crate::model::non_empty::NonEmpty;
22use crate::model::pin::Pin;
23use crate::model::release_version::{
24 ErlangMajor, ErlangVersionRange, HostSeries, HostTag, OtpLineVersion,
25};
26use crate::model::snapshot::FORMAT_VERSION;
27
28const CACHE_KEY_DIGEST_BYTES: usize = 8;
29
30vocabulary!(
31 pub enum TagSourceKind: "tag source" {
32 Repository => "repository",
33 Store => "store",
34 }
35);
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct HostRelease {
39 pub tag: HostTag,
40 pub commit: CommitSha,
41 pub pins: BTreeMap<ProjectName, TagName>,
42 pub erlang_support: ErlangVersionRange,
43}
44
45impl HasHostTag for HostRelease {
46 fn host_tag(&self) -> &HostTag {
47 &self.tag
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct ReleaseFacts {
53 pub tag: HostTag,
54 pub commit: CommitSha,
55 pub pins: BTreeMap<ProjectName, TagName>,
56}
57
58impl HasHostTag for ReleaseFacts {
59 fn host_tag(&self) -> &HostTag {
60 &self.tag
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct UnsupportedMajor {
66 pub release: TagName,
67 pub range: ErlangVersionRange,
68 pub line: OtpLineVersion,
69}
70
71impl fmt::Display for UnsupportedMajor {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 write!(
74 f,
75 "{} supports Erlang {}, which excludes {} (major {})",
76 self.release,
77 self.range,
78 self.line,
79 self.line.major()
80 )
81 }
82}
83
84fn join_failures(failures: &[UnsupportedMajor]) -> String {
85 failures
86 .iter()
87 .map(ToString::to_string)
88 .collect::<Vec<_>>()
89 .join("; ")
90}
91
92#[derive(Debug, Error)]
93pub enum PlanError {
94 #[error("no release of series {series} matches the host's tag pattern")]
95 EmptySeries { series: HostSeries },
96
97 #[error("--up-to {up_to} is not a release of series {series}")]
98 UpToNotInSeries { up_to: TagName, series: HostSeries },
99
100 #[error("no OTP release tag for Erlang major {major}")]
101 NoOtpTags { major: ErlangMajor },
102
103 #[error("{}", join_failures(failures))]
104 ErlangMajorUnsupported { failures: Vec<UnsupportedMajor> },
105
106 #[error("release {tag}: {detail}")]
107 ReleaseUnreadable { tag: TagName, detail: String },
108
109 #[error("plan field {field}: {detail}")]
110 Wire { field: &'static str, detail: String },
111
112 #[error(transparent)]
113 Releases(#[from] SeriesReleasesError),
114
115 #[error(transparent)]
116 OtpLine(#[from] OtpLineError),
117
118 #[error(transparent)]
119 Config(#[from] ConfigError),
120
121 #[error(transparent)]
122 Store(#[from] StoreError),
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct JigsawPlan {
127 host: HostName,
128 host_project: ProjectName,
129 otp_project: ProjectName,
130 releases: SeriesReleases<HostRelease>,
131 otp_lines: NonEmpty<OtpLine>,
132 untracked_dependencies: BTreeSet<DependencyName>,
133 excluded_otp_apps: BTreeSet<ApplicationName>,
134 host_facts: HostFactsDigest,
135 tag_source: TagSourceKind,
136 extractor_version: ExtractorVersion,
137 cache_key: PlanCacheKey,
138}
139
140pub(super) struct PlanParts {
141 pub(super) host: HostName,
142 pub(super) host_project: ProjectName,
143 pub(super) otp_project: ProjectName,
144 pub(super) releases: SeriesReleases<HostRelease>,
145 pub(super) otp_lines: NonEmpty<OtpLine>,
146 pub(super) untracked_dependencies: BTreeSet<DependencyName>,
147 pub(super) excluded_otp_apps: BTreeSet<ApplicationName>,
148 pub(super) host_facts: HostFactsDigest,
149 pub(super) tag_source: TagSourceKind,
150 pub(super) extractor_version: ExtractorVersion,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct DraftInputs {
155 pub releases: SeriesReleases<ReleaseFacts>,
156 pub otp_lines: NonEmpty<OtpLine>,
157 pub untracked_dependencies: BTreeSet<DependencyName>,
158 pub excluded_otp_apps: BTreeSet<ApplicationName>,
159 pub tag_source: TagSourceKind,
160 pub extractor_version: ExtractorVersion,
161}
162
163pub fn draft_plan(host: &Host, inputs: DraftInputs) -> Result<JigsawPlan, PlanError> {
164 let releases = inputs.releases.map(|facts| HostRelease {
165 erlang_support: host.erlang_support.range_for(facts.tag.version()).clone(),
166 tag: facts.tag,
167 commit: facts.commit,
168 pins: facts.pins,
169 })?;
170 JigsawPlan::assemble(PlanParts {
171 host: host.name.clone(),
172 host_project: host.project.clone(),
173 otp_project: host.otp_project.clone(),
174 releases,
175 otp_lines: inputs.otp_lines,
176 untracked_dependencies: inputs.untracked_dependencies,
177 excluded_otp_apps: inputs.excluded_otp_apps,
178 host_facts: host_facts_digest(&host.pins, &host.release_roots),
179 tag_source: inputs.tag_source,
180 extractor_version: inputs.extractor_version,
181 })
182}
183
184impl JigsawPlan {
185 pub(super) fn assemble(parts: PlanParts) -> Result<Self, PlanError> {
186 let failures: Vec<UnsupportedMajor> = parts
187 .releases
188 .iter()
189 .flat_map(|release| {
190 parts
191 .otp_lines
192 .iter()
193 .filter(|line| !release.erlang_support.contains(line.line()))
194 .map(|line| UnsupportedMajor {
195 release: release.tag.tag().clone(),
196 range: release.erlang_support.clone(),
197 line: line.line(),
198 })
199 })
200 .collect();
201 if !failures.is_empty() {
202 return Err(PlanError::ErlangMajorUnsupported { failures });
203 }
204 let required = required_pins(
205 &parts.host_project,
206 &parts.releases,
207 &parts.otp_project,
208 &parts.otp_lines,
209 );
210 let cache_key = cache_key(&parts, &required);
211 Ok(Self {
212 host: parts.host,
213 host_project: parts.host_project,
214 otp_project: parts.otp_project,
215 releases: parts.releases,
216 otp_lines: parts.otp_lines,
217 untracked_dependencies: parts.untracked_dependencies,
218 excluded_otp_apps: parts.excluded_otp_apps,
219 host_facts: parts.host_facts,
220 tag_source: parts.tag_source,
221 extractor_version: parts.extractor_version,
222 cache_key,
223 })
224 }
225
226 #[must_use]
227 pub fn host(&self) -> &HostName {
228 &self.host
229 }
230
231 #[must_use]
232 pub fn host_project(&self) -> &ProjectName {
233 &self.host_project
234 }
235
236 #[must_use]
237 pub fn otp_project(&self) -> &ProjectName {
238 &self.otp_project
239 }
240
241 #[must_use]
242 pub fn series(&self) -> HostSeries {
243 self.releases.series()
244 }
245
246 #[must_use]
247 pub fn releases(&self) -> &SeriesReleases<HostRelease> {
248 &self.releases
249 }
250
251 #[must_use]
252 pub fn otp_lines(&self) -> &NonEmpty<OtpLine> {
253 &self.otp_lines
254 }
255
256 #[must_use]
257 pub fn untracked_dependencies(&self) -> &BTreeSet<DependencyName> {
258 &self.untracked_dependencies
259 }
260
261 #[must_use]
262 pub fn excluded_otp_apps(&self) -> &BTreeSet<ApplicationName> {
263 &self.excluded_otp_apps
264 }
265
266 #[must_use]
267 pub fn host_facts(&self) -> &HostFactsDigest {
268 &self.host_facts
269 }
270
271 #[must_use]
272 pub fn tag_source(&self) -> TagSourceKind {
273 self.tag_source
274 }
275
276 #[must_use]
277 pub fn extractor_version(&self) -> &ExtractorVersion {
278 &self.extractor_version
279 }
280
281 #[must_use]
282 pub fn cache_key(&self) -> &PlanCacheKey {
283 &self.cache_key
284 }
285
286 #[must_use]
287 pub fn erlang_majors(&self) -> BTreeSet<ErlangMajor> {
288 erlang_majors(&self.otp_lines)
289 }
290
291 #[must_use]
292 pub fn required(&self) -> BTreeSet<Pin> {
293 required_pins(
294 &self.host_project,
295 &self.releases,
296 &self.otp_project,
297 &self.otp_lines,
298 )
299 }
300}
301
302fn required_pins(
303 host_project: &ProjectName,
304 releases: &SeriesReleases<HostRelease>,
305 otp_project: &ProjectName,
306 otp_lines: &NonEmpty<OtpLine>,
307) -> BTreeSet<Pin> {
308 let mut pins = BTreeSet::new();
309 for release in releases {
310 pins.insert(Pin::new(host_project.clone(), release.tag.tag().clone()));
311 for (project, tag) in &release.pins {
312 pins.insert(Pin::new(project.clone(), tag.clone()));
313 }
314 }
315 for line in otp_lines {
316 for tag in line.tags() {
317 pins.insert(Pin::new(otp_project.clone(), tag.tag().clone()));
318 }
319 }
320 pins
321}
322
323fn erlang_majors(otp_lines: &NonEmpty<OtpLine>) -> BTreeSet<ErlangMajor> {
324 otp_lines.iter().map(|line| line.line().major()).collect()
325}
326
327fn cache_key(parts: &PlanParts, required: &BTreeSet<Pin>) -> PlanCacheKey {
328 let mut writer = DigestWriter::new("jigsaw plan cache key");
329 writer.count(required.len());
330 for pin in required {
331 writer.text(pin.project.as_str()).text(pin.tag.as_str());
332 }
333 let majors = erlang_majors(&parts.otp_lines);
334 writer.count(majors.len());
335 for major in majors {
336 writer.text(&major.to_string());
337 }
338 writer.text(parts.host_facts.as_str());
339 let digest = writer.finish();
340 let mut short = [0; CACHE_KEY_DIGEST_BYTES];
341 short.copy_from_slice(&digest[..CACHE_KEY_DIGEST_BYTES]);
342 PlanCacheKey::compose(
343 &parts.host,
344 parts.releases.series(),
345 FORMAT_VERSION,
346 &parts.extractor_version,
347 short,
348 )
349}