Compare commits

...
Author SHA1 Message Date
Douglas Lowder 150b8c5cdf Enable manual run of changelog workflow 2026-03-11 17:29:32 -07:00
Matheus Michels f256e1f044 add changelog entry for build:dev skip flag 2026-03-11 17:26:56 -07:00
Matheus Michels 9f5e8d5547 add skip-build-if-not-found to build:dev 2026-03-11 17:26:55 -07:00
Jacob KaplanandClaude Opus 4.6 88506d01a6 fix(build-tools): check all certificates in provisioning profile during verification (#3484)
* [build-tools] Check all certificates in provisioning profile during verification

Apple provisioning profiles can contain multiple developer certificates
(e.g., when a team has multiple developers). The previous code only
checked DeveloperCertificates[0], causing false "profile and
distribution certificate don't match" errors when the signing
certificate was at a different index.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* [build-tools] Add CHANGELOG entry for multi-cert provisioning profile fix

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-03-11 15:06:29 -07:00
Stanisław Chmiela 77e36e0743 [build-tools] Enforce C.utf8 locale for Gradle invocations (#3494)
# Why

Some environments that run Gradle do not have `en_US.UTF-8` available, which can lead to locale warnings and Gradle failures when Java falls back to a non-UTF-8 default charset.

In the affected environment, `locale -a` returned only:

```sh
C
C.utf8
POSIX
```

Setting `LC_ALL=C.UTF-8` fixed the Gradle issue. This matches the behavior described in [gradle/gradle#23391](https://github.com/gradle/gradle/issues/23391#issuecomment-1878979127), where Gradle/archive handling fails under a non-UTF-8 locale.

# How

Inline `LC_ALL: 'C.UTF-8'` at the two Android Gradle subprocess call sites in `@expo/build-tools`:

- `packages/build-tools/src/android/gradle.ts`
- `packages/build-tools/src/steps/utils/android/gradle.ts`

This keeps the change narrowly scoped to Gradle execution instead of changing the broader worker environment or unrelated iOS/macOS build paths.

# Test Plan

- Verified the affected environment does not support `en_US.UTF-8`:
  ```sh
  locale -a
  ```
  Output:
  ```sh
  C
  C.utf8
  POSIX
  ```

- Confirmed that setting `LC_ALL=C.UTF-8` fixes the Gradle issue locally.

- Ran a targeted TypeScript check after the code change:
  ```sh
  ./node_modules/.bin/tsc -p packages/build-tools/tsconfig.json --noEmit
  ```
2026-03-11 13:39:38 +01:00
Stanisław Chmiela ce8f438610 [build-tools] Inject Android version without credentials (#3482)
# Why

The Android builder currently only injects Gradle config during `PREPARE_CREDENTIALS`. That means `job.version.versionCode` and `job.version.versionName` are ignored for builds that do not provide Android build credentials, even though those version values are present on the job.

This ports the Android version injection change from `expo/eas-build#425`, now on top of the merged enum-backed `CONFIGURE_ANDROID_VERSION` build phase.

# How

- switch the Android builder from the legacy `eas-build.gradle` shim to the existing direct Gradle injection helpers
- keep credential injection gated on build credentials
- inject the Android version Gradle config whenever `job.version` includes `versionCode` or `versionName`, even if there are no credentials
- run that injection in `BuildPhase.CONFIGURE_ANDROID_VERSION`
- remove the now-unused legacy shim files
- warn when deprecated `eas-build.gradle` still exists, matching the previous conservative behavior
- add focused Android builder coverage for the no-credentials version case
- add helper coverage for the legacy warning path

# Test Plan

- `mise exec -- yarn workspace @expo/build-tools jest-unit src/steps/utils/android/__tests__/gradleConfig.test.ts src/builders/__tests__/android.test.ts`
- `mise exec -- yarn workspace @expo/build-tools test`
- `mise exec -- yarn workspace @expo/build-tools typecheck`
- `git diff --check`
2026-03-11 13:16:44 +01:00
13 changed files with 394 additions and 139 deletions

No files matched your search

+1
View File
@@ -1,6 +1,7 @@
name: CHANGELOG.md PR check
on:
workflow_dispatch:
pull_request:
types:
- opened
+3
View File
@@ -8,6 +8,8 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Add `--skip-build-if-not-found` flag to `eas build:dev`. ([#3495](https://github.com/expo/eas-cli/pull/3495) by [@mmichels-brex](https://github.com/mmichels-brex))
### 🐛 Bug fixes
### 🧹 Chores
@@ -28,6 +30,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes
- Check all certificates in provisioning profile during verification instead of only the first one. ([#3484](https://github.com/expo/eas-cli/pull/3484) by [@qwertey6](https://github.com/qwertey6))
- Provide an override for the new --environment flag requirement in the update command. ([#3442](https://github.com/expo/eas-cli/pull/3442) by [@douglowder](https://github.com/douglowder))
- Add missing `--include=dev` for `npm install` commands ([#3459](https://github.com/expo/eas-cli/pull/3459) by [@kitten](https://github.com/kitten))
- Add missing `--production false` for `yarn install` commands for Yarn Classic ([#3459](https://github.com/expo/eas-cli/pull/3459) by [@kitten](https://github.com/kitten))
+1 -1
View File
@@ -42,7 +42,7 @@ export async function runGradleCommand(
return line;
}
},
env: { ...ctx.env, ...extraEnv, ...resolveVersionOverridesEnvs(ctx) },
env: { ...ctx.env, ...extraEnv, ...resolveVersionOverridesEnvs(ctx), LC_ALL: 'C.UTF-8' },
});
if (ctx.env.EAS_BUILD_RUNNER === 'eas-build' && process.platform === 'linux') {
adjustOOMScore(spawnPromise, logger);
@@ -1,57 +0,0 @@
import { AndroidConfig } from '@expo/config-plugins';
import { Android } from '@expo/eas-build-job';
import fs from 'fs-extra';
import path from 'path';
import { BuildContext } from '../context';
import { EasBuildGradle } from '../templates/EasBuildGradle';
const APPLY_EAS_BUILD_GRADLE_LINE = 'apply from: "./eas-build.gradle"';
export async function configureBuildGradle(ctx: BuildContext<Android.Job>): Promise<void> {
ctx.logger.info('Injecting signing config into build.gradle');
if (await fs.pathExists(getEasBuildGradlePath(ctx.getReactNativeProjectDirectory()))) {
ctx.markBuildPhaseHasWarnings();
ctx.logger.warn('eas-build.gradle script is deprecated, please remove it from your project.');
}
await deleteEasBuildGradle(ctx.getReactNativeProjectDirectory());
await createEasBuildGradle(ctx.getReactNativeProjectDirectory());
await addApplyToBuildGradle(ctx.getReactNativeProjectDirectory());
}
async function deleteEasBuildGradle(projectRoot: string): Promise<void> {
const easBuildGradlePath = getEasBuildGradlePath(projectRoot);
await fs.remove(easBuildGradlePath);
}
function getEasBuildGradlePath(projectRoot: string): string {
return path.join(projectRoot, 'android/app/eas-build.gradle');
}
async function createEasBuildGradle(projectRoot: string): Promise<void> {
const easBuildGradlePath = getEasBuildGradlePath(projectRoot);
await fs.writeFile(easBuildGradlePath, EasBuildGradle);
}
async function addApplyToBuildGradle(projectRoot: string): Promise<void> {
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectRoot);
const buildGradleContents = await fs.readFile(path.join(buildGradlePath), 'utf8');
if (hasLine(buildGradleContents, APPLY_EAS_BUILD_GRADLE_LINE)) {
return;
}
await fs.writeFile(
buildGradlePath,
`${buildGradleContents.trim()}\n${APPLY_EAS_BUILD_GRADLE_LINE}\n`
);
}
function hasLine(haystack: string, needle: string): boolean {
return (
haystack
.replace(/\r\n/g, '\n')
.split('\n')
// Check for both single and double quotes
.some(line => line === needle || line === needle.replace(/"/g, "'"))
);
}
@@ -0,0 +1,266 @@
import { Android, BuildPhase, BuildPhaseResult } from '@expo/eas-build-job';
import { vol } from 'memfs';
import { createTestAndroidJob } from '../../__tests__/utils/job';
import { createMockLogger } from '../../__tests__/utils/logger';
import { BuildContext } from '../../context';
import { restoreCredentials } from '../../android/credentials';
import androidBuilder from '../android';
import { runBuilderWithHooksAsync } from '../common';
import {
injectConfigureVersionGradleConfig,
injectCredentialsGradleConfig,
} from '../../steps/utils/android/gradleConfig';
jest.mock('../common', () => ({
runBuilderWithHooksAsync: jest.fn(async (ctx, buildFn) => {
await buildFn(ctx);
return ctx.artifacts;
}),
}));
jest.mock('../custom', () => ({
runCustomBuildAsync: jest.fn(),
}));
jest.mock('../../android/credentials');
jest.mock('../../android/gradle', () => ({
ensureLFLineEndingsInGradlewScript: jest.fn(),
resolveGradleCommand: jest.fn(() => ':app:bundleRelease'),
runGradleCommand: jest.fn(),
}));
jest.mock('../../common/eagerBundle', () => ({
eagerBundleAsync: jest.fn(),
shouldUseEagerBundle: jest.fn(() => false),
}));
jest.mock('../../common/prebuild', () => ({
prebuildAsync: jest.fn(),
}));
jest.mock('../../common/setup', () => ({
setupAsync: jest.fn(),
}));
jest.mock('../../steps/functions/restoreBuildCache', () => ({
cacheStatsAsync: jest.fn(),
restoreCcacheAsync: jest.fn(),
}));
jest.mock('../../steps/functions/saveBuildCache', () => ({
saveCcacheAsync: jest.fn(),
}));
jest.mock('../../steps/utils/android/gradleConfig', () => ({
...jest.requireActual('../../steps/utils/android/gradleConfig'),
injectConfigureVersionGradleConfig: jest.fn(),
injectCredentialsGradleConfig: jest.fn(),
}));
jest.mock('../../utils/artifacts', () => ({
uploadApplicationArchive: jest.fn(),
}));
jest.mock('../../utils/expoUpdates', () => ({
configureExpoUpdatesIfInstalledAsync: jest.fn(),
resolveRuntimeVersionForExpoUpdatesIfConfiguredAsync: jest.fn(async () => null),
}));
jest.mock('../../utils/hooks', () => ({
Hook: {
POST_INSTALL: 'POST_INSTALL',
PRE_UPLOAD_ARTIFACTS: 'PRE_UPLOAD_ARTIFACTS',
},
runHookIfPresent: jest.fn(),
}));
jest.mock('../../utils/prepareBuildExecutable', () => ({
prepareExecutableAsync: jest.fn(),
}));
describe(androidBuilder, () => {
beforeEach(() => {
vol.fromJSON(
{
'/workingdir/env/.gitkeep': '',
'/workingdir/build/package.json': JSON.stringify({
name: 'test-app',
version: '1.0.0',
}),
},
'/'
);
});
it('injects Android version config without build credentials', async () => {
const job: Android.Job = {
...createTestAndroidJob(),
secrets: {
buildCredentials: undefined,
},
version: {
versionCode: '42',
versionName: '1.2.3',
},
};
const ctx = new BuildContext(job, {
workingdir: '/workingdir',
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
logger: createMockLogger(),
env: {
__API_SERVER_URL: 'http://api.expo.test',
},
uploadArtifact: jest.fn(),
});
await androidBuilder(ctx);
expect(injectConfigureVersionGradleConfig).toHaveBeenCalledWith(
expect.anything(),
'/workingdir/build',
{
versionCode: '42',
versionName: '1.2.3',
}
);
expect(injectCredentialsGradleConfig).not.toHaveBeenCalled();
expect(restoreCredentials).not.toHaveBeenCalled();
});
it('injects Android credentials config when build credentials are present', async () => {
const ctx = new BuildContext(createTestAndroidJob(), {
workingdir: '/workingdir',
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
logger: createMockLogger(),
env: {
__API_SERVER_URL: 'http://api.expo.test',
},
uploadArtifact: jest.fn(),
});
await androidBuilder(ctx);
expect(injectCredentialsGradleConfig).toHaveBeenCalledWith(
expect.anything(),
'/workingdir/build'
);
expect(restoreCredentials).toHaveBeenCalledWith(ctx);
expect(injectConfigureVersionGradleConfig).not.toHaveBeenCalled();
});
it('marks the configure Android version phase as warning for legacy eas-build.gradle', async () => {
const reportBuildPhaseStats = jest.fn();
const job: Android.Job = {
...createTestAndroidJob(),
secrets: {
buildCredentials: undefined,
},
version: {
versionCode: '42',
versionName: '1.2.3',
},
};
const ctx = new BuildContext(job, {
workingdir: '/workingdir',
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
logger: createMockLogger(),
env: {
__API_SERVER_URL: 'http://api.expo.test',
},
uploadArtifact: jest.fn(),
reportBuildPhaseStats,
});
vol.mkdirSync('/workingdir/build/android/app', { recursive: true });
vol.writeFileSync('/workingdir/build/android/app/eas-build.gradle', '// Legacy content');
await androidBuilder(ctx);
expect(reportBuildPhaseStats).toHaveBeenCalledWith(
expect.objectContaining({
buildPhase: BuildPhase.CONFIGURE_ANDROID_VERSION,
result: BuildPhaseResult.WARNING,
})
);
});
it('marks the prepare credentials phase as warning for legacy eas-build.gradle', async () => {
const reportBuildPhaseStats = jest.fn();
const ctx = new BuildContext(createTestAndroidJob(), {
workingdir: '/workingdir',
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
logger: createMockLogger(),
env: {
__API_SERVER_URL: 'http://api.expo.test',
},
uploadArtifact: jest.fn(),
reportBuildPhaseStats,
});
vol.mkdirSync('/workingdir/build/android/app', { recursive: true });
vol.writeFileSync('/workingdir/build/android/app/eas-build.gradle', '// Legacy content');
await androidBuilder(ctx);
expect(reportBuildPhaseStats).toHaveBeenCalledWith(
expect.objectContaining({
buildPhase: BuildPhase.PREPARE_CREDENTIALS,
result: BuildPhaseResult.WARNING,
})
);
});
it('injects credentials and version config when both are configured', async () => {
const reportBuildPhaseStats = jest.fn();
const job: Android.Job = {
...createTestAndroidJob(),
version: {
versionCode: '42',
versionName: '1.2.3',
},
};
const ctx = new BuildContext(job, {
workingdir: '/workingdir',
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
logger: createMockLogger(),
env: {
__API_SERVER_URL: 'http://api.expo.test',
},
uploadArtifact: jest.fn(),
reportBuildPhaseStats,
});
vol.mkdirSync('/workingdir/build/android/app', { recursive: true });
vol.writeFileSync('/workingdir/build/android/app/eas-build.gradle', '// Legacy content');
await androidBuilder(ctx);
expect(injectCredentialsGradleConfig).toHaveBeenCalledWith(
expect.anything(),
'/workingdir/build'
);
expect(restoreCredentials).toHaveBeenCalledWith(ctx);
expect(injectConfigureVersionGradleConfig).toHaveBeenCalledWith(
expect.anything(),
'/workingdir/build',
{
versionCode: '42',
versionName: '1.2.3',
}
);
expect(reportBuildPhaseStats).toHaveBeenCalledWith(
expect.objectContaining({
buildPhase: BuildPhase.PREPARE_CREDENTIALS,
result: BuildPhaseResult.WARNING,
})
);
expect(reportBuildPhaseStats).toHaveBeenCalledWith(
expect.objectContaining({
buildPhase: BuildPhase.CONFIGURE_ANDROID_VERSION,
result: BuildPhaseResult.WARNING,
})
);
});
it('runs the builder through hooks', async () => {
const ctx = new BuildContext(createTestAndroidJob(), {
workingdir: '/workingdir',
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
logger: createMockLogger(),
env: {
__API_SERVER_URL: 'http://api.expo.test',
},
uploadArtifact: jest.fn(),
});
await androidBuilder(ctx);
expect(runBuilderWithHooksAsync).toHaveBeenCalledWith(ctx, expect.any(Function));
});
});
+18 -2
View File
@@ -10,13 +10,17 @@ import {
resolveGradleCommand,
runGradleCommand,
} from '../android/gradle';
import { configureBuildGradle } from '../android/gradleConfig';
import { eagerBundleAsync, shouldUseEagerBundle } from '../common/eagerBundle';
import { prebuildAsync } from '../common/prebuild';
import { setupAsync } from '../common/setup';
import { Artifacts, BuildContext, SkipNativeBuildError } from '../context';
import { cacheStatsAsync, restoreCcacheAsync } from '../steps/functions/restoreBuildCache';
import { saveCcacheAsync } from '../steps/functions/saveBuildCache';
import {
injectConfigureVersionGradleConfig,
injectCredentialsGradleConfig,
warnIfLegacyEasBuildGradleExists,
} from '../steps/utils/android/gradleConfig';
import { uploadApplicationArchive } from '../utils/artifacts';
import {
configureExpoUpdatesIfInstalledAsync,
@@ -101,10 +105,22 @@ async function buildAsync(ctx: BuildContext<Android.Job>): Promise<void> {
nullthrows(ctx.job.secrets, 'Secrets must be defined for non-custom builds').buildCredentials
) {
await ctx.runBuildPhase(BuildPhase.PREPARE_CREDENTIALS, async () => {
await warnIfLegacyEasBuildGradleExists(ctx);
await restoreCredentials(ctx);
await configureBuildGradle(ctx);
await injectCredentialsGradleConfig(ctx.logger, ctx.getReactNativeProjectDirectory());
});
}
if (ctx.job.version?.versionCode || ctx.job.version?.versionName) {
await ctx.runBuildPhase(BuildPhase.CONFIGURE_ANDROID_VERSION, async () => {
await warnIfLegacyEasBuildGradleExists(ctx);
await injectConfigureVersionGradleConfig(ctx.logger, ctx.getReactNativeProjectDirectory(), {
versionCode: ctx.job.version?.versionCode,
versionName: ctx.job.version?.versionName,
});
});
}
await ctx.runBuildPhase(BuildPhase.CONFIGURE_EXPO_UPDATES, async () => {
await configureExpoUpdatesIfInstalledAsync(ctx, {
resolvedRuntimeVersion: resolvedExpoUpdatesRuntimeVersion?.runtimeVersion ?? null,
@@ -1,5 +1,6 @@
import { Ios } from '@expo/eas-build-job';
import { createLogger } from '@expo/logger';
import crypto from 'crypto';
import { provisioningProfile } from './fixtures';
import { BuildContext } from '../../../context';
@@ -73,5 +74,45 @@ describe('ProvisioningProfile class', () => {
await pp.destroy();
}
});
it('should match a certificate that is not at index 0 in DeveloperCertificates', async () => {
const pp = new ProvisioningProfile(
ctx,
Buffer.from(provisioningProfile.dataBase64, 'base64'),
keychain.data.path,
'testapp',
'Abc 123'
);
try {
await pp.init();
// Prepend an unrelated certificate so the real one moves to index 1
const dummyCert = Buffer.from('dummy-certificate-data');
(pp as any).developerCertificates = [dummyCert, ...(pp as any).developerCertificates];
// The original cert fingerprint should still match
expect(() => {
pp.verifyCertificate(provisioningProfile.certFingerprint);
}).not.toThrow();
// A non-matching fingerprint should still fail
expect(() => {
pp.verifyCertificate('2137');
}).toThrowError(/don't match/);
// The dummy cert's fingerprint should also match
const dummyFingerprint = crypto
.createHash('sha1')
.update(new Uint8Array(dummyCert))
.digest('hex')
.toUpperCase();
expect(() => {
pp.verifyCertificate(dummyFingerprint);
}).not.toThrow();
} finally {
await pp.destroy();
}
});
});
});
@@ -43,6 +43,7 @@ export default class ProvisioningProfile<TJob extends Ios.Job> {
private readonly profilePath: string;
private profileData?: ProvisioningProfileData;
private developerCertificates: Buffer[] = [];
constructor(
private readonly ctx: BuildContext<TJob>,
@@ -77,11 +78,12 @@ export default class ProvisioningProfile<TJob extends Ios.Job> {
}
public verifyCertificate(fingerprint: string): void {
const devCertFingerprint = this.genDerCertFingerprint();
if (devCertFingerprint !== fingerprint) {
const devCertFingerprints = this.getAllDerCertFingerprints();
if (!devCertFingerprints.includes(fingerprint)) {
throw new errors.CredentialsDistCertMismatchError(
`Provisioning profile and distribution certificate don't match.
Profile's certificate fingerprint = ${devCertFingerprint}, distribution certificate fingerprint = ${fingerprint}`
`Provisioning profile and distribution certificate don't match.\n` +
`Profile's certificate fingerprints = [${devCertFingerprints.join(', ')}], ` +
`distribution certificate fingerprint = ${fingerprint}`
);
}
}
@@ -114,6 +116,10 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
] as string;
const bundleIdentifier = applicationIdentifier.replace(/^.+?\./, '');
this.developerCertificates = (plistData.DeveloperCertificates as string[]).map((cert: string) =>
Buffer.from(cert, 'base64')
);
this.profileData = {
path: this.profilePath,
target: this.target,
@@ -121,7 +127,7 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
teamId: (plistData.TeamIdentifier as string[])[0],
uuid: plistData.UUID as string,
name: plistData.Name as string,
developerCertificate: Buffer.from((plistData.DeveloperCertificates as string[])[0], 'base64'),
developerCertificate: this.developerCertificates[0],
certificateCommonName: this.certificateCommonName,
distributionType: this.resolveDistributionType(plistData),
};
@@ -137,11 +143,9 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
}
}
private genDerCertFingerprint(): string {
return crypto
.createHash('sha1')
.update(new Uint8Array(this.data.developerCertificate))
.digest('hex')
.toUpperCase();
private getAllDerCertFingerprints(): string[] {
return this.developerCertificates.map(cert =>
crypto.createHash('sha1').update(new Uint8Array(cert)).digest('hex').toUpperCase()
);
}
}
@@ -33,7 +33,7 @@ export async function runGradleCommand({
return line;
}
},
env: { ...env, ...extraEnv },
env: { ...env, ...extraEnv, LC_ALL: 'C.UTF-8' },
});
if (env.EAS_BUILD_RUNNER === 'eas-build' && process.platform === 'linux') {
adjustOOMScore(spawnPromise, logger);
@@ -1,4 +1,5 @@
import { AndroidConfig } from '@expo/config-plugins';
import { Android } from '@expo/eas-build-job';
import { bunyan } from '@expo/logger';
import { templateString } from '@expo/template-file';
import fs from 'fs-extra';
@@ -6,6 +7,7 @@ import path from 'path';
import { EasBuildConfigureVersionGradleTemplate } from '../../../templates/EasBuildConfigureVersionGradle';
import { EasBuildInjectAndroidCredentialsGradle } from '../../../templates/EasBuildInjectAndroidCredentialsGradle';
import { BuildContext } from '../../../context';
const APPLY_EAS_BUILD_INJECT_CREDENTIALS_GRADLE_LINE =
'apply from: "./eas-build-inject-android-credentials.gradle"';
@@ -51,6 +53,26 @@ async function deleteEasBuildConfigureVersionGradle(workingDir: string): Promise
await fs.remove(targetPath);
}
let legacyEasBuildGradleWarningEmitted = false;
export async function warnIfLegacyEasBuildGradleExists(
ctx: BuildContext<Android.Job>
): Promise<void> {
const legacyGradlePath = getLegacyEasBuildGradlePath(ctx.getReactNativeProjectDirectory());
if (
(await fs.pathExists(legacyGradlePath)) &&
(process.env.NODE_ENV === 'test' || !legacyEasBuildGradleWarningEmitted)
) {
legacyEasBuildGradleWarningEmitted = true;
ctx.logger.warn('eas-build.gradle script is deprecated, please remove it from your project.');
ctx.markBuildPhaseHasWarnings();
}
}
function getLegacyEasBuildGradlePath(projectRoot: string): string {
return path.join(projectRoot, 'android/app/eas-build.gradle');
}
function getEasBuildInjectCredentialsGradlePath(workingDir: string): string {
return path.join(workingDir, 'android/app/eas-build-inject-android-credentials.gradle');
}
@@ -113,7 +135,7 @@ function hasLine(haystack: string, needle: string): boolean {
haystack
.replace(/\r\n/g, '\n')
.split('\n')
// Check for both single and double quotes
// Check for both single and double quotes.
.some(line => line === needle || line === needle.replace(/"/g, "'"))
);
}
@@ -42,6 +42,7 @@ export default class ProvisioningProfile {
private readonly profilePath: string;
private profileData?: ProvisioningProfileData;
private developerCertificates: Buffer[] = [];
constructor(
private readonly profile: Buffer,
@@ -73,11 +74,12 @@ export default class ProvisioningProfile {
}
public verifyCertificate(fingerprint: string): void {
const devCertFingerprint = this.genDerCertFingerprint();
if (devCertFingerprint !== fingerprint) {
const devCertFingerprints = this.getAllDerCertFingerprints();
if (!devCertFingerprints.includes(fingerprint)) {
throw new errors.CredentialsDistCertMismatchError(
`Provisioning profile and distribution certificate don't match.
Profile's certificate fingerprint = ${devCertFingerprint}, distribution certificate fingerprint = ${fingerprint}`
`Provisioning profile and distribution certificate don't match.\n` +
`Profile's certificate fingerprints = [${devCertFingerprints.join(', ')}], ` +
`distribution certificate fingerprint = ${fingerprint}`
);
}
}
@@ -110,6 +112,10 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
] as string;
const bundleIdentifier = applicationIdentifier.replace(/^.+?\./, '');
this.developerCertificates = (plistData.DeveloperCertificates as string[]).map((cert: string) =>
Buffer.from(cert, 'base64')
);
this.profileData = {
path: this.profilePath,
target: this.target,
@@ -117,7 +123,7 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
teamId: (plistData.TeamIdentifier as string[])[0],
uuid: plistData.UUID as string,
name: plistData.Name as string,
developerCertificate: Buffer.from((plistData.DeveloperCertificates as string[])[0], 'base64'),
developerCertificate: this.developerCertificates[0],
certificateCommonName: this.certificateCommonName,
distributionType: this.resolveDistributionType(plistData),
};
@@ -133,11 +139,9 @@ Profile's certificate fingerprint = ${devCertFingerprint}, distribution certific
}
}
private genDerCertFingerprint(): string {
return crypto
.createHash('sha1')
.update(new Uint8Array(this.data.developerCertificate))
.digest('hex')
.toUpperCase();
private getAllDerCertFingerprints(): string[] {
return this.developerCertificates.map(cert =>
crypto.createHash('sha1').update(new Uint8Array(cert)).digest('hex').toUpperCase()
);
}
}
@@ -1,54 +0,0 @@
export const EasBuildGradle = `// Build integration with EAS
import java.nio.file.Paths
def versionCodeFromEnv = System.getenv("EAS_BUILD_ANDROID_VERSION_CODE")
def versionNameFromEnv = System.getenv("EAS_BUILD_ANDROID_VERSION_NAME")
android {
defaultConfig {
if (versionCodeFromEnv) {
versionCode = Integer.parseInt(versionCodeFromEnv)
}
if (versionNameFromEnv) {
versionName = versionNameFromEnv
}
}
applicationVariants.all { variant ->
variant.outputs.each { output ->
if (versionCodeFromEnv) {
output.versionCodeOverride = Integer.parseInt(versionCodeFromEnv)
}
if (versionNameFromEnv) {
output.versionNameOverride = versionNameFromEnv
}
}
}
signingConfigs {
release {
def credentialsJson = Paths.get(System.getenv("EAS_BUILD_WORKINGDIR")).resolve("credentials.json").toFile();
def credentials = new groovy.json.JsonSlurper().parse(credentialsJson)
def keystorePath = Paths.get(credentials.android.keystore.keystorePath);
storeFile keystorePath.toFile()
storePassword credentials.android.keystore.keystorePassword
keyAlias credentials.android.keystore.keyAlias
if (credentials.android.keystore.containsKey("keyPassword")) {
keyPassword credentials.android.keystore.keyPassword
} else {
// key password is required by Gradle, but PKCS keystores don't have one
// using the keystore password seems to satisfy the requirement
keyPassword credentials.android.keystore.keystorePassword
}
}
}
buildTypes {
release {
signingConfig android.signingConfigs.release
}
debug {
signingConfig android.signingConfigs.release
}
}
}`;
+10 -1
View File
@@ -43,6 +43,10 @@ export default class BuildDev extends EasCommand {
description: `Name of the build profile from eas.json. It must be a profile allowing to create emulator/simulator internal distribution dev client builds. The "${DEFAULT_EAS_BUILD_RUN_PROFILE_NAME}" build profile will be selected by default.`,
helpValue: 'PROFILE_NAME',
}),
'skip-build-if-not-found': Flags.boolean({
description: 'Skip build if no successful build with matching fingerprint is found.',
default: false,
}),
};
static override contextDefinition = {
@@ -128,10 +132,15 @@ export default class BuildDev extends EasCommand {
await this.startDevServerAsync({ projectDir, platform });
return;
} else {
Log.warn('Artifacts for this build expired. New build will be started.');
Log.warn('Artifacts for this build expired.');
}
}
if (flags['skip-build-if-not-found']) {
Log.log('No successful build with matching fingerprint found. Skipping build.');
return;
}
Log.log('🚀 No successful build with matching fingerprint found. Starting a new build...');
const previousBuildsForSelectedProfile = await this.getBuildsAsync({