smaug_lib/
dragonruby.rs

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}