| # Copyright 2019 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 isolating build artifacts.""" |
| |
| from google.protobuf import text_format |
| |
| from PB.infra.fuchsia import Fuchsia |
| from recipe_engine.recipe_api import Property |
| |
| TEST_MULTIPLIER_KEY = 'MULTIPLY' |
| |
| # These represent the location of the isolated hash in the output of this |
| # recipe when building SDK archives. Must be kept in sync with sdk.py. |
| ISOLATE_STEP_NAME = 'isolate artifacts' |
| ISOLATED_OUTPUT_KEY = 'isolated_output_hash' |
| |
| DEPS = [ |
| 'fuchsia/artifacts', |
| 'fuchsia/build', |
| 'fuchsia/buildbucket_util', |
| 'fuchsia/build_input_resolver', |
| 'fuchsia/checkout', |
| 'fuchsia/git', |
| 'fuchsia/gitiles', |
| 'fuchsia/jiri', |
| 'fuchsia/jsonutil', |
| 'fuchsia/spec', |
| 'fuchsia/testing_requests', |
| 'fuchsia/testsharder', |
| 'fuchsia/upload_debug_symbols', |
| 'recipe_engine/buildbucket', |
| 'recipe_engine/cipd', |
| 'recipe_engine/context', |
| 'recipe_engine/file', |
| 'recipe_engine/isolated', |
| 'recipe_engine/json', |
| 'recipe_engine/path', |
| 'recipe_engine/properties', |
| 'recipe_engine/python', |
| 'recipe_engine/step', |
| ] |
| |
| PROPERTIES = { |
| 'parent_id': |
| Property( |
| # This is a string because led IDs are not integers. |
| kind=str, |
| help='Parent build\'s buildbucket or led id', |
| default=None), |
| 'spec_remote': |
| Property( |
| kind=str, |
| help='URL of the specs git repository', |
| default='https://fuchsia.googlesource.com/integration'), |
| 'spec_revision': |
| Property( |
| kind=str, |
| help='The revision of spec_remote to fetch', |
| default='HEAD'), |
| } |
| |
| |
| def RunSteps(api, parent_id, spec_remote, spec_revision): |
| # 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') |
| commit_remote = 'https://%s/%s' % (bb_build.input.gitiles_commit.host, |
| bb_build.input.gitiles_commit.project) |
| if commit_remote == spec_remote: |
| # If there was no parent build, then spec_revision may not have been |
| # resolved, so use the revision that we resolved immediately above. |
| if spec_revision == 'HEAD': |
| spec_revision = bb_build.input.gitiles_commit.id |
| # Otherwise the spec_revision was specified by the parent, and we want all |
| # accesses to that remote to use that revision. In this case, |
| # build_input_resolver.resolve() is unnecessary since we overwrite the |
| # revision, but we only use it to pre-populate the gitiles_commit of the |
| # build input and to keep the conditional logic simpler. |
| else: |
| bb_build.input.gitiles_commit.id = spec_revision |
| |
| with api.step.nest('got_revision') as presentation: |
| # Read by the binary-size Gerrit plugin |
| presentation.properties['got_revision'] = bb_build.input.gitiles_commit.id |
| |
| with api.step.nest('load spec') as presentation: |
| presentation.step_text = 'loading spec' |
| try: |
| spec = api.spec.load_from_build( |
| build=bb_build, |
| spec_remote=spec_remote, |
| Type=Fuchsia, |
| spec_revision=spec_revision, |
| ) |
| except api.spec.ParseError as e: |
| raise api.step.StepFailure('failed to parse spec: %s' % str(e)) |
| |
| presentation.logs['textproto'] = text_format.MessageToString(spec).split( |
| '\n') |
| # The artifacts tool relies on this output property. |
| # This design has been fragile. Please don't add more dependencies on any |
| # additional output properties. |
| presentation.properties['gcs_bucket'] = spec.gcs_bucket |
| |
| checkout = api.checkout.from_spec(spec.checkout) |
| |
| 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, namespace=parent_id) |
| |
| # We can only derive repo from buildbucket when triggered in CI or CQ; |
| # validate the checkout then. |
| bb_input = api.buildbucket.build_input |
| if bb_input.gerrit_changes or bb_input.gitiles_commit.project: |
| 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], |
| ) |
| |
| collect_build_metrics = False |
| if spec.build.upload_results: |
| assert spec.gcs_bucket, ( |
| 'gcs_bucket must be set if checkout.upload_results is') |
| collect_build_metrics = True |
| # Build infratools. |
| spec.build.universe_packages.append('//bundles:infratools') |
| spec.build.ninja_targets.append('bundles:infratools') |
| # If SDK subbuild, set SDK ID to parent ID. |
| sdk_id = parent_id if spec.build.sdk_subbuild else None |
| build = api.build.from_spec( |
| spec.build, |
| checkout, |
| collect_build_metrics, |
| sdk_id=sdk_id, |
| 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 |
| |
| # In SDK subbuild mode, isolate SDK archive and ninja targets. |
| if spec.build.sdk_subbuild: |
| sdk_archive_path = build.fuchsia_build_dir.join('sdk', 'archive') |
| isolated = api.isolated.isolated(sdk_archive_path) |
| sdk_ninja_targets = [ |
| target for target in spec.build.ninja_targets |
| if target.startswith('sdk/archive') |
| ] |
| for ninja_target in sdk_ninja_targets: |
| isolated.add_file( |
| path=build.fuchsia_build_dir.join(*ninja_target.split('/'))) |
| sdk_archive_isolated_hash = isolated.archive(ISOLATE_STEP_NAME) |
| api.step.active_result.presentation.properties[ |
| ISOLATED_OUTPUT_KEY] = sdk_archive_isolated_hash |
| |
| 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, |
| namespace=parent_id, |
| ) |
| |
| # TODO(garymm): assert spec.gcs_bucket set if upload_results set. |
| if spec.gcs_bucket and spec.build.upload_results: |
| build.upload_results( |
| gcs_bucket=spec.gcs_bucket, |
| namespace=parent_id, |
| ) |
| |
| # 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) |
| |
| if spec.build.enforce_size_limits: |
| build.check_filesystem_sizes() |
| |
| # Run the testsharder to collect test specifications and shard them. |
| # Always create shards regardless test_in_shards in true or not. |
| 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, |
| ) |
| |
| # Must be set before testing.shard_requests() is called. |
| api.artifacts.gcs_bucket = spec.artifact_gcs_bucket |
| api.artifacts.uuid = parent_id or api.buildbucket_util.id |
| |
| if spec.build.run_tests: |
| if parent_id.isdigit(): |
| # Use parent build so that testing task requests refer to |
| # that build, which actually orchestrates testing. |
| buildbucket_build = api.buildbucket.get(int(parent_id)) |
| else: |
| # When the parent was launched by led, it's not possible to retrieve |
| # the parent build, so we fall back to using our own build. |
| # This is technically incorrect and any tests that rely on having |
| # correct buildbucket metadata may fail when run via led. Ideally |
| # we wouldn't have any tests that knew about buildbucket, but |
| # for now this is OK since none of those tests run in recipes CQ, |
| # which uses led to test recipes changes. |
| buildbucket_build = bb_build |
| |
| if spec.test.test_in_shards: |
| shard_requests = api.testing_requests.shard_requests( |
| build, |
| buildbucket_build, |
| spec.test.per_test_timeout_secs, |
| spec.test.pool, |
| shards, |
| spec.test.swarming_expiration_timeout_secs, |
| spec.test.swarming_io_timeout_secs, |
| spec.test.use_runtests, |
| default_service_account=spec.test.default_service_account, |
| pave_from_gcs=spec.test.pave_from_gcs, |
| timeout_secs=spec.test.timeout_secs) |
| else: |
| shard_requests = api.testing_requests.deprecated_shard_requests( |
| build, |
| api.testing_requests.deprecated_test_cmds(spec.test), |
| spec.test.device_type, |
| spec.test.pool, |
| spec.test.timeout_secs, |
| spec.test.pave, |
| swarming_expiration_timeout_secs=spec.test |
| .swarming_expiration_timeout_secs, |
| swarming_io_timeout_secs=spec.test.swarming_io_timeout_secs, |
| default_service_account=spec.test.default_service_account, |
| pave_from_gcs=spec.test.pave_from_gcs, |
| ) |
| |
| orchestration_inputs = api.build.TestOrchestrationInputs.from_build_results( |
| build, shard_requests) |
| orchestration_inputs_hash = orchestration_inputs.isolate(api) |
| step_result = api.step('logging orchestration_inputs_hash', cmd=None) |
| step_result.presentation.properties[ |
| orchestration_inputs.HASH_PROPERTY] = orchestration_inputs_hash |
| |
| # 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) |
| |
| |
| def GenTests(api): |
| |
| def spec_data(use_snapshot=False, |
| build_type='debug', |
| ninja_targets=(), |
| sdk_subbuild=False, |
| variants=(), |
| device_type='QEMU', |
| enforce_size_limits=False, |
| run_tests=True, |
| test_in_shards=True, |
| gcs_bucket=None, |
| debug_symbol_gcs_bucket='debug-symbols'): |
| test_spec = None |
| if run_tests: |
| test_spec = Fuchsia.Test( |
| device_type=device_type, |
| max_shard_size=0, |
| timeout_secs=30 * 60, |
| pool='fuchsia.tests', |
| test_in_shards=test_in_shards, |
| swarming_expiration_timeout_secs=10 * 60, |
| swarming_io_timeout_secs=5 * 60, |
| default_service_account='service_account', |
| pave_from_gcs=True, |
| ) |
| |
| spec = Fuchsia( |
| checkout=Fuchsia.Checkout( |
| manifest='minimal', |
| project='integration', |
| remote='https://fuchsia.googlesource.com/manifest', |
| upload_results=bool(gcs_bucket), |
| use_snapshot=use_snapshot, |
| ), |
| build=Fuchsia.Build( |
| variants=variants, |
| build_type=build_type, |
| run_tests=run_tests, |
| ninja_targets=ninja_targets, |
| sdk_subbuild=sdk_subbuild, |
| board='boards/x64.gni', |
| product='products/core.gni', |
| target='x64', |
| include_symbol_archive=False, |
| include_breakpad_symbols=False, |
| 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', |
| ) |
| return api.spec.spec_loaded_ok( |
| step_name='load spec.build_init', message=spec) |
| |
| default_gitiles_refs_steps = api.gitiles.refs('refs', [ |
| 'refs/heads/master', |
| 'deadbeef', |
| ]) |
| |
| spec_remote = 'https://fuchsia.googlesource.com/integration' |
| |
| properties = { |
| # We rely on the buildbucket test API using this same |
| # ID for ci_build_message and the builds returned by get(). |
| 'parent_id': str(api.buildbucket.ci_build_message().id), |
| 'spec_remote': spec_remote, |
| } |
| |
| #yapf: disable |
| yield ( |
| api.checkout.test('default', tryjob=False) + |
| api.build.test('default') + |
| spec_data(gcs_bucket='fuchsia-infra', run_tests=True) + |
| api.properties(**properties) |
| ) |
| |
| yield ( |
| api.checkout.test('non-numeric-parent-id', tryjob=False) + |
| api.build.test('default') + |
| spec_data(gcs_bucket='fuchsia-infra', run_tests=True) + |
| api.properties(parent_id='not-a-number') |
| ) |
| |
| yield ( |
| api.checkout.test('default_cq', tryjob=True) + |
| api.build.test('default_cq', tryjob=True) + |
| spec_data(run_tests=True) + |
| api.buildbucket.try_build( |
| # Values chosen to match the test data in jiri/test_api.py |
| # example_source_manifest() so that we trigger the test multipliers |
| # code path. |
| project='manifest', |
| git_repo='https://fuchsia.googlesource.com/manifest') + |
| api.build_input_resolver.set_gerrit_branch('master') + |
| default_gitiles_refs_steps + |
| api.properties(**properties) |
| ) |
| |
| yield ( |
| api.checkout.test('spec_remote_cq', tryjob=True) + |
| api.build.test('spec_remote_cq', tryjob=True) + |
| spec_data(run_tests=True) + |
| api.buildbucket.try_build(git_repo=spec_remote) + |
| api.build_input_resolver.set_gerrit_branch('master') + |
| default_gitiles_refs_steps + |
| api.properties(**properties) |
| ) |
| |
| yield ( |
| api.checkout.test('spec_remote_cq_with_spec_revision', tryjob=True) + |
| api.build.test('spec_remote_cq', tryjob=True) + |
| spec_data(run_tests=True) + |
| api.buildbucket.try_build(git_repo=spec_remote) + |
| api.build_input_resolver.set_gerrit_branch('master') + |
| default_gitiles_refs_steps + |
| api.properties(spec_revision='deadbeef2', **properties) |
| ) |
| |
| yield ( |
| api.checkout.test('build_type_release_not_run_tests', tryjob=False) + |
| api.build.test('default') + |
| spec_data(build_type='release', gcs_bucket='fuchsia-infra', |
| run_tests=False, enforce_size_limits=True) + |
| api.properties(**properties) |
| ) |
| |
| yield (api.checkout.test('spec_parse_error', tryjob=False, status='failure') + |
| api.spec.spec_parse_error(step_name='load spec.build_init')) |
| # yapf: enable |
| |
| yield (api.checkout.test('sdk', tryjob=False) + |
| api.build.test('sdk', tryjob=False) + spec_data( |
| run_tests=False, |
| ninja_targets=[ |
| 'sdk/archive/core.tar.gz', 'sdk/archive/fuchsia_dart.tar.gz' |
| ], |
| sdk_subbuild=True) + api.properties(parent_id='sdk-id')) |
| |
| yield (api.checkout.test('not_test_in_shards') + api.build.test('') + |
| spec_data(run_tests=True, test_in_shards=False) + |
| api.properties(**properties)) |