| package bazel |
| |
| import ( |
| "encoding/json" |
| "strings" |
| |
| "github.com/go-python/gpython/ast" |
| "github.com/go-python/gpython/parser" |
| "go.skia.org/infra/go/skerr" |
| ) |
| |
| // IsBazelFile returns true if the filename looks like a Bazel file. |
| func IsBazelFile(file string) bool { |
| return strings.HasSuffix(file, "WORKSPACE") || |
| strings.HasSuffix(file, ".bazel") || |
| strings.HasSuffix(file, ".bzl") |
| } |
| |
| // DependencyID represents the unique identifier for a dependency. |
| type DependencyID string |
| |
| // Dependency represents one dependency version pin. |
| type Dependency struct { |
| ID DependencyID |
| Version string |
| versionPos *ast.Pos |
| SHA256 string |
| sha256Pos *ast.Pos |
| } |
| |
| // Validate returns an error if the Dependency is not valid. |
| func (d Dependency) Validate() error { |
| if d.ID == "" { |
| return skerr.Fmt("ID is unset") |
| } |
| if d.Version == "" { |
| return skerr.Fmt("Version is unset") |
| } |
| if d.versionPos == nil { |
| return skerr.Fmt("versionPos is unset") |
| } |
| if d.SHA256 == "" { |
| return skerr.Fmt("SHA256 is unset") |
| } |
| if d.sha256Pos == nil { |
| return skerr.Fmt("sha256Pos is unset") |
| } |
| return nil |
| } |
| |
| // GetDep parses the file contents and returns the given dependency. |
| func GetDep(content string, dep DependencyID) (Dependency, error) { |
| entries, err := ParseDeps(content) |
| if err != nil { |
| return Dependency{}, skerr.Wrap(err) |
| } |
| entry, ok := entries[dep] |
| if !ok { |
| b, err := json.MarshalIndent(entries, "", " ") |
| if err == nil { |
| return Dependency{}, skerr.Fmt("Cannot find item with id=%q from these entries:\n%s", dep, string(b)) |
| } else { |
| return Dependency{}, skerr.Fmt("Unable to find %q! Failed to encode entries with: %s", dep, err) |
| } |
| } |
| return entry, nil |
| } |
| |
| // SetDep parses the file contents, updates the version of the given |
| // dependency, and returns the new file contents or any error that occurred. |
| func SetDep(content string, id DependencyID, version, sha256 string) (string, error) { |
| if version == "" || sha256 == "" { |
| return "", skerr.Fmt("version and sha256 are required") |
| } |
| |
| // Parse the file content. |
| entries, err := ParseDeps(content) |
| if err != nil { |
| return "", skerr.Wrap(err) |
| } |
| |
| // Find the requested dependency. |
| dep, ok := entries[id] |
| if !ok { |
| return "", skerr.Fmt("Failed to find dependency with id %q", id) |
| } |
| |
| // Replace the old version with the new. |
| lines := strings.Split(content, "\n") |
| updateDep(lines, dep.versionPos, dep.Version, version) |
| updateDep(lines, dep.sha256Pos, dep.SHA256, sha256) |
| return strings.Join(lines, "\n"), nil |
| } |
| |
| // updateDep updates a dependency version in the given file content lines. |
| func updateDep(contentLines []string, pos *ast.Pos, old, new string) { |
| lineIdx := pos.Lineno - 1 // Lineno starts at 1. |
| line := contentLines[lineIdx] |
| newLine := line[:pos.ColOffset] + strings.Replace(line[pos.ColOffset:], old, new, 1) |
| contentLines[lineIdx] = newLine |
| } |
| |
| // ParseDeps parses the file contents and returns all dependencies, keyed by ID. |
| func ParseDeps(content string) (map[DependencyID]Dependency, error) { |
| parsed, err := parser.ParseString(content, "exec") |
| if err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| // Loop through all top-level statements in the file, collecting |
| // dependencies and the positions in the file where their versions are set. |
| deps := map[DependencyID]Dependency{} |
| for _, stmt := range parsed.(*ast.Module).Body { |
| // The dependencies we're looking for are function calls, each of which |
| // lives inside an ExprStmt. We ignore all other types of Stmt. |
| // |
| // For example: |
| // |
| // # This is a top-level expression statement, which is a function call: |
| // container_pull( |
| // name = "empty_container", |
| // digest = "sha256:1e014f84205d569a5cc3be4e108ca614055f7e21d11928946113ab3f36054801", |
| // registry = "index.docker.io", |
| // repository = "alpine", |
| // ) |
| // |
| // # This is a top-level assignment statement. It will be ignored: |
| // PROTOC_BUILD_FILE_CONTENT = """ |
| // exports_files(["bin/protoc"], visibility = ["//visibility:public"]) |
| // """ |
| if stmt.Type().Name == ast.ExprStmtType.Name { |
| exprStmt := stmt.(*ast.ExprStmt).Value |
| if exprStmt.Type().Name == ast.CallType.Name { |
| dep, foundDep, err := parseCall(exprStmt.(*ast.Call)) |
| if err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| if foundDep { |
| if err := dep.Validate(); err != nil { |
| return nil, skerr.Wrap(err) |
| } |
| if _, ok := deps[dep.ID]; ok { |
| return nil, skerr.Fmt("found multiple entries for %q", dep.ID) |
| } |
| deps[dep.ID] = dep |
| } |
| } |
| } |
| } |
| return deps, nil |
| } |
| |
| // parseCall parses a Call to return a dependency and the position where its |
| // version is defined, if a dependency can be found. The boolean return value |
| // indicates whether a dependency was found. |
| func parseCall(call *ast.Call) (Dependency, bool, error) { |
| funcName := string(call.Func.(*ast.Name).Id) |
| parseFn := map[string]func(*ast.Call) (Dependency, error){ |
| "cipd_install": parseDepFromCallFunc("cipd_package", "tag"), |
| "container_pull": parseContainerPull, |
| }[funcName] |
| if parseFn != nil { |
| dep, err := parseFn(call) |
| if err != nil { |
| return Dependency{}, false, skerr.Wrapf(err, "parsing %q call at line %d", funcName, call.Func.(*ast.Name).Lineno) |
| } |
| return dep, true, nil |
| } |
| return Dependency{}, false, nil |
| } |
| |
| // parseDepFromCall is a helper function used to extract a dependency from a |
| // Call using the given idKeyword and versionKeyword. |
| func parseDepFromCallFunc(idKeyword, versionKeyword string) func(*ast.Call) (Dependency, error) { |
| return func(call *ast.Call) (Dependency, error) { |
| id, _, err := getCallArgValueString(call, idKeyword) |
| if err != nil { |
| return Dependency{}, skerr.Wrap(err) |
| } |
| version, versionPos, err := getCallArgValueString(call, versionKeyword) |
| if err != nil { |
| return Dependency{}, skerr.Wrap(err) |
| } |
| sha256, sha256Pos, err := getCallArgValueString(call, "sha256") |
| if err != nil { |
| return Dependency{}, skerr.Wrap(err) |
| } |
| return Dependency{ |
| ID: DependencyID(id), |
| Version: version, |
| versionPos: versionPos, |
| SHA256: sha256, |
| sha256Pos: sha256Pos, |
| }, nil |
| } |
| } |
| |
| // parseContainerPull parses a call to container_pull to return a dependency and |
| // the position where the version is defined. |
| func parseContainerPull(call *ast.Call) (Dependency, error) { |
| registry, _, err := getCallArgValueString(call, "registry") |
| if err != nil { |
| return Dependency{}, skerr.Wrap(err) |
| } |
| repository, _, err := getCallArgValueString(call, "repository") |
| if err != nil { |
| return Dependency{}, skerr.Wrap(err) |
| } |
| id := registry + "/" + repository |
| digest, digestPos, err := getCallArgValueString(call, "digest") |
| if err != nil { |
| return Dependency{}, skerr.Wrap(err) |
| } |
| return Dependency{ |
| ID: DependencyID(id), |
| Version: digest, |
| versionPos: digestPos, |
| SHA256: digest, |
| sha256Pos: digestPos, |
| }, nil |
| } |
| |
| // getCallArgValueString searches the keyword arguments of the call for one with |
| // the matching keyword. If found, it returns the string value of the argument, |
| // and the position of that string value. This only works for string literals. |
| func getCallArgValueString(call *ast.Call, keyword string) (string, *ast.Pos, error) { |
| for _, kw := range call.Keywords { |
| if string(kw.Arg) == keyword { |
| if kw.Value.Type().Name == ast.StrType.Name { |
| rv := string(kw.Value.(*ast.Str).S) |
| if rv == "" { |
| return "", nil, skerr.Fmt("found empty string for argument %q", keyword) |
| } |
| return rv, &ast.Pos{ |
| Lineno: kw.Value.GetLineno(), |
| ColOffset: kw.Value.GetColOffset(), |
| }, nil |
| } |
| } |
| } |
| return "", nil, skerr.Fmt("no keyword argument %q found for call", keyword) |
| } |