build_utils/source/
download.rs1use 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(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 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(); 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 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}