|  | 
|  | 1 | +// Copyright 2013 The Flutter Authors. All rights reserved. | 
|  | 2 | +// Use of this source code is governed by a BSD-style license that can be | 
|  | 3 | +// found in the LICENSE file. | 
|  | 4 | + | 
|  | 5 | +import 'package:yaml/yaml.dart' as y; | 
|  | 6 | + | 
|  | 7 | +// This file contains classes for parsing information about CI configuration | 
|  | 8 | +// from the .ci.yaml file at the root of the flutter/engine repository. | 
|  | 9 | +// The meanings of the sections and fields are documented at: | 
|  | 10 | +// | 
|  | 11 | +// https://github.com/flutter/cocoon/blob/main/CI_YAML.md | 
|  | 12 | +// | 
|  | 13 | +// The classes here don't parse every possible field, but rather only those that | 
|  | 14 | +// are useful for working locally in the engine repo. | 
|  | 15 | + | 
|  | 16 | +const String _targetsField = 'targets'; | 
|  | 17 | +const String _nameField = 'name'; | 
|  | 18 | +const String _recipeField = 'recipe'; | 
|  | 19 | +const String _propertiesField = 'properties'; | 
|  | 20 | +const String _configNameField = 'config_name'; | 
|  | 21 | + | 
|  | 22 | +/// A class containing the information deserialized from the .ci.yaml file. | 
|  | 23 | +/// | 
|  | 24 | +/// The file contains three sections. "enabled_branches", "platform_properties", | 
|  | 25 | +/// and "targets". The "enabled_branches" section is not meaningful when working | 
|  | 26 | +/// locally. The configurations listed in the "targets" section inherit | 
|  | 27 | +/// properties listed in the "platform_properties" section depending on their | 
|  | 28 | +/// names. The configurations listed in the "targets" section are the names, | 
|  | 29 | +/// recipes, build configs, etc. of the builders in CI. | 
|  | 30 | +class CiConfig { | 
|  | 31 | +  /// Builds a [CiConfig] instance from parsed yaml data. | 
|  | 32 | +  /// | 
|  | 33 | +  /// If the yaml was malformed, then `CiConfig.valid` will be false, and | 
|  | 34 | +  /// `CiConfig.error` will be populated with an informative error message. | 
|  | 35 | +  /// Otherwise, `CiConfig.ciTargets` will contain a mapping from target name | 
|  | 36 | +  /// to [CiTarget] instance. | 
|  | 37 | +  factory CiConfig.fromYaml(y.YamlNode yaml) { | 
|  | 38 | +    if (yaml is! y.YamlMap) { | 
|  | 39 | +      final String error = yaml.span.message('Expected a map'); | 
|  | 40 | +      return CiConfig._error(error); | 
|  | 41 | +    } | 
|  | 42 | +    final y.YamlMap ymap = yaml; | 
|  | 43 | +    final y.YamlNode? targetsNode = ymap.nodes[_targetsField]; | 
|  | 44 | +    if (targetsNode == null) { | 
|  | 45 | +      final String error = ymap.span.message('Expected a "$_targetsField" key'); | 
|  | 46 | +      return CiConfig._error(error); | 
|  | 47 | +    } | 
|  | 48 | +    if (targetsNode is! y.YamlList) { | 
|  | 49 | +      final String error = targetsNode.span.message( | 
|  | 50 | +        'Expected "$_targetsField" to be a list.', | 
|  | 51 | +      ); | 
|  | 52 | +      return CiConfig._error(error); | 
|  | 53 | +    } | 
|  | 54 | +    final y.YamlList targetsList = targetsNode; | 
|  | 55 | + | 
|  | 56 | +    final Map<String, CiTarget> result = <String, CiTarget>{}; | 
|  | 57 | +    for (final y.YamlNode yamlTarget in targetsList.nodes) { | 
|  | 58 | +      final CiTarget target = CiTarget.fromYaml(yamlTarget); | 
|  | 59 | +      if (!target.valid) { | 
|  | 60 | +        return CiConfig._error(target.error); | 
|  | 61 | +      } | 
|  | 62 | +      result[target.name] = target; | 
|  | 63 | +    } | 
|  | 64 | + | 
|  | 65 | +    return CiConfig._(ciTargets: result); | 
|  | 66 | +  } | 
|  | 67 | + | 
|  | 68 | +  CiConfig._({ | 
|  | 69 | +    required this.ciTargets, | 
|  | 70 | +  }) : error = null; | 
|  | 71 | + | 
|  | 72 | +  CiConfig._error( | 
|  | 73 | +    this.error, | 
|  | 74 | +  ) : ciTargets = <String, CiTarget>{}; | 
|  | 75 | + | 
|  | 76 | +  /// Information about CI builder configurations, which .ci.yaml calls | 
|  | 77 | +  /// "targets". | 
|  | 78 | +  final Map<String, CiTarget> ciTargets; | 
|  | 79 | + | 
|  | 80 | +  /// An error message when this instance is invalid. | 
|  | 81 | +  final String? error; | 
|  | 82 | + | 
|  | 83 | +  /// Whether this is a valid instance. | 
|  | 84 | +  late final bool valid = error == null; | 
|  | 85 | +} | 
|  | 86 | + | 
|  | 87 | +/// Information about the configuration of a builder on CI, which .ci.yaml | 
|  | 88 | +/// calls a "target". | 
|  | 89 | +class CiTarget { | 
|  | 90 | +  /// Builds a [CiTarget] from parsed yaml data. | 
|  | 91 | +  /// | 
|  | 92 | +  /// If the yaml was malformed then `CiTarget.valid` is false and | 
|  | 93 | +  /// `CiTarget.error` contains a useful error message. Otherwise, the other | 
|  | 94 | +  /// fields contain information about the target. | 
|  | 95 | +  factory CiTarget.fromYaml(y.YamlNode yaml) { | 
|  | 96 | +    if (yaml is! y.YamlMap) { | 
|  | 97 | +      final String error = yaml.span.message('Expected a map.'); | 
|  | 98 | +      return CiTarget._error(error); | 
|  | 99 | +    } | 
|  | 100 | +    final y.YamlMap targetMap = yaml; | 
|  | 101 | +    final String? name = _stringOfNode(targetMap.nodes[_nameField]); | 
|  | 102 | +    if (name == null) { | 
|  | 103 | +      final String error = targetMap.span.message( | 
|  | 104 | +        'Expected map to contain a string value for key "$_nameField".', | 
|  | 105 | +      ); | 
|  | 106 | +      return CiTarget._error(error); | 
|  | 107 | +    } | 
|  | 108 | + | 
|  | 109 | +    final String? recipe = _stringOfNode(targetMap.nodes[_recipeField]); | 
|  | 110 | +    if (recipe == null) { | 
|  | 111 | +      final String error = targetMap.span.message( | 
|  | 112 | +        'Expected map to contain a string value for key "$_recipeField".', | 
|  | 113 | +      ); | 
|  | 114 | +      return CiTarget._error(error); | 
|  | 115 | +    } | 
|  | 116 | + | 
|  | 117 | +    final y.YamlNode? propertiesNode = targetMap.nodes[_propertiesField]; | 
|  | 118 | +    if (propertiesNode == null) { | 
|  | 119 | +      final String error = targetMap.span.message( | 
|  | 120 | +        'Expected map to contain a string value for key "$_propertiesField".', | 
|  | 121 | +      ); | 
|  | 122 | +      return CiTarget._error(error); | 
|  | 123 | +    } | 
|  | 124 | +    final CiTargetProperties properties = CiTargetProperties.fromYaml( | 
|  | 125 | +      propertiesNode, | 
|  | 126 | +    ); | 
|  | 127 | +    if (!properties.valid) { | 
|  | 128 | +      return CiTarget._error(properties.error); | 
|  | 129 | +    } | 
|  | 130 | + | 
|  | 131 | +    return CiTarget._( | 
|  | 132 | +      name: name, | 
|  | 133 | +      recipe: recipe, | 
|  | 134 | +      properties: properties, | 
|  | 135 | +    ); | 
|  | 136 | +  } | 
|  | 137 | + | 
|  | 138 | +  CiTarget._({ | 
|  | 139 | +    required this.name, | 
|  | 140 | +    required this.recipe, | 
|  | 141 | +    required this.properties, | 
|  | 142 | +  }) : error = null; | 
|  | 143 | + | 
|  | 144 | +  CiTarget._error( | 
|  | 145 | +    this.error, | 
|  | 146 | +  ) : name = '', | 
|  | 147 | +      recipe = '', | 
|  | 148 | +      properties = CiTargetProperties._error('Invalid'); | 
|  | 149 | + | 
|  | 150 | +  /// The name of the builder in CI. | 
|  | 151 | +  final String name; | 
|  | 152 | + | 
|  | 153 | +  /// The CI recipe used to run the build. | 
|  | 154 | +  final String recipe; | 
|  | 155 | + | 
|  | 156 | +  /// The properties of the build or builder. | 
|  | 157 | +  final CiTargetProperties properties; | 
|  | 158 | + | 
|  | 159 | +  /// An error message when this instance is invalid. | 
|  | 160 | +  final String? error; | 
|  | 161 | + | 
|  | 162 | +  /// Whether this is a valid instance. | 
|  | 163 | +  late final bool valid = error == null; | 
|  | 164 | +} | 
|  | 165 | + | 
|  | 166 | +/// Various properties of a [CiTarget]. | 
|  | 167 | +class CiTargetProperties { | 
|  | 168 | +  /// Builds a [CiTargetProperties] instance from parsed yaml data. | 
|  | 169 | +  /// | 
|  | 170 | +  /// If the yaml was malformed then `CiTargetProperties.valid` is false and | 
|  | 171 | +  /// `CiTargetProperties.error` contains a useful error message. Otherwise, the | 
|  | 172 | +  /// other fields contain information about the target properties. | 
|  | 173 | +  factory CiTargetProperties.fromYaml(y.YamlNode yaml) { | 
|  | 174 | +    if (yaml is! y.YamlMap) { | 
|  | 175 | +      final String error = yaml.span.message( | 
|  | 176 | +        'Expected "$_propertiesField" to be a map.', | 
|  | 177 | +      ); | 
|  | 178 | +      return CiTargetProperties._error(error); | 
|  | 179 | +    } | 
|  | 180 | +    final y.YamlMap propertiesMap = yaml; | 
|  | 181 | +    final String? configName = _stringOfNode( | 
|  | 182 | +      propertiesMap.nodes[_configNameField], | 
|  | 183 | +    ); | 
|  | 184 | +    return CiTargetProperties._( | 
|  | 185 | +      configName: configName ?? '', | 
|  | 186 | +    ); | 
|  | 187 | +  } | 
|  | 188 | + | 
|  | 189 | +  CiTargetProperties._({ | 
|  | 190 | +    required this.configName, | 
|  | 191 | +  }) : error = null; | 
|  | 192 | + | 
|  | 193 | +  CiTargetProperties._error( | 
|  | 194 | +    this.error, | 
|  | 195 | +  ) : configName = ''; | 
|  | 196 | + | 
|  | 197 | +  /// The name of the build configuration. If the containing [CiTarget] instance | 
|  | 198 | +  /// is using the engine_v2 recipes, then this name is the same as the name | 
|  | 199 | +  /// of the build config json file under ci/builders. | 
|  | 200 | +  final String configName; | 
|  | 201 | + | 
|  | 202 | +  /// An error message when this instance is invalid. | 
|  | 203 | +  final String? error; | 
|  | 204 | + | 
|  | 205 | +  /// Whether this is a valid instance. | 
|  | 206 | +  late final bool valid = error == null; | 
|  | 207 | +} | 
|  | 208 | + | 
|  | 209 | +String? _stringOfNode(y.YamlNode? stringNode) { | 
|  | 210 | +  if (stringNode == null) { | 
|  | 211 | +    return null; | 
|  | 212 | +  } | 
|  | 213 | +  if (stringNode is! y.YamlScalar) { | 
|  | 214 | +    return null; | 
|  | 215 | +  } | 
|  | 216 | +  final y.YamlScalar stringScalar = stringNode; | 
|  | 217 | +  if (stringScalar.value is! String) { | 
|  | 218 | +    return null; | 
|  | 219 | +  } | 
|  | 220 | +  return stringScalar.value as String; | 
|  | 221 | +} | 
0 commit comments