]> go.fuhry.dev Git - runtime.git/commitdiff
[constants] use utils/subst library to generate constants
authorDan Fuhry <dan@fuhry.com>
Sat, 14 Mar 2026 23:26:44 +0000 (19:26 -0400)
committerDan Fuhry <dan@fuhry.com>
Sun, 15 Mar 2026 01:17:10 +0000 (21:17 -0400)
bazel/subst/BUILD.bazel [new file with mode: 0644]
bazel/subst/main.go [new file with mode: 0644]
constants/BUILD.bazel
constants/constants_in.go [deleted file]
constants/generate/BUILD.bazel [new file with mode: 0644]
constants/generate/main.go [new file with mode: 0644]
utils/subst/BUILD.bazel [new file with mode: 0644]
utils/subst/subst.go [new file with mode: 0644]

diff --git a/bazel/subst/BUILD.bazel b/bazel/subst/BUILD.bazel
new file mode 100644 (file)
index 0000000..9bad5cd
--- /dev/null
@@ -0,0 +1,15 @@
+load("@rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "subst_lib",
+    srcs = ["main.go"],
+    importpath = "go.fuhry.dev/runtime/bazel/subst",
+    visibility = ["//visibility:private"],
+    deps = ["//utils/subst"],
+)
+
+go_binary(
+    name = "subst",
+    embed = [":subst_lib"],
+    visibility = ["//visibility:public"],
+)
diff --git a/bazel/subst/main.go b/bazel/subst/main.go
new file mode 100644 (file)
index 0000000..1f5d113
--- /dev/null
@@ -0,0 +1,38 @@
+package main
+
+import (
+       "errors"
+       "flag"
+       "fmt"
+       "io"
+       "os"
+       "strings"
+
+       "go.fuhry.dev/runtime/utils/subst"
+)
+
+func main() {
+       kv := make(subst.KV)
+       addKv := func(flagVal string) error {
+               k, v, ok := strings.Cut(flagVal, "=")
+               if !ok {
+                       return errors.New("-var= usage: var=value, e.g. -var=FOO=BAR")
+               }
+               kv[k] = v
+               return nil
+       }
+       flag.Func("var", "extra variable to set", addKv)
+       flag.Parse()
+
+       stdin, err := io.ReadAll(os.Stdin)
+       if err != nil {
+               panic(err)
+       }
+
+       out, err := subst.Eval(kv, string(stdin))
+       if err != nil {
+               panic(err)
+       }
+
+       fmt.Print(out)
+}
index a474c1f4025c06d5bfc2c04fa5c834263557f3de..e1c0ac0381de99787421b2b91d6a84c4b5ed5902 100644 (file)
@@ -1,23 +1,16 @@
 load("@rules_go//go:def.bzl", "go_library")
 
 # Ignore this package in gazelle so constants_in.go is picked up by IDEs but not builds.
