Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENG-9500] Implement save-cache and restore-cache functions #289

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions packages/build-tools/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
import path from 'path';

import fs from 'fs-extra';
import { ExpoConfig } from '@expo/config';
import {
ManagedArtifactType,
BuildPhase,
BuildPhaseResult,
BuildPhaseStats,
Job,
LogMarker,
Env,
errors,
Metadata,
EnvironmentSecretType,
GenericArtifactType,
Job,
LogMarker,
ManagedArtifactType,
Metadata,
errors,
isGenericArtifact,
} from '@expo/eas-build-job';
import { ExpoConfig } from '@expo/config';
import { bunyan } from '@expo/logger';
import { BuildTrigger } from '@expo/eas-build-job/dist/common';
import { bunyan } from '@expo/logger';
import { DynamicCacheManager } from '@expo/steps';
import fs from 'fs-extra';

import { PackageManager, resolvePackageManager } from './utils/packageManager';
import { resolveBuildPhaseErrorAsync } from './buildErrors/detectError';
import { readAppConfig } from './utils/appConfig';
import { createTemporaryEnvironmentSecretFile } from './utils/environmentSecrets';
import { PackageManager, resolvePackageManager } from './utils/packageManager';

export type Artifacts = Partial<Record<ManagedArtifactType, string>>;

