1use crate::hashing::sha256_prefixed;
2use crate::{PrayError, PrayResult};
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use std::fs;
6use std::io;
7use std::path::Path;
8
9pub use crate::manifest_format::{format_package_declaration, replace_package_declaration};
10pub use crate::manifest_parse::parse_manifest;
11
12#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
13pub struct Manifest {
14 pub prayfile_version: String,
15 pub sources: Vec<ManifestSource>,
16 pub targets: Vec<ManifestTarget>,
17 pub packages: Vec<ManifestPackage>,
18 pub local: Vec<ManifestLocal>,
19 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
20 pub symbols: BTreeMap<String, String>,
21 pub render: RenderPolicy,
22 #[serde(default, skip)]
24 pub deprecated_keywords: Vec<String>,
25}
26
27impl Manifest {
28 pub fn note_deprecated_keyword(&mut self, keyword: &str) {
29 if !crate::deprecation::is_deprecated_prayfile_keyword(keyword) {
30 return;
31 }
32 if self
33 .deprecated_keywords
34 .iter()
35 .any(|existing| existing == keyword)
36 {
37 return;
38 }
39 self.deprecated_keywords.push(keyword.to_string());
40 }
41
42 pub fn deprecation_warnings(&self) -> Vec<String> {
43 crate::deprecation::deprecation_warnings_for(&self.deprecated_keywords)
44 }
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
48pub struct ManifestSource {
49 pub name: String,
50 pub kind: String,
51 pub url: String,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub subdir: Option<String>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub rev: Option<String>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub tag: Option<String>,
58}
59
60#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
61#[serde(rename_all = "snake_case")]
62pub enum DestinationMode {
63 #[default]
64 Legacy,
65 Compose,
66 Tree,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
70#[serde(tag = "kind", rename_all = "snake_case")]
71pub enum DestinationEntry {
72 Package { name: String },
73 Local { path: String },
74}
75
76#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
77#[serde(rename_all = "snake_case")]
78pub enum ExportRole {
79 Fragment,
80 Folder,
81 File,
82}
83
84#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
85pub struct ManifestTarget {
86 pub name: String,
87 #[serde(default)]
88 pub outputs: Vec<String>,
89 #[serde(default)]
90 pub skills: Vec<String>,
91 #[serde(default)]
92 pub commands: Vec<String>,
93 #[serde(default)]
94 pub rules: Vec<String>,
95 pub max_bytes: Option<u64>,
96 #[serde(default)]
97 pub mode: DestinationMode,
98 #[serde(default)]
99 pub scoped: bool,
100 #[serde(default)]
101 pub entries: Vec<DestinationEntry>,
102}
103
104#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
105pub struct ManifestPackage {
106 pub name: String,
107 #[serde(default = "default_constraint")]
108 pub constraint: String,
109 pub source: Option<String>,
110 #[serde(default)]
111 pub exports: Vec<String>,
112 #[serde(default)]
113 pub targets: Vec<String>,
114 #[serde(default)]
115 pub features: Vec<String>,
116 #[serde(default)]
117 pub groups: Vec<String>,
118 #[serde(default)]
119 pub optional: bool,
120 pub path: Option<String>,
121 pub git: Option<String>,
122 pub tag: Option<String>,
123 pub rev: Option<String>,
124 pub tarball: Option<String>,
125 pub oci: Option<String>,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub file: Option<String>,
128 #[serde(default, skip_serializing_if = "Vec::is_empty")]
129 pub roles: Vec<ExportRole>,
130 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
131 pub bound: bool,
132}
133
134fn default_constraint() -> String {
135 "*".to_string()
136}
137
138#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
139pub struct ManifestLocal {
140 pub path: String,
141 #[serde(default = "default_local_position")]
142 pub position: String,
143 #[serde(default)]
144 pub optional: bool,
145 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
146 pub bound: bool,
147}
148
149fn default_local_position() -> String {
150 "after".to_string()
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
154pub struct RenderPolicy {
155 pub mode: String,
156 pub conflict: String,
157 pub churn: String,
158 pub header: bool,
159 pub section_markers: bool,
160 pub line_endings: String,
161}
162
163impl Default for RenderPolicy {
164 fn default() -> Self {
165 Self {
166 mode: "managed".to_string(),
167 conflict: "fail".to_string(),
168 churn: "minimal".to_string(),
169 header: true,
170 section_markers: true,
171 line_endings: "lf".to_string(),
172 }
173 }
174}
175
176impl Manifest {
177 pub fn canonicalized(&self) -> Self {
178 let mut manifest = self.clone();
179 manifest
180 .sources
181 .sort_by(|left, right| left.name.cmp(&right.name));
182 manifest
183 .targets
184 .sort_by(|left, right| left.name.cmp(&right.name));
185 manifest.packages.sort_by(|left, right| {
186 left.name
187 .cmp(&right.name)
188 .then(left.source.cmp(&right.source))
189 .then(left.constraint.cmp(&right.constraint))
190 });
191 manifest
192 .local
193 .sort_by(|left, right| left.path.cmp(&right.path));
194 manifest
195 }
196
197 pub fn manifest_hash(&self) -> PrayResult<String> {
198 let canonical = self.canonicalized();
199 let bytes = serde_json::to_vec(&canonical)
200 .map_err(|error| PrayError::Manifest(error.to_string()))?;
201 Ok(sha256_prefixed(&bytes))
202 }
203}
204
205pub fn read_manifest_text(manifest_path: &Path) -> PrayResult<String> {
206 fs::read_to_string(manifest_path).map_err(|error| {
207 if error.kind() == io::ErrorKind::NotFound {
208 let manifest_label = manifest_path
209 .file_name()
210 .and_then(|name| name.to_str())
211 .unwrap_or("Prayfile");
212 PrayError::Manifest(format!(
213 "missing {manifest_label}; run pray init to create one"
214 ))
215 } else {
216 PrayError::Io(error)
217 }
218 })
219}
220