Skip to content

Commit a954c36

Browse files
committed
src: add --preserve-symlinks command line flag
Add the `--preserve-symlinks` flag. This makes the changes added in nodejs#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.
1 parent f52b2f1 commit a954c36

13 files changed

+271
-9
lines changed

doc/api/cli.md

+37-1
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

+5
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

+8-6
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

+9
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

+4-1
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

+5
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

+13
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);
+8
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+
}
+13
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

+34
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+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use strict';
2+
3+
// This tests to make sure that modules with symlinked circular dependencies
4+
// do not blow out the module cache and recurse forever. See issue
5+
// https://github.com/nodejs/node/pull/5950 for context. PR #5950 attempted
6+
// to solve a problem with symlinked peer dependencies by caching using the
7+
// symlink path. Unfortunately, that breaks the case tested in this module
8+
// because each symlinked module, despite pointing to the same code on disk,
9+
// is loaded and cached as a separate module instance, which blows up the
10+
// cache and leads to a recursion bug.
11+
12+
// This test should pass in Node.js v4 and v5. It should pass in Node.js v6
13+
// after https://github.com/nodejs/node/pull/5950 has been reverted.
14+
15+
const common = require('../common');
16+
const assert = require('assert');
17+
const path = require('path');
18+
const fs = require('fs');
19+
20+
// {tmpDir}
21+
// ├── index.js
22+
// └── node_modules
23+
// ├── moduleA
24+
// │ ├── index.js
25+
// │ └── node_modules
26+
// │ └── moduleB -> {tmpDir}/node_modules/moduleB
27+
// └── moduleB
28+
// ├── index.js
29+
// └── node_modules
30+
// └── moduleA -> {tmpDir}/node_modules/moduleA
31+
32+
common.refreshTmpDir();
33+
const tmpDir = common.tmpDir;
34+
35+
const node_modules = path.join(tmpDir, 'node_modules');
36+
const moduleA = path.join(node_modules, 'moduleA');
37+
const moduleB = path.join(node_modules, 'moduleB');
38+
const moduleA_link = path.join(moduleB, 'node_modules', 'moduleA');
39+
const moduleB_link = path.join(moduleA, 'node_modules', 'moduleB');
40+
41+
fs.mkdirSync(node_modules);
42+
fs.mkdirSync(moduleA);
43+
fs.mkdirSync(moduleB);
44+
fs.mkdirSync(path.join(moduleA, 'node_modules'));
45+
fs.mkdirSync(path.join(moduleB, 'node_modules'));
46+
47+
try {
48+
fs.symlinkSync(moduleA, moduleA_link);
49+
fs.symlinkSync(moduleB, moduleB_link);
50+
} catch (err) {
51+
if (err.code !== 'EPERM') throw err;
52+
common.skip('insufficient privileges for symlinks');
53+
return;
54+
}
55+
56+
fs.writeFileSync(path.join(tmpDir, 'index.js'),
57+
'module.exports = require(\'moduleA\');', 'utf8');
58+
fs.writeFileSync(path.join(moduleA, 'index.js'),
59+
'module.exports = {b: require(\'moduleB\')};', 'utf8');
60+
fs.writeFileSync(path.join(moduleB, 'index.js'),
61+
'module.exports = {a: require(\'moduleA\')};', 'utf8');
62+
63+
// Ensure that the symlinks are not followed forever...
64+
const obj = require(path.join(tmpDir, 'index'));
65+
assert.ok(obj);
66+
assert.ok(obj.b);
67+
assert.ok(obj.b.a);
68+
assert.ok(!obj.b.a.b);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Flags: --preserve-symlinks
2+
'use strict';
3+
// Refs: https://github.com/nodejs/node/pull/5950
4+
5+
// This test verifies that symlinked modules are able to find their peer
6+
// dependencies when using the --preserve-symlinks command line flag.
7+
8+
// This test passes in v6.2+ with --preserve-symlinks on and in v6.0/v6.1.
9+
// This test will fail in Node.js v4 and v5 and should not be backported.
10+
11+
const common = require('../common');
12+
const fs = require('fs');
13+
const path = require('path');
14+
const assert = require('assert');
15+
16+
common.refreshTmpDir();
17+
18+
const tmpDir = common.tmpDir;
19+
20+
// Creates the following structure
21+
// {tmpDir}
22+
// ├── app
23+
// │ ├── index.js
24+
// │ └── node_modules
25+
// │ ├── moduleA -> {tmpDir}/moduleA
26+
// │ └── moduleB
27+
// │ ├── index.js
28+
// │ └── package.json
29+
// └── moduleA
30+
// ├── index.js
31+
// └── package.json
32+
33+
const moduleA = path.join(tmpDir, 'moduleA');
34+
const app = path.join(tmpDir, 'app');
35+
const moduleB = path.join(app, 'node_modules', 'moduleB');
36+
const moduleA_link = path.join(app, 'node_modules', 'moduleA');
37+
fs.mkdirSync(moduleA);
38+
fs.mkdirSync(app);
39+
fs.mkdirSync(path.join(app, 'node_modules'));
40+
fs.mkdirSync(moduleB);
41+
42+
// Attempt to make the symlink. If this fails due to lack of sufficient
43+
// permissions, the test will bail out and be skipped.
44+
try {
45+
fs.symlinkSync(moduleA, moduleA_link);
46+
} catch (err) {
47+
if (err.code !== 'EPERM') throw err;
48+
common.skip('insufficient privileges for symlinks');
49+
return;
50+
}
51+
52+
fs.writeFileSync(path.join(moduleA, 'package.json'),
53+
JSON.stringify({name: 'moduleA', main: 'index.js'}), 'utf8');
54+
fs.writeFileSync(path.join(moduleA, 'index.js'),
55+
'module.exports = require(\'moduleB\');', 'utf8');
56+
fs.writeFileSync(path.join(app, 'index.js'),
57+
'\'use strict\'; require(\'moduleA\');', 'utf8');
58+
fs.writeFileSync(path.join(moduleB, 'package.json'),
59+
JSON.stringify({name: 'moduleB', main: 'index.js'}), 'utf8');
60+
fs.writeFileSync(path.join(moduleB, 'index.js'),
61+
'module.exports = 1;', 'utf8');
62+
63+
assert.doesNotThrow(() => {
64+
require(path.join(app, 'index'));
65+
});

test/parallel/test-require-symlink.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Flags: --preserve-symlinks
12
'use strict';
23
const common = require('../common');
34
const assert = require('assert');
@@ -49,7 +50,7 @@ function test() {
4950

5051
// load symlinked-script as main
5152
var node = process.execPath;
52-
var child = spawn(node, [linkScript]);
53+
var child = spawn(node, ['--preserve-symlinks', linkScript]);
5354
child.on('close', function(code, signal) {
5455
assert(!code);
5556
assert(!signal);

0 commit comments

Comments
 (0)