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