1#![allow(dead_code)]
7pub mod manifest;
8
9use manifest::Manifest;
10use serde::{Deserialize, Serialize};
11use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
12
13pub mod error {
14 use thiserror::Error;
15 #[derive(Error, Debug)]
16 pub enum PkgError {
17 #[error("Pkg Error: {0}")]
19 Generic(String),
20 #[error(transparent)]
21 Io(#[from] std::io::Error),
22 }
23}
24
25#[derive(Debug, Serialize, Deserialize, Clone)]
26#[serde(untagged)]
27pub enum Dependency {
28 Git(GitDependency),
29 Path(PathDependency),
30}
31
32#[derive(Debug, Serialize, Deserialize)]
33pub struct Lockfile {
34 entries: Vec<LockfileEntry>,
35}
36#[derive(Clone, Debug, Serialize, Deserialize)]
37pub struct LockfileEntry {
38 name: String,
39 hash: String,
40 depends_on: BTreeMap<String, Dependency>,
41}
42
43#[derive(Debug, Serialize, Deserialize, Clone)]
44pub struct GitDependency {
45 pub git: String,
46 pub branch: Option<String>,
47 pub tag: Option<String>,
48 pub rev: Option<String>,
49}
50
51#[derive(Debug, Serialize, Deserialize, Clone)]
52pub struct PathDependency {
53 pub path: String,
54}
55
56use std::{
57 collections::BTreeMap,
58 fs,
59 path::{Path, PathBuf},
60};
61
62#[derive(Debug)]
64pub struct BuildPlan {
65 pub items: Vec<BuildableItem>,
66}
67
68#[derive(Debug)]
69pub struct BuildableItem {
70 pub path_to_source: PathBuf,
71 pub depends_on: Vec<DependencyKey>,
72 pub key: DependencyKey,
73 pub manifest: Manifest,
74}
75
76pub type DependencyKey = String;
77
78pub fn load_dependencies(deps: BTreeMap<String, Dependency>) -> Result<(Lockfile, BuildPlan), crate::error::PkgError> {
79 let mut entries = Vec::new();
80 let mut files = Vec::new();
81
82 let mut stdout = StandardStream::stdout(ColorChoice::Always);
83 for (dep_name, dep_source) in deps {
86 stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?;
88 print!("Fetching ");
89 stdout.set_color(ColorSpec::new().set_fg(None).set_bold(false))?;
90 println!("{dep_name}");
91 let dep = match dep_source {
92 Dependency::Git(ref git_dep) => load_git_dependency(git_dep),
93 Dependency::Path(ref path_dep) => load_path_dependency(path_dep, path_dep.path.clone()),
94 }?;
95
96 entries.push(dep.lock.clone());
97 files.push(dep);
98 }
99
100 let lockfile = Lockfile { entries };
101 Ok((lockfile, order_dependencies(files)?))
102}
103
104fn order_dependencies(deps: Vec<LoadDependencyResult>) -> Result<BuildPlan, crate::error::PkgError> {
105 let mut graph: BTreeMap<DependencyKey, Vec<DependencyKey>> = BTreeMap::new();
106
107 for dep in &deps {
108 graph.entry(dep.key.clone()).or_default();
109 for top_level_dependency in dep.lock.depends_on.values() {
110 let key = match top_level_dependency {
111 Dependency::Git(git_dep) => git_dep.git.clone(),
112 Dependency::Path(path_dep) => path_dep.path.clone(),
113 };
114 graph.entry(dep.key.clone()).or_default().push(key);
115
116 fn add_transient_dependencies(
118 graph: &mut BTreeMap<DependencyKey, Vec<DependencyKey>>,
119 dep_key: &DependencyKey,
120 dependency: &Dependency,
121 deps: &[LoadDependencyResult],
122 ) {
123 let key = match dependency {
124 Dependency::Git(git_dep) => git_dep.git.clone(),
125 Dependency::Path(path_dep) => path_dep.path.clone(),
126 };
127 graph.entry(dep_key.clone()).or_default().push(key.clone());
128
129 if let Some(dep) = deps.iter().find(|d| d.key == key) {
130 for trans_dependency in dep.lock.depends_on.values() {
131 add_transient_dependencies(graph, &key, trans_dependency, deps);
132 }
133 }
134 }
135
136 add_transient_dependencies(&mut graph, &dep.key, top_level_dependency, &deps[..]);
137 }
138 }
139
140 for dep in &deps {
141 for dependency in dep.lock.depends_on.values() {
142 let dep_key = match dependency {
143 Dependency::Git(git_dep) => git_dep.git.parse().unwrap(),
144 Dependency::Path(path_dep) => path_dep.path.parse().unwrap(),
145 };
146 graph.entry(dep_key).or_default().push(dep.key.clone());
147 }
148 }
149
150 let mut sorted_keys = Vec::new();
151 let mut visited = BTreeMap::new();
152
153 fn visit(
154 node: DependencyKey,
155 graph: &BTreeMap<DependencyKey, Vec<DependencyKey>>,
156 visited: &mut BTreeMap<DependencyKey, bool>,
157 sorted_keys: &mut Vec<DependencyKey>,
158 ) {
159 if let Some(&is_visited) = visited.get(&node) {
160 if is_visited {
161 return;
162 }
163 }
164
165 visited.insert(node.clone(), true);
166
167 if let Some(neighbors) = graph.get(&node) {
168 for neighbor in neighbors.iter() {
169 visit(neighbor.clone(), graph, visited, sorted_keys);
170 }
171 }
172
173 sorted_keys.push(node);
174 }
175
176 for node in graph.keys() {
177 visit(node.clone(), &graph, &mut visited, &mut sorted_keys);
178 }
179
180 let mut all_deps = deps.clone();
183 for dep in &deps {
184 for dependency in dep.lock.depends_on.values() {
185 let key = match dependency {
186 Dependency::Git(git_dep) => git_dep.git.clone(),
187 Dependency::Path(path_dep) => path_dep.path.clone(),
188 };
189 if !all_deps.iter().any(|d| d.key == key) {
190 let transient_dep = match dependency {
191 Dependency::Git(git_dep) => load_git_dependency(git_dep),
192 Dependency::Path(path_dep) => load_path_dependency(path_dep, key.clone()),
193 }?;
194 all_deps.push(transient_dep);
195 }
196 }
197 }
198
199 let items = sorted_keys
200 .into_iter()
201 .map(|key| {
202 let dep = all_deps.iter().find(|x| x.key == key).unwrap();
203 let path = Path::new(&dep.lock.name);
204 BuildableItem {
205 path_to_source: path.to_path_buf(),
206 depends_on: dep
207 .lock
208 .depends_on
209 .values()
210 .map(|dep| match dep {
211 Dependency::Git(git_dep) => git_dep.git.clone(),
212 Dependency::Path(path_dep) => path_dep.path.clone(),
213 })
214 .collect(),
215 key,
216 manifest: dep.manifest.clone(),
217 }
218 })
219 .collect();
220
221 Ok(BuildPlan { items })
222}
223
224#[derive(Clone)]
225struct LoadDependencyResult {
226 lock: LockfileEntry,
227 files: Vec<(String, String)>,
228 manifest: Manifest,
229 key: String,
231}
232
233fn load_git_dependency(dep: &GitDependency) -> Result<LoadDependencyResult, error::PkgError> {
234 let home_dir = dirs::home_dir().expect("Failed to get home directory");
239 let petr_dir = home_dir.join(".petr");
240 if !petr_dir.exists() {
241 fs::create_dir_all(&petr_dir).expect("Failed to create .petr directory");
242 }
243
244 let repo_dir = petr_dir.join(dep.git.replace('/', "_"));
245
246 if !repo_dir.exists() {
247 let _ = git2::Repository::clone(&dep.git, &repo_dir).expect("Failed to clone Git repository");
248 }
249
250 let path_dep = PathDependency {
251 path: repo_dir.to_string_lossy().into_owned(),
252 };
253
254 load_path_dependency(&path_dep, dep.git.clone())
255}
256
257fn load_path_dependency(
258 dep: &PathDependency,
259 key: DependencyKey,
260) -> Result<LoadDependencyResult, error::PkgError> {
261 let path = Path::new(&dep.path).join("src");
262 let entries = fs::read_dir(path).unwrap_or_else(|_| panic!("Failed to read directory: {}", dep.path));
263
264 let files: Vec<_> = entries
265 .filter_map(|entry| {
266 let entry = entry.ok()?;
267 let path = entry.path();
268 if path.is_file() {
269 let file_name = path.file_name()?.to_string_lossy().into_owned();
270 let content = fs::read_to_string(&path).ok()?;
271 Some((file_name, content))
272 } else {
273 None
274 }
275 })
276 .collect();
277
278 let petr_toml_path = Path::new(&dep.path).join("pete.toml");
280 let manifest_content = fs::read_to_string(&petr_toml_path)
281 .map_err(|e| error::PkgError::Generic(format!("Could not read petr.toml file at {petr_toml_path:?}: {e:?}")))?;
282 let manifest: Manifest = toml::from_str(&manifest_content).expect("Failed to parse pete.toml");
283
284 let lockfile_entry = LockfileEntry {
285 name: dep.path.clone(),
286 hash: calculate_lockfile_hash(files.clone()),
287 depends_on: manifest.dependencies.clone(),
288 };
289
290 Ok(LoadDependencyResult {
291 lock: lockfile_entry,
292 files,
293 manifest,
294 key,
295 })
296}
297
298fn calculate_lockfile_hash(sources: Vec<(String, String)>) -> String {
299 use bcrypt::{hash, DEFAULT_COST};
301
302 let mut hasher = blake3::Hasher::new();
303 for (name, content) in sources {
304 hasher.update(name.as_bytes());
305 hasher.update(content.as_bytes());
306 }
307 let hash_bytes = hasher.finalize();
308 let hash_bytes = hash_bytes.as_bytes();
309 let hash_string = hash(hash_bytes, DEFAULT_COST).expect("Failed to hash");
310 u128::from_be_bytes(hash_string.as_bytes()[..16].try_into().expect("Failed to convert hash to u128")).to_string()
311}