Compare commits

...
Author SHA1 Message Date
Stanisław Chmiela 4f5bdcf06f Apply Gradle cache env in Android builder 2026-06-11 13:33:08 +02:00
Stanisław Chmiela 6c59086ed8 Return Gradle cache env from restore flow 2026-06-11 11:37:33 +02:00
Stanisław Chmiela 1431a20b86 Log user-provided custom functions during parsing (#3847)
I want to learn how much are custom functions being used.
2026-06-11 10:40:49 +02:00
5 changed files with 126 additions and 12 deletions

No files matched your search

@@ -13,6 +13,10 @@ import {
injectConfigureVersionGradleConfig,
injectCredentialsGradleConfig,
} from '../../steps/utils/android/gradleConfig';
import {
logGradleCacheEnv,
restoreGradleCacheAsync,
} from '../../steps/functions/restoreBuildCache';
jest.mock('../common', () => ({
runBuilderWithHooksAsync: jest.fn(async (ctx, buildFn) => {
@@ -41,10 +45,13 @@ jest.mock('../../common/setup', () => ({
}));
jest.mock('../../steps/functions/restoreBuildCache', () => ({
cacheStatsAsync: jest.fn(),
logGradleCacheEnv: jest.fn(),
restoreCcacheAsync: jest.fn(),
restoreGradleCacheAsync: jest.fn(async () => ({ env: {} })),
}));
jest.mock('../../steps/functions/saveBuildCache', () => ({
saveCcacheAsync: jest.fn(),
saveGradleCacheAsync: jest.fn(),
}));
jest.mock('../../steps/utils/android/gradleConfig', () => ({
...jest.requireActual('../../steps/utils/android/gradleConfig'),
@@ -145,6 +152,30 @@ describe(androidBuilder, () => {
expect(injectConfigureVersionGradleConfig).not.toHaveBeenCalled();
});
it('logs Gradle cache environment variables returned by restoreGradleCacheAsync', async () => {
jest.mocked(restoreGradleCacheAsync).mockResolvedValueOnce({
env: {
'ORG_GRADLE_PROJECT_org.gradle.caching': 'true',
},
});
const ctx = new BuildContext(createTestAndroidJob(), {
workingdir: '/workingdir',
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
logger: createMockLogger(),
env: {
EAS_BUILD_RUNNER: 'eas-build',
__API_SERVER_URL: 'http://api.expo.test',
},
uploadArtifact: jest.fn(),
});
await androidBuilder(ctx);
expect(logGradleCacheEnv).toHaveBeenCalledWith(expect.anything(), {
'ORG_GRADLE_PROJECT_org.gradle.caching': 'true',
});
});
it('marks the configure Android version phase as warning for legacy eas-build.gradle', async () => {
const job: Android.Job = {
...createTestAndroidJob(),
@@ -7,6 +7,7 @@ import { createMockLogger } from '../../__tests__/utils/logger';
import { prepareProjectSourcesAsync } from '../../common/projectSources';
import { BuildContext } from '../../context';
import { CustomBuildContext } from '../../customBuildContext';
import { Datadog } from '../../datadog';
import { findAndUploadXcodeBuildLogsAsync } from '../../ios/xcodeBuildLogs';
import { runCustomBuildAsync } from '../custom';
@@ -17,6 +18,7 @@ const findAndUploadXcodeBuildLogsAsyncMock = jest.mocked(findAndUploadXcodeBuild
describe(runCustomBuildAsync, () => {
let ctx: BuildContext<BuildJob>;
const datadogLogSpy = jest.spyOn(Datadog, 'log');
beforeEach(() => {
const job = createTestIosJob();
@@ -27,10 +29,24 @@ describe(runCustomBuildAsync, () => {
vol.fromJSON(
{
'test.yaml': `
functions:
test_function:
name: Test function
path: ./custom-function
shell: /bin/zsh
supported_platforms:
- darwin
- linux
inputs:
- name
- profile
outputs:
- artifact_path
build:
steps:
- eas/checkout
`,
'custom-function/package.json': '{}',
},
'/workingdir/temporary-custom-build'
);
@@ -77,6 +93,21 @@ describe(runCustomBuildAsync, () => {
expect(prepareProjectSourcesAsync).toHaveBeenCalledTimes(2);
});
it('reports user-provided custom functions when parsing the custom build config', async () => {
await runCustomBuildAsync(ctx);
expect(datadogLogSpy).toHaveBeenCalledWith('Custom build saw user-provided function', {
event: 'custom_build_user_provided_function',
custom_function_id: 'test_function',
custom_function_input_count: '2',
custom_function_module_path: '/workingdir/temporary-custom-build/custom-function',
custom_function_name: 'Test function',
custom_function_output_count: '1',
custom_function_shell: '/bin/zsh',
custom_function_supported_runtime_platforms: 'darwin,linux',
});
});
it('awaits drainPendingMetricUploads after workflow execution', async () => {
let resolveDrain!: () => void;
const drainSpy = jest
+6 -1
View File
@@ -18,6 +18,7 @@ import { setupAsync } from '../common/setup';
import { Artifacts, BuildContext, SkipNativeBuildError } from '../context';
import {
cacheStatsAsync,
logGradleCacheEnv,
restoreCcacheAsync,
restoreGradleCacheAsync,
} from '../steps/functions/restoreBuildCache';
@@ -88,12 +89,16 @@ async function buildAsync(ctx: BuildContext<Android.Job>): Promise<void> {
env: ctx.env,
secrets: ctx.job.secrets,
});
await restoreGradleCacheAsync({
const { env } = await restoreGradleCacheAsync({
logger: ctx.logger,
workingDirectory,
env: ctx.env,
secrets: ctx.job.secrets,
});
if (Object.keys(env).length > 0) {
Object.assign(ctx.env, env);
logGradleCacheEnv(ctx.logger, env);
}
});
await ctx.runBuildPhase(BuildPhase.POST_INSTALL_HOOK, async () => {
+32 -1
View File
@@ -1,5 +1,11 @@
import { BuildJob, BuildPhase, BuildTrigger, Ios, Platform } from '@expo/eas-build-job';
import { BuildConfigParser, BuildStepGlobalContext, StepsConfigParser, errors } from '@expo/steps';
import {
BuildConfigParser,
BuildStepGlobalContext,
BuildWorkflow,
StepsConfigParser,
errors,
} from '@expo/steps';
import assert from 'assert';
import fs from 'fs/promises';
import nullthrows from 'nullthrows';
@@ -9,6 +15,7 @@ import { resolveEnvFromBuildProfileAsync } from '../common/easBuildInternal';
import { prepareProjectSourcesAsync } from '../common/projectSources';
import { Artifacts, BuildContext } from '../context';
import { CustomBuildContext } from '../customBuildContext';
import { Datadog } from '../datadog';
import { findAndUploadXcodeBuildLogsAsync } from '../ios/xcodeBuildLogs';
import { getEasFunctionGroups } from '../steps/easFunctionGroups';
import { getEasFunctions } from '../steps/easFunctions';
@@ -81,6 +88,7 @@ export async function runCustomBuildAsync(ctx: BuildContext<BuildJob>): Promise<
throw parseError;
}
});
logUserProvidedCustomFunctions(workflow);
try {
try {
await workflow.executeAsync();
@@ -103,3 +111,26 @@ export async function runCustomBuildAsync(ctx: BuildContext<BuildJob>): Promise<
return ctx.artifacts;
}
function logUserProvidedCustomFunctions(workflow: BuildWorkflow): void {
for (const buildFunction of Object.values(workflow.buildFunctions)) {
if (!buildFunction.customFunctionModulePath) {
continue;
}
Datadog.log('Custom build saw user-provided function', {
event: 'custom_build_user_provided_function',
custom_function_id: buildFunction.getFullId(),
custom_function_module_path: buildFunction.customFunctionModulePath,
custom_function_input_count: String(buildFunction.inputProviders?.length ?? 0),
custom_function_output_count: String(buildFunction.outputProviders?.length ?? 0),
...(buildFunction.name ? { custom_function_name: buildFunction.name } : {}),
...(buildFunction.shell ? { custom_function_shell: buildFunction.shell } : {}),
...(buildFunction.supportedRuntimePlatforms
? {
custom_function_supported_runtime_platforms:
buildFunction.supportedRuntimePlatforms.join(','),
}
: {}),
});
}
}
@@ -57,12 +57,19 @@ export function createRestoreBuildCacheFunction(): BuildFunction {
});
if (platform === Platform.ANDROID) {
await restoreGradleCacheAsync({
const { env: gradleCacheEnv } = await restoreGradleCacheAsync({
logger,
workingDirectory,
env,
secrets: stepCtx.global.staticContext.job.secrets,
});
if (Object.keys(gradleCacheEnv).length > 0) {
stepCtx.global.updateEnv({
...stepCtx.global.env,
...gradleCacheEnv,
});
logGradleCacheEnv(logger, gradleCacheEnv);
}
}
},
});
@@ -200,19 +207,16 @@ export async function restoreGradleCacheAsync({
workingDirectory: string;
env: Record<string, string | undefined>;
secrets?: { robotAccessToken?: string };
}): Promise<void> {
}): Promise<{ env: Record<string, string> }> {
if (env.EAS_GRADLE_CACHE !== '1') {
return;
return { env: {} };
}
try {
const gradlePropertiesPath = path.join(workingDirectory, 'android', 'gradle.properties');
const gradlePropertiesContent = await fs.promises.readFile(gradlePropertiesPath, 'utf-8');
await fs.promises.writeFile(
gradlePropertiesPath,
`${gradlePropertiesContent}\n\norg.gradle.caching=true\n`
);
const gradleCacheEnv = {
'ORG_GRADLE_PROJECT_org.gradle.caching': 'true',
};
try {
// Configure cache cleanup via init script (works with both Gradle 8 and 9,
// org.gradle.cache.cleanup property was removed in Gradle 9)
const initScriptDir = path.join(os.homedir(), '.gradle', 'init.d');
@@ -294,6 +298,18 @@ export async function restoreGradleCacheAsync({
logger.warn('Failed to restore Gradle cache: ', err);
}
}
return { env: gradleCacheEnv };
}
export function logGradleCacheEnv(logger: bunyan, env: Record<string, string>): void {
logger.info(
`Enabling Gradle cache. Running Gradle with additional environment variables.\n${Object.entries(
env
)
.map(([key, value]) => `${key}=${value}`)
.join('\n')}`
);
}
export async function cacheStatsAsync({