blob: c7aa80c9a4f16f47af5867c4988036d0053dcbe0 [file] [log] [blame]
# Copyright 2021 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Flutter Engine builder recipe.
This recipe is used to build flavors of flutter engine identified by lists of
gn flags and ninja configs and targets.
The following are examples of valid configurations passed to builders using
this recipe in the builds property:
{
"gn" : [
"--ios",
"--runtime-mode",
"debug",
"--simulator",
"--no-lto"
],
"ninja": {
"config": "ios_debug_sim",
"targets": ["ios_test_flutter"]
}
}
"""
import copy
from contextlib import contextmanager
from google.protobuf import struct_pb2
from PB.recipes.flutter.engine.engine import InputProperties
from PB.recipes.flutter.engine.engine import EnvProperties
from PB.go.chromium.org.luci.buildbucket.proto import build as build_pb2
DEPS = [
'depot_tools/gsutil',
'flutter/build_util',
'flutter/flutter_deps',
'flutter/os_utils',
'flutter/osx_sdk',
'flutter/repo_util',
'flutter/retry',
'flutter/shard_util_v2',
'flutter/test_utils',
'fuchsia/cas_util',
'recipe_engine/buildbucket',
'recipe_engine/context',
'recipe_engine/file',
'recipe_engine/path',
'recipe_engine/platform',
'recipe_engine/properties',
'recipe_engine/raw_io',
'recipe_engine/step',
]
PROPERTIES = InputProperties
ENV_PROPERTIES = EnvProperties
def Build(api, checkout, env, env_prefixes, outputs):
"""Builds a flavor identified as a set of gn and ninja configs."""
ninja_tool = {
"ninja": api.build_util.build,
"autoninja": api.build_util.build_autoninja,
}
deps = api.properties.get('dependencies', [])
api.flutter_deps.required_deps(env, env_prefixes, deps)
build = api.properties.get('build')
api.build_util.run_gn(build.get('gn'), checkout)
ninja = build.get('ninja')
ninja_tool[ninja.get('tool', 'ninja')
](ninja.get('config'), checkout, ninja.get('targets'))
# Archive full build. This is inneficient but necessary for global generators.
full_build_hash = api.shard_util_v2.archive_full_build(
checkout.join('out', build.get('name')), build.get('name'))
outputs['full_build'] = full_build_hash
generator_tasks = build.get('generators', {}).get('tasks', [])
pub_dirs = build.get('generators', {}).get('pub_dirs', [])
archives = build.get('archives', [])
# Get only local tests.
tests = [t for t in build.get('tests', []) if t.get('type') == 'local']
with api.context(env=env, env_prefixes=env_prefixes,
cwd=checkout.join('flutter')):
# Run pub on all of the pub_dirs.
for pub in pub_dirs:
pub_dir = api.path.abs_to_path(
api.path.dirname(
checkout.join(pub))
)
with api.context(env=env, env_prefixes=env_prefixes,
cwd=pub_dir):
api.step('dart pub get', ['dart', 'pub', 'get'])
for generator_task in generator_tasks:
# Generators must run from inside flutter folder.
cmd = []
for script in generator_task.get('scripts'):
full_path_script = checkout.join(script)
cmd.append(full_path_script)
cmd.extend(generator_task.get('parameters', []))
api.step(generator_task.get('name'), cmd)
# Run local tests in the builder to optimize resource usage.
for test in tests:
command = [test.get('language')] if test.get('language') else []
# Ideally local tests should be completely hermetic and in theory we can run
# them in parallel using futures. I haven't found a flutter engine
# configuration with more than one local test but once we find it we
# should run the list of tests using parallelism.
# TODO(godofredoc): Optimize to run multiple local tests in parallel.
command.append(checkout.join(test.get('script')))
command.extend(test.get('parameters', []))
#api.step(test.get('name'), command)
step_name = api.test_utils.test_step_name(test.get('name'))
def run_test():
return api.step(step_name, command)
# Rerun test step 3 times by default if failing.
# TODO(keyonghan): notify tree gardener for test failures/flakes:
# https://github.com/flutter/flutter/issues/89308
api.retry.wrap(run_test, step_name=test.get('name'))
for archive_config in archives:
outputs[archive_config['name']] = Archive(api, checkout, archive_config)
def Archive(api, checkout, archive_config):
archive_dir = api.path.mkdtemp(archive_config['name'])
# First remove paths from excluding list.
for exclude_path in archive_config.get('exclude_paths', []):
full_exclude_path = api.path.abspath(checkout.join(exclude_path))
api.file.rmtree('Remove %s' % exclude_path, full_exclude_path)
# Mock a directory path to make tests pass.
api.path.mock_add_directory(api.path.abspath(checkout.join('out/host_debug_unopt/')))
api.path.mock_add_file(api.path.abspath(checkout.join('out/host_debug_unopt/file.zip')))
# Android artifacts are uploaded to a different bucket. If they exist we need a second
# gsutil upload.
upload_android_artifacts = False
for include_path in archive_config.get('include_paths', []):
full_include_path = api.path.abspath(checkout.join(include_path))
if include_path.endswith('download.flutter.io'):
upload_android_artifacts = True
if api.path.isdir(full_include_path):
dir_name = api.path.basename(full_include_path)
api.file.copytree('Copy %s' % include_path, full_include_path, archive_dir.join(dir_name))
else:
dir_name = api.path.dirname(full_include_path)
full_base_path = api.path.abspath(checkout.join(archive_config.get('base_path','')))
rel_path = api.path.relpath(dir_name, full_base_path)
rel_path = '' if rel_path == '.' else rel_path
base_name = api.path.basename(full_include_path)
api.file.ensure_directory('Ensuring %s' % archive_dir.join(rel_path), archive_dir.join(rel_path))
api.file.copy('Copy %s' % include_path, full_include_path, archive_dir.join(rel_path, base_name))
if archive_config.get('type') == 'gcs' and archive_config.get('include_paths', []):
bucket = 'flutter_archives_v2'
if api.buildbucket.gitiles_commit.project == 'monorepo':
commit = api.repo_util.get_commit(checkout.join('../../monorepo'))
artifact_prefix = 'monorepo/'
else:
commit = api.repo_util.get_commit(checkout.join('flutter'))
artifact_prefix = ''
artifact_path = '%sflutter_infra_release/flutter/%s/' % (artifact_prefix,
commit)
api.gsutil.upload(
source='%s/*' % archive_dir,
bucket=bucket,
dest=artifact_path,
args=['-r'],
name=archive_config['name'],
)
# Jar and pom files are uploaded to download.flutter.io while all the other artifacts
# are uploaded to flutter_infra_release. If we override paths artifacts need to be organized
# as gs://<overriden_bucket>/flutter_infra_release for non android artifacts and
# gs://<overriden_bucket>/download.flutter.io for android artifacts.
if upload_android_artifacts:
android_artifact_path = artifact_prefix
api.gsutil.upload(
source='%s/download.flutter.io/' % archive_dir,
bucket=bucket,
dest=android_artifact_path,
args=['-r'],
name=archive_config['name'],
)
return 'gs://%s/%s/%s' % ( bucket, artifact_path, api.path.basename(archive_dir))
# Archive using CAS by default
return api.cas_util.upload(archive_dir, step_name='Archive %s' % archive_config['name'])
def RunSteps(api, properties, env_properties):
checkout = api.path['cache'].join('builder', 'src')
api.file.rmtree('Clobber build output', checkout.join('out'))
cache_root = api.path['cache'].join('builder')
api.file.ensure_directory('Ensure checkout cache', cache_root)
# Enable long path support on Windows.
api.os_utils.enable_long_paths()
env, env_prefixes = api.repo_util.engine_environment(api.path['cache'].join('builder'))
# Engine path is used inconsistently across the engine repo. We'll start
# with [cache]/builder and will adjust it to start using it consistently.
env['ENGINE_PATH'] = api.path['cache'].join('builder')
custom_vars = api.properties.get('gclient_custom_vars', {})
clobber = api.properties.get('clobber', False)
if api.buildbucket.gitiles_commit.project == 'monorepo':
api.repo_util.monorepo_checkout(
cache_root, env, env_prefixes, clobber=clobber, custom_vars=custom_vars
)
checkout = api.path['cache'].join('builder', 'engine', 'src')
else:
api.repo_util.engine_checkout(
cache_root, env, env_prefixes, clobber=clobber, custom_vars=custom_vars
)
outputs = {}
if api.platform.is_mac:
with api.osx_sdk('ios'):
Build(api, checkout, env, env_prefixes, outputs)
else:
Build(api, checkout, env, env_prefixes, outputs)
output_props = api.step('Set output properties', None)
output_props.presentation.properties['cas_output_hash'] = outputs
def GenTests(api):
build = {
"archives": [
{
"name": "host_debug_unopt",
"type": "cas",
"include_paths": ['out/host_debug_unopt/', 'out/host_debug_unopt/file.zip',
'out/host_debug_unopt/download.flutter.io'],
"exclude_paths": ['out/host_debug_unopt/obj', 'out/host_debug_unopt/stripped.exe']
}
],
"gn": ["--ios"], "ninja": {"config": "ios_debug", "targets": []},
"generators": {
"pub_dirs": ["dev"],
"tasks": [
{
"name": "generator1",
"scripts": ["script1.sh", "dev/felt.dart"],
"parameters": ["--argument1"]
}
]
},
"tests": [
{
"name": "mytest", "script": "myscript.sh",
"parameters": ["param1", "param2"], "type": "local"
}
]
}
yield api.test('basic', api.properties(build=build, goma_jobs="100"))
yield api.test(
'mac', api.properties(build=build, goma_jobs="100"),
api.platform('mac', 64),
)
yield api.test(
'monorepo', api.properties(build=build, goma_jobs="100"),
api.buildbucket.ci_build(
project='dart',
bucket='ci.sandbox',
git_repo='https://dart.googlesource.com/monorepo',
git_ref='refs/heads/main'
),
)
build_custom = dict(build)
build_custom["gclient_custom_vars"] = {"example_custom_var": True}
build_custom["tests"] = []
yield api.test(
'basic_custom_vars', api.properties(build=build_custom, goma_jobs="100")
)
# gcs archives
build_gcs = copy.deepcopy(build)
build_gcs['archives'][0]['type'] = 'gcs'
yield api.test(
'basic_gcs', api.properties(build=build_gcs, goma_jobs="100"),
api.step_data(
'git rev-parse',
stdout=api.raw_io
.output_text('12345abcde12345abcde12345abcde12345abcde\n')
)
)
yield api.test(
'monorepo_gcs', api.properties(build=build_gcs, goma_jobs="100"),
api.buildbucket.ci_build(
project='dart',
bucket='ci.sandbox',
git_repo='https://dart.googlesource.com/monorepo',
git_ref='refs/heads/main'
),
api.step_data(
'git rev-parse',
stdout=api.raw_io
.output_text('12345abcde12345abcde12345abcde12345abcde\n')
)
)