platform_build/tools/compliance/policy/resolve.go
Bob Badour b285515ca1 license metadata remove path on top-down walk
Performance optimization means not every path will be traversed.

Instead of updating parents via the path, perform a 2nd bottom-up walk
after the top-down walk to propagate the new resolutions to parents.

Note: the 2nd walk method will add resolutions to statically linked
libraries etc. at deeper levels, but those do not affect what gets
reported. In particular, note that test data for dumpresolutions
changes, but none of the test data for listshare, checkshare etc.
changes.

Test: m all systemlicense listshare checkshare dumpgraph dumpresolutions

Bug: 68860345
Bug: 151177513
Bug: 151953481
Change-Id: I76361c4e33bbadbbea38cbec260430e8f9407628
2021-12-08 12:52:59 -08:00

241 lines
7.2 KiB
Go

// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package compliance
// ResolveBottomUpConditions performs a bottom-up walk of the LicenseGraph
// propagating conditions up the graph as necessary according to the properties
// of each edge and according to each license condition in question.
//
// Subsequent top-down walks of the graph will filter some resolutions and may
// introduce new resolutions.
//
// e.g. if a "restricted" condition applies to a binary, it also applies to all
// of the statically-linked libraries and the transitive closure of their static
// dependencies; even if neither they nor the transitive closure of their
// dependencies originate any "restricted" conditions. The bottom-up walk will
// not resolve the library and its transitive closure, but the later top-down
// walk will.
func ResolveBottomUpConditions(lg *LicenseGraph) *ResolutionSet {
// short-cut if already walked and cached
lg.mu.Lock()
rs := lg.rsBU
lg.mu.Unlock()
if rs != nil {
return rs
}
// must be indexed for fast lookup
lg.indexForward()
rs = resolveBottomUp(lg, make(map[*TargetNode]actionSet) /* empty map; no prior resolves */)
// if not yet cached, save the result
lg.mu.Lock()
if lg.rsBU == nil {
lg.rsBU = rs
} else {
// if we end up with 2, release the later for garbage collection
rs = lg.rsBU
}
lg.mu.Unlock()
return rs
}
// ResolveTopDownCondtions performs a top-down walk of the LicenseGraph
// resolving all reachable nodes for `condition`. Policy establishes the rules
// for transforming and propagating resolutions down the graph.
//
// e.g. For current policy, none of the conditions propagate from target to
// dependency except restricted. For restricted, the policy is to share the
// source of any libraries linked to restricted code and to provide notice.
func ResolveTopDownConditions(lg *LicenseGraph) *ResolutionSet {
// short-cut if already walked and cached
lg.mu.Lock()
rs := lg.rsTD
lg.mu.Unlock()
if rs != nil {
return rs
}
// start with the conditions propagated up the graph
rs = ResolveBottomUpConditions(lg)
// rmap maps 'appliesTo' targets to their applicable conditions
//
// rmap is the resulting ResolutionSet
rmap := make(map[*TargetNode]actionSet)
// cmap contains the set of targets walked as pure aggregates. i.e. containers
cmap := make(map[*TargetNode]bool)
var walk func(fnode *TargetNode, cs *LicenseConditionSet, treatAsAggregate bool)
walk = func(fnode *TargetNode, cs *LicenseConditionSet, treatAsAggregate bool) {
if _, ok := rmap[fnode]; !ok {
rmap[fnode] = make(actionSet)
}
rmap[fnode].add(fnode, cs)
if treatAsAggregate {
cmap[fnode] = true
}
// add conditions attached to `fnode`
cs = cs.Copy()
for _, fcs := range rs.resolutions[fnode] {
cs.AddSet(fcs)
}
// for each dependency
for _, edge := range lg.index[fnode.name] {
e := TargetEdge{lg, edge}
// dcs holds the dpendency conditions inherited from the target
dcs := targetConditionsApplicableToDep(e, cs, treatAsAggregate)
if dcs.IsEmpty() && !treatAsAggregate {
continue
}
dnode := lg.targets[edge.dependency]
if as, alreadyWalked := rmap[dnode]; alreadyWalked {
diff := dcs.Copy()
diff.RemoveSet(as.conditions())
if diff.IsEmpty() {
// no new conditions
// pure aggregates never need walking a 2nd time with same conditions
if treatAsAggregate {
continue
}
// non-aggregates don't need walking as non-aggregate a 2nd time
if _, asAggregate := cmap[dnode]; !asAggregate {
continue
}
// previously walked as pure aggregate; need to re-walk as non-aggregate
delete(cmap, dnode)
}
}
// add the conditions to the dependency
walk(dnode, dcs, treatAsAggregate && lg.targets[edge.dependency].IsContainer())
}
}
// walk each of the roots
for _, r := range lg.rootFiles {
rnode := lg.targets[r]
as, ok := rs.resolutions[rnode]
if !ok {
// no conditions in root or transitive closure of dependencies
continue
}
if as.isEmpty() {
continue
}
// add the conditions to the root and its transitive closure
walk(rnode, newLicenseConditionSet(), lg.targets[r].IsContainer())
}
// back-fill any bottom-up conditions on targets missed by top-down walk
for attachesTo, as := range rs.resolutions {
if _, ok := rmap[attachesTo]; !ok {
rmap[attachesTo] = as.copy()
} else {
rmap[attachesTo].addSet(as)
}
}
// propagate any new conditions back up the graph
rs = resolveBottomUp(lg, rmap)
// if not yet cached, save the result
lg.mu.Lock()
if lg.rsTD == nil {
lg.rsTD = rs
} else {
// if we end up with 2, release the later for garbage collection
rs = lg.rsTD
}
lg.mu.Unlock()
return rs
}
// resolveBottomUp implements a bottom-up resolve propagating conditions both
// from the graph, and from a `priors` map of resolutions.
func resolveBottomUp(lg *LicenseGraph, priors map[*TargetNode]actionSet) *ResolutionSet {
rs := newResolutionSet()
// cmap contains an entry for every target that was previously walked as a pure aggregate only.
cmap := make(map[string]bool)
var walk func(f string, treatAsAggregate bool) actionSet
walk = func(f string, treatAsAggregate bool) actionSet {
target := lg.targets[f]
result := make(actionSet)
result[target] = newLicenseConditionSet()
result[target].add(target, target.proto.LicenseConditions...)
if pas, ok := priors[target]; ok {
result.addSet(pas)
}
if preresolved, ok := rs.resolutions[target]; ok {
if treatAsAggregate {
result.addSet(preresolved)
return result
}
if _, asAggregate := cmap[f]; !asAggregate {
result.addSet(preresolved)
return result
}
// previously walked in a pure aggregate context,
// needs to walk again in non-aggregate context
delete(cmap, f)
}
if treatAsAggregate {
cmap[f] = true
}
// add all the conditions from all the dependencies
for _, edge := range lg.index[f] {
// walk dependency to get its conditions
as := walk(edge.dependency, treatAsAggregate && lg.targets[edge.dependency].IsContainer())
// turn those into the conditions that apply to the target
as = depActionsApplicableToTarget(TargetEdge{lg, edge}, as, treatAsAggregate)
// add them to the result
result.addSet(as)
}
// record these conditions as applicable to the target
rs.addConditions(target, result)
if len(priors) == 0 {
// on the first bottom-up resolve, parents have their own sharing and notice needs
// on the later resolve, if priors is empty, there will be nothing new to add
rs.addSelf(target, result.byName(ImpliesRestricted))
}
// return this up the tree
return result
}
// walk each of the roots
for _, r := range lg.rootFiles {
_ = walk(r, lg.targets[r].IsContainer())
}
return rs
}