Skip to content

Commit f479e67

Browse files
committed
fix(plugin-nm): set binary permissions for partial installs
1 parent 8661aac commit f479e67

File tree

3 files changed

+116
-12
lines changed

3 files changed

+116
-12
lines changed

.yarn/versions/944c0a3d.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
releases:
2+
"@yarnpkg/cli": patch
3+
"@yarnpkg/plugin-nm": patch
4+
5+
declined:
6+
- "@yarnpkg/plugin-compat"
7+
- "@yarnpkg/plugin-constraints"
8+
- "@yarnpkg/plugin-dlx"
9+
- "@yarnpkg/plugin-essentials"
10+
- "@yarnpkg/plugin-init"
11+
- "@yarnpkg/plugin-interactive-tools"
12+
- "@yarnpkg/plugin-npm-cli"
13+
- "@yarnpkg/plugin-pack"
14+
- "@yarnpkg/plugin-patch"
15+
- "@yarnpkg/plugin-pnp"
16+
- "@yarnpkg/plugin-pnpm"
17+
- "@yarnpkg/plugin-stage"
18+
- "@yarnpkg/plugin-typescript"
19+
- "@yarnpkg/plugin-version"
20+
- "@yarnpkg/plugin-workspace-tools"
21+
- "@yarnpkg/builder"
22+
- "@yarnpkg/core"
23+
- "@yarnpkg/doctor"

packages/acceptance-tests/pkg-tests-specs/sources/node-modules.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1780,6 +1780,72 @@ describe(`Node_Modules`, () => {
17801780
),
17811781
);
17821782

