Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 1 | // Copyright (C) 2021 The Android Open Source Project |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | package sdk |
| 16 | |
| 17 | import ( |
| 18 | "fmt" |
Paul Duffin | 42a49f1 | 2022-08-17 22:09:55 +0000 | [diff] [blame] | 19 | "math" |
Paul Duffin | 0c3acbf | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 20 | "reflect" |
Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 21 | "strings" |
| 22 | ) |
| 23 | |
| 24 | // Supports customizing sdk snapshot output based on target build release. |
| 25 | |
| 26 | // buildRelease represents the version of a build system used to create a specific release. |
| 27 | // |
Paul Duffin | 1364891 | 2022-07-15 13:12:35 +0000 | [diff] [blame] | 28 | // The name of the release, is the same as the code for the dessert release, e.g. S, Tiramisu, etc. |
Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 29 | type buildRelease struct { |
Paul Duffin | 1364891 | 2022-07-15 13:12:35 +0000 | [diff] [blame] | 30 | // The name of the release, e.g. S, Tiramisu, etc. |
Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 31 | name string |
| 32 | |
Paul Duffin | 42a49f1 | 2022-08-17 22:09:55 +0000 | [diff] [blame] | 33 | // The index of this structure within the dessertBuildReleases list. |
| 34 | // |
| 35 | // The buildReleaseCurrent does not appear in the dessertBuildReleases list as it has an ordinal value |
| 36 | // that is larger than the size of the dessertBuildReleases. |
Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 37 | ordinal int |
| 38 | } |
| 39 | |
Paul Duffin | 1364891 | 2022-07-15 13:12:35 +0000 | [diff] [blame] | 40 | func (br *buildRelease) EarlierThan(other *buildRelease) bool { |
| 41 | return br.ordinal < other.ordinal |
| 42 | } |
| 43 | |
Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 44 | // String returns the name of the build release. |
Paul Duffin | 1364891 | 2022-07-15 13:12:35 +0000 | [diff] [blame] | 45 | func (br *buildRelease) String() string { |
| 46 | return br.name |
Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 47 | } |
| 48 | |
| 49 | // buildReleaseSet represents a set of buildRelease objects. |
| 50 | type buildReleaseSet struct { |
| 51 | // Set of *buildRelease represented as a map from *buildRelease to struct{}. |
| 52 | contents map[*buildRelease]struct{} |
| 53 | } |
| 54 | |
| 55 | // addItem adds a build release to the set. |
| 56 | func (s *buildReleaseSet) addItem(release *buildRelease) { |
| 57 | s.contents[release] = struct{}{} |
| 58 | } |
| 59 | |
| 60 | // addRange adds all the build releases from start (inclusive) to end (inclusive). |
| 61 | func (s *buildReleaseSet) addRange(start *buildRelease, end *buildRelease) { |
| 62 | for i := start.ordinal; i <= end.ordinal; i += 1 { |
Paul Duffin | 42a49f1 | 2022-08-17 22:09:55 +0000 | [diff] [blame] | 63 | s.addItem(dessertBuildReleases[i]) |
Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 64 | } |
| 65 | } |
| 66 | |
| 67 | // contains returns true if the set contains the specified build release. |
| 68 | func (s *buildReleaseSet) contains(release *buildRelease) bool { |
| 69 | _, ok := s.contents[release] |
| 70 | return ok |
| 71 | } |
| 72 | |
| 73 | // String returns a string representation of the set, sorted from earliest to latest release. |
| 74 | func (s *buildReleaseSet) String() string { |
| 75 | list := []string{} |
Paul Duffin | 42a49f1 | 2022-08-17 22:09:55 +0000 | [diff] [blame] | 76 | addRelease := func(release *buildRelease) { |
Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 77 | if _, ok := s.contents[release]; ok { |
| 78 | list = append(list, release.name) |
| 79 | } |
| 80 | } |
Paul Duffin | 42a49f1 | 2022-08-17 22:09:55 +0000 | [diff] [blame] | 81 | // Add the names of the build releases in this set in the order in which they were created. |
| 82 | for _, release := range dessertBuildReleases { |
| 83 | addRelease(release) |
| 84 | } |
| 85 | // Always add "current" to the list of names last if it is present in the set. |
| 86 | addRelease(buildReleaseCurrent) |
Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 87 | return fmt.Sprintf("[%s]", strings.Join(list, ",")) |
| 88 | } |
| 89 | |
| 90 | var ( |
| 91 | // nameToBuildRelease contains a map from name to build release. |
| 92 | nameToBuildRelease = map[string]*buildRelease{} |
| 93 | |
Paul Duffin | 42a49f1 | 2022-08-17 22:09:55 +0000 | [diff] [blame] | 94 | // dessertBuildReleases lists all the available dessert build releases, i.e. excluding current. |
| 95 | dessertBuildReleases = []*buildRelease{} |
Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 96 | |
| 97 | // allBuildReleaseSet is the set of all build releases. |
| 98 | allBuildReleaseSet = &buildReleaseSet{contents: map[*buildRelease]struct{}{}} |
| 99 | |
Paul Duffin | 42a49f1 | 2022-08-17 22:09:55 +0000 | [diff] [blame] | 100 | // Add the dessert build releases from oldest to newest. |
Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 101 | buildReleaseS = initBuildRelease("S") |
Paul Duffin | e7babdb | 2022-02-10 13:06:54 +0000 | [diff] [blame] | 102 | buildReleaseT = initBuildRelease("Tiramisu") |
Paul Duffin | 03d2cef | 2022-09-29 17:37:49 +0100 | [diff] [blame] | 103 | buildReleaseU = initBuildRelease("UpsideDownCake") |
Paul Duffin | 42a49f1 | 2022-08-17 22:09:55 +0000 | [diff] [blame] | 104 | |
| 105 | // Add the current build release which is always treated as being more recent than any other |
| 106 | // build release, including those added in tests. |
| 107 | buildReleaseCurrent = initBuildRelease("current") |
Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 108 | ) |
| 109 | |
| 110 | // initBuildRelease creates a new build release with the specified name. |
| 111 | func initBuildRelease(name string) *buildRelease { |
Paul Duffin | 42a49f1 | 2022-08-17 22:09:55 +0000 | [diff] [blame] | 112 | ordinal := len(dessertBuildReleases) |
| 113 | if name == "current" { |
| 114 | // The current build release is more recent than all other build releases, including those |
| 115 | // created in tests so use the max int value. It cannot just rely on being created after all |
| 116 | // the other build releases as some are created in tests which run after the current build |
| 117 | // release has been created. |
| 118 | ordinal = math.MaxInt |
| 119 | } |
Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 120 | release := &buildRelease{name: name, ordinal: ordinal} |
| 121 | nameToBuildRelease[name] = release |
Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 122 | allBuildReleaseSet.addItem(release) |
Paul Duffin | 42a49f1 | 2022-08-17 22:09:55 +0000 | [diff] [blame] | 123 | if name != "current" { |
| 124 | // As the current build release has an ordinal value that does not correspond to its position |
| 125 | // in the dessertBuildReleases list do not add it to the list. |
| 126 | dessertBuildReleases = append(dessertBuildReleases, release) |
| 127 | } |
Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 128 | return release |
| 129 | } |
| 130 | |
Paul Duffin | 42a49f1 | 2022-08-17 22:09:55 +0000 | [diff] [blame] | 131 | // latestDessertBuildRelease returns the latest dessert release build name, i.e. the last dessert |
| 132 | // release added to the list, which does not include current. |
| 133 | func latestDessertBuildRelease() *buildRelease { |
| 134 | return dessertBuildReleases[len(dessertBuildReleases)-1] |
Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 135 | } |
| 136 | |
| 137 | // nameToRelease maps from build release name to the corresponding build release (if it exists) or |
| 138 | // the error if it does not. |
| 139 | func nameToRelease(name string) (*buildRelease, error) { |
| 140 | if r, ok := nameToBuildRelease[name]; ok { |
| 141 | return r, nil |
| 142 | } |
| 143 | |
| 144 | return nil, fmt.Errorf("unknown release %q, expected one of %s", name, allBuildReleaseSet) |
| 145 | } |
| 146 | |
| 147 | // parseBuildReleaseSet parses a build release set string specification into a build release set. |
| 148 | // |
| 149 | // The specification consists of one of the following: |
| 150 | // * a single build release name, e.g. S, T, etc. |
| 151 | // * a closed range (inclusive to inclusive), e.g. S-T |
| 152 | // * an open range, e.g. T+. |
| 153 | // |
| 154 | // This returns the set if the specification was valid or an error. |
| 155 | func parseBuildReleaseSet(specification string) (*buildReleaseSet, error) { |
| 156 | set := &buildReleaseSet{contents: map[*buildRelease]struct{}{}} |
| 157 | |
| 158 | if strings.HasSuffix(specification, "+") { |
| 159 | rangeStart := strings.TrimSuffix(specification, "+") |
| 160 | start, err := nameToRelease(rangeStart) |
| 161 | if err != nil { |
| 162 | return nil, err |
| 163 | } |
Paul Duffin | 42a49f1 | 2022-08-17 22:09:55 +0000 | [diff] [blame] | 164 | end := latestDessertBuildRelease() |
Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 165 | set.addRange(start, end) |
Paul Duffin | 42a49f1 | 2022-08-17 22:09:55 +0000 | [diff] [blame] | 166 | // An open-ended range always includes the current release. |
| 167 | set.addItem(buildReleaseCurrent) |
Paul Duffin | 1812294 | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 168 | } else if strings.Contains(specification, "-") { |
| 169 | limits := strings.SplitN(specification, "-", 2) |
| 170 | start, err := nameToRelease(limits[0]) |
| 171 | if err != nil { |
| 172 | return nil, err |
| 173 | } |
| 174 | |
| 175 | end, err := nameToRelease(limits[1]) |
| 176 | if err != nil { |
| 177 | return nil, err |
| 178 | } |
| 179 | |
| 180 | if start.ordinal > end.ordinal { |
| 181 | return nil, fmt.Errorf("invalid closed range, start release %q is later than end release %q", start.name, end.name) |
| 182 | } |
| 183 | |
| 184 | set.addRange(start, end) |
| 185 | } else { |
| 186 | release, err := nameToRelease(specification) |
| 187 | if err != nil { |
| 188 | return nil, err |
| 189 | } |
| 190 | set.addItem(release) |
| 191 | } |
| 192 | |
| 193 | return set, nil |
| 194 | } |
Paul Duffin | 0c3acbf | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 195 | |
| 196 | // Given a set of properties (struct value), set the value of a field within that struct (or one of |
| 197 | // its embedded structs) to its zero value. |
| 198 | type fieldPrunerFunc func(structValue reflect.Value) |
| 199 | |
| 200 | // A property that can be cleared by a propertyPruner. |
| 201 | type prunerProperty struct { |
| 202 | // The name of the field for this property. It is a "."-separated path for fields in non-anonymous |
| 203 | // sub-structs. |
| 204 | name string |
| 205 | |
| 206 | // Sets the associated field to its zero value. |
| 207 | prunerFunc fieldPrunerFunc |
| 208 | } |
| 209 | |
| 210 | // propertyPruner provides support for pruning (i.e. setting to their zero value) properties from |
| 211 | // a properties structure. |
| 212 | type propertyPruner struct { |
| 213 | // The properties that the pruner will clear. |
| 214 | properties []prunerProperty |
| 215 | } |
| 216 | |
| 217 | // gatherFields recursively processes the supplied structure and a nested structures, selecting the |
| 218 | // fields that require pruning and populates the propertyPruner.properties with the information |
| 219 | // needed to prune those fields. |
| 220 | // |
| 221 | // containingStructAccessor is a func that if given an object will return a field whose value is |
| 222 | // of the supplied structType. It is nil on initial entry to this method but when this method is |
| 223 | // called recursively on a field that is a nested structure containingStructAccessor is set to a |
| 224 | // func that provides access to the field's value. |
| 225 | // |
| 226 | // namePrefix is the prefix to the fields that are being visited. It is "" on initial entry to this |
| 227 | // method but when this method is called recursively on a field that is a nested structure |
| 228 | // namePrefix is the result of appending the field name (plus a ".") to the previous name prefix. |
| 229 | // Unless the field is anonymous in which case it is passed through unchanged. |
| 230 | // |
| 231 | // selector is a func that will select whether the supplied field requires pruning or not. If it |
| 232 | // returns true then the field will be added to those to be pruned, otherwise it will not. |
| 233 | func (p *propertyPruner) gatherFields(structType reflect.Type, containingStructAccessor fieldAccessorFunc, namePrefix string, selector fieldSelectorFunc) { |
| 234 | for f := 0; f < structType.NumField(); f++ { |
| 235 | field := structType.Field(f) |
| 236 | if field.PkgPath != "" { |
| 237 | // Ignore unexported fields. |
| 238 | continue |
| 239 | } |
| 240 | |
| 241 | // Save a copy of the field index for use in the function. |
| 242 | fieldIndex := f |
| 243 | |
| 244 | name := namePrefix + field.Name |
| 245 | |
| 246 | fieldGetter := func(container reflect.Value) reflect.Value { |
| 247 | if containingStructAccessor != nil { |
| 248 | // This is an embedded structure so first access the field for the embedded |
| 249 | // structure. |
| 250 | container = containingStructAccessor(container) |
| 251 | } |
| 252 | |
| 253 | // Skip through interface and pointer values to find the structure. |
| 254 | container = getStructValue(container) |
| 255 | |
| 256 | defer func() { |
| 257 | if r := recover(); r != nil { |
| 258 | panic(fmt.Errorf("%s for fieldIndex %d of field %s of container %#v", r, fieldIndex, name, container.Interface())) |
| 259 | } |
| 260 | }() |
| 261 | |
| 262 | // Return the field. |
| 263 | return container.Field(fieldIndex) |
| 264 | } |
| 265 | |
Paul Duffin | 545c592 | 2022-01-27 16:51:51 +0000 | [diff] [blame] | 266 | fieldType := field.Type |
| 267 | if selector(name, field) { |
| 268 | zeroValue := reflect.Zero(fieldType) |
| 269 | fieldPruner := func(container reflect.Value) { |
| 270 | if containingStructAccessor != nil { |
| 271 | // This is an embedded structure so first access the field for the embedded |
| 272 | // structure. |
| 273 | container = containingStructAccessor(container) |
| 274 | } |
| 275 | |
| 276 | // Skip through interface and pointer values to find the structure. |
| 277 | container = getStructValue(container) |
| 278 | |
| 279 | defer func() { |
| 280 | if r := recover(); r != nil { |
| 281 | panic(fmt.Errorf("%s\n\tfor field (index %d, name %s)", r, fieldIndex, name)) |
| 282 | } |
| 283 | }() |
| 284 | |
| 285 | // Set the field. |
| 286 | container.Field(fieldIndex).Set(zeroValue) |
Paul Duffin | 0c3acbf | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 287 | } |
| 288 | |
Paul Duffin | 0c3acbf | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 289 | property := prunerProperty{ |
| 290 | name, |
| 291 | fieldPruner, |
| 292 | } |
| 293 | p.properties = append(p.properties, property) |
Paul Duffin | 545c592 | 2022-01-27 16:51:51 +0000 | [diff] [blame] | 294 | } else { |
| 295 | switch fieldType.Kind() { |
| 296 | case reflect.Struct: |
| 297 | // Gather fields from the nested or embedded structure. |
| 298 | var subNamePrefix string |
| 299 | if field.Anonymous { |
| 300 | subNamePrefix = namePrefix |
| 301 | } else { |
| 302 | subNamePrefix = name + "." |
| 303 | } |
| 304 | p.gatherFields(fieldType, fieldGetter, subNamePrefix, selector) |
Paul Duffin | 106a3a4 | 2022-01-27 16:39:06 +0000 | [diff] [blame] | 305 | |
| 306 | case reflect.Map: |
| 307 | // Get the type of the values stored in the map. |
| 308 | valueType := fieldType.Elem() |
| 309 | // Skip over * types. |
| 310 | if valueType.Kind() == reflect.Ptr { |
| 311 | valueType = valueType.Elem() |
| 312 | } |
| 313 | if valueType.Kind() == reflect.Struct { |
| 314 | // If this is not referenced by a pointer then it is an error as it is impossible to |
| 315 | // modify a struct that is stored directly as a value in a map. |
| 316 | if fieldType.Elem().Kind() != reflect.Ptr { |
| 317 | panic(fmt.Errorf("Cannot prune struct %s stored by value in map %s, map values must"+ |
| 318 | " be pointers to structs", |
| 319 | fieldType.Elem(), name)) |
| 320 | } |
| 321 | |
| 322 | // Create a new pruner for the values of the map. |
| 323 | valuePruner := newPropertyPrunerForStructType(valueType, selector) |
| 324 | |
| 325 | // Create a new fieldPruner that will iterate over all the items in the map and call the |
| 326 | // pruner on them. |
| 327 | fieldPruner := func(container reflect.Value) { |
| 328 | mapValue := fieldGetter(container) |
| 329 | |
| 330 | for _, keyValue := range mapValue.MapKeys() { |
| 331 | itemValue := mapValue.MapIndex(keyValue) |
| 332 | |
| 333 | defer func() { |
| 334 | if r := recover(); r != nil { |
| 335 | panic(fmt.Errorf("%s\n\tfor key %q", r, keyValue)) |
| 336 | } |
| 337 | }() |
| 338 | |
| 339 | valuePruner.pruneProperties(itemValue.Interface()) |
| 340 | } |
| 341 | } |
| 342 | |
| 343 | // Add the map field pruner to the list of property pruners. |
| 344 | property := prunerProperty{ |
| 345 | name + "[*]", |
| 346 | fieldPruner, |
| 347 | } |
| 348 | p.properties = append(p.properties, property) |
| 349 | } |
Paul Duffin | 0c3acbf | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 350 | } |
Paul Duffin | 0c3acbf | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 351 | } |
| 352 | } |
| 353 | } |
| 354 | |
Paul Duffin | 545c592 | 2022-01-27 16:51:51 +0000 | [diff] [blame] | 355 | // pruneProperties will prune (set to zero value) any properties in the struct referenced by the |
| 356 | // supplied struct pointer. |
Paul Duffin | 0c3acbf | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 357 | // |
| 358 | // The struct must be of the same type as was originally passed to newPropertyPruner to create this |
| 359 | // propertyPruner. |
| 360 | func (p *propertyPruner) pruneProperties(propertiesStruct interface{}) { |
Paul Duffin | 545c592 | 2022-01-27 16:51:51 +0000 | [diff] [blame] | 361 | |
| 362 | defer func() { |
| 363 | if r := recover(); r != nil { |
| 364 | panic(fmt.Errorf("%s\n\tof container %#v", r, propertiesStruct)) |
| 365 | } |
| 366 | }() |
| 367 | |
Paul Duffin | 0c3acbf | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 368 | structValue := reflect.ValueOf(propertiesStruct) |
| 369 | for _, property := range p.properties { |
| 370 | property.prunerFunc(structValue) |
| 371 | } |
| 372 | } |
| 373 | |
| 374 | // fieldSelectorFunc is called to select whether a specific field should be pruned or not. |
| 375 | // name is the name of the field, including any prefixes from containing str |
| 376 | type fieldSelectorFunc func(name string, field reflect.StructField) bool |
| 377 | |
| 378 | // newPropertyPruner creates a new property pruner for the structure type for the supplied |
| 379 | // properties struct. |
| 380 | // |
| 381 | // The returned pruner can be used on any properties structure of the same type as the supplied set |
| 382 | // of properties. |
| 383 | func newPropertyPruner(propertiesStruct interface{}, selector fieldSelectorFunc) *propertyPruner { |
| 384 | structType := getStructValue(reflect.ValueOf(propertiesStruct)).Type() |
Paul Duffin | 106a3a4 | 2022-01-27 16:39:06 +0000 | [diff] [blame] | 385 | return newPropertyPrunerForStructType(structType, selector) |
| 386 | } |
| 387 | |
| 388 | // newPropertyPruner creates a new property pruner for the supplied properties struct type. |
| 389 | // |
| 390 | // The returned pruner can be used on any properties structure of the supplied type. |
| 391 | func newPropertyPrunerForStructType(structType reflect.Type, selector fieldSelectorFunc) *propertyPruner { |
Paul Duffin | 0c3acbf | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 392 | pruner := &propertyPruner{} |
| 393 | pruner.gatherFields(structType, nil, "", selector) |
| 394 | return pruner |
| 395 | } |
| 396 | |
| 397 | // newPropertyPrunerByBuildRelease creates a property pruner that will clear any properties in the |
| 398 | // structure which are not supported by the specified target build release. |
| 399 | // |
| 400 | // A property is pruned if its field has a tag of the form: |
Colin Cross | d079e0b | 2022-08-16 10:27:33 -0700 | [diff] [blame] | 401 | // |
| 402 | // `supported_build_releases:"<build-release-set>"` |
| 403 | // |
Paul Duffin | 0c3acbf | 2021-08-24 19:01:25 +0100 | [diff] [blame] | 404 | // and the resulting build release set does not contain the target build release. Properties that |
| 405 | // have no such tag are assumed to be supported by all releases. |
| 406 | func newPropertyPrunerByBuildRelease(propertiesStruct interface{}, targetBuildRelease *buildRelease) *propertyPruner { |
| 407 | return newPropertyPruner(propertiesStruct, func(name string, field reflect.StructField) bool { |
| 408 | if supportedBuildReleases, ok := field.Tag.Lookup("supported_build_releases"); ok { |
| 409 | set, err := parseBuildReleaseSet(supportedBuildReleases) |
| 410 | if err != nil { |
| 411 | panic(fmt.Errorf("invalid `supported_build_releases` tag on %s of %T: %s", name, propertiesStruct, err)) |
| 412 | } |
| 413 | |
| 414 | // If the field does not support tha target release then prune it. |
| 415 | return !set.contains(targetBuildRelease) |
| 416 | |
| 417 | } else { |
| 418 | // Any untagged fields are assumed to be supported by all build releases so should never be |
| 419 | // pruned. |
| 420 | return false |
| 421 | } |
| 422 | }) |
| 423 | } |