Preview: fully customizable builds on EAS Build

Aug 10, 2023 by

Avatar of Szymon Dziedzic

Szymon Dziedzic

A custom build job running in sequential steps on EAS Build.

It’s extremely satisfying when you eas build on a new project and it Just Works — at Expo, we know what we need to do to build Android and iOS React Native apps, and we handle all of that automatically for you. In many cases, this is all a project ever needs; but, not always.

We’ve come across a number of cases where developers have asked to replace single steps in their build process with their own implementations (such as for installing dependencies for Rush or Nx monorepos) or even replace the majority of the build process in order to build for another platform (such as Electron app or one of the many TV platforms). We’ve also heard from developers that want to use EAS Build to run their test suite, so that they could consolidate their CI into a single service.

To enable all of these use cases and more, today we are releasing a preview of our new fully customizable build process. You can use it by creating a yml file in .eas/build and pointing to that file from the config field on your build profile. For example, you can configure a build profile to run tests with the following:

eas.json

{
  "build": {
    "test": {
      "config": "test.yml",
      "withoutCredentials": true
    }
    // other build profiles...
  }
}

.eas/build/test.yml

build:
  name: Run tests
  steps:
    - eas/checkout
    - eas/install_node_modules
    - run:
        name: Unit tests
        command: |
          echo "Running tests..."
          npm test

When you run eas build -p android --profile test, you'll see the following:

Reusing existing build steps

If you needed to rewrite the entire build process from scratch as soon as you wanted to replace a single step, this wouldn’t be particularly useful. Each step in the standard build process can be used in your custom build definition. In the above test.yml example, we are using eas/checkout to check out the repository and eas/install_node_modules to re-use the same logic for installing Node modules (select the correct package manager, handle monorepos appropriately, etc).

For example, the following steps will create a development build for Android:

build:
  name: Development build - Android
  steps:
    - eas/checkout
    - eas/use_npm_token
    - eas/install_node_modules
    - eas/prebuild
    - eas/run_gradle
    - eas/find_and_upload_build_artifacts

And the equivalent for iOS:

build:
  name: Development build - iOS
  steps:
    - eas/checkout
    - eas/use_npm_token
    - eas/install_node_modules
    - eas/prebuild
    - run:
        name: Install pods
        working_directory: ./ios
        command: pod install
    - eas/generate_gymfile_from_template
    - eas/run_fastlane
    - eas/find_and_upload_build_artifacts

Example configurations for store-ready builds are available in the eas-custom-builds-example repository: Android, iOS.

You can learn more about the provided reusable steps and others in the “Built-in EAS functions” documentation. You can also fork these steps in your own JavaScript / TypeScript functions, or write your own build steps from scratch.

Writing build steps in JavaScript / TypeScript

If you’d like to fork a default build step, or if you just want to use JavaScript or TypeScript instead of Bash because the logic for a step is complex, you can run npx create-eas-build-function@latest .eas/build/my-new-function to create a new function (we suggest creating the function in the .eas/build directory, next to your YAML configuration — but you can put the functions anywhere in your repository).

After you’ve defined your function, you can refer to it from your build configuration YAML file. Refer to the README.md in the generated directory for more information about how to use it. Your configuration will look something like this:

eas.json

{
  "build": {
    "test": {
      "config": "test.yml",
      "withoutCredentials": true
    }
    // other build profiles...
  }
}

.eas/build/test.yml

build:
  name: Run tests
  steps:
    - eas/checkout
    - eas/install_node_modules
    - run:
        name: Unit tests
        command: npm run test
    - my-new-function

functions:
  my-new-function:
    name: my-new-function
    path: ./my-new-function

.eas/build/my-new-function/src/index.ts

import { BuildStepContext } from '@expo/steps';

async function myFunction(ctx: BuildStepContext): Promise<void> {
  ctx.logger.info('Hello from my TypeScript function!');
}

export default myFunction;

Learn more about creating EAS Build functions with TypeScript.

Current limitations

  • These features are currently in preview and are likely to change: APIs may change as we continue to iterate on them and on the the system as a whole. We expect custom builds to reach General Availability (GA) by late 2023 / early 2024.
  • EAS Build pricing model is not yet adapted to new use cases enabled by custom builds: while in preview, builds with custom configurations will be priced the same as standard builds. As the feature approaches GA, we will roll out pricing that is more appropriate to the usage patterns. We understand that for many folks this uncertainty may be a blocker for beginning to invest in adopting custom builds, and we will do our best to communicate the pricing model as soon as possible.
  • Artifacts can only be uploaded once during a build job: it is currently only possible to upload files once for each artifact type (application archive, generic build artifacts). So if you want to upload multiple artifacts, you will need to do that after all of the artifacts are ready, rather than as part of the steps where those artifacts are prepared. Example of uploading artifacts.
  • Reusable step for caching dependencies and outputs is not ready yet: we don’t yet expose a re-usable step for caching files that would speed up subsequent build runs.
  • Classic Update is not supported: the eas/configure-eas-update-if-installed step only supports EAS Update. Example of configuring EAS Update.
  • Build jobs nominally tied to target platforms: custom builds are still identified as either Android or iOS builds — this may not be true for many use cases, such as if you are running unit tests for multiple platforms or building for a different platform. For now, perform an “Android” build if you would like to use a Linux worker, and “iOS” builds if you’d like to use a macOS worker.
  • GitHub integration not ready yet: custom build jobs can’t yet be triggered from the EAS GitHub integration.

Try it out and give us feedback!

This new feature helps bring EAS Build more in alignment with the spirit of how we think about building apps at Expo — our tools and services give you great defaults out of the box, and when you need to, you can opt-out of any those default choices and customize any part of your experience without having to face a sudden complexity cliff. Keep what works for you, change what doesn’t.

Some ideas for areas we’d like feedback on:

  • There are small differences between the default build process and the equivalent composed of assembling the required steps (Android example, iOS example). Some are intentional, such as not supporting classic update configuration, and others might not be. Please report any discrepancies that you encounter that impact your project.
  • Try out the reusable eas/ functions. Are they the right level of abstraction? Should they accept additional inputs for further customization, or provide additional outputs for use in subsequent steps?
  • Every team has their own unique culture and preferences around tools and workflows, let us know if we’re missing something or could improve the APIs to better fit your use cases.

Feel free to send us feedback on Discord, @expo, Threads, or Bluesky.

A special mention goes out to Dominik Sokal, who built out the foundation of custom builds.