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

pnpm env use --global lts - EPERM: operation not permitted, symlink (Windows 11) #4315

Closed
colinmollenhour opened this issue Feb 9, 2022 · 12 comments · Fixed by #6405
Closed

Comments

@colinmollenhour
Copy link

pnpm version: 6.30.0 (compiled to binary; bundled Node.js v14.17.0)

Code to reproduce the issue:

PS > iwr https://get.pnpm.io/install.ps1 -useb | iex

PATH did not contain pnpm after launching a new shell, had to run the above twice for some reason?

PS > pnpm env use --global lts

Expected behavior:

Installs the latest LTS and links it into PATH

Actual behavior:

EPERM  EPERM: operation not permitted, symlink 'C:\Users\USER\AppData\Local\pnpm\nodejs\16.14.0\node.exe' -> 'C:\Users\USER\AppData\Local\pnpm\node.exe'

Additional information:

This is a very clean install of Windows 11 - no previous version of Node or PNPM had ever been installed before.

I ran the same steps in a PowerShell invoked with Run as Administrator and that worked. Is it expected that the installer needs to run as Administrator? If so, I believe this should be noted in the installation guide.

@ghost
Copy link

ghost commented Apr 8, 2022

Same here, need to use administrator privileges to install.

@kaminskypavel
Copy link

you'll need to run either cmd or git bash "as administrator" 🫤

@mominshaikhdevs
Copy link

... Is it expected that the installer needs to run as Administrator?...

It's been more than 6 years since you don't need to "Run As Administrator" to create "Symlinks" in windows 10 v10.0.14972 and newer Windows versions - when you have "Developer Mode" enabled. ( Settings > Update and Security > For Developers > toggle on Developer Mode ).

Screenshot 2022-12-18 160133

@colinmollenhour
Copy link
Author

@mominshaikhdevs Good to know, but is it expected that one must first enable Developer Mode to install pnpm?
I don't think "Developer Mode" means just anyone that is a developer but rather developers who specifically need to run locally compiled code and disable security features of Windows. Running node.exe doesn't otherwise require developer mode so IMHO it shouldn't be required for installation and should be discouraged for security reasons.

@mominshaikhdevs
Copy link

@colinmollenhour Answer to that question will be a no as pnpm uses hardlinks , not symlinks. Hardlinks don't require "Developer Mode".

@RihanArfan
Copy link

I can replicate this on Version 8.2.0 (compiled to binary; bundled Node.js v18.1.0) installed on Windows 10 without admin.

PS C:\Users\rihan> pnpm env use --global lts
Fetching Node.js 18.15.0 ...
 EPERM  EPERM: operation not permitted, symlink 'C:\Users\rihan\AppData\Local\pnpm\nodejs\18.15.0\node.exe' -> 'C:\Users\rihan\AppData\Local\pnpm\node.exe'

@RihanArfan
Copy link

RihanArfan commented Apr 12, 2023

I'm digging in the source code and I think I found the culprit.

await fs.symlink(src, dest, 'file')

I think the options are:

  • Changing it from file to junction might work, but I'm still reading up on it. Doesn't work, just makes a shortcut folder
  • Alternatively, we could hard link? EDIT: Using the same code to hard link worked.

The code the rest of pnpm uses for hard linking is here I believe, but it's not exported.

async function linkOrCopyFile (srcFile: string, destFile: string) {
try {
await linkOrCopy(srcFile, destFile)
} catch (err: any) { // eslint-disable-line
if (err.code === 'ENOENT') {
await fs.mkdir(path.dirname(destFile), { recursive: true })
await linkOrCopy(srcFile, destFile)
return
}
if (err.code !== 'EEXIST') {
throw err
}
}
}
/*
* This function could be optimized because we don't really need to try linking again
* if linking failed once.
*/
async function linkOrCopy (srcFile: string, destFile: string) {
try {
await fs.link(srcFile, destFile)
} catch (err: any) { // eslint-disable-line
if (err.code !== 'EXDEV') throw err
await fs.copyFile(srcFile, destFile)
}
}

@zkochan
Copy link
Member

zkochan commented Apr 12, 2023

The solution is to use the symlink-dir package instead of fs.symlink. It creates junctions on Windows, which work with no issues. symlink-dir is used a lot by pnpm CLI.

@RihanArfan
Copy link

Thank you very much for the recommendation. I've tried but I'm running into some issues while testing it.

symlink-dir

// symlink.js
const symlinkDir = require("symlink-dir");

