#[macro_use]
extern crate log;
extern crate slog;
extern crate slog_async;
extern crate slog_term;
extern crate cargo;
pub use self::error::{Error, RegistryError, Result};
pub use self::registry::{CrateVersion, Versions};
use std::sync::Arc;
use cargo::{
core::{
compiler::{BuildConfig, CompileMode, DefaultExecutor, Executor},
package::Package,
SourceId, Verbosity, Workspace,
},
ops::{self, PackageOpts, PublishOpts},
sources::PathSource,
util::{paths, FileLock},
Config,
};
use flate2::read::GzDecoder;
use hashbrown::HashMap;
use petgraph::Graph;
use tar::Archive;
mod error;
pub mod logger;
mod registry;
pub fn compute_package_order(ws: &Workspace<'_>) -> Result<Vec<Package>> {
ws.config().shell().status("Resolving", "workspace")?;
let mut graph = Graph::<Package, (), _, _>::new();
let mut map = HashMap::new();
for member in ws.members() {
let name = member.name();
let index = graph.add_node(member.clone());
assert!(map.insert(name, index).is_none());
}
for member in ws.members() {
let current_index = map.get(&member.name()).unwrap();
for dep in member.dependencies() {
if !dep.is_transitive() {
continue;
}
let dep_index = match map.get(&dep.package_name()) {
Some(index) => index,
None => continue,
};
graph.add_edge(*current_index, *dep_index, ());
}
}
let indices = petgraph::algo::toposort(&graph, None)
.map_err(|c| Error::Cycle(graph.node_weight(c.node_id()).unwrap().name().to_string()))?;
let packages = indices
.into_iter()
.rev()
.map(|i| graph.node_weight(i).unwrap().clone())
.collect();
Ok(packages)
}
pub fn fetch_crates_io_versions(ws: &Workspace, packages: &[Package]) -> Result<Vec<Versions>> {
ws.config()
.shell()
.status("Fetching", "crates.io versions")?;
let v: Vec<_> = packages
.into_iter()
.map(|p| registry::fetch_cratesio(p.name().as_str()))
.collect::<std::result::Result<_, _>>()?;
Ok(v)
}
pub enum Mode {
Publish,
Skip,
}
impl Mode {
pub fn is_skip(&self) -> bool {
match *self {
Mode::Publish => false,
Mode::Skip => true,
}
}
}
pub fn package_modes(packages: &[Package], versions: &[Versions]) -> Result<Vec<Mode>> {
use failure::format_err;
let mut modes = vec![];
for (i, pkg) in packages.into_iter().enumerate() {
let version = pkg.version();
let available: &[CrateVersion] = &versions[i].versions;
let mut mode = Mode::Publish;
for available in available {
assert_eq!(available.name, pkg.name().as_str());
if available.version == *version {
if available.yanked {
return Err(cargo::CargoError::from(format_err!(
"Version {} of package `{}` is \
yanked; cannot use for publish",
pkg.name(),
version
))
.into());
} else {
info!("Skipping crate `{}`, version already published", pkg.name());
mode = Mode::Skip;
}
}
}
modes.push(mode);
}
Ok(modes)
}
pub fn create_replace_entry(pkg: &Package, mode: &Mode) -> Option<(String, String)> {
match *mode {
Mode::Publish => Some((
pkg.name().as_str().to_owned(),
pkg.manifest_path()
.parent()
.unwrap()
.to_str()
.unwrap()
.to_owned(),
)),
Mode::Skip => None,
}
}
pub fn verify_all(
ws: &Workspace<'_>,
packages: &[Package],
modes: &[Mode],
allow_dirty: bool,
) -> Result<()> {
ws.config().shell().status("Verifying", "workspace")?;
let target_dir = ws.target_dir();
let mut replaces = vec![];
for (i, pkg) in packages.into_iter().enumerate() {
replaces.extend(create_replace_entry(pkg, &modes[i]));
}
for pkg in packages {
let tmp_ws =
Workspace::ephemeral(pkg.clone(), ws.config(), Some(target_dir.clone()), false)?;
ws.config().shell().status("Verifying", pkg.name())?;
quiet(ws.config(), || {
verify_single(&tmp_ws, &replaces, allow_dirty)
})?;
}
Ok(())
}
pub fn verify_single(
eph_ws: &Workspace<'_>,
replace: &[(String, String)],
allow_dirty: bool,
) -> Result<()> {
let opts = PackageOpts {
config: eph_ws.config(),
list: false,
check_metadata: false,
allow_dirty,
verify: false,
jobs: None,
target: None,
registry: None,
};
let lock = ops::package(eph_ws, &opts)?.unwrap();
run_verify(eph_ws, &lock, &opts, replace)?;
Ok(())
}
fn quiet<F: FnOnce() -> R, R>(config: &Config, f: F) -> R {
let backup = config.shell().verbosity();
if backup != Verbosity::Verbose {
config.shell().set_verbosity(Verbosity::Quiet);
}
let r = f();
config.shell().set_verbosity(backup);
r
}
fn inject_replacement(pkg: &Package, replace: &[(String, String)]) -> Result<()> {
use std::fs::{read_to_string, write};
let manifest = pkg.manifest_path();
let document = read_to_string(manifest).unwrap();
let mut document = document.parse::<toml_edit::Document>().unwrap();
for (name, path) in replace {
if document["dependencies"][name.as_str()]
.as_table_mut()
.map(|t| t.remove("version").is_some())
.unwrap_or(false)
{
document["dependencies"][name.as_str()]["path"] = toml_edit::value(path.as_ref());
}
}
write(manifest, document.to_string().as_bytes()).unwrap();
Ok(())
}
fn run_verify(
ws: &Workspace,
tar: &FileLock,
opts: &PackageOpts,
replace: &[(String, String)],
) -> cargo::CargoResult<()> {
use failure::bail;
let config = ws.config();
let pkg = ws.current()?;
let f = GzDecoder::new(tar.file());
let dst = tar
.parent()
.join(&format!("{}-{}", pkg.name(), pkg.version()));
if dst.exists() {
paths::remove_dir_all(&dst)?;
}
let mut archive = Archive::new(f);
archive.unpack(dst.parent().unwrap())?;
let (src, new_pkg) = {
let id = SourceId::for_path(&dst)?;
let mut src = PathSource::new(&dst, &id, ws.config());
let new_pkg = src.root_package()?;
inject_replacement(&new_pkg, replace)?;
let mut src = PathSource::new(&dst, &id, ws.config());
let new_pkg = src.root_package()?;
(src, new_pkg)
};
let pkg_fingerprint = src.last_modified_file(&new_pkg)?;
let ws = Workspace::ephemeral(new_pkg, config, None, true)?;
let exec: Arc<Executor> = Arc::new(DefaultExecutor);
ops::compile_ws(
&ws,
None,
&ops::CompileOptions {
config,
build_config: BuildConfig::new(config, opts.jobs, &opts.target, CompileMode::Build)?,
features: Vec::new(),
no_default_features: false,
all_features: false,
spec: ops::Packages::Packages(Vec::new()),
filter: ops::CompileFilter::Default {
required_features_filterable: true,
},
target_rustdoc_args: None,
target_rustc_args: None,
local_rustdoc_args: None,
export_dir: None,
},
&exec,
)?;
let ws_fingerprint = src.last_modified_file(ws.current()?)?;
if pkg_fingerprint != ws_fingerprint {
let (_, path) = ws_fingerprint;
bail!(
"Source directory was modified by build.rs during cargo publish. \
Build scripts should not modify anything outside of OUT_DIR. \
Modified file: {}\n\n\
To proceed despite this, pass the `--no-verify` flag.",
path.display()
)
}
Ok(())
}
pub fn publish_all(
ws: &Workspace<'_>,
packages: &[Package],
modes: &[Mode],
opts: &PublishOpts,
) -> Result<()> {
ws.config().shell().status("Publishing", "workspace")?;
let target_dir = ws.target_dir();
for (i, pkg) in packages.into_iter().enumerate() {
let config = ws.config();
if modes[i].is_skip() {
config.shell().status("Skipping", pkg.name())?;
continue;
}
let tmp_ws =
Workspace::ephemeral(pkg.clone(), ws.config(), Some(target_dir.clone()), false)?;
config.shell().status("Publishing", pkg.name())?;
quiet(config, || ops::publish(&tmp_ws, opts))?;
}
Ok(())
}