blob: a3f0899731714a010c8801f1459bf52b1504b59b [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")
88 buildReleaseT = initBuildRelease("T")
89)
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
233 zeroValue := reflect.Zero(field.Type)
234 fieldPruner := func(container reflect.Value) {
235 if containingStructAccessor != nil {
236 // This is an embedded structure so first access the field for the embedded
237 // structure.
238 container = containingStructAccessor(container)
239 }
240
241 // Skip through interface and pointer values to find the structure.
242 container = getStructValue(container)
243
244 defer func() {
245 if r := recover(); r != nil {
246 panic(fmt.Errorf("%s for fieldIndex %d of field %s of container %#v", r, fieldIndex, name, container.Interface()))
247 }
248 }()
249
250 // Set the field.
251 container.Field(fieldIndex).Set(zeroValue)
252 }
253
254 if selector(name, field) {
255 property := prunerProperty{
256 name,
257 fieldPruner,
258 }
259 p.properties = append(p.properties, property)
260 } else if field.Type.Kind() == reflect.Struct {
261 // Gather fields from the nested or embedded structure.
262 var subNamePrefix string
263 if field.Anonymous {
264 subNamePrefix = namePrefix
265 } else {
266 subNamePrefix = name + "."
267 }
268 p.gatherFields(field.Type, fieldGetter, subNamePrefix, selector)
269 }
270 }
271}
272
273// pruneProperties will prune (set to zero value) any properties in the supplied struct.
274//
275// The struct must be of the same type as was originally passed to newPropertyPruner to create this
276// propertyPruner.
277func (p *propertyPruner) pruneProperties(propertiesStruct interface{}) {
278 structValue := reflect.ValueOf(propertiesStruct)
279 for _, property := range p.properties {
280 property.prunerFunc(structValue)
281 }
282}
283
284// fieldSelectorFunc is called to select whether a specific field should be pruned or not.
285// name is the name of the field, including any prefixes from containing str
286type fieldSelectorFunc func(name string, field reflect.StructField) bool
287
288// newPropertyPruner creates a new property pruner for the structure type for the supplied
289// properties struct.
290//
291// The returned pruner can be used on any properties structure of the same type as the supplied set
292// of properties.
293func newPropertyPruner(propertiesStruct interface{}, selector fieldSelectorFunc) *propertyPruner {
294 structType := getStructValue(reflect.ValueOf(propertiesStruct)).Type()
295 pruner := &propertyPruner{}
296 pruner.gatherFields(structType, nil, "", selector)
297 return pruner
298}
299
300// newPropertyPrunerByBuildRelease creates a property pruner that will clear any properties in the
301// structure which are not supported by the specified target build release.
302//
303// A property is pruned if its field has a tag of the form:
304// `supported_build_releases:"<build-release-set>"`
305// and the resulting build release set does not contain the target build release. Properties that
306// have no such tag are assumed to be supported by all releases.
307func newPropertyPrunerByBuildRelease(propertiesStruct interface{}, targetBuildRelease *buildRelease) *propertyPruner {
308 return newPropertyPruner(propertiesStruct, func(name string, field reflect.StructField) bool {
309 if supportedBuildReleases, ok := field.Tag.Lookup("supported_build_releases"); ok {
310 set, err := parseBuildReleaseSet(supportedBuildReleases)
311 if err != nil {
312 panic(fmt.Errorf("invalid `supported_build_releases` tag on %s of %T: %s", name, propertiesStruct, err))
313 }
314
315 // If the field does not support tha target release then prune it.
316 return !set.contains(targetBuildRelease)
317
318 } else {
319 // Any untagged fields are assumed to be supported by all build releases so should never be
320 // pruned.
321 return false
322 }
323 })
324}