1use crate::lockfile::Lockfile;
2use crate::manifest::{Manifest, ManifestPackage, ManifestSource};
3use crate::package_spec::{parse_package_spec, PackageSpec};
4use crate::resolve_context::ResolveOptions;
5use crate::resolve_exports::{
6 build_skill_file_index, load_export_bodies, load_package_file_bytes, select_exports,
7};
8use crate::resolve_git_sources::{prepare_git_sources, prepare_pray_ssh_host_keys, GitSourceSet};
9
10use crate::paths::find_prayspec_file;
11pub use crate::resolve_git::{
12 discover_distribution_root, git_source_cache_directory, global_git_cache_ready,
13};
14pub use crate::resolve_git_legacy::quarantine_legacy_git_checkouts;
15pub use crate::resolve_git_lookup::git_source_cached_repository;
16pub use crate::resolve_git_paths::git_source_cache_directory_with_subdir;
17pub use crate::resolve_git_refresh::{
18 annotate_failed_git_refresh, annotate_missing_git_catalog,
19 resolution_may_benefit_from_git_source_refresh,
20};
21pub use crate::resolve_git_sources::refresh_git_sources;
22use crate::{PrayError, PrayResult};
23use std::collections::BTreeMap;
24use std::fs;
25use std::path::{Path, PathBuf};
26
27#[derive(Debug, Clone)]
28pub struct ResolvedProject {
29 pub manifest_path: PathBuf,
30 pub project_root: PathBuf,
31 pub manifest: Manifest,
32 pub manifest_hash: String,
33 pub packages: Vec<ResolvedPackage>,
34 pub local_files: Vec<ResolvedLocalFile>,
35 pub source_revisions: BTreeMap<String, String>,
36 pub source_host_keys: BTreeMap<String, String>,
37 pub environment: Option<String>,
38}
39
40#[derive(Debug, Clone)]
41pub struct ResolvedPackage {
42 pub declaration: ManifestPackage,
43 pub root: PathBuf,
44 pub spec: PackageSpec,
45 pub tree_hash: String,
46 pub artifact_hash: String,
47 pub artifact: String,
48 pub selected_exports: Vec<String>,
49 pub source_checksum: String,
50 pub export_bodies: BTreeMap<String, String>,
51 pub skill_files: BTreeMap<String, Vec<String>>,
52 pub signer_fingerprint: Option<String>,
53 pub registry_latest_version: Option<String>,
55 pub explicit: bool,
57 pub upstream: Option<crate::package_upstream::LockedUpstream>,
58}
59
60#[derive(Debug, Clone)]
61pub struct ResolvedLocalFile {
62 pub path: PathBuf,
63 pub manifest_path: String,
64 pub content: String,
65 pub source_checksum: String,
66 pub position: String,
67 pub optional: bool,
68}
69
70impl ResolvedProject {
71 pub fn lockfile_hash(&self) -> PrayResult<String> {
72 Ok(self.manifest_hash.clone())
73 }
74}
75
76pub fn project_root_from_manifest(manifest_path: &Path) -> PathBuf {
77 match manifest_path.parent() {
78 Some(parent) if !parent.as_os_str().is_empty() => parent.to_path_buf(),
79 _ => PathBuf::from("."),
80 }
81}
82
83fn canonical_project_root(manifest_path: &Path) -> PrayResult<PathBuf> {
84 let root = project_root_from_manifest(manifest_path);
85 if root.is_absolute() {
86 return Ok(root);
87 }
88 let cwd = std::env::current_dir().map_err(|error| {
89 PrayError::Resolution(format!("failed to resolve project root from cwd: {error}"))
90 })?;
91 Ok(cwd.join(root))
92}
93
94pub fn resolve_project(manifest_path: &Path) -> PrayResult<ResolvedProject> {
95 resolve_project_with_options(manifest_path, &ResolveOptions::default())
96}
97
98pub fn resolve_project_with_git_refresh_fallback(
99 manifest_path: &Path,
100 options: &ResolveOptions,
101 allow_git_refresh_fallback: bool,
102) -> PrayResult<ResolvedProject> {
103 match resolve_project_with_options(manifest_path, options) {
104 Ok(project) => Ok(project),
105 Err(PrayError::Resolution(message))
106 if allow_git_refresh_fallback
107 && !options.offline
108 && !options.refresh_source_revisions
109 && resolution_may_benefit_from_git_source_refresh(&message) =>
110 {
111 let refreshed_options = ResolveOptions {
112 refresh_source_revisions: true,
113 ..options.clone()
114 };
115 match resolve_project_with_options(manifest_path, &refreshed_options) {
116 Ok(project) => Ok(project),
117 Err(error) => {
118 let lockfile_path =
119 project_root_from_manifest(manifest_path).join("Prayfile.lock");
120 Err(annotate_failed_git_refresh(&lockfile_path, error))
121 }
122 }
123 }
124 Err(error) => Err(error),
125 }
126}
127
128pub fn resolve_project_with_options(
129 manifest_path: &Path,
130 options: &ResolveOptions,
131) -> PrayResult<ResolvedProject> {
132 let project_root = canonical_project_root(manifest_path)?;
133 resolve_project_in_context(manifest_path, &project_root, options)
134}
135
136#[path = "resolve_project.rs"]
137mod project;
138pub use project::{resolve_manifest_in_context, resolve_project_in_context};
139
140#[path = "resolve_implied_source.rs"]
141mod implied_source;
142pub(crate) use implied_source::implied_source_name;
143
144#[path = "resolve_package_root.rs"]
145mod package_root;
146pub(crate) use package_root::{resolve_package_root, PackageRootResolution};
147
148#[path = "resolve_upstream.rs"]
149pub mod resolve_upstream;
150pub use crate::resolve_git_materialize_confirm::confirm_git_source_revisions_materializable;
151pub use resolve_upstream::{
152 apply_path_upstream_refreshes, path_fork_drift_lines, plan_path_upstream_conflicts,
153 PathUpstreamConflict,
154};
155
156fn resolve_package(
157 project_root: &Path,
158 sources: &BTreeMap<String, ManifestSource>,
159 git_sources: &GitSourceSet,
160 user_config: &crate::config::PrayConfig,
161 declaration: &ManifestPackage,
162 lockfile: Option<&Lockfile>,
163 options: &ResolveOptions,
164) -> PrayResult<ResolvedPackage> {
165 let PackageRootResolution {
166 root,
167 signer_fingerprint,
168 registry_latest_version,
169 } = resolve_package_root(
170 project_root,
171 sources,
172 git_sources,
173 user_config,
174 declaration,
175 lockfile,
176 options,
177 )?;
178 let spec_path = find_prayspec_file(&root)?;
179 let spec_text = fs::read_to_string(&spec_path)?;
180 let spec = parse_package_spec(&spec_text)?.canonicalized();
181 if spec.name != declaration.name {
182 return Err(PrayError::Resolution(format!(
183 "package path {:?} declares {:?}, expected {:?}",
184 root, spec.name, declaration.name
185 )));
186 }
187 spec.satisfy_constraint(&declaration.constraint)?;
188 let selected_exports = select_exports(declaration, &spec)?;
189 let file_bytes = load_package_file_bytes(&root, &spec)?;
190 let tree_hash = PackageSpec::tree_hash_from_file_bytes(&file_bytes)?;
191 let export_bodies = load_export_bodies(&file_bytes, &spec, &selected_exports)?;
192 let skill_files = build_skill_file_index(&spec);
193 let source_checksum = tree_hash.clone();
194 let upstream_context = resolve_upstream::UpstreamResolutionContext::new(
195 project_root,
196 sources,
197 git_sources,
198 user_config,
199 lockfile,
200 options,
201 );
202 let upstream = resolve_upstream::lock_path_upstream(&upstream_context, declaration, &spec)?;
203 Ok(ResolvedPackage {
204 declaration: declaration.clone(),
205 root,
206 spec: spec.clone(),
207 tree_hash: tree_hash.clone(),
208 artifact_hash: tree_hash.clone(),
209 artifact: format!(
210 "path:{}",
211 spec_path.parent().unwrap_or(&spec_path).to_string_lossy()
212 ),
213 selected_exports,
214 source_checksum,
215 export_bodies,
216 skill_files,
217 signer_fingerprint,
218 registry_latest_version,
219 explicit: false,
220 upstream,
221 })
222}
223
224#[path = "resolve_local.rs"]
225mod local;
226pub use local::missing_local_embed_guidance;
227pub(crate) use local::resolve_local_file;
228
229fn source_map(sources: &[ManifestSource]) -> BTreeMap<String, ManifestSource> {
230 sources
231 .iter()
232 .map(|source| (source.name.clone(), source.clone()))
233 .collect()
234}
235
236#[cfg(test)]
237mod tests {
238 use super::{discover_distribution_root, project_root_from_manifest};
239 use std::fs;
240 use std::path::Path;
241
242 #[test]
243 fn project_root_from_manifest_uses_cwd_for_bare_filename() {
244 let root = project_root_from_manifest(Path::new("Prayfile"));
245 assert_eq!(root, Path::new("."));
246 }
247
248 #[test]
249 fn project_root_from_manifest_uses_parent_directory() {
250 let root = project_root_from_manifest(Path::new("examples/simple-project/Prayfile"));
251 assert_eq!(root, Path::new("examples/simple-project"));
252 }
253
254 #[test]
255 fn discover_distribution_root_finds_root_and_prayers_subdirectory() {
256 let workspace =
257 std::env::temp_dir().join(format!("pray-discover-distribution-{}", std::process::id()));
258 let _ = fs::remove_dir_all(&workspace);
259 let repo_root = workspace.join("repo");
260 let prayers_root = repo_root.join("prayers");
261 fs::create_dir_all(prayers_root.join("v1/packages")).expect("prayers distribution");
262 fs::create_dir_all(repo_root.join("v1/packages")).expect("root distribution");
263
264 assert_eq!(
265 discover_distribution_root(&repo_root),
266 Some(repo_root.clone())
267 );
268
269 fs::remove_dir_all(repo_root.join("v1")).expect("remove root distribution");
270 assert_eq!(discover_distribution_root(&repo_root), Some(prayers_root));
271 let _ = fs::remove_dir_all(&workspace);
272 }
273
274 #[test]
275 fn discover_distribution_root_returns_none_without_registry_layout() {
276 let workspace =
277 std::env::temp_dir().join(format!("pray-discover-missing-{}", std::process::id()));
278 let _ = fs::remove_dir_all(&workspace);
279 let repo_root = workspace.join("repo");
280 fs::create_dir_all(&repo_root).expect("repo root");
281 assert_eq!(discover_distribution_root(&repo_root), None);
282 let _ = fs::remove_dir_all(&workspace);
283 }
284}