Skip to content

Commit 5cfc6c9

Browse files
authored
fix: handle parallel installs (#84)
* fix: handle parallel installs * build: skip unnecessary shims
1 parent f17384e commit 5cfc6c9

15 files changed

+184
-45
lines changed

.github/workflows/ci.yml

+3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ jobs:
3232
env:
3333
TARGET_BRANCH: ${{github.event.pull_request.base.ref}}
3434

35+
- name: 'Check for type errors'
36+
run: yarn typecheck
37+
3538
build:
3639
strategy:
3740
fail-fast: false

.pnp.cjs

+40-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

mkshims.ts

+15-6
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@ import path from 'path';
55
import {Engine} from './sources/Engine';
66
import {SupportedPackageManagerSet} from './sources/types';
77

8-
const EXCLUDE_SHIMS = new Set([
9-
`vcc.js`,
10-
]);
8+
function shouldGenerateShim(name: string) {
9+
if (name === 'vcc.js') {
10+
return false;
11+
} else if (name.startsWith('vendors')) {
12+
return false;
13+
}
14+
return true;
15+
}
1116

1217
const engine = new Engine();
1318

@@ -38,7 +43,7 @@ async function main() {
3843
}
3944

4045
for (const binaryName of fs.readdirSync(distDir)) {
41-
if (EXCLUDE_SHIMS.has(binaryName))
46+
if (shouldGenerateShim(binaryName) === false)
4247
continue;
4348

4449
await cmdShim(path.join(distDir, binaryName), path.join(shimsDir, path.basename(binaryName, `.js`)), {createCmdFile: true});
@@ -55,12 +60,16 @@ async function main() {
5560
const remapPath = (p: string) => path.resolve(__dirname, path.relative(virtualNodewinDir, p));
5661

5762
const easyStatFs = Object.assign(Object.create(fs), {
58-
readFile: (p: string, encoding: string, cb: (err: any, str: string) => void) => fs.readFile(remapPath(p), encoding, cb),
63+
readFile: (p: string, encoding: BufferEncoding, cb: (err: any, str: string) => void) => fs.readFile(remapPath(p), encoding, cb),
5964
stat: (p: string, cb: () => void) => fs.stat(remapPath(p), cb),
6065
});
6166

62-
for (const binaryName of fs.readdirSync(distDir))
67+
for (const binaryName of fs.readdirSync(distDir)) {
68+
if (shouldGenerateShim(binaryName) === false)
69+
continue;
70+
6371
await cmdShim(path.join(virtualNodewinDir, `dist/${binaryName}`), path.join(physicalNodewinDir, path.basename(binaryName, `.js`)), {createCmdFile: true, fs: easyStatFs});
72+
}
6473

6574
console.log(`All shims have been generated.`);
6675
}

package.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"@babel/preset-typescript": "^7.13.0",
2828
"@types/debug": "^4.1.5",
2929
"@types/jest": "^26.0.23",
30-
"@types/node": "^13.9.2",
30+
"@types/node": "^17.0.10",
31+
"@types/rimraf": "^3.0.2",
3132
"@types/semver": "^7.1.0",
3233
"@types/tar": "^4.0.3",
3334
"@types/which": "^1.3.2",
@@ -43,6 +44,7 @@
4344
"eslint-plugin-arca": "^0.9.5",
4445
"jest": "^26.0.0",
4546
"nock": "^13.0.4",
47+
"rimraf": "^3.0.2",
4648
"semver": "^7.1.3",
4749
"supports-color": "^7.1.0",
4850
"tar": "^6.0.1",
@@ -56,10 +58,11 @@
5658
"which": "^2.0.2"
5759
},
5860
"scripts": {
59-
"build": "rm -rf dist && webpack && ts-node ./mkshims.ts",
61+
"build": "rm -rf dist shims && webpack && ts-node ./mkshims.ts",
6062
"corepack": "ts-node ./sources/main.ts",
6163
"prepack": "node ./.yarn/releases/*.*js build",
6264
"postpack": "rm -rf dist shims",
65+
"typecheck": "tsc --noEmit",
6366
"test": "yarn jest"
6467
},
6568
"files": [

sources/corepackUtils.ts

+35-24
Original file line numberDiff line numberDiff line change
@@ -87,39 +87,50 @@ export async function installVersion(installTarget: string, locator: Locator, {s
8787
const url = spec.url.replace(`{}`, locator.reference);
8888
debugUtils.log(`Installing ${locator.name}@${locator.reference} from ${url}`);
8989

90-
return await fsUtils.mutex(installFolder, async () => {
91-
// Creating a temporary folder inside the install folder means that we
92-
// are sure it'll be in the same drive as the destination, so we can
93-
// just move it there atomically once we are done
90+
// Creating a temporary folder inside the install folder means that we
91+
// are sure it'll be in the same drive as the destination, so we can
92+
// just move it there atomically once we are done
9493

95-
const tmpFolder = folderUtils.getTemporaryFolder(installTarget);
96-
const stream = await httpUtils.fetchUrlStream(url);
94+
const tmpFolder = folderUtils.getTemporaryFolder(installTarget);
95+
const stream = await httpUtils.fetchUrlStream(url);
9796

98-
const parsedUrl = new URL(url);
99-
const ext = path.posix.extname(parsedUrl.pathname);
97+
const parsedUrl = new URL(url);
98+
const ext = path.posix.extname(parsedUrl.pathname);
10099

101-
let outputFile: string | null = null;
100+
let outputFile: string | null = null;
102101

103-
let sendTo: any;
104-
if (ext === `.tgz`) {
105-
sendTo = tar.x({strip: 1, cwd: tmpFolder});
106-
} else if (ext === `.js`) {
107-
outputFile = path.join(tmpFolder, path.posix.basename(parsedUrl.pathname));
108-
sendTo = fs.createWriteStream(outputFile);
109-
}
102+
let sendTo: any;
103+
if (ext === `.tgz`) {
104+
sendTo = tar.x({strip: 1, cwd: tmpFolder});
105+
} else if (ext === `.js`) {
106+
outputFile = path.join(tmpFolder, path.posix.basename(parsedUrl.pathname));
107+
sendTo = fs.createWriteStream(outputFile);
108+
}
110109

111-
stream.pipe(sendTo);
110+
stream.pipe(sendTo);
112111

113-
await new Promise(resolve => {
114-
sendTo.on(`finish`, resolve);
115-
});
112+
await new Promise(resolve => {
113+
sendTo.on(`finish`, resolve);
114+
});
116115

117-
await fs.promises.mkdir(path.dirname(installFolder), {recursive: true});
116+
await fs.promises.mkdir(path.dirname(installFolder), {recursive: true});
117+
try {
118118
await fs.promises.rename(tmpFolder, installFolder);
119+
} catch (err) {
120+
if (
121+
err.code === `ENOTEMPTY` ||
122+
// On Windows the error code is EPERM so we check if it is a directory
123+
(err.code === `EPERM` && (await fs.promises.stat(installFolder)).isDirectory())
124+
) {
125+
debugUtils.log(`Another instance of corepack installed ${locator.name}@${locator.reference}`);
126+
await fsUtils.rimraf(tmpFolder);
127+
} else {
128+
throw err;
129+
}
130+
}
119131

120-
debugUtils.log(`Install finished`);
121-
return installFolder;
122-
});
132+
debugUtils.log(`Install finished`);
133+
return installFolder;
123134
}
124135

125136
export async function runVersion(installSpec: { location: string, spec: PackageManagerSpec }, locator: Locator, binName: string, args: Array<string>, context: Context) {

sources/fsUtils.ts

+23-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1-
export async function mutex<T>(p: string, cb: () => Promise<T>) {
2-
return await cb();
1+
import fs from 'fs';
2+
3+
export async function rimraf(path: string) {
4+
const [major, minor] = process.versions.node.split(`.`).map(section => Number(section));
5+
6+
if (major > 14 || (major === 14 && minor >= 14)) {
7+
// rm was added in v14.14.0
8+
return fs.promises.rm(path, {recursive: true});
9+
} else if (major > 12 || (major === 12 && minor >= 10)) {
10+
// rmdir got support for recursive in v12.10.0 and was deprecated in v14.14.0
11+
return fs.promises.rmdir(path, {recursive: true});
12+
} else {
13+
const rimraf = await import(`rimraf`);
14+
return new Promise<void>((resolve, reject) => {
15+
rimraf.default(path, err => {
16+
if (err) {
17+
reject(err);
18+
} else {
19+
resolve();
20+
}
21+
});
22+
});
23+
}
324
}

tests/main.test.ts

+27
Original file line numberDiff line numberDiff line change
@@ -328,3 +328,30 @@ it(`should support running package managers with bin array`, async () => {
328328
});
329329
});
330330
});
331+
332+
it(`should handle parallel installs`, async () => {
333+
await xfs.mktempPromise(async cwd => {
334+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
335+
packageManager: `yarn@2.2.2`,
336+
});
337+
338+
await expect(Promise.all([
339+
runCli(cwd, [`yarn`, `--version`]),
340+
runCli(cwd, [`yarn`, `--version`]),
341+
runCli(cwd, [`yarn`, `--version`]),
342+
])).resolves.toMatchObject([
343+
{
344+
stdout: `2.2.2\n`,
345+
exitCode: 0,
346+
},
347+
{
348+
stdout: `2.2.2\n`,
349+
exitCode: 0,
350+
},
351+
{
352+
stdout: `2.2.2\n`,
353+
exitCode: 0,
354+
},
355+
]);
356+
});
357+
});
10.3 MB
Binary file not shown.

tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"lib": ["dom", "es2017", "esnext.asynciterable"],
1111
"module": "commonjs",
1212
"resolveJsonModule": true,
13+
"skipLibCheck": true,
1314
"target": "es2017"
1415
}
1516
}

0 commit comments

Comments
 (0)