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}
150
151fn default_local_position() -> String {
152 "after".to_string()
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
156pub struct RenderPolicy {
157 pub mode: String,
158 pub conflict: String,
159 pub churn: String,
160 pub header: bool,
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 }
171 }
172}
173
174impl Manifest {
175 pub fn canonicalized(&self) -> Self {
176 let mut manifest = self.clone();
177 manifest
178 .sources
179 .sort_by(|left, right| left.name.cmp(&right.name));
180 manifest
181 .targets
182 .sort_by(|left, right| left.name.cmp(&right.name));
183 manifest.packages.sort_by(|left, right| {
184 left.name
185 .cmp(&right.name)
186 .then(left.source.cmp(&right.source))
187 .then(left.constraint.cmp(&right.constraint))
188 });
189 manifest
190 .local
191 .sort_by(|left, right| left.path.cmp(&right.path));
192 manifest
193 }
194
195 pub fn manifest_hash(&self) -> PrayResult<String> {
196 let canonical = self.canonicalized();
197 let bytes = serde_json::to_vec(&canonical)
198 .map_err(|error| PrayError::Manifest(error.to_string()))?;
199 Ok(sha256_prefixed(&bytes))
200 }
201}
202
203pub fn read_manifest_text(manifest_path: &Path) -> PrayResult<String> {
204 fs::read_to_string(manifest_path).map_err(|error| {
205 if error.kind() == io::ErrorKind::NotFound {
206 let manifest_label = manifest_path
207 .file_name()
208 .and_then(|name| name.to_str())
209 .unwrap_or("Prayfile");
210 PrayError::Manifest(format!(
211 "missing {manifest_label}; run pray init to create one"
212 ))
213 } else {
214 PrayError::Io(error)
215 }
216 })
217}