use std::fmt::Display;
#[cfg(target_os = "linux")]
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
use crate::HOME_DIR;
use color_eyre::eyre::Result;
#[cfg(target_os = "linux")]
use nix::unistd::Uid;
use rust_i18n::t;
use semver::Version;
use tracing::debug;
use crate::command::CommandExt;
use crate::terminal::{print_info, print_separator};
use crate::utils::{PathExt, require};
use crate::{error::SkipStep, execution_context::ExecutionContext};
enum NPMVariant {
Npm,
Pnpm,
}
impl NPMVariant {
const fn short_name(&self) -> &str {
match self {
NPMVariant::Npm => "npm",
NPMVariant::Pnpm => "pnpm",
}
}
const fn is_npm(&self) -> bool {
matches!(self, NPMVariant::Npm)
}
}
impl Display for NPMVariant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.short_name())
}
}
#[allow(clippy::upper_case_acronyms)]
struct NPM {
command: PathBuf,
variant: NPMVariant,
}
impl NPM {
fn new(command: PathBuf, variant: NPMVariant) -> Self {
Self { command, variant }
}
fn is_npm_8(&self, ctx: &ExecutionContext) -> bool {
let v = self.version(ctx);
self.variant.is_npm() && matches!(v, Ok(v) if v >= Version::new(8, 11, 0))
}
fn global_location_arg(&self, ctx: &ExecutionContext) -> &str {
if self.is_npm_8(ctx) { "--location=global" } else { "-g" }
}
#[cfg(target_os = "linux")]
fn root(&self, ctx: &ExecutionContext) -> Result<PathBuf> {
let args = ["root", self.global_location_arg(ctx)];
ctx.execute(&self.command)
.always()
.args(args)
.output_checked_utf8()
.map(|s| PathBuf::from(s.stdout.trim()))
}
fn version(&self, ctx: &ExecutionContext) -> Result<Version> {
let version_str = ctx
.execute(&self.command)
.always()
.args(["--version"])
.output_checked_utf8()
.map(|s| s.stdout.trim().to_owned());
Version::parse(&version_str?).map_err(std::convert::Into::into)
}
fn upgrade(&self, ctx: &ExecutionContext, use_sudo: bool) -> Result<()> {
let args = ["update", self.global_location_arg(ctx)];
if use_sudo {
let sudo = ctx.require_sudo()?;
sudo.execute(ctx, &self.command)?.args(args).status_checked()?;
} else {
ctx.execute(&self.command).args(args).status_checked()?;
}
Ok(())
}
#[cfg(target_os = "linux")]
pub fn should_use_sudo(&self, ctx: &ExecutionContext) -> Result<bool> {
let npm_root = self.root(ctx)?;
if !npm_root.exists() {
return Err(SkipStep(format!("{} root at {} doesn't exist", self.variant, npm_root.display())).into());
}
let metadata = std::fs::metadata(&npm_root)?;
let uid = Uid::effective();
Ok(metadata.uid() != uid.as_raw() && metadata.uid() == 0)
}
}
struct Yarn {
command: PathBuf,
}
impl Yarn {
fn new(command: PathBuf) -> Self {
Self { command }
}
fn has_global_subcmd(&self, ctx: &ExecutionContext) -> bool {
let version = ctx
.execute(&self.command)
.always()
.args(["--version"])
.output_checked_utf8();
matches!(version, Ok(ver) if ver.stdout.starts_with('1') || ver.stdout.starts_with('0'))
}
#[cfg(target_os = "linux")]
fn root(&self, ctx: &ExecutionContext) -> Result<PathBuf> {
let args = ["global", "dir"];
ctx.execute(&self.command)
.always()
.args(args)
.output_checked_utf8()
.map(|s| PathBuf::from(s.stdout.trim()))
}
fn upgrade(&self, ctx: &ExecutionContext, use_sudo: bool) -> Result<()> {
let args = ["global", "upgrade"];
if use_sudo {
let sudo = ctx.require_sudo()?;
sudo.execute(ctx, &self.command)?.args(args).status_checked()?;
} else {
ctx.execute(&self.command).args(args).status_checked()?;
}
Ok(())
}
#[cfg(target_os = "linux")]
pub fn should_use_sudo(&self, ctx: &ExecutionContext) -> Result<bool> {
let yarn_root = self.root(ctx)?;
if !yarn_root.exists() {
return Err(SkipStep(format!("Yarn root at {} doesn't exist", yarn_root.display(),)).into());
}
let metadata = std::fs::metadata(&yarn_root)?;
let uid = Uid::effective();
Ok(metadata.uid() != uid.as_raw() && metadata.uid() == 0)
}
}
struct Deno {
command: PathBuf,
}
impl Deno {
fn new(command: PathBuf) -> Self {
Self { command }
}
fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> {
let mut args = vec![];
let version = ctx.config().deno_version();
if let Some(version) = version {
let bin_version = self.version(ctx)?;
if bin_version >= Version::new(2, 0, 0) {
args.push(version);
} else if bin_version >= Version::new(1, 6, 0) {
match version {
"stable" => { }
"rc" => {
return Err(SkipStep(
"Deno (1.6.0-2.0.0) cannot be upgraded to a release candidate".to_string(),
)
.into());
}
"canary" => args.push("--canary"),
_ => {
if Version::parse(version).is_err() {
return Err(SkipStep("Invalid Deno version".to_string()).into());
}
args.push("--version");
args.push(version);
}
}
} else if bin_version >= Version::new(1, 0, 0) {
match version {
"stable" | "rc" | "canary" => {
return Err(
SkipStep("Deno (1.0.0-1.6.0) cannot be upgraded to a named channel".to_string()).into(),
);
}
_ => {
if Version::parse(version).is_err() {
return Err(SkipStep("Invalid Deno version".to_string()).into());
}
args.push("--version");
args.push(version);
}
}
} else {
return Err(SkipStep("Unsupported Deno version".to_string()).into());
}
}
ctx.execute(&self.command).arg("upgrade").args(args).status_checked()?;
Ok(())
}
fn version(&self, ctx: &ExecutionContext) -> Result<Version> {
let version_str = ctx
.execute(&self.command)
.always()
.args(["-V"])
.output_checked_utf8()
.map(|s| s.stdout.trim().to_owned().split_off(5)); Version::parse(&version_str?).map_err(std::convert::Into::into)
}
}
struct VitePlus {
command: PathBuf,
}
impl VitePlus {
fn new(command: PathBuf) -> Self {
Self { command }
}
fn self_upgrade(&self, ctx: &ExecutionContext, use_sudo: bool) -> Result<()> {
let mut args = vec!["upgrade"];
if ctx.run_type().dry() {
args.push("--check");
}
if use_sudo {
let sudo = ctx.require_sudo()?;
sudo.execute(ctx, &self.command)?.args(args).status_checked()?;
} else {
ctx.execute(&self.command).args(args).status_checked()?;
}
Ok(())
}
fn upgrade_packages(&self, ctx: &ExecutionContext, use_sudo: bool) -> Result<()> {
let args = ["update", "--global"];
if use_sudo {
let sudo = ctx.require_sudo()?;
sudo.execute(ctx, &self.command)?.args(args).status_checked()?;
} else {
ctx.execute(&self.command).args(args).status_checked()?;
}
Ok(())
}
#[cfg(target_os = "linux")]
pub fn should_use_sudo(&self, _ctx: &ExecutionContext) -> Result<bool> {
let vp_home = match std::env::var_os("VP_HOME") {
None => return Ok(false),
Some(s) if s.is_empty() => return Ok(false),
Some(s) => s,
};
let uid = Uid::effective();
let metadata = std::fs::metadata(&vp_home)?;
Ok(metadata.uid() != uid.as_raw() && metadata.uid() == 0)
}
}
#[cfg(target_os = "linux")]
fn should_use_sudo(npm: &NPM, ctx: &ExecutionContext) -> Result<bool> {
if npm.should_use_sudo(ctx)? {
if ctx.config().npm_use_sudo() {
Ok(true)
} else {
Err(SkipStep(format!("{} root is owned by another user which is not the current user. Set use_sudo = true under the [npm] section in your configuration to run {} as sudo", npm.variant, npm.variant))
.into())
}
} else {
Ok(false)
}
}
#[cfg(target_os = "linux")]
fn should_use_sudo_viteplus(viteplus: &VitePlus, ctx: &ExecutionContext) -> Result<bool> {
if viteplus.should_use_sudo(ctx)? {
if ctx.config().viteplus_use_sudo() {
Ok(true)
} else {
Err(SkipStep("Vite+ root is owned by another user which is not the current user. Set use_sudo = true under the [viteplus] section in your configuration to run Vite+ as sudo".to_string())
.into())
}
} else {
Ok(false)
}
}
#[cfg(target_os = "linux")]
fn should_use_sudo_yarn(yarn: &Yarn, ctx: &ExecutionContext) -> Result<bool> {
if yarn.should_use_sudo(ctx)? {
if ctx.config().yarn_use_sudo() {
Ok(true)
} else {
Err(SkipStep("Yarn root is owned by another user which is not the current user. Set use_sudo = true under the [yarn] section in your configuration to run Yarn as sudo".to_string())
.into())
}
} else {
Ok(false)
}
}
pub fn run_npm_upgrade(ctx: &ExecutionContext) -> Result<()> {
let npm = require("npm").map(|b| NPM::new(b, NPMVariant::Npm))?;
print_separator(t!("Node Package Manager"));
#[cfg(target_os = "linux")]
{
npm.upgrade(ctx, should_use_sudo(&npm, ctx)?)
}
#[cfg(not(target_os = "linux"))]
{
npm.upgrade(ctx, false)
}
}
pub fn run_pnpm_upgrade(ctx: &ExecutionContext) -> Result<()> {
let pnpm = require("pnpm").map(|b| NPM::new(b, NPMVariant::Pnpm))?;
print_separator(t!("Performant Node Package Manager"));
#[cfg(target_os = "linux")]
{
pnpm.upgrade(ctx, should_use_sudo(&pnpm, ctx)?)
}
#[cfg(not(target_os = "linux"))]
{
pnpm.upgrade(ctx, false)
}
}
pub fn run_viteplus_upgrade(ctx: &ExecutionContext) -> Result<()> {
let viteplus = require("vp").map(VitePlus::new)?;
print_separator("Vite+");
let use_sudo;
#[cfg(target_os = "linux")]
{
use_sudo = should_use_sudo_viteplus(&viteplus, ctx)?;
}
#[cfg(not(target_os = "linux"))]
{
use_sudo = false;
}
viteplus.self_upgrade(ctx, use_sudo)?;
viteplus.upgrade_packages(ctx, use_sudo)?;
Ok(())
}
pub fn run_yarn_upgrade(ctx: &ExecutionContext) -> Result<()> {
let yarn = require("yarn").map(Yarn::new)?;
if !yarn.has_global_subcmd(ctx) {
debug!("Yarn is 2.x or above, skipping global upgrade");
return Ok(());
}
print_separator(t!("Yarn Package Manager"));
#[cfg(target_os = "linux")]
{
yarn.upgrade(ctx, should_use_sudo_yarn(&yarn, ctx)?)
}
#[cfg(not(target_os = "linux"))]
{
yarn.upgrade(ctx, false)
}
}
pub fn deno_upgrade(ctx: &ExecutionContext) -> Result<()> {
let deno = require("deno").map(Deno::new)?;
let deno_dir = HOME_DIR.join(".deno");
if !deno.command.canonicalize()?.is_descendant_of(&deno_dir) {
let skip_reason = SkipStep(t!("Deno installed outside of .deno directory").to_string());
return Err(skip_reason.into());
}
print_separator("Deno");
deno.upgrade(ctx)
}
pub fn run_volta_packages_upgrade(ctx: &ExecutionContext) -> Result<()> {
let volta = require("volta")?;
print_separator("Volta");
if ctx.run_type().dry() {
print_info(t!("Updating Volta packages..."));
return Ok(());
}
let list_output = ctx
.execute(&volta)
.always()
.args(["list", "--format=plain"])
.output_checked_utf8()?
.stdout;
let installed_packages: Vec<&str> = list_output
.lines()
.filter_map(|line| {
let mut parts = line.split_whitespace();
parts.next();
let package_part = parts.next()?;
let version_index = package_part.rfind('@').unwrap_or(package_part.len());
Some(package_part[..version_index].trim())
})
.collect();
if installed_packages.is_empty() {
print_info(t!("No packages installed with Volta"));
return Ok(());
}
for package in &installed_packages {
ctx.execute(&volta).args(["install", package]).status_checked()?;
}
Ok(())
}