use std::cmp::Ordering;
use std::collections::{HashSet, HashMap, BinaryHeap, BTreeMap};
-use std::iter::FromIterator;
use std::fmt;
+use std::iter::FromIterator;
use std::ops::Range;
use std::rc::Rc;
+use std::time::{Instant, Duration};
use semver;
use url::Url;
pub fn resolve(summaries: &[(Summary, Method)],
replacements: &[(PackageIdSpec, Dependency)],
registry: &mut Registry,
- config: Option<&Config>) -> CargoResult<Resolve> {
+ config: Option<&Config>,
+ print_warnings: bool) -> CargoResult<Resolve> {
let cx = Context {
resolve_graph: RcList::new(),
resolve_features: HashMap::new(),
warnings: RcList::new(),
};
let _p = profile::start("resolving");
- let cx = activate_deps_loop(cx, registry, summaries)?;
+ let cx = activate_deps_loop(cx, registry, summaries, config)?;
let mut resolve = Resolve {
graph: cx.graph(),
// If we have a shell, emit warnings about required deps used as feature.
if let Some(config) = config {
- let mut shell = config.shell();
- let mut warnings = &cx.warnings;
- while let Some(ref head) = warnings.head {
- shell.warn(&head.0)?;
- warnings = &head.1;
+ if print_warnings {
+ let mut shell = config.shell();
+ let mut warnings = &cx.warnings;
+ while let Some(ref head) = warnings.head {
+ shell.warn(&head.0)?;
+ warnings = &head.1;
+ }
}
}
parent: Option<&Summary>,
candidate: Candidate,
method: &Method)
- -> CargoResult<Option<DepsFrame>> {
+ -> CargoResult<Option<(DepsFrame, Duration)>> {
if let Some(parent) = parent {
cx.resolve_graph.push(GraphNode::Link(parent.package_id().clone(),
candidate.summary.package_id().clone()));
}
};
+ let now = Instant::now();
let deps = cx.build_deps(registry, &candidate, method)?;
-
- Ok(Some(DepsFrame {
+ let frame = DepsFrame {
parent: candidate,
remaining_siblings: RcVecIter::new(Rc::new(deps)),
- }))
+ };
+ Ok(Some((frame, now.elapsed())))
}
struct RcVecIter<T> {
/// dependency graph, cx.resolve is returned.
fn activate_deps_loop<'a>(mut cx: Context<'a>,
registry: &mut Registry,
- summaries: &[(Summary, Method)])
+ summaries: &[(Summary, Method)],
+ config: Option<&Config>)
-> CargoResult<Context<'a>> {
// Note that a `BinaryHeap` is used for the remaining dependencies that need
// activation. This heap is sorted such that the "largest value" is the most
for &(ref summary, ref method) in summaries {
debug!("initial activation: {}", summary.package_id());
let candidate = Candidate { summary: summary.clone(), replace: None };
- remaining_deps.extend(activate(&mut cx, registry, None, candidate,
- method)?);
+ let res = activate(&mut cx, registry, None, candidate, method)?;
+ if let Some((frame, _)) = res {
+ remaining_deps.push(frame);
+ }
}
+ let mut ticks = 0;
+ let start = Instant::now();
+ let time_to_print = Duration::from_millis(500);
+ let mut printed = false;
+ let mut deps_time = Duration::new(0, 0);
+
// Main resolution loop, this is the workhorse of the resolution algorithm.
//
// You'll note that a few stacks are maintained on the side, which might
// backtracking states where if we hit an error we can return to in order to
// attempt to continue resolving.
while let Some(mut deps_frame) = remaining_deps.pop() {
+
+ // If we spend a lot of time here (we shouldn't in most cases) then give
+ // a bit of a visual indicator as to what we're doing. Only enable this
+ // when stderr is a tty (a human is likely to be watching) to ensure we
+ // get deterministic output otherwise when observed by tools.
+ //
+ // Also note that we hit this loop a lot, so it's fairly performance
+ // sensitive. As a result try to defer a possibly expensive operation
+ // like `Instant::now` by only checking every N iterations of this loop
+ // to amortize the cost of the current time lookup.
+ ticks += 1;
+ if let Some(config) = config {
+ if config.shell().is_err_tty() &&
+ !printed &&
+ ticks % 1000 == 0 &&
+ start.elapsed() - deps_time > time_to_print
+ {
+ printed = true;
+ config.shell().status("Resolving", "dependency graph...")?;
+ }
+ }
+
let frame = match deps_frame.remaining_siblings.next() {
Some(sibling) => {
let parent = Summary::clone(&deps_frame.parent);
};
trace!("{}[{}]>{} trying {}", parent.name(), cur, dep.name(),
candidate.summary.version());
- remaining_deps.extend(activate(&mut cx, registry, Some(&parent),
- candidate, &method)?);
+ let res = activate(&mut cx, registry, Some(&parent), candidate, &method)?;
+ if let Some((frame, dur)) = res {
+ remaining_deps.push(frame);
+ deps_time += dur;
+ }
}
Ok(cx)
use std::io::prelude::*;
use atty;
-use termcolor::Color::{Green, Red, Yellow};
+use termcolor::Color::{Green, Red, Yellow, Cyan};
use termcolor::{self, StandardStream, Color, ColorSpec, WriteColor};
use util::errors::CargoResult;
impl fmt::Debug for Shell {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.err {
- &ShellOut::Write(_) => f.debug_struct("Shell")
- .field("verbosity", &self.verbosity)
- .finish(),
- &ShellOut::Stream(_, color_choice) => f.debug_struct("Shell")
- .field("verbosity", &self.verbosity)
- .field("color_choice", &color_choice)
- .finish()
+ &ShellOut::Write(_) => {
+ f.debug_struct("Shell")
+ .field("verbosity", &self.verbosity)
+ .finish()
+ }
+ &ShellOut::Stream { color_choice, .. } => {
+ f.debug_struct("Shell")
+ .field("verbosity", &self.verbosity)
+ .field("color_choice", &color_choice)
+ .finish()
+ }
}
}
}
/// A plain write object without color support
Write(Box<Write>),
/// Color-enabled stdio, with information on whether color should be used
- Stream(StandardStream, ColorChoice),
+ Stream {
+ stream: StandardStream,
+ tty: bool,
+ color_choice: ColorChoice,
+ },
}
/// Whether messages should use color output
/// output.
pub fn new() -> Shell {
Shell {
- err: ShellOut::Stream(
- StandardStream::stderr(ColorChoice::CargoAuto.to_termcolor_color_choice()),
- ColorChoice::CargoAuto,
- ),
+ err: ShellOut::Stream {
+ stream: StandardStream::stderr(ColorChoice::CargoAuto.to_termcolor_color_choice()),
+ color_choice: ColorChoice::CargoAuto,
+ tty: atty::is(atty::Stream::Stderr),
+ },
verbosity: Verbosity::Verbose,
}
}
/// messages follows without color.
fn print(&mut self,
status: &fmt::Display,
- message: &fmt::Display,
+ message: Option<&fmt::Display>,
color: Color,
justified: bool) -> CargoResult<()> {
match self.verbosity {
}
}
+ /// Returns the width of the terminal in spaces, if any
+ pub fn err_width(&self) -> Option<usize> {
+ match self.err {
+ ShellOut::Stream { tty: true, .. } => imp::stderr_width(),
+ _ => None,
+ }
+ }
+
+ /// Returns whether stderr is a tty
+ pub fn is_err_tty(&self) -> bool {
+ match self.err {
+ ShellOut::Stream { tty, .. } => tty,
+ _ => false,
+ }
+ }
+
/// Get a reference to the underlying writer
pub fn err(&mut self) -> &mut Write {
self.err.as_write()
pub fn status<T, U>(&mut self, status: T, message: U) -> CargoResult<()>
where T: fmt::Display, U: fmt::Display
{
- self.print(&status, &message, Green, true)
+ self.print(&status, Some(&message), Green, true)
+ }
+
+ pub fn status_header<T>(&mut self, status: T) -> CargoResult<()>
+ where T: fmt::Display,
+ {
+ self.print(&status, None, Cyan, true)
}
/// Shortcut to right-align a status message.
color: Color) -> CargoResult<()>
where T: fmt::Display, U: fmt::Display
{
- self.print(&status, &message, color, true)
+ self.print(&status, Some(&message), color, true)
}
/// Run the callback only if we are in verbose mode
/// Print a red 'error' message
pub fn error<T: fmt::Display>(&mut self, message: T) -> CargoResult<()> {
- self.print(&"error:", &message, Red, false)
+ self.print(&"error:", Some(&message), Red, false)
}
/// Print an amber 'warning' message
pub fn warn<T: fmt::Display>(&mut self, message: T) -> CargoResult<()> {
match self.verbosity {
Verbosity::Quiet => Ok(()),
- _ => self.print(&"warning:", &message, Yellow, false),
+ _ => self.print(&"warning:", Some(&message), Yellow, false),
}
}
/// Update the color choice (always, never, or auto) from a string.
pub fn set_color_choice(&mut self, color: Option<&str>) -> CargoResult<()> {
- if let ShellOut::Stream(ref mut err, ref mut cc) = self.err {
+ if let ShellOut::Stream { ref mut stream, ref mut color_choice, .. } = self.err {
let cfg = match color {
Some("always") => ColorChoice::Always,
Some("never") => ColorChoice::Never,
Some(arg) => bail!("argument for --color must be auto, always, or \
never, but found `{}`", arg),
};
- *cc = cfg;
- *err = StandardStream::stderr(cfg.to_termcolor_color_choice());
+ *color_choice = cfg;
+ *stream = StandardStream::stderr(cfg.to_termcolor_color_choice());
}
Ok(())
}
/// has been set to something else.
pub fn color_choice(&self) -> ColorChoice {
match self.err {
- ShellOut::Stream(_, cc) => cc,
+ ShellOut::Stream { color_choice, .. } => color_choice,
ShellOut::Write(_) => ColorChoice::Never,
}
}
/// The status can be justified, in which case the max width that will right align is 12 chars.
fn print(&mut self,
status: &fmt::Display,
- message: &fmt::Display,
+ message: Option<&fmt::Display>,
color: Color,
justified: bool) -> CargoResult<()> {
match *self {
- ShellOut::Stream(ref mut err, _) => {
- err.reset()?;
- err.set_color(ColorSpec::new()
+ ShellOut::Stream { ref mut stream, .. } => {
+ stream.reset()?;
+ stream.set_color(ColorSpec::new()
.set_bold(true)
.set_fg(Some(color)))?;
if justified {
- write!(err, "{:>12}", status)?;
+ write!(stream, "{:>12}", status)?;
} else {
- write!(err, "{}", status)?;
+ write!(stream, "{}", status)?;
+ }
+ stream.reset()?;
+ match message {
+ Some(message) => write!(stream, " {}\n", message)?,
+ None => write!(stream, " ")?,
}
- err.reset()?;
- write!(err, " {}\n", message)?;
}
ShellOut::Write(ref mut w) => {
if justified {
} else {
write!(w, "{}", status)?;
}
- write!(w, " {}\n", message)?;
+ match message {
+ Some(message) => write!(w, " {}\n", message)?,
+ None => write!(w, " ")?,
+ }
}
}
Ok(())
/// Get this object as a `io::Write`.
fn as_write(&mut self) -> &mut Write {
match *self {
- ShellOut::Stream(ref mut err, _) => err,
+ ShellOut::Stream { ref mut stream, .. } => stream,
ShellOut::Write(ref mut w) => w,
}
}
}
}
}
+
+#[cfg(unix)]
+mod imp {
+ use std::mem;
+
+ use libc;
+
+ pub fn stderr_width() -> Option<usize> {
+ unsafe {
+ let mut winsize: libc::winsize = mem::zeroed();
+ if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ, &mut winsize) < 0 {
+ return None
+ }
+ if winsize.ws_col > 0 {
+ Some(winsize.ws_col as usize)
+ } else {
+ None
+ }
+ }
+ }
+}
+
+#[cfg(windows)]
+mod imp {
+ use std::mem;
+
+ extern crate winapi;
+ extern crate kernel32;
+
+ pub fn stderr_width() -> Option<usize> {
+ unsafe {
+ let stdout = kernel32::GetStdHandle(winapi::STD_ERROR_HANDLE);
+ let mut csbi: winapi::CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
+ if kernel32::GetConsoleScreenBufferInfo(stdout, &mut csbi) == 0 {
+ return None
+ }
+ Some((csbi.srWindow.Right - csbi.srWindow.Left) as usize)
+ }
+ }
+}
None => root_replace.to_vec(),
};
- let config = if warn {
- Some(ws.config())
- } else {
- None
- };
let mut resolved = resolver::resolve(&summaries,
&replace,
registry,
- config)?;
+ Some(ws.config()),
+ warn)?;
resolved.register_used_patches(registry.patches());
if let Some(previous) = previous {
resolved.merge_from(previous)?;
use url::Url;
use core::GitReference;
-use util::{ToUrl, internal, Config, network};
+use util::{ToUrl, internal, Config, network, Progress};
use util::errors::{CargoResult, CargoResultExt, CargoError};
#[derive(PartialEq, Clone, Debug)]
}
impl GitDatabase {
- fn path(&self) -> &Path {
- &self.path
- }
-
pub fn copy_to(&self, rev: GitRevision, dest: &Path, cargo_config: &Config)
-> CargoResult<GitCheckout> {
let checkout = match git2::Repository::open(dest) {
let mut checkout = GitCheckout::new(dest, self, rev, repo);
if !checkout.is_fresh() {
checkout.fetch(cargo_config)?;
- checkout.reset()?;
+ checkout.reset(cargo_config)?;
assert!(checkout.is_fresh());
}
checkout
}
- Err(..) => GitCheckout::clone_into(dest, self, rev)?,
+ Err(..) => GitCheckout::clone_into(dest, self, rev, cargo_config)?,
};
checkout.update_submodules(cargo_config)?;
Ok(checkout)
}
}
- fn clone_into(into: &Path, database: &'a GitDatabase,
- revision: GitRevision)
+ fn clone_into(into: &Path,
+ database: &'a GitDatabase,
+ revision: GitRevision,
+ config: &Config)
-> CargoResult<GitCheckout<'a>>
{
- let repo = GitCheckout::clone_repo(database.path(), into)?;
- let checkout = GitCheckout::new(into, database, revision, repo);
- checkout.reset()?;
- Ok(checkout)
- }
-
- fn clone_repo(source: &Path, into: &Path) -> CargoResult<git2::Repository> {
let dirname = into.parent().unwrap();
-
fs::create_dir_all(&dirname).chain_err(|| {
format!("Couldn't mkdir {}", dirname.display())
})?;
-
if fs::metadata(&into).is_ok() {
fs::remove_dir_all(into).chain_err(|| {
format!("Couldn't rmdir {}", into.display())
})?;
}
-
- let url = source.to_url()?;
- let url = url.to_string();
- let repo = git2::Repository::clone(&url, into)
- .chain_err(|| {
- internal(format!("failed to clone {} into {}", source.display(),
- into.display()))
- })?;
- Ok(repo)
+ let repo = git2::Repository::init(into)?;
+ let mut checkout = GitCheckout::new(into, database, revision, repo);
+ checkout.fetch(config)?;
+ checkout.reset(config)?;
+ Ok(checkout)
}
fn is_fresh(&self) -> bool {
Ok(())
}
- fn reset(&self) -> CargoResult<()> {
+ fn reset(&self, config: &Config) -> CargoResult<()> {
// If we're interrupted while performing this reset (e.g. we die because
// of a signal) Cargo needs to be sure to try to check out this repo
// again on the next go-round.
let _ = fs::remove_file(&ok_file);
info!("reset {} to {}", self.repo.path().display(), self.revision);
let object = self.repo.find_object(self.revision.0, None)?;
- self.repo.reset(&object, git2::ResetType::Hard, None)?;
+ reset(&self.repo, &object, config)?;
File::create(ok_file)?;
Ok(())
}
Err(..) => {
let path = parent.workdir().unwrap().join(child.path());
let _ = fs::remove_dir_all(&path);
- git2::Repository::clone(url, &path)?
+ git2::Repository::init(&path)?
}
};
child.name().unwrap_or(""), url))
})?;
- repo.find_object(head, None)
- .and_then(|obj| { repo.reset(&obj, git2::ResetType::Hard, None)})?;
+ let obj = repo.find_object(head, None)?;
+ reset(&repo, &obj, cargo_config)?;
update_submodules(&repo, cargo_config)
}
}
})
}
+fn reset(repo: &git2::Repository,
+ obj: &git2::Object,
+ config: &Config) -> CargoResult<()> {
+ let mut pb = Progress::new("Checkout", config);
+ let mut opts = git2::build::CheckoutBuilder::new();
+ opts.progress(|_, cur, max| {
+ drop(pb.tick(cur, max));
+ });
+ repo.reset(obj, git2::ResetType::Hard, Some(&mut opts))?;
+ Ok(())
+}
+
pub fn fetch(repo: &mut git2::Repository,
url: &Url,
refspec: &str,
maybe_gc_repo(repo)?;
debug!("doing a fetch for {}", url);
+ let mut progress = Progress::new("Fetch", config);
with_authentication(url.as_str(), &repo.config()?, |f| {
let mut cb = git2::RemoteCallbacks::new();
cb.credentials(f);
+ cb.transfer_progress(|stats| {
+ progress.tick(stats.indexed_objects(), stats.total_objects()).is_ok()
+ });
+
// Create a local anonymous remote in the repository to fetch the url
let mut remote = repo.remote_anonymous(url.as_str())?;
let mut opts = git2::FetchOptions::new();
use std::io::prelude::*;
use std::mem;
use std::path::Path;
+use std::str;
use git2;
use hex::ToHex;
use sources::registry::{RegistryData, RegistryConfig, INDEX_LOCK};
use util::network;
use util::{FileLock, Filesystem, LazyCell};
-use util::{Config, Sha256, ToUrl};
+use util::{Config, Sha256, ToUrl, Progress};
use util::errors::{CargoErrorKind, CargoResult, CargoResultExt};
pub struct RemoteRegistry<'cfg> {
network::with_retry(self.config, || {
state = Sha256::new();
body = Vec::new();
+ let mut pb = Progress::new("Fetch", self.config);
{
+ handle.progress(true)?;
let mut handle = handle.transfer();
+ handle.progress_function(|dl_total, dl_cur, _, _| {
+ pb.tick(dl_cur as usize, dl_total as usize).is_ok()
+ })?;
handle.write_function(|buf| {
state.update(buf);
body.extend_from_slice(buf);
}
pub fn http(&self) -> CargoResult<&RefCell<Easy>> {
- self.easy.get_or_try_init(|| {
+ let http = self.easy.get_or_try_init(|| {
ops::http_handle(self).map(RefCell::new)
- })
+ })?;
+ http.borrow_mut().reset();
+ Ok(http)
}
}
pub use self::to_url::ToUrl;
pub use self::vcs::{GitRepo, HgRepo, PijulRepo, FossilRepo};
pub use self::read2::read2;
+pub use self::progress::Progress;
pub mod config;
pub mod errors;
mod lazy_cell;
mod flock;
mod read2;
+mod progress;
--- /dev/null
+use std::cmp;
+use std::iter;
+use std::time::{Instant, Duration};
+
+use util::{Config, CargoResult};
+
+pub struct Progress<'cfg> {
+ state: Option<State<'cfg>>,
+}
+
+struct State<'cfg> {
+ config: &'cfg Config,
+ width: usize,
+ first: bool,
+ last_update: Instant,
+ name: String,
+ done: bool,
+}
+
+impl<'cfg> Progress<'cfg> {
+ pub fn new(name: &str, cfg: &'cfg Config) -> Progress<'cfg> {
+ Progress {
+ state: cfg.shell().err_width().map(|n| {
+ State {
+ config: cfg,
+ width: cmp::min(n, 80),
+ first: true,
+ last_update: Instant::now(),
+ name: name.to_string(),
+ done: false,
+ }
+ }),
+ }
+ }
+
+ pub fn tick(&mut self, cur: usize, max: usize) -> CargoResult<()> {
+ match self.state {
+ Some(ref mut s) => s.tick(cur, max),
+ None => Ok(())
+ }
+ }
+}
+
+impl<'cfg> State<'cfg> {
+ fn tick(&mut self, cur: usize, max: usize) -> CargoResult<()> {
+ if self.done {
+ return Ok(())
+ }
+
+ // Don't update too often as it can cause excessive performance loss
+ // just putting stuff onto the terminal. We also want to avoid
+ // flickering by not drawing anything that goes away too quickly. As a
+ // result we've got two branches here:
+ //
+ // 1. If we haven't drawn anything, we wait for a period of time to
+ // actually start drawing to the console. This ensures that
+ // short-lived operations don't flicker on the console. Currently
+ // there's a 500ms delay to when we first draw something.
+ // 2. If we've drawn something, then we rate limit ourselves to only
+ // draw to the console every so often. Currently there's a 100ms
+ // delay between updates.
+ if self.first {
+ let delay = Duration::from_millis(500);
+ if self.last_update.elapsed() < delay {
+ return Ok(())
+ }
+ self.first = false;
+ } else {
+ let interval = Duration::from_millis(100);
+ if self.last_update.elapsed() < interval {
+ return Ok(())
+ }
+ }
+ self.last_update = Instant::now();
+
+ // Render the percentage at the far right and then figure how long the
+ // progress bar is
+ let pct = (cur as f64) / (max as f64);
+ let pct = if !pct.is_finite() { 0.0 } else { pct };
+ let stats = format!(" {:6.02}%", pct * 100.0);
+ let extra_len = stats.len() + 2 /* [ and ] */ + 15 /* status header */;
+ let display_width = match self.width.checked_sub(extra_len) {
+ Some(n) => n,
+ None => return Ok(()),
+ };
+ let mut string = String::from("[");
+ let hashes = display_width as f64 * pct;
+ let hashes = hashes as usize;
+
+ // Draw the `===>`
+ if hashes > 0 {
+ for _ in 0..hashes-1 {
+ string.push_str("=");
+ }
+ if cur == max {
+ self.done = true;
+ string.push_str("=");
+ } else {
+ string.push_str(">");
+ }
+ }
+
+ // Draw the empty space we have left to do
+ for _ in 0..(display_width - hashes) {
+ string.push_str(" ");
+ }
+ string.push_str("]");
+ string.push_str(&stats);
+
+ // Write out a pretty header, then the progress bar itself, and then
+ // return back to the beginning of the line for the next print.
+ self.config.shell().status_header(&self.name)?;
+ write!(self.config.shell().err(), "{}\r", string)?;
+ Ok(())
+ }
+}
+
+fn clear(width: usize, config: &Config) {
+ let blank = iter::repeat(" ").take(width).collect::<String>();
+ drop(write!(config.shell().err(), "{}\r", blank));
+}
+
+impl<'cfg> Drop for State<'cfg> {
+ fn drop(&mut self) {
+ clear(self.width, self.config);
+ }
+}
let mut registry = MyRegistry(registry);
let summary = Summary::new(pkg.clone(), deps, BTreeMap::new()).unwrap();
let method = Method::Everything;
- let resolve = resolver::resolve(&[(summary, method)], &[], &mut registry, None)?;
+ let resolve = resolver::resolve(&[(summary, method)], &[], &mut registry, None, false)?;
let res = resolve.iter().cloned().collect();
Ok(res)
}