-# gazelle:exclude constants_in.go
+# gazelle:exclude constants_fake.go
 
 genrule(
     name = "constants_go",
-    srcs = ["constants_in.go"],
     outs = ["constants.go"],
-    cmd_bash = """
-    sedexp=()
-    while read line; do
-        varname="$$(cut -d\\  -f1 <<< "$$line")"
-        len=$$(( $${#varname} + 1 ))
-        varval="$${line:$$len}"
-        sedexp+=("-e" 's;\\$$'"$$varname;$$varval;g")
-    done < bazel-out/volatile-status.txt
-    sed -r "$${sedexp[@]}" < "$<" > "$@"
-    """,
+    cmd = "$(location //constants/generate:generate) > $@",
     stamp = 1,
+    tools = [
+        "//constants/generate",
+    ],
 )
 
 go_library(
diff --git a/constants/constants_in.go b/constants/constants_in.go
deleted file mode 100644 (file)
index 5bf1938..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-package constants
-
-const (
-       RootDomain        = "$ROOT_DOMAIN"
-       DefaultRegion     = "$DEFAULT_REGION"
-       DefaultHostDomain = "$DEFAULT_HOST_DOMAIN"
-       SDDomain          = "$SD_DOMAIN"
-       WebServicesDomain = "$WEB_SERVICES_DOMAIN"
-       MachinesHost      = "$MACHINES_HOST"
-       MachinesMqttTopic = "$MACHINES_MQTT_TOPIC"
-       DbusPrefix        = "$DBUS_PREFIX"
-       DbusPath          = "$DBUS_PATH"
-
-       OrgName       = "$ORG_NAME"
-       OrgSlug       = "$ORG_SLUG"
-       SystemConfDir = "$SYSTEM_CONF_DIR"
-
-       RootCAName           = "$ROOT_CA_NAME"
-       IntCAName            = "$INT_CA_NAME"
-       DeviceTrustTokenName = "$DEVICE_TRUST_TOKEN_NAME"
-       DeviceTrustPrincipal = "$DEVICE_TRUST_PRINCIPAL"
-
-       Version = "$VERSION"
-)
diff --git a/constants/generate/BUILD.bazel b/constants/generate/BUILD.bazel
new file mode 100644 (file)
index 0000000..ef577c4
--- /dev/null
@@ -0,0 +1,14 @@
+load("@rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "generate_lib",
+    srcs = ["main.go"],
+    importpath = "go.fuhry.dev/runtime/constants/generate",
+    visibility = ["//visibility:private"],
+)
+
+go_binary(
+    name = "generate",
+    embed = [":generate_lib"],
+    visibility = ["//visibility:public"],
+)
diff --git a/constants/generate/main.go b/constants/generate/main.go
new file mode 100644 (file)
index 0000000..ea77551
--- /dev/null
@@ -0,0 +1,183 @@
+package main
+
+import (
+       "fmt"
+       "os"
+       "path"
+       "strings"
+)
+
+type statusVar struct {
+       key, value string
+}
+
+var specialCases = map[string]string{
+       "ROOT_CA_NAME": "RootCAName",
+       "INT_CA_NAME":  "IntCAName",
+       "SD_DOMAIN":    "SDDomain",
+}
+
+func (s statusVar) CamelCase() string {
+       if c, ok := specialCases[s.key]; ok {
+               return c
+       }
+
+       out := ""
+       cap := true
+       for _, c := range s.key {
+               if c == '_' {
+                       cap = true
+               } else if cap {
+                       out += string(c)
+                       cap = false
+               } else {
+                       out += strings.ToLower(string(c))
+               }
+       }
+       return out
+}
+
+type statusCollection []statusVar
+
+func (s statusCollection) MaxKeyLen() int {
+       m := 0
+       for _, v := range s {
+               m = max(m, len(v.key))
+       }
+       return m
+}
+
+func (s statusCollection) AsConstants() string {
+       out := "const (\n"
+       for _, v := range s {
+               out += fmt.Sprintf("\t%s = %q\n", v.CamelCase(), v.value)
+       }
+       out += ")\n"
+       return out
+}
+
+func (s statusCollection) AsTypedConstants() string {
+       out := "type WorkspaceStatusVar uint\n\n"
+       out += "const (\n"
+       for i, v := range s {
+               if i == 0 {
+                       out += fmt.Sprintf("\tK%s WorkspaceStatusVar = iota\n", v.CamelCase())
+               } else {
+                       out += fmt.Sprintf("\tK%s\n", v.CamelCase())
+               }
+       }
+       out += ")\n"
+       return out
+}
+
+func (s statusCollection) AsAllFunc() string {
+       out := "func All() []WorkspaceStatusVar {\n"
+       out += "\treturn []WorkspaceStatusVar{\n"
+       for _, v := range s {
+               out += fmt.Sprintf("\t\tK%s,\n", v.CamelCase())
+       }
+       out += "\t}\n"
+       out += "}\n"
+
+       return out
+}
+
+func (s statusCollection) AsNameFunc() string {
+       out := "func (w WorkspaceStatusVar) Name() string {\n"
+       out += "\tswitch w {\n"
+       for _, v := range s {
+               out += fmt.Sprintf("\tcase K%s:\n", v.CamelCase())
+               out += fmt.Sprintf("\t\treturn %q\n", v.key)
+       }
+       out += "\tdefault:\n"
+       out += "\t\tpanic(\"invalid value for WorkspaceStatusVar\")\n"
+       out += "\t}\n"
+       out += "}\n"
+
+       return out
+}
+
+func (s statusCollection) AsCamelCaseNameFunc() string {
+       out := "func (w WorkspaceStatusVar) CamelCaseName() string {\n"
+       out += "\tswitch w {\n"
+       for _, v := range s {
+               out += fmt.Sprintf("\tcase K%s:\n", v.CamelCase())
+               out += fmt.Sprintf("\t\treturn %q\n", v.CamelCase())
+       }
+       out += "\tdefault:\n"
+       out += "\t\tpanic(\"invalid value for WorkspaceStatusVar\")\n"
+       out += "\t}\n"
+       out += "}\n"
+
+       return out
+}
+
+func (s statusCollection) AsValueFunc() string {
+       out := "func (w WorkspaceStatusVar) Value() string {\n"
+       out += "\tswitch w {\n"
+       for _, v := range s {
+               out += fmt.Sprintf("\tcase K%s:\n", v.CamelCase())
+               out += fmt.Sprintf("\t\treturn %q\n", v.value)
+       }
+       out += "\tdefault:\n"
+       out += "\t\tpanic(\"invalid value for WorkspaceStatusVar\")\n"
+       out += "\t}\n"
+       out += "}\n"
+
+       return out
+}
+
+func (s statusCollection) AsLookupFunc() string {
+       out := "func Lookup(name string) (WorkspaceStatusVar, bool) {\n"
+       out += "\tswitch name {\n"
+       for _, v := range s {
+               out += fmt.Sprintf("\tcase %q, %q:\n", v.key, v.CamelCase())
+               out += fmt.Sprintf("\t\treturn K%s, true\n", v.CamelCase())
+       }
+       out += "\tdefault:\n"
+       out += "\t\treturn WorkspaceStatusVar(0), false\n"
+       out += "\t}\n"
+       out += "}\n"
+
+       return out
+}
+
+func main() {
+       wd, err := os.Getwd()
+       if err != nil {
+               panic(err)
+       }
+
+       statusFile := path.Join(wd, "bazel-out", "volatile-status.txt")
+
+       statusContentsBytes, err := os.ReadFile(statusFile)
+       if err != nil {
+               panic(err)
+       }
+
+       statusContents := strings.Split(
+               strings.ReplaceAll(
+                       strings.TrimSpace(string(statusContentsBytes)),
+                       "\r\n", "\n",
+               ),
+               "\n",
+       )
+
+       var vars statusCollection
+
+       for _, line := range statusContents {
+               key, value, found := strings.Cut(line, " ")
+               if found {
+                       vars = append(vars, statusVar{key, value})
+               }
+       }
+
+       fmt.Printf("package constants\n\n")
+       fmt.Println(vars.AsConstants())
+       fmt.Println(vars.AsTypedConstants())
+       fmt.Println(vars.AsAllFunc())
+       fmt.Println(vars.AsNameFunc())
+       fmt.Println(vars.AsCamelCaseNameFunc())
+       fmt.Println(vars.AsValueFunc())
+       fmt.Println(vars.AsLookupFunc())
+}
diff --git a/utils/subst/BUILD.bazel b/utils/subst/BUILD.bazel
new file mode 100644 (file)
index 0000000..f795b73
--- /dev/null
@@ -0,0 +1,9 @@
+load("@rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "subst",
+    srcs = ["subst.go"],
+    importpath = "go.fuhry.dev/runtime/utils/subst",
+    visibility = ["//visibility:public"],
+    deps = ["//constants"],
+)
diff --git a/utils/subst/subst.go b/utils/subst/subst.go
new file mode 100644 (file)
index 0000000..758888b
--- /dev/null
@@ -0,0 +1,193 @@
+package subst
+
+import (
+       "errors"
+       "fmt"
+       "os"
+       "path"
+       "regexp"
+       "strings"
+
+       "go.fuhry.dev/runtime/constants"
+)
+
+type (
+       KV        map[string]string
+       substFunc = func(KV, string) (string, error)
+)
+
+var (
+       globals    = make(KV)
+       substFuncs = make(map[string]substFunc)
+       validKv    = regexp.MustCompile(`^[A-Za-z0-9_]+$`)
+)
+
+const (
+       substStart = `${`
+       substEnd   = `}`
+)
+
+func Eval(ctx KV, input string) (string, error) {
+       return substEval(ctx, input)
+}
+
+func AddFunction(name string, f substFunc) {
+       if _, ok := substFuncs[name]; ok {
+               panic(fmt.Sprintf("subst function %q already registered", name))
+       }
+
+       substFuncs[name] = f
+}
+
+func findSubsts(input, start, end string) (matches []string, err error) {
+       var depth, startPos int
+
+       for i := 0; i < len(input); i++ {
+               if input[i] == '\\' {
+                       i++
+                       continue
+               }
+
+               if i+len(start) <= len(input) && input[i:i+len(start)] == start {
+                       depth++
+                       if depth == 1 {
+                               startPos = i + len(start)
+                       }
+               } else if i+len(end) <= len(input) && input[i:i+len(end)] == end {
+                       depth--
+                       if depth == 0 {
+                               matches = append(matches, input[startPos:i])
+                       }
+               }
+       }
+
+       if depth > 0 {
+               return nil, fmt.Errorf("mismatched opening expression %q at pos %d", start, startPos)
+       } else if depth < 0 {
+               return nil, fmt.Errorf("mismatched closing expression %q", end)
+       }
+
+       return
+}
+
+func substEval(ctx KV, input string) (string, error) {
+       matches, err := findSubsts(input, substStart, substEnd)
+       if err != nil {
+               return "", err
+       }
+
+       for _, match := range matches {
+               token := substStart + match + substEnd
+               if varName, nullval, ok := strings.Cut(match, ":-"); ok && validKv.MatchString(varName) && varName != "nullor" {
+                       if varVal, ok := ctx[varName]; ok && varVal != "" {
+                               match, err = substEval(ctx, varVal)
+                       } else {
+                               match, err = substEval(ctx, nullval)
+                       }
+                       if err != nil {
+                               return "", err
+                       }
+               } else if before, after, ok := strings.Cut(match, ":"); ok {
+                       if fun, ok := substFuncs[before]; ok {
+                               var err error
+                               match, err = fun(ctx, after)
+                               if err != nil {
+                                       return "", fmt.Errorf(
+                                               "while evaluating sub-expression %q: %v", token, err)
+                               }
+                       } else {
+                               return "", fmt.Errorf("while evaluating sub-expression %q: no such sub-expression function %q", token, before)
+                       }
+               } else if ctxVar, ok := ctx[match]; ok {
+                       match = ctxVar
+               } else {
+                       return "", fmt.Errorf(
+                               "sub-expression %q isn't a valid expression", token)
+               }
+
+               input = strings.Replace(input, token, match, 1)
+       }
+
+       return input, nil
+}
+
+func init() {
+       substFuncs["g"] = func(ctx KV, subexpr string) (string, error) {
+               subexpr, err := substEval(ctx, subexpr)
+               if err != nil {
+                       return "", err
+               }
+
+               if out, ok := globals[subexpr]; !ok {
+                       return "", fmt.Errorf("undefined global: %q", subexpr)
+               } else {
+                       return out, nil
+               }
+       }
+
+       substFuncs["dirname"] = func(ctx KV, subexpr string) (string, error) {
+               if outStr, err := substEval(ctx, subexpr); err != nil {
+                       return "", err
+               } else {
+                       return path.Dir(outStr), nil
+               }
+       }
+
+       substFuncs["basename"] = func(ctx KV, subexpr string) (string, error) {
+               if outStr, err := substEval(ctx, subexpr); err != nil {
+                       return "", err
+               } else {
+                       return path.Base(outStr), nil
+               }
+       }
+
+       substFuncs["env"] = func(ctx KV, subexpr string) (string, error) {
+               if outStr, err := substEval(ctx, subexpr); err != nil {
+                       return "", err
+               } else {
+                       return os.Getenv(outStr), nil
+               }
+       }
+
+       substFuncs["const"] = func(ctx KV, subexpr string) (string, error) {
+               constVar, ok := constants.Lookup(subexpr)
+               if !ok {
+                       return "", fmt.Errorf("unknown runtime constant: %q", subexpr)
+               }
+               return constVar.Value(), nil
+       }
+
+       substFuncs["nullor"] = func(ctx KV, subexpr string) (string, error) {
+               before, after, ok := strings.Cut(subexpr, ":-")
+               if !ok {
+                       before = subexpr
+                       after = ""
+               }
+               if outStr, err := substEval(ctx, before); err != nil {
+                       return "", err
+               } else {
+                       if outStr != "" {
+                               return outStr, nil
+                       } else {
+                               return substEval(ctx, after)
+                       }
+               }
+       }
+
+       substFuncs["sub"] = func(ctx KV, subexpr string) (string, error) {
+               parts := strings.Split(subexpr, ",")
+               if len(parts) != 3 {
+                       return "", errors.New("sub expects 3 comma-separated args: haystack, needle, replacement")
+               }
+               haystack, needle, replacement := parts[0], parts[1], parts[2]
+               haystack, err := substEval(ctx, haystack)
+               if err != nil {
+                       return "", err
+               }
+               return strings.ReplaceAll(haystack, needle, replacement), nil
+       }
+
+       substFuncs["literal"] = func(ctx KV, subexpr string) (string, error) {
+               return subexpr, nil
+       }
+}