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

chore: don't remove optional dependencies in clean-shrinkwrap.js #1551

Merged
merged 1 commit into from
Jul 3, 2017

Conversation

jviotti
Copy link
Contributor

@jviotti jviotti commented Jun 27, 2017

If we include a platform specific optional dependency in the shrinkwrap
file, then npm will insist in installing it even if the platform doesn't
match. As a solution, we figured out we can avoid putting this platform
specific optional dependencies in the npm-shrinkwrap.json file.

In order to do this, we currently have a script called
clean-shrinkwrap.js that runs before any npm shrinkwrap file (its
a preshrinkwrap npm script) that deletes all the platform specific
modules we know about using npm rm.

The problem with this approach is that npm rm will remove the module's
code from node_modules, which means that if we run npm shrinkwrap,
we will lose certain optional dependencies, that may be needed at a
later stage.

The solution is to modify the clean-shrinkwrap.js script to parse
npm-shrinkwrap.json, and manually delete the entries that we want to
omit. Also, the script needs to be run after npm shrinkwrap, so we
change the npm script name to postshrinkwrap.

Signed-off-by: Juan Cruz Viotti [email protected]

@jviotti
Copy link
Contributor Author

jviotti commented Jun 27, 2017

Let's see what the CI servers say. Fingers-crossed!

@jviotti
Copy link
Contributor Author

jviotti commented Jun 27, 2017

YESS, finally everything works \o/

@jviotti jviotti force-pushed the smart-shrinkwrap-traverse branch from 7d79302 to 6132500 Compare June 27, 2017 17:13
@jviotti
Copy link
Contributor Author

jviotti commented Jun 27, 2017

What do you think @jhermsmeier ?

Copy link
Contributor

@jhermsmeier jhermsmeier left a comment

Choose a reason for hiding this comment

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

I haven't parsed this fully yet, but looks good to me so far, just a few nits.

], getDependencyTree(shrinkwrapFile, dependencyPath)));
}, []);

const JSON_INDENTATION_SPACES = 2;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we declare this at the very top?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

