Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

corepack disable incorrectly deletes original package manager #112

Open
styfle opened this issue May 27, 2022 · 9 comments
Open

corepack disable incorrectly deletes original package manager #112

styfle opened this issue May 27, 2022 · 9 comments

Comments

@styfle
Copy link
Member

styfle commented May 27, 2022

Running with Node.js v16.15.0 on macOS

npm --version   # 8.5.5
pnpm --version  # command not found

npm i -g pnpm@7.1.6
pnpm --version  # 7.1.6

echo '{"packageManager": "pnpm@6.32.18"}' > package.json
corepack enable pnpm
pnpm --version  # 6.32.18

corepack disable pnpm
pnpm --version  # command not found

I would expected corepack disable pnpm to revert to the previous global install of 7.1.6

@fastfedora
Copy link

Just ran into this issue too trying to make sure Corepack didn't alter the configuration on my system. Simple test case:

yarn --version        # returns version
corepack enable
yarn --version        # returns version
corepack disable
yarn --version        # command not found

Enabling and disabling Corepack should return the system to the original state.

@arcanis
Copy link
Contributor

arcanis commented Mar 23, 2023

It seems difficult to do in a way that won't result in other edge cases. For that to work, we'd need to store the links somewhere, so we can restore them. When using nvm or similar tools, this "where" isn't obvious.

Generally, I feel like it's not a huge deal if corepack enable/disable are "destructive" operations. I'm more interested by the reasons why you'd call disable?

@styfle
Copy link
Member Author

styfle commented Mar 23, 2023

I'm more interested by the reasons why you'd call disable?

I created this issue before COREPACK_ENABLE_STRICT=0 existed so I think that will cover my use case.

It was really unexpected to have disable break my system. I was expecting it to work similar to uninstall and put my system back to the way it was before.

we'd need to store the links somewhere

Perhaps keep a backup copy like the following pseudocode:

  • enable: mv bin/pnpm bin/backup/pnpm && ln -s bin/corepack/pnpm bin/pnpm
  • disable: rm bin/pnpm && mv bin/backup/pnpm bin/pnpm

@fastfedora
Copy link

Generally, I feel like it's not a huge deal if corepack enable/disable are "destructive" operations. I'm more interested by the reasons why you'd call disable?

In general, when trying out a new tool, I try not to use tools that will completely destroy my existing setup. I'm not choosing to use corepack. I'm required to use it because I'm working on a project that uses it in a setup script, so I'm trying to understand what it does.

While it may be the case that once I have everything working, I would use corepack forever and never disable it, it doesn't give one confidence when the default is to destroy your existing configuration when disabling.

I've already been struggling with the docs that state that corepack uses the "latest stable version" rather than the version that was already installed on your machine. I read enough of the documentation to use these as a workaround for that issue:

# Save the current package manger versions as the default versions
command -v yarn &>/dev/null && corepack prepare yarn@`yarn --version` --activate 
command -v pnpm &>/dev/null && corepack prepare pnpm@`pnpm --version` --activate 

And I now have this as a way to find out if it's enabled or disabled, since corepack doesn't provide that functionality out-of-the-box:

ls -al `command -v yarn` | grep corepack &>/dev/null && echo "Enabled" || echo "Disabled"

The last thing I'm trying to figure out is how to ensure it doesn't kill one of our developers configuration just because they decide to disable it after switching to a different project.

Bottom line: it's a trust issue. If I can't trust that an operation can be reversed, I'm much less likely to want to use that operation in the first place.

@fastfedora
Copy link

Also, one of the reasons I looked into this is that the script in question had this line:

corepack prepare yarn@3.3.0 --activate

Whoever wrote the script either didn't understand the consequences of --activate making 3.3.0 the default yarn across the entire system (it would have been better named --make-default).

One of our developers ran this script and then had to figure out why all her other projects were running the wrong version of yarn. And since corepack disable deletes yarn entirely, rather than just disabling corepack, there was no easy way to reverse it (if she happened to remember which version of yarn she was running beforehand, she could re-install it, but who remembers exactly which version of yarn their running?)

