Skip to content

Commit 95b7560

Browse files
jasnellevanlucas
authored andcommitted
src,module: add --preserve-symlinks command line flag
Add the `--preserve-symlinks` flag. This makes the changes added in #5950 conditional. By default the old behavior is used. With the flag set, symlinks are preserved, switching to the new behavior. This should be considered to be a temporary solution until we figure out how to solve the symlinked peer dependency problem in a more general way that does not break everything else. Additional test cases are included. PR-URL: #6537 Reviewed-By: Ben Noordhuis <[email protected]>
1 parent b4fb95e commit 95b7560

File tree

13 files changed

+271
-9
lines changed

13 files changed

+271
-9
lines changed

doc/api/cli.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,43 @@ Automatically zero-fills all newly allocated [Buffer][] and [SlowBuffer][]
9797
instances.
9898

9999

100+
### `--preserve-symlinks`
101+
102+
Instructs the module loader to preserve symbolic links when resolving and
103+
caching modules.
104+
105+
By default, when Node.js loads a module from a path that is symbolically linked
106+
to a different on-disk location, Node.js will dereference the link and use the
107+
actual on-disk "real path" of the module as both an identifier and as a root
108+
path to locate other dependency modules. In most cases, this default behavior
109+
is acceptable. However, when using symbolically linked peer dependencies, as
110+
illustrated in the example below, the default behavior causes an exception to
111+
be thrown if `moduleA` attempts to require `moduleB` as a peer dependency:
112+
113+
```text
114+
{appDir}
115+
├── app
116+
│ ├── index.js
117+
│ └── node_modules
118+
│ ├── moduleA -> {appDir}/moduleA
119+
│ └── moduleB
120+
│ ├── index.js
121+
│ └── package.json
122+
└── moduleA
123+
├── index.js
124+
└── package.json
125+
```
126+
127+
The `--preserve-symlinks` command line flag instructs Node.js to use the
128+
symlink path for modules as opposed to the real path, allowing symbolically
129+
linked peer dependencies to be found.
130+
131+
Note, however, that using `--preserve-symlinks` can have other side effects.
132+
Specifically, symbolically linked *native* modules can fail to load if those
133+
are linked from more than one location in the dependency tree (Node.js would
134+
see those as two separate modules and would attempt to load the module multiple
135+
times, causing an exception to be thrown).
136+
100137
### `--track-heap-objects`
101138

102139
Track heap object allocations for heap snapshots.
@@ -138,7 +175,6 @@ Force FIPS-compliant crypto on startup. (Cannot be disabled from script code.)
138175

139176
Specify ICU data load path. (overrides `NODE_ICU_DATA`)
140177

141-
142178
## Environment Variables
143179

144180
### `NODE_DEBUG=module[,…]`

doc/node.1

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ of the event loop.
9595
.BR \-\-zero\-fill\-buffers
9696
Automatically zero-fills all newly allocated Buffer and SlowBuffer instances.
9797

98+
.TP
99+
.BR \-\-preserve\-symlinks
100+
Instructs the module loader to preserve symbolic links when resolving and
101+
caching modules.
102+
98103
.TP
99104
.BR \-\-track\-heap-objects
100105
Track heap object allocations for heap snapshots.

lib/module.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const fs = require('fs');
1010
const path = require('path');
1111
const internalModuleReadFile = process.binding('fs').internalModuleReadFile;
1212
const internalModuleStat = process.binding('fs').internalModuleStat;
13+
const preserveSymlinks = !!process.binding('config').preserveSymlinks;
1314

1415
// If obj.hasOwnProperty has been overridden, then calling
1516
// obj.hasOwnProperty(prop) will break.
@@ -109,14 +110,15 @@ function tryPackage(requestPath, exts, isMain) {
109110
}
110111

