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 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub header: Option<bool>,
104}
105
106#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
107pub struct ManifestPackage {
108 pub name: String,
109 #[serde(default = "default_constraint")]
110 pub constraint: String,
111 pub source: Option<String>,
112 #[serde(default)]
113 pub exports: Vec<String>,
114 #[serde(default)]
115 pub targets: Vec<String>,
116 #[serde(default)]
117 pub features: Vec<String>,
118 #[serde(default)]
119 pub groups: Vec<String>,
120 #[serde(default)]
121 pub optional: bool,
122 pub path: Option<String>,
123 pub git: Option<String>,
124 pub tag: Option<String>,
125 pub rev: Option<String>,
126 pub tarball: Option<String>,
127 pub oci: Option<String>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub file: Option<String>,
130 #[serde(default, skip_serializing_if = "Vec::is_empty")]
131 pub roles: Vec<ExportRole>,
132 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
133 pub bound: bool,
134}
135
136fn default_constraint() -> String {
137 "*".to_string()
138}
139
140#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
141pub struct ManifestLocal {
142 pub path: String,
143 #[serde(default = "default_local_position")]
144 pub position: String,
145 #[serde(default)]
146 pub optional: bool,
147 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
148 pub bound: bool,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub file: Option<String>,
151}
152
153fn default_local_position() -> String {
154 "after".to_string()
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
158pub struct RenderPolicy {
159 pub mode: String,
160 pub conflict: String,
161 pub churn: String,
162 pub header: bool,
163}
164
165impl Default for RenderPolicy {
166 fn default() -> Self {
167 Self {
168 mode: "managed".to_string(),
169 conflict: "fail".to_string(),
170 churn: "minimal".to_string(),
171 header: true,
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}