@rn-iso/expo-build-cache 1.1.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +23 -0
- package/dist/index.js +173 -0
- package/package.json +20 -7
- package/index.js +0 -228
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region index.d.ts
|
|
2
|
+
interface RunOptions {
|
|
3
|
+
variant?: unknown;
|
|
4
|
+
configuration?: unknown;
|
|
5
|
+
buildConfiguration?: unknown;
|
|
6
|
+
isSimulator?: unknown;
|
|
7
|
+
device?: unknown;
|
|
8
|
+
}
|
|
9
|
+
declare function cacheRoot(): string;
|
|
10
|
+
declare function buildCacheKey(platform: string, fingerprintHash: string, runOptions?: RunOptions): string;
|
|
11
|
+
declare function resolveBuildCache({ platform, fingerprintHash, runOptions }: {
|
|
12
|
+
platform: string;
|
|
13
|
+
fingerprintHash: string;
|
|
14
|
+
runOptions?: RunOptions;
|
|
15
|
+
}): Promise<string | null>;
|
|
16
|
+
declare function uploadBuildCache({ platform, fingerprintHash, buildPath, runOptions }: {
|
|
17
|
+
platform: string;
|
|
18
|
+
fingerprintHash: string;
|
|
19
|
+
buildPath?: string;
|
|
20
|
+
runOptions?: RunOptions;
|
|
21
|
+
}): Promise<string | null>;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { buildCacheKey, cacheRoot, resolveBuildCache, uploadBuildCache };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let node_fs = require("node:fs");
|
|
25
|
+
node_fs = __toESM(node_fs);
|
|
26
|
+
let node_os = require("node:os");
|
|
27
|
+
node_os = __toESM(node_os);
|
|
28
|
+
let node_path = require("node:path");
|
|
29
|
+
node_path = __toESM(node_path);
|
|
30
|
+
let node_child_process = require("node:child_process");
|
|
31
|
+
//#region index.ts
|
|
32
|
+
function configDir() {
|
|
33
|
+
return process.env.RN_ISO_HOME || node_path.default.join(node_os.default.homedir(), ".rn-iso");
|
|
34
|
+
}
|
|
35
|
+
function cacheRoot() {
|
|
36
|
+
return process.env.RN_ISO_BUILD_CACHE || node_path.default.join(configDir(), "build-cache");
|
|
37
|
+
}
|
|
38
|
+
function entryDir(platform, key) {
|
|
39
|
+
return node_path.default.join(cacheRoot(), platform, key);
|
|
40
|
+
}
|
|
41
|
+
const SIMULATOR_UDID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
42
|
+
const EMULATOR_SERIAL = /^emulator-\d+$/;
|
|
43
|
+
function slug(value) {
|
|
44
|
+
return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
45
|
+
}
|
|
46
|
+
function buildVariant(platform, options) {
|
|
47
|
+
const raw = platform === "android" ? options.variant : options.configuration != null ? options.configuration : options.buildConfiguration;
|
|
48
|
+
return (typeof raw === "string" ? slug(raw) : "") || "debug";
|
|
49
|
+
}
|
|
50
|
+
function buildTarget(options) {
|
|
51
|
+
if (typeof options.isSimulator === "boolean") return options.isSimulator ? "sim" : "device";
|
|
52
|
+
const device = options.device;
|
|
53
|
+
if (device === void 0 || device === null || device === false) return "sim";
|
|
54
|
+
if (typeof device !== "string") return "prompted";
|
|
55
|
+
const name = device.trim();
|
|
56
|
+
if (name === "" || name === "generic") return "sim";
|
|
57
|
+
if (SIMULATOR_UDID.test(name) || EMULATOR_SERIAL.test(name)) return "sim";
|
|
58
|
+
return `on-${slug(name)}`;
|
|
59
|
+
}
|
|
60
|
+
function buildCacheKey(platform, fingerprintHash, runOptions) {
|
|
61
|
+
const opts = runOptions && typeof runOptions === "object" ? runOptions : {};
|
|
62
|
+
return `${fingerprintHash}-${buildVariant(platform, opts)}-${buildTarget(opts)}`;
|
|
63
|
+
}
|
|
64
|
+
function shortKey(key, fingerprintHash) {
|
|
65
|
+
return `${String(fingerprintHash).slice(0, 12)}${key.slice(String(fingerprintHash).length)}`;
|
|
66
|
+
}
|
|
67
|
+
function artifactIn(dir) {
|
|
68
|
+
if (!node_fs.default.existsSync(dir)) return null;
|
|
69
|
+
let found;
|
|
70
|
+
try {
|
|
71
|
+
found = node_fs.default.readdirSync(dir).find((f) => f.endsWith(".app") || f.endsWith(".apk"));
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return found ? node_path.default.join(dir, found) : null;
|
|
76
|
+
}
|
|
77
|
+
function registerCache({ dir, name, prune, note, entriesDepth }) {
|
|
78
|
+
try {
|
|
79
|
+
const home = configDir();
|
|
80
|
+
const file = node_path.default.join(home, "caches.json");
|
|
81
|
+
let manifest = {
|
|
82
|
+
version: 1,
|
|
83
|
+
caches: []
|
|
84
|
+
};
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(node_fs.default.readFileSync(file, "utf-8"));
|
|
87
|
+
if (Array.isArray(parsed?.caches)) manifest = {
|
|
88
|
+
version: 1,
|
|
89
|
+
caches: parsed.caches
|
|
90
|
+
};
|
|
91
|
+
} catch {}
|
|
92
|
+
const others = manifest.caches.filter((c) => c.dir !== dir);
|
|
93
|
+
const record = {
|
|
94
|
+
dir,
|
|
95
|
+
name,
|
|
96
|
+
prune,
|
|
97
|
+
note,
|
|
98
|
+
registeredBy: process.cwd()
|
|
99
|
+
};
|
|
100
|
+
if (entriesDepth) record.entriesDepth = entriesDepth;
|
|
101
|
+
others.push(record);
|
|
102
|
+
node_fs.default.mkdirSync(home, { recursive: true });
|
|
103
|
+
node_fs.default.writeFileSync(file, JSON.stringify({
|
|
104
|
+
version: 1,
|
|
105
|
+
caches: others
|
|
106
|
+
}, null, 2));
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
let registeredDir = null;
|
|
110
|
+
function registerOnce() {
|
|
111
|
+
const root = cacheRoot();
|
|
112
|
+
if (registeredDir === root) return;
|
|
113
|
+
registeredDir = root;
|
|
114
|
+
registerCache({
|
|
115
|
+
dir: root,
|
|
116
|
+
name: "Expo build cache",
|
|
117
|
+
prune: "entries",
|
|
118
|
+
entriesDepth: 2,
|
|
119
|
+
note: "built .app/.apk keyed on the native fingerprint"
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
async function resolveBuildCache({ platform, fingerprintHash, runOptions }) {
|
|
123
|
+
registerOnce();
|
|
124
|
+
const key = buildCacheKey(platform, fingerprintHash, runOptions);
|
|
125
|
+
const hit = artifactIn(entryDir(platform, key));
|
|
126
|
+
if (hit) {
|
|
127
|
+
console.log(`[build-cache] hit ${platform} ${shortKey(key, fingerprintHash)}`);
|
|
128
|
+
try {
|
|
129
|
+
node_fs.default.utimesSync(node_path.default.dirname(hit), /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date());
|
|
130
|
+
} catch {}
|
|
131
|
+
return hit;
|
|
132
|
+
}
|
|
133
|
+
console.log(`[build-cache] miss ${platform} ${shortKey(key, fingerprintHash)}`);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
async function uploadBuildCache({ platform, fingerprintHash, buildPath, runOptions }) {
|
|
137
|
+
registerOnce();
|
|
138
|
+
if (!buildPath || !node_fs.default.existsSync(buildPath)) return null;
|
|
139
|
+
const key = buildCacheKey(platform, fingerprintHash, runOptions);
|
|
140
|
+
const dest = entryDir(platform, key);
|
|
141
|
+
if (artifactIn(dest)) return artifactIn(dest);
|
|
142
|
+
const staging = `${dest}.staging-${process.pid}`;
|
|
143
|
+
node_fs.default.rmSync(staging, {
|
|
144
|
+
recursive: true,
|
|
145
|
+
force: true
|
|
146
|
+
});
|
|
147
|
+
node_fs.default.mkdirSync(staging, { recursive: true });
|
|
148
|
+
(0, node_child_process.execFileSync)("cp", [
|
|
149
|
+
"-R",
|
|
150
|
+
buildPath,
|
|
151
|
+
node_path.default.join(staging, node_path.default.basename(buildPath))
|
|
152
|
+
]);
|
|
153
|
+
node_fs.default.mkdirSync(node_path.default.dirname(dest), { recursive: true });
|
|
154
|
+
node_fs.default.rmSync(dest, {
|
|
155
|
+
recursive: true,
|
|
156
|
+
force: true
|
|
157
|
+
});
|
|
158
|
+
try {
|
|
159
|
+
node_fs.default.renameSync(staging, dest);
|
|
160
|
+
} catch {
|
|
161
|
+
node_fs.default.rmSync(staging, {
|
|
162
|
+
recursive: true,
|
|
163
|
+
force: true
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
console.log(`[build-cache] stored ${platform} ${shortKey(key, fingerprintHash)}`);
|
|
167
|
+
return artifactIn(dest);
|
|
168
|
+
}
|
|
169
|
+
//#endregion
|
|
170
|
+
exports.buildCacheKey = buildCacheKey;
|
|
171
|
+
exports.cacheRoot = cacheRoot;
|
|
172
|
+
exports.resolveBuildCache = resolveBuildCache;
|
|
173
|
+
exports.uploadBuildCache = uploadBuildCache;
|
package/package.json
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rn-iso/expo-build-cache",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Local Expo build cache provider: installs a cached .app/.apk instead of compiling when no native input changed.",
|
|
5
|
-
"
|
|
5
|
+
"license": "MIT",
|
|
6
6
|
"files": [
|
|
7
|
-
"
|
|
8
|
-
"README.md"
|
|
7
|
+
"dist",
|
|
8
|
+
"README.md",
|
|
9
|
+
"LICENSE"
|
|
9
10
|
],
|
|
10
|
-
"
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"types": "dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"require": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
11
20
|
"scripts": {
|
|
12
|
-
"test": "node --test test/*.test.js"
|
|
21
|
+
"test": "node --test test/*.test.js",
|
|
22
|
+
"build": "tsdown"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22"
|
|
13
26
|
}
|
|
14
|
-
}
|
|
27
|
+
}
|
package/index.js
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
// A local Expo build cache provider.
|
|
2
|
-
//
|
|
3
|
-
// Expo fingerprints the project's native inputs and hands the hash to a
|
|
4
|
-
// provider. If a build already exists for that hash, the provider returns it and
|
|
5
|
-
// the CLI installs it instead of compiling. That is the difference between a
|
|
6
|
-
// JS-only change costing a simulator boot and costing a full native build, and
|
|
7
|
-
// most changes are JS-only.
|
|
8
|
-
//
|
|
9
|
-
// Local on purpose: a directory under $HOME shared by every worktree on the
|
|
10
|
-
// machine. No account, no network, and a second worktree building the same
|
|
11
|
-
// commit is a hit rather than a second five-minute build.
|
|
12
|
-
//
|
|
13
|
-
// Wire it up in app.json. Which key depends on the SDK, and the wrong one is a
|
|
14
|
-
// silent no-op rather than an error:
|
|
15
|
-
//
|
|
16
|
-
// SDK 54+ { "expo": { "buildCacheProvider": { "plugin": "@rn-iso/expo-build-cache" } } }
|
|
17
|
-
// SDK 53 { "expo": { "experiments": { "buildCacheProvider": { ... } } } }
|
|
18
|
-
//
|
|
19
|
-
// SDK 53's CLI reads only the experiments key and ignores the top-level one
|
|
20
|
-
// without saying so; SDK 54+ reads the top-level key and falls back to
|
|
21
|
-
// experiments.
|
|
22
|
-
|
|
23
|
-
const fs = require('node:fs');
|
|
24
|
-
const os = require('node:os');
|
|
25
|
-
const path = require('node:path');
|
|
26
|
-
const { execFileSync } = require('node:child_process');
|
|
27
|
-
|
|
28
|
-
// THIS RESOLUTION EXISTS THREE TIMES: here, in packages/metro/index.js,
|
|
29
|
-
// and in rn-iso's own src/paths.js (sharedBuildCache / sharedMetroCache). This
|
|
30
|
-
// package cannot import that module -- it has to work on a machine with no
|
|
31
|
-
// rn-iso installed at all -- so the duplication is deliberate, exactly like
|
|
32
|
-
// buildCacheKey below. Change one and you must change all three: when they
|
|
33
|
-
// drift, the CLI stores a build in one directory and this provider looks for it
|
|
34
|
-
// in another, and neither of them says so. rn-iso's
|
|
35
|
-
// test/cache-packages.test.js asserts all three agree.
|
|
36
|
-
//
|
|
37
|
-
// RN_ISO_BUILD_CACHE comes first because it did before the layout existed, and
|
|
38
|
-
// quietly ignoring an override someone already set reads as an empty cache
|
|
39
|
-
// rather than as an error.
|
|
40
|
-
function configDir() {
|
|
41
|
-
return process.env.RN_ISO_HOME || path.join(os.homedir(), '.rn-iso');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// A function rather than a constant: resolving it at load time froze whatever
|
|
45
|
-
// the environment was when a metro.config.js or an Expo config first required
|
|
46
|
-
// this file, which is not necessarily what it is when a build runs.
|
|
47
|
-
function cacheRoot() {
|
|
48
|
-
return process.env.RN_ISO_BUILD_CACHE || path.join(configDir(), 'build-cache');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function entryDir(platform, key) {
|
|
52
|
-
return path.join(cacheRoot(), platform, key);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// A simulator udid is a canonical UUID. Apple's hardware identifiers are not:
|
|
56
|
-
// they are 40 hex characters, or the 8-digits-dash-16-hex form newer devices
|
|
57
|
-
// use.
|
|
58
|
-
const SIMULATOR_UDID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
59
|
-
// adb's name for a running emulator.
|
|
60
|
-
const EMULATOR_SERIAL = /^emulator-\d+$/;
|
|
61
|
-
|
|
62
|
-
function slug(value) {
|
|
63
|
-
return String(value).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// The Xcode configuration on iOS, the gradle variant on Android. `expo run:ios`
|
|
67
|
-
// defaults to Debug and `expo run:android` to debug, so an absent value is that
|
|
68
|
-
// and not "unknown".
|
|
69
|
-
function buildVariant(platform, options) {
|
|
70
|
-
const raw = platform === 'android'
|
|
71
|
-
? options.variant
|
|
72
|
-
: (options.configuration != null ? options.configuration : options.buildConfiguration);
|
|
73
|
-
return (typeof raw === 'string' ? slug(raw) : '') || 'debug';
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// A binary built for real hardware cannot run on a simulator, and the reverse is
|
|
77
|
-
// equally true, so the target class is part of the key. `runOptions.device` is
|
|
78
|
-
// the only signal Expo passes, and it is ambiguous by nature:
|
|
79
|
-
// absent -- the CLI targets a simulator or emulator. The default.
|
|
80
|
-
// "generic" -- a build-only simulator build.
|
|
81
|
-
// a udid or serial -- classifiable when it has the shape of a simulator id.
|
|
82
|
-
// a bare -d flag -- the CLI prompts, and the answer can be hardware.
|
|
83
|
-
// a name -- unclassifiable.
|
|
84
|
-
// The last two get a bucket of their own rather than sharing the simulator one:
|
|
85
|
-
// a wasted rebuild is cheap, and a binary that cannot launch is not. Two
|
|
86
|
-
// workspaces naming the same device still share their entries.
|
|
87
|
-
function buildTarget(options) {
|
|
88
|
-
if (typeof options.isSimulator === 'boolean') return options.isSimulator ? 'sim' : 'device';
|
|
89
|
-
const device = options.device;
|
|
90
|
-
if (device === undefined || device === null || device === false) return 'sim';
|
|
91
|
-
if (typeof device !== 'string') return 'prompted';
|
|
92
|
-
const name = device.trim();
|
|
93
|
-
if (name === '' || name === 'generic') return 'sim';
|
|
94
|
-
if (SIMULATOR_UDID.test(name) || EMULATOR_SERIAL.test(name)) return 'sim';
|
|
95
|
-
return `on-${slug(name)}`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// The fingerprint covers what the project IS, never how it was built. Keying on
|
|
99
|
-
// it alone means a Release build answers a Debug resolve, and a device build
|
|
100
|
-
// answers a simulator one -- both silently, both producing a binary that cannot
|
|
101
|
-
// run. Only the run-option keys named above are read, so a future Expo CLI
|
|
102
|
-
// cannot change the key by adding one.
|
|
103
|
-
//
|
|
104
|
-
// rn-iso's own `rn-iso build-cache` command computes this key the same way
|
|
105
|
-
// (src/build-cache.js), so both entry points address the same entry. Changing
|
|
106
|
-
// one without the other splits them onto separate sets of entries.
|
|
107
|
-
function buildCacheKey(platform, fingerprintHash, runOptions) {
|
|
108
|
-
const opts = runOptions && typeof runOptions === 'object' ? runOptions : {};
|
|
109
|
-
return `${fingerprintHash}-${buildVariant(platform, opts)}-${buildTarget(opts)}`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Log lines name the entry, so they have to carry what distinguishes it: the
|
|
113
|
-
// fingerprint abbreviates fine, the variant and target do not -- a Debug miss
|
|
114
|
-
// and a Release miss on the same commit read identically without them.
|
|
115
|
-
function shortKey(key, fingerprintHash) {
|
|
116
|
-
return `${String(fingerprintHash).slice(0, 12)}${key.slice(String(fingerprintHash).length)}`;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// The cached artifact is the single .app / .apk inside the entry directory.
|
|
120
|
-
function artifactIn(dir) {
|
|
121
|
-
if (!fs.existsSync(dir)) return null;
|
|
122
|
-
const found = fs.readdirSync(dir).find(f => f.endsWith('.app') || f.endsWith('.apk'));
|
|
123
|
-
return found ? path.join(dir, found) : null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Registering makes this cache visible to `rn-iso gc`'s report, which is the
|
|
127
|
-
// only thing that will ever trim it.
|
|
128
|
-
//
|
|
129
|
-
// The manifest is written directly rather than through rn-iso's own module, for
|
|
130
|
-
// two reasons that both made the import silently do nothing:
|
|
131
|
-
// - the documented way to use the CLI is `npx rn-iso`, so it is usually not a
|
|
132
|
-
// dependency of the project and the specifier does not resolve at all
|
|
133
|
-
// - rn-iso is an ES module, so `require` of it throws ERR_REQUIRE_ESM on Node
|
|
134
|
-
// before 20.19
|
|
135
|
-
// A dynamic import fixes the second and not the first. The format is a stable
|
|
136
|
-
// contract, so writing it is the cheaper trade.
|
|
137
|
-
function registerCache({ dir, name, prune, note, entriesDepth }) {
|
|
138
|
-
try {
|
|
139
|
-
const home = configDir();
|
|
140
|
-
const file = path.join(home, 'caches.json');
|
|
141
|
-
let manifest = { version: 1, caches: [] };
|
|
142
|
-
try {
|
|
143
|
-
const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
144
|
-
if (Array.isArray(parsed?.caches)) manifest = { version: 1, caches: parsed.caches };
|
|
145
|
-
} catch {
|
|
146
|
-
// No manifest yet, or an unreadable one: start clean rather than fail.
|
|
147
|
-
}
|
|
148
|
-
// Keyed on the directory so repeated calls update rather than accumulate --
|
|
149
|
-
// these run on every build.
|
|
150
|
-
const others = manifest.caches.filter(c => c.dir !== dir);
|
|
151
|
-
const record = { dir, name, prune, note, registeredBy: process.cwd() };
|
|
152
|
-
// Only written when the caller sets it: an absent depth means the entries
|
|
153
|
-
// are the directory's immediate children, which is the common case.
|
|
154
|
-
if (entriesDepth) record.entriesDepth = entriesDepth;
|
|
155
|
-
others.push(record);
|
|
156
|
-
fs.mkdirSync(home, { recursive: true });
|
|
157
|
-
fs.writeFileSync(file, JSON.stringify({ version: 1, caches: others }, null, 2));
|
|
158
|
-
} catch {
|
|
159
|
-
// A cache that cannot announce itself still works; it is just invisible.
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Keyed on the directory rather than a plain boolean, so a root that changes
|
|
164
|
-
// under a long-lived process still reaches the manifest.
|
|
165
|
-
let registeredDir = null;
|
|
166
|
-
function registerOnce() {
|
|
167
|
-
const root = cacheRoot();
|
|
168
|
-
if (registeredDir === root) return;
|
|
169
|
-
registeredDir = root;
|
|
170
|
-
registerCache({
|
|
171
|
-
dir: root,
|
|
172
|
-
name: 'Expo build cache',
|
|
173
|
-
// Every entry is an independent directory keyed by fingerprint, so old ones
|
|
174
|
-
// can be trimmed individually. They sit two levels down --
|
|
175
|
-
// <root>/<platform>/<key> -- and gc has to be told, or it treats ios/ and
|
|
176
|
-
// android/ as the entries and one removal takes a whole platform.
|
|
177
|
-
prune: 'entries',
|
|
178
|
-
entriesDepth: 2,
|
|
179
|
-
note: 'built .app/.apk keyed on the native fingerprint',
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// The hash the CLI passes is stable once .fingerprintignore excludes generated
|
|
184
|
-
// files that embed absolute paths -- ios/Podfile.lock is the usual culprit, as
|
|
185
|
-
// pod checksums can carry machine-specific paths. Use it directly rather than
|
|
186
|
-
// recomputing: fingerprinting walks node_modules and is otherwise the single
|
|
187
|
-
// largest cost of a cache hit.
|
|
188
|
-
async function resolveBuildCache({ platform, fingerprintHash, runOptions }) {
|
|
189
|
-
registerOnce();
|
|
190
|
-
const key = buildCacheKey(platform, fingerprintHash, runOptions);
|
|
191
|
-
const hit = artifactIn(entryDir(platform, key));
|
|
192
|
-
if (hit) {
|
|
193
|
-
console.log(`[build-cache] hit ${platform} ${shortKey(key, fingerprintHash)}`);
|
|
194
|
-
// Touch on hit so age-based trimming can tell a working entry from a dead
|
|
195
|
-
// one. Without this, the entries earning their keep look identical to the
|
|
196
|
-
// ones nothing has used in months.
|
|
197
|
-
fs.utimesSync(path.dirname(hit), new Date(), new Date());
|
|
198
|
-
return hit;
|
|
199
|
-
}
|
|
200
|
-
console.log(`[build-cache] miss ${platform} ${shortKey(key, fingerprintHash)}`);
|
|
201
|
-
return null;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async function uploadBuildCache({ platform, fingerprintHash, buildPath, runOptions }) {
|
|
205
|
-
registerOnce();
|
|
206
|
-
if (!buildPath || !fs.existsSync(buildPath)) return null;
|
|
207
|
-
|
|
208
|
-
const key = buildCacheKey(platform, fingerprintHash, runOptions);
|
|
209
|
-
const dest = entryDir(platform, key);
|
|
210
|
-
if (artifactIn(dest)) return artifactIn(dest);
|
|
211
|
-
|
|
212
|
-
// Stage in a sibling and rename into place. A copy interrupted halfway must
|
|
213
|
-
// never be readable as a complete entry by a worktree building in parallel,
|
|
214
|
-
// and rename is the only step that is atomic.
|
|
215
|
-
const staging = `${dest}.staging-${process.pid}`;
|
|
216
|
-
fs.rmSync(staging, { recursive: true, force: true });
|
|
217
|
-
fs.mkdirSync(staging, { recursive: true });
|
|
218
|
-
execFileSync('cp', ['-R', buildPath, path.join(staging, path.basename(buildPath))]);
|
|
219
|
-
|
|
220
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
221
|
-
fs.rmSync(dest, { recursive: true, force: true });
|
|
222
|
-
fs.renameSync(staging, dest);
|
|
223
|
-
|
|
224
|
-
console.log(`[build-cache] stored ${platform} ${shortKey(key, fingerprintHash)}`);
|
|
225
|
-
return artifactIn(dest);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
module.exports = { resolveBuildCache, uploadBuildCache, buildCacheKey, cacheRoot };
|