github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/platform/runtime/buildexpression/merge/merge.go (about) 1 package merge 2 3 import ( 4 "encoding/json" 5 "reflect" 6 7 "github.com/ActiveState/cli/internal/errs" 8 "github.com/ActiveState/cli/internal/logging" 9 "github.com/ActiveState/cli/internal/multilog" 10 "github.com/ActiveState/cli/pkg/platform/api/buildplanner/types" 11 "github.com/ActiveState/cli/pkg/platform/api/mono/mono_models" 12 "github.com/ActiveState/cli/pkg/platform/runtime/buildexpression" 13 ) 14 15 func Merge(exprA *buildexpression.BuildExpression, exprB *buildexpression.BuildExpression, strategies *mono_models.MergeStrategies) (*buildexpression.BuildExpression, error) { 16 if !isAutoMergePossible(exprA, exprB) { 17 return nil, errs.New("Unable to merge buildexpressions") 18 } 19 if len(strategies.Conflicts) > 0 { 20 return nil, errs.New("Unable to merge buildexpressions due to conflicting requirements") 21 } 22 23 // Update build expression requirements with merge results. 24 for _, req := range strategies.OverwriteChanges { 25 var op types.Operation 26 err := op.Unmarshal(req.Operation) 27 if err != nil { 28 return nil, errs.Wrap(err, "Unable to convert requirement operation to buildplan operation") 29 } 30 31 var versionRequirements []types.VersionRequirement 32 for _, constraint := range req.VersionConstraints { 33 data, err := constraint.MarshalBinary() 34 if err != nil { 35 return nil, errs.Wrap(err, "Could not marshal requirement version constraints") 36 } 37 m := make(map[string]string) 38 err = json.Unmarshal(data, &m) 39 if err != nil { 40 return nil, errs.Wrap(err, "Could not unmarshal requirement version constraints") 41 } 42 versionRequirements = append(versionRequirements, m) 43 } 44 45 bpReq := types.Requirement{ 46 Name: req.Requirement, 47 Namespace: req.Namespace, 48 VersionRequirement: versionRequirements, 49 } 50 51 if err := exprB.UpdateRequirement(op, bpReq); err != nil { 52 return nil, errs.Wrap(err, "Unable to update buildexpression with merge results") 53 } 54 } 55 56 return exprB, nil 57 } 58 59 // isAutoMergePossible determines whether or not it is possible to auto-merge the given build 60 // expressions. 61 // This is only possible if the two build expressions differ ONLY in requirements. 62 func isAutoMergePossible(exprA *buildexpression.BuildExpression, exprB *buildexpression.BuildExpression) bool { 63 jsonA, err := getComparableJson(exprA) 64 if err != nil { 65 multilog.Error("Unable to get buildexpression minus requirements: %v", errs.JoinMessage(err)) 66 return false 67 } 68 jsonB, err := getComparableJson(exprB) 69 if err != nil { 70 multilog.Error("Unable to get buildxpression minus requirements: %v", errs.JoinMessage(err)) 71 return false 72 } 73 logging.Debug("Checking for possibility of auto-merging build expressions") 74 logging.Debug("JsonA: %v", jsonA) 75 logging.Debug("JsonB: %v", jsonB) 76 return reflect.DeepEqual(jsonA, jsonB) 77 } 78 79 // getComparableJson returns a comparable JSON map[string]interface{} structure for the given build 80 // expression. The map will not have a "requirements" field, nor will it have an "at_time" field. 81 // String lists will also be sorted. 82 func getComparableJson(expr *buildexpression.BuildExpression) (map[string]interface{}, error) { 83 data, err := json.Marshal(expr) 84 if err != nil { 85 return nil, errs.New("Unable to unmarshal marshaled buildxpression") 86 } 87 88 m := make(map[string]interface{}) 89 err = json.Unmarshal(data, &m) 90 if err != nil { 91 return nil, errs.New("Unable to unmarshal marshaled buildxpression") 92 } 93 94 letValue, ok := m["let"] 95 if !ok { 96 return nil, errs.New("Build expression has no 'let' key") 97 } 98 letMap, ok := letValue.(map[string]interface{}) 99 if !ok { 100 return nil, errs.New("'let' key is not a JSON object") 101 } 102 deleteKey(&letMap, "requirements") 103 deleteKey(&letMap, "at_time") 104 105 return m, nil 106 } 107 108 // deleteKey recursively iterates over the given JSON map until it finds the given key and deletes 109 // it and its value. 110 func deleteKey(m *map[string]interface{}, key string) bool { 111 for k, v := range *m { 112 if k == key { 113 delete(*m, k) 114 return true 115 } 116 if m2, ok := v.(map[string]interface{}); ok { 117 if deleteKey(&m2, key) { 118 return true 119 } 120 } 121 } 122 return false 123 }