blob: 5d831e7584cf1a0227f318cdb07e7926b28a2472 [file] [log] [blame]
use std::cell::RefCell;
use std::collections::HashSet;
use std::env;
use std::fs::{self, File, OpenOptions};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use anyhow::anyhow;
use cargo::core::{Dependency, PackageId, Summary, Verbosity, Workspace};
use cargo::ops::{update_lockfile, UpdateOptions};
use cargo::util::errors::CargoResultExt;
use cargo::util::{CargoResult, Config};
use semver::{Identifier, Version, VersionReq};
use tempfile::{Builder, TempDir};
use toml::value::Table;
use toml::Value;
use super::{ElaborateWorkspace, Manifest};
use crate::Options;
/// A temporary project
pub struct TempProject<'tmp> {
pub workspace: Rc<RefCell<Option<Workspace<'tmp>>>>,
pub temp_dir: TempDir,
manifest_paths: Vec<PathBuf>,
config: Config,
relative_manifest: String,
options: &'tmp Options,
is_workspace_project: bool,
}
impl<'tmp> TempProject<'tmp> {
/// Copy needed manifest and lock files from an existing workspace
pub fn from_workspace(
orig_workspace: &ElaborateWorkspace<'_>,
orig_manifest: &str,
options: &'tmp Options,
) -> CargoResult<TempProject<'tmp>> {
// e.g. /path/to/project
let workspace_root = orig_workspace.workspace.root();
let workspace_root_str = workspace_root.to_string_lossy();
let temp_dir = Builder::new().prefix("cargo-outdated").tempdir()?;
let manifest_paths = manifest_paths(orig_workspace)?;
let mut tmp_manifest_paths = vec![];
for from in &manifest_paths {
// e.g. /path/to/project/src/sub
let mut from_dir = from.clone();
from_dir.pop();
let from_dir_str = from_dir.to_string_lossy();
// e.g. /tmp/cargo.xxx/src/sub
let mut dest = if workspace_root_str.len() < from_dir_str.len() {
temp_dir
.path()
.join(&from_dir_str[workspace_root_str.len() + 1..])
} else {
temp_dir.path().to_owned()
};
fs::create_dir_all(&dest)?;
// e.g. /tmp/cargo.xxx/src/sub/Cargo.toml
dest.push("Cargo.toml");
tmp_manifest_paths.push(dest.clone());
fs::copy(from, &dest)?;
//removing default-run key if it exists to check dependencies
let mut om: Manifest = {
let mut buf = String::new();
let mut file = File::open(&dest)?;
file.read_to_string(&mut buf)?;
::toml::from_str(&buf)?
};
if om.package.contains_key("default-run") {
om.package.remove("default-run");
let om_serialized = ::toml::to_string(&om).expect("Cannot format as toml file");
let mut cargo_toml = OpenOptions::new()
.read(true)
.write(true)
.truncate(true)
.open(&dest)?;
write!(cargo_toml, "{}", om_serialized)?;
}
// if build script is specified in the original Cargo.toml (from links or build) remove it as we do not need
// it for checking dependencies
if om.package.contains_key("links") {
om.package.remove("links");
let om_serialized = ::toml::to_string(&om).expect("Cannot format as toml file");
let mut cargo_toml = OpenOptions::new()
.read(true)
.write(true)
.truncate(true)
.open(&dest)?;
write!(cargo_toml, "{}", om_serialized)?;
}
if om.package.contains_key("build") {
om.package.remove("build");
let om_serialized = ::toml::to_string(&om).expect("Cannot format as toml file");
let mut cargo_toml = OpenOptions::new()
.read(true)
.write(true)
.truncate(true)
.open(&dest)?;
write!(cargo_toml, "{}", om_serialized)?;
}
let lockfile = from_dir.join("Cargo.lock");
if lockfile.is_file() {
dest.pop();
dest.push("Cargo.lock");
fs::copy(lockfile, dest)?;
}
}
// virtual root
let mut virtual_root = workspace_root.join("Cargo.toml");
if !manifest_paths.contains(&virtual_root) && virtual_root.is_file() {
fs::copy(&virtual_root, temp_dir.path().join("Cargo.toml"))?;
virtual_root.pop();
virtual_root.push("Cargo.lock");
if virtual_root.is_file() {
fs::copy(&virtual_root, temp_dir.path().join("Cargo.lock"))?;
}
}
//.cargo/config.toml
// this is the preferred way
// https://doc.rust-lang.org/cargo/reference/config.html
if workspace_root.join(".cargo/config.toml").is_file() {
fs::create_dir_all(temp_dir.path().join(".cargo"))?;
fs::copy(
&workspace_root.join(".cargo/config.toml"),
temp_dir.path().join(".cargo/config.toml"),
)?;
}
//.cargo/config
// this is legacy support for config files without the `.toml` extension
if workspace_root.join(".cargo/config").is_file() {
fs::create_dir_all(temp_dir.path().join(".cargo"))?;
fs::copy(
&workspace_root.join(".cargo/config"),
temp_dir.path().join(".cargo/config"),
)?;
}
let relative_manifest = String::from(&orig_manifest[workspace_root_str.len() + 1..]);
let config = Self::generate_config(temp_dir.path(), &relative_manifest, options)?;
Ok(TempProject {
workspace: Rc::new(RefCell::new(None)),
temp_dir,
manifest_paths: tmp_manifest_paths,
config,
relative_manifest,
options,
is_workspace_project: orig_workspace.workspace_mode,
})
}
fn generate_config(
root: &Path,
relative_manifest: &str,
options: &Options,
) -> CargoResult<Config> {
let shell = ::cargo::core::Shell::new();
let cwd = env::current_dir()
.chain_err(|| "Cargo couldn't get the current directory of the process")?;
let homedir = ::cargo::util::homedir(&cwd).ok_or_else(|| {
anyhow!(
"Cargo couldn't find your home directory. \
This probably means that $HOME was not set.",
)
})?;
let mut cwd = Path::new(root).join(relative_manifest);
cwd.pop();
// Check if $CARGO_HOME is set before capturing the config environment
// if it is, set it in the configure options
let cargo_home_path = std::env::var_os("CARGO_HOME").map(std::path::PathBuf::from);
let mut config = Config::new(shell, cwd, homedir);
config.configure(
0,
options.flag_verbose == 0,
options.flag_color.as_deref(),
options.frozen(),
options.locked(),
false,
&cargo_home_path,
&[],
&[],
)?;
Ok(config)
}
/// Run `cargo update` against the temporary project
pub fn cargo_update(&self) -> CargoResult<()> {
let update_opts = UpdateOptions {
aggressive: false,
precise: None,
to_update: Vec::new(),
config: &self.config,
dry_run: false,
workspace: self.is_workspace_project,
};
update_lockfile(self.workspace.borrow().as_ref().unwrap(), &update_opts)?;
Ok(())
}
fn write_manifest<P: AsRef<Path>>(manifest: &Manifest, path: P) -> CargoResult<()> {
let mut file = File::create(path)?;
let serialized = ::toml::to_string(manifest).expect("Failed to serialized Cargo.toml");
write!(file, "{}", serialized)?;
Ok(())
}
fn manipulate_dependencies<F>(manifest: &mut Manifest, f: &F) -> CargoResult<()>
where
F: Fn(&mut Table) -> CargoResult<()>,
{
if let Some(dep) = manifest.dependencies.as_mut() {
f(dep)?;
}
if let Some(dep) = manifest.dev_dependencies.as_mut() {
f(dep)?;
}
if let Some(dep) = manifest.build_dependencies.as_mut() {
f(dep)?;
}
if let Some(t) = manifest.target.as_mut() {
for (_key, target) in t.iter_mut() {
if let Value::Table(ref mut target) = *target {
for dependency_tables in
&["dependencies", "dev-dependencies", "build-dependencies"]
{
if let Some(&mut Value::Table(ref mut dep_table)) =
target.get_mut(*dependency_tables)
{
f(dep_table)?;
}
}
}
}
}
Ok(())
}
/// Write manifests with SemVer requirements
pub fn write_manifest_semver<P: AsRef<Path>>(
&'tmp self,
orig_root: P,
tmp_root: P,
workspace: &ElaborateWorkspace<'_>,
) -> CargoResult<()> {
let bin = {
let mut bin = Table::new();
bin.insert("name".to_owned(), Value::String("test".to_owned()));
bin.insert("path".to_owned(), Value::String("test.rs".to_owned()));
bin
};
for manifest_path in &self.manifest_paths {
let mut manifest: Manifest = {
let mut buf = String::new();
let mut file = File::open(manifest_path)?;
file.read_to_string(&mut buf)?;
::toml::from_str(&buf)?
};
manifest.bin = Some(vec![bin.clone()]);
// provide lib.path
if let Some(lib) = manifest.lib.as_mut() {
lib.insert("path".to_owned(), Value::String("test_lib.rs".to_owned()));
}
Self::manipulate_dependencies(&mut manifest, &|deps| {
Self::replace_path_with_absolute(
deps,
orig_root.as_ref(),
tmp_root.as_ref(),
manifest_path,
)
})?;
let package_name = manifest.name();
let features = manifest.features.clone();
Self::manipulate_dependencies(&mut manifest, &|deps| {
self.update_version_and_feature(deps, &features, workspace, &package_name, false)
})?;
Self::write_manifest(&manifest, manifest_path)?;
}
let root_manifest = self.temp_dir.path().join(&self.relative_manifest);
*self.workspace.borrow_mut() =
Some(Workspace::new(Path::new(&root_manifest), &self.config)?);
Ok(())
}
/// Write manifests with wildcard requirements
pub fn write_manifest_latest<P: AsRef<Path>>(
&'tmp self,
orig_root: P,
tmp_root: P,
workspace: &ElaborateWorkspace<'_>,
) -> CargoResult<()> {
let bin = {
let mut bin = Table::new();
bin.insert("name".to_owned(), Value::String("test".to_owned()));
bin.insert("path".to_owned(), Value::String("test.rs".to_owned()));
bin
};
for manifest_path in &self.manifest_paths {
let mut manifest: Manifest = {
let mut buf = String::new();
let mut file = File::open(manifest_path)?;
file.read_to_string(&mut buf)?;
::toml::from_str(&buf)?
};
manifest.bin = Some(vec![bin.clone()]);
// provide lib.path
if let Some(lib) = manifest.lib.as_mut() {
lib.insert("path".to_owned(), Value::String("test_lib.rs".to_owned()));
}
Self::manipulate_dependencies(&mut manifest, &|deps| {
Self::replace_path_with_absolute(
deps,
orig_root.as_ref(),
tmp_root.as_ref(),
manifest_path,
)
})?;
let package_name = manifest.name();
let features = manifest.features.clone();
Self::manipulate_dependencies(&mut manifest, &|deps| {
self.update_version_and_feature(deps, &features, workspace, &package_name, true)
})?;
Self::write_manifest(&manifest, manifest_path)?;
}
let root_manifest = self.temp_dir.path().join(&self.relative_manifest);
*self.workspace.borrow_mut() =
Some(Workspace::new(Path::new(&root_manifest), &self.config)?);
Ok(())
}
fn find_update(
&self,
name: &str,
dependent_package_name: &str,
requirement: Option<&str>,
workspace: &ElaborateWorkspace<'_>,
find_latest: bool,
) -> CargoResult<Summary> {
let package_id = workspace.find_direct_dependency(name, dependent_package_name)?;
let version = package_id.version();
let source_id = package_id.source_id().with_precise(None);
let mut source = source_id.load(&self.config, &HashSet::new())?;
if !source_id.is_default_registry() {
let _lock = self.config.acquire_package_cache_lock()?;
source.update()?;
}
let dependency = Dependency::parse_no_deprecated(name, None, source_id)?;
let query_result = {
let _lock = self.config.acquire_package_cache_lock()?;
let mut query_result = source.query_vec(&dependency)?;
query_result.sort_by(|a, b| b.version().cmp(a.version()));
query_result
};
let version_req = match requirement {
Some(requirement) => Some(VersionReq::parse(requirement)?),
None => None,
};
let latest_result = query_result
.iter()
.find(|summary| {
if summary.version() < version {
false
} else if version_req.is_none() {
true
} else if find_latest {
self.options.flag_aggressive
|| valid_latest_version(requirement.unwrap(), summary.version())
} else {
version_req.as_ref().unwrap().matches(summary.version())
}
})
.unwrap_or_else(|| {
// If the version_req cannot be found use the version
// this happens when we use a git repository as a dependency, without specifying
// the version in Cargo.toml, preventing us from needing an unwrap below in the warn
let ver_req = match version_req {
Some(v_r) => format!("{}", v_r),
None => format!("{}", version),
};
self.warn(format!(
"cannot compare {} crate version found in toml {} with crates.io latest {}",
name,
ver_req,
query_result[0].version()
))
.unwrap();
//this returns the latest version
&query_result[0]
});
Ok(latest_result.clone())
}
fn feature_includes(&self, name: &str, optional: bool, features_table: &Option<Value>) -> bool {
if self.options.all_features() {
return true;
}
if !optional
&& self
.options
.flag_features
.contains(&String::from("default"))
{
return true;
}
let features_table = match *features_table {
Some(Value::Table(ref features_table)) => features_table,
_ => return false,
};
let mut to_resolve: Vec<&str> = self
.options
.flag_features
.iter()
.filter(|f| !f.is_empty())
.map(String::as_str)
.collect();
let mut visited: HashSet<&str> = HashSet::new();
while let Some(feature) = to_resolve.pop() {
if feature == name {
return true;
}
if visited.contains(feature) {
continue;
}
visited.insert(feature);
if features_table.contains_key(feature) {
let specified_features = match features_table.get(feature) {
None => panic!("Feature {} does not exist", feature),
Some(&Value::Array(ref specified_features)) => specified_features,
_ => panic!("Feature {} is not mapped to an array", feature),
};
for spec in specified_features {
if let Value::String(ref spec) = *spec {
to_resolve.push(spec.as_str());
}
}
}
}
false
}
fn update_version_and_feature(
&self,
dependencies: &mut Table,
features: &Option<Value>,
workspace: &ElaborateWorkspace<'_>,
package_name: &str,
version_to_latest: bool,
) -> CargoResult<()> {
let dep_keys: Vec<_> = dependencies.keys().cloned().collect();
for dep_key in dep_keys {
let original = dependencies.get(&dep_key).cloned().unwrap();
match original {
Value::String(requirement) => {
let name = dep_key;
if version_to_latest {
match self.find_update(
&name,
package_name,
Some(requirement.as_str()),
workspace,
version_to_latest,
) {
Result::Ok(val) => dependencies
.insert(name.clone(), Value::String(val.version().to_string())),
Result::Err(_err) => {
eprintln!(
"Updates to dependency {} could not be found",
name.clone()
);
None
}
};
}
}
Value::Table(ref t) => {
let mut name = match t.get("package") {
Some(&Value::String(ref s)) => s,
Some(_) => panic!("'package' of dependency {} is not a string", dep_key),
None => &dep_key,
};
let mut orig_name = "";
if t.contains_key("package") {
orig_name = name;
name = &dep_key;
}
if !(version_to_latest || t.contains_key("features")) {
continue;
}
let optional = t
.get("optional")
.map(|optional| {
if let Value::Boolean(optional) = *optional {
optional
} else {
false
}
})
.unwrap_or(false);
if !self.feature_includes(&name, optional, features) {
continue;
}
let mut replaced = t.clone();
let requirement = match t.get("version") {
Some(&Value::String(ref requirement)) => Some(requirement.as_str()),
Some(_) => panic!("Version of {} is not a string", name),
_ => None,
};
let r_summary = self.find_update(
if orig_name.is_empty() {
&name
} else {
&orig_name
},
package_name,
requirement,
workspace,
version_to_latest,
);
let summary = match r_summary {
Result::Ok(val) => val,
Result::Err(_) => {
eprintln!("Update for {} could not be found!", name.clone());
return Ok(());
}
};
if version_to_latest && t.contains_key("version") {
replaced.insert(
"version".to_owned(),
Value::String(summary.version().to_string()),
);
}
if replaced.contains_key("features") {
let features = match replaced.get("features") {
Some(&Value::Array(ref features)) => features
.iter()
.filter(|&feature| {
let feature = match *feature {
Value::String(ref feature) => feature,
_ => panic!(
"Features section of {} is not an array of strings",
name
),
};
let retained =
features_and_options(&summary).contains(feature.as_str());
if !retained {
self.warn(format!(
"Feature {} of package {} \
has been obsolete in version {}",
feature,
name,
summary.version()
))
.unwrap();
}
retained
})
.cloned()
.collect::<Vec<Value>>(),
_ => panic!("Features section of {} is not an array", name),
};
replaced.insert("features".to_owned(), Value::Array(features));
}
dependencies.insert(name.clone(), Value::Table(replaced));
}
_ => panic!(
"Dependency spec is neither a string nor a table {}",
dep_key
),
}
}
Ok(())
}
fn replace_path_with_absolute(
dependencies: &mut Table,
orig_root: &Path,
tmp_root: &Path,
tmp_manifest: &Path,
) -> CargoResult<()> {
let dep_names: Vec<_> = dependencies.keys().cloned().collect();
for name in dep_names {
let original = dependencies.get(&name).cloned().unwrap();
match original {
Value::Table(ref t) if t.contains_key("path") => {
if let Value::String(ref orig_path) = t["path"] {
let orig_path = Path::new(orig_path);
if orig_path.is_relative() {
let relative = {
let delimiter: &[_] = &['/', '\\'];
let relative = &tmp_manifest.to_string_lossy()
[tmp_root.to_string_lossy().len()..];
let mut relative =
PathBuf::from(relative.trim_start_matches(delimiter));
relative.pop();
relative.join(orig_path)
};
if !tmp_root.join(&relative).join("Cargo.toml").exists() {
let mut replaced = t.clone();
replaced.insert(
"path".to_owned(),
Value::String(
fs::canonicalize(orig_root.join(relative))?
.to_string_lossy()
.to_string(),
),
);
dependencies.insert(name, Value::Table(replaced));
}
}
}
}
_ => {}
}
}
Ok(())
}
fn warn<T: ::std::fmt::Display>(&self, message: T) -> CargoResult<()> {
let original_verbosity = self.config.shell().verbosity();
self.config
.shell()
.set_verbosity(if self.options.flag_quiet {
Verbosity::Quiet
} else {
Verbosity::Normal
});
self.config.shell().warn(message)?;
self.config.shell().set_verbosity(original_verbosity);
Ok(())
}
}
/// Features and optional dependencies of a Summary
fn features_and_options(summary: &Summary) -> HashSet<&str> {
let mut result: HashSet<&str> = summary.features().keys().map(|s| s.as_str()).collect();
summary
.dependencies()
.iter()
.filter(|d| d.is_optional())
.map(Dependency::package_name)
.for_each(|d| {
result.insert(d.as_str());
});
result
}
/// Paths of all manifest files in current workspace
fn manifest_paths(elab: &ElaborateWorkspace<'_>) -> CargoResult<Vec<PathBuf>> {
let mut visited: HashSet<PackageId> = HashSet::new();
let mut manifest_paths = vec![];
fn manifest_paths_recursive(
pkg_id: PackageId,
elab: &ElaborateWorkspace<'_>,
workspace_path: &str,
visited: &mut HashSet<PackageId>,
manifest_paths: &mut Vec<PathBuf>,
) -> CargoResult<()> {
if visited.contains(&pkg_id) {
return Ok(());
}
visited.insert(pkg_id);
let pkg = &elab.pkgs[&pkg_id];
let pkg_path = pkg.root().to_string_lossy();
// Checking if there's a CARGO_HOME set and that it is not an empty string
let cargo_home_path = match std::env::var_os("CARGO_HOME") {
Some(path) if !path.is_empty() => Some(
path.into_string()
.expect("Error getting string from OsString"),
),
_ => None,
};
// If there is a CARGO_HOME make sure we do not crawl the registry for more Cargo.toml files
// Otherwise add all Cargo.toml files to the manifest paths
if pkg.root().starts_with(PathBuf::from(workspace_path))
&& (cargo_home_path.is_none()
|| !pkg_path
.starts_with(&cargo_home_path.expect("Error extracting CARGO_HOME string")))
{
manifest_paths.push(pkg.manifest_path().to_owned());
}
for &dep in elab.pkg_deps[&pkg_id].keys() {
manifest_paths_recursive(dep, elab, workspace_path, visited, manifest_paths)?;
}
Ok(())
}
// executed against a virtual manifest
let workspace_path = elab.workspace.root().to_string_lossy();
// if cargo workspace is not explicitly used, the package itself would be a member
for member in elab.workspace.members() {
let root_pkg_id = member.package_id();
manifest_paths_recursive(
root_pkg_id,
elab,
&workspace_path,
&mut visited,
&mut manifest_paths,
)?;
}
Ok(manifest_paths)
}
fn valid_latest_version(mut requirement: &str, version: &Version) -> bool {
match (requirement.contains('-'), version.is_prerelease()) {
// if user was on a stable channel, it's unlikely for him to update to an unstable one
(false, true) => false,
// both are stable, leave for further filters
// ...or...
// user was on an unstable one, newer stable ones are still candidates
(false, false) | (true, false) => true,
// both are unstable, must be in the same channel
(true, true) => {
requirement = requirement.trim_start_matches(&['=', ' ', '~', '^'][..]);
let requirement_version = Version::parse(&requirement)
.expect("Error could not parse requirement into a semantic version");
let requirement_channel = requirement_version.pre[0].to_string();
match (requirement_channel.is_empty(), &version.pre[0]) {
(true, &Identifier::Numeric(_)) => true,
(false, &Identifier::AlphaNumeric(_)) => {
Identifier::AlphaNumeric(requirement_channel) == version.pre[0]
}
_ => false,
}
}
}
}