Skip to content

Commit

Permalink
[updates] Change fingeprint diff to use versioned fingerprint CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
wschurman committed Nov 7, 2024
1 parent 9b3bdfa commit 3b22599
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { vol } from 'memfs';
import fs from 'fs-extra';

import { diffFingerprintsAsync } from '../diffFingerprintsAsync';
import {
ExpoFingerprintCLIModuleNotFoundError,
expoFingerprintCommandAsync,
} from '../expoFingerprintCli';

jest.mock('../expoFingerprintCli', () => ({
...jest.requireActual('../expoFingerprintCli'),
expoFingerprintCommandAsync: jest.fn(),
}));

jest.mock('fs');

describe(diffFingerprintsAsync, () => {
beforeEach(async () => {
vol.reset();
});

it('falls back to local when CLI is not available', async () => {
await fs.mkdirp('test');
await fs.writeFile(
'test/fp1',
Buffer.from(
JSON.stringify({
sources: [
{
type: 'file',
filePath: './assets/images/adaptive-icon.png',
reasons: ['expoConfigExternalFile'],
hash: '19b53640a95efdc2ccc7fc20f3ea4d0d381bb5c4',
},
],
})
)
);

await fs.writeFile(
'test/fp2',
Buffer.from(
JSON.stringify({
sources: [
{
type: 'file',
filePath: './assets/images/adaptive-icon.png',
reasons: ['expoConfigExternalFile'],
hash: '19b53640a95efdc2ccc7fc20f3ea4d0d381bb5c5',
},
],
})
)
);

jest
.mocked(expoFingerprintCommandAsync)
.mockRejectedValue(new ExpoFingerprintCLIModuleNotFoundError());

const diff = await diffFingerprintsAsync('test', 'test/fp1', 'test/fp2', {
env: {},
logger: { debug: jest.fn() } as any,
});
expect(diff).toEqual(
JSON.stringify([
{
op: 'changed',
beforeSource: {
type: 'file',
filePath: './assets/images/adaptive-icon.png',
reasons: ['expoConfigExternalFile'],
hash: '19b53640a95efdc2ccc7fc20f3ea4d0d381bb5c4',
},
afterSource: {
type: 'file',
filePath: './assets/images/adaptive-icon.png',
reasons: ['expoConfigExternalFile'],
hash: '19b53640a95efdc2ccc7fc20f3ea4d0d381bb5c5',
},
},
])
);
});
});
56 changes: 56 additions & 0 deletions packages/build-tools/src/utils/diffFingerprintsAsync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { BuildStepEnv } from '@expo/steps';
import fs from 'fs-extra';
import { bunyan } from '@expo/logger';

import {
ExpoFingerprintCLICommandFailedError,
ExpoFingerprintCLIInvalidCommandError,
ExpoFingerprintCLIModuleNotFoundError,
expoFingerprintCommandAsync,
} from './expoFingerprintCli';
import { diffFingerprints } from './fingerprint';

export async function diffFingerprintsAsync(
projectDir: string,
fingerprint1File: string,
fingerprint2File: string,
{ env, logger }: { env: BuildStepEnv; logger: bunyan }
): Promise<string> {
try {
return await diffFingerprintsCommandAsync(projectDir, fingerprint1File, fingerprint2File, {
env,
});
} catch (e) {
if (
e instanceof ExpoFingerprintCLIModuleNotFoundError ||
e instanceof ExpoFingerprintCLICommandFailedError ||
e instanceof ExpoFingerprintCLIInvalidCommandError
) {
logger.debug('Falling back to local fingerprint diff');
return await diffFingerprintsFallbackAsync(fingerprint1File, fingerprint2File);
}
throw e;
}
}

async function diffFingerprintsCommandAsync(
projectDir: string,
fingerprint1File: string,
fingerprint2File: string,
{ env }: { env: BuildStepEnv }
): Promise<string> {
return await expoFingerprintCommandAsync(projectDir, [fingerprint1File, fingerprint2File], {
env,
});
}

async function diffFingerprintsFallbackAsync(
fingerprint1File: string,
fingerprint2File: string
): Promise<string> {
const [fingeprint1, fingerprint2] = await Promise.all([
fs.readJSON(fingerprint1File),
fs.readJSON(fingerprint2File),
]);
return JSON.stringify(diffFingerprints(fingeprint1, fingerprint2));
}
51 changes: 51 additions & 0 deletions packages/build-tools/src/utils/expoFingerprintCli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import resolveFrom from 'resolve-from';
import spawnAsync from '@expo/turtle-spawn';
import { BuildStepEnv } from '@expo/steps';

export class ExpoFingerprintCLIModuleNotFoundError extends Error {}
export class ExpoFingerprintCLIInvalidCommandError extends Error {}
export class ExpoFingerprintCLICommandFailedError extends Error {}

function resolveExpoFingerprintCLI(projectRoot: string): string {
const expoPackageRoot = resolveFrom.silent(projectRoot, 'expo/package.json');
try {
return (
resolveFrom.silent(expoPackageRoot ?? projectRoot, '@expo/fingerprint/bin/cli') ??
resolveFrom(expoPackageRoot ?? projectRoot, '@expo/fingerprint/bin/cli.js')
);
} catch (e: any) {
if (e.code === 'MODULE_NOT_FOUND') {
throw new ExpoFingerprintCLIModuleNotFoundError(
`The \`@expo/fingerprint\` package was not found.`
);
}
throw e;
}
}

