build/toolbase: init
In an effort to better the developer/CI experience, I'm moving some of
our presubmit checks into a Go tool. This is a helper library that will
be used to interact with a Monogon workspace checkout from Go, both in
the new presubmit tool but also any other future tools that would like
to operate on source code.
Change-Id: Ie5f1b1d0153a1c853c241e167d2d3a469c636c94
Reviewed-on: https://review.monogon.dev/c/monogon/+/328
Reviewed-by: Mateusz Zalega <mateusz@monogon.tech>
diff --git a/build/toolbase/label.go b/build/toolbase/label.go
new file mode 100644
index 0000000..b6e8fda
--- /dev/null
+++ b/build/toolbase/label.go
@@ -0,0 +1,87 @@
+package toolbase
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+)
+
+// BazelLabel is a label, as defined by Bazel's documentation:
+//
+// https://docs.bazel.build/versions/main/skylark/lib/Label.html
+type BazelLabel struct {
+ WorkspaceName string
+ PackagePath []string
+ Name string
+}
+
+func (b BazelLabel) Package() string {
+ return strings.Join(b.PackagePath, "/")
+}
+
+func (b BazelLabel) String() string {
+ return fmt.Sprintf("@%s//%s:%s", b.WorkspaceName, b.Package(), b.Name)
+}
+
+var (
+ // reLabel splits a Bazel label into a workspace name (if set) and a
+ // workspace root relative package/name.
+ reLabel = regexp.MustCompile(`^(@[^:/]+)?//(.+)$`)
+ // rePathPart matches valid label path parts.
+ rePathPart = regexp.MustCompile(`^[^:/]+$`)
+)
+
+// ParseBazelLabel converts parses a string representation of a Bazel Label. If
+// the given representation is invalid or for some other reason unparseable,
+// nil is returned.
+func ParseBazelLabel(s string) *BazelLabel {
+ res := BazelLabel{
+ WorkspaceName: "dev_source_monogon",
+ }
+
+ // Split label into workspace name (if set) and a workspace root relative
+ // package/name.
+ m := reLabel.FindStringSubmatch(s)
+ if m == nil {
+ return nil
+ }
+ packageRel := m[2]
+ if m[1] != "" {
+ res.WorkspaceName = m[1][1:]
+ }
+
+ // Split path by ':', which is the target name delimiter. If it appears
+ // exactly once, interpret everything to its right as the target name.
+ targetSplit := strings.Split(packageRel, ":")
+ switch len(targetSplit) {
+ case 1:
+ case 2:
+ packageRel = targetSplit[0]
+ res.Name = targetSplit[1]
+ if !rePathPart.MatchString(res.Name) {
+ return nil
+ }
+ default:
+ return nil
+ }
+
+ // Split the package path by /, and if the name was not explicitly given,
+ // use the last element of the package path.
+ if packageRel == "" {
+ res.PackagePath = nil
+ } else {
+ res.PackagePath = strings.Split(packageRel, "/")
+ }
+ if res.Name == "" {
+ res.Name = res.PackagePath[len(res.PackagePath)-1]
+ }
+
+ // Ensure all parts of the package path are valid.
+ for _, p := range res.PackagePath {
+ if !rePathPart.MatchString(p) {
+ return nil
+ }
+ }
+
+ return &res
+}