Skip to content

Commit

Permalink
test: automatically install anvil during yarn install via `foundr…
Browse files Browse the repository at this point in the history
…yup.ts` (#28393)

I've ported Foundry's foundryup bash script to TypeScript. It downloads
a pinned version of the `anvil` binary on `yarn install`.

While you don't need to do anything, other than run `yarn` (or `yarn
install`), you can customize the installation options by running `yarn
foundryup <options>`. Try running `yarn foundryup --help` to see all
that it can do.

An explanation of how it works:

The `yarn foundryup` command caches (to `.metamask/cache/*`) the
downloaded binaries (defaults to only `anvil`, but it _can_ install all
the others) and symlinks them from the cache into
`node_modules/.bin/<binary name>`. This makes successive installs nearly
instant.

In CI the cache itself is cached (same process as for the `.yarn/cache`
cache), so that CI doesn't have to redownload binaries on every
workflow.

The `foundryup` code is intended to be generic, and could eventually be
a partial replacement for the `ganache` package for most users (for cli
users only, not for programmatic use cases). For this reason, the script
uses defaults that are not tailored to our needs; instead we use a new
`package.json` property _`foundryup`_ to describe what defaults we do
want, and the script uses those values.

In addition to the original `foundryup` bash script features, this
script supports checksumming the downloaded binaries; a feature
requested by the MM security team. See the `foundryup` property in our
`package.json` for more details.

A feature that is _missing_ from this script, but is in the original
`foundryup` bash script,is support for downloading the `man` pages. I
don't really care about this feature and don't think anyone would use
it.

This script _should_ support the following platforms, but I've only
tested Linux+AMD:

Linux on ARM and AMD
Mac on ARM and AMD
Windows on AMD (likely works on arm-based Windows machines via OS's
emulation)

--

This also adds a new yarn command: `anvil`. You can use it just like you
did with `yarn ganache`, i.e., `yarn anvil`; note: the defaults and CLI
options are not going to be exactly the same as `ganache`.


<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.


## **Description**


[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28393?quickstart=1)

## **Related issues**

Fixes:

## **Manual testing steps**

1. Go to this page...
2.
3.

## **Screenshots/Recordings**

### **Before**


### **After**


## **Pre-merge author checklist**

- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

-->

---------

Co-authored-by: seaona <[email protected]>
Co-authored-by: seaona <[email protected]>
Co-authored-by: Howard Braham <[email protected]>
  • Loading branch information
4 people authored Jan 31, 2025
1 parent fc20a80 commit df3931e
Show file tree
Hide file tree
Showing 17 changed files with 1,528 additions and 132 deletions.
9 changes: 7 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -321,12 +321,15 @@ jobs:
- run:
name: Save Yarn version
command: yarn --version > /tmp/YARN_VERSION
- run:
name: Save Foundry version
command: node -e "process.stdout.write(require('./package.json').foundryup.version)" > /tmp/FOUNDRY_VERSION
- restore_cache:
keys:
# First try to get the specific cache for the checksum of the yarn.lock file.
# This cache key lookup will fail if the lock file is modified and a cache
# has not yet been persisted for the new checksum.
- dependency-cache-{{ checksum "/tmp/YARN_VERSION" }}-{{ checksum "yarn.lock" }}
- dependency-cache-{{ checksum "/tmp/YARN_VERSION" }}-{{ checksum "yarn.lock" }}-{{ checksum "/tmp/FOUNDRY_VERSION" }}
# To prevent having to do a full install of every node_module when
# dependencies change, restore from the last known cache of any
# branch/checksum with the same Yarn version, the install step will remove
Expand All @@ -338,12 +341,14 @@ jobs:
name: Install dependencies
command: yarn --immutable
- save_cache:
key: dependency-cache-{{ checksum "/tmp/YARN_VERSION" }}-{{ checksum "yarn.lock" }}
key: dependency-cache-{{ checksum "/tmp/YARN_VERSION" }}-{{ checksum "yarn.lock" }}-{{ checksum "/tmp/FOUNDRY_VERSION" }}
paths:
- .yarn/cache
- .metamask/cache # should match yarn's relative location
- persist_to_workspace:
root: .
paths:
- .metamask/cache # ensures anvil is installed
- node_modules
- build-artifacts

Expand Down
4 changes: 3 additions & 1 deletion .depcheckrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ ignores:
- 'wait-on'
- 'tsx' # used in .devcontainer
- 'prettier-eslint' # used by the Prettier ESLint VSCode extension
- 'tar' # used by foundryup.ts
- 'minipass' # used by foundryup.ts
# storybook
- '@storybook/cli'
- '@storybook/core'
Expand All @@ -65,7 +67,7 @@ ignores:
- 'html-bundler-webpack-plugin' # build tool
- 'postcss-loader' # build tool
- '@swc/helpers' # build tool
- browserslist # build tool
- 'browserslist' # build tool
- 'buffer' # polyfill
- 'crypto-browserify' # polyfill
- 'process' # polyfill
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ test-results/
!.yarn/versions
development/generate-attributions/.yarn/*

# MetaMask
.metamask/*

# Playwright
public/playwright
lavamoat/**/policy-debug.json
Expand Down
1 change: 1 addition & 0 deletions .vscode/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"metamaskbot",
"metamaskrc",
"metametrics",
"Minipass",
"mocharc",
"MULTICHAIN",
"MULTIPROVIDER",
Expand Down
4 changes: 4 additions & 0 deletions .vscode/package.json-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
"type": "string",
"description": "Deletes webpack's build cache. Useful to force a rebuild (webpack not detecting changes, node_modules have changed, etc)."
},
"foundryup": {
"type": "string",
"description": "Installs foundry's Anvil. Run `yarn foundryup --help` for advanced usage."
},
"postinstall": {
"type": "string",
"description": "Runs automatically after running `yarn` (`yarn install`) in order to prime the webpack dev build."
Expand Down
2 changes: 2 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module.exports = {
'<rootDir>/ui/**/*.(js|ts|tsx)',
'<rootDir>/development/build/transforms/**/*.js',
'<rootDir>/test/unit-global/**/*.test.(js|ts|tsx)',
'<rootDir>/test/helpers/foundry/**/*.(js|ts|tsx)',
],
coverageDirectory: './coverage/unit',
coveragePathIgnorePatterns: ['.stories.*', '.snap'],
Expand Down Expand Up @@ -36,6 +37,7 @@ module.exports = {
'<rootDir>/test/unit-global/**/*.test.(js|ts|tsx)',
'<rootDir>/test/e2e/helpers.test.js',
'<rootDir>/test/e2e/helpers/**/*.test.(js|ts|tsx)',
'<rootDir>/test/helpers/foundry/**/*.test.(js|ts|tsx)',
],
testPathIgnorePatterns: ['<rootDir>/development/webpack/'],
testTimeout: 5500,
Expand Down
22 changes: 11 additions & 11 deletions lavamoat/build-system/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -2401,9 +2401,9 @@
"stylelint>postcss-html>htmlparser2>domelementtype": true
}
},
"browserify>duplexer2": {
"unzipper>duplexer2": {
"packages": {
"browserify>duplexer2>readable-stream": true
"unzipper>duplexer2>readable-stream": true
}
},
"debounce-stream>duplexer": {
Expand Down Expand Up @@ -4646,7 +4646,7 @@
"browserify>concat-stream": true,
"watchify>defined": true,
"browserify>module-deps>detective": true,
"browserify>duplexer2": true,
"unzipper>duplexer2": true,
"pumpify>inherits": true,
"loose-envify": true,
"browserify>parents": true,
Expand Down Expand Up @@ -6421,7 +6421,7 @@
"readable-stream>util-deprecate": true
}
},
"browserify>duplexer2>readable-stream": {
"unzipper>duplexer2>readable-stream": {
"builtin": {
"events.EventEmitter": true,
"stream": true,
Expand All @@ -6438,10 +6438,10 @@
"packages": {
"readable-stream-2>core-util-is": true,
"pumpify>inherits": true,
"browserify>duplexer2>readable-stream>isarray": true,
"unzipper>duplexer2>readable-stream>isarray": true,
"readable-stream-2>process-nextick-args": true,
"browserify>duplexer2>readable-stream>safe-buffer": true,
"browserify>duplexer2>readable-stream>string_decoder": true,
"unzipper>duplexer2>readable-stream>safe-buffer": true,
"unzipper>duplexer2>readable-stream>string_decoder": true,
"readable-stream>util-deprecate": true
}
},
Expand Down Expand Up @@ -7373,7 +7373,7 @@
"buffer": true
}
},
"browserify>duplexer2>readable-stream>safe-buffer": {
"unzipper>duplexer2>readable-stream>safe-buffer": {
"builtin": {
"buffer": true
}
Expand Down Expand Up @@ -7851,7 +7851,7 @@
},
"browserify>module-deps>stream-combiner2": {
"packages": {
"browserify>duplexer2": true,
"unzipper>duplexer2": true,
"browserify>module-deps>stream-combiner2>readable-stream": true
}
},
Expand Down Expand Up @@ -7942,9 +7942,9 @@
"browserify>concat-stream>readable-stream>safe-buffer": true
}
},
"browserify>duplexer2>readable-stream>string_decoder": {
"unzipper>duplexer2>readable-stream>string_decoder": {
"packages": {
"browserify>duplexer2>readable-stream>safe-buffer": true
"unzipper>duplexer2>readable-stream>safe-buffer": true
}
},
"gulp-watch>vinyl-file>strip-bom-stream>first-chunk-stream>readable-stream>string_decoder": {
Expand Down
32 changes: 28 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"scripts": {
"webpack": "tsx ./development/webpack/launch.ts",
"webpack:clearcache": "./development/clear-webpack-cache.js",
"postinstall": "yarn webpack:clearcache",
"foundryup": "tsx ./test/helpers/foundry/foundryup.ts",
"postinstall": "yarn webpack:clearcache && yarn foundryup",
"env:e2e": "SEGMENT_HOST='https://api.segment.io' SEGMENT_WRITE_KEY='FAKE' yarn",
"start": "yarn build:dev dev --apply-lavamoat=false --snow=false",
"start:with-state": "node ./app/scripts/start-with-wallet-state.mjs",
Expand Down Expand Up @@ -44,6 +45,7 @@
"forwarder": "node ./development/static-server.js ./node_modules/@metamask/forwarder/dist/ --port 9010",
"dapp-forwarder": "concurrently -k -n forwarder,dapp -p '[{time}][{name}]' 'yarn forwarder' 'yarn dapp'",
"test:unit": "jest",
"anvil": "node_modules/.bin/anvil",
"test:unit:watch": "jest --watch",
"test:unit:coverage": "jest --coverage",
"test:unit:webpack": "tsx --test development/webpack/test/*.test.ts",
Expand Down Expand Up @@ -533,6 +535,7 @@
"@types/serve-handler": "^6.1.4",
"@types/sinon": "^10.0.13",
"@types/sprintf-js": "^1",
"@types/unzipper": "^0.10.10",
"@types/w3c-web-hid": "^1.0.3",
"@types/watchify": "^3.11.1",
"@types/webextension-polyfill": "^0.10.4",
Expand Down Expand Up @@ -616,6 +619,7 @@
"lockfile-lint": "^4.10.6",
"loose-envify": "^1.4.0",
"mini-css-extract-plugin": "^2.9.1",
"minipass": "^7.1.2",
"mocha": "^10.2.0",
"mocha-junit-reporter": "^2.2.1",
"mockttp": "^3.10.1",
Expand Down Expand Up @@ -658,13 +662,15 @@
"style-loader": "^0.21.0",
"stylelint": "^13.6.1",
"superstruct": "^1.0.3",
"tar": "^7.4.3",
"terser": "^5.7.0",
"terser-webpack-plugin": "^5.3.10",
"through2": "^4.0.2",
"ts-node": "^10.9.2",
"tsx": "^4.7.1",
"tsx": "^4.19.2",
"ttest": "^2.1.1",
"typescript": "~5.4.5",
"unzipper": "^0.12.3",
"vinyl": "^2.2.1",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
Expand All @@ -681,7 +687,7 @@
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">= 20",
"node": ">=20.12.0 <20.15.0 || >=20.17.0",
"yarn": "^4.4.1"
},
"lavamoat": {
Expand Down Expand Up @@ -764,5 +770,23 @@
"@lavamoat/allow-scripts>@lavamoat/preinstall-always-fail": false
}
},
"packageManager": "[email protected]"
"packageManager": "[email protected]",
"foundryup": {
"binaries": [
"anvil"
],
"checksums": {
"algorithm": "sha256",
"binaries": {
"anvil": {
"darwin-amd64": "8404e555223fe884557d5e22de494baf8b5f0b82c6f87a4c790c5150e546c9d0",
"darwin-arm64": "888500bc210752e71a355ed4d492ad6dcb4c0ef54d283c105a29a5ccc73d0dbd",
"linux-amd64": "6104069b183fa0f3cdcb692681da9dbd203a3c1bceb435853bbf7abd991c649e",
"linux-arm64": "d66ed8f848e829882ebb65d28aaac72aeab6a101655bb62147186040655928b5",
"win32-amd64": "6c71d9a7be39ed32b53c89bdbc83aa748f41587517212ffe2a8b955c3e9c2e9b"
}
}
},
"version": "v0.3.0"
}
}
82 changes: 82 additions & 0 deletions test/helpers/foundry/download.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { request as httpRequest, type IncomingMessage } from 'node:http';
import { request as httpsRequest } from 'node:https';
import { Stream } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { DownloadOptions } from './types';

