mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fef31f74e5 | ||
|
|
aa77ef6485 | ||
|
|
c5a12d9c26 | ||
|
|
8a9bc57e28 | ||
|
|
94bf1ca46f | ||
|
|
8671d262f7 | ||
|
|
9aeaba46eb | ||
|
|
d68a7fa34a | ||
|
|
b6450b28b6 | ||
|
|
6090749291 | ||
|
|
10c148a359 |
No files matched your search
@@ -16,8 +16,13 @@ import { runFastlaneGym, runFastlaneResign } from '../ios/fastlane';
|
||||
import { installPods } from '../ios/pod';
|
||||
import { downloadApplicationArchiveAsync } from '../ios/resign';
|
||||
import { resolveArtifactPath, resolveBuildConfiguration, resolveScheme } from '../ios/resolve';
|
||||
import { cacheStatsAsync, restoreCcacheAsync } from '../steps/functions/restoreBuildCache';
|
||||
import { saveCcacheAsync } from '../steps/functions/saveBuildCache';
|
||||
import {
|
||||
cacheStatsAsync,
|
||||
patchPodsXcodeprojAsync,
|
||||
restoreCcacheAsync,
|
||||
restoreXcodeCacheAsync,
|
||||
} from '../steps/functions/restoreBuildCache';
|
||||
import { saveCcacheAsync, saveXcodeCacheAsync } from '../steps/functions/saveBuildCache';
|
||||
import { uploadApplicationArchive } from '../utils/artifacts';
|
||||
import {
|
||||
configureExpoUpdatesIfInstalledAsync,
|
||||
@@ -51,6 +56,7 @@ async function buildAsync(ctx: BuildContext<Ios.Job>): Promise<void> {
|
||||
const evictUsedBefore = new Date();
|
||||
const credentialsManager = new CredentialsManager(ctx);
|
||||
const workingDirectory = ctx.getReactNativeProjectDirectory();
|
||||
let xcodeCacheHit = false;
|
||||
try {
|
||||
const credentials = await ctx.runBuildPhase(BuildPhase.PREPARE_CREDENTIALS, async () => {
|
||||
return await credentialsManager.prepare();
|
||||
@@ -87,10 +93,23 @@ async function buildAsync(ctx: BuildContext<Ios.Job>): Promise<void> {
|
||||
env: ctx.env,
|
||||
secrets: ctx.job.secrets,
|
||||
});
|
||||
xcodeCacheHit = await restoreXcodeCacheAsync({
|
||||
logger: ctx.logger,
|
||||
workingDirectory: ctx.buildDirectory,
|
||||
env: ctx.env,
|
||||
secrets: ctx.job.secrets,
|
||||
simulator: ctx.job.simulator,
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.INSTALL_PODS, async () => {
|
||||
await runInstallPodsAsync(ctx);
|
||||
await patchPodsXcodeprojAsync({
|
||||
logger: ctx.logger,
|
||||
workingDirectory: ctx.buildDirectory,
|
||||
env: ctx.env,
|
||||
cacheHit: xcodeCacheHit,
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.POST_INSTALL_HOOK, async () => {
|
||||
@@ -188,6 +207,13 @@ async function buildAsync(ctx: BuildContext<Ios.Job>): Promise<void> {
|
||||
ctx.logger.info('Local builds do not support saving cache.');
|
||||
return;
|
||||
}
|
||||
await saveXcodeCacheAsync({
|
||||
logger: ctx.logger,
|
||||
workingDirectory: ctx.buildDirectory,
|
||||
env: ctx.env,
|
||||
secrets: ctx.job.secrets,
|
||||
simulator: ctx.job.simulator,
|
||||
});
|
||||
await ctx.cacheManager?.saveCache(ctx);
|
||||
await saveCcacheAsync({
|
||||
logger: ctx.logger,
|
||||
|
||||
@@ -19,6 +19,8 @@ export_options({
|
||||
|
||||
export_xcargs "OTHER_CODE_SIGN_FLAGS=\\"--keychain /Users/expo/Library/Keychains/login.keychain\\""
|
||||
|
||||
derived_data_path("./build")
|
||||
|
||||
disable_xcpretty(true)
|
||||
buildlog_path("/tmp/logs")
|
||||
|
||||
@@ -42,6 +44,8 @@ export_options({
|
||||
|
||||
export_xcargs "OTHER_CODE_SIGN_FLAGS=\\"--keychain /tmp/keychain\\""
|
||||
|
||||
derived_data_path("./build")
|
||||
|
||||
disable_xcpretty(true)
|
||||
buildlog_path("/tmp/logs")
|
||||
|
||||
@@ -67,6 +71,8 @@ export_options({
|
||||
|
||||
export_xcargs "OTHER_CODE_SIGN_FLAGS=\\"--keychain /tmp/keychain\\""
|
||||
|
||||
derived_data_path("./build")
|
||||
|
||||
disable_xcpretty(true)
|
||||
buildlog_path("/tmp/logs")
|
||||
|
||||
@@ -94,6 +100,8 @@ export_options({
|
||||
|
||||
export_xcargs "OTHER_CODE_SIGN_FLAGS=\\"--keychain /tmp/keychain\\""
|
||||
|
||||
derived_data_path("./build")
|
||||
|
||||
disable_xcpretty(true)
|
||||
buildlog_path("/tmp/logs")
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ describe('gymfile', () => {
|
||||
credentials: mockCredentials,
|
||||
scheme: 'MyApp',
|
||||
buildConfiguration: 'Release',
|
||||
derivedDataPath: './build',
|
||||
outputDirectory: '/tmp/output',
|
||||
clean: true,
|
||||
logsDirectory: '/tmp/logs',
|
||||
@@ -93,6 +94,7 @@ describe('gymfile', () => {
|
||||
outputFile,
|
||||
credentials: mockCredentials,
|
||||
scheme: 'TestScheme',
|
||||
derivedDataPath: './build',
|
||||
outputDirectory: '/tmp/output',
|
||||
clean: false,
|
||||
logsDirectory: '/tmp/logs',
|
||||
@@ -130,6 +132,7 @@ describe('gymfile', () => {
|
||||
credentials: mockCredentials,
|
||||
scheme: 'MyApp',
|
||||
buildConfiguration: 'Release',
|
||||
derivedDataPath: './build',
|
||||
outputDirectory: '/tmp/output',
|
||||
clean: true,
|
||||
logsDirectory: '/tmp/logs',
|
||||
@@ -191,6 +194,7 @@ describe('gymfile', () => {
|
||||
outputFile,
|
||||
credentials: mockCredentials,
|
||||
scheme: 'MyApp',
|
||||
derivedDataPath: './build',
|
||||
outputDirectory: '/tmp/output',
|
||||
clean: true,
|
||||
logsDirectory: '/tmp/logs',
|
||||
|
||||
@@ -141,6 +141,7 @@ async function ensureGymfileExists<TJob extends Ios.Job>(
|
||||
credentials: nullthrows(credentials, 'credentials must exist for non-simulator builds'),
|
||||
scheme,
|
||||
buildConfiguration,
|
||||
derivedDataPath: './build',
|
||||
outputDirectory: './build',
|
||||
clean: false,
|
||||
logsDirectory,
|
||||
|
||||
@@ -10,6 +10,7 @@ interface ArchiveBuildOptions {
|
||||
credentials: Credentials;
|
||||
scheme: string;
|
||||
buildConfiguration?: string;
|
||||
derivedDataPath: string;
|
||||
outputDirectory: string;
|
||||
clean: boolean;
|
||||
logsDirectory: string;
|
||||
@@ -32,6 +33,7 @@ export async function createGymfileForArchiveBuild({
|
||||
credentials,
|
||||
scheme,
|
||||
buildConfiguration,
|
||||
derivedDataPath,
|
||||
entitlements,
|
||||
outputDirectory,
|
||||
logsDirectory,
|
||||
@@ -58,6 +60,7 @@ export async function createGymfileForArchiveBuild({
|
||||
KEYCHAIN_PATH: credentials.keychainPath,
|
||||
SCHEME: scheme,
|
||||
SCHEME_BUILD_CONFIGURATION: buildConfiguration,
|
||||
DERIVED_DATA_PATH: derivedDataPath,
|
||||
OUTPUT_DIRECTORY: outputDirectory,
|
||||
EXPORT_METHOD: credentials.distributionType,
|
||||
CLEAN: String(clean),
|
||||
|
||||
@@ -74,6 +74,8 @@ function createStepsForIosSimulatorBuild({
|
||||
globalCtx,
|
||||
buildToolsContext,
|
||||
}: HelperFunctionsInput): BuildStep[] {
|
||||
const evictUsedBefore = new Date();
|
||||
|
||||
const calculateEASUpdateRuntimeVersion =
|
||||
calculateEASUpdateRuntimeVersionFunction().createBuildStepFromFunctionCall(globalCtx, {
|
||||
id: 'calculate_eas_update_runtime_version',
|
||||
@@ -106,6 +108,11 @@ function createStepsForIosSimulatorBuild({
|
||||
createPrebuildBuildFunction().createBuildStepFromFunctionCall(globalCtx),
|
||||
calculateEASUpdateRuntimeVersion,
|
||||
installPods,
|
||||
createRestoreBuildCacheFunction().createBuildStepFromFunctionCall(globalCtx, {
|
||||
callInputs: {
|
||||
platform: Platform.IOS,
|
||||
},
|
||||
}),
|
||||
configureEASUpdate,
|
||||
...(shouldUseEagerBundle(globalCtx.staticContext.metadata)
|
||||
? [
|
||||
@@ -122,6 +129,11 @@ function createStepsForIosSimulatorBuild({
|
||||
createFindAndUploadBuildArtifactsBuildFunction(
|
||||
buildToolsContext
|
||||
).createBuildStepFromFunctionCall(globalCtx),
|
||||
createSaveBuildCacheFunction(evictUsedBefore).createBuildStepFromFunctionCall(globalCtx, {
|
||||
callInputs: {
|
||||
platform: Platform.IOS,
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -195,9 +207,9 @@ function createStepsForIosBuildWithCredentials({
|
||||
),
|
||||
resolveAppleTeamIdFromCredentials,
|
||||
prebuildStep,
|
||||
restoreCache,
|
||||
calculateEASUpdateRuntimeVersion,
|
||||
installPods,
|
||||
restoreCache,
|
||||
configureEASUpdate,
|
||||
configureIosCredentialsFunction().createBuildStepFromFunctionCall(globalCtx),
|
||||
configureIosVersionFunction().createBuildStepFromFunctionCall(globalCtx),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { Ios, Platform } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { asyncResult } from '@expo/results';
|
||||
import {
|
||||
@@ -7,7 +7,10 @@ import {
|
||||
BuildStepInputValueTypeName,
|
||||
spawnAsync,
|
||||
} from '@expo/steps';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
import fs from 'fs';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
|
||||
import { sendCcacheStatsAsync } from './ccacheStats';
|
||||
import { decompressCacheAsync, downloadCacheAsync, downloadPublicCacheAsync } from './restoreCache';
|
||||
@@ -17,6 +20,11 @@ import {
|
||||
getCcachePath,
|
||||
} from '../../utils/cacheKey';
|
||||
import { TurtleFetchError } from '../../utils/turtleFetch';
|
||||
import { XCODE_CACHE_KEY_PREFIX, generateXcodeCacheKeyAsync } from '../../utils/xcodeCacheKey';
|
||||
|
||||
// Stable path outside DerivedData where cached pod products are restored.
|
||||
// xcodebuild archive wipes ArchiveIntermediates, so products must live elsewhere.
|
||||
export const PODS_CACHE_DIR = '/tmp/pods-cache';
|
||||
|
||||
export function createRestoreBuildCacheFunction(): BuildFunction {
|
||||
return new BuildFunction({
|
||||
@@ -50,6 +58,23 @@ export function createRestoreBuildCacheFunction(): BuildFunction {
|
||||
env,
|
||||
secrets: stepCtx.global.staticContext.job.secrets,
|
||||
});
|
||||
|
||||
if (platform === Platform.IOS) {
|
||||
const simulator = (stepCtx.global.staticContext.job as Ios.Job).simulator;
|
||||
const cacheHit = await restoreXcodeCacheAsync({
|
||||
logger,
|
||||
workingDirectory,
|
||||
env,
|
||||
secrets: stepCtx.global.staticContext.job.secrets,
|
||||
simulator,
|
||||
});
|
||||
await patchPodsXcodeprojAsync({
|
||||
logger,
|
||||
workingDirectory,
|
||||
env,
|
||||
cacheHit,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -176,6 +201,191 @@ export async function restoreCcacheAsync({
|
||||
}
|
||||
}
|
||||
|
||||
export async function restoreXcodeCacheAsync({
|
||||
logger,
|
||||
workingDirectory,
|
||||
env,
|
||||
secrets,
|
||||
simulator,
|
||||
}: {
|
||||
logger: bunyan;
|
||||
workingDirectory: string;
|
||||
env: Record<string, string | undefined>;
|
||||
secrets?: { robotAccessToken?: string };
|
||||
simulator?: boolean;
|
||||
}): Promise<boolean> {
|
||||
if (env.EAS_XCODE_CACHE !== '1') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const robotAccessToken = nullthrows(
|
||||
secrets?.robotAccessToken,
|
||||
'Robot access token is required for cache operations'
|
||||
);
|
||||
const expoApiServerURL = nullthrows(env.__API_SERVER_URL, '__API_SERVER_URL is not set');
|
||||
|
||||
try {
|
||||
const cacheKey = await generateXcodeCacheKeyAsync(workingDirectory, simulator);
|
||||
logger.info(`Restoring Xcode cache key: ${cacheKey}`);
|
||||
|
||||
const jobId = nullthrows(env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
|
||||
|
||||
const { archivePath, matchedKey } = await downloadCacheAsync({
|
||||
logger,
|
||||
jobId,
|
||||
expoApiServerURL,
|
||||
robotAccessToken,
|
||||
paths: ['pods-products-v2'],
|
||||
key: cacheKey,
|
||||
keyPrefixes: [`${XCODE_CACHE_KEY_PREFIX}${simulator ? 'sim' : 'device'}-`],
|
||||
platform: Platform.IOS,
|
||||
});
|
||||
|
||||
await fs.promises.mkdir(PODS_CACHE_DIR, { recursive: true });
|
||||
|
||||
await spawn('tar', ['xzf', archivePath, '-C', PODS_CACHE_DIR]);
|
||||
|
||||
// Modulemap files contain hardcoded absolute paths from the original build.
|
||||
// Rewrite them to point to the cache directory instead.
|
||||
await rewriteModulemapPathsAsync(PODS_CACHE_DIR, logger);
|
||||
|
||||
logger.info(
|
||||
`Xcode cache restored to ${PODS_CACHE_DIR} ${matchedKey === cacheKey ? '(direct hit)' : '(prefix match)'}`
|
||||
);
|
||||
return true;
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof TurtleFetchError && err.response?.status === 404) {
|
||||
logger.info('No Xcode cache found for this key');
|
||||
} else {
|
||||
logger.warn('Failed to restore Xcode cache: ', err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function rewriteModulemapPathsAsync(cacheDir: string, logger: bunyan): Promise<void> {
|
||||
let modulemapFiles: string;
|
||||
try {
|
||||
const result = await spawn('find', [cacheDir, '-name', '*.modulemap'], { stdio: 'pipe' });
|
||||
modulemapFiles = result.stdout.trim();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!modulemapFiles) {
|
||||
return;
|
||||
}
|
||||
|
||||
let patchCount = 0;
|
||||
for (const filePath of modulemapFiles.split('\n')) {
|
||||
if (!filePath) continue;
|
||||
const content = await fs.promises.readFile(filePath, 'utf-8');
|
||||
// Match absolute paths ending with /Release-<platform>/ (iphoneos, iphonesimulator, etc.)
|
||||
const replaced = content.replace(/"[^"]*\/Release-[a-z]+\//g, `"${cacheDir}/`);
|
||||
if (replaced !== content) {
|
||||
await fs.promises.writeFile(filePath, replaced);
|
||||
patchCount++;
|
||||
}
|
||||
}
|
||||
logger.info(`Rewrote paths in ${patchCount} modulemap files`);
|
||||
}
|
||||
|
||||
const PATCH_RUBY_SCRIPT = `
|
||||
require 'xcodeproj'
|
||||
|
||||
project = Xcodeproj::Project.open('ios/Pods/Pods.xcodeproj')
|
||||
|
||||
umbrella_targets = project.native_targets.select { |t| t.name.start_with?('Pods-') }
|
||||
abort "ERROR: No Pods-* umbrella targets found!" if umbrella_targets.empty?
|
||||
|
||||
umbrella_names = umbrella_targets.map(&:name)
|
||||
puts "Keeping umbrella targets: #{umbrella_names.join(', ')}"
|
||||
|
||||
pod_targets = project.native_targets.reject { |t| t.name.start_with?('Pods-') }
|
||||
puts "Found #{pod_targets.size} pod targets to remove"
|
||||
|
||||
umbrella_targets.each do |umbrella|
|
||||
deps = umbrella.dependencies.to_a
|
||||
deps.each { |dep| dep.remove_from_project }
|
||||
puts "Cleared #{deps.size} dependencies from #{umbrella.name}"
|
||||
end
|
||||
|
||||
pod_targets.each do |target|
|
||||
target.remove_from_project
|
||||
end
|
||||
|
||||
project.save
|
||||
puts "Saved — #{project.native_targets.size} target(s) remaining"
|
||||
`;
|
||||
|
||||
export async function patchPodsXcodeprojAsync({
|
||||
logger,
|
||||
workingDirectory,
|
||||
env,
|
||||
cacheHit,
|
||||
}: {
|
||||
logger: bunyan;
|
||||
workingDirectory: string;
|
||||
env: Record<string, string | undefined>;
|
||||
cacheHit: boolean;
|
||||
}): Promise<void> {
|
||||
if (env.EAS_XCODE_CACHE !== '1' || !cacheHit) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('Patching Pods.xcodeproj to remove pod targets...');
|
||||
|
||||
await spawnAsync('ruby', ['-e', PATCH_RUBY_SCRIPT], {
|
||||
cwd: workingDirectory,
|
||||
logger,
|
||||
env,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
|
||||
logger.info('Pods.xcodeproj patched successfully');
|
||||
|
||||
// Point PODS_CONFIGURATION_BUILD_DIR to the stable cache directory
|
||||
// so Xcode finds pre-built products there instead of in DerivedData
|
||||
await patchPodXcconfigAsync(workingDirectory, logger);
|
||||
|
||||
logger.info('Pods xcconfig patched successfully');
|
||||
}
|
||||
|
||||
async function patchPodXcconfigAsync(workingDirectory: string, logger: bunyan): Promise<void> {
|
||||
const targetSupportDir = path.join(workingDirectory, 'ios', 'Pods', 'Target Support Files');
|
||||
|
||||
let patchCount = 0;
|
||||
const entries = await fs.promises.readdir(targetSupportDir);
|
||||
for (const entry of entries) {
|
||||
if (!entry.startsWith('Pods-')) {
|
||||
continue;
|
||||
}
|
||||
const dir = path.join(targetSupportDir, entry);
|
||||
const stat = await fs.promises.stat(dir);
|
||||
if (!stat.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
const files = await fs.promises.readdir(dir);
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.xcconfig')) {
|
||||
continue;
|
||||
}
|
||||
const filePath = path.join(dir, file);
|
||||
let content = await fs.promises.readFile(filePath, 'utf-8');
|
||||
if (content.includes('PODS_CONFIGURATION_BUILD_DIR')) {
|
||||
content = content.replace(
|
||||
/PODS_CONFIGURATION_BUILD_DIR\s*=\s*.*/g,
|
||||
`PODS_CONFIGURATION_BUILD_DIR = ${PODS_CACHE_DIR}`
|
||||
);
|
||||
await fs.promises.writeFile(filePath, content);
|
||||
patchCount++;
|
||||
logger.info(`Patched ${filePath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.info(`Patched ${patchCount} xcconfig files`);
|
||||
}
|
||||
|
||||
export async function cacheStatsAsync({
|
||||
logger,
|
||||
env,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { Ios, Platform } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { asyncResult } from '@expo/results';
|
||||
import {
|
||||
@@ -9,9 +9,11 @@ import {
|
||||
} from '@expo/steps';
|
||||
import fs from 'fs';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
|
||||
import { compressCacheAsync, uploadCacheAsync } from './saveCache';
|
||||
import { generateDefaultBuildCacheKeyAsync, getCcachePath } from '../../utils/cacheKey';
|
||||
import { generateXcodeCacheKeyAsync } from '../../utils/xcodeCacheKey';
|
||||
|
||||
export function createSaveBuildCacheFunction(evictUsedBefore: Date): BuildFunction {
|
||||
return new BuildFunction({
|
||||
@@ -38,6 +40,17 @@ export function createSaveBuildCacheFunction(evictUsedBefore: Date): BuildFuncti
|
||||
);
|
||||
}
|
||||
|
||||
if (platform === Platform.IOS) {
|
||||
const simulator = (stepCtx.global.staticContext.job as Ios.Job).simulator;
|
||||
await saveXcodeCacheAsync({
|
||||
logger,
|
||||
workingDirectory,
|
||||
env,
|
||||
secrets: stepCtx.global.staticContext.job.secrets,
|
||||
simulator,
|
||||
});
|
||||
}
|
||||
|
||||
await saveCcacheAsync({
|
||||
logger,
|
||||
workingDirectory,
|
||||
@@ -135,3 +148,121 @@ export async function saveCcacheAsync({
|
||||
logger.error({ err }, 'Failed to save cache');
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveXcodeCacheAsync({
|
||||
logger,
|
||||
workingDirectory,
|
||||
env,
|
||||
secrets,
|
||||
simulator,
|
||||
}: {
|
||||
logger: bunyan;
|
||||
workingDirectory: string;
|
||||
env: Record<string, string | undefined>;
|
||||
secrets?: { robotAccessToken?: string };
|
||||
simulator?: boolean;
|
||||
}): Promise<void> {
|
||||
if (env.EAS_XCODE_CACHE !== '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
const productsPath = await findBuildProductsPathAsync(workingDirectory, logger);
|
||||
if (!productsPath) {
|
||||
logger.warn('No build products found, skipping Xcode cache save');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Found build products at: ${productsPath}`);
|
||||
|
||||
const cachePaths = ['pods-products-v2'];
|
||||
|
||||
try {
|
||||
const cacheKey = await generateXcodeCacheKeyAsync(workingDirectory, simulator);
|
||||
logger.info(`Saving Xcode cache key: ${cacheKey}`);
|
||||
|
||||
const jobId = nullthrows(env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
|
||||
const robotAccessToken = nullthrows(
|
||||
secrets?.robotAccessToken,
|
||||
'Robot access token is required for cache operations'
|
||||
);
|
||||
const expoApiServerURL = nullthrows(env.__API_SERVER_URL, '__API_SERVER_URL is not set');
|
||||
|
||||
logger.info('Compressing Xcode build products...');
|
||||
|
||||
const { archivePath } = await compressCacheAsync({
|
||||
paths: ['.'],
|
||||
workingDirectory: productsPath,
|
||||
verbose: env.EXPO_DEBUG === '1',
|
||||
logger,
|
||||
});
|
||||
|
||||
const { size } = await fs.promises.stat(archivePath);
|
||||
|
||||
await uploadCacheAsync({
|
||||
logger,
|
||||
jobId,
|
||||
expoApiServerURL,
|
||||
robotAccessToken,
|
||||
archivePath,
|
||||
key: cacheKey,
|
||||
paths: cachePaths,
|
||||
size,
|
||||
platform: Platform.IOS,
|
||||
});
|
||||
|
||||
logger.info('Xcode cache saved successfully');
|
||||
} catch (err) {
|
||||
logger.error({ err }, 'Failed to save Xcode cache');
|
||||
}
|
||||
}
|
||||
|
||||
async function findBuildProductsPathAsync(
|
||||
workingDirectory: string,
|
||||
logger: bunyan
|
||||
): Promise<string | null> {
|
||||
const iosBuildDir = path.join(workingDirectory, 'ios', 'build');
|
||||
|
||||
// Archive build: Build/Intermediates.noindex/ArchiveIntermediates/<AppName>/BuildProductsPath/Release-iphoneos/
|
||||
const archiveIntermediatesDir = path.join(
|
||||
iosBuildDir,
|
||||
'Build',
|
||||
'Intermediates.noindex',
|
||||
'ArchiveIntermediates'
|
||||
);
|
||||
try {
|
||||
const entries = await fs.promises.readdir(archiveIntermediatesDir);
|
||||
for (const entry of entries) {
|
||||
const buildProductsPath = path.join(archiveIntermediatesDir, entry, 'BuildProductsPath');
|
||||
try {
|
||||
const configs = await fs.promises.readdir(buildProductsPath);
|
||||
const releaseDir = configs.find(c => c.startsWith('Release-'));
|
||||
if (releaseDir) {
|
||||
const fullPath = path.join(buildProductsPath, releaseDir);
|
||||
logger.info(`Found archive build products: ${fullPath}`);
|
||||
return fullPath;
|
||||
}
|
||||
} catch {
|
||||
// No BuildProductsPath in this entry
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// No ArchiveIntermediates directory
|
||||
}
|
||||
|
||||
// Simulator build: Build/Products/Release-iphonesimulator/
|
||||
const productsDir = path.join(iosBuildDir, 'Build', 'Products');
|
||||
try {
|
||||
const configs = await fs.promises.readdir(productsDir);
|
||||
const releaseDir = configs.find(c => c.startsWith('Release-'));
|
||||
if (releaseDir) {
|
||||
const fullPath = path.join(productsDir, releaseDir);
|
||||
logger.info(`Found simulator build products: ${fullPath}`);
|
||||
return fullPath;
|
||||
}
|
||||
} catch {
|
||||
// No Products directory
|
||||
}
|
||||
|
||||
logger.info('No build products found');
|
||||
return null;
|
||||
}
|
||||
@@ -17,6 +17,8 @@ export_options({
|
||||
|
||||
export_xcargs "OTHER_CODE_SIGN_FLAGS=\\"--keychain <%- KEYCHAIN_PATH %>\\""
|
||||
|
||||
derived_data_path("<%- DERIVED_DATA_PATH %>")
|
||||
|
||||
disable_xcpretty(true)
|
||||
buildlog_path("<%- LOGS_DIRECTORY %>")
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import * as PackageManagerUtils from '@expo/package-manager';
|
||||
import { hashFiles } from '@expo/steps';
|
||||
import path from 'path';
|
||||
|
||||
import { findPackagerRootDir } from './packageManager';
|
||||
|
||||
export const XCODE_CACHE_KEY_PREFIX = 'ios-xcode-cache-';
|
||||
|
||||
export async function generateXcodeCacheKeyAsync(
|
||||
workingDirectory: string,
|
||||
simulator?: boolean
|
||||
): Promise<string> {
|
||||
const packagerRunDir = findPackagerRootDir(workingDirectory);
|
||||
const manager = PackageManagerUtils.createForProject(packagerRunDir);
|
||||
const lockPath = path.join(packagerRunDir, manager.lockFile);
|
||||
const variant = simulator ? 'sim' : 'device';
|
||||
|
||||
try {
|
||||
return `${XCODE_CACHE_KEY_PREFIX}${variant}-${hashFiles([lockPath])}`;
|
||||
} catch (err: any) {
|
||||
throw new Error(`Failed to read lockfile for Xcode cache key generation: ${err.message}`);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user