build_utils/source/
download.rs

1use crate::source::{BuildSource};
2use std::path::{PathBuf};
3use std::process::Command;
4use std::io::ErrorKind;
5use lazy_static::lazy_static;
6use std::ops::Deref;
7use crate::util::{create_temporary_path, TemporaryPath, execute_build_command};
8use std::hash::{Hash, Hasher};
9use std::collections::hash_map::DefaultHasher;
10use crate::build::BuildStepError;
11
12#[derive(Ord, PartialOrd, Eq, PartialEq, Debug)]
13enum GitBinaryStatus {
14    /// Ok, version is the first argument
15    Ok(String),
16    NotFound,
17    Outdated(String),
18    Unknown(String)
19}
20
21fn check_git() -> GitBinaryStatus {
22    match Command::new("git")
23                .arg("--version")
24                .output() {
25        Ok(result) => {
26            let version = String::from_utf8(result.stdout).expect("command result isn't utf-8")
27                .lines().nth(0).map(|e| e.to_owned());
28            if let Some(version) = version {
29                if version.contains(" 2.") {
30                    GitBinaryStatus::Ok(version)
31                } else {
32                    GitBinaryStatus::Outdated(version)
33                }
34            } else {
35                GitBinaryStatus::Unknown(format!("truncated git version output"))
36            }
37        },
38        Err(error) => {
39            if error.kind() == ErrorKind::NotFound {
40                GitBinaryStatus::NotFound
41            } else {
42                GitBinaryStatus::Unknown(format!("{:?}", error).to_owned())
43            }
44        }
45    }
46}
47
48lazy_static! {
49    static ref GIT_STATUS: GitBinaryStatus = check_git();
50}
51
52pub struct BuildSourceGit {
53    repository_url: String,
54    /* TODO: Branch? */
55    revision: Option<String>,
56
57    checkout_submodule: bool,
58    skip_revision_checkout: bool,
59
60    checkout_folder: Option<PathBuf>,
61    local_folder: Option<TemporaryPath>
62}
63
64impl BuildSourceGit {
65    pub fn builder(repository_url: String) -> BuildSourceGitBuilder {
66        BuildSourceGitBuilder::new(repository_url)
67    }
68
69    fn temporary_directory_name(&self) -> String {
70        let mut hash = DefaultHasher::new();
71        self.repository_url.hash(&mut hash);
72        self.revision.as_ref().map(|e| e.hash(&mut hash));
73        let hash = hash.finish();
74        let hash = base64::encode(hash.to_be_bytes()).replace("/", "_");
75
76        let project_name = self.repository_url.split("/").last().unwrap_or("__unknown");
77        format!("git_{}_{}", project_name, hash).to_owned()
78    }
79}
80
81impl BuildSource for BuildSourceGit {
82    fn name(&self) -> &str {
83        "remote git repository"
84    }
85
86    fn hash(&self, target: &mut Box<dyn Hasher>) {
87        self.repository_url.hash(target);
88        self.revision.hash(target);
89    }
90
91    fn setup(&mut self) -> Result<(), BuildStepError> {
92        if self.local_folder.is_some() {
93            return Err(BuildStepError::new_simple("the source has already been initialized"));
94        }
95
96        if !matches!(GIT_STATUS.deref(), GitBinaryStatus::Ok(_)) {
97            return Err(BuildStepError::new_simple(format!("git error: {:?}", GIT_STATUS.deref())));
98        }
99
100        let target_folder = match create_temporary_path(&self.temporary_directory_name(), self.checkout_folder.clone()) {
101            Ok(folder) => {
102                folder.release(); /* FIXME! */
103                self.local_folder = Some(folder.clone());
104                folder
105            },
106            Err(err) => return Err(BuildStepError::new_simple(format!("failed to create git checkout directory: {:?}", err)))
107        };
108
109        let mut repository_exists = false;
110        if target_folder.join(".git").exists() {
111            println!("Updating existing repository ({:?})", target_folder);
112
113            let mut command = Command::new("git");
114            command.arg("fetch")
115                   .current_dir(target_folder.deref());
116
117            if let Err(error) = execute_build_command(&mut command, "git fetch failed") {
118                if error.stderr().find("not a git repository").is_none() {
119                    return Err(error);
120                } else {
121                    std::fs::remove_dir_all(target_folder.deref())
122                        .map_err(|err| BuildStepError::new_io("failed to remove old temporary checkout directory", err))?;
123
124                    std::fs::create_dir_all(target_folder.deref())
125                        .map_err(|err| BuildStepError::new_io("failed to create new temporary checkout directory", err))?;
126                }
127            } else {
128                repository_exists = true;
129            }
130        }
131
132        if !repository_exists {
133            println!("Cloning git repository");
134
135            let mut command = Command::new("git");
136            command.arg("clone")
137                   .arg(&self.repository_url)
138                   .arg(target_folder.deref());
139
140            execute_build_command(&mut command, "git clone failed")?;
141        }
142
143        if !self.skip_revision_checkout {
144            let revision = self.revision.clone().unwrap_or("HEAD".to_owned());
145            println!("Checking out revision {}", &revision);
146
147
148            let mut command = Command::new("git");
149            command.arg("reset")
150                   .arg("--hard")
151                   .arg(&revision)
152                   .current_dir(target_folder.deref());
153
154            execute_build_command(&mut command, "git revision checkout failed")?;
155        }
156
157        Ok(())
158    }
159
160    fn local_directory(&self) -> &PathBuf {
161        self.local_folder.as_ref().expect("expected a path")
162            .path()
163    }
164
165    fn cleanup(&mut self) {
166        /* FIXME: Remove this? */
167        self.local_folder.as_mut().map(|e| e.release());
168        self.local_folder = None;
169    }
170}
171
172pub struct BuildSourceGitBuilder {
173    inner: BuildSourceGit
174}
175
176impl BuildSourceGitBuilder {
177    fn new(repository_url: String) -> Self {
178        BuildSourceGitBuilder {
179            inner: BuildSourceGit {
180                repository_url,
181
182                checkout_submodule: false,
183                skip_revision_checkout: false,
184
185                checkout_folder: None,
186                local_folder: None,
187                revision: None
188            }
189        }
190    }
191
192    pub fn checkout_submodule(mut self, enabled: bool) -> Self {
193        self.inner.checkout_submodule = enabled;
194        self
195    }
196
197    pub fn checkout_folder(mut self, path: Option<PathBuf>) -> Self {
198        self.inner.checkout_folder = path;
199        self
200    }
201
202    pub fn revision(mut self, revision: Option<String>) -> Self {
203        self.inner.revision = revision;
204        self
205    }
206
207    pub fn skip_revision_checkout(mut self, enabled: bool) -> Self {
208        self.inner.skip_revision_checkout = enabled;
209        self
210    }
211
212    pub fn build(self) -> BuildSourceGit {
213        self.inner
214    }
215}
216
217#[cfg(test)]
218mod test {
219    use crate::source::{BuildSourceGit, BuildSource};
220
221    #[test]
222    fn test_git() {
223        let mut source = BuildSourceGit::builder("https://github.com/WolverinDEV/libnice.git".to_owned())
224            .build();
225
226        source.setup().unwrap();
227    }
228}