Skip to content

Commit a9e343c

Browse files
authored
Fallbacks to /tmp if the preferred cache folder isn't writable (#4143)
* Fallbacks to /tmp if the preferred cache folder isn't writable * Adds a preferred-cache-folder option * Adds an integration test * Node 4 doesn't expose fs.constants * Remove the /tmp fallback * Lints * Adds a test * wip * Lints
1 parent 804fb82 commit a9e343c

File tree

10 files changed

+131
-14
lines changed

10 files changed

+131
-14
lines changed

__tests__/integration.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import execa from 'execa';
55
import makeTemp from './_temp.js';
66
import * as fs from '../src/util/fs.js';
7+
import * as misc from '../src/util/misc.js';
8+
import * as constants from '../src/constants.js';
79

810
jasmine.DEFAULT_TIMEOUT_INTERVAL = 90000;
911

@@ -70,3 +72,37 @@ test('--mutex network', async () => {
7072
execa(command, ['add', 'foo'].concat(args), options),
7173
]);
7274
});
75+
76+
test('cache folder fallback', async () => {
77+
const cwd = await makeTemp();
78+
const cacheFolder = path.join(cwd, '.cache');
79+
80+
const command = path.resolve(__dirname, '../bin/yarn');
81+
const args = ['--preferred-cache-folder', cacheFolder];
82+
83+
const options = {cwd};
84+
85+
function runCacheDir(): Promise<Array<Buffer>> {
86+
const {stderr, stdout} = execa(command, ['cache', 'dir'].concat(args), options);
87+
88+
const stdoutPromise = misc.consumeStream(stdout);
89+
const stderrPromise = misc.consumeStream(stderr);
90+
91+
return Promise.all([stdoutPromise, stderrPromise]);
92+
}
93+
94+
const [stdoutOutput, stderrOutput] = await runCacheDir();
95+
96+
expect(stdoutOutput.toString().trim()).toEqual(path.join(cacheFolder, `v${constants.CACHE_VERSION}`));
97+
expect(stderrOutput.toString()).not.toMatch(/Skipping preferred cache folder/);
98+
99+
await fs.unlink(cacheFolder);
100+
await fs.writeFile(cacheFolder, `not a directory`);
101+
102+
const [stdoutOutput2, stderrOutput2] = await runCacheDir();
103+
104+
expect(stdoutOutput2.toString().trim()).toEqual(
105+
path.join(constants.PREFERRED_MODULE_CACHE_DIRECTORIES[0], `v${constants.CACHE_VERSION}`),
106+
);
107+
expect(stderrOutput2.toString()).toMatch(/Skipping preferred cache folder/);
108+
});