Expand Down Expand Up @@ -53,6 +54,7 @@ export interface BuildContextOptions {
logBuffer: LogBuffer;
env: Env;
cacheManager?: CacheManager;
dynamicCacheManager?: DynamicCacheManager;
uploadArtifact: (spec: { artifact: ArtifactToUpload; logger: bunyan }) => Promise<string | null>;
reportError?: (
msg: string,
Expand All @@ -71,6 +73,10 @@ export class BuildContext<TJob extends Job = Job> {
public logger: bunyan;
public readonly logBuffer: LogBuffer;
public readonly cacheManager?: CacheManager;
public readonly dynamicCacheManager?: DynamicCacheManager;
/**
* @deprecated
*/
public readonly reportError?: (
msg: string,
err?: Error,
Expand All @@ -96,6 +102,7 @@ export class BuildContext<TJob extends Job = Job> {
this.logger = this.defaultLogger;
this.logBuffer = options.logBuffer;
this.cacheManager = options.cacheManager;
this.dynamicCacheManager = options.dynamicCacheManager;
this._uploadArtifact = options.uploadArtifact;
this.reportError = options.reportError;
this._job = job;
Expand Down
14 changes: 12 additions & 2 deletions packages/build-tools/src/customBuildContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ import {
Platform,
} from '@expo/eas-build-job';
import { bunyan } from '@expo/logger';
import { ExternalBuildContextProvider, BuildRuntimePlatform } from '@expo/steps';
import {
BuildRuntimePlatform,
DynamicCacheManager,
ExternalBuildContextProvider,
} from '@expo/steps';

import { ArtifactToUpload, BuildContext } from './context';
import { ArtifactToUpload, BuildContext, CacheManager } from './context';

const platformToBuildRuntimePlatform: Record<Platform, BuildRuntimePlatform> = {
[Platform.ANDROID]: BuildRuntimePlatform.LINUX,
Expand All @@ -23,6 +27,7 @@ const platformToBuildRuntimePlatform: Record<Platform, BuildRuntimePlatform> = {

export interface BuilderRuntimeApi {
uploadArtifact: (spec: { artifact: ArtifactToUpload; logger: bunyan }) => Promise<void>;
cacheManager?: DynamicCacheManager;
}

export class CustomBuildContext<TJob extends Job = Job> implements ExternalBuildContextProvider {
Expand Down Expand Up @@ -50,6 +55,8 @@ export class CustomBuildContext<TJob extends Job = Job> implements ExternalBuild
public readonly runtimeApi: BuilderRuntimeApi;
public job: TJob;
public metadata?: Metadata;
public readonly cacheManager?: CacheManager;
public readonly buildDirectory: string;

private _env: Env;

Expand All @@ -65,7 +72,10 @@ export class CustomBuildContext<TJob extends Job = Job> implements ExternalBuild
this.buildLogsDirectory = path.join(buildCtx.workingdir, 'logs');
this.runtimeApi = {
uploadArtifact: (...args) => buildCtx['uploadArtifact'](...args),
cacheManager: buildCtx.dynamicCacheManager,
};
this.cacheManager = buildCtx.cacheManager;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could consider rolling cacheManager into runtimeApi for simplicity. Like, how many functions may we put into runtimeApi? I don't think the list will be too long.

this.buildDirectory = buildCtx.buildDirectory;
}

public hasBuildJob(): this is CustomBuildContext<BuildJob> {
Expand Down
6 changes: 3 additions & 3 deletions packages/build-tools/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import * as Builders from './builders';

export { Builders };

export { PackageManager } from './utils/packageManager';

export {
ArtifactToUpload,
Artifacts,
BuildContext,
BuildContextOptions,
CacheManager,
LogBuffer,
SkipNativeBuildError,
CacheManager,
} from './context';

export { PackageManager } from './utils/packageManager';

export { findAndUploadXcodeBuildLogsAsync } from './ios/xcodeBuildLogs';

export { Hook, runHookIfPresent } from './utils/hooks';
Expand Down
33 changes: 18 additions & 15 deletions packages/build-tools/src/steps/easFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,30 @@ import { BuildFunction } from '@expo/steps';

import { CustomBuildContext } from '../customBuildContext';

import { createUploadArtifactBuildFunction } from './functions/uploadArtifact';
import { createRestoreCacheBuildFunction, createSaveCacheBuildFunction } from './functions/cache';
import { calculateEASUpdateRuntimeVersionFunction } from './functions/calculateEASUpdateRuntimeVersion';
import { createCheckoutBuildFunction } from './functions/checkout';
import { createSetUpNpmrcBuildFunction } from './functions/useNpmToken';
import { createInstallNodeModulesBuildFunction } from './functions/installNodeModules';
import { createPrebuildBuildFunction } from './functions/prebuild';
import { createFindAndUploadBuildArtifactsBuildFunction } from './functions/findAndUploadBuildArtifacts';
import { configureEASUpdateIfInstalledFunction } from './functions/configureEASUpdateIfInstalled';
import { injectAndroidCredentialsFunction } from './functions/injectAndroidCredentials';
import { configureAndroidVersionFunction } from './functions/configureAndroidVersion';
import { runGradleFunction } from './functions/runGradle';
import { resolveAppleTeamIdFromCredentialsFunction } from './functions/resolveAppleTeamIdFromCredentials';
import { configureEASUpdateIfInstalledFunction } from './functions/configureEASUpdateIfInstalled';
import { configureIosCredentialsFunction } from './functions/configureIosCredentials';
import { configureIosVersionFunction } from './functions/configureIosVersion';
import { createFindAndUploadBuildArtifactsBuildFunction } from './functions/findAndUploadBuildArtifacts';
import { generateGymfileFromTemplateFunction } from './functions/generateGymfileFromTemplate';
import { runFastlaneFunction } from './functions/runFastlane';
import { createStartAndroidEmulatorBuildFunction } from './functions/startAndroidEmulator';
import { createStartIosSimulatorBuildFunction } from './functions/startIosSimulator';
import { createInstallMaestroBuildFunction } from './functions/installMaestro';
import { createGetCredentialsForBuildTriggeredByGithubIntegration } from './functions/getCredentialsForBuildTriggeredByGitHubIntegration';
import { injectAndroidCredentialsFunction } from './functions/injectAndroidCredentials';
import { createInstallMaestroBuildFunction } from './functions/installMaestro';
import { createInstallNodeModulesBuildFunction } from './functions/installNodeModules';
import { createInstallPodsBuildFunction } from './functions/installPods';
import { createSendSlackMessageFunction } from './functions/sendSlackMessage';
import { createPrebuildBuildFunction } from './functions/prebuild';
import { resolveAppleTeamIdFromCredentialsFunction } from './functions/resolveAppleTeamIdFromCredentials';
import { createResolveBuildConfigBuildFunction } from './functions/resolveBuildConfig';
import { calculateEASUpdateRuntimeVersionFunction } from './functions/calculateEASUpdateRuntimeVersion';
import { runFastlaneFunction } from './functions/runFastlane';
import { runGradleFunction } from './functions/runGradle';
import { createSendSlackMessageFunction } from './functions/sendSlackMessage';
import { createStartAndroidEmulatorBuildFunction } from './functions/startAndroidEmulator';
import { createStartIosSimulatorBuildFunction } from './functions/startIosSimulator';
import { createUploadArtifactBuildFunction } from './functions/uploadArtifact';
import { createSetUpNpmrcBuildFunction } from './functions/useNpmToken';

export function getEasFunctions(ctx: CustomBuildContext): BuildFunction[] {
const functions = [
Expand Down Expand Up @@ -56,6 +57,8 @@ export function getEasFunctions(ctx: CustomBuildContext): BuildFunction[] {
if (ctx.hasBuildJob()) {
functions.push(
...[
createSaveCacheBuildFunction(ctx),
createRestoreCacheBuildFunction(ctx),
createFindAndUploadBuildArtifactsBuildFunction(ctx),
createResolveBuildConfigBuildFunction(ctx),
createGetCredentialsForBuildTriggeredByGithubIntegration(ctx),
Expand Down
118 changes: 118 additions & 0 deletions packages/build-tools/src/steps/functions/__tests__/cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import fs from 'fs/promises';
import os from 'os';
import path from 'path';

import { Job, Metadata } from '@expo/eas-build-job';
import {
BuildRuntimePlatform,
BuildStepGlobalContext,
Cache,
DynamicCacheManager,
ExternalBuildContextProvider,
} from '@expo/steps';
import { anything, capture, instance, mock, reset, verify, when } from 'ts-mockito';

import { createLogger } from '../../../__mocks__/@expo/logger';
import { createTestIosJob } from '../../../__tests__/utils/job';
import { createMockLogger } from '../../../__tests__/utils/logger';
import { BuildContext } from '../../../context';
import { CustomBuildContext } from '../../../customBuildContext';
import { createRestoreCacheBuildFunction, createSaveCacheBuildFunction } from '../cache';

const dynamicCacheManagerMock = mock<DynamicCacheManager>();
const dynamicCacheManager = instance(dynamicCacheManagerMock);

const buildCtx = new BuildContext(createTestIosJob({}), {
env: {},
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
logger: createMockLogger(),
uploadArtifact: jest.fn(),
workingdir: '',
dynamicCacheManager,
});
const customContext = new CustomBuildContext(buildCtx);

const cacheSaveBuildFunction = createSaveCacheBuildFunction(customContext);
const cacheRestoreBuildFunction = createRestoreCacheBuildFunction(customContext);

const providerMock = mock<ExternalBuildContextProvider>();

const initialCache: Cache = { disabled: false, clear: false, paths: [] };

const provider = instance(providerMock);

let ctx: BuildStepGlobalContext;

describe('cache functions', () => {
let key: string;
let paths: string[];
beforeEach(async () => {
key = '${ hashFiles("./src/*") }-value';
paths = ['path1', 'path2'];
reset(dynamicCacheManagerMock);
reset(providerMock);

const projectSourceDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'project-'));
when(providerMock.logger).thenReturn(createLogger());
when(providerMock.runtimePlatform).thenReturn(BuildRuntimePlatform.LINUX);
when(providerMock.staticContext()).thenReturn({
metadata: {} as Metadata,
env: {},
job: { cache: initialCache } as Job,
});
when(providerMock.projectSourceDirectory).thenReturn(projectSourceDirectory);
when(providerMock.defaultWorkingDirectory).thenReturn(projectSourceDirectory);
when(providerMock.projectTargetDirectory).thenReturn(projectSourceDirectory);

ctx = new BuildStepGlobalContext(provider, false);

await fs.mkdir(path.join(projectSourceDirectory, 'src'));
await fs.writeFile(path.join(projectSourceDirectory, 'src', 'path1'), 'placeholder');
await fs.writeFile(path.join(projectSourceDirectory, 'src', 'path2'), 'placeholder');
});

describe('cacheRestoreBuildFunction', () => {
test('has correct identifiers', () => {
expect(cacheRestoreBuildFunction.id).toBe('restore-cache');
expect(cacheRestoreBuildFunction.namespace).toBe('eas');
expect(cacheRestoreBuildFunction.name).toBe('Restore Cache');
});

test('restores cache if it exists', async () => {
const buildStep = cacheRestoreBuildFunction.createBuildStepFromFunctionCall(ctx, {
callInputs: { key, paths },
});

when(providerMock.defaultWorkingDirectory).thenReturn('/tmp');

await buildStep.executeAsync();

verify(dynamicCacheManagerMock.restoreCache(anything(), anything())).once();

const [, cache] = capture(dynamicCacheManagerMock.restoreCache).first();
expect(cache.key).toMatch(/^\w+-value/);
expect(cache.paths).toStrictEqual(paths);
});
});

describe('cacheSaveBuildFunction', () => {
test('has correct identifiers', () => {
expect(cacheSaveBuildFunction.id).toBe('save-cache');
expect(cacheSaveBuildFunction.namespace).toBe('eas');
expect(cacheSaveBuildFunction.name).toBe('Save Cache');
});

test('saves cache if it does not exist', async () => {
const buildStep = cacheSaveBuildFunction.createBuildStepFromFunctionCall(ctx, {
callInputs: { key, paths },
});

await buildStep.executeAsync();

verify(dynamicCacheManagerMock.saveCache(anything(), anything())).once();

const [, cache] = capture(dynamicCacheManagerMock.saveCache).first();
expect(cache?.key).toMatch(/^\w+-value/);
});
});
});
Loading
Loading