Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ ci-build: install proto http-api-docs
install: grpc-install api-linter-install buf-install

# Run all linters and compile proto files.
proto: grpc http-api-docs nexus-rpc-yaml
proto: grpc http-api-docs nexus-rpc-yaml system-nexus-wit
########################################################################

##### Variables ######
Expand All @@ -17,7 +17,9 @@ GOPATH := $(shell go env GOPATH)
endif

GOBIN := $(if $(shell go env GOBIN),$(shell go env GOBIN),$(GOPATH)/bin)
PATH := $(GOBIN):$(PATH)
CARGO_HOME ?= $(HOME)/.cargo
CARGO_BIN := $(CARGO_HOME)/bin
PATH := $(GOBIN):$(CARGO_BIN):$(PATH)
STAMPDIR := .stamp

COLOR := "\e[1;36m%s\e[0m\n"
Expand All @@ -33,6 +35,8 @@ PROTO_PATHS = paths=source_relative:$(PROTO_OUT)
OAPI_OUT := openapi
OAPI3_PATH := .components.schemas.Payload

NEX_GEN ?= nex-gen

$(PROTO_OUT):
mkdir $(PROTO_OUT)

Expand Down Expand Up @@ -137,6 +141,24 @@ nexus-rpc-yaml-install:
printf $(COLOR) "Build and install protoc-gen-nexus-rpc-yaml..."
@cd cmd/protoc-gen-nexus-rpc-yaml && go install .

##### Compile system Nexus WIT files #####
system-nexus-wit: system-nexus-wit-install nex-gen-install
printf $(COLOR) "Generate system Nexus WIT..."
protoc -I $(PROTO_ROOT) \
--system-nexus-wit_opt=output=nexus/workflow-service.wit \
--system-nexus-wit_opt=nex_gen=$(NEX_GEN) \
--system-nexus-wit_opt=linked_input=nexus/deps \
--system-nexus-wit_out=. \
temporal/api/workflowservice/v1/service.proto

system-nexus-wit-install:
printf $(COLOR) "Build and install protoc-gen-system-nexus-wit..."
@cd cmd/protoc-gen-system-nexus-wit && go install .

nex-gen-install:
printf $(COLOR) "Install nex-gen if missing..."
command -v $(NEX_GEN) >/dev/null || cargo install nex-gen

##### Clean #####
clean:
printf $(COLOR) "Delete generated go files..."
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Install as git submodule to the project.
## Contribution

Make your change to the temporal/proto files, and run `make` to update the openapi definitions.
Rust is also required because `make` installs and runs `nex-gen` when regenerating system Nexus WIT files.

## Breaking changes

Expand Down
187 changes: 187 additions & 0 deletions cmd/protoc-gen-system-nexus-wit/generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package main

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"

nexusannotationsv1 "github.com/nexus-rpc/nexus-proto-annotations/go/nexusannotations/v1"
"google.golang.org/protobuf/compiler/protogen"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/descriptorpb"
)

type params struct {
nexGen string
output string
input string
linkedInputs []string
}

// parseParams parses the comma-separated key=value parameter string provided by protoc.
//
// - output: required. Path to the WIT file to generate, relative to the
// --system-nexus-wit_out directory. Example: "nexus/temporal-system.wit".
//
// - nex_gen: optional. Path to the nex-gen binary. Defaults to "nex-gen".
//
// - linked_input: optional, repeatable. Additional WIT input passed to
// nex-gen after the main input.
//
// - input: optional. Existing WIT file to update. Defaults to output, so
// existing handwritten annotations and type refinements are preserved when
// regenerating in place.
func parseParams(raw string) (params, error) {
p := params{
nexGen: "nex-gen",
}
if raw == "" {
return p, nil
}
for kv := range strings.SplitSeq(raw, ",") {
key, value, ok := strings.Cut(kv, "=")
if !ok {
return p, fmt.Errorf("invalid parameter %q: expected key=value", kv)
}
switch key {
case "nex_gen":
p.nexGen = value
case "output":
p.output = value
case "input":
p.input = value
case "linked_input":
p.linkedInputs = append(p.linkedInputs, value)
default:
return p, fmt.Errorf("unknown parameter %q", key)
}
}
return p, nil
}

