Skip to content

Commit e509e49

Browse files
committed
wip
1 parent dc4f4a6 commit e509e49

File tree

2 files changed

+263
-38
lines changed

2 files changed

+263
-38
lines changed

packages/tools/src/lib/build-cache/getBinaryPath.ts

Lines changed: 234 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { color, colorLink } from '../color.js';
33
import type { RockError } from '../error.js';
44
import { getAllIgnorePaths } from '../fingerprint/ignorePaths.js';
55
import { type FingerprintSources } from '../fingerprint/index.js';
6+
import { isInteractive } from '../isInteractive.js';
67
import logger from '../logger.js';
78
import { getProjectRoot } from '../project.js';
9+
import { promptConfirm } from '../prompts.js';
810
import { spawn } from '../spawn.js';
9-
import type { RemoteBuildCache } from './common.js';
11+
import { formatArtifactName, type RemoteBuildCache } from './common.js';
1012
import { fetchCachedBuild } from './fetchCachedBuild.js';
11-
import { getLocalBuildCacheBinaryPath } from './localBuildCache.js';
13+
import { getLocalBuildCacheBinaryPath, hasUsedRemoteCacheBefore } from './localBuildCache.js';
1214

1315
export async function getBinaryPath({
1416
artifactName,
@@ -37,50 +39,45 @@ export async function getBinaryPath({
3739

3840
// 3. If not, check if the remote cache is requested
3941
if (!binaryPath && !localFlag) {
40-
try {
41-
const cachedBuild = await fetchCachedBuild({
42-
artifactName,
43-
remoteCacheProvider,
44-
});
45-
if (cachedBuild) {
46-
binaryPath = cachedBuild.binaryPath;
47-
}
48-
} catch (error) {
49-
const message = (error as RockError).message;
50-
const cause = (error as RockError).cause;
51-
logger.warn(
52-
`Remote Cache: Failed to fetch cached build for ${color.bold(
53-
artifactName,
54-
)}.
55-
Cause: ${message}${cause ? `\n${cause.toString()}` : ''}
56-
Read more: ${colorLink(
57-
'https://rockjs.dev/docs/configuration#remote-cache-configuration',
58-
)}`,
59-
);
60-
await warnIgnoredFiles(fingerprintOptions, platformName, sourceDir);
61-
logger.debug('Remote cache failure error:', error);
62-
logger.info('Continuing with local build');
63-
}
42+
binaryPath = await tryFetchCachedBuild({
43+
artifactName,
44+
remoteCacheProvider,
45+
fingerprintOptions,
46+
platformName,
47+
sourceDir,
48+
});
6449
}
6550

6651
return binaryPath;
6752
}
6853

69-
async function warnIgnoredFiles(
70-
fingerprintOptions: FingerprintSources,
71-
platformName: string,
72-
sourceDir: string,
73-
) {
74-
// @todo unify git helpers from create-app
54+
/**
55+
* Checks if the current directory is a git repository
56+
*/
57+
async function isGitRepository(sourceDir: string): Promise<boolean> {
7558
try {
7659
await spawn('git', ['rev-parse', '--is-inside-work-tree'], {
7760
stdio: 'ignore',
7861
cwd: sourceDir,
7962
});
63+
return true;
8064
} catch {
81-
// Not a git repository, skip the git clean check
82-
return;
65+
return false;
8366
}
67+
}
68+
69+
/**
70+
* Gets the list of files that would be removed by git clean
71+
*/
72+
async function getFilesToClean(
73+
fingerprintOptions: FingerprintSources,
74+
platformName: string,
75+
sourceDir: string,
76+
): Promise<string[]> {
77+
if (!(await isGitRepository(sourceDir))) {
78+
return [];
79+
}
80+
8481
const projectRoot = getProjectRoot();
8582
const ignorePaths = [
8683
...(fingerprintOptions?.ignorePaths ?? []),
@@ -90,21 +87,61 @@ async function warnIgnoredFiles(
9087
projectRoot,
9188
),
9289
];
90+
9391
const { output } = await spawn('git', [
9492
'clean',
9593
'-fdx',
9694
'--dry-run',
9795
sourceDir,
9896
...ignorePaths.flatMap((path) => ['-e', `${path}`]),
9997
]);
100-
const ignoredFiles = output
98+
99+
return output
101100
.split('\n')
102101
.map((line) => line.replace('Would remove ', ''))
103102
.filter((line) => line !== '');
103+
}
104+
105+
/**
106+
* Executes git clean to remove files
107+
*/
108+
async function executeGitClean(
109+
fingerprintOptions: FingerprintSources,
110+
platformName: string,
111+
sourceDir: string,
112+
): Promise<void> {
113+
if (!(await isGitRepository(sourceDir))) {
114+
throw new Error('Not a git repository');
115+
}
104116

105-
if (ignoredFiles.length > 0) {
117+
const projectRoot = getProjectRoot();
118+
const ignorePaths = [
119+
...(fingerprintOptions?.ignorePaths ?? []),
120+
...getAllIgnorePaths(
121+
platformName,
122+
path.relative(projectRoot, sourceDir), // git expects relative paths
123+
projectRoot,
124+
),
125+
];
126+
127+
await spawn('git', [
128+
'clean',
129+
'-fdx',
130+
sourceDir,
131+
...ignorePaths.flatMap((path) => ['-e', `${path}`]),
132+
]);
133+
}
134+
135+
async function warnIgnoredFiles(
136+
fingerprintOptions: FingerprintSources,
137+
platformName: string,
138+
sourceDir: string,
139+
) {
140+
const filesToClean = await getFilesToClean(fingerprintOptions, platformName, sourceDir);
141+
142+
if (filesToClean.length > 0) {
106143
logger.warn(`There are files that likely affect fingerprint:
107-
${ignoredFiles.map((file) => `- ${color.bold(file)}`).join('\n')}
144+
${filesToClean.map((file) => `- ${color.bold(file)}`).join('\n')}
108145
Consider removing them or update ${color.bold(
109146
'fingerprint.ignorePaths',
110147
)} in ${colorLink('rock.config.mjs')}:
@@ -113,3 +150,163 @@ Read more: ${colorLink(
113150
)}`);
114151
}
115152
}
153+
154+
/**
155+
* Tries to fetch cached build with optional debugging workflow
156+
*/
157+
async function tryFetchCachedBuild({
158+
artifactName,
159+
remoteCacheProvider,
160+
fingerprintOptions,
161+
platformName,
162+
sourceDir,
163+
}: {
164+
artifactName: string;
165+
remoteCacheProvider: null | (() => RemoteBuildCache) | undefined;
166+
fingerprintOptions: FingerprintSources;
167+
platformName: string;
168+
sourceDir: string;
169+
}): Promise<string | undefined> {
170+
try {
171+
const cachedBuild = await fetchCachedBuild({
172+
artifactName,
173+
remoteCacheProvider,
174+
});
175+
if (cachedBuild) {
176+
return cachedBuild.binaryPath;
177+
}
178+
} catch (error) {
179+
const message = (error as RockError).message;
180+
const cause = (error as RockError).cause;
181+
logger.warn(
182+
`Remote Cache: Failed to fetch cached build for ${color.bold(
183+
artifactName,
184+
)}.
185+
Cause: ${message}${cause ? `\n${cause.toString()}` : ''}
186+
Read more: ${colorLink(
187+
'https://rockjs.dev/docs/configuration#remote-cache-configuration',
188+
)}`,
189+
);
190+
191+
// Check if user has used remote cache before and offer debugging
192+
if (isInteractive() && hasUsedRemoteCacheBefore()) {
193+
const cleanedAndRetried = await runCacheMissDebugging({
194+
fingerprintOptions,
195+
platformName,
196+
sourceDir,
197+
artifactName,
198+
remoteCacheProvider,
199+
});
200+
201+
if (cleanedAndRetried) {
202+
return cleanedAndRetried;
203+
}
204+
}
205+
206+
await warnIgnoredFiles(fingerprintOptions, platformName, sourceDir);
207+
logger.debug('Remote cache failure error:', error);
208+
logger.info('Continuing with local build');
209+
}
210+
211+
return undefined;
212+
}
213+
214+
/**
215+
* Runs the cache miss debugging workflow and returns binary path if successful
216+
*/
217+
async function runCacheMissDebugging({
218+
fingerprintOptions,
219+
platformName,
220+
sourceDir,
221+
artifactName,
222+
remoteCacheProvider,
223+
}: {
224+
fingerprintOptions: FingerprintSources;
225+
platformName: string;
226+
sourceDir: string;
227+
artifactName: string;
228+
remoteCacheProvider: null | (() => RemoteBuildCache) | undefined;
229+
}): Promise<string | undefined> {
230+
logger.info(''); // Add spacing
231+
const shouldDebug = await promptConfirm({
232+
message: `Would you like to debug this remote cache miss?`,
233+
confirmLabel: 'Yes, help me debug this',
234+
cancelLabel: 'No, continue with local build',
235+
});
236+
237+
if (!shouldDebug) {
238+
return undefined;
239+
}
240+
241+
// Step 1: Check what files would be cleaned
242+
const filesToClean = await getFilesToClean(fingerprintOptions, platformName, sourceDir);
243+
244+
if (filesToClean.length === 0) {
245+
logger.info('✅ No files found that would affect fingerprinting.');
246+
// TODO: backlink to the docs here instead of a 404
247+
logger.info(' The cache miss might be due to other factors (CI environment, etc.)');
248+
return undefined;
249+
}
250+
251+
// Step 2: Show user what would be cleaned and offer to clean
252+
logger.info(`📋 Found ${color.bold(filesToClean.length.toString())} files that affect cache fingerprint:`);
253+
filesToClean.slice(0, 10).forEach(file => {
254+
logger.info(` - ${color.bold(file)}`);
255+
});
256+
257+
if (filesToClean.length > 10) {
258+
logger.info(` ... and ${filesToClean.length - 10} more files`);
259+
}
260+
logger.info(''); // Add spacing
261+
262+
const shouldClean = await promptConfirm({
263+
message: `Clean these files and retry fetching?`,
264+
confirmLabel: 'Yes, clean files and retry',
265+
cancelLabel: 'No, continue with local build',
266+
});
267+
268+
if (!shouldClean) {
269+
return undefined;
270+
}
271+
272+
// Step 3: Clean files first
273+
logger.info('🧹 Cleaning files...');
274+
try {
275+
await executeGitClean(fingerprintOptions, platformName, sourceDir);
276+
logger.info('✅ Files cleaned successfully');
277+
logger.info(''); // Add spacing
278+
} catch (error) {
279+
logger.error(`❌ Failed to clean files: ${error}`);
280+
logger.info(' Continuing with local build...');
281+
return undefined;
282+
}
283+
284+
// Extract platform and traits from the original artifact name to recalculate
285+
const projectRoot = getProjectRoot();
286+
const nameParts = artifactName.split('-');
287+
const platform = nameParts[1] as 'ios' | 'android' | 'harmony';
288+
const traits = nameParts.slice(2, -1); // Everything except 'rock', platform, and hash
289+
290+
const cleanArtifactName = await formatArtifactName({
291+
platform,
292+
traits,
293+
root: projectRoot,
294+
fingerprintOptions,
295+
});
296+
297+
// Step 5: Retry the fetch with the correct artifact name
298+
logger.info('🔄 Retrying remote cache with clean fingerprint...');
299+
300+
const cachedBuild = await fetchCachedBuild({
301+
artifactName: cleanArtifactName,
302+
remoteCacheProvider,
303+
});
304+
305+
if (cachedBuild) {
306+
logger.info('✅ Successfully fetched from remote cache after cleaning!');
307+
return cachedBuild.binaryPath;
308+
} else {
309+
logger.info('❌ Remote cache still missed after cleaning. Continuing with local build...');
310+
return undefined;
311+
}
312+
}

packages/tools/src/lib/build-cache/localBuildCache.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import path from 'node:path';
33
import { color, colorLink } from '../color.js';
44
import logger from '../logger.js';
55
import { relativeToCwd } from '../path.js';
6-
import { getLocalArtifactPath, getLocalBinaryPath } from './common.js';
6+
import { getCacheRootPath } from '../project.js';
7+
import { BUILD_CACHE_DIR, getLocalArtifactPath, getLocalBinaryPath } from './common.js';
78

89
export type LocalBuild = {
910
name: string;
@@ -63,3 +64,30 @@ export function getLocalBuildCacheBinaryPath(
6364
}
6465
return undefined;
6566
}
67+
68+
/**
69+
* Checks if there are any existing remote cache artifacts, indicating previous successful remote cache usage.
70+
*/
71+
export function hasUsedRemoteCacheBefore(): boolean {
72+
try {
73+
const remoteCacheDir = path.join(getCacheRootPath(), BUILD_CACHE_DIR);
74+
75+
if (!fs.existsSync(remoteCacheDir)) {
76+
return false;
77+
}
78+
79+
const entries = fs.readdirSync(remoteCacheDir);
80+
81+
// Look for any rock- directories
82+
const rockArtifacts = entries.filter(entry => {
83+
const entryPath = path.join(remoteCacheDir, entry);
84+
const stats = fs.statSync(entryPath, { throwIfNoEntry: false });
85+
return stats?.isDirectory() && entry.startsWith('rock-');
86+
});
87+
88+
return rockArtifacts.length > 0;
89+
} catch (error) {
90+
logger.debug('Failed to check remote cache usage history:', error);
91+
return false;
92+
}
93+
}

0 commit comments

Comments
 (0)