cargo_travis/
lib.rs

1extern crate badge;
2extern crate cargo;
3extern crate fs_extra;
4#[macro_use]
5extern crate serde_json;
6
7use badge::{Badge, BadgeOptions};
8use cargo::core::Workspace;
9use cargo::ops::CompileOptions;
10use cargo::util::{config::Config, errors::ProcessError, process, CargoTestError, Test};
11use cargo::CargoResult;
12use std::env;
13use std::ffi::OsString;
14use std::fs;
15use std::io::Write;
16use std::path::{Path, PathBuf};
17use std::process::{self, Command};
18
19pub struct CoverageOptions<'a> {
20    pub compile_opts: CompileOptions<'a>,
21    pub merge_dir: &'a Path,
22    pub no_fail_fast: bool,
23    pub kcov_path: &'a Path,
24    pub merge_args: Vec<OsString>, // TODO: Or &[str] ?
25    pub exclude_pattern: Option<String>
26}
27
28pub fn run_coverage(ws: &Workspace, options: &CoverageOptions, test_args: &[String]) -> CargoResult<Option<CargoTestError>> {
29    // TODO: It'd be nice if there was a flag in compile_opts for this.
30
31    // The compiler needs to be told to not remove any code that isn't called or
32    // it'll be missed in the coverage counts, but the existing user-provided
33    // RUSTFLAGS should be preserved as well (and should be put last, so that
34    // they override any earlier repeats).
35    let mut rustflags: std::ffi::OsString = "-C link-dead-code".into();
36    if options.compile_opts.build_config.release {
37        // In release mode, ensure that there's debuginfo in some form so that
38        // kcov has something to work with.
39        rustflags.push(" -C debuginfo=2");
40    }
41    if let Some(existing) = std::env::var_os("RUSTFLAGS") {
42        rustflags.push(" ");
43        rustflags.push(existing);
44    }
45    std::env::set_var("RUSTFLAGS", rustflags);
46
47
48    let mut compilation = try!(cargo::ops::compile(ws, &options.compile_opts));
49    compilation.tests.sort_by(|a, b| {
50        (a.0.package_id(), &a.1).cmp(&(b.0.package_id(), &b.1))
51    });
52
53    let config = options.compile_opts.config;
54    let cwd = options.compile_opts.config.cwd();
55
56    let mut errors = vec![];
57
58    let v : Vec<std::ffi::OsString> = test_args.iter().cloned().map::<std::ffi::OsString, _>(|val| val.into()).collect();
59
60    //let x = &compilation.tests.map(run_single_coverage);
61
62    for &(ref pkg, ref kind, ref test, ref exe) in &compilation.tests {
63        let to_display = match cargo::util::without_prefix(exe, &cwd) {
64            Some(path) => path,
65            None => &**exe
66        };
67
68        // DLYB trick on OSX is here v
69        let mut cmd = try!(compilation.target_process(options.kcov_path, pkg));
70        // TODO: Make all that more configurable
71        //TODO: The unwraps shouldn't cause problems... right ?
72        let target = ws.target_dir().join("kcov-".to_string() + to_display.file_name().unwrap().to_str().unwrap()).into_path_unlocked();
73        let default_include_path = format!("--include-path={}", ws.root().display());
74
75        let mut args = vec![
76            OsString::from("--verify"),
77            OsString::from(default_include_path),
78            OsString::from(target)];
79
80        // add exclude path
81        if let Some(ref exclude) = options.exclude_pattern {
82            let exclude_option = OsString::from(format!("--exclude-pattern={}", exclude));
83            args.push(exclude_option);
84        }
85
86        args.push(OsString::from(exe));
87
88        args.extend(v.clone());
89        cmd.args(&args);
90        try!(config.shell().concise(|shell| {
91            shell.status("Running", to_display.display().to_string())
92        }));
93        try!(config.shell().verbose(|shell| {
94            shell.status("Running", cmd.to_string())
95        }));
96
97        let result = cmd.exec();
98
99        match result {
100            Err(e) => {
101                match e.downcast::<ProcessError>() {
102                    Ok(e) => {
103                        errors.push(e);
104                        if !options.no_fail_fast {
105                            return Ok(Some(CargoTestError::new(Test::UnitTest {
106                                kind: kind.clone(),
107                                name: test.clone(),
108                                pkg_name: pkg.name().to_string(),
109                            }, errors)))
110                        }
111                    }
112                    Err(e) => {
113                        //This is an unexpected Cargo error rather than a test failure
114                        return Err(e)
115                    }
116                }
117            }
118            Ok(()) => {}
119        }
120    }
121
122    // Let the user pass mergeargs
123    let mut mergeargs : Vec<OsString> = vec!["--merge".to_string().into(), options.merge_dir.as_os_str().to_os_string()];
124    mergeargs.extend(options.merge_args.iter().cloned());
125    mergeargs.extend(compilation.tests.iter().map(|&(_, _, _, ref exe)|
126        ws.target_dir().join("kcov-".to_string() + exe.file_name().unwrap().to_str().unwrap()).into_path_unlocked().into()
127    ));
128    let mut cmd = process(options.kcov_path.as_os_str().to_os_string());
129    cmd.args(&mergeargs);
130    try!(config.shell().concise(|shell| {
131        shell.status("Merging coverage", options.merge_dir.display().to_string())
132    }));
133    try!(config.shell().verbose(|shell| {
134        shell.status("Merging coverage", cmd.to_string())
135    }));
136    try!(cmd.exec());
137    if errors.is_empty() {
138        Ok(None)
139    } else {
140        Ok(Some(CargoTestError::new(Test::Multiple, errors)))
141    }
142}
143
144fn require_success(status: process::ExitStatus) {
145    if !status.success() {
146        process::exit(status.code().unwrap())
147    }
148}
149
150pub fn build_kcov<P: AsRef<Path>>(kcov_dir: P) -> PathBuf {
151    // If kcov is in path
152    if let Some(paths) = std::env::var_os("PATH") {
153        for path in std::env::split_paths(&paths) {
154            if path.join("kcov").exists() {
155                return path.join("kcov");
156            }
157        }
158    }
159
160    let kcov_dir: &Path = kcov_dir.as_ref();
161    let kcov_master_dir = kcov_dir.join("kcov-master");
162    let kcov_build_dir = kcov_master_dir.join("build");
163    let kcov_built_path = kcov_build_dir.join("src/kcov");
164
165    // If we already built kcov
166    if kcov_built_path.exists() {
167        return kcov_built_path;
168    }
169
170    // Download kcov
171    println!("Downloading kcov");
172    require_success(
173        Command::new("wget")
174            .current_dir(kcov_dir)
175            .arg("https://github.com/SimonKagstrom/kcov/archive/master.zip")
176            .status()
177            .unwrap()
178    );
179
180    // Extract kcov
181    println!("Extracting kcov");
182    require_success(
183        Command::new("unzip")
184            .current_dir(kcov_dir)
185            .arg("master.zip")
186            .status()
187            .unwrap()
188    );
189
190    // Build kcov
191    fs::create_dir(&kcov_build_dir).expect(&format!("Failed to created dir {:?} for kcov", kcov_build_dir));
192    println!("CMaking kcov");
193    require_success(
194        Command::new("cmake")
195            .current_dir(&kcov_build_dir)
196            .arg("..")
197            .status()
198            .unwrap()
199    );
200    println!("Making kcov");
201    require_success(
202        Command::new("make")
203            .current_dir(&kcov_build_dir)
204            .status()
205            .unwrap()
206    );
207
208    assert!(kcov_build_dir.exists());
209    kcov_built_path
210}
211
212pub fn doc_upload(message: &str, origin: &str, gh_pages: &str, doc_path: &str, local_doc_path: &Path, clobber_index: bool) -> Result<(), (String, i32)> {
213    let doc_upload = Path::new("target/doc-upload");
214
215    if !doc_upload.exists() {
216        // If the folder doesn't exist, clone it from remote
217        // ASSUME: if target/doc-upload exists, it's ours
218        let status = Command::new("git")
219            .arg("clone")
220            .arg("--verbose")
221            .args(&["--branch", gh_pages])
222            .args(&["--depth", "1"])
223            .arg(origin)
224            .arg(doc_upload)
225            .status()
226            .unwrap();
227        if !status.success() {
228            // If clone fails, that means that the remote doesn't exist
229            // So we create a new repository for the documentation branch
230            require_success(
231                Command::new("git")
232                    .arg("init")
233                    .arg(doc_upload)
234                    .status()
235                    .unwrap()
236            );
237            require_success(
238                Command::new("git")
239                    .current_dir(doc_upload)
240                    .arg("checkout")
241                    .args(&["-b", gh_pages])
242                    .status()
243                    .unwrap()
244            );
245        }
246    }
247
248    let doc_upload_branch = doc_upload.join(doc_path);
249    fs::create_dir(&doc_upload_branch).ok(); // Create dir if not exists
250
251    // we can't canonicalize before we create the folder
252    let doc_upload_branch = doc_upload_branch.canonicalize().unwrap();
253
254    if !doc_upload_branch.starts_with(env::current_dir().unwrap().join(doc_upload)) {
255        return Err(("Path passed in `--path` is outside the intended `target/doc-upload` folder".to_string(), 1));
256    }
257
258    for entry in doc_upload_branch.read_dir().unwrap() {
259        let dir = entry.unwrap();
260        // Delete all files in directory, as we'll be copying in everything
261        // Ignore index.html (at root) so a redirect page can be manually added
262        // Unless user wants otherwise (--clobber-index)
263        // Or a new one was generated
264        if dir.file_name() != OsString::from("index.html")
265            || clobber_index
266            || local_doc_path.join("index.html").exists()
267        {
268            let path = dir.path();
269            println!("rm -r {}", path.to_string_lossy());
270            fs::remove_dir_all(&path).ok();
271            fs::remove_file(path).ok();
272        }
273    }
274
275    // default badge shows that no successful build was made
276    let mut badge_status = "no builds".to_string();
277    let mut badge_color = "#e05d44".to_string();
278
279    // try to read manifest to extract version number
280    let config = Config::default().expect("failed to create cargo Config");
281    let mut version = Err(());
282
283    let mut manifest = env::current_dir().unwrap();
284    manifest.push("Cargo.toml");
285
286    match Workspace::new(&manifest, &config) {
287        Ok(workspace) => match workspace.current() {
288            Ok(package) => version = Ok(format!("{}", package.manifest().version())),
289            Err(error) => println!("couldn't get package: {}", error),
290        },
291        Err(error) => println!("couldn't generate workspace: {}", error),
292    }
293
294    // update badge to contain version number
295    if let Ok(version) = &version {
296        badge_status = version.clone();
297    }
298
299    let doc = local_doc_path;
300    println!("cp {} {}", doc.to_string_lossy(), doc_upload_branch.to_string_lossy());
301    let mut last_progress = 0;
302
303    let mut result = Ok(());
304
305    if let Ok(doc) = doc.read_dir() {
306        fs_extra::copy_items_with_progress(
307            &doc.map(|entry| entry.unwrap().path()).collect(),
308            &doc_upload_branch,
309            &fs_extra::dir::CopyOptions::new(),
310            |info| {
311                // Some documentation can be very large, especially with a large number of dependencies
312                // Don't go silent during copy, give updates every MiB processed
313                if info.copied_bytes >> 20 > last_progress {
314                    last_progress = info.copied_bytes >> 20;
315                    println!("{}/{} MiB", info.copied_bytes >> 20, info.total_bytes >> 20);
316                }
317                fs_extra::dir::TransitProcessResult::ContinueOrAbort
318            }
319        ).unwrap();
320
321        // update the badge to reflect build was successful
322        // but only if we managed to extract a version number
323        if version.is_ok() {
324            badge_color = "#4d76ae".to_string();
325        }
326    }
327    else {
328        println!("No documentation found to upload.");
329        result = Err(("No documentation generated".to_string(), 1));
330    }
331    
332    // make badge.json
333    let json = json!({
334        "schemaVersion": 1,
335        "label": "docs",
336        "message": badge_status,
337        "color": badge_color
338    });
339
340    let mut file = fs::File::create(doc_upload_branch.join("badge.json")).unwrap();
341    file.write_all(json.to_string().as_bytes()).unwrap();
342
343    // make badge.svg
344    let badge_options = BadgeOptions {
345        subject: "docs".to_string(),
346        status: badge_status.to_string(),
347        color: badge_color.to_string(),
348    };
349
350    let mut file = fs::File::create(doc_upload_branch.join("badge.svg")).unwrap();
351    file.write_all(Badge::new(badge_options).unwrap().to_svg().as_bytes()).unwrap();
352
353    // Tell git to track all of the files we copied over
354    // Also tracks deletions of files if things changed
355    require_success(
356        Command::new("git")
357            .current_dir(doc_upload)
358            .arg("add")
359            .arg("--verbose")
360            .arg("--all")
361            .status()
362            .unwrap()
363    );
364
365    // Save the changes
366    if Command::new("git")
367        .current_dir(doc_upload)
368        .arg("commit")
369        .arg("--verbose")
370        .args(&["-m", message])
371        .status().is_err()
372    {
373        println!("No changes to the documentation.");
374    } else {
375        // Push changes to GitHub
376        require_success(
377            Command::new("git")
378                .current_dir(doc_upload)
379                .arg("push")
380                .arg(origin)
381                .arg(gh_pages)
382                .status()
383                .unwrap(),
384        );
385    }
386    result
387}