class DownloadStream extends Stream.PassThrough {
async response(): Promise<IncomingMessage> {
return new Promise((resolve, reject) => {
this.once('response', resolve);
this.once('error', reject);
});
}
}

/**
* Starts a download from the given URL.
*
* @param url - The URL to download from
* @param options - The download options
* @param redirects - The number of redirects that have occurred
* @returns A stream of the download
*/

export function startDownload(
url: URL,
options: DownloadOptions = {},
redirects: number = 0,
) {
const MAX_REDIRECTS = options.maxRedirects ?? 5;
const request = url.protocol === 'http:' ? httpRequest : httpsRequest;
const stream = new DownloadStream();
request(url, options, async (response) => {
stream.once('close', () => {
response.destroy();
});

const { statusCode, statusMessage, headers } = response;
// handle redirects
if (
statusCode &&
statusCode >= 300 &&
statusCode < 400 &&
headers.location
) {
if (redirects >= MAX_REDIRECTS) {
stream.emit('error', new Error('Too many redirects'));
response.destroy();
} else {
// note: we don't emit a response until we're done redirecting, because
// handlers only expect it to be emitted once.
await pipeline(
startDownload(new URL(headers.location, url), options, redirects + 1)
// emit the response event to the stream
.once('response', stream.emit.bind(stream, 'response')),
stream,
).catch(stream.emit.bind(stream, 'error'));
response.destroy();
}
}

// check for HTTP errors
else if (!statusCode || statusCode < 200 || statusCode >= 300) {
stream.emit(
'error',
new Error(
`Request to ${url} failed. Status Code: ${statusCode} - ${statusMessage}`,
),
);
response.destroy();
} else {
// resolve with response stream
stream.emit('response', response);

response.once('error', stream.emit.bind(stream, 'error'));
await pipeline(response, stream).catch(stream.emit.bind(stream, 'error'));
}
})
.once('error', stream.emit.bind(stream, 'error'))
.end();
return stream;
}
Loading

0 comments on commit df3931e

Please sign in to comment.