const src = `C:\\Users\\rihan\\AppData\\Local\\pnpm\\nodejs\\18.15.0\\node.exe`;
const dest = `C:\\Users\\rihan\\AppData\\Local\\pnpm\\node.exe`;

symlinkDir(src, dest).then((testOutput) => console.log(testOutput));

image

ll ~/AppData/Local/pnpm/
total 46844
lrwxrwxrwx 1 rihan x       64 Apr 12 15:58 node.exe -> /c/Users/rihan/AppData/Local/pnpm/nodejs/18.15.0/node.exe/
drwxr-xr-x 1 rihan x        0 Apr 11 17:26 nodejs/
-rwxr-xr-x 1 rihan x 47966246 Apr 11 17:21 pnpm.exe*
drwxr-xr-x 1 rihan x        0 Nov 18 06:05 store/
$ ~/AppData/Local/pnpm/nodejs.exe
bash: /c/Users/rihan/AppData/Local/pnpm/nodejs.exe: No such file or directory

It looks like it's treating node.exe from the source dir as if it's a folder (trailing slash in ll).

Is there something I'm missing/doing wrong here?

Hard linking

Code
// hardlink.mjs
import { promises as fs } from "fs";

const src = `C:\\Users\\rihan\\AppData\\Local\\pnpm\\nodejs\\18.15.0\\node.exe`;
const dest = `C:\\Users\\rihan\\AppData\\Local\\pnpm\\node.exe`;

await linkOrCopyFile(src, dest);

async function linkOrCopyFile(srcFile, destFile) {
  try {
    await linkOrCopy(srcFile, destFile);
  } catch (err) {
    // eslint-disable-line
    if (err.code === "ENOENT") {
      await fs.mkdir(path.dirname(destFile), { recursive: true });
      await linkOrCopy(srcFile, destFile);
      return;
    }
    if (err.code !== "EEXIST") {
      throw err;
    }
  }
}

/*
 * This function could be optimized because we don't really need to try linking again
 * if linking failed once.
 */
async function linkOrCopy(srcFile, destFile) {
  try {
    await fs.link(srcFile, destFile);
  } catch (err) {
    // eslint-disable-line
    if (err.code !== "EXDEV") throw err;
    await fs.copyFile(srcFile, destFile);
  }
}

image

$ ll ~/AppData/Local/pnpm/
total 115092
-rwxr-xr-x 3 rihan x 69881976 Mar  5 05:37 node.exe*
drwxr-xr-x 1 rihan x        0 Apr 11 17:26 nodejs/
-rwxr-xr-x 1 rihan x 47966246 Apr 11 17:21 pnpm.exe*
drwxr-xr-x 1 rihan x        0 Nov 18 06:05 store/
$ /c/Users/rihan/AppData/Local/pnpm/node.exe --version
v18.15.0

Original behaviour

After enabling developer mode (requires admin and reduces Windows security)

image

$ ll ~/AppData/Local/pnpm/
total 46848
lrwxrwxrwx 1 rihan x       63 Apr 12 16:12 node.exe -> /c/Users/rihan/AppData/Local/pnpm/nodejs/18.15.0/node.exe*
drwxr-xr-x 1 rihan  x        0 Apr 11 17:26 nodejs/
-rwxr-xr-x 1 rihan  x      338 Apr 12 16:12 npm*
-rw-r--r-- 1 rihan x      234 Apr 12 16:12 npm.CMD
-rwxr-xr-x 1 rihan x      338 Apr 12 16:12 npx*
-rw-r--r-- 1 rihan  x      234 Apr 12 16:12 npx.CMD
-rwxr-xr-x 1 rihan x 47966246 Apr 12 16:12 pnpm.exe*
drwxr-xr-x 1 rihan x        0 Nov 18 06:05 store/
$ /c/Users/rihan/AppData/Local/pnpm/node.exe --version
v18.15.0

@zkochan
Copy link
Member

zkochan commented Apr 13, 2023

I have added a test for symlinking a file and it seem to work: https://github.com/pnpm/symlink-dir/actions/runs/4690793134/jobs/8314383394

EDIT: no, it fails

@zkochan
Copy link
Member

zkochan commented Apr 16, 2023

@RihanArfan it appears that junctions only work with directories, so symlink-dir won't work. I guess we may use hard links if they work.

@colinmollenhour
Copy link
Author

Thanks @zkochan !

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

Successfully merging a pull request may close this issue.

5 participants