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

Fully support agoric install <dist-tag> #4129

Merged
merged 4 commits into from
Dec 3, 2021
Merged

Fully support agoric install <dist-tag> #4129

merged 4 commits into from
Dec 3, 2021

Conversation

michaelfig
Copy link
Member

@michaelfig michaelfig commented Nov 26, 2021

closes: #3857

Description

If a dist-tag is specified with agoric install <dist-tag>, ensure we refresh the dapp's yarn.lock dependencies.

This makes it possible to build and develop a published dapp without needing the agoric install command (just yarn install suffices). Indeed, npm install -g agoric or yarn global add agoric should be able to produce a working agoric command that can do everything that yarn link-cli ~/bin/agoric does, once this PR's version of agoric-cli has been published to NPM.

Security Considerations

Documentation Considerations

agoric install beta will update the workspace's yarn.lock with the beta dependencies. From that point on, installing the dapp only needs yarn install (though agoric install will work, too).

Testing Considerations

Tested manually on dapp-oracle.

@michaelfig michaelfig added the agoric-cli package: agoric-cli label Nov 26, 2021
@michaelfig michaelfig requested a review from dckc November 26, 2021 20:44
@michaelfig michaelfig self-assigned this Nov 26, 2021
@michaelfig michaelfig force-pushed the mfig-npm-dist-tag branch 3 times, most recently from 165916e to 3d8ff0f Compare November 26, 2021 21:56
Copy link
Member

@dckc dckc left a comment

Choose a reason for hiding this comment

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

I would need to level up my npm / yarn game to feel confident about this.

Maybe @kriskowal could take a look?

packages/agoric-cli/src/install.js Show resolved Hide resolved
@@ -0,0 +1,47 @@
#! /bin/bash
# dist-tag.sh - manage dist-tags for NPM registry packages 
Copy link
Member

Choose a reason for hiding this comment

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

elaborate a bit on "manage"?

I'm not really fluent in bash. I could use a comment for each clump of lines, actually.

