Skip to content

Commit f95d496

Browse files
authored
[PHP-wasm Node] Dynamically mount symlinks (#125)
## Motivation for the change, related issues This PR introduces dynamic mounting of symlinked directories into PHP-wasm. Before this PR, users had to mount symlinked directories into PHP-wasm for them to work. This behavior isn't expected by users as alternative solutions like Apache or Nginx don't require mounting and symlinks will "just" work. In the Node version of PHP-wasm, users can mount directories from their OS filesystem into PHP-wasm. This is done using Emscripten's `mount` function, which adds the mounted directory as a Node to Emscripten's filesystem. If the directory in the OS filesystem has a symlink, Emscripten will follow it within its filesystem, and if the path doesn't exist inside Emscripten's filesystem, Emscripten will throw an error. In practice, this means that if a user has a directory `/A` on their OS that links to `/B`, Emscripten will only be able to follow `/A/B` if `/B` is also mounted into Emscripten. ## Implementation details To implement dynamic symlink mounting, this PR overrides `FS.filesystems.NODEFS.node_ops.readlink` inside Emscripten, which is used to obtain the string value stored in a symlink. The modified `readlink` function mounts the link path into Emscripten if the path doesn't exist in Emscripten's filesystem and if it exists in the OS filesystem. Afterwards, it returns the absolute path to the linked path. Dynamic symlink mounting will be only enabled if `emscriptenOptions.allowSymlinkMounting` is set to `true` in `loadNodeRuntime` options. ## Testing Instructions (or ideally a Blueprint) - CI
1 parent 0b998b2 commit f95d496

File tree

7 files changed

+471
-49
lines changed

7 files changed

+471
-49
lines changed

packages/php-wasm/node/src/lib/load-runtime.ts

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import type {
22
SupportedPHPVersion,
33
EmscriptenOptions,
4+
PHPRuntime,
45
} from '@php-wasm/universal';
5-
import { loadPHPRuntime } from '@php-wasm/universal';
6-
6+
import { loadPHPRuntime, FSHelpers } from '@php-wasm/universal';
7+
import fs from 'fs';
78
import { getPHPLoaderModule } from '.';
89
import { withNetworking } from './networking/with-networking';
10+
import { joinPaths } from '@php-wasm/util';
911

1012
export interface PHPLoaderOptions {
1113
emscriptenOptions?: EmscriptenOptions;
14+
followSymlinks?: boolean;
1215
}
1316

1417
/**
@@ -32,6 +35,99 @@ export async function loadNodeRuntime(
3235
throw error;
3336
},
3437
...(options.emscriptenOptions || {}),
38+
onRuntimeInitialized: (phpRuntime: PHPRuntime) => {
39+
/**
40+
* When users mount a directory using the `mount` function,
41+
* the directory becomes accessible in the Emscripten's filesystem.
42+
* But if the directory contains symlinks to directories that
43+
* are not mounted, the symlinks will not be accessible to Emscripten.
44+
*
45+
* To work around this, we intercept the `readlink` function and
46+
* mount the OS directory on demand.
47+
*
48+
* If a link path is missing from the Emscripten's filesystem
49+
* and the link path exists on the OS filesystem, create the directory
50+
* in the Emscripten's filesystem and mount the OS directory
51+
* to the Emscripten filesystem.
52+
*
53+
* The directory is mounted to the `/internals/symlinks` directory to avoid
54+
* conflicts with existing VFS directories.
55+
* We can set a arbitrary mount path because readlink is the source of truth
56+
* for the path and Emscripten will accept it as if it was the real link path.
57+
*/
58+
if (options?.followSymlinks === true) {
59+
phpRuntime.FS.filesystems.NODEFS.node_ops.readlink = (
60+
node: any
61+
) => {
62+
const absoluteSourcePath =
63+
phpRuntime.FS.filesystems.NODEFS.tryFSOperation(() =>
64+
fs.realpathSync(
65+
phpRuntime.FS.filesystems.NODEFS.realPath(node)
66+
)
67+
);
68+
const symlinkPath = joinPaths(
69+
`/internals/symlinks`,
70+
absoluteSourcePath
71+
);
72+
if (
73+
!FSHelpers.fileExists(phpRuntime.FS, symlinkPath) &&
74+
fs.existsSync(absoluteSourcePath)
75+
) {
76+
phpRuntime.FS.mkdirTree(symlinkPath);
77+
phpRuntime.FS.mount(
78+
phpRuntime.FS.filesystems.NODEFS,
79+
{ root: absoluteSourcePath },
80+
symlinkPath
81+
);
82+
}
83+
return symlinkPath;
84+
};
85+
}
86+
87+
/**
88+
* Emscripten automatically detects the filesystem for a given path,
89+
* and because the root path always uses the MEMFS filesystem, `statfs`
90+
* will return the default hardcoded value for MEMFS instead of the
91+
* actual disk space.
92+
*
93+
* To ensure `statfs` works in the Node version of PHP-WASM,
94+
* we need to add `statfs` from NODEFS to the root FS.
95+
* Otherwise, `statfs` is undefined in the root FS and the NODEFS
96+
* implementation wouldn't be used for paths that exist in MEMFS.
97+
*
98+
* The only place `statfs` is used in PHP are the `disk_total_space`
99+
* and `disk_free_space` functions.
100+
* Both functions return the disk space for a given disk partition.
101+
* If a subdirectory is passed, the function will return the disk space
102+
* for its partition.
103+
*/
104+
phpRuntime.FS.root.node_ops = {
105+
...phpRuntime.FS.root.node_ops,
106+
statfs: phpRuntime.FS.filesystems.NODEFS.node_ops.statfs,
107+
};
108+
109+
/**
110+
* By default FS.root node value of `mount.opts.root` is `undefined`.
111+
* As a result `FS.lookupPath` will return a node with a `undefined`
112+
* `mount.opts.root` path when looking up the `/` path using `FS.lookupPath`.
113+
*
114+
* The `NODEFS.realPath` function works with `undefined` because it uses
115+
* `path.join` to build the path and for the `[undefined]` it will
116+
* return the `.` path.
117+
*
118+
* Because the `node.mount.opts.root` path is `undefined`,
119+
* `fs.statfsSync` will throw an error when trying to get the
120+
* disk space for an undefined path.
121+
* For the `/` path to correctly resolve, we must set the
122+
* `mount.opts.root` path to the current working directory.
123+
*
124+
* We chose the current working directory over `/` because
125+
* NODERAWFS defines the root path as `.`.
126+
* Emscripten reference to setting the root path in NODERAWFS:
127+
* https://github.com/emscripten-core/emscripten/pull/19400/files#diff-456b6256111c90ca5e6bdb583ab87108cd51cbbefc812c4785ea315c0728b3a8R11
128+
*/
129+
phpRuntime.FS.root.mount.opts.root = '.';
130+
},
35131
};
36132
return await loadPHPRuntime(
37133
await getPHPLoaderModule(phpVersion),

0 commit comments

Comments
 (0)