blob: 3a70770ea41fdeb6bc2cbae16df2fe36ad5c91f3 [file] [log] [blame]
// 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:cocoon_service/src/service/scheduler/policy.dart';
import 'package:github/github.dart';
import '../luci/buildbucket.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
class Target {
Target({
required this.value,
required this.schedulerConfig,
required this.slug,
});
/// 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';
/// 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. [schedulerConfig.platformDimensions]
List<RequestedDimension> getDimensions() {
final Map<String, RequestedDimension> dimensionsMap = <String, RequestedDimension>{};
final Map<String, Object> platformDimensions = _getPlatformDimensions();
for (String key in platformDimensions.keys) {
String value = platformDimensions[key].toString();
dimensionsMap[key] = RequestedDimension(key: key, value: value);
}
final Map<String, Object> properties = getProperties();
// TODO(xilaizhang): https://github.com/flutter/flutter/issues/103557
// remove this logic after dimensions are supported in ci.yaml files
for (String dimension in dimensionList) {
if (properties.containsKey(dimension)) {
String value = properties[dimension].toString();
dimensionsMap[dimension] = RequestedDimension(key: dimension, value: value);
}
}
final Map<String, Object> targetDimensions = _getTargetDimensions();
for (String key in targetDimensions.keys) {
String 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 run with [BatchPolicy] to reduce queue time.
SchedulerPolicy get schedulerPolicy {
if (value.scheduler != pb.SchedulerSystem.cocoon) {
return OmitPolicy();
}
return BatchPolicy();
}
/// 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 Map<String, Object> platformProperties = _getPlatformProperties();
final Map<String, Object> properties = _getTargetProperties();
final Map<String, Object> mergedProperties = <String, Object>{}
..addAll(platformProperties)
..addAll(properties);
final List<Dependency> targetDependencies = <Dependency>[];
if (properties.containsKey('dependencies')) {
final List<dynamic> rawDeps = properties['dependencies'] as List<dynamic>;
final Iterable<Dependency> deps = rawDeps.map((dynamic rawDep) => Dependency.fromJson(rawDep as Object));
targetDependencies.addAll(deps);
}
final List<Dependency> platformDependencies = <Dependency>[];
if (platformProperties.containsKey('dependencies')) {
final List<dynamic> rawDeps = platformProperties['dependencies'] as List<dynamic>;
final Iterable<Dependency> deps = rawDeps.map((dynamic rawDep) => Dependency.fromJson(rawDep as Object));
platformDependencies.addAll(deps);
}
// Lookup map to make merging [targetDependencies] and [platformDependencies] simpler.
final Map<String, Dependency> mergedDependencies = <String, Dependency>{};
for (Dependency dep in platformDependencies) {
mergedDependencies[dep.name] = dep;
}
for (Dependency dep in targetDependencies) {
mergedDependencies[dep.name] = dep;
}
mergedProperties['dependencies'] = mergedDependencies.values.map((Dependency dep) => dep.toJson()).toList();
// xcode is a special property as there's different download policies if its in the devicelab.
if (mergedProperties.containsKey('xcode')) {
final Object xcodeVersion = <String, Object>{
'sdk_version': mergedProperties['xcode']!,
};
if (iosPlatforms.contains(getPlatform())) {
mergedProperties['\$flutter/devicelab_osx_sdk'] = xcodeVersion;
} else {
mergedProperties['\$flutter/osx_sdk'] = xcodeVersion;
}
}
// runtime_versions is a special property
if (mergedProperties.containsKey('runtime_versions')) {
// add to existing property, or create new one
final Object osxSdk = mergedProperties['\$flutter/osx_sdk'] ?? <String, Object>{};
(osxSdk as Map)['runtime_versions'] = mergedProperties['runtime_versions']!;
mergedProperties['\$flutter/osx_sdk'] = osxSdk;
}
mergedProperties['bringup'] = value.bringup;
return mergedProperties;
}
Map<String, Object> _getTargetDimensions() {
final Map<String, Object> dimensions = <String, Object>{};
for (String key in value.dimensions.keys) {
dimensions[key] = _parseProperty(key, value.dimensions[key]!);
}
return dimensions;
}
Map<String, Object> _getTargetProperties() {
final Map<String, Object> properties = <String, Object>{};
for (String 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 Map<String, String> platformProperties = schedulerConfig.platformProperties[getPlatform()]!.properties;
final Map<String, Object> properties = <String, Object>{};
for (String 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 Map<String, String> platformDimensions = schedulerConfig.platformProperties[getPlatform()]!.dimensions;
final Map<String, Object> dimensions = <String, Object>{};
for (String 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 List<String> newLineIssues = <String>['android_sdk_license', 'android_sdk_preview_license'];
if (value == 'true') {
return true;
} else if (value == 'false') {
return false;
} else if (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);
}
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();
}
/// Indicates whether this target should be scheduled via batches.
///
/// DeviceLab targets are special as they run on a host + physical device, and there is limited
/// capacity in the labs to run them. Their platforms contain one of `android`, `ios`, and `samsung`.
///
/// Mac host only targets are scheduled via patches due to high queue time. This can be relieved
/// when we have capacity support in Q4/2022.
bool get shouldBatchSchedule {
final String platform = getPlatform();
return platform.contains('android') ||
platform.contains('ios') ||
platform.contains('samsung') ||
platform == 'mac';
}
/// 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 Map<String, Object> properties = getProperties();
if (properties.containsKey(kIgnoreFlakiness)) {
return properties[kIgnoreFlakiness] as bool;
}
return false;
}
}
/// 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<String, dynamic> 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,
if (version != null) 'version': version!,
};
}
}