cargo_erlangapp/
lib.rs

1
2extern crate serde_json as json;
3
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::fs::DirEntry;
7use std::process;
8use std::error::Error;
9use std::io::{self, stderr, Write};
10use std::convert::From;
11use std::result;
12use std::fmt::{self, Display};
13
14/// `try!` for `Option`
15macro_rules! otry(
16    ($e:expr) => (match $e { Some(e) => e, None => return None })
17);
18
19// Special OSX link args
20// Without them linker throws a fit about NIF API calls.
21#[cfg(target_os="macos")]
22static DYLIB_LINKER_ARGS: &'static[&'static str] = &["--", "--codegen", "link-args=-flat_namespace -undefined suppress"];
23
24#[cfg(not(target_os="macos"))]
25static DYLIB_LINKER_ARGS: &'static[&'static str] = &[];
26
27
28static BIN_LINKER_ARGS: &'static[&'static str] = &[];
29
30
31
32#[derive(Debug)]
33enum MsgError {
34    Msg(&'static str),
35    MsgIo(&'static str, io::Error),
36}
37
38use MsgError::*;
39
40impl Error for MsgError {
41    fn description(&self) -> &str {
42        match self {
43            &Msg(s) => s,
44            &MsgIo(s, ref _err) => s,
45        }
46    }
47}
48
49impl Display for MsgError {
50    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
51        match self {
52            &Msg(s) =>
53                write!(f, "{}", s),
54            &MsgIo(s, ref err) =>
55                write!(f, "{} ({})", s, err),
56        }
57    }
58}
59
60/// Main entry point into this application.  Invoked by main() and integration tests
61pub fn invoke_with_args_str(args: &[&str], appdir: &Path) {
62    let args_string: Vec<String> = args.into_iter().cloned().map(From::from).collect();
63    invoke_with_args(&args_string, appdir)
64}
65
66pub fn invoke_with_args(args: &[String], appdir: &Path)
67{
68    match ArgsInfo::from_args(&args) {
69        Some(ref ai) => invoke(ai, appdir),
70        None => usage(),
71    }
72}
73
74
75fn usage() {
76    writeln!(stderr(), "Usage:").unwrap();
77    writeln!(stderr(), "\tcargo-erlangapp build [cargo rustc args]").unwrap();
78    writeln!(stderr(), "\tcargo-erlangapp clean [cargo clean args]").unwrap();
79    writeln!(stderr(), "\tcargo-erlangapp test [cargo test args]").unwrap();
80    process::exit(1);
81}
82
83
84
85fn invoke(argsinfo: &ArgsInfo, appdir: &Path) {
86    match do_command(argsinfo, appdir) {
87        Ok(_) => (),
88        Err(err) => {
89            writeln!(stderr(), "Error: {}", err).unwrap();
90            process::exit(1);
91        }
92    }
93}
94
95fn do_command(argsinfo: &ArgsInfo, appdir: &Path) -> Result<(), MsgError> {
96    match argsinfo.command {
97        CargoCommand::Build =>
98            build_crates(argsinfo, appdir),
99        CargoCommand::Test =>
100            test_crates(argsinfo, appdir),
101        CargoCommand::Clean =>
102            clean_crates(argsinfo, appdir),
103    }
104}
105
106fn build_crates(argsinfo: &ArgsInfo, appdir: &Path) -> Result<(), MsgError> {
107    // build(rustc) each crate
108    for crate_dir in try!(enumerate_crate_dirs(appdir)).iter() {
109        for target in try!(enumerate_targets(crate_dir)).into_iter() {
110            println!("Building {}", crate_dir.to_string_lossy());
111
112            // args for build target
113            let mut rustc_args: Vec<String> = match target {
114                Target::Bin(ref s) => vec!("--bin".to_string(), s.to_string()),
115                Target::Dylib(_) => vec!("--lib".to_string()),  // only 1 lib permitted per crate, name is implicit
116            };
117
118            // args from commandline
119            rustc_args.extend(argsinfo.cargo_args.iter().cloned());
120
121            // linker args
122            rustc_args.extend(linker_args(&target).iter().map(|x|x.to_string()));
123
124            // build it!
125            try!(cargo_command("rustc", rustc_args.as_slice(), crate_dir));
126
127            // copy artifacts to priv/crates/<cratename>
128            let (dst_name, src_name) = target_filenames(&target);
129
130            // build src path
131            let mut src_path = crate_dir.join("target");
132            if let Some(ref target_arch) = argsinfo.target {
133                src_path.push(target_arch);
134            }
135            src_path.push( match argsinfo.build_type {
136                BuildType::Release => "release",
137                _ => "debug",
138            });
139            src_path.push(src_name);
140
141            // build dst path
142            let mut dst_path = appdir.join("priv");
143            dst_path.push("crates");
144            dst_path.push(crate_dir.file_name().unwrap()); // filename will be valid if rustc worked
145            try!(fs::create_dir_all(&dst_path)
146                     .map_err(|err| MsgIo("cannot create dest directories in priv/", err)));
147            dst_path.push(dst_name);
148
149            // finally, copy the artifact with its new name.
150            try!(fs::copy(src_path, dst_path)
151                .map_err(|err| MsgIo("cannot copy artifact", err)));
152        }
153    };
154
155    Ok(())
156}
157
158fn linker_args(target: &Target) -> &'static [&'static str] {
159    match *target {
160        Target::Dylib(_) => DYLIB_LINKER_ARGS,
161        Target::Bin(_) => BIN_LINKER_ARGS,
162    }
163}
164
165
166/// OS X naming
167///
168/// Dylibs have `lib` prefix, and `.dylib` suffix gets changed to `.so`.
169#[cfg(target_os="macos")]
170pub fn target_filenames(target: &Target) -> (String, String) {
171    match *target {
172        Target::Bin(ref s) => (s.to_string(), s.to_string()),
173        Target::Dylib(ref s) => ("lib".to_string() + s + ".so", "lib".to_string() + s + ".dylib"),
174    }
175}
176/// Windows naming
177///
178/// Bins have `.exe` suffix, dylibs have `.dll` suffix.
179#[cfg(windows)]
180pub fn target_filenames(target: &Target) -> (String, String) {
181    match *target {
182        Target::Bin(ref s) => (s.to_string() + ".exe", s.to_string() + ".exe"),
183        Target::Dylib(ref s) => (s.to_string() + ".dll", s.to_string() + ".dll"),
184    }
185}
186
187/// Non-windows, non-OSX nameing
188///
189/// Dylibs have `lib` prefix and `.so` suffix.
190#[cfg(all(unix, not(target_os="macos")))]
191pub fn target_filenames(target: &Target) -> (String, String) {
192    match *target {
193        Target::Bin(ref s) => (s.to_string(), s.to_string()),
194        Target::Dylib(ref s) => ("lib".to_string() + s + ".so", "lib".to_string() + s + ".so"),
195    }
196}
197
198
199/// Build artifact types
200#[derive(Debug)]
201pub enum Target {
202    Bin(String),
203    Dylib(String),
204}
205
206impl AsRef<String> for Target {
207    fn as_ref(&self) -> &String {
208        match *self {
209            Target::Bin(ref s) => s,
210            Target::Dylib(ref s) => s,
211        }
212    }
213}
214
215impl Target {
216    /// Create target from cargo manifest fragment
217    fn from_json(obj: &json::Value) -> Option<Target> {
218        let name = otry!(obj.find("name")
219                    .and_then(|s| s.as_string())
220                    .map(|s| s.to_string()));
221        let kinds: Vec<&str> = otry!(obj.find("kind")
222            .and_then(|s| s.as_array())
223            .map(|arr| arr.iter()
224                .filter_map( |s| s.as_string())
225                .collect()));
226
227        if kinds.contains(&"bin") {
228            Some(Target::Bin(name))
229        } else if kinds.contains(&"dylib") || kinds.contains(&"cdylib"){
230            Some(Target::Dylib(name))
231        } else {
232            None
233        }
234    }
235}
236
237/// Read manifest for given crate and enumerate targets
238fn enumerate_targets(crate_dir: &Path) -> Result<Vec<Target>, MsgError> {
239    let output = try!(process::Command::new("cargo").arg("read-manifest")
240                          .current_dir(crate_dir)
241                          .output()
242                          .map_err(|err| MsgIo("Cannot read crate manifest",err)));
243
244    enumerate_targets_opt(output.stdout.as_slice())
245        .ok_or(Msg("Cannot parse crate manifest"))
246}
247/// Parse "targets" portion of JSON text to extract targets
248fn enumerate_targets_opt(json_slice: &[u8]) -> Option<Vec<Target>> {
249    let value: json::Value = otry!(json::from_slice(json_slice).ok());
250    value.find("targets")
251        .and_then(|v| v.as_array())   // :Option<Vec<Value>>
252        .map(|targets|
253                 targets
254                     .iter()
255                     .filter_map(Target::from_json)  // :Vec<Target>
256                     .collect())
257}
258
259/// Test all crates
260fn test_crates(argsinfo: &ArgsInfo, appdir: &Path) -> Result<(), MsgError> {
261    // test each create, short circuit fail
262    for crate_dir in try!(enumerate_crate_dirs(appdir)).iter() {
263        println!("Testing {}", crate_dir.to_string_lossy());
264        try!(cargo_command("test", &argsinfo.cargo_args, crate_dir));
265    };
266    Ok(())
267}
268
269/// Clean all crates, remote artifacts in `priv/`
270fn clean_crates(argsinfo: &ArgsInfo, appdir: &Path) -> Result<(), MsgError> {
271    // clean all crate dirs
272    for crate_dir in try!(enumerate_crate_dirs(appdir)).iter() {
273        println!("Cleaning {}", crate_dir.to_string_lossy());
274        try!(cargo_command("clean", &argsinfo.cargo_args, crate_dir));
275    };
276
277    // clean priv/crates
278    let output_dir =  appdir.join("priv").join("crates");
279    remove_dir_all_force(output_dir).map_err(|err| MsgIo("can't delete output dir", err))
280}
281
282// Remove dir.  The dir being absent is not an error.
283fn remove_dir_all_force<P: AsRef<Path>>(path: P) -> io::Result<()> {
284
285    match fs::metadata(path.as_ref()) {
286        Err(err) => {
287            match err.kind() {
288                io::ErrorKind::NotFound => Ok(()),  // not finding is okay (already cleaned)
289                _ => Err(err),   // permission error on parent dir?
290            }
291        },
292        Ok(m) => {
293            match m.is_dir() {
294                true => fs::remove_dir_all(path),
295                false => Ok(()),
296            }
297        },
298    }
299}
300
301fn cargo_command(cmd: &str, args: &[String], dir: &Path) -> Result<(), MsgError> {
302    process::Command::new("cargo")
303        .arg(cmd)
304        .args(args)
305        .current_dir(dir)
306        .status()
307        .map_err(|err| MsgIo("cannot start cargo", err))
308        .and_then(|status| {
309            match status.success() {
310                true => Ok(()),
311                false => Err(Msg("cargo command failed")),
312            }
313        })
314}
315
316
317fn enumerate_crate_dirs(appdir: &Path) -> Result<Vec<PathBuf>, MsgError> {
318
319    appdir
320        .join("crates")              // :PathBuf
321        .read_dir()                  // :Result<ReadDir>
322        .map_err(|err|
323            MsgIo("Cannot read 'crates' directory", err)
324        )
325        .map(|dirs|
326            dirs.filter_map(result::Result::ok)      // discard Error entries and unwrap
327            .filter(is_crate)            // discard non-crate entries
328            .map(|x| x.path())           // take whole path
329            .collect()
330        )
331}
332
333fn is_crate(dirent: &DirEntry) -> bool {
334    let mut toml_path = dirent.path();
335    toml_path.push("Cargo.toml");
336    toml_path
337        .metadata()             // :Result<Metadata>
338        .map(|x| x.is_file())   // :Result<bool>
339        .unwrap_or(false)
340}
341
342#[derive(Debug)]
343enum CargoCommand { Build, Test, Clean }
344#[derive(Debug)]
345enum BuildType { Release, Debug, DefaultDebug }
346#[derive(Debug)]
347pub struct ArgsInfo {
348    command: CargoCommand,
349    target: Option<String>,
350    build_type: BuildType,
351    cargo_args: Vec<String>,
352}
353
354impl ArgsInfo {
355    pub fn from_args(args: &[String]) -> Option<ArgsInfo> {
356        if args.len() < 2 {
357            return None;
358        }
359
360        let build_type =
361        if find_option(args, "--release") { BuildType::Release }
362            else if find_option(args, "--debug") { BuildType::Debug }
363            else { BuildType::DefaultDebug };
364
365        Some(ArgsInfo {
366            command: otry!(parse_cmd_name(args[1].as_str())),
367            target: find_option_value(&args[2..], "--target").map(Into::into),
368            build_type: build_type,
369            cargo_args: args[2..].into_iter().cloned().collect(),
370        })
371    }
372}
373
374fn parse_cmd_name(arg: &str) -> Option<CargoCommand> {
375    match arg {
376        "build" => Some(CargoCommand::Build),
377        "test" => Some(CargoCommand::Test),
378        "clean" => Some(CargoCommand::Clean),
379        _ => None,
380    }
381}
382
383fn find_option(args: &[String], key: &str) -> bool {
384    args.iter().any(|x| **x == *key)
385}
386
387/// Search args for "key=value", "key= value", "key =value", or "key = value"
388pub fn find_option_value(args: &[String], key: &str) -> Option<String> {
389    let mut i = args.iter();
390    loop {
391        let arg0 = otry!(i.next());
392        if arg0.starts_with(key) {
393            // check 'key=value'
394            match arg0.split('=').nth(1) { // try to get "value"
395                Some("") => return i.next().map(Clone::clone), // "key= value"
396                Some(x) => return Some(x.to_string()), // "key=value"
397                None => {
398                    if **arg0 == *key { // "key =.."
399                        let arg1 = otry!(i.next());
400                        if **arg1 == *"=" { return i.next().map(Clone::clone) } // "key = value"
401                        if arg1.starts_with('=') {
402                            return arg1.split('=').nth(1).map(From::from) // "key =value"
403                        }
404                        // something else, drop through and loop
405                    }
406                }
407            }
408        }
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415
416    fn find_option_value_wrapper(args: &[&str], key: &str) -> Option<String> {
417        let argsv: Vec<String> = args.into_iter().cloned().map(From::from).collect();
418        find_option_value(&argsv, key)
419    }
420
421    #[test]
422    fn test_find_option_value() {
423        assert_eq!(None, find_option_value_wrapper(&[], "key"));
424        assert_eq!(None, find_option_value_wrapper(&["asdfasdfasdfsdf"], "key"));
425        assert_eq!(None, find_option_value_wrapper(&["asdfasdfasdfsdf", "sdfsf"], "key"));
426        assert_eq!(None, find_option_value_wrapper(&["asdfasdfasdfsdf", "sdfsf", "sdfsdf"], "key"));
427        assert_eq!(Some("value".to_string()), find_option_value_wrapper(&["key=value"], "key"));
428        assert_eq!(Some("value".to_string()), find_option_value_wrapper(&["key", "=value"], "key"));
429        assert_eq!(Some("value".to_string()), find_option_value_wrapper(&["key=", "value"], "key"));
430        assert_eq!(Some("value".to_string()), find_option_value_wrapper(&["key", "=", "value"], "key"));
431        assert_eq!(None, find_option_value_wrapper(&["key", "value"], "key"));
432        assert_eq!(None, find_option_value_wrapper(&["key", "="], "key"));
433        assert_eq!(None, find_option_value_wrapper(&["key"], "key"));
434        assert_eq!(None, find_option_value_wrapper(&["key="], "key"));
435    }
436}