// Package deps package provides methods to extract and manipulate external code dependencies from the QUICHE WORKSPACE.bazel file. package deps import ( "fmt" "regexp" "github.com/bazelbuild/buildtools/build" ) var lastUpdatedRE = regexp.MustCompile(`Last updated (\d{4}-\d{2}-\d{2})`) // Entry is a parsed representation of a dependency entry in the WORKSPACE.bazel file. type Entry struct { Name string SHA256 string Prefix string URL string LastUpdated string } // HTTPArchiveRule returns a CallExpr describing the provided http_archive // rule, or nil if the expr in question is not an http_archive rule. func HTTPArchiveRule(expr build.Expr) (*build.CallExpr, bool) { callexpr, ok := expr.(*build.CallExpr) if !ok { return nil, false } name, ok := callexpr.X.(*build.Ident) if !ok || name.Name != "http_archive" { return nil, false } return callexpr, true } func parseString(expr build.Expr) (string, error) { str, ok := expr.(*build.StringExpr) if !ok { return "", fmt.Errorf("expected string as the function argument") } return str.Value, nil } func parseSingleElementList(expr build.Expr) (string, error) { list, ok := expr.(*build.ListExpr) if !ok { return "", fmt.Errorf("expected a list as the function argument") } if len(list.List) != 1 { return "", fmt.Errorf("expected a single-element list as the function argument, got %d elements", len(list.List)) } return parseString(list.List[0]) } // ParseHTTPArchiveRule parses the provided http_archive rule and returns all of the dependency metadata embedded. func ParseHTTPArchiveRule(callexpr *build.CallExpr) (*Entry, error) { result := Entry{} for _, arg := range callexpr.List { assign, ok := arg.(*build.AssignExpr) if !ok { return nil, fmt.Errorf("a non-named argument passed as a function parameter") } argname, _ := build.GetParamName(assign.LHS) var err error = nil switch argname { case "name": result.Name, err = parseString(assign.RHS) case "sha256": result.SHA256, err = parseString(assign.RHS) if len(assign.Comments.Suffix) != 1 { return nil, fmt.Errorf("missing the \"Last updated\" comment on the sha256 field") } comment := assign.Comments.Suffix[0].Token match := lastUpdatedRE.FindStringSubmatch(comment) if match == nil { return nil, fmt.Errorf("unable to parse the \"Last updated\" comment, comment value: %s", comment) } result.LastUpdated = match[1] case "strip_prefix": result.Prefix, err = parseString(assign.RHS) case "urls": result.URL, err = parseSingleElementList(assign.RHS) default: continue } if err != nil { return nil, err } } if result.Name == "" { return nil, fmt.Errorf("missing the name field") } if result.SHA256 == "" { return nil, fmt.Errorf("missing the sha256 field") } if result.URL == "" { return nil, fmt.Errorf("missing the urls field") } return &result, nil } // ParseHTTPArchiveRules parses the entire WORKSPACE.bazel file and returns all of the http_archive rules in it. func ParseHTTPArchiveRules(source []byte) ([]*Entry, error) { file, err := build.ParseWorkspace("WORKSPACE.bazel", source) if err != nil { return []*Entry{}, err } result := make([]*Entry, 0) for _, expr := range file.Stmt { callexpr, ok := HTTPArchiveRule(expr) if !ok { continue } parsed, err := ParseHTTPArchiveRule(callexpr) if err != nil { return []*Entry{}, err } result = append(result, parsed) } return result, nil }