blob: 94a2e36ee055ef3c1d6e3f1e876813e8c9482367 [file] [log] [blame]
// Copyright 2013 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 'package:yaml/yaml.dart' as y;
// This file contains classes for parsing information about CI configuration
// from the .ci.yaml file at the root of the flutter/engine repository.
// The meanings of the sections and fields are documented at:
//
// https://github.com/flutter/cocoon/blob/main/CI_YAML.md
//
// The classes here don't parse every possible field, but rather only those that
// are useful for working locally in the engine repo.
const String _targetsField = 'targets';
const String _nameField = 'name';
const String _recipeField = 'recipe';
const String _propertiesField = 'properties';
const String _configNameField = 'config_name';
/// A class containing the information deserialized from the .ci.yaml file.
///
/// The file contains three sections. "enabled_branches", "platform_properties",
/// and "targets". The "enabled_branches" section is not meaningful when working
/// locally. The configurations listed in the "targets" section inherit
/// properties listed in the "platform_properties" section depending on their
/// names. The configurations listed in the "targets" section are the names,
/// recipes, build configs, etc. of the builders in CI.
class CiConfig {
/// Builds a [CiConfig] instance from parsed yaml data.
///
/// If the yaml was malformed, then `CiConfig.valid` will be false, and
/// `CiConfig.error` will be populated with an informative error message.
/// Otherwise, `CiConfig.ciTargets` will contain a mapping from target name
/// to [CiTarget] instance.
factory CiConfig.fromYaml(y.YamlNode yaml) {
if (yaml is! y.YamlMap) {
final String error = yaml.span.message('Expected a map');
return CiConfig._error(error);
}
final y.YamlMap ymap = yaml;
final y.YamlNode? targetsNode = ymap.nodes[_targetsField];
if (targetsNode == null) {
final String error = ymap.span.message('Expected a "$_targetsField" key');
return CiConfig._error(error);
}
if (targetsNode is! y.YamlList) {
final String error = targetsNode.span.message(
'Expected "$_targetsField" to be a list.',
);
return CiConfig._error(error);
}
final y.YamlList targetsList = targetsNode;
final Map<String, CiTarget> result = <String, CiTarget>{};
for (final y.YamlNode yamlTarget in targetsList.nodes) {
final CiTarget target = CiTarget.fromYaml(yamlTarget);
if (!target.valid) {
return CiConfig._error(target.error);
}
result[target.name] = target;
}
return CiConfig._(ciTargets: result);
}
CiConfig._({
required this.ciTargets,
}) : error = null;
CiConfig._error(
this.error,
) : ciTargets = <String, CiTarget>{};
/// Information about CI builder configurations, which .ci.yaml calls
/// "targets".
final Map<String, CiTarget> ciTargets;
/// An error message when this instance is invalid.
final String? error;
/// Whether this is a valid instance.
late final bool valid = error == null;
}
/// Information about the configuration of a builder on CI, which .ci.yaml
/// calls a "target".
class CiTarget {
/// Builds a [CiTarget] from parsed yaml data.
///
/// If the yaml was malformed then `CiTarget.valid` is false and
/// `CiTarget.error` contains a useful error message. Otherwise, the other
/// fields contain information about the target.
factory CiTarget.fromYaml(y.YamlNode yaml) {
if (yaml is! y.YamlMap) {
final String error = yaml.span.message('Expected a map.');
return CiTarget._error(error);
}
final y.YamlMap targetMap = yaml;
final String? name = _stringOfNode(targetMap.nodes[_nameField]);
if (name == null) {
final String error = targetMap.span.message(
'Expected map to contain a string value for key "$_nameField".',
);
return CiTarget._error(error);
}
final String? recipe = _stringOfNode(targetMap.nodes[_recipeField]);
if (recipe == null) {
final String error = targetMap.span.message(
'Expected map to contain a string value for key "$_recipeField".',
);
return CiTarget._error(error);
}
final y.YamlNode? propertiesNode = targetMap.nodes[_propertiesField];
if (propertiesNode == null) {
final String error = targetMap.span.message(
'Expected map to contain a string value for key "$_propertiesField".',
);
return CiTarget._error(error);
}
final CiTargetProperties properties = CiTargetProperties.fromYaml(
propertiesNode,
);
if (!properties.valid) {
return CiTarget._error(properties.error);
}
return CiTarget._(
name: name,
recipe: recipe,
properties: properties,
);
}
CiTarget._({
required this.name,
required this.recipe,
required this.properties,
}) : error = null;
CiTarget._error(
this.error,
) : name = '',
recipe = '',
properties = CiTargetProperties._error('Invalid');
/// The name of the builder in CI.
final String name;
/// The CI recipe used to run the build.
final String recipe;
/// The properties of the build or builder.
final CiTargetProperties properties;
/// An error message when this instance is invalid.
final String? error;
/// Whether this is a valid instance.
late final bool valid = error == null;
}
/// Various properties of a [CiTarget].
class CiTargetProperties {
/// Builds a [CiTargetProperties] instance from parsed yaml data.
///
/// If the yaml was malformed then `CiTargetProperties.valid` is false and
/// `CiTargetProperties.error` contains a useful error message. Otherwise, the
/// other fields contain information about the target properties.
factory CiTargetProperties.fromYaml(y.YamlNode yaml) {
if (yaml is! y.YamlMap) {
final String error = yaml.span.message(
'Expected "$_propertiesField" to be a map.',
);
return CiTargetProperties._error(error);
}
final y.YamlMap propertiesMap = yaml;
final String? configName = _stringOfNode(
propertiesMap.nodes[_configNameField],
);
return CiTargetProperties._(
configName: configName ?? '',
);
}
CiTargetProperties._({
required this.configName,
}) : error = null;
CiTargetProperties._error(
this.error,
) : configName = '';
/// The name of the build configuration. If the containing [CiTarget] instance
/// is using the engine_v2 recipes, then this name is the same as the name
/// of the build config json file under ci/builders.
final String configName;
/// An error message when this instance is invalid.
final String? error;
/// Whether this is a valid instance.
late final bool valid = error == null;
}
String? _stringOfNode(y.YamlNode? stringNode) {
if (stringNode == null) {
return null;
}
if (stringNode is! y.YamlScalar) {
return null;
}
final y.YamlScalar stringScalar = stringNode;
if (stringScalar.value is! String) {
return null;
}
return stringScalar.value as String;
}