github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/platform/runtime/buildscript/buildscript.go (about) 1 package buildscript 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "strconv" 9 "strings" 10 11 "golang.org/x/text/cases" 12 "golang.org/x/text/language" 13 14 "github.com/go-openapi/strfmt" 15 "github.com/thoas/go-funk" 16 17 "github.com/ActiveState/cli/internal/constants" 18 "github.com/ActiveState/cli/internal/errs" 19 "github.com/ActiveState/cli/internal/locale" 20 "github.com/ActiveState/cli/internal/multilog" 21 "github.com/ActiveState/cli/internal/rtutils/ptr" 22 "github.com/ActiveState/cli/pkg/platform/runtime/buildexpression" 23 "github.com/alecthomas/participle/v2" 24 ) 25 26 // Script's tagged fields will be initially filled in by Participle. 27 // expr will be constructed later and is this script's buildexpression. We keep a copy of the build 28 // expression here with any changes that have been applied before either writing it to disk or 29 // submitting it to the build planner. It's easier to operate on build expressions directly than to 30 // modify or manually populate the Participle-produced fields and re-generate a build expression. 31 type Script struct { 32 Assignments []*Assignment `parser:"@@+"` 33 AtTime *strfmt.DateTime 34 Expr *buildexpression.BuildExpression 35 } 36 37 type Assignment struct { 38 Key string `parser:"@Ident '='"` 39 Value *Value `parser:"@@"` 40 } 41 42 type Value struct { 43 FuncCall *FuncCall `parser:"@@"` 44 List *[]*Value `parser:"| '[' (@@ (',' @@)* ','?)? ']'"` 45 Str *string `parser:"| @String"` 46 Number *float64 `parser:"| (@Float | @Int)"` 47 Null *Null `parser:"| @@"` 48 49 Assignment *Assignment `parser:"| @@"` // only in FuncCall 50 Object *[]*Assignment `parser:"| '{' @@ (',' @@)* ','? '}'"` // only in List 51 Ident *string `parser:"| @Ident"` // only in FuncCall or Assignment 52 } 53 54 type Null struct { 55 Null string `parser:"'null'"` 56 } 57 58 type FuncCall struct { 59 Name string `parser:"@Ident"` 60 Arguments []*Value `parser:"'(' @@ (',' @@)* ','? ')'"` 61 } 62 63 type In struct { 64 FuncCall *FuncCall `parser:"@@"` 65 Name *string `parser:"| @Ident"` 66 } 67 68 var ( 69 reqFuncName = "Req" 70 eqFuncName = "Eq" 71 neFuncName = "Ne" 72 gtFuncName = "Gt" 73 gteFuncName = "Gte" 74 ltFuncName = "Lt" 75 lteFuncName = "Lte" 76 andFuncName = "And" 77 ) 78 79 func New(data []byte) (*Script, error) { 80 parser, err := participle.Build[Script]() 81 if err != nil { 82 return nil, errs.Wrap(err, "Could not create parser for build script") 83 } 84 85 script, err := parser.ParseBytes(constants.BuildScriptFileName, data) 86 if err != nil { 87 var parseError participle.Error 88 if errors.As(err, &parseError) { 89 return nil, locale.WrapExternalError(err, "err_parse_buildscript_bytes", "Could not parse build script: {{.V0}}: {{.V1}}", parseError.Position().String(), parseError.Message()) 90 } 91 return nil, locale.WrapError(err, "err_parse_buildscript_bytes", "Could not parse build script: {{.V0}}", err.Error()) 92 } 93 94 // Construct the equivalent buildexpression. 95 bytes, err := json.Marshal(script) 96 if err != nil { 97 return nil, errs.Wrap(err, "Could not marshal build script to build expression") 98 } 99 100 expr, err := buildexpression.New(bytes) 101 if err != nil { 102 return nil, locale.WrapError(err, "err_parse_buildscript_bytes", "Could not construct build expression: {{.V0}}", errs.JoinMessage(err)) 103 } 104 script.Expr = expr 105 106 return script, nil 107 } 108 109 func NewFromBuildExpression(atTime *strfmt.DateTime, expr *buildexpression.BuildExpression) (*Script, error) { 110 // Copy incoming build expression to keep any modifications local. 111 var err error 112 expr, err = expr.Copy() 113 if err != nil { 114 return nil, errs.Wrap(err, "Could not copy build expression") 115 } 116 117 // Update old expressions that bake in at_time as a timestamp instead of as a variable. 118 err = expr.MaybeSetDefaultTimestamp(atTime) 119 if err != nil { 120 return nil, errs.Wrap(err, "Could not set default timestamp") 121 } 122 123 return &Script{AtTime: atTime, Expr: expr}, nil 124 } 125 126 func indent(s string) string { 127 return fmt.Sprintf("\t%s", strings.ReplaceAll(s, "\n", "\n\t")) 128 } 129 130 func (s *Script) String() string { 131 buf := strings.Builder{} 132 133 if s.AtTime != nil { 134 buf.WriteString(assignmentString(&buildexpression.Var{ 135 Name: buildexpression.AtTimeKey, 136 Value: &buildexpression.Value{Str: ptr.To(s.AtTime.String())}, 137 })) 138 buf.WriteString("\n") 139 } 140 141 for _, assignment := range s.Expr.Let.Assignments { 142 if assignment.Name == buildexpression.RequirementsKey { 143 assignment = transformRequirements(assignment) 144 } 145 buf.WriteString(assignmentString(assignment)) 146 buf.WriteString("\n") 147 } 148 149 buf.WriteString("\n") 150 buf.WriteString("main = ") 151 switch { 152 case s.Expr.Let.In.FuncCall != nil: 153 buf.WriteString(apString(s.Expr.Let.In.FuncCall)) 154 case s.Expr.Let.In.Name != nil: 155 buf.WriteString(*s.Expr.Let.In.Name) 156 } 157 158 return buf.String() 159 } 160 161 // transformRequirements transforms a buildexpression list of requirements in object form into a 162 // list of requirements in function-call form, which is how requirements are represented in 163 // buildscripts. 164 // This is to avoid custom marshaling code and reuse existing marshaling code. 165 func transformRequirements(reqs *buildexpression.Var) *buildexpression.Var { 166 newReqs := &buildexpression.Var{ 167 Name: buildexpression.RequirementsKey, 168 Value: &buildexpression.Value{ 169 List: &[]*buildexpression.Value{}, 170 }, 171 } 172 173 for _, req := range *reqs.Value.List { 174 *newReqs.Value.List = append(*newReqs.Value.List, transformRequirement(req)) 175 } 176 177 return newReqs 178 } 179 180 // transformRequirement transforms a buildexpression requirement in object form into a requirement 181 // in function-call form. 182 // For example, transform something like 183 // 184 // {"name": "<name>", "namespace": "<namespace>", 185 // "version_requirements": [{"comparator": "<op>", "version": "<version>"}]} 186 // 187 // into something like 188 // 189 // Req(name = "<name>", namespace = "<namespace>", version = <op>(value = "<version>")) 190 func transformRequirement(req *buildexpression.Value) *buildexpression.Value { 191 newReq := &buildexpression.Value{ 192 Ap: &buildexpression.Ap{ 193 Name: reqFuncName, 194 Arguments: []*buildexpression.Value{}, 195 }, 196 } 197 198 for _, arg := range *req.Object { 199 name := arg.Name 200 value := arg.Value 201 202 // Transform the version value from the requirement object. 203 if name == buildexpression.RequirementVersionRequirementsKey { 204 name = buildexpression.RequirementVersionKey 205 value = &buildexpression.Value{Ap: transformVersion(arg)} 206 } 207 208 // Add the argument to the function transformation. 209 newReq.Ap.Arguments = append(newReq.Ap.Arguments, &buildexpression.Value{ 210 Assignment: &buildexpression.Var{Name: name, Value: value}, 211 }) 212 } 213 214 return newReq 215 } 216 217 // transformVersion transforms a buildexpression version_requirements list in object form into 218 // function-call form. 219 // For example, transform something like 220 // 221 // [{"comparator": "<op1>", "version": "<version1>"}, {"comparator": "<op2>", "version": "<version2>"}] 222 // 223 // into something like 224 // 225 // And(<op1>(value = "<version1>"), <op2>(value = "<version2>")) 226 func transformVersion(requirements *buildexpression.Var) *buildexpression.Ap { 227 var aps []*buildexpression.Ap 228 for _, constraint := range *requirements.Value.List { 229 ap := &buildexpression.Ap{} 230 for _, o := range *constraint.Object { 231 switch o.Name { 232 case buildexpression.RequirementVersionKey: 233 ap.Arguments = []*buildexpression.Value{{ 234 Assignment: &buildexpression.Var{Name: "value", Value: &buildexpression.Value{Str: o.Value.Str}}, 235 }} 236 case buildexpression.RequirementComparatorKey: 237 ap.Name = cases.Title(language.English).String(*o.Value.Str) 238 } 239 } 240 aps = append(aps, ap) 241 } 242 243 if len(aps) == 1 { 244 return aps[0] // e.g. Eq(value = "1.0") 245 } 246 247 // e.g. And(left = Gt(value = "1.0"), right = Lt(value = "3.0")) 248 // Iterate backwards over the requirements array and construct a binary tree of 'And()' functions. 249 // For example, given [Gt(value = "1.0"), Ne(value = "2.0"), Lt(value = "3.0")], produce: 250 // And(left = Gt(value = "1.0"), right = And(left = Ne(value = "2.0"), right = Lt(value = "3.0"))) 251 var ap *buildexpression.Ap 252 for i := len(aps) - 2; i >= 0; i-- { 253 right := &buildexpression.Value{Ap: aps[i+1]} 254 if ap != nil { 255 right = &buildexpression.Value{Ap: ap} 256 } 257 args := []*buildexpression.Value{ 258 {Assignment: &buildexpression.Var{Name: "left", Value: &buildexpression.Value{Ap: aps[i]}}}, 259 {Assignment: &buildexpression.Var{Name: "right", Value: right}}, 260 } 261 ap = &buildexpression.Ap{Name: andFuncName, Arguments: args} 262 } 263 return ap 264 } 265 266 func assignmentString(a *buildexpression.Var) string { 267 if a.Name == buildexpression.RequirementsKey { 268 a = transformRequirements(a) 269 } 270 return fmt.Sprintf("%s = %s", a.Name, valueString(a.Value)) 271 } 272 273 func valueString(v *buildexpression.Value) string { 274 switch { 275 case v.Ap != nil: 276 return apString(v.Ap) 277 278 case v.List != nil: 279 buf := bytes.Buffer{} 280 buf.WriteString("[\n") 281 for i, item := range *v.List { 282 buf.WriteString(indent(valueString(item))) 283 if i+1 < len(*v.List) { 284 buf.WriteString(",") 285 } 286 buf.WriteString("\n") 287 } 288 buf.WriteString("]") 289 return buf.String() 290 291 case v.Str != nil: 292 if strings.HasPrefix(*v.Str, "$") { // variable reference 293 return strings.TrimLeft(*v.Str, "$") 294 } 295 return strconv.Quote(*v.Str) 296 297 case v.Float != nil: 298 return strconv.FormatFloat(*v.Float, 'G', -1, 64) // 64-bit float with minimum digits on display 299 300 case v.Null != nil: 301 return "null" 302 303 case v.Assignment != nil: 304 return assignmentString(v.Assignment) 305 306 case v.Object != nil: 307 buf := bytes.Buffer{} 308 buf.WriteString("{\n") 309 for i, pair := range *v.Object { 310 buf.WriteString(indent(assignmentString(pair))) 311 if i+1 < len(*v.Object) { 312 buf.WriteString(",") 313 } 314 buf.WriteString("\n") 315 } 316 buf.WriteString("}") 317 return buf.String() 318 319 case v.Ident != nil: 320 return *v.Ident 321 } 322 323 return "[\n]" // participle does not create v.List if it's empty 324 } 325 326 // inlineFunctions contains buildscript function names whose arguments should all be written on a 327 // single line. By default, function arguments are written one per line. 328 var inlineFunctions = []string{ 329 reqFuncName, 330 eqFuncName, neFuncName, 331 gtFuncName, gteFuncName, 332 ltFuncName, lteFuncName, 333 andFuncName, 334 } 335 336 func apString(f *buildexpression.Ap) string { 337 var ( 338 newline = "\n" 339 comma = "," 340 indent = indent 341 ) 342 343 if funk.Contains(inlineFunctions, f.Name) { 344 newline = "" 345 comma = ", " 346 indent = func(s string) string { 347 return s 348 } 349 } 350 351 buf := bytes.Buffer{} 352 buf.WriteString(fmt.Sprintf("%s(%s", f.Name, newline)) 353 354 for i, argument := range f.Arguments { 355 buf.WriteString(indent(valueString(argument))) 356 357 if i+1 < len(f.Arguments) { 358 buf.WriteString(comma) 359 } 360 361 buf.WriteString(newline) 362 } 363 364 buf.WriteString(")") 365 return buf.String() 366 } 367 368 func (s *Script) Equals(other *Script) bool { 369 // Compare top-level at_time. 370 switch { 371 case s.AtTime != nil && other.AtTime != nil && s.AtTime.String() != other.AtTime.String(): 372 return false 373 case (s.AtTime == nil) != (other.AtTime == nil): 374 return false 375 } 376 377 // Compare buildexpression JSON. 378 myJson, err := json.Marshal(s.Expr) 379 if err != nil { 380 multilog.Error("Unable to marshal this buildscript to JSON: %v", err) 381 return false 382 } 383 otherJson, err := json.Marshal(other.Expr) 384 if err != nil { 385 multilog.Error("Unable to marshal other buildscript to JSON: %v", err) 386 return false 387 } 388 return string(myJson) == string(otherJson) 389 }