--- /dev/null
+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"],
+)
--- /dev/null
+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)
+}
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(
+++ /dev/null
-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"
-)
--- /dev/null
+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"],
+)
--- /dev/null
+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())
+}
--- /dev/null
+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"],
+)
--- /dev/null
+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
+ }
+}