1use crate::{config::Config, smaug};
2use derive_more::Display;
3use derive_more::Error;
4use log::*;
5use semver::Version as SemVer;
6use semver::VersionReq;
7use serde::Serialize;
8use serde::Serializer;
9use std::fs;
10use std::io;
11use std::path::Path;
12use std::path::PathBuf;
13
14#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Display)]
15pub enum Edition {
16 #[display(fmt = "")]
17 Standard,
18 #[display(fmt = "Indie")]
19 Indie,
20 #[display(fmt = "Pro")]
21 Pro,
22}
23
24#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Display)]
25#[display(
26 fmt = "DragonRuby {} {}.{} ({})",
27 "edition",
28 "version.major",
29 "version.minor",
30 "identifier"
31)]
32pub struct Version {
33 pub edition: Edition,
34 pub version: SemVer,
35 pub identifier: String,
36}
37
38#[derive(Debug, Clone, Display)]
39#[display(fmt = "{}", "version")]
40pub struct DragonRuby {
41 pub path: PathBuf,
42 pub version: Version,
43}
44
45#[derive(Debug, Error, Display)]
46pub enum DragonRubyError {
47 #[display(fmt = "Could not find a valid DragonRuby at {}", "path.display()")]
48 DragonRubyNotFound { path: PathBuf },
49 #[display(
50 fmt = "There is no version of DragonRuby installed.\nInstall with `smaug dragonruby install`."
51 )]
52 DragonRubyNotInstalled,
53}
54
55type DragonRubyResult = Result<DragonRuby, DragonRubyError>;
56
57pub fn new<P: AsRef<Path>>(path: &P) -> DragonRubyResult {
58 let dragonruby_path = path.as_ref();
59
60 if dragonruby_path.is_dir() {
61 parse_dragonruby_dir(dragonruby_path)
62 } else if zip_extensions::is_zip(&dragonruby_path.to_path_buf()) {
63 parse_dragonruby_zip(dragonruby_path)
64 } else {
65 Err(DragonRubyError::DragonRubyNotFound {
66 path: dragonruby_path.to_path_buf(),
67 })
68 }
69}
70
71impl DragonRuby {
72 pub fn install_dir(&self) -> PathBuf {
73 let location = smaug::data_dir().join("dragonruby");
74 match self.version.edition {
75 Edition::Pro => location.join(format!(
76 "pro-{}.{}",
77 self.version.version.major, self.version.version.minor
78 )),
79 Edition::Indie => location.join(format!(
80 "indie-{}.{}",
81 self.version.version.major, self.version.version.minor
82 )),
83 Edition::Standard => location.join(format!(
84 "{}.{}",
85 self.version.version.major, self.version.version.minor
86 )),
87 }
88 }
89}
90
91pub fn latest() -> DragonRubyResult {
92 let list = list_installed();
93
94 match list {
95 Err(..) => Err(DragonRubyError::DragonRubyNotInstalled),
96 Ok(mut versions) => {
97 if versions.is_empty() {
98 Err(DragonRubyError::DragonRubyNotInstalled)
99 } else {
100 versions.sort_by(|a, b| a.version.partial_cmp(&b.version).unwrap());
101 let latest = versions.last().unwrap();
102
103 Ok((*latest).clone())
104 }
105 }
106 }
107}
108
109pub fn configured_version(config: &Config) -> Option<DragonRuby> {
110 let version = VersionReq::parse(config.dragonruby.version.as_str())
111 .expect("Not a valid DragonRuby version.");
112 let edition = if config.dragonruby.edition == "pro" {
113 Edition::Pro
114 } else if config.dragonruby.edition == "indie" {
115 Edition::Indie
116 } else {
117 Edition::Standard
118 };
119
120 let mut installed = list_installed().expect("Could not list installed.");
121 installed.sort_by(|a, b| a.version.partial_cmp(&b.version).unwrap());
122 let matched = installed
123 .iter()
124 .find(|v| version.matches(&v.version.version) && v.version.edition >= edition);
125
126 matched.map(|dragonruby| dragonruby.to_owned())
127}
128
129pub fn list_installed() -> io::Result<Vec<DragonRuby>> {
130 let location = smaug::data_dir().join("dragonruby");
131 fs::create_dir_all(location.as_path())?;
132
133 let folders = fs::read_dir(location).expect("DragonRuby install folder not found.");
134 let versions: Vec<DragonRuby> = folders
135 .map(|folder| {
136 let path = folder.expect("Invalid folder");
137 parse_dragonruby_dir(&path.path())
138 })
139 .filter(|path| path.is_ok())
140 .map(|path| path.unwrap())
141 .collect();
142
143 Ok(versions)
144}
145
146pub fn dragonruby_docs_path() -> String {
147 "docs/docs.html".to_string()
148}
149
150pub fn dragonruby_bin_name() -> String {
151 if cfg!(windows) {
152 "dragonruby.exe".to_string()
153 } else {
154 "dragonruby".to_string()
155 }
156}
157
158pub fn dragonruby_bind_name() -> String {
159 if cfg!(windows) {
160 "dragonruby-bind.exe".to_string()
161 } else {
162 "dragonruby-bind".to_string()
163 }
164}
165
166pub fn dragonruby_httpd_name() -> String {
167 if cfg!(windows) {
168 "dragonruby-httpd.exe".to_string()
169 } else {
170 "dragonruby-httpd".to_string()
171 }
172}
173
174pub fn dragonruby_publish_name() -> String {
175 if cfg!(windows) {
176 "dragonruby-publish.exe".to_string()
177 } else {
178 "dragonruby-publish".to_string()
179 }
180}
181
182fn parse_dragonruby_zip(path: &Path) -> DragonRubyResult {
183 let cache = smaug::cache_dir();
184 trace!("Unzipping DragonRuby from {}", path.display());
185 rm_rf::ensure_removed(cache.clone()).expect("Couldn't clear cache");
186 zip_extensions::zip_extract(&path.to_path_buf(), &cache).expect("Could not extract zip");
187 trace!("Unzipped DragonRuby to {}", cache.display());
188
189 parse_dragonruby_dir(&cache)
190}
191
192fn find_base_dir(path: &Path) -> io::Result<PathBuf> {
193 if !path.is_dir() {
194 return Err(io::Error::new(
195 io::ErrorKind::NotFound,
196 "did not pass in a directory",
197 ));
198 }
199
200 let files = path.read_dir()?;
201
202 for entry in files {
203 let entry = entry?.path();
204 trace!("Looking for dragonruby at {:?}", entry);
205
206 if entry.is_dir() {
207 let bd = find_base_dir(entry.as_path());
208
209 if bd.is_ok() {
210 return bd;
211 }
212 } else if entry
213 .file_name()
214 .expect("entry did not have a file name")
215 .to_string_lossy()
216 == dragonruby_bin_name()
217 {
218 let parent = entry.parent();
219
220 match parent {
221 Some(parent_path) => return Ok(parent_path.to_path_buf()),
222 None => {
223 return Err(io::Error::new(
224 io::ErrorKind::NotFound,
225 "could not find DragonRuby directory",
226 ))
227 }
228 }
229 }
230 }
231
232 Err(io::Error::new(
233 io::ErrorKind::NotFound,
234 "could not find DragonRuby directory",
235 ))
236}
237
238fn parse_dragonruby_dir(path: &Path) -> DragonRubyResult {
239 trace!("Parsing DragonRuby directory at {}", path.display());
240 let edition: Edition;
241
242 if !path.is_dir() {
243 trace!("{:?} is not a directory", path);
244 return Err(DragonRubyError::DragonRubyNotFound {
245 path: path.to_path_buf(),
246 });
247 };
248
249 let base_path = match find_base_dir(path) {
250 Ok(base) => base,
251 Err(_) => {
252 trace!("No base path found");
253 return Err(DragonRubyError::DragonRubyNotFound {
254 path: path.to_path_buf(),
255 });
256 }
257 };
258
259 let dragonruby_bin = base_path.join(dragonruby_bin_name());
260 debug!("DragonRuby bin {}", dragonruby_bin.display());
261 let dragonruby_bind_bin = base_path.join(dragonruby_bind_name());
262 debug!("DragonRuby Bind bin {}", dragonruby_bind_bin.display());
263 let dragonruby_android_stub = base_path.join(".dragonruby/stubs/android");
264 debug!("DragonRuby iOS app bin {}", dragonruby_bind_bin.display());
265 let mut changelog = base_path.join("CHANGELOG.txt");
266 if !changelog.exists() {
267 changelog = base_path.join("CHANGELOG-CURR.txt");
268 }
269 debug!("Changelog {}", changelog.display());
270
271 if !dragonruby_bin.exists() || !changelog.exists() {
272 return Err(DragonRubyError::DragonRubyNotFound { path: base_path });
273 };
274
275 let changelog_contents = fs::read_to_string(changelog).expect("CHANGELOG could not be read.");
276
277 let first_line = changelog_contents
278 .lines()
279 .next()
280 .expect("No lines in changelog");
281
282 debug!("First Line: {}", first_line);
283
284 let latest = first_line.replace("* ", "");
285
286 debug!("Latest: {}", latest);
287
288 let version =
289 SemVer::parse(format!("{}.0", latest.as_str()).as_str()).expect("not a valid version");
290 debug!("Version: {}", version);
291
292 if dragonruby_android_stub.exists() {
293 edition = Edition::Pro;
294 } else if dragonruby_bind_bin.exists() {
295 edition = Edition::Indie;
296 } else {
297 edition = Edition::Standard;
298 }
299
300 let dragonruby = DragonRuby {
301 path: base_path.clone(),
302 version: Version {
303 edition,
304 version,
305 identifier: base_path.file_name().unwrap().to_string_lossy().to_string(),
306 },
307 };
308
309 Ok(dragonruby)
310}
311
312impl Serialize for Version {
313 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
314 where
315 S: Serializer,
316 {
317 serializer.serialize_str(format!("{}", self).as_str())
318 }
319}