Part of what I'm doing in learning the issues around using corepack is so I can update this script correctly for others and warn others of the problems running the script (like the fact that it's not reversible).

@fastfedora
Copy link

For those who want a safe way to enable/disable corepack, I wrote this bash script.

With this script, corepack-safe enable will backup the existing environment before enabling corepack and corepack-safe disable will restore the original environment after disabling corepack. enable also sets the default versions within corepack to your original versions, so if you're behind on upgrading a package manager, you won't have an unexpected upgrade to the latest stable version.

This isn't guaranteed to work in all environments, but in theory it should. I tested it with nvm and it'll only affect your active environment. I work on a Mac, so while I added the code for it to work on Windows, I have no way to test it.

Additional commands include:

status
: Shows the corepack status (enabled/disabled) of each package manager

backup
: Backup existing environment only

restore
: Restore existing environment only

restore-all-envs
: Restores all backups in the backup directory, regardless of the current environment (disable corepack globally)

All other commands are passed through to corepack as-is.

#!/usr/bin/env bash

# corepack-safe: safely enable/disable corepack and discover current status

encodePath() {
  echo $(curl -Gso /dev/null -w %{url_effective} --data-urlencode "input=$1" "" | sed 's/\/\?input=//')
}

decodePath() {
  echo $(printf '%b' "${1//%/\\x}")
}

getInstallFolder() {
  if [[ ! -z ${COREPACK_HOME} ]]; then
    echo "$COREPACK_HOME"
  fi

  echo "${XDG_CACHE_HOME:-${LOCALAPPDATA:-$([[ "${OS}" == "Windows_NT" ]] && echo "${HOMEPATH}/AppData/Local" || echo "${HOME}/.cache")}}/node/corepack"
}

getIntalledPackageManagerPaths() {
  local packageManagers=()

  for packageManager in "${PACKAGE_MANAGERS[@]}"
  do
    executable=$(command -v $packageManager)

    if [ ! -z "$executable" ]; then
      packageManagers+=($executable)
    fi
  done

  echo "${packageManagers[@]}"
}

isCorepackEnabledFor() {
  executablePath=$(command -v $1)

  echo $(readlink "$executablePath" | grep corepack &>/dev/null && echo "y")
}

backupExecutableIfExists() {
  executablePath=$(command -v $1)

  if [ ! -z "$(isCorepackEnabledFor $1)" ]; then
    echo "Skipping backup for $1 -- corepack already enabled"
    return 1
  fi

  if [ ! -z "$executablePath" ]; then
    encodedPath=$(encodePath "$executablePath")
    linkType=$(readlink "$executablePath" &>/dev/null && echo "link" || echo "file")
    # linkPath=$(readlink -f "$executablePath")

    mkdir -p "$BACKUP_PATH"

    echo "Backing up $executablePath (as $linkType)"

    # cp will give an error copying over an existing symbolic link
    if [ -h "$BACKUP_PATH/$encodedPath" ]; then
      rm "$BACKUP_PATH/$encodedPath"
    fi

    cp -RP "$executablePath" "$BACKUP_PATH/$encodedPath"
  fi
}

backup() {
  for packageManager in "${PACKAGE_MANAGERS[@]}"
  do
    backupExecutableIfExists $packageManager
  done
}

restoreExecutableIfExists() {
  encodedPath=$(encodePath "$1")
  backupPath="$BACKUP_PATH/$encodedPath"
  linkType=$(readlink "$backupPath" &>/dev/null && echo "link" || echo "file")
  executablePath=$(decodePath "$encodedPath")

  if [ -e "$backupPath" ] || [ -h "$backupPath" ]; then
    echo "Restoring $executablePath (as $linkType)"

    cp -RP "$backupPath" "$executablePath"
  fi
}