func generate(gen *protogen.Plugin) error {
p, err := parseParams(gen.Request.GetParameter())
if err != nil {
return err
}
if p.output == "" {
return fmt.Errorf("missing required output parameter")
}
if p.input == "" {
p.input = p.output
}

rpcs := exposedRPCs(gen)
if len(rpcs) == 0 {
return fmt.Errorf("no proto RPCs are marked as exposed Nexus operations")
}

tempDir, err := os.MkdirTemp("", "system-nexus-wit-*")
if err != nil {
return err
}
defer os.RemoveAll(tempDir)

descriptorPath := filepath.Join(tempDir, "temporal_api.bin")
if err := writeDescriptorSet(gen, descriptorPath); err != nil {
return err
}

tempOutput := filepath.Join(tempDir, "system-nexus.wit")
input := ""
if _, err := os.Stat(p.input); err == nil {
if err := copyFile(p.input, tempOutput); err != nil {
return err
}
input = tempOutput
} else if !os.IsNotExist(err) {
return err
}

for _, rpc := range rpcs {
if err := runAddRPC(p.nexGen, descriptorPath, rpc, tempOutput, input, p.linkedInputs); err != nil {
return err
}
input = tempOutput
}

content, err := os.ReadFile(tempOutput)
if err != nil {
return err
}
_, err = gen.NewGeneratedFile(p.output, "").Write(content)
return err
}

func exposedRPCs(gen *protogen.Plugin) []string {
var rpcs []string
for _, f := range gen.Files {
if !f.Generate {
continue
}
for _, svc := range f.Services {
for _, m := range svc.Methods {
if isExposedOperation(m) {
rpcs = append(rpcs, string(m.Desc.FullName()))
}
}
}
}
return rpcs
}

func isExposedOperation(m *protogen.Method) bool {
opts, ok := m.Desc.Options().(*descriptorpb.MethodOptions)
if !ok || opts == nil {
return false
}
if !proto.HasExtension(opts, nexusannotationsv1.E_Operation) {
return false
}
tags := proto.GetExtension(opts, nexusannotationsv1.E_Operation).(*nexusannotationsv1.OperationOptions).GetTags()
return slices.Contains(tags, "exposed")
}

func writeDescriptorSet(gen *protogen.Plugin, descriptorPath string) error {
data, err := proto.Marshal(&descriptorpb.FileDescriptorSet{
File: gen.Request.GetProtoFile(),
})
if err != nil {
return err
}
return os.WriteFile(descriptorPath, data, 0o644)
}

func runAddRPC(nexGen string, descriptors string, rpc string, output string, input string, linkedInputs []string) error {
args := []string{
"add-rpc",
"--descriptors", descriptors,
"--rpc", rpc,
"--output", output,
}
if input != "" {
args = append(args, "--input", input)
}
for _, linkedInput := range linkedInputs {
args = append(args, "--input", linkedInput)
}

command := exec.Command(nexGen, args...)
command.Stdout = os.Stdout
command.Stderr = os.Stderr
if err := command.Run(); err != nil {
return fmt.Errorf("%s %s: %w", nexGen, strings.Join(args, " "), err)
}
return nil
}

func copyFile(source string, destination string) error {
content, err := os.ReadFile(source)
if err != nil {
return err
}
return os.WriteFile(destination, content, 0o644)
}
8 changes: 8 additions & 0 deletions cmd/protoc-gen-system-nexus-wit/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/temporalio/api/cmd/protoc-gen-system-nexus-wit

go 1.25.4

require (
github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84
google.golang.org/protobuf v1.36.1
)
8 changes: 8 additions & 0 deletions cmd/protoc-gen-system-nexus-wit/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84 h1:SWHt3Coj0VvF0Km1A0wlY+IjnHKsjQLgO29io84r3wY=
github.com/nexus-rpc/nexus-proto-annotations v0.0.0-20260330194009-e558d6edaf84/go.mod h1:n3UjF1bPCW8llR8tHvbxJ+27yPWrhpo8w/Yg1IOuY0Y=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
11 changes: 11 additions & 0 deletions cmd/protoc-gen-system-nexus-wit/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// protoc-gen-system-nexus-wit generates Nexus WIT from proto service
// methods annotated with option (nexusannotations.v1.operation).tags = "exposed".
package main

import "google.golang.org/protobuf/compiler/protogen"

func main() {
protogen.Options{}.Run(func(gen *protogen.Plugin) error {
return generate(gen)
})
}
Loading
Loading