src/cli/commands/cache.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const {run, setFlags, examples} = buildSubCommands('cache', {
5555
},
5656

5757
dir(config: Config, reporter: Reporter) {
58-
reporter.log(config.cacheFolder);
58+
reporter.log(config.cacheFolder, {force: true});
5959
},
6060

6161
async clean(config: Config, reporter: Reporter, flags: Object, args: Array<string>): Promise<void> {

src/cli/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ export function main({
6060
'--modules-folder <path>',
6161
'rather than installing modules into the node_modules folder relative to the cwd, output them here',
6262
);
63-
commander.option('--cache-folder <path>', 'specify a custom folder to store the yarn cache');
63+
commander.option('--preferred-cache-folder <path>', 'specify a custom folder to store the yarn cache if possible');
64+
commander.option('--cache-folder <path>', 'specify a custom folder that must be used to store the yarn cache');
6465
commander.option('--mutex <type>[:specifier]', 'use a mutex to ensure only one yarn instance is executing');
6566
commander.option('--emoji [bool]', 'enable emoji in output', process.platform === 'darwin');
6667
commander.option('-s, --silent', 'skip Yarn console logs, other types of logs (script output) will be printed');
@@ -325,6 +326,7 @@ export function main({
325326
binLinks: commander.binLinks,
326327
modulesFolder: commander.modulesFolder,
327328
globalFolder: commander.globalFolder,
329+
preferredCacheFolder: commander.preferredCacheFolder,
328330
cacheFolder: commander.cacheFolder,
329331
preferOffline: commander.preferOffline,
330332
captureHar: commander.har,

src/config.js

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,9 +276,47 @@ export default class Config {
276276
networkConcurrency: this.networkConcurrency,
277277
networkTimeout: this.networkTimeout,
278278
});
279-
this._cacheRootFolder = String(
280-
opts.cacheFolder || this.getOption('cache-folder', true) || constants.MODULE_CACHE_DIRECTORY,
281-
);
279+
280+
let cacheRootFolder = opts.cacheFolder || this.getOption('cache-folder', true);
281+
282+
if (!cacheRootFolder) {
283+
let preferredCacheFolders = constants.PREFERRED_MODULE_CACHE_DIRECTORIES;
284+
const preferredCacheFolder = opts.preferredCacheFolder || this.getOption('preferred-cache-folder', true);
285+
286+
if (preferredCacheFolder) {
287+
preferredCacheFolders = [preferredCacheFolder].concat(preferredCacheFolders);
288+
}
289+
290+
for (let t = 0; t < preferredCacheFolders.length && !cacheRootFolder; ++t) {
291+
const tentativeCacheFolder = String(preferredCacheFolders[t]);
292+
293+
try {
294+
await fs.mkdirp(tentativeCacheFolder);
295+
296+
const testFile = path.join(tentativeCacheFolder, 'testfile');
297+
298+
// fs.access is not enough, because the cache folder could actually be a file.
299+
await fs.writeFile(testFile, 'content');
300+
await fs.readFile(testFile);
301+
await fs.unlink(testFile);
302+
303+
cacheRootFolder = tentativeCacheFolder;
304+
} catch (error) {
305+
this.reporter.warn(this.reporter.lang('cacheFolderSkipped', tentativeCacheFolder));
306+
}
307+
308+
if (cacheRootFolder && t > 0) {
309+
this.reporter.warn(this.reporter.lang('cacheFolderSelected', cacheRootFolder));
310+
}
311+
}
312+
}
313+
314+
if (!cacheRootFolder) {
315+
throw new MessageError(this.reporter.lang('cacheFolderMissing'));
316+
} else {
317+
this._cacheRootFolder = String(cacheRootFolder);
318+
}
319+
282320
this.workspacesEnabled = Boolean(this.getOption('workspaces-experimental'));
283321

284322
this.pruneOfflineMirror = Boolean(this.getOption('yarn-offline-mirror-pruning'));

src/constants.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* @flow */
22

3+
const os = require('os');
34
const path = require('path');
45
const userHome = require('./util/user-home-dir').default;
56

@@ -46,15 +47,21 @@ function getDirectory(category: string): string {
4647
return path.join(userHome, `.${category}`, 'yarn');
4748
}
4849

49-
function getCacheDirectory(): string {
50+
function getPreferredCacheDirectories(): Array<string> {
51+
const preferredCacheDirectories = [];
52+
5053
if (process.platform === 'darwin') {
51-
return path.join(userHome, 'Library', 'Caches', 'Yarn');
54+
preferredCacheDirectories.push(path.join(userHome, 'Library', 'Caches', 'Yarn'));
55+
} else {
56+
preferredCacheDirectories.push(getDirectory('cache'));
5257
}
5358

54-
return getDirectory('cache');
59+
preferredCacheDirectories.push(path.join(os.tmpdir(), '.yarn-cache'));
60+
61+
return preferredCacheDirectories;
5562
}
5663

57-
export const MODULE_CACHE_DIRECTORY = getCacheDirectory();
64+
export const PREFERRED_MODULE_CACHE_DIRECTORIES = getPreferredCacheDirectories();
5865
export const CONFIG_DIRECTORY = getDirectory('config');
5966
export const LINK_REGISTRY_DIRECTORY = path.join(CONFIG_DIRECTORY, 'link');
6067
export const GLOBAL_MODULE_DIRECTORY = path.join(CONFIG_DIRECTORY, 'global');
@@ -70,6 +77,7 @@ export const LOCKFILE_FILENAME = 'yarn.lock';
7077
export const METADATA_FILENAME = '.yarn-metadata.json';
7178
export const TARBALL_FILENAME = '.yarn-tarball.tgz';
7279
export const CLEAN_FILENAME = '.yarnclean';
80+
export const ACCESS_FILENAME = '.yarn-access';
7381

7482
export const DEFAULT_INDENT = ' ';
7583
export const SINGLE_INSTANCE_PORT = 31997;

src/reporters/base-reporter.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,8 @@ export default class BaseReporter {
195195
success(message: string) {}
196196

197197
// a simple log message
198-
log(message: string) {}
198+
// TODO: rethink the {force} parameter. In the meantime, please don't use it (cf comments in #4143).
199+
log(message: string, {force = false}: {force?: boolean} = {}) {}
199200

200201
// a shell command has been executed
201202
command(command: string) {}

src/reporters/console/console-reporter.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,13 +160,13 @@ export default class ConsoleReporter extends BaseReporter {
160160
this.log(this._prependEmoji(msg, '✨'));
161161
}
162162
163-
log(msg: string) {
163+
log(msg: string, {force = false}: {force?: boolean} = {}) {
164164
this._lastCategorySize = 0;
165-
this._log(msg);
165+
this._log(msg, {force});
166166
}
167167
168-
_log(msg: string) {
169-
if (this.isSilent) {
168+
_log(msg: string, {force = false}: {force?: boolean} = {}) {
169+
if (this.isSilent && !force) {
170170
return;
171171
}
172172
clearLine(this.stdout);

src/reporters/lang/en.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,11 @@ const messages = {
181181
workspaceNameMandatory: 'Missing name in workspace at $0, ignoring.',
182182
workspaceNameDuplicate: 'There are more than one workspace with name $0',
183183

184+
cacheFolderSkipped: 'Skipping preferred cache folder $0 because it is not writable.',
185+
cacheFolderMissing:
186+
"Yarn hasn't been able to find a cache folder it can use. Please use the explicit --cache-folder option to tell it what location to use, or make one of the preferred locations writable.",
187+
cacheFolderSelected: 'Selected the next writable cache folder in the list, will be $0.',
188+
184189
execMissingCommand: 'Missing command name.',
185190

186191
commandNotSpecified: 'No command specified.',

src/util/fs.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ const globModule = require('glob');
1313
const os = require('os');
1414
const path = require('path');
1515

16+
export const constants =
17+
typeof fs.constants !== 'undefined'
18+
? fs.constants
19+
: {
20+
R_OK: fs.R_OK,
21+
W_OK: fs.W_OK,
22+
X_OK: fs.X_OK,
23+
};
24+
1625
export const lockQueue = new BlockingQueue('fs lock');
1726

1827
export const readFileBuffer = promisify(fs.readFile);

src/util/misc.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22

33
const _camelCase = require('camelcase');
44

5+
export function consumeStream(stream: Object): Promise<Buffer> {
6+
return new Promise((resolve, reject) => {
7+
const buffers = [];
8+
9+
stream.on(`data`, buffer => {
10+
buffers.push(buffer);
11+
});
12+
13+
stream.on(`end`, () => {
14+
resolve(Buffer.concat(buffers));
15+
});
16+
17+
stream.on(`error`, error => {
18+
reject(error);
19+
});
20+
});
21+
}
22+
523
export function sortAlpha(a: string, b: string): number {
624
// sort alphabetically in a deterministic way
725
const shortLen = Math.min(a.length, b.length);

0 commit comments

Comments
 (0)