Skip to main content

pray_core/
manifest.rs

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    /// Deprecated Prayfile keywords encountered while parsing (`target`, `output`, `agent`).
23    #[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}