export async function expoFingerprintCommandAsync(
projectDir: string,
args: string[],
{ env }: { env: BuildStepEnv }
): Promise<string> {
const expoFingerprintCli = resolveExpoFingerprintCLI(projectDir);
try {
const spawnResult = await spawnAsync(expoFingerprintCli, args, {
stdio: 'pipe',
cwd: projectDir,
env,
});
return spawnResult.stdout;
} catch (e: any) {
if (e.stderr && typeof e.stderr === 'string') {
if (e.stderr.includes('Invalid command')) {
throw new ExpoFingerprintCLIInvalidCommandError(
`The command specified by ${args} was not valid in the \`@expo/fingerprint\` CLI.`
);
} else {
throw new ExpoFingerprintCLICommandFailedError(e.stderr);
}
}
throw e;
}
}
36 changes: 23 additions & 13 deletions packages/build-tools/src/utils/expoUpdates.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import assert from 'assert';
import os from 'os';
import path from 'path';

import { v4 as uuidv4 } from 'uuid';
import { Platform, Job, BuildJob, Workflow, FingerprintSourceType } from '@expo/eas-build-job';
import semver from 'semver';
import { ExpoConfig } from '@expo/config';
Expand All @@ -24,12 +27,8 @@ import { BuildContext } from '../context';

import getExpoUpdatesPackageVersionIfInstalledAsync from './getExpoUpdatesPackageVersionIfInstalledAsync';
import { resolveRuntimeVersionAsync } from './resolveRuntimeVersionAsync';
import {
Fingerprint,
FingerprintSource,
diffFingerprints,
stringifyFingerprintDiff,
} from './fingerprint';
import { diffFingerprintsAsync } from './diffFingerprintsAsync';
import { stringifyFingerprintDiff } from './fingerprint';

export async function setRuntimeVersionNativelyAsync(
ctx: BuildContext<Job>,
Expand Down Expand Up @@ -85,7 +84,7 @@ export async function configureEASExpoUpdatesAsync(ctx: BuildContext<BuildJob>):

type ResolvedRuntime = {
resolvedRuntimeVersion: string | null;
resolvedFingerprintSources?: FingerprintSource[] | null;
resolvedFingerprintSources?: object[] | null;
};

export async function configureExpoUpdatesIfInstalledAsync(
Expand Down Expand Up @@ -176,7 +175,7 @@ export async function resolveRuntimeVersionForExpoUpdatesIfConfiguredAsync({
env: BuildStepEnv;
}): Promise<{
runtimeVersion: string | null;
fingerprintSources: FingerprintSource[] | null;
fingerprintSources: object[] | null;
} | null> {
const expoUpdatesPackageVersion = await getExpoUpdatesPackageVersionIfInstalledAsync(cwd, logger);
if (expoUpdatesPackageVersion === null) {
Expand Down Expand Up @@ -261,23 +260,34 @@ async function logDiffFingerprints({
try {
const fingerprintSource = ctx.metadata.fingerprintSource;

let localFingerprint: Fingerprint | null = null;
let localFingerprintFile: string | null = null;

if (fingerprintSource.type === FingerprintSourceType.URL) {
const result = await fetch(fingerprintSource.url);
localFingerprint = await result.json();
const localFingerprintJSON = await result.json();
localFingerprintFile = path.join(os.tmpdir(), `eas-build-${uuidv4()}-local-fingerprint`);
await fs.writeFile(localFingerprintFile, JSON.stringify(localFingerprintJSON));
} else if (fingerprintSource.type === FingerprintSourceType.PATH) {
localFingerprint = await fs.readJson(fingerprintSource.path);
localFingerprintFile = fingerprintSource.path;
} else {
ctx.logger.warn(`Invalid fingerprint source type: ${fingerprintSource.type}`);
}

if (localFingerprint) {
if (localFingerprintFile) {
const easFingerprint = {
hash: resolvedRuntimeVersion,
sources: resolvedFingerprintSources,
};
const changes = diffFingerprints(localFingerprint, easFingerprint);
const easFingerprintFile = path.join(os.tmpdir(), `eas-build-${uuidv4()}-eas-fingerprint`);
await fs.writeFile(easFingerprintFile, JSON.stringify(easFingerprint));

const changesJSONString = await diffFingerprintsAsync(
ctx.getReactNativeProjectDirectory(),
localFingerprintFile,
easFingerprintFile,
{ env: ctx.env, logger: ctx.logger }
);
const changes = JSON.parse(changesJSONString);
if (changes.length) {
ctx.logger.warn('Difference between local and EAS fingerprints:');
ctx.logger.warn(stringifyFingerprintDiff(changes));
Expand Down
3 changes: 1 addition & 2 deletions packages/build-tools/src/utils/resolveRuntimeVersionAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { BuildStepEnv } from '@expo/steps';

import { ExpoUpdatesCLIModuleNotFoundError, expoUpdatesCommandAsync } from './expoUpdatesCli';
import { isModernExpoUpdatesCLIWithRuntimeVersionCommandSupported } from './expoUpdates';
import { FingerprintSource } from './fingerprint';

export async function resolveRuntimeVersionAsync({
exp,
Expand All @@ -26,7 +25,7 @@ export async function resolveRuntimeVersionAsync({
env: BuildStepEnv;
}): Promise<{
runtimeVersion: string | null;
fingerprintSources: FingerprintSource[] | null;
fingerprintSources: object[] | null;
} | null> {
if (!isModernExpoUpdatesCLIWithRuntimeVersionCommandSupported(expoUpdatesPackageVersion)) {
logger.debug('Using expo-updates config plugin for runtime version resolution');
Expand Down

0 comments on commit 3b22599

Please sign in to comment.