| # Copyright 2017 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Recipe for building Fuchsia and running tests.""" |
| |
| from google.protobuf import text_format as textpb |
| |
| from recipe_engine.config import Enum, List, Set, Single |
| from recipe_engine.recipe_api import Property |
| |
| from PB.infra.fuchsia import Fuchsia |
| |
| TARGETS = ['arm64', 'x64'] |
| |
| BUILD_TYPES = ['debug', 'release', 'thinlto', 'lto'] |
| |
| BUILD_ARTIFACT_HASH_PROPERTY = 'build_artifact_hash' |
| |
| TEST_MULTIPLIER_KEY = 'MULTIPLY' |
| |
| DEPS = [ |
| 'fuchsia/artifacts', |
| 'fuchsia/build', |
| 'fuchsia/build_input_resolver', |
| 'fuchsia/buildbucket_util', |
| 'fuchsia/checkout', |
| 'fuchsia/experimental', |
| 'fuchsia/fuchsia', |
| 'fuchsia/git', |
| 'fuchsia/gitiles', |
| 'fuchsia/gsutil', |
| 'fuchsia/hash', |
| 'fuchsia/jiri', |
| 'fuchsia/jsonutil', |
| 'fuchsia/tar', |
| 'fuchsia/testsharder', |
| 'fuchsia/testing', |
| 'fuchsia/testing_requests', |
| 'fuchsia/spec', |
| 'recipe_engine/buildbucket', |
| 'recipe_engine/cipd', |
| 'recipe_engine/context', |
| 'recipe_engine/file', |
| 'recipe_engine/isolated', |
| 'recipe_engine/json', |
| 'recipe_engine/path', |
| 'recipe_engine/platform', |
| 'recipe_engine/properties', |
| 'recipe_engine/python', |
| 'recipe_engine/raw_io', |
| 'recipe_engine/step', |
| 'recipe_engine/swarming', |
| 'recipe_engine/url', |
| ] |
| |
| PROPERTIES = { |
| 'spec_remote': |
| Property( |
| kind=str, |
| help='URL of the specs git repository', |
| default='http://fuchsia.googlesource.com/integration'), |
| } |
| |
| |
| def RunSteps(api, spec_remote): |
| tryjob = api.buildbucket_util.is_tryjob |
| |
| # Resolve the build input to always contain a Gitiles commit. |
| bb_build = api.buildbucket.build |
| api.build_input_resolver.resolve( |
| bb_build.input, |
| default_project_url='https://fuchsia.googlesource.com/fuchsia') |
| bb_input = bb_build.input |
| with api.step.nest('got_revision') as presentation: |
| # Read by the binary-size Gerrit plugin |
| presentation.properties['got_revision'] = bb_input.gitiles_commit.id |
| |
| with api.step.nest('load spec') as bootstrap_step: |
| try: |
| bootstrap_step.presentation.step_text = 'loading spec' |
| spec = api.spec.load_from_build( |
| build=bb_build, |
| spec_remote=spec_remote, |
| Type=Fuchsia, |
| ) |
| except api.spec.ParseError as e: |
| raise api.step.StepFailure('failed to parse spec: %s' % str(e)) |
| |
| # Log the textproto to logdog so we can programmatically scrape spec files |
| # to check into the tree. |
| bootstrap_step.presentation.logs['textproto'] = textpb.MessageToString( |
| spec).split('\n') |
| set_output_properties_from_spec(bootstrap_step, spec) |
| |
| # Handle illegal settings around secrets. |
| if spec.test.requires_secrets: |
| if tryjob or spec.test.device_type != 'QEMU': # pragma: no cover |
| raise api.step.InfraFailure( |
| 'the secrets pipeline is only supported in CI and on QEMU') |
| |
| checkout_root = api.path['start_dir'].join('fuchsia') |
| if spec.checkout.use_snapshot: |
| if bb_input.gerrit_changes: |
| assert len(bb_input.gerrit_changes) == 1 |
| checkout = api.checkout.from_patchset_snapshot( |
| path=checkout_root, |
| gerrit_change=bb_input.gerrit_changes[0], |
| attributes=spec.checkout.attributes) |
| else: |
| checkout = api.checkout.from_commit_snapshot( |
| path=checkout_root, |
| gitiles_commit=bb_input.gitiles_commit, |
| attributes=spec.checkout.attributes) |
| |
| else: |
| assert spec.checkout.manifest |
| assert spec.checkout.remote |
| checkout = api.checkout.fuchsia_with_options( |
| path=checkout_root, |
| build=bb_build, |
| manifest=spec.checkout.manifest, |
| remote=spec.checkout.remote, |
| project=spec.checkout.project, |
| rebase_revision=bb_input.gitiles_commit.id, |
| is_release_version=spec.checkout.is_release_version, |
| attributes=spec.checkout.attributes, |
| ) |
| |
| if spec.checkout.upload_results: |
| assert spec.gcs_bucket, ( |
| 'gcs_bucket must be set if checkout.upload_results is') |
| checkout.upload_results(spec.gcs_bucket) |
| |
| # We can only derive repo from buildbucket when triggered in CI or CQ; |
| # validate the checkout then. |
| # Moreover, skip validation if checking out a snapshot, as that may have been |
| # triggered by a project not corresponding to a repo in the checkout. |
| triggered_by_ci_cq = ( |
| len(bb_input.gerrit_changes) > 0 or bb_input.gitiles_commit.project) |
| if triggered_by_ci_cq and not spec.checkout.use_snapshot: |
| if bb_input.gerrit_changes: |
| project = bb_input.gerrit_changes[0].project |
| else: |
| project = bb_input.gitiles_commit.project |
| with api.step.nest('validate checkout'), api.context(cwd=checkout.root_dir): |
| repo_path = api.jiri.project(projects=[project]).json.output[0]['path'] |
| api.python( |
| 'validate FIDL namespaces', |
| checkout.root_dir.join('scripts', 'style', |
| 'verify-fidl-libraries.py'), |
| args=['--repo', repo_path], |
| ) |
| |
| # Check for properly formatted JSON in commit message before build |
| multipliers_path = None |
| if bb_input.gerrit_changes and spec.test.test_in_shards: |
| with api.step.nest('test multipliers'): |
| # Get project dir for gerrit change from source manifest |
| gerrit_change = bb_input.gerrit_changes[0] |
| project_dir = checkout.root_dir |
| repo_url = 'https://%s/%s' % (gerrit_change.host.replace( |
| '-review', ''), gerrit_change.project) |
| dirs = checkout.source_manifest['directories'] |
| for d in dirs: |
| if dirs[d]['git_checkout']['repo_url'] == repo_url: |
| if d != '.': |
| project_dir = checkout.root_dir.join(d) |
| break |
| |
| with api.context(cwd=project_dir): |
| commit_msg = api.git.get_commit_message(name='get commit msg') |
| multipliers = api.jsonutil.extract_from_text( |
| 'extract', |
| text=commit_msg, |
| key=TEST_MULTIPLIER_KEY, |
| default_contents=[]) |
| multipliers_path = api.path.mkstemp() |
| api.file.write_json('write', multipliers_path, multipliers, indent=2) |
| |
| collect_build_metrics = spec.build.upload_results |
| archives_to_build = [] |
| # If we are not building images, then we need not build package-related |
| # targets. |
| if not spec.build.exclude_images: |
| archives_to_build.extend(['archive', 'package-archive']) |
| if spec.build.include_breakpad_symbols: |
| archives_to_build.append('breakpad-symbol-archive') |
| if spec.build.include_symbol_archive: |
| archives_to_build.append('symbol-archive') |
| |
| build_dir = checkout.root_dir.join('out') |
| |
| # Add infratools to universe packages and ninja_targets to ensure they are built. |
| spec.build.universe_packages.append('//bundles:infratools') |
| spec.build.ninja_targets.append('bundles:infratools') |
| |
| build = api.build.with_options( |
| build_dir=build_dir, |
| checkout=checkout, |
| target=str(spec.build.target), |
| build_type=str(spec.build.build_type), |
| packages=spec.build.packages, |
| universe_packages=spec.build.universe_packages, |
| variants=spec.build.variants, |
| gn_args=spec.build.gn_args, |
| ninja_targets=list(spec.build.ninja_targets), |
| board=spec.build.board, |
| product=spec.build.product, |
| collect_build_metrics=collect_build_metrics, |
| build_images=not spec.build.exclude_images, |
| archives_to_build=tuple(archives_to_build), |
| gcs_bucket=spec.gcs_bucket, |
| ) |
| |
| check_sizes_result = api.step( |
| 'check sizes', [ |
| build.tool('size_checker'), '--build-dir', build.fuchsia_build_dir, |
| '--sizes-json-out', |
| api.json.output() |
| ], |
| step_test_data=lambda: api.json.test_api.output({'some-file': 123})) |
| |
| # This property is read by the binary-size Gerrit plugin. |
| if check_sizes_result.json.output: |
| check_sizes_result.presentation.properties[ |
| 'binary_sizes'] = check_sizes_result.json.output |
| |
| if spec.debug_symbol_gcs_bucket: |
| build.upload_debug_symbols( |
| debug_symbol_gcs_bucket=spec.debug_symbol_gcs_bucket, |
| gcs_bucket=spec.gcs_bucket if spec.build.upload_results else None, |
| ) |
| |
| if spec.build.upload_results: |
| assert spec.gcs_bucket, 'gcs_bucket must be set if build.upload_results is' |
| build.upload_results(gcs_bucket=spec.gcs_bucket) |
| |
| shards = [] |
| if spec.test.test_in_shards: |
| shards = api.testsharder.execute( |
| 'create test shards', |
| testsharder_path=build.tool('testsharder'), |
| build_dir=build.fuchsia_build_dir, |
| max_shard_size=spec.test.max_shard_size, |
| multipliers=multipliers_path, |
| tags=spec.build.environment_tags, |
| ) |
| |
| build_artifacts = build.get_artifacts(shards) |
| |
| api.artifacts.gcs_bucket = spec.artifact_gcs_bucket |
| api.artifacts.uuid = api.buildbucket_util.id |
| |
| orchestration_inputs = None |
| if spec.build.run_tests: |
| if spec.test.test_in_shards: |
| shard_requests = api.testing_requests.shard_requests( |
| build_artifacts, |
| bb_build, |
| spec.test.per_test_timeout_secs, |
| spec.test.pool, |
| spec.test.swarming_expiration_timeout_secs, |
| spec.test.swarming_io_timeout_secs, |
| spec.test.use_runtests, |
| timeout_secs=spec.test.timeout_secs) |
| else: |
| shard_requests = api.testing_requests.deprecated_shard_requests( |
| build_artifacts, |
| api.testing_requests.deprecated_test_cmds(spec.test), |
| spec.test.device_type, |
| spec.test.pool, |
| spec.test.timeout_secs, |
| spec.test.pave, |
| requires_secrets=spec.test.requires_secrets, |
| swarming_expiration_timeout_secs=spec.test |
| .swarming_expiration_timeout_secs, |
| swarming_io_timeout_secs=spec.test.swarming_io_timeout_secs, |
| ) |
| |
| orchestration_inputs = api.build.TestOrchestrationInputs( |
| build_artifacts.llvm_symbolizer, build_artifacts.minfs, |
| build_artifacts.symbolize_tool, shard_requests, |
| build_artifacts.tests_file) |
| |
| # Must be done after testing.shard_requests() is called, because that |
| # modifies the filesystem images. TODO(garymm,joshuaseaton): once legacy_qemu |
| # code paths are removed, remove this comment as it will become false. |
| api.artifacts.upload('upload artifacts', build=build) |
| |
| all_results = [] |
| if spec.build.run_tests: |
| if spec.test.test_in_shards: |
| all_results = api.testing.test_in_shards( |
| collect_timeout_secs=spec.test.collect_timeout_secs, |
| debug_symbol_gcs_bucket=spec.debug_symbol_gcs_bucket, |
| orchestration_inputs=orchestration_inputs, |
| max_attempts=spec.test.max_attempts) |
| else: |
| all_results = [ |
| api.testing.deprecated_test( |
| spec.debug_symbol_gcs_bucket, |
| spec.test.device_type, |
| orchestration_inputs, |
| max_attempts=spec.test.max_attempts) |
| ] |
| |
| if spec.test.upload_results: |
| assert spec.gcs_bucket, 'gcs_bucket must be set if test.upload_results is' |
| for test_results in all_results: |
| test_results.upload_results( |
| gcs_bucket=spec.gcs_bucket, |
| # Mixing data from and pre-submit and post-submit in catapult results |
| # in a confusing UI, so always exclude pre-submit (tryjob). We set |
| # upload_to_catapult to True for some tryjobs because it makes |
| # generating our configs easier. |
| upload_to_catapult=(not tryjob and spec.test.upload_to_catapult), |
| ) |
| |
| if 'profile' in build_artifacts.variants: |
| api.testing.process_coverage( |
| covargs_path=build_artifacts.covargs, |
| test_results=[ |
| # TODO(fxb/27336): coverage is only supported for tests on Fuchsia. |
| result for result in all_results if result.from_fuchsia |
| ], |
| ids_txt=build_artifacts.ids, |
| llvm_profdata=build_artifacts.llvm_profdata, |
| llvm_cov=build_artifacts.llvm_cov, |
| gcs_bucket=spec.gcs_bucket, |
| ) |
| |
| # Raise test failures |
| with api.step.defer_results(): |
| api.testing.raise_failures() |
| for test_results in all_results: |
| test_results.raise_failures() |
| |
| if spec.build.enforce_size_limits: |
| build.check_filesystem_sizes() |
| |
| |
| def set_output_properties_from_spec(step, spec): |
| """Sets certain properties from the spec as output properties on this recipe. |
| |
| Some builders read data from these output properties, and fail if they are not |
| present. This is to prevent them from failing. |
| """ |
| step.presentation.properties['gcs_bucket'] = spec.gcs_bucket |
| |
| |
| def GenTests(api): |
| ci_build = api.buildbucket.ci_build( |
| project='fuchsia', |
| git_repo='https://fuchsia.googlesource.com/fuchsia', |
| ) |
| |
| def try_build(project='fuchsia'): |
| return api.buildbucket.try_build( |
| project=project, |
| git_repo='https://fuchsia.googlesource.com/%s' % project, |
| ) |
| |
| # Mock gitiles refs fetched when the buildbucket build input is an unresolved |
| # Gerrit change or Gitiles commit. |
| default_gitiles_refs_steps = api.gitiles.refs('refs', [ |
| 'refs/heads/master', |
| 'deadbeef', |
| ]) |
| |
| def source_manifest_for_cq(use_snapshot=False): |
| step_name = 'checkout.jiri source-manifest' |
| return api.step_data( |
| step_name, |
| api.json.output( |
| { |
| 'directories': { |
| '.': { |
| 'git_checkout': { |
| 'repo_url': |
| 'https://fuchsia.googlesource.com/fuchsia', |
| 'revision': |
| '4c2b0da3c06341db5cebe4d02c78c93c3b2bd78b', |
| } |
| }, |
| 'garnet': { |
| 'git_checkout': { |
| 'repo_url': |
| 'https://fuchsia.googlesource.com/garnet', |
| 'revision': |
| '4c2b0da3c06341db5cebe4d02c78c93c3b2bd78b' |
| } |
| } |
| } |
| }, |
| name='source manifest')) |
| |
| def test_step_data(deprecated=False): |
| if deprecated: |
| return api.testing.task_step_data([ |
| api.swarming.task_result( |
| id='610', |
| name='QEMU', |
| outputs=['out.tar'], |
| ), |
| ]) |
| |
| return ( |
| api.testing.task_retry_step_data( |
| [ |
| api.swarming.task_result( |
| id='610', |
| name='QEMU', |
| outputs=['out.tar'], |
| ), |
| ], |
| iteration=0) + |
| api.testing_requests.shards_step_data( |
| step_name='create test shards', |
| shards=[ |
| api.testsharder.shard( |
| name='QEMU', |
| tests=api.testing_requests.default_tests(), |
| dimensions=dict(device_type='QEMU'), |
| ), |
| ]) + |
| api.testing.test_step_data(shard_name='QEMU')) # yapf: disable |
| |
| def spec_data(use_snapshot=False, |
| variants=(), |
| build_target='x64', |
| build_type='debug', |
| include_symbol_archive=False, |
| include_breakpad_symbols=False, |
| enforce_size_limits=False, |
| max_shard_size=0, |
| run_tests=True, |
| test_in_shards=True, |
| requires_secrets=False, |
| gcs_bucket=None, |
| debug_symbol_gcs_bucket='debug-symbols'): |
| test_spec = None |
| if run_tests: |
| test_spec = Fuchsia.Test( |
| max_shard_size=max_shard_size, |
| timeout_secs=30 * 60, |
| pool='fuchsia.tests', |
| test_in_shards=test_in_shards, |
| requires_secrets=requires_secrets, |
| # We should never expire a test task. This is currently 5 hours, but |
| # should be treated as infinite. |
| swarming_expiration_timeout_secs=18000, |
| swarming_io_timeout_secs=5 * 60, |
| upload_results=bool(gcs_bucket), |
| use_runtests='profile' in variants, |
| per_test_timeout_secs=5 * 60, |
| ) |
| if not test_in_shards: |
| test_spec.device_type = 'QEMU' |
| |
| step_data = api.spec.spec_loaded_ok( |
| step_name='load spec.build_init', |
| message=Fuchsia( |
| checkout=Fuchsia.Checkout( |
| manifest='manifest', |
| remote='remote', |
| upload_results=bool(gcs_bucket), |
| use_snapshot=use_snapshot, |
| ), |
| build=Fuchsia.Build( |
| variants=variants, |
| build_type=build_type, |
| run_tests=run_tests, |
| board='boards/x64.gni', |
| product='products/core.gni', |
| target=build_target, |
| include_symbol_archive=include_symbol_archive, |
| include_breakpad_symbols=include_breakpad_symbols, |
| enforce_size_limits=enforce_size_limits, |
| upload_results=bool(gcs_bucket), |
| ), |
| test=test_spec, |
| debug_symbol_gcs_bucket=debug_symbol_gcs_bucket, |
| gcs_bucket=gcs_bucket, |
| artifact_gcs_bucket='fuchsia-infra-artifacts', |
| ), |
| ) |
| if run_tests: |
| step_data += test_step_data(deprecated=not test_in_shards) |
| return step_data |
| |
| def test(name, tryjob=False, project='fuchsia', use_snapshot=False): |
| if tryjob: |
| return api.test(name) + try_build( |
| project) + default_gitiles_refs_steps + source_manifest_for_cq( |
| use_snapshot) |
| else: |
| return api.test(name) + ci_build |
| |
| # ParseError. Should result in a failure. |
| yield (test('spec_parse_error') + |
| api.spec.spec_parse_error(step_name='load spec.build_init')) |
| yield test('ci') + spec_data() |
| yield (test('cq', tryjob=True) + spec_data() + |
| api.build_input_resolver.set_gerrit_branch()) |
| |
| yield test('with_uploading') + spec_data(gcs_bucket='fuchsia-build') |
| |
| yield test('profile') + spec_data( |
| variants=['profile'], gcs_bucket='fuchsia-build') |
| |
| yield test('commit_snapshot') + spec_data(use_snapshot=True) |
| |
| yield ( |
| test('patchset_snapshot', |
| tryjob=True, |
| project='garnet', |
| use_snapshot=True) + |
| spec_data(use_snapshot=True) + |
| api.build_input_resolver.set_gerrit_branch()) # yapf: disable |
| |
| yield (test('release-buid_only') + spec_data( |
| build_type='release', run_tests=False, enforce_size_limits=True)) |
| |
| yield (test('with_archives') + |
| spec_data(include_symbol_archive=True, include_breakpad_symbols=True)) |
| |
| yield test('deprecated_test') + spec_data(test_in_shards=False) |
| |
| yield (test('requires_secrets') + |
| spec_data(requires_secrets=True, test_in_shards=False)) |
| |
| yield test('max_shard_size') + spec_data(max_shard_size=200) |