From 37f90deadb9e0efb64c110bf764ba938b7a35d5d Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:32:25 -0700 Subject: [PATCH] fix: make GRPC.Stub generation opt-out for server-only protoc runs --- grpc_server/lib/grpc/protoc/cli.ex | 44 +++++++++++- .../lib/grpc/protoc/generator/service.ex | 1 + grpc_server/priv/templates/service.ex.eex | 2 + .../grpc/protoc/generator/service_test.exs | 67 +++++++++++++++++++ 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 grpc_server/test/grpc/protoc/generator/service_test.exs diff --git a/grpc_server/lib/grpc/protoc/cli.ex b/grpc_server/lib/grpc/protoc/cli.ex index d673d7d2f..e17894d3e 100644 --- a/grpc_server/lib/grpc/protoc/cli.ex +++ b/grpc_server/lib/grpc/protoc/cli.ex @@ -20,6 +20,8 @@ defmodule GRPC.Protoc.CLI do alias Protobuf.Protoc.Context + @gen_stubs_disabled_plugin "__grpc_server_gen_stubs_disabled__" + # Entrypoint for the escript (protoc-gen-elixir). @doc false def main(args) @@ -74,6 +76,11 @@ defmodule GRPC.Protoc.CLI do Google.Protobuf.Compiler.CodeGeneratorResponse.Feature.value(:FEATURE_PROTO3_OPTIONAL) end + @doc false + def gen_stubs?(%Context{plugins: plugins}) do + @gen_stubs_disabled_plugin not in plugins + end + # Made public for testing. @doc false def parse_params(%Context{} = ctx, params_str) when is_binary(params_str) do @@ -83,7 +90,16 @@ defmodule GRPC.Protoc.CLI do end defp parse_param("plugins=" <> plugins, %Context{} = ctx) do - %Context{ctx | plugins: String.split(plugins, "+")} + plugins = String.split(plugins, "+") + + plugins = + if gen_stubs?(ctx) do + plugins + else + [@gen_stubs_disabled_plugin | plugins] + end + + %Context{ctx | plugins: plugins} end defp parse_param("gen_descriptors=" <> value, %Context{} = ctx) do @@ -96,6 +112,19 @@ defmodule GRPC.Protoc.CLI do end end + defp parse_param("gen_stubs=" <> value, %Context{} = ctx) do + case value do + "true" -> + put_gen_stubs(ctx, true) + + "false" -> + put_gen_stubs(ctx, false) + + other -> + raise "invalid value for gen_stubs option, expected \"true\" or \"false\", got: #{inspect(other)}" + end + end + defp parse_param("package_prefix=" <> package, %Context{} = ctx) do if package == "" do raise "package_prefix can't be empty" @@ -132,6 +161,19 @@ defmodule GRPC.Protoc.CLI do ctx end + defp put_gen_stubs(%Context{} = ctx, true) do + %Context{ctx | plugins: List.delete(ctx.plugins, @gen_stubs_disabled_plugin)} + end + + defp put_gen_stubs(%Context{} = ctx, false) do + plugins = + ctx.plugins + |> List.delete(@gen_stubs_disabled_plugin) + |> then(&[@gen_stubs_disabled_plugin | &1]) + + %Context{ctx | plugins: plugins} + end + # Made public for testing. @doc false def find_types(%Context{} = ctx, descs, files_to_generate) diff --git a/grpc_server/lib/grpc/protoc/generator/service.ex b/grpc_server/lib/grpc/protoc/generator/service.ex index 68902d7c2..4cedd73b7 100644 --- a/grpc_server/lib/grpc/protoc/generator/service.ex +++ b/grpc_server/lib/grpc/protoc/generator/service.ex @@ -34,6 +34,7 @@ defmodule GRPC.Protoc.Generator.Service do methods: methods, descriptor_fun_body: descriptor_fun_body, version: Util.version(), + gen_stubs?: GRPC.Protoc.CLI.gen_stubs?(ctx), module_doc?: false ) )} diff --git a/grpc_server/priv/templates/service.ex.eex b/grpc_server/priv/templates/service.ex.eex index bbea50b62..63a0a79e5 100644 --- a/grpc_server/priv/templates/service.ex.eex +++ b/grpc_server/priv/templates/service.ex.eex @@ -16,9 +16,11 @@ defmodule <%= @module %>.Service do <% end %> end +<%= if @gen_stubs? do %> defmodule <%= @module %>.Stub do <%= unless @module_doc? do %> @moduledoc false <% end %> use GRPC.Stub, service: <%= @module %>.Service end +<% end %> diff --git a/grpc_server/test/grpc/protoc/generator/service_test.exs b/grpc_server/test/grpc/protoc/generator/service_test.exs new file mode 100644 index 000000000..beb1aeb07 --- /dev/null +++ b/grpc_server/test/grpc/protoc/generator/service_test.exs @@ -0,0 +1,67 @@ +defmodule GRPC.Protoc.Generator.ServiceTest do + use ExUnit.Case, async: true + + alias GRPC.Protoc.CLI + alias GRPC.Protoc.Generator.Service + alias Protobuf.Protoc.Context + + describe "generate/2" do + test "generates service and stub modules by default" do + {_module, content} = Service.generate(context(), service()) + + assert content =~ "defmodule Helloworld.Greeter.Service do" + assert content =~ "defmodule Helloworld.Greeter.Stub do" + assert content =~ "use GRPC.Stub, service: Helloworld.Greeter.Service" + end + + test "generates service module without stub when gen_stubs=false" do + ctx = CLI.parse_params(context(), "gen_stubs=false") + + {_module, content} = Service.generate(ctx, service()) + + assert content =~ "defmodule Helloworld.Greeter.Service do" + refute content =~ "defmodule Helloworld.Greeter.Stub do" + refute content =~ "use GRPC.Stub" + end + end + + describe "parse_params/2" do + test "accepts gen_stubs=true and gen_stubs=false" do + assert CLI.gen_stubs?(CLI.parse_params(context(), "gen_stubs=true")) + refute CLI.gen_stubs?(CLI.parse_params(context(), "gen_stubs=false")) + refute CLI.gen_stubs?(CLI.parse_params(context(), "gen_stubs=false,plugins=grpc")) + end + + test "raises on invalid gen_stubs values" do + assert_raise RuntimeError, + ~s(invalid value for gen_stubs option, expected "true" or "false", got: "maybe"), + fn -> + CLI.parse_params(context(), "gen_stubs=maybe") + end + end + end + + defp context do + %Context{ + package: "helloworld", + dep_type_mapping: %{ + ".helloworld.HelloRequest" => %{type_name: "Helloworld.HelloRequest"}, + ".helloworld.HelloReply" => %{type_name: "Helloworld.HelloReply"} + } + } + end + + defp service do + %Google.Protobuf.ServiceDescriptorProto{ + name: "Greeter", + method: [ + %Google.Protobuf.MethodDescriptorProto{ + name: "SayHello", + input_type: ".helloworld.HelloRequest", + output_type: ".helloworld.HelloReply", + options: %Google.Protobuf.MethodOptions{} + } + ] + } + end +end