111112
// check if the file exists and is not a directory
112-
// resolve to the absolute realpath if running main module,
113-
// otherwise resolve to absolute while keeping symlinks intact.
113+
// if using --preserve-symlinks and isMain is false,
114+
// keep symlinks intact, otherwise resolve to the
115+
// absolute realpath.
114116
function tryFile(requestPath, isMain) {
115117
const rc = stat(requestPath);
116-
if (isMain) {
117-
return rc === 0 && fs.realpathSync(requestPath);
118+
if (preserveSymlinks && !isMain) {
119+
return rc === 0 && path.resolve(requestPath);
118120
}
119-
return rc === 0 && path.resolve(requestPath);
121+
return rc === 0 && fs.realpathSync(requestPath);
120122
}
121123

122124
// given a path check a the file exists with any of the set extensions
@@ -159,7 +161,7 @@ Module._findPath = function(request, paths, isMain) {
159161
if (!trailingSlash) {
160162
const rc = stat(basePath);
161163
if (rc === 0) { // File.
162-
if (!isMain) {
164+
if (preserveSymlinks && !isMain) {
163165
filename = path.resolve(basePath);
164166
} else {
165167
filename = fs.realpathSync(basePath);

src/node.cc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@ bool force_fips_crypto = false;
168168
bool no_process_warnings = false;
169169
bool trace_warnings = false;
170170

171+
// Set in node.cc by ParseArgs when --preserve-symlinks is used.
172+
// Used in node_config.cc to set a constant on process.binding('config')
173+
// that is used by lib/module.js
174+
bool config_preserve_symlinks = false;
175+
171176
// process-relative uptime base, initialized at start-up
172177
static double prog_start_time;
173178
static bool debugger_running;
@@ -3460,6 +3465,8 @@ static void PrintHelp() {
34603465
" note: linked-in ICU data is\n"
34613466
" present.\n"
34623467
#endif
3468+
" --preserve-symlinks preserve symbolic links when resolving\n"
3469+
" and caching modules.\n"
34633470
#endif
34643471
"\n"
34653472
"Environment variables:\n"
@@ -3589,6 +3596,8 @@ static void ParseArgs(int* argc,
35893596
} else if (strncmp(arg, "--security-revert=", 18) == 0) {
35903597
const char* cve = arg + 18;
35913598
Revert(cve);
3599+
} else if (strcmp(arg, "--preserve-symlinks") == 0) {
3600+
config_preserve_symlinks = true;
35923601
} else if (strcmp(arg, "--prof-process") == 0) {
35933602
prof_process = true;
35943603
short_circuit = true;

src/node_config.cc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ void InitConfig(Local<Object> target,
4141
if (flag_icu_data_dir)
4242
READONLY_BOOLEAN_PROPERTY("usingICUDataDir");
4343
#endif // NODE_HAVE_I18N_SUPPORT
44-
}
44+
45+
if (config_preserve_symlinks)
46+
READONLY_BOOLEAN_PROPERTY("preserveSymlinks");
47+
} // InitConfig
4548

4649
} // namespace node
4750

src/node_internals.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ struct sockaddr;
3030

3131
namespace node {
3232

33+
// Set in node.cc by ParseArgs when --preserve-symlinks is used.
34+
// Used in node_config.cc to set a constant on process.binding('config')
35+
// that is used by lib/module.js
36+
extern bool config_preserve_symlinks;
37+
3338
// Forward declaration
3439
class Environment;
3540

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#include <node.h>
2+
#include <v8.h>
3+
4+
void Method(const v8::FunctionCallbackInfo<v8::Value>& args) {
5+
v8::Isolate* isolate = args.GetIsolate();
6+
args.GetReturnValue().Set(v8::String::NewFromUtf8(isolate, "world"));
7+
}
8+
9+
void init(v8::Local<v8::Object> target) {
10+
NODE_SET_METHOD(target, "hello", Method);
11+
}
12+
13+
NODE_MODULE(binding, init);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
'targets': [
3+
{
4+
'target_name': 'binding',
5+
'sources': [ 'binding.cc' ]
6+
}
7+
]
8+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use strict';
2+
require('../../common');
3+
const path = require('path');
4+
const assert = require('assert');
5+
6+
// This is a subtest of symlinked-module/test.js. This is not
7+
// intended to be run directly.
8+
9+
module.exports.test = function test(bindingDir) {
10+
const mod = require(path.join(bindingDir, 'binding.node'));
11+
assert.notStrictEqual(mod, null);
12+
assert.strictEqual(mod.hello(), 'world');
13+
};

test/addons/symlinked-module/test.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
const common = require('../../common');
3+
const fs = require('fs');
4+
const path = require('path');
5+
const assert = require('assert');
6+
7+
// This test verifies that symlinked native modules can be required multiple
8+
// times without error. The symlinked module and the non-symlinked module
9+
// should be the same instance. This expectation was not previously being
10+
// tested and ended up being broken by https://github.com/nodejs/node/pull/5950.
11+
12+
// This test should pass in Node.js v4 and v5. This test will pass in Node.js
13+
// with https://github.com/nodejs/node/pull/5950 reverted.
14+
15+
common.refreshTmpDir();
16+
17+
const addonPath = path.join(__dirname, 'build', 'Release');
18+
const addonLink = path.join(common.tmpDir, 'addon');
19+
20+
try {
21+
fs.symlinkSync(addonPath, addonLink);
22+
} catch (err) {
23+
if (err.code !== 'EPERM') throw err;
24+
common.skip('module identity test (no privs for symlinks)');
25+
return;
26+
}
27+
28+
const sub = require('./submodule');
29+
[addonPath, addonLink].forEach((i) => {
30+
const mod = require(path.join(i, 'binding.node'));
31+
assert.notStrictEqual(mod, null);
32+
assert.strictEqual(mod.hello(), 'world');
33+
assert.doesNotThrow(() => sub.test(i));
34+
});

0 commit comments

Comments
 (0)