blob: 10c8d2bda06673c17c65c524b5ec2f1768b5a798 [file]
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'package:buildbucket/buildbucket_pb.dart';
import 'package:github/github.dart';
import '../../service/config.dart';
import '../../service/scheduler/policy.dart';
import '../proto/internal/scheduler.pb.dart' as pb;
/// Wrapper class around [pb.Target] to support aggregate properties.
///
/// Changes here may also need to be upstreamed in:
/// * https://flutter.googlesource.com/infra/+/refs/heads/main/config/lib/ci_yaml/ci_yaml.star
final class Target {
Target({
required pb.Target value,
required this.schedulerConfig,
required this.slug,
}) : _value = value;
/// Underlying [Target] this is based on.
final pb.Target _value;
/// The [SchedulerConfig] [_value] is from.
///
/// This is passed for necessary lookups to platform level details.
final pb.SchedulerConfig schedulerConfig;
/// The [RepositorySlug] this [Target] is run for.
final RepositorySlug slug;
/// Target prefixes that indicate it will run on an ios device.
static const List<String> iosPlatforms = <String>['mac_ios', 'mac_arm64_ios'];
/// Dimension list defined in .ci.yaml.
static List<String> dimensionList = <String>[
'os',
'device_os',
'device_type',
'mac_model',
'cores',
'cpu',
];
static String kIgnoreFlakiness = 'ignore_flakiness';
static const String kFlakinessThreshold = 'flakiness_threshold';
/// Gets assembled dimensions for this [pb.Target].
///
/// Swarming dimension doc: https://chromium.googlesource.com/infra/luci/luci-go/+/HEAD/lucicfg/doc/README.md#swarming.dimension
///
/// Target dimensions are prioritized in:
/// 1. [pb.Target.dimensions]
/// 1. [pb.Target.properties]
/// 2. [pb.SchedulerConfig.platformProperties]
List<RequestedDimension> getDimensions() {
final dimensionsMap = <String, RequestedDimension>{};
final platformDimensions = _getPlatformDimensions();
for (var key in platformDimensions.keys) {
final value = platformDimensions[key].toString();
dimensionsMap[key] = RequestedDimension(key: key, value: value);
}
final properties = getProperties();
// TODO(xilaizhang): https://github.com/flutter/flutter/issues/103557
// remove this logic after dimensions are supported in ci.yaml files
for (var dimension in dimensionList) {
if (properties.containsKey(dimension)) {
final value = properties[dimension].toString();
dimensionsMap[dimension] = RequestedDimension(
key: dimension,
value: value,
);
}
}
final targetDimensions = _getTargetDimensions();
for (var key in targetDimensions.keys) {
final value = targetDimensions[key].toString();
dimensionsMap[key] = RequestedDimension(key: key, value: value);
}
return dimensionsMap.values.toList();
}
/// [SchedulerPolicy] this target follows.
///
/// Targets not triggered by Cocoon will not be triggered.
///
/// All targets except from [Config.guaranteedSchedulingRepos] run with [BatchPolicy] to reduce queue time.
SchedulerPolicy get schedulerPolicy {
if (_value.scheduler != pb.SchedulerSystem.cocoon) {
return const OmitPolicy();
}
if (Config.guaranteedSchedulingRepos.contains(slug)) {
return const GuaranteedPolicy();
}
return const BatchPolicy();
}
/// Get the tags from the defined properties in the ci.
///
/// Return an empty list if no tags are found.
List<String> get tags {
final properties = getProperties();
return (properties.containsKey('tags'))
? (properties['tags'] as List).map((e) => e as String).toList()
: [];
}
/// Name of the target.
String get name => _value.name;
/// Timeout in minutes.
int get timeoutInMinutes => _value.timeout;
/// Scheduler system of the target.
pb.SchedulerSystem get scheduler => _value.scheduler;
/// Whether the target runs in presubmit.
bool get presubmit => _value.presubmit;
/// Whether the target runs in postsubmit.
bool get postsubmit => _value.postsubmit;
/// Whether the target should be considered for backfill.
bool get backfill {
return _value.hasBackfill()
? _value.backfill
: schedulerPolicy is BatchPolicy;
}
/// The target's enabled branches.
///
/// This set is unmodifiable.
late final Set<String> enabledBranches = Set.unmodifiable(
_value.enabledBranches,
);
/// The target's `runIf` configuration.
///
/// This list is unmodifiable.
late final List<String> runIf = List.unmodifiable(_value.runIf);
/// Gets the assembled properties for this [pb.Target].
///
/// Target properties are prioritized in:
/// 1. [schedulerConfig.platformProperties]
/// 2. [pb.Target.properties]
Map<String, Object> getProperties() {
final platformProperties = _getPlatformProperties();
final properties = _getTargetProperties();
final mergedProperties = <String, Object>{}
..addAll(platformProperties)
..addAll(properties);
final targetDependencies = <Dependency>[];
if (properties.containsKey('dependencies')) {
final rawDeps = properties['dependencies'] as List<dynamic>;
final deps = rawDeps.map(
(dynamic rawDep) => Dependency.fromJson(rawDep as Object),
);
targetDependencies.addAll(deps);
}
final platformDependencies = <Dependency>[];
if (platformProperties.containsKey('dependencies')) {
final rawDeps = platformProperties['dependencies'] as List<dynamic>;
final deps = rawDeps.map(
(dynamic rawDep) => Dependency.fromJson(rawDep as Object),
);
platformDependencies.addAll(deps);
}
// Lookup map to make merging [targetDependencies] and [platformDependencies] simpler.
final mergedDependencies = <String, Dependency>{};
for (var dep in platformDependencies) {
mergedDependencies[dep.name] = dep;
}
for (var dep in targetDependencies) {
mergedDependencies[dep.name] = dep;
}
mergedProperties['dependencies'] = mergedDependencies.values
.map((Dependency dep) => dep.toJson())
.toList();
mergedProperties['bringup'] = _value.bringup;
// Do not allow configurations to set values of properties that affect the
// location of the sources checked out by the LUCI recipes.
const checkoutProperties = <String>['git_ref', 'git_url'];
for (var property in checkoutProperties) {
mergedProperties.remove(property);
}
return mergedProperties;
}
Map<String, Object> _getTargetDimensions() {
final dimensions = <String, Object>{};
for (var key in _value.dimensions.keys) {
dimensions[key] = _parseProperty(key, _value.dimensions[key]!);
}
return dimensions;
}
Map<String, Object> _getTargetProperties() {
final properties = <String, Object>{'recipe': _value.recipe};
for (var key in _value.properties.keys) {
properties[key] = _parseProperty(key, _value.properties[key]!);
}
return properties;
}
Map<String, Object> _getPlatformProperties() {
if (!schedulerConfig.platformProperties.containsKey(getPlatform())) {
return <String, Object>{};
}
final platformProperties =
schedulerConfig.platformProperties[getPlatform()]!.properties;
final properties = <String, Object>{};
for (var key in platformProperties.keys) {
properties[key] = _parseProperty(key, platformProperties[key]!);
}
return properties;
}
Map<String, Object> _getPlatformDimensions() {
if (!schedulerConfig.platformProperties.containsKey(getPlatform())) {
return <String, Object>{};
}
final platformDimensions =
schedulerConfig.platformProperties[getPlatform()]!.dimensions;
final dimensions = <String, Object>{};
for (var key in platformDimensions.keys) {
dimensions[key] = _parseProperty(key, platformDimensions[key]!);
}
return dimensions;
}
/// Converts property strings to their correct type.
///
/// Changes made here should also be made to [_platform_properties] and [_properties] in:
/// * https://cs.opensource.google/flutter/infra/+/main:config/lib/ci_yaml/ci_yaml.star
Object _parseProperty(String key, String value) {
// Yaml will escape new lines unnecessarily for strings.
final newLineIssues = <String>[
'android_sdk_license',
'android_sdk_preview_license',
];
if (value == 'true') {
return true;
} else if (value == 'false') {
return false;
} else if (value.startsWith('[') || value.startsWith('{')) {
return jsonDecode(value) as Object;
} else if (newLineIssues.contains(key)) {
return value.replaceAll('\\n', '\n');
} else if (int.tryParse(value) != null) {
return int.parse(value);
} else if (double.tryParse(value) != null) {
// double parsing must come after int because it parses ints as well.
// note: Luci (starlark) does not support float/double parsing.
return double.parse(value);
}
return value;
}
/// Get the platform of this [Target].
///
/// Platform is extracted as the first word in a target's name.
String getPlatform() {
return _value.name.split(' ').first.toLowerCase();
}
/// Get the associated LUCI bucket to run this [Target] in.
String getBucket() {
return _value.bringup ? 'staging' : 'prod';
}
/// Returns value of ignore_flakiness property.
///
/// Returns the value of ignore_flakiness property if
/// it has been specified, else default returns false.
bool getIgnoreFlakiness() {
final properties = getProperties();
if (properties.containsKey(kIgnoreFlakiness)) {
return properties[kIgnoreFlakiness] as bool;
}
return false;
}
/// Returns the value of flakiness_threshold property or the [global].
double? get flakinessThreshold {
final properties = getProperties();
if (properties.containsKey(kFlakinessThreshold)) {
return properties[kFlakinessThreshold] as double;
}
return null;
}
/// Whether this target was marked with `properties: release_build: "true"`.
bool get isReleaseBuild {
return getProperties()['release_build'] == true;
}
/// Whether this target was marked with `bringup: true` in `.ci.yaml`.
bool get isBringup => _value.bringup;
@override
String toString() {
return 'Target <$_value>';
}
}
/// Representation of a Flutter dependency.
///
/// See more:
/// * https://flutter.googlesource.com/recipes/+/refs/heads/main/recipe_modules/flutter_deps/api.py
class Dependency {
Dependency(this.name, this.version);
/// Constructor for converting from the flutter_deps format.
factory Dependency.fromJson(Object json) {
final map = json as Map<String, dynamic>;
return Dependency(map['dependency']! as String, map['version'] as String?);
}
/// Human readable name of the dependency.
final String name;
/// CIPD tag to use.
///
/// If null, will use the version set in the flutter_deps recipe_module.
final String? version;
Map<String, Object> toJson() {
return <String, Object>{'dependency': name, 'version': ?version};
}
}