liblingo/package/
lock.rs

1use crate::util::sha1dir;
2use colored::Colorize;
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use versions::Versioning;
5
6use log::error;
7use serde::de::Error as DeserializationError;
8use serde::ser::Error as SerializationError;
9use std::cmp::PartialEq;
10use std::collections::HashMap;
11use std::fmt::Display;
12use std::fs;
13use std::path::{Path, PathBuf};
14use std::str::FromStr;
15
16use crate::GitCloneAndCheckoutCap;
17
18use crate::package::management::copy_dir_all;
19use crate::package::{
20    deserialize_version, serialize_version,
21    target_properties::{LibraryTargetProperties, MergeTargetProperties},
22    tree::{DependencyTreeNode, PackageDetails, ProjectSource},
23    ConfigFile,
24};
25use crate::util::errors::LingoError;
26
27pub struct ParseLockSourceError {}
28
29/// Different package sources types, available inside the lock file.
30#[derive(PartialEq, Debug)]
31pub enum PackageLockSourceType {
32    REGISTRY,
33    GIT,
34    TARBALL,
35    PATH,
36}
37
38/// Struct that saves the source uri string
39#[derive(Debug)]
40pub struct PackageLockSource {
41    pub source_type: PackageLockSourceType,
42    pub uri: String,
43    pub rev: Option<String>,
44}
45
46// Tries to parse the enum value from given string
47impl FromStr for PackageLockSourceType {
48    type Err = ParseLockSourceError;
49
50    fn from_str(s: &str) -> Result<Self, Self::Err> {
51        match s {
52            "registry" => Ok(Self::REGISTRY),
53            "git" => Ok(Self::GIT),
54            "path" => Ok(Self::PATH),
55            "tar" => Ok(Self::TARBALL),
56            _ => Err(ParseLockSourceError {}),
57        }
58    }
59}
60
61/// generates the corresponding string based on enum value
62impl Display for PackageLockSourceType {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        let str = match self {
65            Self::REGISTRY => "registry".to_string(),
66            Self::GIT => "git".to_string(),
67            Self::PATH => "path".to_string(),
68            Self::TARBALL => "tar".to_string(),
69        };
70        write!(f, "{}", str)
71    }
72}
73
74impl From<ProjectSource> for PackageLockSourceType {
75    fn from(value: ProjectSource) -> Self {
76        match value {
77            ProjectSource::Git(_) => Self::GIT,
78            ProjectSource::TarBall(_) => Self::TARBALL,
79            ProjectSource::Path(_) => Self::PATH,
80        }
81    }
82}
83
84/// Parses the whole source uri string of a package
85/// the uri string follows the pattern <type>+<url>(#<git-rev>)
86impl FromStr for PackageLockSource {
87    type Err = ParseLockSourceError;
88
89    fn from_str(s: &str) -> Result<Self, Self::Err> {
90        match s.split_once("+") {
91            Some((source_type_string, mut uri)) => {
92                let source_type = PackageLockSourceType::from_str(source_type_string)?;
93
94                let rev: Option<String> = match source_type {
95                    PackageLockSourceType::GIT => match uri.split_once("#") {
96                        Some((url, rev)) => {
97                            uri = url;
98                            Some(rev.to_string())
99                        }
100                        None => {
101                            return Err(ParseLockSourceError {});
102                        }
103                    },
104                    _ => None,
105                };
106
107                Ok(PackageLockSource {
108                    source_type,
109                    uri: uri.to_string(),
110                    rev,
111                })
112            }
113            None => Err(ParseLockSourceError {}),
114        }
115    }
116}
117
118#[derive(Deserialize, Serialize, Debug)]
119pub struct PackageLock {
120    pub name: String,
121    #[serde(
122        serialize_with = "serialize_version",
123        deserialize_with = "deserialize_version"
124    )]
125    pub version: Versioning,
126    pub source: PackageLockSource,
127    pub checksum: String,
128}
129
130impl From<DependencyTreeNode> for PackageLock {
131    fn from(value: DependencyTreeNode) -> Self {
132        let uri = match &value.package.mutual_exclusive {
133            ProjectSource::Git(git) => git.to_string(),
134            ProjectSource::TarBall(tar) => tar.to_string(),
135            ProjectSource::Path(path) => format!("{:?}", path),
136        };
137
138        PackageLock {
139            name: value.name,
140            version: value.version,
141            source: PackageLockSource {
142                source_type: PackageLockSourceType::from(value.package.mutual_exclusive),
143                uri,
144                rev: value.package.git_rev,
145            },
146            checksum: value.hash,
147        }
148    }
149}
150
151impl<'de> Deserialize<'de> for PackageLockSource {
152    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
153    where
154        D: Deserializer<'de>,
155    {
156        let s: String = Deserialize::deserialize(deserializer)?;
157        match PackageLockSource::from_str(&s) {
158            Ok(value) => Ok(value),
159            Err(_) => Err(D::Error::custom("cannot parse package source string!")),
160        }
161    }
162}
163
164impl Serialize for PackageLockSource {
165    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
166    where
167        S: Serializer,
168    {
169        let source_type = self.source_type.to_string();
170        let mut serialized_string = format!("{}+{}", source_type, self.uri);
171
172        if self.source_type == PackageLockSourceType::GIT {
173            if let Some(rev) = self.rev.clone() {
174                serialized_string = format!("{}#{}", serialized_string, rev)
175            } else {
176                error!("expected and revision but got none during serialization of lock file!");
177                return Err(S::Error::custom("expected revision but gone None"));
178            }
179        }
180
181        serializer.serialize_str(&serialized_string)
182    }
183}
184
185#[derive(Deserialize, Serialize, Default, Debug)]
186pub struct DependencyLock {
187    /// mapping from package name to location
188    #[serde(flatten)]
189    pub dependencies: HashMap<String, PackageLock>,
190
191    /// this will be populated when the project is successfully loaded from the lock file
192    #[serde(skip)]
193    loaded_dependencies: Vec<DependencyTreeNode>,
194}
195
196impl DependencyLock {
197    pub(crate) fn create(selected_dependencies: Vec<DependencyTreeNode>) -> DependencyLock {
198        let mut map = HashMap::new();
199        for dependency in &selected_dependencies {
200            map.insert(
201                dependency.name.clone(),
202                PackageLock::from(dependency.clone()),
203            );
204        }
205        Self {
206            dependencies: map,
207            loaded_dependencies: selected_dependencies,
208        }
209    }
210
211    pub fn init(
212        &mut self,
213        lfc_include_folder: &Path,
214        git_clone_and_checkout_cap: &GitCloneAndCheckoutCap,
215    ) -> anyhow::Result<()> {
216        for (_, lock) in self.dependencies.iter() {
217            let temp = lfc_include_folder.join(&lock.name);
218            // the Lingo.toml for this dependency doesnt exists, hence we need to fetch this package
219            if !temp.join("Lingo.toml").exists() {
220                let mut details = PackageDetails::try_from(&lock.source)?;
221
222                details
223                    .fetch(&temp, git_clone_and_checkout_cap)
224                    .expect("cannot pull package");
225            }
226
227            let hash = sha1dir::checksum_current_dir(&temp, false);
228
229            if hash.to_string() != lock.checksum {
230                error!("checksum does not match aborting!");
231            }
232
233            let lingo_toml_text = fs::read_to_string(temp.join("Lingo.toml"))?;
234            let read_toml = toml::from_str::<ConfigFile>(&lingo_toml_text)?.to_config(&temp);
235
236            println!(
237                "{} {} ... {}",
238                "Reading".green().bold(),
239                lock.name,
240                read_toml.package.version
241            );
242
243            let lib = match read_toml.library {
244                Some(value) => value,
245                None => {
246                    // error we expected a library here
247                    return Err(LingoError::NoLibraryInLingoToml(
248                        temp.join("Lingo.toml").display().to_string(),
249                    )
250                    .into());
251                }
252            };
253
254            self.loaded_dependencies.push(DependencyTreeNode {
255                name: read_toml.package.name.clone(),
256                version: read_toml.package.version.clone(),
257                package: PackageDetails {
258                    version: Default::default(),
259                    mutual_exclusive: ProjectSource::Path(PathBuf::new()),
260                    git_tag: None,
261                    git_rev: None,
262                },
263                location: temp.clone(),
264                include_path: lib.location.clone(),
265                hash: lock.checksum.clone(),
266                dependencies: vec![],
267                properties: lib.properties.clone(),
268            });
269        }
270
271        Ok(())
272    }
273
274    pub fn create_library_folder(
275        &self,
276        source_path: &Path,
277        target_path: &PathBuf,
278    ) -> anyhow::Result<()> {
279        fs::create_dir_all(target_path)?;
280        for (_, dep) in self.dependencies.iter() {
281            let local_source = source_path.join(&dep.checksum);
282            let find_source = target_path.clone().join(&dep.name);
283            fs::create_dir_all(&find_source)?;
284            copy_dir_all(&local_source, &find_source)?;
285        }
286
287        Ok(())
288    }
289
290    pub fn aggregate_target_properties(&self) -> anyhow::Result<LibraryTargetProperties> {
291        let mut i = LibraryTargetProperties::default();
292        for tp in &self.loaded_dependencies {
293            i.merge(&tp.properties)?;
294        }
295
296        Ok(i)
297    }
298}