blob: 4c2277e855a380a8e50ad602865acf4d431876d6 [file] [log] [blame]
Paul Duffin18122942021-08-24 19:01:25 +01001// 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
15package sdk
16
17import (
18 "fmt"
Paul Duffin0c3acbf2021-08-24 19:01:25 +010019 "reflect"
Paul Duffin18122942021-08-24 19:01:25 +010020 "strings"
21)
22
23// Supports customizing sdk snapshot output based on target build release.
24
25// buildRelease represents the version of a build system used to create a specific release.
26//
27// The name of the release, is the same as the code for the dessert release, e.g. S, T, etc.
28type buildRelease struct {
29 // The name of the release, e.g. S, T, etc.
30 name string
31
32 // The index of this structure within the buildReleases list.
33 ordinal int
34}
35
36// String returns the name of the build release.
37func (s *buildRelease) String() string {
38 return s.name
39}
40
41// buildReleaseSet represents a set of buildRelease objects.
42type buildReleaseSet struct {
43 // Set of *buildRelease represented as a map from *buildRelease to struct{}.
44 contents map[*buildRelease]struct{}
45}
46
47// addItem adds a build release to the set.
48func (s *buildReleaseSet) addItem(release *buildRelease) {
49 s.contents[release] = struct{}{}
50}
51
52// addRange adds all the build releases from start (inclusive) to end (inclusive).
53func (s *buildReleaseSet) addRange(start *buildRelease, end *buildRelease) {
54 for i := start.ordinal; i <= end.ordinal; i += 1 {
55 s.addItem(buildReleases[i])
56 }
57}
58
59// contains returns true if the set contains the specified build release.
60func (s *buildReleaseSet) contains(release *buildRelease) bool {
61 _, ok := s.contents[release]
62 return ok
63}
64
65// String returns a string representation of the set, sorted from earliest to latest release.
66func (s *buildReleaseSet) String() string {
67 list := []string{}
68 for _, release := range buildReleases {
69 if _, ok := s.contents[release]; ok {
70 list = append(list, release.name)
71 }
72 }
73 return fmt.Sprintf("[%s]", strings.Join(list, ","))
74}
75
76var (
77 // nameToBuildRelease contains a map from name to build release.
78 nameToBuildRelease = map[string]*buildRelease{}
79
80 // buildReleases lists all the available build releases.
81 buildReleases = []*buildRelease{}
82
83 // allBuildReleaseSet is the set of all build releases.
84 allBuildReleaseSet = &buildReleaseSet{contents: map[*buildRelease]struct{}{}}
85
86 // Add the build releases from oldest to newest.
87 buildReleaseS = initBuildRelease("S")
Paul Duffine7babdb2022-02-10 13:06:54 +000088 buildReleaseT = initBuildRelease("Tiramisu")
Paul Duffin18122942021-08-24 19:01:25 +010089)
90
91// initBuildRelease creates a new build release with the specified name.
92func initBuildRelease(name string) *buildRelease {
93 ordinal := len(nameToBuildRelease)
94 release := &buildRelease{name: name, ordinal: ordinal}
95 nameToBuildRelease[name] = release
96 buildReleases = append(buildReleases, release)
97 allBuildReleaseSet.addItem(release)
98 return release
99}
100
101// latestBuildRelease returns the latest build release, i.e. the last one added.
102func latestBuildRelease() *buildRelease {
103 return buildReleases[len(buildReleases)-1]
104}
105
106// nameToRelease maps from build release name to the corresponding build release (if it exists) or
107// the error if it does not.
108func nameToRelease(name string) (*buildRelease, error) {
109 if r, ok := nameToBuildRelease[name]; ok {
110 return r, nil
111 }
112
113 return nil, fmt.Errorf("unknown release %q, expected one of %s", name, allBuildReleaseSet)
114}
115
116// parseBuildReleaseSet parses a build release set string specification into a build release set.
117//
118// The specification consists of one of the following:
119// * a single build release name, e.g. S, T, etc.
120// * a closed range (inclusive to inclusive), e.g. S-T
121// * an open range, e.g. T+.
122//
123// This returns the set if the specification was valid or an error.
124func parseBuildReleaseSet(specification string) (*buildReleaseSet, error) {
125 set := &buildReleaseSet{contents: map[*buildRelease]struct{}{}}
126
127 if strings.HasSuffix(specification, "+") {
128 rangeStart := strings.TrimSuffix(specification, "+")
129 start, err := nameToRelease(rangeStart)
130 if err != nil {
131 return nil, err
132 }
133 end := latestBuildRelease()
134 set.addRange(start, end)
135 } else if strings.Contains(specification, "-") {
136 limits := strings.SplitN(specification, "-", 2)
137 start, err := nameToRelease(limits[0])
138 if err != nil {
139 return nil, err
140 }
141
142 end, err := nameToRelease(limits[1])
143 if err != nil {
144 return nil, err
145 }
146
147 if start.ordinal > end.ordinal {
148 return nil, fmt.Errorf("invalid closed range, start release %q is later than end release %q", start.name, end.name)
149 }
150
151 set.addRange(start, end)
152 } else {
153 release, err := nameToRelease(specification)
154 if err != nil {
155 return nil, err
156 }
157 set.addItem(release)
158 }
159
160 return set, nil
161}
Paul Duffin0c3acbf2021-08-24 19:01:25 +0100162
163// Given a set of properties (struct value), set the value of a field within that struct (or one of
164// its embedded structs) to its zero value.
165type fieldPrunerFunc func(structValue reflect.Value)
166
167// A property that can be cleared by a propertyPruner.
168type prunerProperty struct {
169 // The name of the field for this property. It is a "."-separated path for fields in non-anonymous
170 // sub-structs.
171 name string
172
173 // Sets the associated field to its zero value.
174 prunerFunc fieldPrunerFunc
175}
176
177// propertyPruner provides support for pruning (i.e. setting to their zero value) properties from
178// a properties structure.
179type propertyPruner struct {
180 // The properties that the pruner will clear.
181 properties []prunerProperty
182}
183
184// gatherFields recursively processes the supplied structure and a nested structures, selecting the
185// fields that require pruning and populates the propertyPruner.properties with the information
186// needed to prune those fields.
187//
188// containingStructAccessor is a func that if given an object will return a field whose value is
189// of the supplied structType. It is nil on initial entry to this method but when this method is
190// called recursively on a field that is a nested structure containingStructAccessor is set to a
191// func that provides access to the field's value.
192//
193// namePrefix is the prefix to the fields that are being visited. It is "" on initial entry to this
194// method but when this method is called recursively on a field that is a nested structure
195// namePrefix is the result of appending the field name (plus a ".") to the previous name prefix.
196// Unless the field is anonymous in which case it is passed through unchanged.
197//
198// selector is a func that will select whether the supplied field requires pruning or not. If it
199// returns true then the field will be added to those to be pruned, otherwise it will not.
200func (p *propertyPruner) gatherFields(structType reflect.Type, containingStructAccessor fieldAccessorFunc, namePrefix string, selector fieldSelectorFunc) {
201 for f := 0; f < structType.NumField(); f++ {
202 field := structType.Field(f)
203 if field.PkgPath != "" {
204 // Ignore unexported fields.
205 continue
206 }
207
208 // Save a copy of the field index for use in the function.
209 fieldIndex := f
210
211 name := namePrefix + field.Name
212
213 fieldGetter := func(container reflect.Value) reflect.Value {
214 if containingStructAccessor != nil {
215 // This is an embedded structure so first access the field for the embedded
216 // structure.
217 container = containingStructAccessor(container)
218 }
219
220 // Skip through interface and pointer values to find the structure.
221 container = getStructValue(container)
222
223 defer func() {
224 if r := recover(); r != nil {
225 panic(fmt.Errorf("%s for fieldIndex %d of field %s of container %#v", r, fieldIndex, name, container.Interface()))
226 }
227 }()
228
229 // Return the field.
230 return container.Field(fieldIndex)
231 }
232
Paul Duffin545c5922022-01-27 16:51:51 +0000233 fieldType := field.Type
234 if selector(name, field) {
235 zeroValue := reflect.Zero(fieldType)
236 fieldPruner := func(container reflect.Value) {
237 if containingStructAccessor != nil {
238 // This is an embedded structure so first access the field for the embedded
239 // structure.
240 container = containingStructAccessor(container)
241 }
242
243 // Skip through interface and pointer values to find the structure.
244 container = getStructValue(container)
245
246 defer func() {
247 if r := recover(); r != nil {
248 panic(fmt.Errorf("%s\n\tfor field (index %d, name %s)", r, fieldIndex, name))
249 }
250 }()
251
252 // Set the field.
253 container.Field(fieldIndex).Set(zeroValue)
Paul Duffin0c3acbf2021-08-24 19:01:25 +0100254 }
255
Paul Duffin0c3acbf2021-08-24 19:01:25 +0100256 property := prunerProperty{
257 name,
258 fieldPruner,
259 }
260 p.properties = append(p.properties, property)
Paul Duffin545c5922022-01-27 16:51:51 +0000261 } else {
262 switch fieldType.Kind() {
263 case reflect.Struct:
264 // Gather fields from the nested or embedded structure.
265 var subNamePrefix string
266 if field.Anonymous {
267 subNamePrefix = namePrefix
268 } else {
269 subNamePrefix = name + "."
270 }
271 p.gatherFields(fieldType, fieldGetter, subNamePrefix, selector)
Paul Duffin106a3a42022-01-27 16:39:06 +0000272
273 case reflect.Map:
274 // Get the type of the values stored in the map.
275 valueType := fieldType.Elem()
276 // Skip over * types.
277 if valueType.Kind() == reflect.Ptr {
278 valueType = valueType.Elem()
279 }
280 if valueType.Kind() == reflect.Struct {
281 // If this is not referenced by a pointer then it is an error as it is impossible to
282 // modify a struct that is stored directly as a value in a map.
283 if fieldType.Elem().Kind() != reflect.Ptr {
284 panic(fmt.Errorf("Cannot prune struct %s stored by value in map %s, map values must"+
285 " be pointers to structs",
286 fieldType.Elem(), name))
287 }
288
289 // Create a new pruner for the values of the map.
290 valuePruner := newPropertyPrunerForStructType(valueType, selector)
291
292 // Create a new fieldPruner that will iterate over all the items in the map and call the
293 // pruner on them.
294 fieldPruner := func(container reflect.Value) {
295 mapValue := fieldGetter(container)
296
297 for _, keyValue := range mapValue.MapKeys() {
298 itemValue := mapValue.MapIndex(keyValue)
299
300 defer func() {
301 if r := recover(); r != nil {
302 panic(fmt.Errorf("%s\n\tfor key %q", r, keyValue))
303 }
304 }()
305
306 valuePruner.pruneProperties(itemValue.Interface())
307 }
308 }
309
310 // Add the map field pruner to the list of property pruners.
311 property := prunerProperty{
312 name + "[*]",
313 fieldPruner,
314 }
315 p.properties = append(p.properties, property)
316 }
Paul Duffin0c3acbf2021-08-24 19:01:25 +0100317 }
Paul Duffin0c3acbf2021-08-24 19:01:25 +0100318 }
319 }
320}
321
Paul Duffin545c5922022-01-27 16:51:51 +0000322// pruneProperties will prune (set to zero value) any properties in the struct referenced by the
323// supplied struct pointer.
Paul Duffin0c3acbf2021-08-24 19:01:25 +0100324//
325// The struct must be of the same type as was originally passed to newPropertyPruner to create this
326// propertyPruner.
327func (p *propertyPruner) pruneProperties(propertiesStruct interface{}) {
Paul Duffin545c5922022-01-27 16:51:51 +0000328
329 defer func() {
330 if r := recover(); r != nil {
331 panic(fmt.Errorf("%s\n\tof container %#v", r, propertiesStruct))
332 }
333 }()
334
Paul Duffin0c3acbf2021-08-24 19:01:25 +0100335 structValue := reflect.ValueOf(propertiesStruct)
336 for _, property := range p.properties {
337 property.prunerFunc(structValue)
338 }
339}
340
341// fieldSelectorFunc is called to select whether a specific field should be pruned or not.
342// name is the name of the field, including any prefixes from containing str
343type fieldSelectorFunc func(name string, field reflect.StructField) bool
344
345// newPropertyPruner creates a new property pruner for the structure type for the supplied
346// properties struct.
347//
348// The returned pruner can be used on any properties structure of the same type as the supplied set
349// of properties.
350func newPropertyPruner(propertiesStruct interface{}, selector fieldSelectorFunc) *propertyPruner {
351 structType := getStructValue(reflect.ValueOf(propertiesStruct)).Type()
Paul Duffin106a3a42022-01-27 16:39:06 +0000352 return newPropertyPrunerForStructType(structType, selector)
353}
354
355// newPropertyPruner creates a new property pruner for the supplied properties struct type.
356//
357// The returned pruner can be used on any properties structure of the supplied type.
358func newPropertyPrunerForStructType(structType reflect.Type, selector fieldSelectorFunc) *propertyPruner {
Paul Duffin0c3acbf2021-08-24 19:01:25 +0100359 pruner := &propertyPruner{}
360 pruner.gatherFields(structType, nil, "", selector)
361 return pruner
362}
363
364// newPropertyPrunerByBuildRelease creates a property pruner that will clear any properties in the
365// structure which are not supported by the specified target build release.
366//
367// A property is pruned if its field has a tag of the form:
368// `supported_build_releases:"<build-release-set>"`
369// and the resulting build release set does not contain the target build release. Properties that
370// have no such tag are assumed to be supported by all releases.
371func newPropertyPrunerByBuildRelease(propertiesStruct interface{}, targetBuildRelease *buildRelease) *propertyPruner {
372 return newPropertyPruner(propertiesStruct, func(name string, field reflect.StructField) bool {
373 if supportedBuildReleases, ok := field.Tag.Lookup("supported_build_releases"); ok {
374 set, err := parseBuildReleaseSet(supportedBuildReleases)
375 if err != nil {
376 panic(fmt.Errorf("invalid `supported_build_releases` tag on %s of %T: %s", name, propertiesStruct, err))
377 }
378
379 // If the field does not support tha target release then prune it.
380 return !set.contains(targetBuildRelease)
381
382 } else {
383 // Any untagged fields are assumed to be supported by all build releases so should never be
384 // pruned.
385 return false
386 }
387 })
388}