1use crate::hashing::sha256_prefixed;
2use crate::render::RenderedTarget;
3use crate::{PrayError, PrayResult};
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::{Component, Path, PathBuf};
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
9pub struct Lockfile {
10 pub prayfile_lock: String,
11 pub spec: String,
12 pub generated_by: String,
13 pub manifest_hash: String,
14 #[serde(default, skip_serializing_if = "Option::is_none")]
15 pub environment: Option<String>,
16 pub source: Vec<LockSource>,
17 pub package: Vec<LockedPackage>,
18 pub target: Vec<LockedTarget>,
19 pub managed_span: Vec<ManagedSpanRecord>,
20 #[serde(default, skip_serializing_if = "Vec::is_empty")]
21 pub provisioned: Vec<ProvisionedFileRecord>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25pub struct LockSource {
26 pub name: String,
27 pub kind: String,
28 pub url: String,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub revision: Option<String>,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub host_key_fingerprint: Option<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
36pub struct LockedPackage {
37 pub name: String,
38 pub version: String,
39 pub source: Option<String>,
40 pub path: String,
41 pub tree_hash: String,
42 pub artifact_hash: String,
43 pub artifact: String,
44 pub exports: Vec<String>,
45 pub dependencies: Vec<String>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub signer_fingerprint: Option<String>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
51pub struct LockedTarget {
52 pub name: String,
53 pub outputs: Vec<String>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
57pub struct ManagedSpanRecord {
58 pub id: String,
59 pub target: String,
60 pub open_line: usize,
61 pub close_line: usize,
62 pub ideal_checksum: String,
63 pub package: String,
64 pub export: String,
65 pub source_checksum: String,
66 pub silenced: bool,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
70pub struct ProvisionedFileRecord {
71 pub path: String,
72 pub content_hash: String,
73 pub package: String,
74 pub export: String,
75}
76
77impl Lockfile {
78 pub fn canonicalized(&self) -> Self {
79 let mut lockfile = self.clone();
80 lockfile
81 .source
82 .sort_by(|left, right| left.name.cmp(&right.name));
83 lockfile.package.sort_by(|left, right| {
84 left.name
85 .cmp(&right.name)
86 .then(left.source.cmp(&right.source))
87 .then(left.version.cmp(&right.version))
88 });
89 lockfile
90 .target
91 .sort_by(|left, right| left.name.cmp(&right.name));
92 lockfile.managed_span.sort_by(|left, right| {
93 left.target
94 .cmp(&right.target)
95 .then(left.open_line.cmp(&right.open_line))
96 .then(left.id.cmp(&right.id))
97 });
98 lockfile.provisioned.sort_by(|left, right| {
99 left.path
100 .cmp(&right.path)
101 .then(left.package.cmp(&right.package))
102 });
103 lockfile
104 }
105
106 pub fn serialized(&self) -> PrayResult<String> {
107 let bytes = toml::to_string_pretty(&self.canonicalized())
108 .map_err(|error| PrayError::Manifest(error.to_string()))?;
109 Ok(bytes)
110 }
111
112 pub fn file_hash(&self) -> PrayResult<String> {
113 let text = self.serialized()?;
114 Ok(sha256_prefixed(text.as_bytes()))
115 }
116
117 pub fn equivalent_to(&self, other: &Self) -> bool {
118 self == &other.canonicalized()
119 }
120}
121
122pub fn lockfiles_equivalent(canonical: &Lockfile, other: &Lockfile) -> bool {
123 canonical.equivalent_to(other)
124}
125
126pub fn write_lockfile_if_changed(path: &Path, lockfile: &Lockfile) -> PrayResult<()> {
127 let serialized = lockfile.serialized()?;
128 if path.exists() {
129 if let Ok(existing) = fs::read(path) {
130 if existing == serialized.as_bytes() {
131 return Ok(());
132 }
133 }
134 }
135 crate::transaction::write_file(path, serialized)?;
136 Ok(())
137}
138
139pub fn write_lockfile(path: &Path, lockfile: &Lockfile) -> PrayResult<()> {
140 let serialized = lockfile.serialized()?;
141 crate::transaction::write_file(path, serialized)?;
142 Ok(())
143}
144
145pub fn read_lockfile(path: &Path) -> PrayResult<Lockfile> {
146 let text = fs::read_to_string(path)?;
147 let lockfile = toml::from_str(&text).map_err(|error| PrayError::Parse {
148 kind: "lockfile",
149 message: error.to_string(),
150 })?;
151 Ok(lockfile)
152}
153
154pub fn relative_lockfile_path(project_root: &Path, path: &Path) -> String {
155 let absolute = if path.is_absolute() {
156 path.to_path_buf()
157 } else {
158 project_root.join(path)
159 };
160 let normalized_root = lexical_normalize_path(project_root);
161 let normalized_absolute = lexical_normalize_path(&absolute);
162 let relative = normalized_absolute
163 .strip_prefix(&normalized_root)
164 .map(Path::to_path_buf)
165 .unwrap_or_else(|_| {
166 if path.is_absolute() {
167 path.to_path_buf()
168 } else {
169 lexical_normalize_path(path)
170 }
171 });
172 format_relative_lockfile_path(&relative)
173}
174
175fn format_relative_lockfile_path(relative: &Path) -> String {
176 let text = relative.to_string_lossy().replace('\\', "/");
177 if text == "." || text.starts_with("./") {
178 text
179 } else {
180 format!("./{text}")
181 }
182}
183
184fn lexical_normalize_path(path: &Path) -> PathBuf {
185 let mut normalized = PathBuf::new();
186 for component in path.components() {
187 match component {
188 Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
189 Component::RootDir => normalized.push(std::path::MAIN_SEPARATOR_STR),
190 Component::CurDir => {}
191 Component::ParentDir => {
192 let _ = normalized.pop();
193 }
194 Component::Normal(segment) => normalized.push(segment),
195 }
196 }
197 normalized
198}
199
200fn normalize_lockfile_artifact(project_root: &Path, artifact: &str, package_root: &Path) -> String {
201 if let Some(path_text) = artifact.strip_prefix("path:") {
202 let path = Path::new(path_text);
203 let relative = if path.is_absolute() {
204 relative_lockfile_path(project_root, path)
205 } else {
206 relative_lockfile_path(project_root, package_root)
207 };
208 format!("path:{relative}")
209 } else {
210 artifact.to_string()
211 }
212}
213
214#[allow(clippy::too_many_arguments)]
215pub fn build_lockfile(
216 manifest_hash: String,
217 environment: Option<String>,
218 project_root: &Path,
219 manifest_sources: &[crate::manifest::ManifestSource],
220 manifest_targets: &[crate::manifest::ManifestTarget],
221 rendered: &[RenderedTarget],
222 packages: &[crate::resolve::ResolvedPackage],
223 source_revisions: &std::collections::BTreeMap<String, String>,
224 source_host_keys: &std::collections::BTreeMap<String, String>,
225) -> Lockfile {
226 Lockfile {
227 prayfile_lock: "1".to_string(),
228 spec: "0.1".to_string(),
229 generated_by: format!("pray {}", env!("CARGO_PKG_VERSION")),
230 manifest_hash,
231 environment,
232 source: manifest_sources
233 .iter()
234 .map(|source| LockSource {
235 name: source.name.clone(),
236 kind: source.kind.clone(),
237 url: source.url.clone(),
238 revision: source_revisions.get(&source.name).cloned(),
239 host_key_fingerprint: source_host_keys.get(&source.name).cloned(),
240 })
241 .collect(),
242 package: packages
243 .iter()
244 .map(|package| LockedPackage {
245 name: package.declaration.name.clone(),
246 version: package.spec.version.clone(),
247 source: package.declaration.source.clone(),
248 path: relative_lockfile_path(project_root, &package.root),
249 tree_hash: package.tree_hash.clone(),
250 artifact_hash: package.artifact_hash.clone(),
251 artifact: normalize_lockfile_artifact(
252 project_root,
253 &package.artifact,
254 &package.root,
255 ),
256 exports: package.selected_exports.clone(),
257 dependencies: package
258 .spec
259 .dependencies
260 .iter()
261 .map(|dependency| dependency.name.clone())
262 .collect(),
263 signer_fingerprint: package.signer_fingerprint.clone(),
264 })
265 .collect(),
266 target: manifest_targets
267 .iter()
268 .map(|target| LockedTarget {
269 name: target.name.clone(),
270 outputs: target.outputs.clone(),
271 })
272 .collect(),
273 managed_span: rendered
274 .iter()
275 .flat_map(|target| target.managed_spans.iter().cloned())
276 .collect(),
277 provisioned: Vec::new(),
278 }
279 .canonicalized()
280}
281
282#[cfg(test)]
283#[path = "lockfile_unit.rs"]
284mod lockfile_unit;