* @returns {Object} shrinkwrap object
*
* @example
* const object = getShrinkwrapDependencyObject(require('npm-shrinkwrap.json'), [
Copy link
Contributor

Choose a reason for hiding this comment

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

The required path needs a ./ to understand that it's a local path afaik – might be good to have that in the example as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed!

* @example
* const dependencies = getTopLevelDependenciesFromShrinkwrapObject([ 'debug' ]);
*/
const getTopLevelDependenciesFromShrinkwrapObject = (shrinkwrapPath) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is starting to feel like I'm reading Java 😝

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, its JavaScript after all :P

Copy link
Contributor

Choose a reason for hiding this comment

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

From what I can tell, would getTopLevelDependenciesForShrinkwrapPath be a better name? *shrug*

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

"platformSpecificDependencies": [
[ "7zip-bin-mac" ],
[ "7zip-bin-win" ],
[ "7zip-bin-linux" ]
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the reasoning behind the nested arrays here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The new approach works based on shrinkwrap "paths", which we set using array syntax. Basically this means that we want to omit 7zip-bin-mac from the top level of the shrinkwrap file for example. If 7zip-bin-mac was present inside foo in the shrinkwrap file, we should add [ 'foo', '7zip-bin-mac' ].

@jviotti jviotti force-pushed the smart-shrinkwrap-traverse branch from 6132500 to 2a000ae Compare June 27, 2017 22:05
* @returns {Object} shrinkwrap object
*
* @example
* const object = getPrettyShrinkwrapDependencyObject(require('npm-shrinkwrap.json'), [
Copy link
Contributor

Choose a reason for hiding this comment

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

Another relative path missing in the example here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed!

@jhermsmeier
Copy link
Contributor

There's also a build failure on Windows:

[...]
mkdir dist/Etcher-cli-1.0.0+2a000ae-win32-x86-app
cp package.json dist/Etcher-cli-1.0.0+2a000ae-win32-x86-app
cp -RLf lib dist/Etcher-cli-1.0.0+2a000ae-win32-x86-app
cp -RLf dist/node-win32-x86-dependencies/* dist/Etcher-cli-1.0.0+2a000ae-win32-x86-app
cp: cannot copy cyclic symbolic link `dist/node-win32-x86-dependencies/node_modules/ajv/node_modules'
make: *** [dist/Etcher-cli-1.0.0+2a000ae-win32-x86-app] Error 1
Command exited with code 2

Is this a transient error, that only occasionally occurs?

@lurch
Copy link
Contributor

lurch commented Jun 28, 2017

cp: cannot copy cyclic symbolic link
Is this a transient error, that only occasionally occurs?

Yeah, it keeps popping up now and then, but only on Appveyor, and we've no idea why :-( When you restart the build (which I've just done), it usually passes on the second attempt.
(and annoyingly, unlike on Travis CI where you can restart just one arch-specific build, on Appveyor you have to restart all arch builds)

Copy link
Contributor

@lurch lurch left a comment

Choose a reason for hiding this comment

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

With all the hardcore hacking @jviotti has been doing on npm-shrinkwrap.json these past few months, I hope he doesn't get poached by the NPM project :-D

* const object = getShrinkwrapDependencyObject(require('./npm-shrinkwrap.json'), [
* 'drivelist',
* 'lodash'
* ]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should these examples also show what the returned object might contain?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I'll add some more example code.

* @private
*
* @description
* This function transforms the output of `getShrinkwrapDependencyObject()` to
Copy link
Contributor

Choose a reason for hiding this comment

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

minor nit: could you change transforms the output of to wraps ? The current description makes it sounds like getPrettyShrinkwrapDependencyObject takes the output of getShrinkwrapDependencyObject as an input parameter, which it doesn't.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed!

*
* @param {Object} shrinkwrap - the shrinkwrap file contents
* @param {String[]} shrinkwrapPath - path to shrinkwrap dependency
* @returns {Object} shrinkwrap object
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps this should be @returns {Object} pretty shrinkwrap object?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

* @private
*
* @param {String[]} shrinkwrapPath - path to shrinkwrap dependency
* @returns {Object} dependency manifest
Copy link
Contributor

Choose a reason for hiding this comment

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

I've got no idea what a dependency manifest is - an example might be great ;)

EDIT: Ah, now I see that it's the package.json for that dependency :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Haha, I clarified it anyways.

.value();

try {
return require(`.${path.sep}${manifestPath}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

And an inline example comment of what this manifestPath might look like would be useful too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

* @example
* const shrinkwrapFile = require('./npm-shrinkwrap.json');
* const dependencyTree = getDependencyTree(shrinkwrapFile, [ 'drivelist' ]);
* const filteredShrinkwrap = removeDependencies(shrinkwrapFile, dependencyTree, {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this example is wrong, because getDependencyTree returns Object[] but the blacklist parameter here is String[] ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops, the type declaration was indeed wrong. Thanks for the heads up!

return _.compact(_.concat(accumulator, [
getPrettyShrinkwrapDependencyObject(shrinkwrapFile, dependencyPath)
], getDependencyTree(shrinkwrapFile, dependencyPath)));
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this chunk of code be moved into e.g. a buildBlacklistFromDependencyTree function or something?
I'm afraid it looks pretty impenetrable to a non-Lodash-expert ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Haha, what this does is basically taking every dependency in the black list, and for each of them, get an array of its dependencies + the dependency itself (hence _.concat). We then flatten the whole thing as we go by using a fold.

I guess we can rename the internal bit to something like "get dependency tree AND the dependency itself". Maybe that could be just getTree()?

*
* @param {Object} shrinkwrap - the shrinkwrap file contents
* @param {String[]} blacklist - dependency blacklist
* @param {Object} qualifiers - qualifiers
Copy link
Contributor

Choose a reason for hiding this comment

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

Given the single-purpose nature of this script, I wonder if it'd be worth renaming removeDependencies to removeOptionalDevDependencies and then getting rid of the qualifiers parameter? *shrug*

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, done!

}), null, JSON_INDENTATION_SPACES);

fs.writeFileSync(NPM_SHRINKWRAP_FILE_PATH, `${result}\n`);
console.log('Done');
Copy link
Contributor

Choose a reason for hiding this comment

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

console.log('Done, thank f***!'); 😄

* level object, remove the blacklisted dependencies, and recurse
* if possible.
*
* @param {Object} shrinkwrap - the shrinkwrap file contents
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this parameter description need to be updated to reflect the recursive nature of this function? (or is that an internal-only detail?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

@jviotti
Copy link
Contributor Author

jviotti commented Jun 28, 2017

Yeah, it keeps popping up now and then, but only on Appveyor, and we've no idea why :-( When you restart the build (which I've just done), it usually passes on the second attempt.
(and annoyingly, unlike on Travis CI where you can restart just one arch-specific build, on Appveyor you have to restart all arch builds)

It looks like a MinGW bug. Moving stuff using Node.js file-system works fine. We only use this for the CLI generation, which we will move to pkg, so it should be gone soon.

@jviotti jviotti force-pushed the smart-shrinkwrap-traverse branch from 2a000ae to adac8c2 Compare June 28, 2017 15:26
@lurch
Copy link
Contributor

lurch commented Jun 29, 2017

./node_modules/.bin/npx eslint lib tests scripts bin versionist.conf.js
C:\projects\etcher\scripts\clean-shrinkwrap.js
  232:16  error  'removeDependencies' is not defined  no-undef
✖ 1 problem (1 error, 0 warnings)

@jviotti jviotti force-pushed the smart-shrinkwrap-traverse branch from adac8c2 to 0d9bad5 Compare June 29, 2017 13:57
@jviotti
Copy link
Contributor Author

jviotti commented Jun 29, 2017

Fixed!

@lurch
Copy link
Contributor

lurch commented Jun 29, 2017

> node ./scripts/clean-shrinkwrap.js
Removing: 7zip-bin-mac, 7zip-bin-win, 7zip-bin-linux
Done
There are unstaged npm-shrinkwrap.json changes. Please commit the result of:
    npm shrinkwrap --dev
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index 2c2240b..44d9863 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -53,6 +53,13 @@
       "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-2.1.0.tgz",
       "dev": true
     },
+    "7zip-bin-win": {
+      "version": "2.1.0",
+      "from": "7zip-bin-win@>=2.1.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/7zip-bin-win/-/7zip-bin-win-2.1.0.tgz",
+      "dev": true,
+      "optional": true
+    },
     "abbrev": {
       "version": "1.1.0",
       "from": "abbrev@>=1.0.0 <2.0.0",
warning: LF will be replaced by CRLF in npm-shrinkwrap.json.
The file will have its original line endings in your working directory.
make: *** [sanity-checks] Error 1
Command exited with code 2
> node ./scripts/clean-shrinkwrap.js
Removing: 7zip-bin-mac, 7zip-bin-win, 7zip-bin-linux
Done
There are unstaged npm-shrinkwrap.json changes. Please commit the result of:
    npm shrinkwrap --dev
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index 2c2240b..dc1282c 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -53,6 +53,13 @@
       "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-2.1.0.tgz",
       "dev": true
     },
+    "7zip-bin-linux": {
+      "version": "1.1.0",
+      "from": "7zip-bin-linux@>=1.1.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/7zip-bin-linux/-/7zip-bin-linux-1.1.0.tgz",
+      "dev": true,
+      "optional": true
+    },
     "abbrev": {
       "version": "1.1.0",
       "from": "abbrev@>=1.0.0 <2.0.0",
make: *** [sanity-checks] Error 1
The command "./scripts/ci/test.sh -o $HOST_OS -r $TARGET_ARCH" exited with 2.

😢


// For example
// ./node_modules/drivelist/node_modules/package.json
return require(`.${path.sep}${manifestPath}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not return require(path.join('.', manifestPath)); ?

const path = require('path');
const os = require('os');
const packageJSON = require('../package.json');
Copy link
Contributor

Choose a reason for hiding this comment

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

As (I think) I asked before, why use '../package.json' here, but then path.join(__dirname, '..', 'npm-shrinkwrap.json') a few lines below?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We only need to require the package.json, however we need to shrinkwrap path in two places: a require call and the output of fs.writeFile, so better to extract it as a constant to avoid path issues (require() has special logic to understand ./ on Windows, but its not safe to assume that for fs.writeFile as well)

Copy link
Contributor

Choose a reason for hiding this comment

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

but its not safe to assume that for fs.writeFile as well)

Ahh, that's the bit I was missing, thanks. So require definitely works with both slash-types on Windows?

* @example
* const shrinkwrapFile = require('./npm-shrinkwrap.json');
* const dependencyTree = getDependencyTree(shrinkwrapFile, [ 'drivelist' ]);
* const filteredShrinkwrap = removeDependencies(shrinkwrapFile, dependencyTree);
Copy link
Contributor

Choose a reason for hiding this comment

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

function-name in the example needs updating ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

Copy link
Contributor

Choose a reason for hiding this comment

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

Really? The example still says removeDependencies whereas the function is named removeOptionalDevelopmentDependencies

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I forgot to push 😆

try {

// For example
// ./node_modules/drivelist/node_modules/package.json
Copy link
Contributor

Choose a reason for hiding this comment

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

Umm, should this be ./node_modules/drivelist/package.json ? Or maybe ./node_modules/drivelist/node_modules/lodash/package.json ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, fixed!

@jviotti jviotti force-pushed the smart-shrinkwrap-traverse branch 2 times, most recently from e789a69 to 2cc7b1f Compare June 30, 2017 15:14
@jviotti
Copy link
Contributor Author

jviotti commented Jun 30, 2017

Looks like I made a couple of subtle issues while addressing the review comments. It should work now.

If we include a platform specific optional dependency in the shrinkwrap
file, then npm will insist in installing it even if the platform doesn't
match. As a solution, we figured out we can avoid putting this platform
specific optional dependencies in the npm-shrinkwrap.json file.

In order to do this, we currently have a script called
`clean-shrinkwrap.js` that runs *before* any `npm shrinkwrap` file (its
a `preshrinkwrap` npm script) that deletes all the platform specific
modules we know about using `npm rm`.

The problem with this approach is that `npm rm` will remove the module's
code from `node_modules`, which means that if we run `npm shrinkwrap`,
we will lose certain optional dependencies, that may be needed at a
later stage.

The solution is to modify the `clean-shrinkwrap.js` script to parse
`npm-shrinkwrap.json`, and manually delete the entries that we want to
omit. Also, the script needs to be run *after* `npm shrinkwrap`, so we
change the npm script name to `postshrinkwrap`.

Signed-off-by: Juan Cruz Viotti <[email protected]>

// For example
// ./node_modules/drivelist/node_modules/lodash/package.json
return require(`.${path.sep}${manifestPath}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

Given your comment about require in a comment just now, does this need to be .${path.sep} instead of just ./ ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would work. Since manifestPath is generated using path.join, it may include backslashes, so I decided to use path.sep just for consistency purposes (so we don't have paths like ./node_modules\package.json).

Copy link
Contributor

Choose a reason for hiding this comment

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

...so this line could also use path.join for consistency purposes? ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I really wanted to, but:

> path.join('.', 'foo')
'foo'

(path.join omits the period, which require needs to work properly, so we need to preppend path.sep anyway)

Copy link
Contributor

Choose a reason for hiding this comment

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

Cool, thanks for the explanation! 👍

@jviotti jviotti force-pushed the smart-shrinkwrap-traverse branch from 2cc7b1f to 11f8127 Compare June 30, 2017 15:31
@jviotti
Copy link
Contributor Author

jviotti commented Jul 3, 2017

Let's merge this to check if the logs are reduced enough to make Travis CI builds complete.

@jviotti jviotti merged commit 53d8118 into master Jul 3, 2017
@jviotti jviotti deleted the smart-shrinkwrap-traverse branch July 3, 2017 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants