Protobuf: Rev of proto-gen-go to ProtoPackageIsVersion3 causing breakage

Created on 27 Nov 2018  Β·  60Comments  Β·  Source: golang/protobuf

I am hopeful that there is a simple solution to get around this issue :)

A few hours ago, this commit introduced proto.ProtoPackageIsVersion3

and set const generatedCodeVersion = 3

Our CI system (and I suspect many other peoples) do a:

go get github.com/golang/protobuf/protoc-gen-go

as part of setting up the system, and also do a:

go generate ./...

and check to make sure that regenerated files match the files in the commit (basically, checking for regen on updating the .proto files).

This has been going swimmingly until a few hours ago. In trying to debug this problem, I came to realize that the fact this worked at all is a testament to how good you guys are at backward compatibility πŸ‘

What I discovered digging in is that we have been vendoring github.com/golang/protobuf/ v1.2.0 (because of other dependencies we have), but of course go get pulls the latest from master.

v1.2.0 lacks proto.ProtoPackageIsVersion3, so our generated code suddenly doesn't compile.

So I fell back to using:

go install ./vendor/github.com/golang/protobuf/protoc-gen-go/

in our CI. Unfortunately, this seems to result in some generation differences. If I run it locally, push, and then our CI runs it, the generated code doesn't quite match.

I get diffs like:

