Skip to content
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

module: add --preserve-symlinks command line flag #6537

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

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)?

Copy link

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:

"scripts": {
    "coverage": "node scripts/coverage.js",
    "install": "node scripts/install.js",
    "postinstall": "node scripts/build.js",
    "pretest": "node_modules/.bin/jshint bin lib scripts test",
    "test": "node_modules/.bin/mocha test",
}

The flag will not be automatically passed to mocha whenever we run npm test.


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
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the pre-6.0 If moduleA requires moduleB to work, it will fail with the symbolic link consequently, regardless whether the "app/index.js" was the entry point or "moduleA" got imported from somewhere else.

With --preserve-symlinks the inclusion under "app/index.js" will start to work, the other one fails as it did before (and it should).

In some complex scenario this could lead to a serious head-scratching why this happens this way.


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).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's almost certainly been discussed before but is there a - fundamental - reason we couldn't identify .node files by their (st_dev, st_ino) tuple?

(I say 'fundamental' because I know st_dev is not always set on Windows, but that's something we can work around.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of any reasons off the top of my head but this would need to be done in a separate PR if at all

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good at see and understand how git handles this issue. It uses some matrix of factor to decide if some directory entry stayed the same or changed. This was also the source of the problems when using git on Windows. It can be even more problematic on SMB shares or maybe NFS.


### `--track-heap-objects`

Track heap object allocations for heap snapshots.
Expand Down Expand Up @@ -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[,…]`
Expand Down
5 changes: 5 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto


.TP
.BR \-\-track\-heap-objects
Track heap object allocations for heap snapshots.
Expand Down
14 changes: 8 additions & 6 deletions lib/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff could be made smaller if you write this as if (isMain && !preserveSymlinks) {.

Copy link
Member Author

@jasnell jasnell May 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I know. I switched it to this purposefully because it flows a bit more
logically. Specifically, we only care about the isMain check if preserve
symlinks is on. If it's off, there's no reason to check 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
Expand Down Expand Up @@ -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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially this change (as was mine #6536) was about reverting #5950. Now this commit just builds on #5950 which was wrong.

In this diff one is under impression that isMain has always been here, but it wasn't - it was added by #5950 which didn't work out.

It would be better (for the sake of a cleaner history) to start clean with a new solution.

Using isMain introduces different behaviour for the node called from the command line and whenever things are included as a module. For example, why behavior of node invoked as the "install" or "postinstall" script from the "package.json" should be different when the module was included?

When troubleshooting a module load, a simple node -e 'require("module");' may behave differently as ifrequire("module");` got invoked from some other module. Some modules that are used standalone can be also included in the test modules, in this case the behaviour here will be different. This can be even more frustrating than the symbolic link behaviour contested in #3402.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR accurately reflect last weeks CTC decision to use the pre-v6 behavior as the default but to preserve the new 5950 behavior behind a flag. It intentionally leaves the 5950 changes in place but puts them behind the --preserve-symlinks flag.

filename = path.resolve(basePath);
} else {
filename = fs.realpathSync(basePath);
Expand Down
9 changes: 9 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ bool force_fips_crypto = false;
bool no_process_warnings = false;
bool trace_warnings = false;

// Set in node.cc by ParseArgs when --preserve-symlinks is used.
// Used in node_config.cc to set a constant on process.binding('config')
// that is used by lib/module.js
bool config_preserve_symlinks = false;

// process-relative uptime base, initialized at start-up
static double prog_start_time;
static bool debugger_running;
Expand Down Expand Up @@ -3460,6 +3465,8 @@ static void PrintHelp() {
" note: linked-in ICU data is\n"
" present.\n"
#endif
" --preserve-symlinks preserve symbolic links when resolving\n"
" and caching modules.\n"
#endif
"\n"
"Environment variables:\n"
Expand Down Expand Up @@ -3589,6 +3596,8 @@ static void ParseArgs(int* argc,
} else if (strncmp(arg, "--security-revert=", 18) == 0) {
const char* cve = arg + 18;
Revert(cve);
} else if (strcmp(arg, "--preserve-symlinks") == 0) {
config_preserve_symlinks = true;
} else if (strcmp(arg, "--prof-process") == 0) {
prof_process = true;
short_circuit = true;
Expand Down
5 changes: 4 additions & 1 deletion src/node_config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ void InitConfig(Local<Object> target,
if (flag_icu_data_dir)
READONLY_BOOLEAN_PROPERTY("usingICUDataDir");
#endif // NODE_HAVE_I18N_SUPPORT
}

if (config_preserve_symlinks)
READONLY_BOOLEAN_PROPERTY("preserveSymlinks");
} // InitConfig

} // namespace node

Expand Down
5 changes: 5 additions & 0 deletions src/node_internals.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ struct sockaddr;

namespace node {

// Set in node.cc by ParseArgs when --preserve-symlinks is used.
// Used in node_config.cc to set a constant on process.binding('config')
// that is used by lib/module.js
extern bool config_preserve_symlinks;

// Forward declaration
class Environment;

Expand Down
13 changes: 13 additions & 0 deletions test/addons/symlinked-module/binding.cc
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);
8 changes: 8 additions & 0 deletions test/addons/symlinked-module/binding.gyp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.cc' ]
}
]
}
13 changes: 13 additions & 0 deletions test/addons/symlinked-module/submodule.js
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');
};
34 changes: 34 additions & 0 deletions test/addons/symlinked-module/test.js
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));
});
68 changes: 68 additions & 0 deletions test/parallel/test-module-circular-symlinks.js
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);
65 changes: 65 additions & 0 deletions test/parallel/test-module-symlinked-peer-modules.js
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'));
});
3 changes: 2 additions & 1 deletion test/parallel/test-require-symlink.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Flags: --preserve-symlinks
'use strict';
const common = require('../common');
const assert = require('assert');
Expand Down Expand Up @@ -49,7 +50,7 @@ function test() {

// load symlinked-script as main
var node = process.execPath;
var child = spawn(node, [linkScript]);
var child = spawn(node, ['--preserve-symlinks', linkScript]);
child.on('close', function(code, signal) {
assert(!code);
assert(!signal);
Expand Down