From da3f19713b91614a11fa8b8bd49c603c241d0f12 Mon Sep 17 00:00:00 2001 From: Dan Fuhry Date: Sat, 14 Mar 2026 19:26:44 -0400 Subject: [PATCH] [constants] use utils/subst library to generate constants --- bazel/subst/BUILD.bazel | 15 +++ bazel/subst/main.go | 38 +++++++ constants/BUILD.bazel | 17 +-- constants/constants_in.go | 24 ---- constants/generate/BUILD.bazel | 14 +++ constants/generate/main.go | 183 +++++++++++++++++++++++++++++++ utils/subst/BUILD.bazel | 9 ++ utils/subst/subst.go | 193 +++++++++++++++++++++++++++++++++ 8 files changed, 457 insertions(+), 36 deletions(-) create mode 100644 bazel/subst/BUILD.bazel create mode 100644 bazel/subst/main.go delete mode 100644 constants/constants_in.go create mode 100644 constants/generate/BUILD.bazel create mode 100644 constants/generate/main.go create mode 100644 utils/subst/BUILD.bazel create mode 100644 utils/subst/subst.go diff --git a/bazel/subst/BUILD.bazel b/bazel/subst/BUILD.bazel new file mode 100644 index 0000000..9bad5cd --- /dev/null +++ b/bazel/subst/BUILD.bazel @@ -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 index 0000000..1f5d113 --- /dev/null +++ b/bazel/subst/main.go @@ -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) +} diff --git a/constants/BUILD.bazel b/constants/BUILD.bazel index a474c1f..e1c0ac0 100644 --- a/constants/BUILD.bazel +++ b/constants/BUILD.bazel @@ -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 index 5bf1938..0000000 --- a/constants/constants_in.go +++ /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 index 0000000..ef577c4 --- /dev/null +++ b/constants/generate/BUILD.bazel @@ -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 index 0000000..ea77551 --- /dev/null +++ b/constants/generate/main.go @@ -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 index 0000000..f795b73 --- /dev/null +++ b/utils/subst/BUILD.bazel @@ -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 index 0000000..758888b --- /dev/null +++ b/utils/subst/subst.go @@ -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 + } +} -- 2.52.0