| # Flutter Engine Build Definition Language |
| |
| The ***Flutter Engine Build Definition Language*** describes a build on CI |
| by defining a combination of *sub-builds*, *archives*, *generators* and *dependencies*. It |
| makes it simple to shard sub-builds by mapping build inputs to workflows, and listing |
| the sub-build-generated artifacts explicitly. The Build Definition Language, Engine |
| Recipes V2 and the generation of artifacts using GN+Ninja set the groundwork |
| for efficient builds with dependency reusability. |
| |
| **Author: Godofredo Contreras (godofredoc)**\ |
| **Go Link: flutter.dev/go/engine-build-definition-language**\ |
| **Created:** 01/2023 / **Last updated:** 04/2023 |
| |
| ## Glossary |
| |
| * **[recipes](https://github.com/luci/recipes-py)** - domain specific |
| language for specifying sequences of subprocess calls in a cross-platform and |
| testable way. |
| * **Generator** - scripts in Dart, python or bash that combine the output of |
| sub-builds to generate artifacts. |
| * **Builder** - a combination of configuration, recipes and a given commit to |
| build and test artifacts. |
| * **Build** - a builder running with specific properties, repository and |
| commit. |
| * **[GN](https://gn.googlesource.com/gn/)** - a meta-build system that |
| generates build files for [Ninja](https://ninja-build.org/). |
| * **[Ninja](https://ninja-build.org)** - Ninja is a small build system with a |
| focus on speed. |
| * **CAS** - a service that stores arbitrary binary blobs addressed by (hash of) |
| their content. It is specialized for low latency, high volume query/read/write |
| operations. |
| |
| ## USAGE EXAMPLES |
| |
| Engine build definition files using the Build Definition Language can be found in the |
| [flutter/engine/ci/builders](https://github.com/flutter/engine/tree/main/ci/builders) directory. |
| |
| The [engine orchestrator recipe](https://flutter.googlesource.com/recipes/+/refs/heads/main/recipes/engine_v2/) |
| reads each file in that directory, shards their builds, collects artifacts and |
| uploads them to the Google Cloud Storage bucket. |
| |
| The [.ci.yaml file](https://github.com/flutter/engine/blob/main/.ci.yaml) at the |
| root of the `flutter/engine` repository puts all the components together. |
| Builds are specified in that file using a property pointing to the build definition |
| file to be used by engine\_v2 recipes. Full documentation of the `.ci.yaml` file format |
| can be found [in the Cocoon repository here](https://github.com/flutter/cocoon/blob/main/CI_YAML.md). |
| |
| The following is a sample build configuration referencing |
| [android\_aot\_engine.json](https://github.com/flutter/engine/blob/main/ci/builders/mac_android_aot_engine.json) |
| in the `config_name` under `properties`: |
| |
| ```yaml |
| - name: Mac mac_android_aot_engine |
| recipe: engine_v2/engine_v2 |
| timeout: 60 |
| properties: |
| config_name: mac_android_aot_engine |
| $flutter/osx_sdk : >- |
| { "sdk_version": "14e300c" } |
| |
| ``` |
| |
| ## Build Definition Language Assumptions |
| |
| To keep the build definition language simple the following assumptions were |
| made during its design: |
| |
| * A build can be expressed as a set of independent sub-builds. |
| * A sub-build can be defined as a sequence of a `gn` configuration step, |
| a `ninja` build step, followed by self-contained test scripts, and self-contained |
| generator scripts. All the elements are optional allowing to use gn+ninja without |
| generators or generators without gn+ninja. |
| * All the sub-builds required by a global generator are defined within the same |
| configuration file. |
| |
| ## Build configuration file |
| |
| The build configuration is a json file containing a list of builds, tests, |
| generators and archives. The following is an example of an empty configuration |
| file: |
| |
| ```json |
| { |
| "builds": [], |
| "tests": [], |
| "generators": { |
| "tasks": [] |
| }, |
| "archives": [ |
| ] |
| } |
| ``` |
| |
| Note: tests, generators and archives can be omited if empty. |
| |
| Build configuration files have to be checked into the |
| [engine_checkout/ci/builder](https://github.com/flutter/engine/tree/main/ci/builders) |
| directory where engine v2 recipes will be reading them from. |
| |
| Configurations with a single build are supported. Single build configurations |
| are located have to be checked into the |
| [engine_checkout/ci/builder/standalone](https://github.com/flutter/engine/tree/main/ci/builders/standalone) |
| |
| A configuration file defines a top-level builder that will show up as a column |
| in the |
| [Flutter Dashboard](https://flutter-dashboard.appspot.com/#/build?repo=engine&branch=master). |
| |
| |
| ### Magic variables |
| |
| Magic variables are special environment variables that can be used as parameters |
| for generators and test commands in the local and global contexts. |
| |
| Magic environment variables have the following limitations: |
| only `${FLUTTER_LOGS_DIR}` is currently supported and it needs to be used |
| alone within the parameter string(e.g. `["${FLUTTER_LOGS_DIR}"]` is OK |
| but `["path=${FLUTTER_LOGS_DIR}"]` is not). |
| |
| The current list of supported magic variables is: |
| |
| * `${FLUTTER_LOGS_DIR}` - translated to the path of the temporary |
| folder where logs are being placed. |
| * `${LUCI_WORKDIR}` - translated to the LUCI chroot working directory. |
| * `${LUCI_CLEANUP}` - translated to the LUCI chroot temp directory. |
| * `${REVISION}` - translated to the engine commit in postsubmit. In presubmit |
| it is translated to an empty string. |
| |
| ### Build |
| |
| A build is a dictionary with a gn command, a ninja command, zero or more |
| generator commands, zero or more local tests, zero or more local |
| generators and zero or more output artifacts. |
| |
| The following is the high level structure of the build component: |
| |
| ```json |
| { |
| "archives": [], |
| "drone_dimensions": [], |
| "gclient_variables": {}, |
| "gn": [], |
| "name": "host_debug", |
| "generators": [], |
| "ninja": {}, |
| "tests": [] |
| } |
| ``` |
| |
| Each build element will be translated to an independent sub-build and its |
| entire out directory will be uploaded to CAS. |
| |
| `gn`, `ninja`, `generators` and `tests` properties are optional. Gn and |
| ninja properties can be used without generators or tests. Generators with |
| no gn and ninja properties is also supported. |
| |
| #### Archives |
| |
| An archive component is used to tell the recipes which artifacts are |
| generated by the build and where to upload them. |
| |
| By default the build output is archived to CAS in order to be used |
| as a dependency for global tests. If no CAS archive |
| is required `cas_archive": false,` needs to be added to the |
| configuration. |
| |
| ```json |
| { |
| "name": "host_debug", |
| "base_path": "out/host_debug/zip_archives/", |
| "type": "gcs", |
| "include_paths": [ |
| "out/host_debug/zip_archives/linux-x64/artifacts.zip" |
| ], |
| "realm": "production" |
| } |
| ``` |
| |
| Description of the fields: |
| |
| * **name:** - by default the entire build output is uploaded to CAS. |
| `name` is used to associate the CAS hash to a value that can be referenced |
| later as a dependency of global tests. Name is also used to select the folder |
| from within src/out to upload to CAS. e.g if the build generates |
| src/out/host_debug name must be `host_debug`. |
| * **base\_path:** - the portion of the path to remove from the full path before |
| uploading to its final destination. In the example the above the |
| base\_path **“out/host\_debug/zip\_archives”** will be removed from the |
| include path **"out/host\_debug/zip\_archives/linux-x64/artifacts.zip"** |
| before uploading to GCS, e.g. |
| <bucket>/flutter/<commit>/linux-x64/artifacts.zip. |
| * **Type:** - the type of storage to use. Currently only **“gcs”** and |
| **“cas”** are supported. "gcs" uploads artifacts to GCS |
| and "cas" to archive to CAS service. Cas value is used during development where |
| we need to inspect the generated artifacts without worrying about location or |
| cleanups. Gcs is expected for any artifacts being consumed by the flutter tool. |
| * **Include\_paths:** - a list of strings with the paths to be uploaded to a |
| given destination. |
| * **cas_archive** - a boolean value indicating whether the build output will |
| be archived to CAS or not. The default value is true. |
| * **realm** - a string value of either `production` or `experimental` |
| where production means the artifact will be uploaded to the location expected |
| by the flutter tool and experimental will add an `experimental` prefix to the |
| path to avoid interfering with production artifacts. |
| |
| #### Drone\_dimensions |
| |
| A list of strings with key value pairs separated by an equal sign. These |
| dimensions are used to select the bot where the sub-build will be running. |
| |
| To find the list of valid keys and values you need to select a [bot from the |
| swarming UI](https://chromium-swarm.appspot.com/botlist?c=id&c=task&c=os&c=status&d=asc&f=pool%3Aluci.flex.try&k=pool&s=id). |
| On the `dimensions` section the left column contains the keys and |
| the right column contains the allowed values. If multiple values are allowed |
| for a key they are separated using `|` (pipe symbol). |
| |
| ```json |
| "drone_dimensions": [ |
| "device_type=none", |
| "os=Linux" |
| ] |
| ``` |
| |
| In the previous example, the build containing this drone\_dimensions component |
| will run on a bot with a Linux OS that does not have any devices attached to it. |
| |
| Drone dimensions accept values separates by `|` to specify more than one value |
| for the dimension. E.g. assuming the pool of bots have Ubuntu and Debian bots |
| a dimension of `"os": "Debian|Ubuntu"` will resolve to use bots running either |
| Debian or Ubuntu. |
| |
| #### Gclient\_variables |
| |
| A dictionary with variables passed to gclient during a gclient sync operation. |
| They are usually used to add or remove gclient dependencies. |
| |
| ```json |
| "gclient_variables": { |
| "download_android_deps": false |
| } |
| ``` |
| |
| The example above is used to avoid downloading the |
| [android sdk dependencies](https://cs.opensource.google/flutter/engine/+/main:DEPS;l=80) |
| in builders that do not need it. |
| |
| #### GN |
| |
| A list of strings representing flags passed to the |
| [tools/gn](https://github.com/flutter/engine/blob/main/tools/gn) script. The strings can be in the form of “--flag=value” or |
| “--flag” followed by “value”. |
| |
| ```json |
| "gn": [ |
| "--runtime-mode", |
| "debug", |
| "--prebuilt-dart-sdk", |
| "--build-embedder-examples" |
| ], |
| ``` |
| |
| The previous example will prepare the configurations to build a host debug |
| version using a prebuilt dart sdk and also build the embedder examples. |
| |
| #### Ninja |
| |
| A dictionary with two keys: “config” which references the configs created by gn |
| and “target” which is a list of strings with the Ninja targets to build. |
| |
| ```json |
| "ninja": { |
| "config": "host_debug", |
| "targets": [ |
| "flutter/build/archives:artifacts", |
| "flutter/build/archives:embedder", |
| ] |
| }, |
| ``` |
| |
| In the example above the ninja command will use the configuration for |
| host\_debug and will build artifacts and embedder targets as described |
| by the |
| [flutter/build/archives/BUILD.gn](https://github.com/flutter/engine/blob/main/build/archives/BUILD.gn) |
| file. |
| |
| #### Tests |
| |
| This section of the build configuration is also known as local tests. It |
| contains a list of dictionaries with configurations for scripts and |
| parameters used to run tests inside the current build unit. These tests |
| should not reference or use anything outside of the commit checkout or |
| the outputs generated by running gn and ninja sections of the build |
| configuration. |
| |
| ```json |
| "tests": [ |
| { |
| "language": "python3", |
| "name": "Host Tests for host_debug_impeller_vulkan", |
| "parameters": [ |
| "--variant", |
| "host_debug_impeller_vulkan", |
| "--type", |
| "impeller-vulkan", |
| "--engine-capture-core-dump" |
| ], |
| "script": "flutter/testing/run_tests.py", |
| "contexts": ["android_virtual_device"] |
| } |
| ] |
| ``` |
| |
| Description of the fields: |
| |
| * **language** - the executable used to run the script, e.g. python3, bash. |
| In general any executable found in the path can be used as language. The |
| default is empty which means no interpreter will be used to run the script |
| and it is assumed the script is already an executable with the right |
| permissions to run in the target platform. |
| * **name** - the name of the step running the script. |
| * **parameters** - flags or parameters passed to the script. Parameters |
| accept magic environment variables(placeholders replaced before executing |
| the test). |
| * **Script** - the path to the script to execute relative to the checkout |
| directory. |
| * **contexts** - a list of available contexts to add to the text execution step. |
| The list of supported contexts can be found [here](https://flutter.googlesource.com/recipes/+/refs/heads/main/recipe_modules/flutter_deps/api.py#687). As of 06/20/23 two contexts are supported: |
| "android_virtual_device" and "metric_center_token". |
| |
| The test scripts will run in a deferred context (failing the step only after |
| logs have been uploaded). Tester and builder recipes provide an environment |
| variable called FLUTTER\_LOGS\_DIR pointing a temporary directory where the |
| test runner can place any logs|artifacts needed to debug issues. At the end |
| of the test execution the content of FLUTTER\_LOGS\_DIR will be uploaded to |
| Google Cloud Storage before signaling the pass | fail test state. |
| |
| Contexts are free form python contexts that communicate with the test script |
| through environment variables. E.g. metric_center_token saves an access token |
| to an [environment variable "token_path"](https://flutter.googlesource.com/recipes/+/refs/heads/main/recipe_modules/token_util/api.py#14) for the test to access it. |
| |
| Note that to keep the recipes generic they don’t know anything about what |
| the test script is doing and it is the responsibility of the test script to |
| copy the relevant files to the FLUTTER\_LOGS\_DIR directory. |
| |
| #### Generators |
| |
| Generators are scripts used to generate artifacts combining the output of two |
| or more sub-builds. The most common use case is to generate universal binaries for |
| Mac/iOS artifacts. |
| |
| Generators can be written in any language but they are required to follow some |
| guidelines to make them compatible with the engine build system. |
| |
| The guidelines are as follows: |
| |
| * Flags receiving paths to resources from multiple sub-builds need to use paths |
| relative to the checkout (`src/`) directory. If there are global generators in a build |
| configuration, the engine\_v2 recipes will download the full sub-build archives |
| to the src/out/<sub-build name> directory. |
| * Flags receiving paths to output directories must use paths relative to the |
| src/out folder. This is to be able to reference the artifacts in the global |
| archives section. |
| * The script is in charge of generating the final artifact, e.g. if the script |
| generates multiple files that will be zipped later, then it is the responsibility |
| of the script to generate the final zip. |
| * If the generator is producing a Mac/iOS artifact, then it is the responsibility |
| of the script to embed the signing metadata. |
| |
| Generators contain a single property “tasks” which is a list of tasks to be |
| performed. |
| |
| ```json |
| "generators": { |
| "tasks": [] |
| } |
| ``` |
| |
| The example above represents a generator configuration with an empty list |
| of tasks. |
| |
| ##### Task |
| |
| A `task` is a dictionary describing the scripts to be executed. |
| |
| The property's description is as follows: |
| |
| * **Name** - the name of the step running the script. |
| * **Parameters** - flags passed to the script. Both input and output paths must |
| be relative to the checkout directory. |
| * **Script**, the script path relative to the checkout repository. |
| * **Language**, the script language executable to run the script. If empty it is assumed to be bash. |
| |
| ```json |
| { |
| "name": "Debug-FlutterMacOS.framework", |
| "parameters": [ |
| "--dst", |
| "out/debug", |
| "--arm64-out-dir", |
| "out/ios_debug", |
| "--simulator-x64-out-dir", |
| "out/ios_debug_sim", |
| "--simulator-arm64-out-dir", |
| "out/ios_debug_sim_arm64" |
| ], |
| "script": "flutter/sky/tools/create_full_ios_framework.py", |
| "language": "python3" |
| } |
| ``` |
| |
| ### Global Tests |
| |
| Tests in this section run on a separate bot as independent sub-builds. |
| As opposed to tests running within builds, global tests have access to the |
| the outputs of all the builds running in the same orchestrator build. A |
| use case for global tests is to run flutter/framework tests using the |
| artifacts generated by an specific engine build. |
| |
| Global tests currently support two different scenarios: |
| |
| * flutter/flutter tests with [tester](https://flutter.googlesource.com/recipes/+/refs/heads/main/recipes/engine_v2/tester.py) |
| recipe. This workflow checks out flutter/flutter to run any of the existing |
| sharded tests using the engine artifacts archived to GCS. |
| * complicated engine tests that require the outputs from multiple subbuilds |
| with [tester_engine](https://flutter.googlesource.com/recipes/+/refs/heads/main/recipes/engine_v2/tester_engine.py). |
| This workflow checks out [flutter/engine] and operates over the dependencies passed to it using cas. |
| |
| Note: the supported scenarios can be later extended to support running devicelab tests although a |
| [smart scheduler](https://github.com/flutter/flutter/issues/128294) is a prerequisite for |
| it to be scalable(build/test separation model). |
| |
| Framework test example: |
| |
| ```json |
| { |
| "tests": [ |
| { |
| "name": "web-tests-1", |
| "shard": "web_tests", |
| "subshard": "1", |
| "test_dependencies": [ |
| { |
| "dependency": "chrome_and_driver", |
| "version": "version:111.0a" |
| } |
| ] |
| } |
| ] |
| } |
| ``` |
| |
| The property's description is as follows: |
| |
| * **name** the name that will be assigned to the sub-build. |
| * **shard** the flutter/flutter shard test to run. The supported shard names can be found |
| on the flutter framework [test.dart](https://github.com/flutter/flutter/blob/master/dev/bots/test.dart#L244). |
| * **subshard** one of the accepted subshard values for shard. Sub-shards are defined as part |
| of the shard implementation, please look at the corresponding shard implementation to find the |
| accepted values. |
| * **test_dependencies** a list of [dependencies](https://flutter.googlesource.com/recipes/+/refs/heads/main/recipe_modules/flutter_deps/api.py#75) |
| required for the test to run. |
| |
| Engine test example: |
| |
| ```json |
| { |
| "tests": [ |
| { |
| "name": "test: clang_tidy android_debug_arm64", |
| "recipe": "engine_v2/tester_engine", |
| "drone_dimensions": [ |
| "device_type=none", |
| "os=Linux" |
| ], |
| "dependencies": [ |
| "host_debug", |
| "android_debug_arm64" |
| ], |
| "tasks": [ |
| { |
| "name": "test: clang_tidy android_debug_arm64", |
| "parameters": [ |
| "--variant", |
| "android_debug_arm64", |
| "--lint-all", |
| "--shard-id=0", |
| "--shard-variants=host_debug" |
| ], |
| "max_attempts": 1, |
| "script": "flutter/ci/clang_tidy.sh" |
| } |
| ] |
| } |
| ] |
| } |
| ``` |
| |
| The property's description is as follows: |
| |
| * **name** the name to assign to the sub-build. |
| * **recipe** the recipe name to use if different than tester. |
| * **drone_dimensions** a list of strings with key values to select the |
| bot where the test will run. |
| * **dependencies** a list of build outputs required |
| by the test. These build outputs are referenced by the name of build |
| generating the output. This type of dependency is shared using CAS and |
| the contents are mounted in checkout/src/out. E.g. a build configuration |
| building the `host_engine` configuration will upload the content of |
| checkout/src/out/host_engine to CAS and a global test with a `host_engine` |
| dependency will mount the content of host engine in the same location of |
| the bot running the test. |
| * **tasks** a list of dictionaries representing scripts and parameters to run them. |
| |
| Example task configuration: |
| |
| ```json |
| { |
| "name": "test: clang_tidy android_debug_arm64", |
| "parameters": [ |
| "--variant", |
| "android_debug_arm64", |
| "--lint-all", |
| "--shard-id=0", |
| "--shard-variants=host_debug" |
| ], |
| "max_attempts": 1, |
| "script": "flutter/ci/clang_tidy.sh" |
| } |
| ``` |
| |
| The property's description is as follows: |
| |
| * **name** the name assigned to the step running the script. |
| * **parameters** a list of parameters passed to the script execution. |
| * **max_attempts** an integer with the maximum number of runs in case of failure. |
| * **script** the path relative to checkout/src/ to run. |
| |
| ### Global Generators |
| |
| Global generators follow the same format as local generators but defined at |
| the build top level. The main difference is that global generators can create |
| new artifacts combining outputs of multiple sub-builds. |
| |
| ### Global Archives |
| |
| The archives component provides instructions to upload the artifacts generated |
| by the global generators. Is a list of dictionaries with three keys: `source` and |
| `destination`, and `realm`. `source` is a path relative to the checkout repository, |
| `destination` is a relative path to <bucket>/flutter/<commit>, and `realm` is |
| a string with either `production` or `experimental` value. |
| |
| The realm value is used to build the destination path of the artifacts. |
| `production` will upload the artifacts to the location expected by the flutter |
| tool and `experimental` will add experimental as a prefix to the path to avoid |
| interfering with the production artifacts. |
| |
| ```json |
| "archives": [ |
| { |
| "source": "out/debug/artifacts.zip", |
| "destination": "ios/artifacts.zip", |
| "realm": "production" |
| }, |
| ] |
| ``` |
| |
| The example above will cause the file <checkout>/out/debug/artifacts.zip to |
| be uploaded <bucket>/flutter/<commit>/ios/artifacts.zip. |
| |
| ## Triaging global generators |
| |
| Global generators can run locally if all their sub-build dependencies are |
| downloaded. This section explains how to triage a local generator. |
| |
| The instructions on this section can be used to triage problems with artifacts |
| created by glocal generators(E.g.`Debug|Release|Profile-ios-Flutter.xcframework`) |
| using the build outputs of CI subbuilds. During the migration to engine v2 we had |
| a regression in the size of the flutter libraries, using this process we were able |
| to inspect the files as they were generated by the CI, make changes to the generators |
| and run the generators locally to validate the fixes. |
| |
| ### Prerequisites (one time installation) |
| |
| #### Install CAS utility |
| |
| CAS client is required to download the sub-build artifacts. To install |
| it in your machine run the following steps: |
| |
| * `mkdir $HOME/tools` |
| * Download and unzip CAS binaries from |
| [https://chrome-infra-packages.appspot.com/p/infra/tools/luci/cas] |
| * Add $HOME/tools to path and your ~/.bashrc |
| |
| #### Gclient engine checkout |
| |
| Create a gclient checkout following instructions from |
| [Setting up the engine environment](https://github.com/flutter/flutter/wiki/Setting-up-the-Engine-development-environment). |
| |
| ### Download sub-builds to the gclient checkout out folder |
| |
| CAS sub-build artifacts can be downloaded using information from a LUCI build. |
| Using [https://ci.chromium.org/p/flutter/builders/prod/Mac%20mac_ios_engine/2818] |
| as an example the execution details from steps 13 - 17 show the commands to |
| download the archives. `-dir` parameter needs to be updated to point to the |
| relative or full path to the out folder in your gclient checkout. |
| |
| These are the commands to execute for our example build: |
| |
| ```bash |
| pushd <gclient checkout>/src/out |
| cas download -cas-instance projects/chromium-swarm/instances/default_instance -digest 39f15436deaed30f861bdd507ba6297f2f26a2ff13d45acfd8819dbcda346faa/88 -dir ./ |
| cas download -cas-instance projects/chromium-swarm/instances/default_instance -digest bdec3208e70ba5e50ee7bbedaaff4588d3f58167ad3d8b1c46d29c6ac3a18c00/94 -dir ./ |
| cas download -cas-instance projects/chromium-swarm/instances/default_instance -digest d19edb65072aa9d872872b55d3c270db40c6a626c8a851ffcb457a28974f3621/84 -dir ./ |
| cas download -cas-instance projects/chromium-swarm/instances/default_instance -digest ac6f08662d18502cfcd844771bae736f4354cb3fe209552fcf2181771e139e0b/86 -dir ./ |
| cas download -cas-instance projects/chromium-swarm/instances/default_instance -digest 1d4d1a3b93847451fe69c1939d7582c0d728b198a40abd06f43d845117ef3214/86 -dir ./ |
| ``` |
| |
| The previous commands will create the ios_debug, ios_debug_sim, ios_debug_sim_arm64, |
| ios_profile, and ios_release folders with the artifacts generated by its |
| corresponding sub-builds. |
| |
| Once the checkout and dependencies are available locally we can cd|pushd to the |
| root of the client checkout and run the global generator. The command can be |
| copied verbatim from the executions details of the build. |
| |
| The following example will run the generator to create the ios artifacts: |
| |
| ```bash |
| python3 flutter/sky/tools/create_full_ios_framework.py --dst out/release \ |
| --arm64-out-dir out/ios_release --simulator-x64-out-dir out/ios_debug_sim \ |
| --simulator-arm64-out-dir out/ios_debug_sim_arm64 --dsym --strip |
| ``` |