restore() {
  for path in "$@"
  do
    restoreExecutableIfExists $path
  done
}

restoreAll() {
  for file in "$BACKUP_PATH"/*; do
    if [ -f "$file" ] || [ -L "$file" ]; then
      filename=$(basename "$file")
      executablePath=$(decodePath "$filename")

      restoreExecutableIfExists "$executablePath"
    fi
  done
}

checkExecutableStatus() {
  executablePath=$(command -v $1)

  if [ ! -z "$executablePath" ]; then
    if [ ! -z "$(isCorepackEnabledFor $1)" ]; then
      echo "$1: $executablePath (corepack enabled)";
    else
      echo "$1: $executablePath (corepack disabled)";
    fi
  else
    echo "$1: not installed"
  fi
}

checkStatus() {
  for packageManager in "${PACKAGE_MANAGERS[@]}"
  do
    checkExecutableStatus $packageManager
  done
}

setExistingVersionAsDefault() {
  executablePath=$(command -v $1)

  if [ ! -z "$executablePath" ] && [ -z "$(isCorepackEnabledFor $1)" ]; then
    defaultVersion=$($1 --version)

    echo "Setting default version of $1 to $defaultVersion"

    corepack prepare $1@$defaultVersion --activate
  fi
}

setExistingVersionsAsDefaults() {
  for packageManager in "${PACKAGE_MANAGERS[@]}"
  do
    setExistingVersionAsDefault $packageManager
  done

}

PACKAGE_MANAGERS=(yarn pnpm)
INSTALL_DIR=$(getInstallFolder)
BACKUP_PATH=${COREPACK_BACKUP_PATH:=$INSTALL_DIR/backups}

case "$1" in
  # backups up executables for current environment
  backup)
    backup
    ;;

  # restores backups for current environment only
  restore)
    restore $(getIntalledPackageManagerPaths)
    ;;

  # restores all backups saved to backup directory (i.e.,  all environments)
  restore-all-envs)
    restoreAll
    ;;

  # Backs up existing environment before enabling corepack in it
  enable)
    echo "Backing up existing installations to $BACKUP_PATH"
    backup
    setExistingVersionsAsDefaults
    echo "Enabling corepack"
    corepack enable
    ;;

  # Disables corepack, then restores original environment
  disable)
    echo "Disabling corepack"
    packageManagerPaths=$(getIntalledPackageManagerPaths)
    corepack disable
    echo "Restoring original installations from $BACKUP_PATH"
    restore $packageManagerPaths
    ;;

  # Shows the corepack enabled status of each package manager
  status)
    checkStatus
    ;;

  # Delegates to corepack
  *)
    corepack "$@"
    ;;
esac

@simonhaenisch
Copy link

simonhaenisch commented Jun 8, 2023

Instead of backing up existing bins/symlinks, how about this?

mkdir -p "$HOME/.corepack/bin"

echo "export PATH=$HOME/.corepack/bin:\$PATH" >> ~/.zshrc

# create corepack shims in my `.corepack` dir, so no system bins will be overridden
corepack enable --install-directory ~/.corepack/bin

# removing the shims now just reveals the previous binaries again
corepack disable --install-directory ~/.corepack/bin

@ryokryok
Copy link

ryokryok commented Jan 9, 2024

I'm not sure if this is correct way, but it worked when recovering from corepack disable npm.
I am using nvm.

$ corepack disable npm 
$ npm -v 
zsh: command not found: npm

# recovery
$ corepack enable npm # shims : $NVM_BIN/npm -> ../lib/node_modules/corepack/dist/npm.js 
$ npm i -g npm # shims : $NVM_BIN/npm -> ../lib/node_modules/npm/bin/npm-cli.js

Also, ls -l $NVM_BIN command can tell us what package manager managed by corepack.

@danchenkov
Copy link

I'm more interested by the reasons why you'd call disable?

I call corepack disable as it does not support http_proxy and so yarn is not getting found, for example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants