blob: 90dbd5cc729d0b6e2d59027bc8791eee4110c21d [file] [log] [blame]
use diffy;
use std::env;
use std::fmt::{Debug, Display};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::str::Utf8Error;
use tracing::info;
use walkdir::WalkDir;
#[derive(Debug)]
pub enum CheckDiffError {
/// Git related errors
FailedGit(GitError),
/// Error for generic commands
FailedCommand(&'static str),
/// UTF8 related errors
FailedUtf8(Utf8Error),
/// Error for building rustfmt from source
FailedSourceBuild(&'static str),
/// Error when obtaining binary version
FailedBinaryVersioning(PathBuf),
/// Error when obtaining cargo version
FailedCargoVersion(&'static str),
IO(std::io::Error),
}
impl From<io::Error> for CheckDiffError {
fn from(error: io::Error) -> Self {
CheckDiffError::IO(error)
}
}
impl From<GitError> for CheckDiffError {
fn from(error: GitError) -> Self {
CheckDiffError::FailedGit(error)
}
}
impl From<Utf8Error> for CheckDiffError {
fn from(error: Utf8Error) -> Self {
CheckDiffError::FailedUtf8(error)
}
}
#[derive(Debug)]
pub enum GitError {
FailedClone { stdout: Vec<u8>, stderr: Vec<u8> },
FailedRemoteAdd { stdout: Vec<u8>, stderr: Vec<u8> },
FailedFetch { stdout: Vec<u8>, stderr: Vec<u8> },
FailedSwitch { stdout: Vec<u8>, stderr: Vec<u8> },
IO(std::io::Error),
}
impl From<io::Error> for GitError {
fn from(error: io::Error) -> Self {
GitError::IO(error)
}
}
pub struct Diff {
src_format: String,
feature_format: String,
}
impl Display for Diff {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let patch = diffy::create_patch(self.src_format.as_str(), self.feature_format.as_str());
write!(f, "{}", patch)
}
}
impl Diff {
pub fn is_empty(&self) -> bool {
let patch = diffy::create_patch(self.src_format.as_str(), self.feature_format.as_str());
patch.hunks().is_empty()
}
}
pub struct CheckDiffRunners<F, S> {
feature_runner: F,
src_runner: S,
}
pub trait CodeFormatter {
fn format_code<'a>(
&self,
code: &'a str,
config: &Option<Vec<String>>,
) -> Result<String, CheckDiffError>;
}
pub struct RustfmtRunner {
ld_library_path: String,
binary_path: PathBuf,
}
impl<F, S> CheckDiffRunners<F, S> {
pub fn new(feature_runner: F, src_runner: S) -> Self {
Self {
feature_runner,
src_runner,
}
}
}
impl<F, S> CheckDiffRunners<F, S>
where
F: CodeFormatter,
S: CodeFormatter,
{
/// Creates a diff generated by running the source and feature binaries on the same file path
pub fn create_diff(
&self,
path: &Path,
additional_configs: &Option<Vec<String>>,
) -> Result<Diff, CheckDiffError> {
let code = std::fs::read_to_string(path)?;
let src_format = self.src_runner.format_code(&code, additional_configs)?;
let feature_format = self.feature_runner.format_code(&code, additional_configs)?;
Ok(Diff {
src_format,
feature_format,
})
}
}
impl RustfmtRunner {
fn get_binary_version(&self) -> Result<String, CheckDiffError> {
let Ok(command) = Command::new(&self.binary_path)
.env("LD_LIBRARY_PATH", &self.ld_library_path)
.args(["--version"])
.output()
else {
return Err(CheckDiffError::FailedBinaryVersioning(
self.binary_path.clone(),
));
};
let binary_version = std::str::from_utf8(&command.stdout)?.trim();
return Ok(binary_version.to_string());
}
}
impl CodeFormatter for RustfmtRunner {
// Run rusfmt to see if a diff is produced. Runs on the code specified
//
// Parameters:
// code: Code to run the binary on
// config: Any additional configuration options to pass to rustfmt
//
fn format_code<'a>(
&self,
code: &'a str,
config: &Option<Vec<String>>,
) -> Result<String, CheckDiffError> {
let config = create_config_arg(config);
let mut command = Command::new(&self.binary_path)
.env("LD_LIBRARY_PATH", &self.ld_library_path)
.args([
"--unstable-features",
"--skip-children",
"--emit=stdout",
config.as_str(),
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
command.stdin.as_mut().unwrap().write_all(code.as_bytes())?;
let output = command.wait_with_output()?;
Ok(std::str::from_utf8(&output.stdout)?.to_string())
}
}
/// Creates a configuration in the following form:
/// <config_name>=<config_val>, <config_name>=<config_val>, ...
fn create_config_arg(config: &Option<Vec<String>>) -> String {
let config_arg: String = match config {
Some(configs) => {
let mut result = String::new();
for arg in configs.iter() {
result.push(',');
result.push_str(arg.as_str());
}
result
}
None => String::new(),
};
let config = format!(
"--config=error_on_line_overflow=false,error_on_unformatted=false{}",
config_arg.as_str()
);
config
}
/// Clone a git repository
///
/// Parameters:
/// url: git clone url
/// dest: directory where the repo should be cloned
pub fn clone_git_repo(url: &str, dest: &Path) -> Result<(), GitError> {
let git_cmd = Command::new("git")
.env("GIT_TERMINAL_PROMPT", "0")
.args([
"clone",
"--quiet",
url,
"--depth",
"1",
dest.to_str().unwrap(),
])
.output()?;
// if the git command does not return successfully,
// any command on the repo will fail. So fail fast.
if !git_cmd.status.success() {
let error = GitError::FailedClone {
stdout: git_cmd.stdout,
stderr: git_cmd.stderr,
};
return Err(error);
}
info!("Successfully clone repository.");
return Ok(());
}
pub fn git_remote_add(url: &str) -> Result<(), GitError> {
let git_cmd = Command::new("git")
.args(["remote", "add", "feature", url])
.output()?;
// if the git command does not return successfully,
// any command on the repo will fail. So fail fast.
if !git_cmd.status.success() {
let error = GitError::FailedRemoteAdd {
stdout: git_cmd.stdout,
stderr: git_cmd.stderr,
};
return Err(error);
}
info!("Successfully added remote: {url}");
return Ok(());
}
pub fn git_fetch(branch_name: &str) -> Result<(), GitError> {
let git_cmd = Command::new("git")
.args(["fetch", "feature", branch_name])
.output()?;
// if the git command does not return successfully,
// any command on the repo will fail. So fail fast.
if !git_cmd.status.success() {
let error = GitError::FailedFetch {
stdout: git_cmd.stdout,
stderr: git_cmd.stderr,
};
return Err(error);
}
info!("Successfully fetched: {branch_name}");
return Ok(());
}
pub fn git_switch(git_ref: &str, should_detach: bool) -> Result<(), GitError> {
let detach_arg = if should_detach { "--detach" } else { "" };
let args = ["switch", git_ref, detach_arg];
let output = Command::new("git")
.args(args.iter().filter(|arg| !arg.is_empty()))
.output()?;
if !output.status.success() {
tracing::error!("Git switch failed: {output:?}");
let error = GitError::FailedSwitch {
stdout: output.stdout,
stderr: output.stderr,
};
return Err(error);
}
info!("Successfully switched to {git_ref}");
return Ok(());
}
pub fn change_directory_to_path(dest: &Path) -> io::Result<()> {
let dest_path = Path::new(&dest);
env::set_current_dir(&dest_path)?;
info!(
"Current directory: {}",
env::current_dir().unwrap().display()
);
return Ok(());
}
pub fn get_ld_library_path(dir: &Path) -> Result<String, CheckDiffError> {
let Ok(command) = Command::new("rustc")
.current_dir(dir)
.args(["--print", "sysroot"])
.output()
else {
return Err(CheckDiffError::FailedCommand("Error getting sysroot"));
};
let sysroot = std::str::from_utf8(&command.stdout)?.trim_end();
let ld_lib_path = format!("{}/lib", sysroot);
return Ok(ld_lib_path);
}
pub fn get_cargo_version() -> Result<String, CheckDiffError> {
let Ok(command) = Command::new("cargo").args(["--version"]).output() else {
return Err(CheckDiffError::FailedCargoVersion(
"Failed to obtain cargo version",
));
};
let cargo_version = std::str::from_utf8(&command.stdout)?.trim_end();
return Ok(cargo_version.to_string());
}
/// Obtains the ld_lib path and then builds rustfmt from source
/// If that operation succeeds, the source is then copied to the output path specified
pub fn build_rustfmt_from_src(
binary_path: PathBuf,
dir: &Path,
) -> Result<RustfmtRunner, CheckDiffError> {
//Because we're building standalone binaries we need to set `LD_LIBRARY_PATH` so each
// binary can find it's runtime dependencies.
// See https://github.com/rust-lang/rustfmt/issues/5675
// This will prepend the `LD_LIBRARY_PATH` for the master rustfmt binary
let ld_lib_path = get_ld_library_path(&dir)?;
info!("Building rustfmt from source");
let Ok(_) = Command::new("cargo")
.current_dir(dir)
.args(["build", "-q", "--release", "--bin", "rustfmt"])
.output()
else {
return Err(CheckDiffError::FailedSourceBuild(
"Error building rustfmt from source",
));
};
std::fs::copy(dir.join("target/release/rustfmt"), &binary_path)?;
return Ok(RustfmtRunner {
ld_library_path: ld_lib_path,
binary_path,
});
}
// Compiles and produces two rustfmt binaries.
// One for the current master, and another for the feature branch
// Parameters:
// dest: Directory where rustfmt will be cloned
pub fn compile_rustfmt(
dest: &Path,
remote_repo_url: String,
feature_branch: String,
commit_hash: Option<String>,
) -> Result<CheckDiffRunners<RustfmtRunner, RustfmtRunner>, CheckDiffError> {
const RUSTFMT_REPO: &str = "https://github.com/rust-lang/rustfmt.git";
clone_git_repo(RUSTFMT_REPO, dest)?;
change_directory_to_path(dest)?;
git_remote_add(remote_repo_url.as_str())?;
git_fetch(feature_branch.as_str())?;
let cargo_version = get_cargo_version()?;
info!("Compiling with {}", cargo_version);
let src_runner = build_rustfmt_from_src(dest.join("src_rustfmt"), dest)?;
let should_detach = commit_hash.is_some();
git_switch(
commit_hash.unwrap_or(feature_branch).as_str(),
should_detach,
)?;
let feature_runner = build_rustfmt_from_src(dest.join("feature_rustfmt"), dest)?;
info!("RUSFMT_BIN {}", src_runner.get_binary_version()?);
info!(
"Runtime dependencies for (src) rustfmt -- LD_LIBRARY_PATH: {}",
src_runner.ld_library_path
);
info!("FEATURE_BIN {}", feature_runner.get_binary_version()?);
info!(
"Runtime dependencies for (feature) rustfmt -- LD_LIBRARY_PATH: {}",
feature_runner.ld_library_path
);
return Ok(CheckDiffRunners {
src_runner,
feature_runner,
});
}
/// Searches for rust files in the particular path and returns an iterator to them.
pub fn search_for_rs_files(repo: &Path) -> impl Iterator<Item = PathBuf> {
return WalkDir::new(repo).into_iter().filter_map(|e| match e.ok() {
Some(entry) => {
let path = entry.path();
if path.is_file() && path.extension().map_or(false, |ext| ext == "rs") {
return Some(entry.into_path());
}
return None;
}
None => None,
});
}
/// Calculates the number of errors when running the compiled binary and the feature binary on the
/// repo specified with the specific configs.
pub fn check_diff(
config: Option<Vec<String>>,
runners: CheckDiffRunners<impl CodeFormatter, impl CodeFormatter>,
repo: &Path,
) -> i32 {
let mut errors = 0;
let iter = search_for_rs_files(repo);
for file in iter {
match runners.create_diff(file.as_path(), &config) {
Ok(diff) => {
if !diff.is_empty() {
eprint!("{diff}");
errors += 1;
}
}
Err(e) => {
eprintln!(
"Error creating diff for {:?}: {:?}",
file.as_path().display(),
e
);
errors += 1;
}
}
}
return errors;
}