Hi, and thank you for creating and maintaining this amazing open-source project.
I've been using PRoot on a Google Pixel 9a through Termux.
While building a Next.js project with pnpm and Turbopack, I encountered the following error:
After investigating the issue, I found what appears to be a bug in PRoot's handling of filesystem metadata for hard-linked files. I've included a minimal reproduction and the investigation results below.
Summary
When running Next.js (Turbopack) inside Termux + PRoot Ubuntu, a project installed with pnpm hardlink mode fails to build with:
TurbopackInternalError: Invalid symlink
The investigation indicates that PRoot returns inconsistent metadata for hard-linked regular files.
For the same filesystem path:
os.scandir().is_symlink() reports True
os.lstat() reports a regular file
os.readlink() returns EINVAL
st_nlink == 2 (hard-linked file)
As a result, applications that trust directory enumeration metadata (such as Turbopack) treat the file as a symbolic link and fail with Invalid symlink.
Environment
- Device: Google Pixel 9a
- Android: 17
- Termux: googleplay.2026.06.21
- Ubuntu: 25.10(PRoot v5.1.0)
- PRoot-distro: v4.38.0
- pnpm: 10.34.5
- Next.js: 16.2.12
- Turbopack: bundled with Next.js 16.2.12
Reproduction
1. Create a new Next.js project
pnpm create next-app proot-test
cd proot-test
2. Install dependencies using hardlink mode
rm -rf node_modules
pnpm install --package-import-method=hardlink
3. Verify hard-linked files exist
find node_modules -links +1 -print | head
Example:
node_modules/.pnpm/has-flag@4.0.0/node_modules/has-flag/package.json
4. Build with Turbopack
Result:
Error [TurbopackInternalError]: Invalid symlink
Relevant stack:
- Execution of try_get_next_package failed
- Execution of read_package_json failed
- Execution of <FileSource as Asset>::content failed
- Invalid symlink
Metadata inspection
The following script finds one hard-linked regular file and compares the metadata returned by os.scandir() and os.lstat().
import os
import stat
import sys
root = "node_modules"
target = None
stack = [root]
while stack:
directory = stack.pop()
try:
entries = list(os.scandir(directory))
except OSError:
continue
for entry in entries:
path = entry.path
try:
st = os.lstat(path)
except OSError:
continue
if stat.S_ISDIR(st.st_mode):
stack.append(path)
continue
if stat.S_ISREG(st.st_mode) and st.st_nlink > 1:
target = path
break
if target:
break
if target is None:
print("No hard-linked regular file found.")
sys.exit(1)
parent = os.path.dirname(target)
name = os.path.basename(target)
print("target:", target)
for entry in os.scandir(parent):
if entry.name != name:
continue
st = os.lstat(target)
print("scandir is_symlink:", entry.is_symlink())
print("scandir inode :", entry.inode())
print("lstat inode :", st.st_ino)
print("lstat mode :", oct(st.st_mode))
print("lstat is regular :", stat.S_ISREG(st.st_mode))
print("lstat is symlink :", stat.S_ISLNK(st.st_mode))
print("nlink :", st.st_nlink)
try:
print("readlink :", os.readlink(target))
except OSError as error:
print("readlink error :", repr(error))
break
Actual output
target: node_modules/.pnpm/has-flag@4.0.0/node_modules/has-flag/package.json
scandir is_symlink: True
scandir inode : 1523265
lstat inode : 1349956
lstat mode : 0o100644
lstat is regular : True
lstat is symlink : False
nlink : 2
readlink error : OSError(22, 'Invalid argument')
Expected behavior
For the same filesystem entry:
os.scandir().is_symlink() == False
entry.inode() == os.lstat().st_ino
os.readlink() should not be called on a regular file.
Actual behavior
For the same filesystem path:
| API |
Result |
os.scandir().is_symlink() |
True |
os.lstat() |
Regular file |
st_nlink |
2 |
os.readlink() |
EINVAL |
Additionally:
entry.inode() != os.lstat().st_ino
which indicates inconsistent metadata for the same filesystem entry.
Workaround
Removing node_modules and reinstalling using copy mode avoids the issue.
rm -rf node_modules
pnpm install --package-import-method=copy
pnpm build
The build succeeds.
Suspected cause
PRoot appears to return inconsistent metadata during directory enumeration.
A hard-linked regular file is reported as a symbolic link by os.scandir(), while os.lstat() correctly reports it as a regular file.
Applications that trust directory enumeration metadata subsequently attempt to resolve the file as a symbolic link.
Since the file is actually a regular file, readlink() fails with:
EINVAL (Invalid argument)
which ultimately causes errors such as:
TurbopackInternalError: Invalid symlink
Additional notes
This issue is reproducible only when pnpm installs packages using hardlink mode.
Reinstalling with:
pnpm install --package-import-method=copy
eliminates the issue, strongly suggesting that the bug is triggered by hard-linked regular files.
Hi, and thank you for creating and maintaining this amazing open-source project.
I've been using PRoot on a Google Pixel 9a through Termux.
While building a Next.js project with pnpm and Turbopack, I encountered the following error:
After investigating the issue, I found what appears to be a bug in PRoot's handling of filesystem metadata for hard-linked files. I've included a minimal reproduction and the investigation results below.
Summary
When running Next.js (Turbopack) inside Termux + PRoot Ubuntu, a project installed with pnpm hardlink mode fails to build with:
The investigation indicates that PRoot returns inconsistent metadata for hard-linked regular files.
For the same filesystem path:
os.scandir().is_symlink()reports Trueos.lstat()reports a regular fileos.readlink()returns EINVALst_nlink == 2(hard-linked file)As a result, applications that trust directory enumeration metadata (such as Turbopack) treat the file as a symbolic link and fail with
Invalid symlink.Environment
Reproduction
1. Create a new Next.js project
pnpm create next-app proot-test cd proot-test2. Install dependencies using hardlink mode
3. Verify hard-linked files exist
find node_modules -links +1 -print | headExample:
4. Build with Turbopack
Result:
Relevant stack:
Metadata inspection
The following script finds one hard-linked regular file and compares the metadata returned by
os.scandir()andos.lstat().Actual output
Expected behavior
For the same filesystem entry:
Actual behavior
For the same filesystem path:
os.scandir().is_symlink()Trueos.lstat()st_nlink2os.readlink()EINVALAdditionally:
which indicates inconsistent metadata for the same filesystem entry.
Workaround
Removing
node_modulesand reinstalling using copy mode avoids the issue.The build succeeds.
Suspected cause
PRoot appears to return inconsistent metadata during directory enumeration.
A hard-linked regular file is reported as a symbolic link by
os.scandir(), whileos.lstat()correctly reports it as a regular file.Applications that trust directory enumeration metadata subsequently attempt to resolve the file as a symbolic link.
Since the file is actually a regular file,
readlink()fails with:which ultimately causes errors such as:
Additional notes
This issue is reproducible only when pnpm installs packages using hardlink mode.
Reinstalling with:
eliminates the issue, strongly suggesting that the bug is triggered by hard-linked regular files.