Skip to content

Commit

Permalink
Adds support for running native binaries without JS intermediaries (#…
Browse files Browse the repository at this point in the history
…5508)

**What's the problem this PR addresses?**

Yarn currently cannot run native binaries without going through a JS
jumper script. Other package managers don't have this restriction, and
it makes the `yarn run` overhead worse on some scripts for little
reasons - or doesn't work at all when packages don't use jumper scripts.

**How did you fix it?**

Two mechanisms are used:

- First we check for the binary extension
- Then we check its magic number

If one of the two match a predetermined list, the binary is spawned
without going through Node. This ensures that this logic is called only
when the binary is truly a native binary, and will not affect the
behaviour of other existing scripts.

**Checklist**
<!--- Don't worry if you miss something, chores are automatically
tested. -->
<!--- This checklist exists to help you remember doing the chores when
you submit a PR. -->
<!--- Put an `x` in all the boxes that apply. -->
- [x] I have read the [Contributing
Guide](https://yarnpkg.com/advanced/contributing).

<!-- See
https://yarnpkg.com/advanced/contributing#preparing-your-pr-to-be-released
for more details. -->
<!-- Check with `yarn version check` and fix with `yarn version check
-i` -->
- [x] I have set the packages that need to be released for my changes to
be effective.

<!-- The "Testing chores" workflow validates that your PR follows our
guidelines. -->
<!-- If it doesn't pass, click on it to see details as to what your PR
might be missing. -->
- [x] I will check that all automated PR checks pass before the PR gets
reviewed.
  • Loading branch information
arcanis authored and merceyz committed Jul 20, 2023
1 parent 4ed09ec commit 6256fdd
Show file tree
Hide file tree
Showing 11 changed files with 202 additions and 68 deletions.
34 changes: 34 additions & 0 deletions .yarn/versions/92ff1c9e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
releases:
"@yarnpkg/cli": patch
"@yarnpkg/core": patch
"@yarnpkg/plugin-pnp": patch

declined:
- "@yarnpkg/plugin-compat"
- "@yarnpkg/plugin-constraints"
- "@yarnpkg/plugin-dlx"
- "@yarnpkg/plugin-essentials"
- "@yarnpkg/plugin-exec"
- "@yarnpkg/plugin-file"
- "@yarnpkg/plugin-git"
- "@yarnpkg/plugin-github"
- "@yarnpkg/plugin-http"
- "@yarnpkg/plugin-init"
- "@yarnpkg/plugin-interactive-tools"
- "@yarnpkg/plugin-link"
- "@yarnpkg/plugin-nm"
- "@yarnpkg/plugin-npm"
- "@yarnpkg/plugin-npm-cli"
- "@yarnpkg/plugin-pack"
- "@yarnpkg/plugin-patch"
- "@yarnpkg/plugin-pnpm"
- "@yarnpkg/plugin-stage"
- "@yarnpkg/plugin-typescript"
- "@yarnpkg/plugin-version"
- "@yarnpkg/plugin-workspace-tools"
- "@yarnpkg/builder"
- "@yarnpkg/doctor"
- "@yarnpkg/extensions"
- "@yarnpkg/nm"
- "@yarnpkg/pnpify"
- "@yarnpkg/sdks"
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
const {npath, ppath, xfs, Filename} = require(`@yarnpkg/fslib`);
const {isAbsolute, resolve} = require(`path`);
import {npath, ppath, xfs, Filename, PortablePath} from '@yarnpkg/fslib';
import {execFile} from 'child_process';
import {isAbsolute, resolve} from 'path';
import {fs} from 'pkg-tests-core';
import util from 'util';

const execP = util.promisify(execFile);
const {
fs: {createTemporaryFolder, makeFakeBinary, walk, readFile, writeJson, writeFile},
fs: {createTemporaryFolder, walk, readFile},
} = require(`pkg-tests-core`);

const globalName = makeTemporaryEnv.getPackageManagerName();
Expand All @@ -21,7 +25,7 @@ describe(`Scripts tests`, () => {
makeTemporaryEnv({scripts: {myScript: `node --version`}}, async ({path, run, source}) => {
await run(`install`);

await makeFakeBinary(`${path}/bin/node`, {exitCode: 0});
await fs.makeFakeBinary(ppath.join(path, `/bin/node` as PortablePath), {exitCode: 0});

await expect(run(`run`, `myScript`)).resolves.not.toMatchObject({stdout: `Fake binary`});
}),
Expand All @@ -32,7 +36,7 @@ describe(`Scripts tests`, () => {
makeTemporaryEnv({scripts: {myScript: `${globalName} --version`}}, async ({path, run, source}) => {
await run(`install`);

await makeFakeBinary(`${path}/bin/${globalName}`, {exitCode: 0});
await fs.makeFakeBinary(ppath.join(path, `/bin/${globalName}` as PortablePath), {exitCode: 0});

await expect(run(`run`, `myScript`)).resolves.not.toMatchObject({stdout: `Fake binary`});
}),
Expand Down Expand Up @@ -69,9 +73,9 @@ describe(`Scripts tests`, () => {
private: true,
workspaces: [`packages/*`],
}, async ({path, run, source}) => {
await xfs.mkdirpPromise(`${path}/packages/test`);
await xfs.mkdirpPromise(ppath.join(path, `/packages/test` as PortablePath));

await xfs.writeJsonPromise(`${path}/packages/test/package.json`, {
await xfs.writeJsonPromise(ppath.join(path, `/packages/test/package.json` as PortablePath), {
scripts: {
[`test:script`]: `echo "$INIT_CWD"`,
},
Expand All @@ -84,9 +88,9 @@ describe(`Scripts tests`, () => {
});

await expect(run(`run`, `test:script`, {
cwd: `${path}/packages`,
cwd: ppath.join(path, `/packages` as PortablePath),
})).resolves.toMatchObject({
stdout: `${npath.fromPortablePath(`${path}/packages`)}\n`,
stdout: `${npath.fromPortablePath(ppath.join(path, `/packages` as PortablePath))}\n`,
});
}),
);
Expand All @@ -97,9 +101,9 @@ describe(`Scripts tests`, () => {
private: true,
workspaces: [`packages/*`],
}, async ({path, run, source}) => {
await xfs.mkdirpPromise(`${path}/packages/test`);
await xfs.mkdirpPromise(ppath.join(path, `/packages/test` as PortablePath));

await xfs.writeJsonPromise(`${path}/packages/test/package.json`, {
await xfs.writeJsonPromise(ppath.join(path, `/packages/test/package.json` as PortablePath), {
scripts: {
[`test:script`]: `echo "$PROJECT_CWD"`,
},
Expand All @@ -112,7 +116,7 @@ describe(`Scripts tests`, () => {
});

await expect(run(`run`, `test:script`, {
cwd: `${path}/packages`,
cwd: ppath.join(path, `/packages` as PortablePath),
})).resolves.toMatchObject({
stdout: `${npath.fromPortablePath(path)}\n`,
});
Expand All @@ -125,7 +129,8 @@ describe(`Scripts tests`, () => {
private: true,
workspaces: [`packages/*`],
}, async ({path, run, source}) => {
await writeJson(`${path}/packages/test 1/package.json`, {
await xfs.mkdirpPromise(ppath.join(path, `/packages/test 1` as PortablePath));
await xfs.writeJsonPromise(ppath.join(path, `/packages/test 1/package.json` as PortablePath), {
scripts: {
[`ws:foo2`]: `yarn run ws:foo`,
[`ws:foo`]: `node -e 'console.log(1)'`,
Expand Down Expand Up @@ -174,22 +179,22 @@ describe(`Scripts tests`, () => {
}, async ({path, run, source}) => {
await run(`install`);

await writeFile(`${path}/test.js`, `
const {existsSync} = require('fs');
const {join} = require('path');
await xfs.writeFilePromise(ppath.join(path, `/test.js` as PortablePath), `
const {existsSync} = require('fs');
const {join} = require('path');
const files = ['has-bin-entries'];
if (process.platform === 'win32')
files.push('has-bin-entries.cmd');
const files = ['has-bin-entries'];
if (process.platform === 'win32')
files.push('has-bin-entries.cmd');
for (const file of files) {
if (!existsSync(join(process.env.BERRY_BIN_FOLDER, file))) {
console.error('Expected ' + file + ' to exist');
process.exit(1);
for (const file of files) {
if (!existsSync(join(process.env.BERRY_BIN_FOLDER, file))) {
console.error('Expected ' + file + ' to exist');
process.exit(1);
}
}
}
console.log('ok');
console.log('ok');
`);

await expect(run(`test`)).resolves.toMatchObject({
Expand All @@ -209,7 +214,7 @@ describe(`Scripts tests`, () => {
},
},
async ({path, run, source}) => {
await xfs.writeFilePromise(`${path}/å.js`, `console.log('ok')`);
await xfs.writeFilePromise(ppath.join(path, `/å.js` as PortablePath), `console.log('ok')`);
await run(`install`);

await expect(run(`test`)).resolves.toMatchObject({
Expand All @@ -230,11 +235,11 @@ describe(`Scripts tests`, () => {
async ({path, run, source}) => {
await run(`install`);

await xfs.mkdirpPromise(`${path}/foo/bar`);
await xfs.mkdirpPromise(ppath.join(path, `/foo/bar` as PortablePath));

await expect(
run(`run`, `has-bin-entries`, `success`, {
cwd: `${path}/foo/bar`,
cwd: ppath.join(path, `/foo/bar` as PortablePath),
}),
).resolves.toMatchObject({
stdout: `success\n`,
Expand Down Expand Up @@ -364,15 +369,15 @@ describe(`Scripts tests`, () => {
[`install`]: `echo 'module.exports.push("root");' >> log.js`,
},
}, async ({path, run, source}) => {
await xfs.mkdirPromise(ppath.join(path, `child`));
await xfs.writeJsonPromise(ppath.join(path, `child/package.json`), {
await xfs.mkdirPromise(ppath.join(path, `child` as PortablePath));
await xfs.writeJsonPromise(ppath.join(path, `child/package.json` as PortablePath), {
name: `child`,
scripts: {
[`install`]: `echo 'module.exports.push("child");' >> ../log.js`,
},
});

await xfs.writeFilePromise(ppath.join(path, `log.js`), `module.exports = [];\n`);
await xfs.writeFilePromise(ppath.join(path, `log.js` as PortablePath), `module.exports = [];\n`);
await run(`install`);

await expect(source(`require("./log")`)).resolves.toEqual([
Expand All @@ -396,16 +401,16 @@ describe(`Scripts tests`, () => {
[`install`]: `echo 'module.exports.push("root");' >> log.js`,
},
}, async ({path, run, source}) => {
await xfs.mkdirPromise(ppath.join(path, `child`), {recursive: true});
await xfs.writeJsonPromise(ppath.join(path, `child/package.json`), {
await xfs.mkdirPromise(ppath.join(path, `child` as PortablePath), {recursive: true});
await xfs.writeJsonPromise(ppath.join(path, `child/package.json` as PortablePath), {
name: `child`,
scripts: {
postinstall: `echo 'module.exports.push("child");' >> ../log.js`,
},
});

await run(`install`);
await xfs.writeFilePromise(ppath.join(path, `log.js`), `module.exports = [];\n`);
await xfs.writeFilePromise(ppath.join(path, `log.js` as PortablePath), `module.exports = [];\n`);

await run(`./child`, `add`, `no-deps@1.0.0`);

Expand All @@ -430,19 +435,19 @@ describe(`Scripts tests`, () => {
[`install`]: `echo 'module.exports.push("root");' >> log.js`,
},
}, async ({path, run, source}) => {
await xfs.mkdirPromise(ppath.join(path, `packages/first`), {recursive: true});
await xfs.mkdirPromise(ppath.join(path, `packages/second`), {recursive: true});
await xfs.mkdirPromise(ppath.join(path, `packages/first` as PortablePath), {recursive: true});
await xfs.mkdirPromise(ppath.join(path, `packages/second` as PortablePath), {recursive: true});

await xfs.writeJsonPromise(ppath.join(path, `packages/first/package.json`), {
await xfs.writeJsonPromise(ppath.join(path, `packages/first/package.json` as PortablePath), {
name: `first`,
});

await xfs.writeJsonPromise(ppath.join(path, `packages/second/package.json`), {
await xfs.writeJsonPromise(ppath.join(path, `packages/second/package.json` as PortablePath), {
name: `bar`,
});

await run(`install`);
await xfs.writeFilePromise(ppath.join(path, `log.js`), `module.exports = [];`);
await xfs.writeFilePromise(ppath.join(path, `log.js` as PortablePath), `module.exports = [];`);

await run(`packages/second`, `add`, `no-deps@1.0.0`);

Expand Down Expand Up @@ -603,13 +608,13 @@ describe(`Scripts tests`, () => {
},
config,
async ({path, run, source}) => {
await xfs.mkdirPromise(`${path}/soft-link`);
await xfs.writeJsonPromise(`${path}/soft-link/package.json`, {
await xfs.mkdirPromise(ppath.join(path, `/soft-link` as PortablePath));
await xfs.writeJsonPromise(ppath.join(path, `/soft-link/package.json` as PortablePath), {
name: `soft-link`,
version: `1.0.0`,
bin: `./bin`,
});
await xfs.writeFilePromise(`${path}/soft-link/bin.js`, `console.log(42);\n`);
await xfs.writeFilePromise(ppath.join(path, `/soft-link/bin.js` as PortablePath), `console.log(42);\n`);

await run(`install`);

Expand All @@ -621,6 +626,53 @@ describe(`Scripts tests`, () => {
},
),
);

test(
`it should run native binaries`,
makeTemporaryEnv({}, async ({path, run, source}) => {
const gitProcess = await execP(`git`, [`--exec-path`]);

const gitExt = process.platform === `win32` ? `.exe` : ``;
const gitPath = ppath.join(npath.toPortablePath(gitProcess.stdout.trim()), `git${gitExt}` as PortablePath);

await xfs.copyFilePromise(gitPath, ppath.join(path, `foo${gitExt}` as PortablePath));

await xfs.writeJsonPromise(ppath.join(path, Filename.manifest), {
bin: {
foo: `./foo${gitExt}`,
},
});

await run(`install`);

await run(`run`, `foo`, `--version`);
}),
);

test(
`it should add native binaries to the PATH`,
makeTemporaryEnv({}, async ({path, run, source}) => {
const gitProcess = await execP(`git`, [`--exec-path`]);

const gitExt = process.platform === `win32` ? `.exe` : ``;
const gitPath = ppath.join(npath.toPortablePath(gitProcess.stdout.trim()), `git${gitExt}` as PortablePath);

await xfs.copyFilePromise(gitPath, ppath.join(path, `foo${gitExt}` as PortablePath));

await xfs.writeJsonPromise(ppath.join(path, Filename.manifest), {
bin: {
foo: `./foo${gitExt}`,
},
scripts: {
bar: `foo`,
},
});

await run(`install`);

console.log(await run(`run`, `bar`, `--version`));
}),
);
});
}
});
2 changes: 2 additions & 0 deletions packages/plugin-pnp/sources/jsInstallUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export function extractBuildScripts(pkg: Package, requirements: ExtractBuildScri
const FORCED_EXTRACT_FILETYPES = new Set([
// Windows can't execute exe files inside zip archives
`.exe`,
// May be used for some binaries on Linux; https://askubuntu.com/a/174356
`.bin`,
// The c/c++ compiler can't read files from zip archives
`.h`, `.hh`, `.hpp`, `.c`, `.cc`, `.cpp`,
// The java runtime can't read files from zip archives
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading

0 comments on commit 6256fdd

Please sign in to comment.