-
-
Notifications
You must be signed in to change notification settings - Fork 34.2k
module: add --preserve-symlinks command line flag #6537
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -97,6 +97,43 @@ Automatically zero-fills all newly allocated [Buffer][] and [SlowBuffer][] | |
| instances. | ||
|
|
||
|
|
||
| ### `--preserve-symlinks` | ||
|
|
||
| Instructs the module loader to preserve symbolic links when resolving and | ||
| caching modules. | ||
|
|
||
| By default, when Node.js loads a module from a path that is symbolically linked | ||
| to a different on-disk location, Node.js will dereference the link and use the | ||
| actual on-disk "real path" of the module as both an identifier and as a root | ||
| path to locate other dependency modules. In most cases, this default behavior | ||
| is acceptable. However, when using symbolically linked peer dependencies, as | ||
| illustrated in the example below, the default behavior causes an exception to | ||
| be thrown if `moduleA` attempts to require `moduleB` as a peer dependency: | ||
|
|
||
| ```text | ||
| {appDir} | ||
| ├── app | ||
| │ ├── index.js | ||
| │ └── node_modules | ||
| │ ├── moduleA -> {appDir}/moduleA | ||
| │ └── moduleB | ||
| │ ├── index.js | ||
| │ └── package.json | ||
| └── moduleA | ||
| ├── index.js | ||
| └── package.json | ||
| ``` | ||
|
||
|
|
||
| The `--preserve-symlinks` command line flag instructs Node.js to use the | ||
| symlink path for modules as opposed to the real path, allowing symbolically | ||
| linked peer dependencies to be found. | ||
|
|
||
| Note, however, that using `--preserve-symlinks` can have other side effects. | ||
| Specifically, symbolically linked *native* modules can fail to load if those | ||
| are linked from more than one location in the dependency tree (Node.js would | ||
| see those as two separate modules and would attempt to load the module multiple | ||
| times, causing an exception to be thrown). | ||
|
||
|
|
||
| ### `--track-heap-objects` | ||
|
|
||
| Track heap object allocations for heap snapshots. | ||
|
|
@@ -138,7 +175,6 @@ Force FIPS-compliant crypto on startup. (Cannot be disabled from script code.) | |
|
|
||
| Specify ICU data load path. (overrides `NODE_ICU_DATA`) | ||
|
|
||
|
|
||
| ## Environment Variables | ||
|
|
||
| ### `NODE_DEBUG=module[,…]` | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -95,6 +95,11 @@ of the event loop. | |
| .BR \-\-zero\-fill\-buffers | ||
| Automatically zero-fills all newly allocated Buffer and SlowBuffer instances. | ||
|
|
||
| .TP | ||
| .BR \-\-preserve\-symlinks | ||
| Instructs the module loader to preserve symbolic links when resolving and | ||
| caching modules. | ||
|
||
|
|
||
| .TP | ||
| .BR \-\-track\-heap-objects | ||
| Track heap object allocations for heap snapshots. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ const fs = require('fs'); | |
| const path = require('path'); | ||
| const internalModuleReadFile = process.binding('fs').internalModuleReadFile; | ||
| const internalModuleStat = process.binding('fs').internalModuleStat; | ||
| const preserveSymlinks = !!process.binding('config').preserveSymlinks; | ||
|
|
||
| // If obj.hasOwnProperty has been overridden, then calling | ||
| // obj.hasOwnProperty(prop) will break. | ||
|
|
@@ -109,14 +110,15 @@ function tryPackage(requestPath, exts, isMain) { | |
| } | ||
|
|
||
| // check if the file exists and is not a directory | ||
| // resolve to the absolute realpath if running main module, | ||
| // otherwise resolve to absolute while keeping symlinks intact. | ||
| // if using --preserve-symlinks and isMain is false, | ||
| // keep symlinks intact, otherwise resolve to the | ||
| // absolute realpath. | ||
| function tryFile(requestPath, isMain) { | ||
| const rc = stat(requestPath); | ||
| if (isMain) { | ||
| return rc === 0 && fs.realpathSync(requestPath); | ||
| if (preserveSymlinks && !isMain) { | ||
|
||
| return rc === 0 && path.resolve(requestPath); | ||
| } | ||
| return rc === 0 && path.resolve(requestPath); | ||
| return rc === 0 && fs.realpathSync(requestPath); | ||
| } | ||
|
|
||
| // given a path check a the file exists with any of the set extensions | ||
|
|
@@ -159,7 +161,7 @@ Module._findPath = function(request, paths, isMain) { | |
| if (!trailingSlash) { | ||
| const rc = stat(basePath); | ||
| if (rc === 0) { // File. | ||
| if (!isMain) { | ||
| if (preserveSymlinks && !isMain) { | ||
|
||
| filename = path.resolve(basePath); | ||
| } else { | ||
| filename = fs.realpathSync(basePath); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| #include <node.h> | ||
| #include <v8.h> | ||
|
|
||
| void Method(const v8::FunctionCallbackInfo<v8::Value>& args) { | ||
| v8::Isolate* isolate = args.GetIsolate(); | ||
| args.GetReturnValue().Set(v8::String::NewFromUtf8(isolate, "world")); | ||
| } | ||
|
|
||
| void init(v8::Local<v8::Object> target) { | ||
| NODE_SET_METHOD(target, "hello", Method); | ||
| } | ||
|
|
||
| NODE_MODULE(binding, init); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| 'targets': [ | ||
| { | ||
| 'target_name': 'binding', | ||
| 'sources': [ 'binding.cc' ] | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| 'use strict'; | ||
| require('../../common'); | ||
| const path = require('path'); | ||
| const assert = require('assert'); | ||
|
|
||
| // This is a subtest of symlinked-module/test.js. This is not | ||
| // intended to be run directly. | ||
|
|
||
| module.exports.test = function test(bindingDir) { | ||
| const mod = require(path.join(bindingDir, 'binding.node')); | ||
| assert.notStrictEqual(mod, null); | ||
| assert.strictEqual(mod.hello(), 'world'); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| 'use strict'; | ||
| const common = require('../../common'); | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const assert = require('assert'); | ||
|
|
||
| // This test verifies that symlinked native modules can be required multiple | ||
| // times without error. The symlinked module and the non-symlinked module | ||
| // should be the same instance. This expectation was not previously being | ||
| // tested and ended up being broken by https://github.com/nodejs/node/pull/5950. | ||
|
|
||
| // This test should pass in Node.js v4 and v5. This test will pass in Node.js | ||
| // with https://github.com/nodejs/node/pull/5950 reverted. | ||
|
|
||
| common.refreshTmpDir(); | ||
|
|
||
| const addonPath = path.join(__dirname, 'build', 'Release'); | ||
| const addonLink = path.join(common.tmpDir, 'addon'); | ||
|
|
||
| try { | ||
| fs.symlinkSync(addonPath, addonLink); | ||
| } catch (err) { | ||
| if (err.code !== 'EPERM') throw err; | ||
| common.skip('module identity test (no privs for symlinks)'); | ||
| return; | ||
| } | ||
|
|
||
| const sub = require('./submodule'); | ||
| [addonPath, addonLink].forEach((i) => { | ||
| const mod = require(path.join(i, 'binding.node')); | ||
| assert.notStrictEqual(mod, null); | ||
| assert.strictEqual(mod.hello(), 'world'); | ||
| assert.doesNotThrow(() => sub.test(i)); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| 'use strict'; | ||
|
|
||
| // This tests to make sure that modules with symlinked circular dependencies | ||
| // do not blow out the module cache and recurse forever. See issue | ||
| // https://github.com/nodejs/node/pull/5950 for context. PR #5950 attempted | ||
| // to solve a problem with symlinked peer dependencies by caching using the | ||
| // symlink path. Unfortunately, that breaks the case tested in this module | ||
| // because each symlinked module, despite pointing to the same code on disk, | ||
| // is loaded and cached as a separate module instance, which blows up the | ||
| // cache and leads to a recursion bug. | ||
|
|
||
| // This test should pass in Node.js v4 and v5. It should pass in Node.js v6 | ||
| // after https://github.com/nodejs/node/pull/5950 has been reverted. | ||
|
|
||
| const common = require('../common'); | ||
| const assert = require('assert'); | ||
| const path = require('path'); | ||
| const fs = require('fs'); | ||
|
|
||
| // {tmpDir} | ||
| // ├── index.js | ||
| // └── node_modules | ||
| // ├── moduleA | ||
| // │ ├── index.js | ||
| // │ └── node_modules | ||
| // │ └── moduleB -> {tmpDir}/node_modules/moduleB | ||
| // └── moduleB | ||
| // ├── index.js | ||
| // └── node_modules | ||
| // └── moduleA -> {tmpDir}/node_modules/moduleA | ||
|
|
||
| common.refreshTmpDir(); | ||
| const tmpDir = common.tmpDir; | ||
|
|
||
| const node_modules = path.join(tmpDir, 'node_modules'); | ||
| const moduleA = path.join(node_modules, 'moduleA'); | ||
| const moduleB = path.join(node_modules, 'moduleB'); | ||
| const moduleA_link = path.join(moduleB, 'node_modules', 'moduleA'); | ||
| const moduleB_link = path.join(moduleA, 'node_modules', 'moduleB'); | ||
|
|
||
| fs.mkdirSync(node_modules); | ||
| fs.mkdirSync(moduleA); | ||
| fs.mkdirSync(moduleB); | ||
| fs.mkdirSync(path.join(moduleA, 'node_modules')); | ||
| fs.mkdirSync(path.join(moduleB, 'node_modules')); | ||
|
|
||
| try { | ||
| fs.symlinkSync(moduleA, moduleA_link); | ||
| fs.symlinkSync(moduleB, moduleB_link); | ||
| } catch (err) { | ||
| if (err.code !== 'EPERM') throw err; | ||
| common.skip('insufficient privileges for symlinks'); | ||
| return; | ||
| } | ||
|
|
||
| fs.writeFileSync(path.join(tmpDir, 'index.js'), | ||
| 'module.exports = require(\'moduleA\');', 'utf8'); | ||
| fs.writeFileSync(path.join(moduleA, 'index.js'), | ||
| 'module.exports = {b: require(\'moduleB\')};', 'utf8'); | ||
| fs.writeFileSync(path.join(moduleB, 'index.js'), | ||
| 'module.exports = {a: require(\'moduleA\')};', 'utf8'); | ||
|
|
||
| // Ensure that the symlinks are not followed forever... | ||
| const obj = require(path.join(tmpDir, 'index')); | ||
| assert.ok(obj); | ||
| assert.ok(obj.b); | ||
| assert.ok(obj.b.a); | ||
| assert.ok(!obj.b.a.b); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| // Flags: --preserve-symlinks | ||
| 'use strict'; | ||
| // Refs: https://github.com/nodejs/node/pull/5950 | ||
|
|
||
| // This test verifies that symlinked modules are able to find their peer | ||
| // dependencies when using the --preserve-symlinks command line flag. | ||
|
|
||
| // This test passes in v6.2+ with --preserve-symlinks on and in v6.0/v6.1. | ||
| // This test will fail in Node.js v4 and v5 and should not be backported. | ||
|
|
||
| const common = require('../common'); | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const assert = require('assert'); | ||
|
|
||
| common.refreshTmpDir(); | ||
|
|
||
| const tmpDir = common.tmpDir; | ||
|
|
||
| // Creates the following structure | ||
| // {tmpDir} | ||
| // ├── app | ||
| // │ ├── index.js | ||
| // │ └── node_modules | ||
| // │ ├── moduleA -> {tmpDir}/moduleA | ||
| // │ └── moduleB | ||
| // │ ├── index.js | ||
| // │ └── package.json | ||
| // └── moduleA | ||
| // ├── index.js | ||
| // └── package.json | ||
|
|
||
| const moduleA = path.join(tmpDir, 'moduleA'); | ||
| const app = path.join(tmpDir, 'app'); | ||
| const moduleB = path.join(app, 'node_modules', 'moduleB'); | ||
| const moduleA_link = path.join(app, 'node_modules', 'moduleA'); | ||
| fs.mkdirSync(moduleA); | ||
| fs.mkdirSync(app); | ||
| fs.mkdirSync(path.join(app, 'node_modules')); | ||
| fs.mkdirSync(moduleB); | ||
|
|
||
| // Attempt to make the symlink. If this fails due to lack of sufficient | ||
| // permissions, the test will bail out and be skipped. | ||
| try { | ||
| fs.symlinkSync(moduleA, moduleA_link); | ||
| } catch (err) { | ||
| if (err.code !== 'EPERM') throw err; | ||
| common.skip('insufficient privileges for symlinks'); | ||
| return; | ||
| } | ||
|
|
||
| fs.writeFileSync(path.join(moduleA, 'package.json'), | ||
| JSON.stringify({name: 'moduleA', main: 'index.js'}), 'utf8'); | ||
| fs.writeFileSync(path.join(moduleA, 'index.js'), | ||
| 'module.exports = require(\'moduleB\');', 'utf8'); | ||
| fs.writeFileSync(path.join(app, 'index.js'), | ||
| '\'use strict\'; require(\'moduleA\');', 'utf8'); | ||
| fs.writeFileSync(path.join(moduleB, 'package.json'), | ||
| JSON.stringify({name: 'moduleB', main: 'index.js'}), 'utf8'); | ||
| fs.writeFileSync(path.join(moduleB, 'index.js'), | ||
| 'module.exports = 1;', 'utf8'); | ||
|
|
||
| assert.doesNotThrow(() => { | ||
| require(path.join(app, 'index')); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add one or two lines explaining when it makes a difference (e.g.
npm link)?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is a commandline option the right way to solve the #3402 issue? What about node scripts executed from npm's package.json for example? Should npm (or other scripts invoking node as the executable - written in JavaScript, shell and what not) have a special facility to supply this flag to its node children magically? For example, suppose have the folllowing stanza in the package.json:
The flag will not be automatically passed to
mochawhenever we runnpm test.