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

Ability to store a built package in offline mirror #5314

Merged
merged 9 commits into from
Feb 22, 2018

Conversation

bestander
Copy link
Member

@bestander bestander commented Feb 3, 2018

Summary

Packages with install scripts will be saved into a .tgz file into offline mirror if experimental-pack-script-packages-in-mirror is set in .yarnrc.

Next time you run yarn install instead of unpacking the original .tgz package and running scripts yarn unpacks the previously built one and does not run any scripts.

Good for CI systems, slow machines and when you don't trust packages to do run shady download scripts.

Of course it adds platform specific suffixes to the .tgz file name, so that you can safely share the prebuilt offline mirror folder with other OS and Node versions.

Follow up: blog post and docs.

Test plan

Added tests for all common cases: rerunning install, switching Node version etc.

@buildsize
Copy link

buildsize bot commented Feb 3, 2018

This change will increase the build size from 10.46 MB to 10.48 MB, an increase of 22.83 KB (0%)

File name Previous Size New Size Change
yarn-[version].noarch.rpm 906.53 KB 908.33 KB 1.8 KB (0%)
yarn-[version].js 3.94 MB 3.95 MB 8.78 KB (0%)
yarn-legacy-[version].js 4.09 MB 4.09 MB 8.96 KB (0%)
yarn-v[version].tar.gz 911.59 KB 913.6 KB 2.01 KB (0%)
yarn_[version]all.deb 673.41 KB 674.67 KB 1.27 KB (0%)


}

static getPrebuiltName(pkg: Manifest): string {
Copy link
Member Author

Choose a reason for hiding this comment

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

todo: put in a separate file

@@ -145,22 +143,6 @@ test('reading a lockfile should not optimize it', async () => {
});
});

Copy link
Member Author

Choose a reason for hiding this comment

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

moved offline mirror tests out

});
});

test('creates the file in the mirror when fetching a git repository', async () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

following tests are copied from integration.js file

@bestander bestander changed the title [WIP] Ability to store a built package in offline mirror Ability to store a built package in offline mirror Feb 9, 2018
@bestander bestander requested review from BYK and arcanis February 9, 2018 01:29
/* @flow */

export function getPlatformSpecificPackageFilename(pkg: {name: string, version: string}): string {
// TODO support hash for all subdependencies that have installs scripts
Copy link
Member

Choose a reason for hiding this comment

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

This will be problematic, yeah :/ Something I had in mind was that maybe we could implement nested lockfiles to solve this issue. Given a nested lockfile, we could guarantee that a package would always be built against a specific set of dependencies, and we could cache it more efficiently. Wdyt?

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 think nested lockfile is an overkill and may be confusing considering that we don't support nested lockfiles in general.
We might generate something in between - a lock-like file that will only have dependencies section because we don't really need "version" and "resolved" fields as they are already defined in the parent yarn.lock

Copy link
Member Author

Choose a reason for hiding this comment

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

On the other hand having that file inside the .tgz file requires us to unzip it before checking whether the archive works for us, we need to encode all dependencies into the file suffix and hashing them seems a reasonable way to shorten the suffix

Copy link
Member Author

Choose a reason for hiding this comment

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

Anyway, we'll discuss it in V2 of this feature, we need to see how it will be used by the users if it will be :)

// TODO support hash for all subdependencies that have installs scripts
const normaliseScope = name => (name[0] === '@' ? name.substr(1).replace('/', '-') : name);
const suffix = getSystemParams();
return `${normaliseScope(pkg.name)}-v${pkg.version}-${suffix}`;
Copy link
Member

Choose a reason for hiding this comment

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

Nit: normalize

Copy link
Member Author

Choose a reason for hiding this comment

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

done

src/config.js Outdated
@@ -328,6 +331,7 @@ export default class Config {
this.enableMetaFolder = Boolean(this.getOption('enable-meta-folder'));
this.enableLockfileVersions = Boolean(this.getOption('yarn-enable-lockfile-versions'));
this.linkFileDependencies = Boolean(this.getOption('yarn-link-file-dependencies'));
this.packBuiltPackages = Boolean(this.getOption('pack-built-packages'));
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't the option be called experimental-build-mirror or something like this, to clearly state that it's experimental?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, makes sense.
How about experimental-pack-script-packages-in-mirror?

if (this.packageCanBeInstalled(pkg)) {
const offlineMirrorPath = this.config.getOfflineMirrorPath();
if (!offlineMirrorPath) {
break;
Copy link
Member

Choose a reason for hiding this comment

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

If the packBuiltPackages option is set but no offline mirror has been set, the saveBuildArtifacts function won't be called. Is it expected?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point!
Not inteded

},
this.reporter,
);
const stream = await pack(pkgConfig, loc);
Copy link
Member

Choose a reason for hiding this comment

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

Does pack also includes the build artifacts? :o

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 pass loc which from local node_modules with built artifacts, pack just generates a .tgz from a folder

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'll name loc better to be clear

@bestander
Copy link
Member Author

@arcanis, ready for another round


const fs2 = require('fs');
Copy link
Member

Choose a reason for hiding this comment

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

I find that a bit confusing - maybe createWriteStream should be simply reexported from util/fs (or maybe we should use fs-extra which takes care of exporting promises when we need them).

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, fs2 is a poor choice for naming this variable.
Let's do it like in other places the Node.js fs module should be fs but out promise wrappers should be named fsUtil.

A year ago there was a proposal to reexport all exports from Node.js fs module there but we decided not to do it because it means we would have to manually list all functions of Node.js fs. Could be just extra trouble to worry when Node.js introduces a new function or breaks API

Copy link
Member

Choose a reason for hiding this comment

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

fs-extra automatically exports all the fs Node functions in such a way that they automatically support promises. Using it would allow us to let someone else care about keeping the list updated! :) Ok, not urgent, let's keep this for later

});
pkg.prebuiltVariants = pkg.prebuiltVariants || {};
pkg.prebuiltVariants[prebuiltFilename] = hash;
// this.artifacts[`${pkg.name}@${pkg.version}`] = [prebuiltFilename];
Copy link
Member

