1// Package deps package provides methods to extract and manipulate external code dependencies from the QUICHE WORKSPACE.bazel file. 2package deps 3 4import ( 5 "fmt" 6 "regexp" 7 8 "github.com/bazelbuild/buildtools/build" 9) 10 11var lastUpdatedRE = regexp.MustCompile(`Last updated (\d{4}-\d{2}-\d{2})`) 12 13// Entry is a parsed representation of a dependency entry in the WORKSPACE.bazel file. 14type Entry struct { 15 Name string 16 SHA256 string 17 Prefix string 18 URL string 19 LastUpdated string 20} 21 22// HTTPArchiveRule returns a CallExpr describing the provided http_archive 23// rule, or nil if the expr in question is not an http_archive rule. 24func HTTPArchiveRule(expr build.Expr) (*build.CallExpr, bool) { 25 callexpr, ok := expr.(*build.CallExpr) 26 if !ok { 27 return nil, false 28 } 29 name, ok := callexpr.X.(*build.Ident) 30 if !ok || name.Name != "http_archive" { 31 return nil, false 32 } 33 return callexpr, true 34} 35 36func parseString(expr build.Expr) (string, error) { 37 str, ok := expr.(*build.StringExpr) 38 if !ok { 39 return "", fmt.Errorf("expected string as the function argument") 40 } 41 return str.Value, nil 42} 43 44func parseSingleElementList(expr build.Expr) (string, error) { 45 list, ok := expr.(*build.ListExpr) 46 if !ok { 47 return "", fmt.Errorf("expected a list as the function argument") 48 } 49 if len(list.List) != 1 { 50 return "", fmt.Errorf("expected a single-element list as the function argument, got %d elements", len(list.List)) 51 } 52 return parseString(list.List[0]) 53} 54 55// ParseHTTPArchiveRule parses the provided http_archive rule and returns all of the dependency metadata embedded. 56func ParseHTTPArchiveRule(callexpr *build.CallExpr) (*Entry, error) { 57 result := Entry{} 58 for _, arg := range callexpr.List { 59 assign, ok := arg.(*build.AssignExpr) 60 if !ok { 61 return nil, fmt.Errorf("a non-named argument passed as a function parameter") 62 } 63 argname, _ := build.GetParamName(assign.LHS) 64 var err error = nil 65 switch argname { 66 case "name": 67 result.Name, err = parseString(assign.RHS) 68 case "sha256": 69 result.SHA256, err = parseString(assign.RHS) 70 71 if len(assign.Comments.Suffix) != 1 { 72 return nil, fmt.Errorf("missing the \"Last updated\" comment on the sha256 field") 73 } 74 comment := assign.Comments.Suffix[0].Token 75 match := lastUpdatedRE.FindStringSubmatch(comment) 76 if match == nil { 77 return nil, fmt.Errorf("unable to parse the \"Last updated\" comment, comment value: %s", comment) 78 } 79 result.LastUpdated = match[1] 80 case "strip_prefix": 81 result.Prefix, err = parseString(assign.RHS) 82 case "urls": 83 result.URL, err = parseSingleElementList(assign.RHS) 84 default: 85 continue 86 } 87 if err != nil { 88 return nil, err 89 } 90 } 91 if result.Name == "" { 92 return nil, fmt.Errorf("missing the name field") 93 } 94 if result.SHA256 == "" { 95 return nil, fmt.Errorf("missing the sha256 field") 96 } 97 if result.URL == "" { 98 return nil, fmt.Errorf("missing the urls field") 99 } 100 return &result, nil 101} 102 103// ParseHTTPArchiveRules parses the entire WORKSPACE.bazel file and returns all of the http_archive rules in it. 104func ParseHTTPArchiveRules(source []byte) ([]*Entry, error) { 105 file, err := build.ParseWorkspace("WORKSPACE.bazel", source) 106 if err != nil { 107 return []*Entry{}, err 108 } 109 110 result := make([]*Entry, 0) 111 for _, expr := range file.Stmt { 112 callexpr, ok := HTTPArchiveRule(expr) 113 if !ok { 114 continue 115 } 116 parsed, err := ParseHTTPArchiveRule(callexpr) 117 if err != nil { 118 return []*Entry{}, err 119 } 120 result = append(result, parsed) 121 } 122 return result, nil 123} 124