-var fileDescriptor_networkservice_d4de7ff5f5769d53 = []byte{
+var fileDescriptor_networkservice_5b085879d0d87bdf = []byte{

I don't precisely consider this a bug on your end, you guys are super good about backward compatibility, and at some point you do have to make the change.

Do you have any ideas about how I might navigate all of this? I suspect lots and lots of folks will shortly have similar issues...

Most helpful comment

dep users should version their protoc-gen-go as a required binary, like so (in Gopkg.toml):

required = [
    "github.com/golang/protobuf/protoc-gen-go",
]

You can then install protoc-gen-go from vendor:

 $ go install ./vendor/github.com/golang/protobuf/protoc-gen-go

I'm not sure what the go modules equivalent workflow is.

All 60 comments

@dsnet do you have any insight?

I'm not sure what we can do on our end, this seems to be an issue really with the Go toolchain that there isn't an easy way (or at least I don't know of one) to build a binary at a specific revision.

For the time being, you can do something like this:

$ git clone [email protected]:golang/protobuf.git && (cd protobuf && git checkout v1.2.0 && go build -o $BIN_PATH/protoc-gen-go ./protoc-gen-go) && rm -r protobuf

Be sure to have $BIN_PATH be the output path where the binary should end up.

I'm going to close this as a duplicate of #751 since the general issue is that the instructions for how to install protoc-gen-go are not ideal. However, the broader issue at hand is https://github.com/golang/go/issues/24250

@dsnet Yeah... I can build the protoc-gen-go from my vendored dir (v1.2.0) but that version (unlike more recent ones) doesn’t seem to always generate the same go code for identical .proto.

At what point did proto-gen-go start generating identical go for identical proto?

The output of protoc-gen-go should be deterministic. If it isn't, then that's a bug.

That said, the most likely cause of non-deterministic output is due to how protoc is invoked. The descriptor protos that are embedded in each .pb.go file contains the filepath of the proto file and depending on the -I flags to protoc, it is possible that those change.

@dsnet Also, I don’t think is the same as #751 as it doesn’t have anything to do with modules and we are seeing it with go 1.10 (pre modules)

@dsnet OK, that’s super helpful to know. Are the determined by the fully qualified path of included proto files? I ask, because we also just started using -I ../.../vendor in our protoc invocations.

Which is to say, the fully qualified path of the imported protofiles (things like github.com/golang/protobufs/ptypes/empty/empty.proto) will different depending on where they are built, as we are pulling them in from vendor/ )

Errr, sorry, I mispoke. It's not the -I flag. It's the how the proto file to compile was specified when given to protoc.

For example:

$ protoc --go_out=paths=source_relative:. ptypes/any/any.proto
$ mv ptypes/any/any.pb.go ptypes/any/any1.pb.go
$ (cd ptypes && protoc --go_out=paths=source_relative:. any/any.proto)
$ mv ptypes/any/any.pb.go ptypes/any/any2.pb.go 
$ (cd ptypes/any && protoc --go_out=paths=source_relative:. any.proto)
$ mv ptypes/any/any.pb.go ptypes/any/any3.pb.go 
$ cat any*.pb.go | grep RegisterFile
func init() { proto.RegisterFile("ptypes/any/any.proto", fileDescriptor_any_fea4429e4270d3cd) }
func init() { proto.RegisterFile("any/any.proto", fileDescriptor_any_f2863f9fa4d3af49) }
func init() { proto.RegisterFile("any.proto", fileDescriptor_any_0a174ad2e4d7ef58) }

Notice that all three have different values?

This behavior is surprising, but is an artifact of how protobufs were originally designed. That is, they assume a single root where all source files universally lived under in a build system similar to Bazel. It's not great for today's world of independently distributed build processes, but such is the status quo.

OK.. so that’s a super helpful hint... let me go look for those differences and see if I can think of a solution.

@dsnet OK... so I can't quite square this with my understanding of your explanation:

-   proto.RegisterFile("networkservice.proto", fileDescriptor_networkservice_efee0c6a43fef699)
+   proto.RegisterFile("networkservice.proto", fileDescriptor_networkservice_1d42c3056c917b94)

So clearly something else is feeding into the hash. What versions of protoc-gen-go were used to produce those .pb.go files? Also what versions of the Go toolchain were used to build those binaries?

dep has our vendored version of github.com/golang/protobuf/protoc-gen-go/ at version v1.2.0.

we are building protoc-gen-go using:

go install ./vendor/github.com/golang/protobuf/protoc-gen-go/

So I'm going to go out on a limb and say its proto-gen-go version v1.2.0 (I couldn't find any way for the protoc-gen-go binary to output a version, would love to know if there's a trick there :) ).

go version yields go1.10.5 linux/amd64

And what are the versions going into the other protoc-gen-go binary being used?

The other go version is:
go version go1.11.2 darwin/amd64
(my dev box, where I did the gen I'm pushing to our CI).

OK... this is super weird.

I installed a downgraded go version on my laptop:
go version go1.10.5 darwin/amd64

and created a fresh new alternate GOPATH and cloned the PR that is failing our CI into it.
And ran
go generate ./...
(at top level, same as where the CI runs it).

And I am not getting different hashes. So I suspect that its not the location of the GOPATH, or the go version feeding into the hash difference... what else might it be?

It's not so much that it depends directly on the Go version, but rather parts of the standard library like compress/gzip, which does not have stable output between versions.

Ah... OK... that makes a lot of sense... hmm... I have now tried with downgrade to the same go version, but on the Mac, and seem to be OK in terms of the output not changing...

This is super weird. I appreciate your patience talking me through some of the ins and outs of how things work it makes it much much easier to figure out how to debug :)

Hi there, this issue hit me this morning. It was fine locally due to having an older version installed, but CI uses clean docker builds.

I switched to installing the vendored protoc-gen-go like @edwarnicke and all is well now!

The actual error I saw was

undefined: proto.ProtoPackageIsVersion3

Hi everyone,

I apologize that 8d0c54c1246661d9a51ca0ba455d22116d485eaa recently broke a lot of people's workflows. The breakage is unfortunate and several people have written to me personally requesting that we issue v1.3.0 of Go protobufs "to resolve the issue". However, I'm going to make the case that the problem at hand is not solved by any form of versioning that we do on golang/protobuf's end. Breakage is still going to happen (unless protoc-gen-go is unreasonably restricted from changing the internal implementation details of protobufs).

Fundamentally, a CI system must be hermetic such that testing a given commit of your project uses a consistent set of dependencies and provides repeatability. This implies that running the CI script on a specific version of your project today, last year, or 10 years from now should produce the exact same test results. CI scripts that hard-code a specific version of the proto package, but continues to obtain protoc-gen-go from HEAD is fundamentally not hermetic. (In fact, if any part of the CI script depends on anything from HEAD, then it is not hermetic). Breakages due to externally changing factors (e.g., 8d0c54c1246661d9a51ca0ba455d22116d485eaa ) is an implicit (and undesirable) property of non-hermetic builds.

Here are things that we can do to help mitigate, but not automatically resolve the issue:

  • Issue #751 should fix README.md to specify how to install a specific version of protoc-gen-go. Unfortunately, this is complicated by the fact that the Go toolchain currently doesn't support directly building at a specific version. See PR #764 for a workaround while golang/go#24250 (@bcmills) is still being addressed.
  • Issue #738 requests that we release pre-compiled binaries of protoc-gen-go, which would help make it easier to download and obtain a specific version of the tool in CI scripts.

Either way, if your CI system broke as a result of 8d0c54c1246661d9a51ca0ba455d22116d485eaa, then some form of change needs to happen to the CI script to ensure hermetic builds. This is not something that we can fix on golang/protobuf's end.

It is unfortunate that a change needs to be made in the user's codebase, but in this situation, I strongly believe it is actually going to put your CI script in a healthier situation.

As an example of a hermetic CI scripts, our own tests for the Go protobuf project hardcodes the exact versions of non-Go package dependencies for protoc and the go toolchain and the exact versions of Go dependencies using a go.mod file. Is it annoying to need to manually increment the versions? Yes, but well worth it in terms of guaranteeing hermetic builds.

I'd like to second @dsnet 's point about hermetic CI. I was frankly a bit embarrassed to discover that our CI was vulnerable to this kind of thing. It was vulnerable because we were go getting (which means the version we were getting tracked master). It's a testament to the overall stability of protoc-gen-go that it took us this long to hit any issues.

@dsnet Thanks for taking the time to talk through with me my hash issues... still trying to get to the bottom of them, but your insight helped a lot.

@sjdweb

Hi there, this issue hit me this morning. It was fine locally due to having an older version installed, but CI uses clean docker builds.

I switched to installing the vendored protoc-gen-go like @edwarnicke and all is well now!

The actual error I saw was

undefined: proto.ProtoPackageIsVersion3

Do keep an eye out for some of the other issues I've been seeing. I'm still uncertain as to whether the issues I'm hitting from downgrading to v1.2.0 are purely of my own making (most probable), but if downgrading from master to v1.2.0 is part of the cause, best to huddle together for mutual assistance in resolving them :)

I'm also a bit embarrassed that our CI got caught by this too. go get -u github.com/golang/protobuf/protoc-gen-go was just too convenient.

Something that could be helpful coming from the golang/protobuf side is to put binaries at a well known stable location. In the repo release would be great, or something like the k8s release notes binaries section. Forcing users to build the tool is a bit of a complexity leak. go get handled this more or less transparently, until it didn't.

dep users should version their protoc-gen-go as a required binary, like so (in Gopkg.toml):

required = [
    "github.com/golang/protobuf/protoc-gen-go",
]

You can then install protoc-gen-go from vendor:

 $ go install ./vendor/github.com/golang/protobuf/protoc-gen-go

I'm not sure what the go modules equivalent workflow is.

What @johanbrandhorst said! I have also pinned the version in dep constraints.

This has made me re-evaluate any go get ... in my builds (mostly Dockerfile stuff) which is a good thing I suppose.

This hack worked for me

cd $GOPATH/src/github.com/golang/protobuf/protoc-gen-go
git checkout v1.2.0
go install

While I agree that a build system should be hermetic and all, I think it's silly this broke on a minor version. I would expect breakages in a X.minor.bugs release but not a major.X.bugs release.

Gopkg even assumes it is safe to do this, and if you don't specify an =VERSION it would have auto-upgraded a number of setups that would have been broken even if people did specify 1.2.0.

I know this is open source and all, but please consider being cognizant of people's setups.

@DarrienG, how do you propose this be addressed? A major version bump is not the right answer:

  • The existence of breakages is not what justifies a major version bump. For example, most projects follow (implicitly or explicitly) the Go1 compatibility policy and that document reserves the right for authors to add new struct fields. However, doing so can break people when depending on a struct being comparable that no longer became so when a field was added. Breakages like this have happened before with data structures in the standard library, yet the standard library did not regard the existence of breakages justification for v2 (i.e., they still broke those users).
  • The change made is explicitly specified in our compatibility agreement as within our right to make. Thus, we are not required to produce a major version bump.
  • A major version bump is a very costly change. It often means that types are no longer compatible, a possibly separate API that people need to know which to import, etc. We are working on a v2 of protobufs, and interestingly the change made was for the purposes of getting v1 and v2 to inter-operate to reduce fragmentation when both APIs exist. If a major version is issued, it should provide strong benefits to justify its costs and 8d0c54c1246661d9a51ca0ba455d22116d485eaa alone does not justify a v2.

I know this is open source and all, but please consider being cognizant of people's setups.

We are cognizant of people's setup within reason. I want to note that this change:

  • does not break people who live at HEAD for both the proto package and protoc-gen-go. I personally think people who do this are risky, but we try hard not to break this situation, and 8d0c54c1246661d9a51ca0ba455d22116d485eaa upheld this expectation as it atomically changed both proto and protoc-gen-go.
  • does not break people who live at a specific version of the proto package and protoc-gen-go. This is my recommended approach for a healthy CI system.
  • does break people who live at a specific version of the proto package and a HEAD version of protoc-gen-go. It's unfortunate that this situation breaks, but there is fundamentally no way that this combination of dependencies remain break-free since protoc-gen-go depends on proto.

Personally, I find this change super disturbing but I understand that's sometimes needed. I can't find a way to make dep happy with grpc-ecosystem/grpc-gateway with this new change. Has anybody tried it ?

@nikkolasg Did you try what I said? https://github.com/golang/protobuf/issues/763#issuecomment-442767135

Yes, protobuf Go struct generation works fine with your trick. However, the grpc-ecosystem/grpc-gateway enforces some specific versions of protobuf/ etc so it does not build anymore. And I can't find the correct [[override]] entry to make it work in my Gopkg.toml. I can make it work without dep: just go get -u and all works fine. But dep can't find a version that satisfies all constraints...

Don't use go get to install the grpc-gateway generator :scream:. This is a bit off topic for this thread, so maybe I can help you in the #grpc-gateway channel on gophers.slack.com instead?

I suspect you might just need something like the following:

[[override]]
  version = "1.1.0"
  name = "github.com/golang/protobuf"

@johanbrandhorst

Don't use go get to install the grpc-gateway generator scream

I'm using dep on a regular basis, but in order to debug why my setup wasn't working, I tried with go get. And to be honest... that's what's written in the README.

I'm not sure why but I tried to reproduce the issue with an simple unique protobuf file and dep figured out all the good versions by itself, without any problems. I copied thoses versions in my repo and now it all works out. Not sure why it did not before. But thanks for your previous trick ;)

I appreciate all the work you are doing, but I want to note that I find it highly disruptive to have released a breaking change on master without a new version.

go get -u does not support versioning so it's cumbersome to require a specific version. It also happens to not be currently possible with an internal solution we have to manage such. I know we could add support, but it goes to show that this is a cumbersome breaking change.

I would recommend releasing a new version asap.

In the mean time, we have changed our projects to track the master branch, but this is not ideal either because in some other cases, the generator is installed in a docker image, so it is not always up-to-date.

Side note: Major version bump would probably be appropriate here. This is not a change where _some people may be affected_, it's a hard version requirement, which completely breaks BC.

A new version would not have helped against use of go get -u. It's only affecting users who rely on a workflow that has never been supported. That may well be a majority of users, but the issue here is not how it was handled but that this practice is so widespread in the community.

I understand a major bump would not have prevented the issue, I simply think it's more in line with semver and would help communicate the concept that it's a hard BC break.

I simply think it's more in line with semver and would help communicate the concept

I'm not sure I understand. Semantic versioning dictates that major versions are issued if "backwards incompatible changes are introduced to the public API". The change made in 8d0c54c1246661d9a51ca0ba455d22116d485eaa is not that. That change is entirely backwards compatible.

This is no different than code that breaks because a library uses sync.Map, but you were trying to build the program with Go1.8, which does not have that type. It does not make sense that a v2 of the standard library should have been issued, but rather you would say that the minimum version of the Go toolchain has been increased to Go1.9.

The exact same thing is happening here. The 8d0c54c1246661d9a51ca0ba455d22116d485eaa version of protoc-gen-go increased the minimum version of the necessary proto package to 8d0c54c1246661d9a51ca0ba455d22116d485eaa as well.

See also golang/go#25922 and golang/go#27653, although in this case protoc-gen-go and proto are already in the same module (github.com/golang/protobuf).

I have solve the problem ! I think its because the protoc-gen-go's version not correct.

git clone https://github.com/golang/protobuf
cd ~/protobuf/protoc-gen-go
git chectout tags/v1.2.0 -b v1.2.0
go install

my environment is mac go1.11

I have solve the problem ! I think its because the protoc-gen-go's version not correct.

git clone https://github.com/golang/protobuf
cd ~/protobuf/protoc-gen-go
git chectout tags/v1.2.0 -b v1.2.0
go install

my environment is mac go1.11

spelling error : git chec[k]out tags/v1.2.0 -b v1.2.0

steps by @lenvs fixed the ProtoPackageIsVersion3 issue, but I'm getting
undefined: utilities.IOReaderFactory with grpc after switch to v1.2.0 .
any help is appreciated

@fertobar I might be wrong but I think that's a grpc-gateway issue. Almost certainly the same problem as here though, you've got one version of your generator and another version of the runtime library. Please make sure you're using the same version of both.

thanks for the feedback @johanbrandhorst ! I will check that

I could fix the dependency issue in this way:

dep ensure
go install ./vendor/github.com/golang/protobuf/protoc-gen-go/
go install ./vendor/github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway/

Thank you @fertobar for research. It worked for me too.

Thank you @fertobar.

Thanks @lenvs inspired me! I also solved the problem !
You may do that.

rm -rf vendor/github.com/golang/protobuf
cd vendor/github.com/golang/
git clone https://github.com/golang/protobuf.git
go install

Stay in branch master, don`t checkout to V1.2.0 !!!

@johanbrandhorst @edwarnicke @lavoiesl If any of you guys happen to be using go modules- this worked for me:
go get github.com/golang/protobuf@8d0c54c1246661d9a51ca0ba455d22116d485eaa
^^^
that will update your go.mod to use the specific revision needed to avoid problems

@johanbrandhorst @edwarnicke @lavoiesl If any of you guys happen to be using go modules- this worked for me:
go get github.com/golang/protobuf@8d0c54c1246661d9a51ca0ba455d22116d485eaa
^^^
that will update your go.mod to use the specific revision needed to avoid problems

Can confirm this worked for me. Thanks!

@longfellowone glad that worked. Go mod is super simple when all your dependencies use semver... But it's still possible with slight deviations like this one (breaking change that technically does not change the API) as I'm painfully learning

@trtg

Not sure what was causing the "undefined: proto.ProtoPackageIsVersion3" error for me. These are the steps I did and your go get is what fixed it.

go get -u github.com/golang/protobuf
protoc --go_out=plugins=grpc:. *.proto

using protoc --version 3.6.1

I want to note some caveats with https://github.com/golang/protobuf/issues/763#issuecomment-452153894 suggestion:

  • The issue that most people are running into is because they are building protoc-gen-go from head, but have a specific version of the proto package pinned.
  • The suggestion above pins the proto package to a more recent version closer to today's head, but doesn't address underlying problem; that is, you are still building protoc-gen-go from head, but have a specific version of proto pinned.
  • The README now contains instructions for how to pin a specific version of protoc-gen-go, which is a more stable path forward, than to still build the binary from head.
  • Go 1.12 will make this much easier, since it will allow installing a Go binary at a specific revision. See golang/go#24250.

@johanbrandhorst @edwarnicke @lavoiesl If any of you guys happen to be using go modules- this worked for me:
go get github.com/golang/protobuf@8d0c54c1246661d9a51ca0ba455d22116d485eaa
^^^
that will update your go.mod to use the specific revision needed to avoid problems

works for me. confirmed! thanks @trtg

problem solved.
This is because proto-go is installed by go get -u github.com/golang/protobuf/protoc-gen-go, which is the newest master branch.
you can switch to tag 1.2.0. reinstall proto-go, reprotoc all files will fix this problem.

@johanbrandhorst @edwarnicke @lavoiesl If any of you guys happen to be using go modules- this worked for me:
go get github.com/golang/protobuf@8d0c54c1246661d9a51ca0ba455d22116d485eaa
^^^
that will update your go.mod to use the specific revision needed to avoid problems

Didn't solve for me. Arrrggggghhh, it's frustrating....!

even tag v1.2.0 didn't solve it either

even tag v1.2.0 didn't solve it either

did you find the answer yet? Please Help Me

even tag v1.2.0 didn't solve it either

did you find the answer yet? Please Help Me

No man nothing so far.

In the spirit of @dsnet's comment above about hermetic builds and because i just solved this with go modules in 1.11 I made a repeatable process you can follow so you don't have to waste all the time I did.

TLDR: I pulled the commit from master and checkout & build protobuf-gen-go on that commit, then i did a go get github.com/golang/protobuf/protoc-gen-go@c823c79ea1570fb5ff454033735a8e68575d1d0f in the folder with my existing go.mod file.

Dump of the pertinent files and build process.

go.mod -- after running the above go get command. ^^

module github.com/placeholder/protobuf-go-mod-fix

require github.com/golang/protobuf v1.2.1-0.20190205222052-c823c79ea157 // indirect

Dockerfile.build

FROM golang:1.11.5-alpine3.9 as golang-builder

RUN apk add --no-cache alpine-sdk git protobuf tree

RUN    go get -d -u github.com/golang/protobuf/protoc-gen-go \
    && cd /go/src/github.com/golang/protobuf/protoc-gen-go/ \
    && git checkout c823c79ea1570fb5ff454033735a8e68575d1d0f \
    && go install

ADD . /mnt/src/

RUN    cd /mnt/src/ \
    && tree \
    && protoc -I ./api --go_out=plugins=grpc:api api.proto \
    && go build -o ./protobuf-go-mod-fix \
    && tree \
    && file ./protobuf-go-mod-fix

Then run...

docker build -t placeholder/protobuf-go-mod-fix:latest -f ./Dockerfile.build .
➜  protobuf-go-mod-fix docker build -t placeholder/protobuf-go-mod-fix:latest -f ./Dockerfile.build .
Sending build context to Docker daemon  9.216kB
Step 1/5 : FROM golang:1.11.5-alpine3.9 as golang-builder
 ---> cb1c8647572c
Step 2/5 : RUN apk add --no-cache alpine-sdk git protobuf tree
 ---> Using cache
 ---> b93e8f8c7342
Step 3/5 : RUN    go get -d -u github.com/golang/protobuf/protoc-gen-go     && cd /go/src/github.com/golang/protobuf/protoc-gen-go/     && git checkout c823c79ea1570fb5ff454033735a8e68575d1d0f     && go install
 ---> Using cache
 ---> 16ba8398861a
Step 4/5 : ADD . /mnt/src/
 ---> ed92ca48f2cc
Step 5/5 : RUN    cd /mnt/src/     && tree     && protoc -I ./api --go_out=plugins=grpc:api api.proto     && go build -o ./protobuf-go-mod-fix     && tree     && file ./protobuf-go-mod-fix
 ---> Running in d7dfc8ca2627
.
β”œβ”€β”€ Dockerfile.build
β”œβ”€β”€ api
β”‚Β Β  β”œβ”€β”€ api.proto
β”‚Β Β  └── handler.go
β”œβ”€β”€ go.mod
β”œβ”€β”€ go.sum
└── main.go

1 directory, 6 files
go: finding github.com/golang/protobuf v1.2.1-0.20190205222052-c823c79ea157
< snipped .. more of these lines here>

.
β”œβ”€β”€ Dockerfile.build
β”œβ”€β”€ api
β”‚Β Β  β”œβ”€β”€ api.pb.go
β”‚Β Β  β”œβ”€β”€ api.proto
β”‚Β Β  └── handler.go
β”œβ”€β”€ go.mod
β”œβ”€β”€ go.sum
β”œβ”€β”€ main.go
└── protobuf-go-mod-fix

1 directory, 8 files
./protobuf-go-mod-fix: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, Go BuildID=DoJscSvpyqlOUpOPWgsH/4YDWvLflS9B7ZGPcODhk/xN3P7Yw83ibPvWI20RzE/83WgRHg5an2yiudtBC0b, not stripped
Removing intermediate container d7dfc8ca2627
 ---> 3875635d4d2a
Successfully built 3875635d4d2a
Successfully tagged placeholder/protobuf-go-mod-fix:latest

Hope that helps some of you wayward internet travelers. πŸ‘‹

Was this page helpful?
0 / 5 - 0 ratings