const pj = JSON.parse(packageJSON);
for (const section of ['dependencies', 'devDependencies']) {
const deps = pj[section];
if (deps) {
for (const pkg of Object.keys(deps)) {
if (versions.has(pkg)) {
if (deps[pkg] === forceSdkVersion) {
Copy link
Member

Choose a reason for hiding this comment

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

I would have thought we need to "invalidate the cache" on a version mismatch.

I can't see where deps is initialized in this diff, and when I looked at the whole file, I got a little dizzy. (color me lazy)

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 would have thought we need to "invalidate the cache" on a version mismatch.

Actually, yarn already updates packages that have changed. It's the packages whose version specifier has not changed that we need to first prune the old one, install, add the new one, and install to get it to update correctly.

@dckc
Copy link
Member

dckc commented Nov 26, 2021

Does this depend on #4120?

@michaelfig
Copy link
Member Author

Does this depend on #4120?

Actually, no. It's still valuable even if someone has to manually publish packages and dist-tags all the time.

Copy link
Member

@kriskowal kriskowal left a comment

Choose a reason for hiding this comment

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

Everything in here looks like it does what it says it’s supposed to, but after a couple passes, I’m still fuzzy on why it works. Is the notion that install does a number of shallow installs and revises the package.jsons for the installed packages in-place? Is this recursive?

Would the effect be comparable to amending the dapp package.json to include resolutions for all the agoric-sdk packages using a particular dist-tag?

Comment on lines 78 to 86
const pjson = `${subdir}/package.json`;
const packageJSON = await fs
.readFile(pjson, 'utf-8')
.catch(_ => undefined);
if (!packageJSON) {
return;
}
const pj = JSON.parse(packageJSON);
const prunedPj = { ...pj };
Copy link
Member

Choose a reason for hiding this comment

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

I struggle with naming variables that all refer to the same thing, but in different forms. My principle is that all the names should have some portion that is the same to reflect that they refer to something common, and a term that varies to indicate what’s different about them. So, for something like this, I’ve been using names like packageDescriptorBytes, packageDescriptorText, packageDescriptor, and packageDescriptorPruned. I think Json instead of Descriptor might be valid but not precise. I personally loathe the case convention conundrum raised by acronyms and initialisms, particularly when they run-on. And, I don’t mind some verbosity, even for local variables where the verbosity is unnecessary.

This is a recommendation.

if (needsPruning) {
pruningTodo.push(async () => {
// Update on exit, in case we are interrupted.
process.on('beforeExit', updatePackageJson);
Copy link
Member

Choose a reason for hiding this comment

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

I presume you considered and decided against framing this in terms of a finally. I don’t think it would occur to me to use the beforeExit event under any circumstances.

updatePackageJson returns a promise that the EventEmitter will ignore. That would suggest that any error will be realized as an unhandled rejection warning. I’d consider reframing this to sink the promise.

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 presume you considered and decided against framing this in terms of a finally. I don’t think it would occur to me to use the beforeExit event under any circumstances.

It is framed in terms of a pruningP.finally(...). It's quite important that these updates are done even if the process is interrupted. Do you have a better mechanism at hand?

Copy link
Member

Choose a reason for hiding this comment

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

No, there’s nothing better in the face of uncaught exceptions.

Comment on lines 132 to 164
// After all have settled, try throwing the first rejection.
const firstFailure = results.find(
({ status }) => status !== 'fulfilled',
);
if (firstFailure) {
throw firstFailure.reason;
}
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps consider using AggregateError. That will of course be impossible inside a Compartment for now, but I do not think this is inside a Compartment.

Copy link
Member Author

Choose a reason for hiding this comment

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

AggregateError is not available in Node v14. Do you have a better suggestion?

Copy link
Member

Choose a reason for hiding this comment

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

In the absence of AggregateError, I like to aggregate error messages, e.g.,

new Error(`Cannot x for multiple causes (${causes.map(x => x.message).join('; '))`)

But I won’t press the issue. The first cause is certainly sufficient and indeed all you’d see in a serial causal graph.

);
}
// This open-coding of the above yarn command is quiet and fast.
const linkName = `${linkFolder}/${pjName}`;
Copy link
Member

Choose a reason for hiding this comment

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

How do you feel about path.join? My current personal rule is not hard and fast either way.

Copy link
Member Author

Choose a reason for hiding this comment

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

Unless portability to older versions or Windows failures demand otherwise, I just use slashes and trust that Node.js's fs module figures out the rest.

# Try: `npm-dist-tag.sh` for usage.

# Exit on any errors.
set -e
Copy link
Member

Choose a reason for hiding this comment

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

Please consider cargoing set -ueo pipefail as a matter of style.

@@ -0,0 +1,67 @@
#! /bin/bash
# npm-dist-tag.sh - manage dist-tags for NPM registry packages 
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps add/remove/list is more evocative than manage.

priv=$(jq -r .private package.json)
case "$priv" in
true)
echo 1>&2 "Private package, skipping npm-dist-tag.sh"
Copy link
Member

Choose a reason for hiding this comment

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

Wherein the reviewer relearns that redirects can be interpolated anywhere in a command.

;;
*)
# Usage instructions.
echo 1>&2 "Usage: $0 [lerna] <add|remove|list> [<tag>]"
Copy link
Member

Choose a reason for hiding this comment

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

Is the distinction that lerna means to apply in bulk to the workspaces? Would we ever not do that?

Copy link
Member Author

Choose a reason for hiding this comment

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

Is the distinction that lerna means to apply in bulk to the workspaces? Would we ever not do that?

Yes and yes, if we want to add/remove/list the dist-tags for just the package in the cwd.

}
// Create symlinks to the SDK packages.
await Promise.all(
dirPackages.map(async ([dir, pjName]) => {
Copy link
Member

Choose a reason for hiding this comment

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

Is this not equivalent to packages.entries()? Can these be consolidated?

Promise.allSettled(updatesTodo.map(async update => update())),
);
};

Copy link
Member

Choose a reason for hiding this comment

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

There’s a hazard on line 157 where a parse error will cause a reference error on 158.

Copy link
Member Author

@michaelfig michaelfig left a comment

Choose a reason for hiding this comment

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

Comments addressed to the best of my ability.

;;
*)
# Usage instructions.
echo 1>&2 "Usage: $0 [lerna] <add|remove|list> [<tag>]"
Copy link
Member Author

Choose a reason for hiding this comment

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

Is the distinction that lerna means to apply in bulk to the workspaces? Would we ever not do that?

Yes and yes, if we want to add/remove/list the dist-tags for just the package in the cwd.

Comment on lines +36 to +41
pkg=$(jq -r .name package.json)
version=$(jq -r .version package.json)
# echo "$OP $pkg@$version"

# Get the second argument, if any.
TAG=$2
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 use TRAIN_CASE for environment variables and command-line arguments.

);
}
// This open-coding of the above yarn command is quiet and fast.
const linkName = `${linkFolder}/${pjName}`;
Copy link
Member Author

Choose a reason for hiding this comment

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

Unless portability to older versions or Windows failures demand otherwise, I just use slashes and trust that Node.js's fs module figures out the rest.

Comment on lines 132 to 164
// After all have settled, try throwing the first rejection.
const firstFailure = results.find(
({ status }) => status !== 'fulfilled',
);
if (firstFailure) {
throw firstFailure.reason;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

AggregateError is not available in Node v14. Do you have a better suggestion?

if (needsPruning) {
pruningTodo.push(async () => {
// Update on exit, in case we are interrupted.
process.on('beforeExit', updatePackageJson);
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 presume you considered and decided against framing this in terms of a finally. I don’t think it would occur to me to use the beforeExit event under any circumstances.

It is framed in terms of a pruningP.finally(...). It's quite important that these updates are done even if the process is interrupted. Do you have a better mechanism at hand?

@michaelfig
Copy link
Member Author

Is the notion that install does a number of shallow installs

No, it handles the old-style dapp environment which is a top-level worktree and another worktree with its own yarn.lock in the ui directory. Typically the package.json are updated and then there is only one yarn install per worktree.

This works fine if the package.json versions don't already match, but yarn really doesn't want to reinstall a package that already appears in yarn.lock if it's semver-compatible.

When a forced version (like agoric install dev) is supplied, and a given package.json' already refers to it, the package.jsonupdates andyarn installabove are preceded by first removing the matching Agoric SDK packages from thepackage.jsons, then run a pruning yarn install` per worktree to clear out the old dependencies.

This is the fastest way to trim out the obsolete dependencies from both the workspaces' node_modules directories and the worktrees' yarn.locks, at a cost of at most 1-2 yarn installs per worktree. Other methods I investigated but threw out were:

  • yarn remove the packages one-by-one, then update the package.json and yarn install
  • prune the yarn.locks with code written especially for agoric install
  • Delete the yarn.locks entirely, and run a yarn install

Would the effect be comparable to amending the dapp package.json to include resolutions for all the agoric-sdk packages using a particular dist-tag?

I don't think so. The idea is that package.jsons and yarn.locks should point to real packages all the time, and be reinstallable from scratch just with yarn install. The "symlink-to-SDK" mode is an add-on, not a replacement.

Copy link
Member

@kriskowal kriskowal left a comment

Choose a reason for hiding this comment

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

I cannot profess to be able to maintain agoric install based on this review, but the code looks like it should do as advertised. Given that hopefully agoric install gives way to yarn install soonish, I’m okay with this.

@michaelfig michaelfig added the automerge:no-update (expert!) Automatically merge without updates label Dec 3, 2021
@mergify mergify bot merged commit fbc7c64 into master Dec 3, 2021
@mergify mergify bot deleted the mfig-npm-dist-tag branch December 3, 2021 01:28
const versions = new Map();
const dirPackages = [];

const sdkRoot = path.resolve(dirname, `../../..`);
Copy link
Member

Choose a reason for hiding this comment

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

Where is this coming from ?

@mhofman
Copy link
Member

mhofman commented Dec 11, 2021

agoric install beta will update the workspace's yarn.lock with the beta dependencies. From that point on, installing the dapp only needs yarn install (though agoric install will work, too).

This actually doesn't work for me. The yarn.lock file of my existing dapp doesn't seem to be updated. The preinstall script is also left in place, which means yarn install is not possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
agoric-cli package: agoric-cli automerge:no-update (expert!) Automatically merge without updates
Projects
None yet
Development

Successfully merging this pull request may close these issues.

no supported workflow for installing from npm
4 participants