1783+
it(`should set correct permissions on binaries in installed packages`,
1784+
makeTemporaryEnv(
1785+
{
1786+
dependencies: {
1787+
[`has-bin-entries`]: `1.0.0`,
1788+
},
1789+
},
1790+
{
1791+
nodeLinker: `node-modules`,
1792+
},
1793+
async ({path, run}) => {
1794+
await run(`install`);
1795+
const {mode} = await xfs.lstatPromise(npath.toPortablePath(`${path}/node_modules/has-bin-entries/bin.js`));
1796+
const permissions = (mode & 0o777).toString(8);
1797+
expect(permissions).toBe(process.platform === `win32` ? `666` : `755`);
1798+
},
1799+
),
1800+
);
1801+
1802+
it(`should set correct permissions on binaries in packages that were upgraded`,
1803+
makeTemporaryEnv(
1804+
{
1805+
dependencies: {
1806+
[`has-bin-entries`]: `1.0.0`,
1807+
},
1808+
},
1809+
{
1810+
nodeLinker: `node-modules`,
1811+
},
1812+
async ({path, run}) => {
1813+
await run(`install`);
1814+
await xfs.writeJsonPromise(`${path}/package.json` as PortablePath, {
1815+
dependencies: {
1816+
'has-bin-entries': `2.0.0`,
1817+
},
1818+
});
1819+
await run(`install`);
1820+
1821+
const {mode} = await xfs.lstatPromise(npath.toPortablePath(`${path}/node_modules/has-bin-entries/bin.js`));
1822+
const permissions = (mode & 0o777).toString(8);
1823+
expect(permissions).toBe(process.platform === `win32` ? `666` : `755`);
1824+
},
1825+
),
1826+
);
1827+
1828+
it(`should reinstall and set correct permissions on next install when packages have been deleted by the user`,
1829+
makeTemporaryEnv(
1830+
{
1831+
dependencies: {
1832+
[`has-bin-entries`]: `1.0.0`,
1833+
},
1834+
},
1835+
{
1836+
nodeLinker: `node-modules`,
1837+
},
1838+
async ({path, run}) => {
1839+
await run(`install`);
1840+
await xfs.removePromise(`${path}/node_modules/has-bin-entries` as PortablePath);
1841+
await run(`install`);
1842+
const {mode} = await xfs.lstatPromise(npath.toPortablePath(`${path}/node_modules/has-bin-entries/bin.js`));
1843+
const permissions = (mode & 0o777).toString(8);
1844+
expect(permissions).toBe(process.platform === `win32` ? `666` : `755`);
1845+
},
1846+
),
1847+
);
1848+
17831849
it(`should only reinstall scoped dependencies deleted by the user on the next install`,
17841850
makeTemporaryEnv(
17851851
{

packages/plugin-nm/sources/NodeModulesLinker.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,20 @@ const INSTALL_STATE_FILE = `.yarn-state.yml` as Filename;
2323
const MTIME_ACCURANCY = 1000;
2424

2525
type InstallState = {locatorMap: NodeModulesLocatorMap, locationTree: LocationTree, binSymlinks: BinSymlinkMap, nmMode: NodeModulesMode, mtimeMs: number};
26-
type BinSymlinkMap = Map<PortablePath, Map<Filename, PortablePath>>;
26+
27+
/**
28+
* @example
29+
* Map {
30+
* 'example-package' -> Map {
31+
* 'bin-file' ->
32+
* [
33+
* '/example-package/node_modules/example-dependency/bin/bin-file',
34+
* '/example-package/node_modules/example-dependency/'
35+
* ]
36+
* }
37+
* }
38+
*/
39+
type BinSymlinkMap = Map<PortablePath, Map<Filename, [PortablePath, PortablePath]>>;
2740
type LoadManifest = (locator: LocatorKey, installLocation: PortablePath) => Promise<Pick<Manifest, `bin`>>;
2841

2942
export enum NodeModulesMode {
@@ -436,7 +449,7 @@ async function writeInstallState(project: Project, locatorMap: NodeModulesLocato
436449
throw new Error(`Assertion failed: Expected the path to be within the project (${location})`);
437450

438451
locatorState += ` ${JSON.stringify(internalPath)}:\n`;
439-
for (const [name, target] of symlinks) {
452+
for (const [name, [target]] of symlinks) {
440453
const relativePath = ppath.relative(ppath.join(location, NODE_MODULES), target);
441454
locatorState += ` ${JSON.stringify(name)}: ${JSON.stringify(relativePath)}\n`;
442455
}
@@ -493,7 +506,7 @@ async function findInstallState(project: Project, {unrollAliases = false}: {unro
493506
const location = ppath.join(rootPath, npath.toPortablePath(relativeLocation));
494507
const symlinks = miscUtils.getMapWithDefault(binSymlinks, location);
495508
for (const [name, target] of Object.entries(locationSymlinks as any)) {
496-
symlinks.set(name as Filename, npath.toPortablePath([location, NODE_MODULES, target].join(ppath.sep)));
509+
symlinks.set(name as Filename, [npath.toPortablePath([location, NODE_MODULES, target].join(ppath.sep)), location]);
497510
}
498511
}
499512
}
@@ -990,14 +1003,14 @@ async function createBinSymlinkMap(installState: NodeModulesLocatorMap, location
9901003

9911004
const binSymlinks: BinSymlinkMap = new Map();
9921005

993-
const getBinSymlinks = (location: PortablePath, parentLocatorLocation: PortablePath, node: LocationNode): Map<Filename, PortablePath> => {
1006+
const getBinSymlinks = (location: PortablePath, parentLocatorLocation: PortablePath, node: LocationNode): Map<Filename, [PortablePath, PortablePath]> => {
9941007
const symlinks = new Map();
9951008
const internalPath = ppath.contains(projectRoot, location);
9961009
if (node.locator && internalPath !== null) {
9971010
const binScripts = locatorScriptMap.get(node.locator)!;
9981011
for (const [filename, scriptPath] of binScripts) {
9991012
const symlinkTarget = ppath.join(location, npath.toPortablePath(scriptPath));
1000-
symlinks.set(filename, symlinkTarget);
1013+
symlinks.set(filename, [symlinkTarget, location]);
10011014
}
10021015
for (const [childLocation, childNode] of node.children) {
10031016
const absChildLocation = ppath.join(location, childLocation);
@@ -1009,8 +1022,8 @@ async function createBinSymlinkMap(installState: NodeModulesLocatorMap, location
10091022
} else {
10101023
for (const [childLocation, childNode] of node.children) {
10111024
const childSymlinks = getBinSymlinks(ppath.join(location, childLocation), parentLocatorLocation, childNode);
1012-
for (const [name, symlinkTarget] of childSymlinks) {
1013-
symlinks.set(name, symlinkTarget);
1025+
for (const [name, value] of childSymlinks) {
1026+
symlinks.set(name, value);
10141027
}
10151028
}
10161029
}
@@ -1315,7 +1328,9 @@ async function persistNodeModules(preinstallState: InstallState, installState: N
13151328
await xfs.mkdirPromise(rootNmDirPath, {recursive: true});
13161329

13171330
const binSymlinks = await createBinSymlinkMap(installState, locationTree, project.cwd, {loadManifest});
1318-
await persistBinSymlinks(prevBinSymlinks, binSymlinks, project.cwd, windowsLinkType);
1331+
const addedDirs = new Set(addList.map(l => l.dstDir));
1332+
1333+
await persistBinSymlinks(prevBinSymlinks, binSymlinks, project.cwd, windowsLinkType, addedDirs);
13191334

13201335
await writeInstallState(project, installState, binSymlinks, nmMode, {installChangedByUser});
13211336

@@ -1327,7 +1342,7 @@ async function persistNodeModules(preinstallState: InstallState, installState: N
13271342
}
13281343
}
13291344

1330-
async function persistBinSymlinks(previousBinSymlinks: BinSymlinkMap, binSymlinks: BinSymlinkMap, projectCwd: PortablePath, windowsLinkType: WindowsLinkType) {
1345+
async function persistBinSymlinks(previousBinSymlinks: BinSymlinkMap, binSymlinks: BinSymlinkMap, projectCwd: PortablePath, windowsLinkType: WindowsLinkType, addedDirs: Set<PortablePath>) {
13311346
// Delete outdated .bin folders
13321347
for (const location of previousBinSymlinks.keys()) {
13331348
if (ppath.contains(projectCwd, location) === null)
@@ -1354,11 +1369,11 @@ async function persistBinSymlinks(previousBinSymlinks: BinSymlinkMap, binSymlink
13541369
}
13551370
}
13561371

1357-
for (const [name, target] of symlinks) {
1372+
for (const [name, [target, binPackageLocation]] of symlinks) {
13581373
const prevTarget = prevSymlinks.get(name);
13591374
const symlinkPath = ppath.join(binDir, name);
1360-
// Skip unchanged .bin symlinks
1361-
if (prevTarget === target)
1375+
// Skip unchanged .bin symlinks except for added/changed packages since they need permissions applied
1376+
if (prevTarget === target && !addedDirs.has(binPackageLocation))
13621377
continue;
13631378

13641379
if (process.platform === `win32`) {

0 commit comments

Comments
 (0)