| 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; |
| } |