Choose a reason for hiding this comment

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

Can we remove this line if it isn't used?

Copy link
Member Author

Choose a reason for hiding this comment

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

Doh, debugging code

const offlineMirrorPath = this.config.getOfflineMirrorPath();
if (prebuiltVariants[prebuiltName] && offlineMirrorPath) {
const filename = path.join(offlineMirrorPath, 'prebuilt', prebuiltName + '.tgz');
if (shrunk._remote && (await fs.exists(filename))) {
Copy link
Member

Choose a reason for hiding this comment

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

Fwiw the Node documentation recommends using fs.existsSync instead of fs.exists to prevent race condition as much as possible (fs.exists is actually deprecated).

Copy link
Member Author

Choose a reason for hiding this comment

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

We have 59 places where this wrapper is used.
If we migrate away from it we should do it in a separate PR, right?

for (const pkg of pkgs) {
if (this.packageCanBeInstalled(pkg)) {
let prebuiltPath = path.join(offlineMirrorPath, 'prebuilt');
if (!await fs.exists(prebuiltPath)) {
Copy link
Member

Choose a reason for hiding this comment

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

We don't have to check it exists since we're using mkdirp which will do it for us, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, makes sense, fixed

@bestander
Copy link
Member Author

@arcanis, ready for another round

Copy link
Member

@arcanis arcanis left a comment

Choose a reason for hiding this comment

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

LGTM! Can't wait to see how people will use it!

@bestander bestander merged commit eab88b8 into yarnpkg:master Feb 22, 2018
@hoodie
Copy link

hoodie commented Feb 27, 2018

Question: does this depend on the engine being used? because some dependencies need to be rebuilt if installed for node or electron

@bestander
Copy link
Member Author

@hoodie, currently platform, architecture and modules (Node libs version) are taken into account
https://github.com/yarnpkg/yarn/blob/master/src/util/package-name-utils.js#L12

Feel free to send a PR to add another parameter.

@hoodie
Copy link

hoodie commented Mar 4, 2018

@bestander that seems to be thorough enough, thank you :D

@lxe
Copy link

lxe commented Mar 8, 2018

I absolutely love this feature. This adds a "supported" way to bring hermetic builds across platforms and environments.

@tommilligan-plutoflume
Copy link

For those wishing to enable this feature from a clean yarn installation, simply to speed up local development rebuilds - add the following in your project root:

.yarnrc

yarn-offline-mirror "./yarn-offline-cache"
yarn-offline-mirror-pruning true
experimental-pack-script-packages-in-mirror true

.gitignore

/yarn-offline-cache

Loving this feature already!

@bbrzoska
Copy link

We're having issues with this feature, as it saves references to prebuilt packages to yarn.lock and those change, I think based on the architecture of the computer (?). It caused yarn installs to fail in some scenarios for us, but we didn't had time to debug fully, and thus had to disable it.

@theandrewlane
Copy link
Contributor

theandrewlane commented Mar 28, 2018

@bestander: Looks like yarn-offline-mirror-pruning true deletes./<yarn-offline-cache>/prebuilt/*, is this working as designed?

@bestander
Copy link
Member Author

bestander commented Mar 31, 2018 via email

@theandrewlane
Copy link
Contributor

@bestander - if you can confirm that the yarn-offline-mirror-pruning true config option is deleting the contents of /prebuilt for you, and shouldn't be, i'll submit a PR :)

@bestander
Copy link
Member Author

Please do and thank you!

@mikaello
Copy link

mikaello commented Apr 4, 2018

@theandrewlane I have the same problem, that yarn-offline-mirror-pruning true removes the content of /prebuilt

@theandrewlane
Copy link
Contributor

theandrewlane commented Apr 4, 2018

@mikaello: Cool, thanks for verifying! Waiting on #5616 merge 😎

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.

8 participants