diff --git a/packages/build-tools/src/common/installDependencies.ts b/packages/build-tools/src/common/installDependencies.ts index 1d907959..fc71b7a9 100644 --- a/packages/build-tools/src/common/installDependencies.ts +++ b/packages/build-tools/src/common/installDependencies.ts @@ -1,24 +1,61 @@ import path from 'path'; +import semver from 'semver'; import { Job } from '@expo/eas-build-job'; import spawn, { SpawnPromise, SpawnResult, SpawnOptions } from '@expo/turtle-spawn'; import { BuildContext } from '../context'; import { PackageManager, findPackagerRootDir } from '../utils/packageManager'; -import { isUsingYarn2 } from '../utils/project'; +import { isUsingModernYarnVersion } from '../utils/project'; export async function installDependenciesAsync( ctx: BuildContext, - { logger, infoCallbackFn, cwd }: SpawnOptions + { + logger, + infoCallbackFn, + cwd, + sdkVersionFromPackageJson, + reactNativeVersionFromPackageJson, + withoutFrozenLockfile, + }: { + logger?: SpawnOptions['logger']; + infoCallbackFn?: SpawnOptions['infoCallbackFn']; + cwd?: SpawnOptions['cwd']; + sdkVersionFromPackageJson?: string; + reactNativeVersionFromPackageJson?: string; + withoutFrozenLockfile?: boolean; + } ): Promise<{ spawnPromise: SpawnPromise }> { - let args = ['install']; - if (ctx.packageManager === PackageManager.PNPM) { - args = ['install', '--no-frozen-lockfile']; - } else if (ctx.packageManager === PackageManager.YARN) { - const isYarn2 = await isUsingYarn2(ctx.getReactNativeProjectDirectory()); - if (isYarn2) { - args = ['install', '--no-immutable', '--inline-builds']; + const shouldUseFrozenLockfile = Boolean( + !withoutFrozenLockfile && + !ctx.env.EAS_NO_FROZEN_LOCKFILE && + ((!!sdkVersionFromPackageJson && semver.satisfies(sdkVersionFromPackageJson, '>=52')) || + (!!reactNativeVersionFromPackageJson && + semver.satisfies(reactNativeVersionFromPackageJson, '>=0.76'))) + ); + + let args: string[]; + switch (ctx.packageManager) { + case PackageManager.NPM: { + args = [shouldUseFrozenLockfile ? 'ci' : 'install']; + break; + } + case PackageManager.PNPM: { + args = ['install', shouldUseFrozenLockfile ? '--frozen-lockfile' : '--no-frozen-lockfile']; + break; + } + case PackageManager.YARN: { + const isYarn2 = await isUsingModernYarnVersion(ctx.getReactNativeProjectDirectory()); + args = isYarn2 + ? ['install', shouldUseFrozenLockfile ? '--immutable' : '--no-immutable', '--inline-builds'] + : ['install', ...(shouldUseFrozenLockfile ? ['--frozen-lockfile'] : [])]; + break; } + case PackageManager.BUN: + args = ['install', ...(shouldUseFrozenLockfile ? ['--frozen-lockfile'] : [])]; + break; + default: + throw new Error(`Unsupported package manager: ${ctx.packageManager}`); } logger?.info(`Running "${ctx.packageManager} ${args.join(' ')}" in ${cwd} directory`); return { diff --git a/packages/build-tools/src/common/prebuild.ts b/packages/build-tools/src/common/prebuild.ts index 108a2e47..04914bd2 100644 --- a/packages/build-tools/src/common/prebuild.ts +++ b/packages/build-tools/src/common/prebuild.ts @@ -35,6 +35,7 @@ export async function prebuildAsync( await installDependenciesAsync(ctx, { logger, cwd: resolvePackagerDir(ctx), + withoutFrozenLockfile: true, // prebuild should can modify package.json in some cases }) ).spawnPromise; await installDependenciesSpawnPromise; diff --git a/packages/build-tools/src/common/setup.ts b/packages/build-tools/src/common/setup.ts index c990239b..91fd3042 100644 --- a/packages/build-tools/src/common/setup.ts +++ b/packages/build-tools/src/common/setup.ts @@ -7,6 +7,7 @@ import { BuildTrigger } from '@expo/eas-build-job/dist/common'; import nullthrows from 'nullthrows'; import { ExpoConfig } from '@expo/config'; import { UserFacingError } from '@expo/eas-build-job/dist/errors'; +import semver from 'semver'; import { BuildContext } from '../context'; import { deleteXcodeEnvLocalIfExistsAsync } from '../ios/xcodeEnv'; @@ -55,8 +56,16 @@ export async function setupAsync(ctx: BuildContext) return packageJson; }); + const sdkVersionFromPackageJson = semver.coerce(packageJson?.dependencies?.expo)?.version; + const reactNativeVersionFromPackageJson = semver.coerce( + packageJson?.dependencies?.['react-native'] + )?.version; + await ctx.runBuildPhase(BuildPhase.INSTALL_DEPENDENCIES, async () => { - await runInstallDependenciesAsync(ctx); + await runInstallDependenciesAsync(ctx, { + sdkVersionFromPackageJson, + reactNativeVersionFromPackageJson, + }); }); await ctx.runBuildPhase(BuildPhase.READ_APP_CONFIG, async () => { @@ -145,7 +154,14 @@ async function runExpoDoctor(ctx: BuildContext): Promise } async function runInstallDependenciesAsync( - ctx: BuildContext + ctx: BuildContext, + { + sdkVersionFromPackageJson, + reactNativeVersionFromPackageJson, + }: { + sdkVersionFromPackageJson?: string; + reactNativeVersionFromPackageJson?: string; + } ): Promise { let warnTimeout: NodeJS.Timeout | undefined; let killTimeout: NodeJS.Timeout | undefined; @@ -163,6 +179,8 @@ async function runInstallDependenciesAsync( } }, cwd: resolvePackagerDir(ctx), + sdkVersionFromPackageJson, + reactNativeVersionFromPackageJson, }) ).spawnPromise; diff --git a/packages/build-tools/src/steps/functions/installNodeModules.ts b/packages/build-tools/src/steps/functions/installNodeModules.ts index 8fc62d07..8e8b5dec 100644 --- a/packages/build-tools/src/steps/functions/installNodeModules.ts +++ b/packages/build-tools/src/steps/functions/installNodeModules.ts @@ -9,7 +9,7 @@ import { PackageManager, resolvePackageManager, } from '../../utils/packageManager'; -import { isUsingYarn2 } from '../../utils/project'; +import { isUsingModernYarnVersion } from '../../utils/project'; export function createInstallNodeModulesBuildFunction(): BuildFunction { return new BuildFunction({ @@ -44,7 +44,7 @@ export async function installNodeModules( if (packageManager === PackageManager.PNPM) { args = ['install', '--no-frozen-lockfile']; } else if (packageManager === PackageManager.YARN) { - const isYarn2 = await isUsingYarn2(stepCtx.workingDirectory); + const isYarn2 = await isUsingModernYarnVersion(stepCtx.workingDirectory); if (isYarn2) { args = ['install', '--no-immutable', '--inline-builds']; } diff --git a/packages/build-tools/src/utils/hooks.ts b/packages/build-tools/src/utils/hooks.ts index a457c6a5..70d01a4e 100644 --- a/packages/build-tools/src/utils/hooks.ts +++ b/packages/build-tools/src/utils/hooks.ts @@ -4,7 +4,7 @@ import spawn from '@expo/turtle-spawn'; import { BuildContext } from '../context'; import { PackageManager } from './packageManager'; -import { isUsingYarn2, readPackageJson } from './project'; +import { isUsingModernYarnVersion, readPackageJson } from './project'; export enum Hook { PRE_INSTALL = 'eas-build-pre-install', @@ -31,7 +31,7 @@ export async function runHookIfPresent( // when using yarn 2, it's not possible to run any scripts before running 'yarn install' // use 'npm' in that case const packageManager = - (await isUsingYarn2(projectDir)) && hook === Hook.PRE_INSTALL + (await isUsingModernYarnVersion(projectDir)) && hook === Hook.PRE_INSTALL ? PackageManager.NPM : ctx.packageManager; await spawn(packageManager, ['run', hook], { diff --git a/packages/build-tools/src/utils/project.ts b/packages/build-tools/src/utils/project.ts index 1cf293fe..624fd563 100644 --- a/packages/build-tools/src/utils/project.ts +++ b/packages/build-tools/src/utils/project.ts @@ -8,7 +8,7 @@ import { findPackagerRootDir, PackageManager } from '../utils/packageManager'; /** * check if .yarnrc.yml exists in the project dir or in the workspace root dir */ -export async function isUsingYarn2(projectDir: string): Promise { +export async function isUsingModernYarnVersion(projectDir: string): Promise { const yarnrcPath = path.join(projectDir, '.yarnrc.yml'); const yarnrcRootPath = path.join(findPackagerRootDir(projectDir), '.yarnrc.yml'); return (await fs.pathExists(yarnrcPath)) || (await fs.pathExists(yarnrcRootPath));