Skip to content
Draft
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
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ This is the log of notable changes to EAS CLI and related packages.

- [build-tools] Use production mode for app config, prebuild, Expo Doctor, and Expo Updates commands. ([#4180](https://github.com/expo/eas-cli/pull/4180) by [@ramonclaudio](https://github.com/ramonclaudio))
- [eas-cli] Use production mode for runtime version resolution and Expo Updates config sync. ([#4229](https://github.com/expo/eas-cli/pull/4229) by [@ramonclaudio](https://github.com/ramonclaudio))
- [eas-cli] Use an isolated env for local builds. ([#4244](https://github.com/expo/eas-cli/pull/4244) by [@ramonclaudio](https://github.com/ramonclaudio))

### 🎉 New features

- [build-tools] Add `ios_signing_backend` option to the repack step. ([#4239](https://github.com/expo/eas-cli/pull/4239) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- [build-tools] Support an optional `package_version` input on `eas/start_serve_sim_remote_session`, so a simulator session can pin the `@expo/serve-sim` version instead of always running `latest`. ([#4253](https://github.com/expo/eas-cli/pull/4253) by [@gwdp](https://github.com/gwdp))

### 🐛 Bug fixes

### 🧹 Chores

## [22.2.0](https://github.com/expo/eas-cli/releases/tag/v22.2.0) - 2026-08-20
Expand Down
51 changes: 51 additions & 0 deletions packages/eas-cli/src/build/__tests__/local-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,57 @@ describe(runLocalBuildAsync, () => {
expect(decodeInput(input)).toEqual({ job, metadata });
});

it('starts the local-build plugin with an isolated env', async () => {
const originalEnv = process.env;
const loadedEnvMarker = '["ANDROID_HOME","EAS_LOCAL_BUILD_WORKINGDIR"]';
const runtimeEnv = {
ANDROID_NDK_HOME: '/local/android-ndk',
ANDROID_SDK_ROOT: '/local/android-sdk',
DEVELOPER_DIR: '/Applications/Xcode.app/Contents/Developer',
GEM_HOME: '/local/gems',
GEM_PATH: '/local/gems:/system/gems',
HOME: '/local/home',
JAVA_HOME: '/local/jdk',
LANG: 'en_US.UTF-8',
LC_ALL: 'en_US.UTF-8',
LC_CTYPE: 'UTF-8',
NVM_NODEJS_ORG_MIRROR: 'https://node.example.test',
TEMP: '/local/temp',
TMP: '/local/tmp',
TMPDIR: '/local/tmpdir',
};
process.env = {
...runtimeEnv,
EAS_LOCAL_BUILD_PLUGIN_PATH: '/path/to/plugin',
PATH: '/local/bin',
ANDROID_HOME: '/dotenv/android',
SHELL_ONLY_VALUE: 'from-shell',
EAS_LOCAL_BUILD_WORKINGDIR: '/dotenv/workingdir',
EAS_LOCAL_BUILD_LOGGER_LEVEL: 'debug',
__EXPO_ENV_LOADED: loadedEnvMarker,
};
const env = { BUILD_ENV_VALUE: 'from-eas', PATH: '/eas/bin' };

try {
await runLocalBuildAsync(job, metadata, { verbose: true }, env);

const spawnEnv = mockSpawnAsync.mock.calls[0][2]?.env;
expect(spawnEnv?.BUILD_ENV_VALUE).toBe('from-eas');
expect(spawnEnv?.PATH).toBe('/eas/bin');
expect(spawnEnv?.ANDROID_HOME).toBeUndefined();
expect(spawnEnv).toEqual(expect.objectContaining(runtimeEnv));
expect(spawnEnv?.EAS_LOCAL_BUILD_WORKINGDIR).toBeUndefined();
expect(spawnEnv?.EAS_LOCAL_BUILD_LOGGER_LEVEL).toBe('debug');
expect(spawnEnv?.SHELL_ONLY_VALUE).toBeUndefined();
expect(spawnEnv?.__EXPO_ENV_LOADED).toBeUndefined();
expect(env).toEqual({ BUILD_ENV_VALUE: 'from-eas', PATH: '/eas/bin' });
expect(process.env.SHELL_ONLY_VALUE).toBe('from-shell');
expect(process.env.__EXPO_ENV_LOADED).toBe(loadedEnvMarker);
} finally {
process.env = originalEnv;
}
});

it('logs a non-secret build context summary and re-throws on failure', async () => {
const richJob = {
type: 'managed',
Expand Down
56 changes: 48 additions & 8 deletions packages/eas-cli/src/build/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,32 @@ import semver from 'semver';
import { getExpoApiBaseUrl } from '../api';
import Log from '../log';
import { ora } from '../ora';
import { getEnvWithoutInheritedDotenvValues } from '../utils/originalEnv';

const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = version; // should match version of @expo/eas-build-job

// The plugin starts with an isolated env, so keep the runtime vars it needs from the user's
// machine.
const LOCAL_BUILD_RUNTIME_ENV_NAMES = [
'ANDROID_HOME',
'ANDROID_NDK_HOME',
'ANDROID_SDK_ROOT',
'DEVELOPER_DIR',
'GEM_HOME',
'GEM_PATH',
'HOME',
'JAVA_HOME',
'LANG',
'LC_ALL',
'LC_CTYPE',
'NVM_NODEJS_ORG_MIRROR',
'PATH',
'TEMP',
'TMP',
'TMPDIR',
] as const;

export enum LocalBuildMode {
/**
* Local build that users can run on their own machines. Instead
Expand Down Expand Up @@ -63,18 +85,25 @@ export async function runLocalBuildAsync(
};
process.on('SIGINT', interruptHandler);
try {
const processEnv = getEnvWithoutInheritedDotenvValues(process.env);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

EXPO_UNSAFE_DOTENV_KEYS and this utility is something I'm not familiar with 🤔 maybe @sjchmiela or @szdziedzic can evaluate this better. I believe, this may be similar to a utility for original-env we have in @expo/env, so not sure if we need to reconsolidate?

@kitten kitten Aug 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Revisiting this; assuming we're keeping parity here with EAS Build, is there any way we could just not inherit any env vars and issue a major bump? It might be easier to build up parity than it is to try to pass through filtered process.env vars, and treat the spawned sub-process as "isolated"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yea, that makes sense! I tested local ios and android builds with the runtime var list I added, but can we confirm these are all the vars we want to keep in the isolated env?

https://github.com/expo/eas-cli/pull/4244/changes#diff-a54f38cd6a574317f91ba14ef751c1868db0d9469f924f18b018222c1c956a6aR18-R33

const mergedEnv = {
...getLocalBuildRuntimeEnv(processEnv),
...env,
...process.env,
EAS_LOCAL_BUILD_PLUGIN_INPUT: pluginInput,
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? processEnv.EAS_LOCAL_BUILD_WORKINGDIR,
EAS_LOCAL_BUILD_LOGGER_LEVEL: processEnv.EAS_LOCAL_BUILD_LOGGER_LEVEL,
__API_SERVER_URL: getExpoApiBaseUrl(),
...(options.skipCleanup || options.skipNativeBuild
? { EAS_LOCAL_BUILD_SKIP_CLEANUP: '1' }
: {}),
...(options.skipNativeBuild ? { EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: '1' } : {}),
...(options.artifactsDir ? { EAS_LOCAL_BUILD_ARTIFACTS_DIR: options.artifactsDir } : {}),
...(options.artifactPath ? { EAS_LOCAL_BUILD_ARTIFACT_PATH: options.artifactPath } : {}),
EAS_LOCAL_BUILD_SKIP_CLEANUP:
options.skipCleanup || options.skipNativeBuild
? '1'
: processEnv.EAS_LOCAL_BUILD_SKIP_CLEANUP,
EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: options.skipNativeBuild
? '1'
: processEnv.EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD,
EAS_LOCAL_BUILD_ARTIFACTS_DIR:
options.artifactsDir ?? processEnv.EAS_LOCAL_BUILD_ARTIFACTS_DIR,
EAS_LOCAL_BUILD_ARTIFACT_PATH:
options.artifactPath ?? processEnv.EAS_LOCAL_BUILD_ARTIFACT_PATH,
};
// log command execution to assist in debugging local builds; redact the job
// input since it contains build credentials.
Expand Down Expand Up @@ -102,6 +131,17 @@ export async function runLocalBuildAsync(
}
}

function getLocalBuildRuntimeEnv(processEnv: NodeJS.ProcessEnv): Env {
const env: Env = {};
for (const name of LOCAL_BUILD_RUNTIME_ENV_NAMES) {
const value = processEnv[name];
if (value !== undefined) {
env[name] = value;
}
}
return env;
}

/**
* Logs an allowlisted, non-secret summary of the build's job/metadata to help
* a user debug a failed local build. Only known-safe fields are included —
Expand Down
Loading