Skip to content

Commit

Permalink
feat: dprint upgrade (#538)
Browse files Browse the repository at this point in the history
  • Loading branch information
dsherret committed Jun 29, 2022
1 parent 83cf458 commit 4391b8b
Show file tree
Hide file tree
Showing 14 changed files with 290 additions and 16 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/website.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- uses: denoland/setup-deno@v1

- name: Install playground dependencies
run: (cd website/playground && npm ci)
run: (cd website/playground && npm ci --legacy-peer-deps)

- name: Build playground
run: npm run build --prefix website/playground
Expand Down
6 changes: 6 additions & 0 deletions crates/dprint/src/arg_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub enum SubCommand {
EditorInfo,
EditorService(EditorServiceSubCommand),
StdInFmt(StdInFmtSubCommand),
Upgrade,
#[cfg(target_os = "windows")]
Hidden(HiddenSubCommand),
}
Expand Down Expand Up @@ -184,6 +185,7 @@ pub fn parse_args<TStdInReader: StdInReader>(args: Vec<String>, std_in_reader: T
("editor-service", matches) => SubCommand::EditorService(EditorServiceSubCommand {
parent_pid: matches.value_of("parent-pid").and_then(|v| v.parse::<u32>().ok()).unwrap(),
}),
("upgrade", _) => SubCommand::Upgrade,
#[cfg(target_os = "windows")]
("hidden", matches) => SubCommand::Hidden(match matches.subcommand().unwrap() {
("windows-install", matches) => HiddenSubCommand::WindowsInstall(matches.value_of("install-path").map(String::from).unwrap()),
Expand Down Expand Up @@ -407,6 +409,10 @@ EXAMPLES:
Command::new("clear-cache")
.about("Deletes the plugin cache directory.")
)
.subcommand(
Command::new("upgrade")
.about("Upgrades the dprint executable.")
)
.subcommand(
Command::new("license")
.about("Outputs the software license.")
Expand Down
4 changes: 2 additions & 2 deletions crates/dprint/src/commands/general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub async fn output_help<TEnvironment: Environment>(

if let Some(latest_version) = is_out_of_date(environment) {
environment.log(&format!(
"\nLatest version: {} (Current is {})\nDownload the latest version at https://dprint.dev/install/",
"\nLatest version: {} (Current is {})\nDownload the latest version by running: dprint upgrade",
latest_version,
environment.cli_version(),
));
Expand Down Expand Up @@ -191,7 +191,7 @@ mod test {
get_expected_help_text(),
concat!(
"\nLatest version: 0.1.0 (Current is 0.0.0)",
"\nDownload the latest version at https://dprint.dev/install/",
"\nDownload the latest version by running: dprint upgrade",
)
]
);
Expand Down
2 changes: 2 additions & 0 deletions crates/dprint/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ mod config;
mod editor;
mod formatting;
mod general;
mod upgrade;
#[cfg(target_os = "windows")]
mod windows_install;

pub use config::*;
pub use editor::*;
pub use formatting::*;
pub use general::*;
pub use upgrade::*;
#[cfg(target_os = "windows")]
pub use windows_install::*;
170 changes: 170 additions & 0 deletions crates/dprint/src/commands/upgrade.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use std::path::Path;
use std::process::Command;
use std::process::Stdio;

use anyhow::bail;
use anyhow::Context;
use anyhow::Result;

use crate::environment::Environment;
use crate::environment::FilePermissions;
use crate::utils::extract_zip;
use crate::utils::latest_cli_version;

pub async fn upgrade<TEnvironment: Environment>(environment: &TEnvironment) -> Result<()> {
let latest_version = latest_cli_version(environment).context("Error fetching latest CLI verison.")?;
let current_version = environment.cli_version();
if current_version == latest_version {
environment.log(&format!("Already on latest version {}", latest_version));
return Ok(());
}

environment.log(&format!("Upgrading from {} to {}...", current_version, latest_version));

let exe_path = environment.current_exe()?;
for component in exe_path.components() {
if component.as_os_str().to_string_lossy().to_lowercase() == "node_modules" {
bail!("Cannot upgrade with `dprint upgrade` when the dprint executable is within a node_modules folder. Upgrade with npm instead.");
}
}
if exe_path.starts_with("/usr/local/Cellar/") {
bail!("Cannot upgrade with `dprint upgrade` when the dprint executable is installed via Homebrew. Run `brew upgrade dprint` instead.");
}

let permissions = environment.file_permissions(&exe_path)?;

if permissions.readonly() {
bail!("You do not have write permission to {}", exe_path.display());
}

let arch = environment.cpu_arch();
let os = environment.os();
let zip_suffix = match os.as_str() {
"linux" => "unknown-linux-gnu",
"macos" => "apple-darwin",
"windows" => "pc-windows-msvc",
_ => bail!("Not implemented operating system: {}", os),
};
let zip_filename = format!("dprint-{}-{}.zip", arch, zip_suffix);
let zip_url = format!("https://github.com/dprint/dprint/releases/download/{}/{}", latest_version, zip_filename);

let zip_bytes = environment.download_file_err_404(&zip_url)?;
let old_executable = exe_path.with_extension("old.exe");

if !environment.is_real() {
// kind of hard to test this with a test environment
panic!("Need real environment.");
}

if cfg!(windows) {
// on windows, we need to rename the current running executable
// to something else in order to be able to replace it.
environment.rename(&exe_path, &old_executable)?;
}

if let Err(err) = try_upgrade(&exe_path, &zip_bytes, permissions, environment) {
if cfg!(windows) {
// try to rename it back
environment.rename(&old_executable, &exe_path).with_context(|| {
format!(
"Upgrade error: {:#}\nError upgrading and then error restoring. You may need to reinstall dprint from scratch. Sorry!",
err
)
})?;
}
return Err(err);
}

// it would be nice if we could delete the old executable here on Windows,
// but we need it in order to keep running the current executable

Ok(())
}

fn try_upgrade(exe_path: &Path, zip_bytes: &[u8], permissions: FilePermissions, environment: &impl Environment) -> Result<()> {
extract_zip("Extracting zip...", zip_bytes, exe_path.parent().unwrap(), environment)?;
environment.set_file_permissions(exe_path, permissions)?;
validate_executable(&exe_path).context("Error validating new executable.")?;
Ok(())
}

fn validate_executable(path: &Path) -> Result<()> {
let status = Command::new(path).stderr(Stdio::null()).stdout(Stdio::null()).arg("-v").status()?;
if !status.success() {
bail!("Status was not success.");
}
Ok(())
}

#[cfg(test)]
mod test {
use crate::environment::Environment;
use crate::environment::FilePermissions;
use crate::environment::TestEnvironment;
use crate::environment::TestFilePermissions;
use crate::test_helpers::run_test_cli;

#[test]
fn should_not_upgrade_same_version() {
let environment = TestEnvironment::new();
environment.add_remote_file("https://plugins.dprint.dev/cli.json", r#"{ "version": "0.0.0" }"#.as_bytes());
run_test_cli(vec!["upgrade"], &environment).unwrap();
assert_eq!(environment.take_stdout_messages(), vec!["Already on latest version 0.0.0"]);
}

#[test]
fn should_upgrade_and_fail_readonly() {
let environment = TestEnvironment::new();
environment
.set_file_permissions(
environment.current_exe().unwrap(),
FilePermissions::Test(TestFilePermissions { readonly: true }),
)
.unwrap();
environment.add_remote_file("https://plugins.dprint.dev/cli.json", r#"{ "version": "0.1.0" }"#.as_bytes());
let err = run_test_cli(vec!["upgrade"], &environment).err().unwrap();
assert_eq!(
err.to_string(),
format!("You do not have write permission to {}", environment.current_exe().unwrap().display())
);
assert_eq!(environment.take_stdout_messages(), vec!["Upgrading from 0.0.0 to 0.1.0..."]);
}

#[test]
fn should_upgrade_and_fail_node_modules() {
let environment = TestEnvironment::new();
environment.add_remote_file("https://plugins.dprint.dev/cli.json", r#"{ "version": "0.1.0" }"#.as_bytes());
environment.set_current_exe_path("/test/node_modules/dprint/dprint");
let err = run_test_cli(vec!["upgrade"], &environment).err().unwrap();
assert_eq!(
err.to_string(),
"Cannot upgrade with `dprint upgrade` when the dprint executable is within a node_modules folder. Upgrade with npm instead.",
);
assert_eq!(environment.take_stdout_messages(), vec!["Upgrading from 0.0.0 to 0.1.0..."]);
}

#[test]
fn should_upgrade_and_fail_homebrew() {
let environment = TestEnvironment::new();
environment.add_remote_file("https://plugins.dprint.dev/cli.json", r#"{ "version": "0.1.0" }"#.as_bytes());
environment.set_current_exe_path("/usr/local/Cellar/dprint");
let err = run_test_cli(vec!["upgrade"], &environment).err().unwrap();
assert_eq!(
err.to_string(),
"Cannot upgrade with `dprint upgrade` when the dprint executable is installed via Homebrew. Run `brew upgrade dprint` instead.",
);
assert_eq!(environment.take_stdout_messages(), vec!["Upgrading from 0.0.0 to 0.1.0..."]);
}

#[test]
fn should_upgrade_and_fail_different_version_no_remote_zip() {
let environment = TestEnvironment::new();
environment
.set_file_permissions(environment.current_exe().unwrap(), FilePermissions::Test(Default::default()))
.unwrap();
environment.add_remote_file("https://plugins.dprint.dev/cli.json", r#"{ "version": "0.1.0" }"#.as_bytes());
let err = run_test_cli(vec!["upgrade"], &environment).err().unwrap();
assert!(err.to_string().starts_with("Error downloading"));
assert_eq!(environment.take_stdout_messages(), vec!["Upgrading from 0.0.0 to 0.1.0..."]);
}
}
25 changes: 25 additions & 0 deletions crates/dprint/src/environment/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@ pub enum DirEntryKind {
File,
}

#[derive(Debug, Clone)]
pub enum FilePermissions {
Std(std::fs::Permissions),
#[allow(dead_code)]
Test(TestFilePermissions),
}

impl FilePermissions {
pub fn readonly(&self) -> bool {
match self {
FilePermissions::Std(p) => p.readonly(),
FilePermissions::Test(p) => p.readonly,
}
}
}

#[derive(Default, Debug, Clone)]
pub struct TestFilePermissions {
pub readonly: bool,
}

pub trait UrlDownloader {
fn download_file(&self, url: &str) -> Result<Option<Vec<u8>>>;
fn download_file_err_404(&self, url: &str) -> Result<Vec<u8>> {
Expand All @@ -37,14 +58,18 @@ pub trait Environment: Clone + Send + Sync + UrlDownloader + 'static {
fn read_file_bytes(&self, file_path: impl AsRef<Path>) -> Result<Vec<u8>>;
fn write_file(&self, file_path: impl AsRef<Path>, file_text: &str) -> Result<()>;
fn write_file_bytes(&self, file_path: impl AsRef<Path>, bytes: &[u8]) -> Result<()>;
fn rename(&self, path_from: impl AsRef<Path>, path_to: impl AsRef<Path>) -> Result<()>;
fn remove_file(&self, file_path: impl AsRef<Path>) -> Result<()>;
fn remove_dir_all(&self, dir_path: impl AsRef<Path>) -> Result<()>;
fn dir_info(&self, dir_path: impl AsRef<Path>) -> Result<Vec<DirEntry>>;
fn path_exists(&self, file_path: impl AsRef<Path>) -> bool;
fn canonicalize(&self, path: impl AsRef<Path>) -> Result<CanonicalizedPathBuf>;
fn is_absolute_path(&self, path: impl AsRef<Path>) -> bool;
fn file_permissions(&self, path: impl AsRef<Path>) -> Result<FilePermissions>;
fn set_file_permissions(&self, path: impl AsRef<Path>, permissions: FilePermissions) -> Result<()>;
fn mk_dir_all(&self, path: impl AsRef<Path>) -> Result<()>;
fn cwd(&self) -> CanonicalizedPathBuf;
fn current_exe(&self) -> Result<PathBuf>;
fn log(&self, text: &str);
fn log_stderr(&self, text: &str) {
self.log_stderr_with_context(text, "dprint");
Expand Down
27 changes: 27 additions & 0 deletions crates/dprint/src/environment/real_environment.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use dprint_cli_core::download_url;
use dprint_cli_core::logging::log_action_with_progress;
Expand All @@ -18,6 +19,7 @@ use super::CanonicalizedPathBuf;
use super::DirEntry;
use super::DirEntryKind;
use super::Environment;
use super::FilePermissions;
use super::UrlDownloader;
use crate::plugins::CompilationResult;

Expand Down Expand Up @@ -95,6 +97,10 @@ impl Environment for RealEnvironment {
}
}

fn rename(&self, path_from: impl AsRef<Path>, path_to: impl AsRef<Path>) -> Result<()> {
Ok(fs::rename(&path_from, &path_to).with_context(|| format!("Error renaming {} to {}", path_from.as_ref().display(), path_to.as_ref().display()))?)
}

fn remove_file(&self, file_path: impl AsRef<Path>) -> Result<()> {
log_verbose!(self, "Deleting file: {}", file_path.as_ref().display());
match fs::remove_file(&file_path) {
Expand Down Expand Up @@ -163,6 +169,23 @@ impl Environment for RealEnvironment {
path.as_ref().is_absolute()
}

fn file_permissions(&self, path: impl AsRef<Path>) -> Result<FilePermissions> {
Ok(FilePermissions::Std(
fs::metadata(&path)
.with_context(|| format!("Error getting file permissions for: {}", path.as_ref().display()))?
.permissions(),
))
}

fn set_file_permissions(&self, path: impl AsRef<Path>, permissions: FilePermissions) -> Result<()> {
let permissions = match permissions {
FilePermissions::Std(p) => p,
_ => panic!("Programming error. Permissions did not contain an std permission."),
};
fs::set_permissions(&path, permissions).with_context(|| format!("Error setting file permissions for: {}", path.as_ref().display()))?;
Ok(())
}

fn mk_dir_all(&self, path: impl AsRef<Path>) -> Result<()> {
log_verbose!(self, "Creating directory: {}", path.as_ref().display());
match fs::create_dir_all(&path) {
Expand All @@ -177,6 +200,10 @@ impl Environment for RealEnvironment {
.expect("expected to canonicalize the cwd")
}

fn current_exe(&self) -> Result<PathBuf> {
Ok(std::env::current_exe().context("Error getting current executable.")?)
}

fn log(&self, text: &str) {
self.logger.log(text, "dprint");
}
Expand Down
Loading

0 comments on commit 4391b8b

Please sign in to comment.