From 5e6fbef48429d0dc6e151367f1d6ce8b580ce82b Mon Sep 17 00:00:00 2001 From: Michael Whittaker Date: Tue, 27 Jun 2023 15:16:06 -0700 Subject: [PATCH] Added hardcoded weaver module version. > Module Version We want a `weaver version` command that prints out the weaver module version the `weaver` binary was built with, or failing that, the commit at which the binary was built. Unfortunately, both of these things are hard. There is currently no nice way to automatically get the version of the main module in a go program [1]. There is a way to get the git commit using `debug.ReadBuildInfo()` [2], but when `go install`ing a binary, the version control information is stripped. Browsing existing open source projects, it seems the standard practice is to hard code the module version in the code. This PR does that and updates the `weaver version` command to use it: ``` $ weaver version weaver v0.17.0 linux/amd64 ``` > Other Versions The weaver repo has two other versioned APIs: the deployer API version and the codegen version. Currently, the deployer API version is the latest module version where the deployer API changed (and the same for the codegen version). We discussed offline the idea of replacing the three versions (module, deployer API, codegen) with just the module version. Then, we could write additional code to check version compatibility. Is codegen v0.17.3 incompatible with v0.12.0, for example? When trying to implement this, however, I ran into some problems. For example, let's say a deployer is at version v0.10.0 and tries to deploy an app at version v0.12.0. Is deployer API version v0.12.0 compatible with version v0.10.0? Well, the deployer was written before v0.12.0 was even created, so it doesn't have a good way to know. The codegen version is also tricky because it relies on some compiler tricks to prevent an app from compiling if it has code generated with a stale version of `weaver generate`. I'm not sure how to implement these tricks without hardcoding a codegen version. Because of these challenges, I decided to stick with our current approach to versioning, for now at least. To clean things up a bit though, I moved all versioning related code to `runtime/version.go`. I also moved some code to the `bin` package because it felt more appropriate there. [1]: https://github.com/golang/go/issues/29228 [2]: https://pkg.go.dev/runtime/debug#ReadBuildInfo --- cmd/weaver/main.go | 4 +- godeps.txt | 29 +++--- internal/envelope/conn/envelope_conn.go | 15 +-- internal/envelope/conn/weavelet_conn.go | 6 +- internal/tool/generate/generator.go | 5 +- internal/tool/multi/multi.go | 3 +- internal/tool/single/single.go | 3 +- internal/tool/ssh/ssh.go | 3 +- internal/tool/version.go | 39 ++++++++ runtime/bin/bin.go | 62 +++++++++++- runtime/bin/bin_test.go | 77 +++++++++++---- runtime/codegen/version.go | 46 +-------- runtime/tool/version.go | 59 ------------ runtime/version/testprogram/main.go | 32 ------- runtime/version/testprogram/weaver_gen.go | 72 -------------- runtime/version/version.go | 112 +++++++++------------- runtime/version/version_test.go | 89 ----------------- 17 files changed, 241 insertions(+), 415 deletions(-) create mode 100644 internal/tool/version.go delete mode 100644 runtime/tool/version.go delete mode 100644 runtime/version/testprogram/main.go delete mode 100644 runtime/version/testprogram/weaver_gen.go delete mode 100644 runtime/version/version_test.go diff --git a/cmd/weaver/main.go b/cmd/weaver/main.go index 1841db48e..020234ef5 100644 --- a/cmd/weaver/main.go +++ b/cmd/weaver/main.go @@ -25,6 +25,7 @@ import ( "os/exec" "strings" + itool "github.com/ServiceWeaver/weaver/internal/tool" "github.com/ServiceWeaver/weaver/internal/tool/callgraph" "github.com/ServiceWeaver/weaver/internal/tool/generate" "github.com/ServiceWeaver/weaver/internal/tool/multi" @@ -84,11 +85,12 @@ func main() { return case "version": - cmd := tool.VersionCmd("weaver") + cmd := itool.VersionCmd("weaver") if err := cmd.Fn(context.Background(), flag.Args()[1:]); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } + return case "callgraph": const usage = `Generate component callgraphs. diff --git a/godeps.txt b/godeps.txt index 46d6f696a..a7823c058 100644 --- a/godeps.txt +++ b/godeps.txt @@ -59,6 +59,7 @@ github.com/ServiceWeaver/weaver/cmd/weaver errors flag fmt + github.com/ServiceWeaver/weaver/internal/tool github.com/ServiceWeaver/weaver/internal/tool/callgraph github.com/ServiceWeaver/weaver/internal/tool/generate github.com/ServiceWeaver/weaver/internal/tool/multi @@ -517,6 +518,13 @@ github.com/ServiceWeaver/weaver/internal/status syscall text/template time +github.com/ServiceWeaver/weaver/internal/tool + context + flag + fmt + github.com/ServiceWeaver/weaver/runtime/tool + github.com/ServiceWeaver/weaver/runtime/version + runtime github.com/ServiceWeaver/weaver/internal/tool/callgraph fmt github.com/ServiceWeaver/weaver/runtime/bin @@ -550,6 +558,7 @@ github.com/ServiceWeaver/weaver/internal/tool/generate github.com/ServiceWeaver/weaver/internal/files github.com/ServiceWeaver/weaver/runtime/codegen github.com/ServiceWeaver/weaver/runtime/colors + github.com/ServiceWeaver/weaver/runtime/version go/ast go/format go/parser @@ -589,6 +598,7 @@ github.com/ServiceWeaver/weaver/internal/tool/multi github.com/ServiceWeaver/weaver/internal/proxy github.com/ServiceWeaver/weaver/internal/routing github.com/ServiceWeaver/weaver/internal/status + github.com/ServiceWeaver/weaver/internal/tool github.com/ServiceWeaver/weaver/internal/tool/certs github.com/ServiceWeaver/weaver/internal/tool/config github.com/ServiceWeaver/weaver/runtime @@ -629,6 +639,7 @@ github.com/ServiceWeaver/weaver/internal/tool/single fmt github.com/ServiceWeaver/weaver/internal/must github.com/ServiceWeaver/weaver/internal/status + github.com/ServiceWeaver/weaver/internal/tool github.com/ServiceWeaver/weaver/internal/tool/config github.com/ServiceWeaver/weaver/runtime github.com/ServiceWeaver/weaver/runtime/codegen @@ -650,6 +661,7 @@ github.com/ServiceWeaver/weaver/internal/tool/ssh flag fmt github.com/ServiceWeaver/weaver/internal/status + github.com/ServiceWeaver/weaver/internal/tool github.com/ServiceWeaver/weaver/internal/tool/config github.com/ServiceWeaver/weaver/internal/tool/ssh/impl github.com/ServiceWeaver/weaver/runtime @@ -743,7 +755,10 @@ github.com/ServiceWeaver/weaver/runtime/bin debug/pe fmt github.com/ServiceWeaver/weaver/runtime/codegen + github.com/ServiceWeaver/weaver/runtime/version os + regexp + strconv github.com/ServiceWeaver/weaver/runtime/bin/testprogram context github.com/ServiceWeaver/weaver @@ -762,6 +777,7 @@ github.com/ServiceWeaver/weaver/runtime/codegen github.com/ServiceWeaver/weaver/metrics github.com/ServiceWeaver/weaver/runtime github.com/ServiceWeaver/weaver/runtime/protos + github.com/ServiceWeaver/weaver/runtime/version go.opentelemetry.io/otel/trace google.golang.org/protobuf/proto math @@ -909,30 +925,17 @@ github.com/ServiceWeaver/weaver/runtime/tool errors flag fmt - github.com/ServiceWeaver/weaver/runtime/codegen github.com/ServiceWeaver/weaver/runtime/colors github.com/ServiceWeaver/weaver/runtime/logging - github.com/ServiceWeaver/weaver/runtime/version io os os/exec - runtime - runtime/debug sort strings text/template time github.com/ServiceWeaver/weaver/runtime/version fmt - github.com/ServiceWeaver/weaver/runtime/bin - regexp - strconv -github.com/ServiceWeaver/weaver/runtime/version/testprogram - context - github.com/ServiceWeaver/weaver - github.com/ServiceWeaver/weaver/runtime/codegen - go.opentelemetry.io/otel/trace - reflect github.com/ServiceWeaver/weaver/weavertest context errors diff --git a/internal/envelope/conn/envelope_conn.go b/internal/envelope/conn/envelope_conn.go index 2de419e79..a28e92c2c 100644 --- a/internal/envelope/conn/envelope_conn.go +++ b/internal/envelope/conn/envelope_conn.go @@ -381,18 +381,13 @@ func verifyWeaveletInfo(wlet *protos.WeaveletInfo) error { // checkVersion checks that the deployer API version the deployer was built // with is compatible with the deployer API version the app was built with, // erroring out if they are not compatible. -func checkVersion(appVersion *protos.SemVer) error { - if appVersion == nil { +func checkVersion(v *protos.SemVer) error { + if v == nil { return fmt.Errorf("version mismatch: nil app version") } - if appVersion.Major != version.Major || - appVersion.Minor != version.Minor || - appVersion.Patch != version.Patch { - return fmt.Errorf( - "version mismatch: deployer version %d.%d.%d is incompatible with app version %d.%d.%d.", - version.Major, version.Minor, version.Patch, - appVersion.Major, appVersion.Minor, appVersion.Patch, - ) + got := version.SemVer{Major: int(v.Major), Minor: int(v.Minor), Patch: int(v.Patch)} + if got != version.DeployerVersion { + return fmt.Errorf("version mismatch: deployer's deployer API version %s is incompatible with app' deployer API version %s.", version.DeployerVersion, got) } return nil } diff --git a/internal/envelope/conn/weavelet_conn.go b/internal/envelope/conn/weavelet_conn.go index 3aa5222dd..8d88e378d 100644 --- a/internal/envelope/conn/weavelet_conn.go +++ b/internal/envelope/conn/weavelet_conn.go @@ -102,9 +102,9 @@ func NewWeaveletConn(r io.ReadCloser, w io.WriteCloser, h WeaveletHandler) (*Wea DialAddr: dialAddr, Pid: int64(os.Getpid()), Version: &protos.SemVer{ - Major: version.Major, - Minor: version.Minor, - Patch: version.Patch, + Major: version.DeployerMajor, + Minor: version.DeployerMinor, + Patch: 0, }, } if err := wc.conn.send(&protos.WeaveletMsg{WeaveletInfo: wc.winfo}); err != nil { diff --git a/internal/tool/generate/generator.go b/internal/tool/generate/generator.go index 59ffda35a..81b26e1ff 100644 --- a/internal/tool/generate/generator.go +++ b/internal/tool/generate/generator.go @@ -37,6 +37,7 @@ import ( "github.com/ServiceWeaver/weaver/internal/files" "github.com/ServiceWeaver/weaver/runtime/codegen" "github.com/ServiceWeaver/weaver/runtime/colors" + "github.com/ServiceWeaver/weaver/runtime/version" "golang.org/x/exp/maps" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/types/typeutil" @@ -928,8 +929,8 @@ func (g *generator) generateVersionCheck(p printFn) { // var _ codegen.LatestVersion = codegen.Version[[0][1]struct{}]("You used ...") p(`var _ %s = %s[[%d][%d]struct{}](%q)`, g.codegen().qualify("LatestVersion"), g.codegen().qualify("Version"), - codegen.Major, codegen.Minor, - fmt.Sprintf(`You used 'weaver generate' codegen version %d.%d.0, but you built your code with an incompatible weaver module version. Try upgrading 'weaver generate' and re-running it.`, codegen.Major, codegen.Minor), + version.CodegenMajor, version.CodegenMinor, + fmt.Sprintf(`You used 'weaver generate' codegen version %d.%d.0, but you built your code with an incompatible weaver module version. Try upgrading 'weaver generate' and re-running it.`, version.CodegenMajor, version.CodegenMinor), ) } diff --git a/internal/tool/multi/multi.go b/internal/tool/multi/multi.go index ea203ed78..f9b7deaef 100644 --- a/internal/tool/multi/multi.go +++ b/internal/tool/multi/multi.go @@ -21,6 +21,7 @@ import ( "github.com/ServiceWeaver/weaver/internal/must" "github.com/ServiceWeaver/weaver/internal/status" + itool "github.com/ServiceWeaver/weaver/internal/tool" "github.com/ServiceWeaver/weaver/runtime" "github.com/ServiceWeaver/weaver/runtime/logging" "github.com/ServiceWeaver/weaver/runtime/tool" @@ -66,6 +67,6 @@ var ( "metrics": status.MetricsCommand("weaver multi", defaultRegistry), "profile": status.ProfileCommand("weaver multi", defaultRegistry), "purge": tool.PurgeCmd(purgeSpec), - "version": tool.VersionCmd("weaver multi"), + "version": itool.VersionCmd("weaver multi"), } ) diff --git a/internal/tool/single/single.go b/internal/tool/single/single.go index 50ebf1ec9..f87459489 100644 --- a/internal/tool/single/single.go +++ b/internal/tool/single/single.go @@ -21,6 +21,7 @@ import ( "github.com/ServiceWeaver/weaver/internal/must" "github.com/ServiceWeaver/weaver/internal/status" + itool "github.com/ServiceWeaver/weaver/internal/tool" "github.com/ServiceWeaver/weaver/runtime" "github.com/ServiceWeaver/weaver/runtime/tool" ) @@ -55,7 +56,7 @@ var ( "metrics": status.MetricsCommand("weaver single", defaultRegistry), "profile": status.ProfileCommand("weaver single", defaultRegistry), "purge": tool.PurgeCmd(purgeSpec), - "version": tool.VersionCmd("weaver single"), + "version": itool.VersionCmd("weaver single"), } ) diff --git a/internal/tool/ssh/ssh.go b/internal/tool/ssh/ssh.go index f8819472c..256e373d8 100644 --- a/internal/tool/ssh/ssh.go +++ b/internal/tool/ssh/ssh.go @@ -16,6 +16,7 @@ package ssh import ( "github.com/ServiceWeaver/weaver/internal/status" + itool "github.com/ServiceWeaver/weaver/internal/tool" "github.com/ServiceWeaver/weaver/runtime/tool" ) @@ -24,7 +25,7 @@ var ( "deploy": &deployCmd, "logs": tool.LogsCmd(&logsSpec), "dashboard": status.DashboardCommand(dashboardSpec), - "version": tool.VersionCmd("weaver ssh"), + "version": itool.VersionCmd("weaver ssh"), // Hidden commands. "babysitter": &babysitterCmd, diff --git a/internal/tool/version.go b/internal/tool/version.go new file mode 100644 index 000000000..480bbe96b --- /dev/null +++ b/internal/tool/version.go @@ -0,0 +1,39 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tool + +import ( + "context" + "flag" + "fmt" + "runtime" + + "github.com/ServiceWeaver/weaver/runtime/tool" + "github.com/ServiceWeaver/weaver/runtime/version" +) + +// VersionCmd returns a command to show a deployer's version. +func VersionCmd(toolname string) *tool.Command { + return &tool.Command{ + Name: "version", + Flags: flag.NewFlagSet("version", flag.ContinueOnError), + Description: fmt.Sprintf("Show %q version", toolname), + Help: fmt.Sprintf("Usage:\n %s version", toolname), + Fn: func(context.Context, []string) error { + fmt.Printf("%s %s %s/%s\n", toolname, version.ModuleVersion, runtime.GOOS, runtime.GOARCH) + return nil + }, + } +} diff --git a/runtime/bin/bin.go b/runtime/bin/bin.go index 8e14363e9..96a17447c 100644 --- a/runtime/bin/bin.go +++ b/runtime/bin/bin.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package bin contains code to extract data from a Service Weaver binary. package bin import ( @@ -21,12 +22,30 @@ import ( "debug/pe" "fmt" "os" + "regexp" + "strconv" "github.com/ServiceWeaver/weaver/runtime/codegen" + "github.com/ServiceWeaver/weaver/runtime/version" ) -// ROData returns the read-only data section of the provided binary. -func ROData(file string) ([]byte, error) { +// deployerVersion exists to embed the deployer API version into a Service +// Weaver binary. We split declaring and assigning version to prevent the +// compiler from erasing it. +// +//nolint:unused +var deployerVersion string + +func init() { + // NOTE that deployerVersion must be assigned a string constant that + // reflects the values of version.DeployerMajor and version.DeployerMinor. + // If the string is not a constant---if we try to use fmt.Sprintf, for + // example---it will not be embedded in a Service Weaver binary. + deployerVersion = "⟦wEaVeRdEpLoYeRvErSiOn:v0.14.0⟧" +} + +// rodata returns the read-only data section of the provided binary. +func rodata(file string) ([]byte, error) { f, err := os.Open(file) if err != nil { return nil, err @@ -76,7 +95,7 @@ func ROData(file string) ([]byte, error) { // // github.com/ServiceWeaver/weaver/Main func ReadComponentGraph(file string) ([][2]string, error) { - data, err := ROData(file) + data, err := rodata(file) if err != nil { return nil, err } @@ -86,9 +105,44 @@ func ReadComponentGraph(file string) ([][2]string, error) { // ReadListeners reads the sets of listeners associated with each component // in the specified binary. func ReadListeners(file string) ([]codegen.ComponentListeners, error) { - data, err := ROData(file) + data, err := rodata(file) if err != nil { return nil, err } return codegen.ExtractListeners(data), nil } + +// ReadDeployerVersion reads the deployer API version from the specified binary. +func ReadDeployerVersion(filename string) (version.SemVer, error) { + data, err := rodata(filename) + if err != nil { + return version.SemVer{}, err + } + return extractDeployerVersion(data) +} + +// extractDeployerVersion returns the deployer API version embedded in data. +func extractDeployerVersion(data []byte) (version.SemVer, error) { + re := regexp.MustCompile(`⟦wEaVeRdEpLoYeRvErSiOn:v([0-9]*?)\.([0-9]*?)\.([0-9]*?)⟧`) + m := re.FindSubmatch(data) + if m == nil { + return version.SemVer{}, fmt.Errorf("embedded deployer API version not found") + } + major, minor, patch := string(m[1]), string(m[2]), string(m[3]) + + v := version.SemVer{} + var err error + v.Major, err = strconv.Atoi(major) + if err != nil { + return version.SemVer{}, fmt.Errorf("invalid embedded deployer API major %q: %w", major, err) + } + v.Minor, err = strconv.Atoi(minor) + if err != nil { + return version.SemVer{}, fmt.Errorf("invalid embedded deployer API minor %q: %w", minor, err) + } + v.Patch, err = strconv.Atoi(patch) + if err != nil { + return version.SemVer{}, fmt.Errorf("invalid embedded deployer API patch %q: %w", patch, err) + } + return v, nil +} diff --git a/runtime/bin/bin_test.go b/runtime/bin/bin_test.go index 575dd53e3..b54eac60a 100644 --- a/runtime/bin/bin_test.go +++ b/runtime/bin/bin_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package bin_test +package bin import ( "fmt" @@ -21,18 +21,13 @@ import ( "path/filepath" "testing" - "github.com/ServiceWeaver/weaver/runtime/bin" "github.com/ServiceWeaver/weaver/runtime/codegen" + "github.com/ServiceWeaver/weaver/runtime/version" "github.com/google/go-cmp/cmp" ) func TestReadComponentGraph(t *testing.T) { - type testCase struct { - os string - arch string - } - - for _, test := range []testCase{ + for _, test := range []struct{ os, arch string }{ {"linux", "amd64"}, {"windows", "amd64"}, {"darwin", "arm64"}, @@ -43,13 +38,12 @@ func TestReadComponentGraph(t *testing.T) { binary := filepath.Join(d, "bin") cmd := exec.Command("go", "build", "-o", binary, "./testprogram") cmd.Env = append(os.Environ(), "GOOS="+test.os, "GOARCH="+test.arch) - err := cmd.Run() - if err != nil { + if err := cmd.Run(); err != nil { t.Fatal(err) } // Read edges. - edges, err := bin.ReadComponentGraph(binary) + edges, err := ReadComponentGraph(binary) if err != nil { t.Fatal(err) } @@ -87,12 +81,7 @@ func TestReadComponentGraph(t *testing.T) { } func TestReadListeners(t *testing.T) { - type testCase struct { - os string - arch string - } - - for _, test := range []testCase{ + for _, test := range []struct{ os, arch string }{ {"linux", "amd64"}, {"windows", "amd64"}, {"darwin", "arm64"}, @@ -109,7 +98,7 @@ func TestReadListeners(t *testing.T) { } // Read listeners. - actual, err := bin.ReadListeners(binary) + actual, err := ReadListeners(binary) if err != nil { t.Fatal(err) } @@ -131,3 +120,55 @@ func TestReadListeners(t *testing.T) { }) } } + +func TestExtractDeployerVersion(t *testing.T) { + for _, want := range []version.SemVer{ + {Major: 0, Minor: 0, Patch: 0}, + {Major: 10, Minor: 10, Patch: 10}, + {Major: 123, Minor: 4567, Patch: 891011}, + } { + t.Run(want.String(), func(t *testing.T) { + // Embed the version string inside a big array of bytes. + var bytes [10000]byte + embedded := fmt.Sprintf("⟦wEaVeRdEpLoYeRvErSiOn:%s⟧", want) + copy(bytes[1234:], []byte(embedded)) + + // Extract the version string. + got, err := extractDeployerVersion(bytes[:]) + if err != nil { + t.Fatal(err) + } + if got != want { + t.Fatalf("bad deployer API version: got %s, want %s", got, want) + } + }) + } +} + +func TestReadVersion(t *testing.T) { + for _, test := range []struct{ os, arch string }{ + {"linux", "amd64"}, + {"windows", "amd64"}, + {"darwin", "arm64"}, + } { + t.Run(fmt.Sprintf("%s/%s", test.os, test.arch), func(t *testing.T) { + // Build the binary for os/arch. + d := t.TempDir() + binary := filepath.Join(d, "bin") + cmd := exec.Command("go", "build", "-o", binary, "./testprogram") + cmd.Env = append(os.Environ(), "GOOS="+test.os, "GOARCH="+test.arch) + if err := cmd.Run(); err != nil { + t.Fatal(err) + } + + // Read version. + got, err := ReadDeployerVersion(binary) + if err != nil { + t.Fatal(err) + } + if got != version.DeployerVersion { + t.Fatalf("bad dipe version: got %s, want %s", got, version.DeployerVersion) + } + }) + } +} diff --git a/runtime/codegen/version.go b/runtime/codegen/version.go index 761b3e1b3..f3bfebe97 100644 --- a/runtime/codegen/version.go +++ b/runtime/codegen/version.go @@ -14,49 +14,7 @@ package codegen -const ( - // The version of the codegen API in semantic version format (Major.Minor). - // - // We need to make sure that the code generated by 'weaver generate' is in - // sync with the weaver module version compiled into a developer's binary. - // For example, 'weaver generate' may generate code using a function in the - // codegen package that doesn't yet exist in the version of the codegen - // package linked into the developer's binary. - // - // We also need to make sure that all generated weaver_gen.go files are - // compatible. For example, if we change the RPC wire protocol, we have to - // ensure that every weaver_gen.go file is re-generated to use the new wire - // protocol. - // - // To address these issues, we introduce a codegen version. Every time we - // make a change to how code is generated---e.g., we change our wire - // protocol, we change which codegen functions get called---we increment - // the codegen version. - // - // In the future, we have to be careful to make backwards compatible - // changes and ensure different versions work with each other. In the short - // term, we make the restriction that all weaver_gen.go files use the same - // codegen API version linked into the developer's binary. - // - // We could assign the codegen API versions v1, v2, v3, and so on. - // However, this makes it hard to understand the relationship between the - // codegen API version and the version of the Service Weaver module. (What - // version of Service Weaver do I need to install to get version 7 of the - // codegen API?) - // - // Instead, we use Service Weaver module versions as codegen API versions. - // For example, if we change the codegen API in v0.12.0 of Service Weaver, - // then we update the codegen API version to v0.12.0. If we don't change - // the codegen API in v0.13.0 of Service Weaver, then we leave the codegen - // API at v0.12.0. - // - // TODO(mwhittaker): Write a doc explaining this version as well as the - // module version and deployer API version. - Major = 0 - Minor = 17 - // NOTE: Patch is omitted because all API changes should bump the major or - // minor version. Patch is assumed to be 0. -) +import "github.com/ServiceWeaver/weaver/runtime/version" // The following types are used to check, at compile time, that every // weaver_gen.go file uses the codegen API version that is linked into the @@ -77,4 +35,4 @@ const ( // (this is "..." above). Again note that we ignore the patch number. type Version[_ any] string -type LatestVersion = Version[[Major][Minor]struct{}] +type LatestVersion = Version[[version.CodegenMajor][version.CodegenMinor]struct{}] diff --git a/runtime/tool/version.go b/runtime/tool/version.go deleted file mode 100644 index 0a4885335..000000000 --- a/runtime/tool/version.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2023 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package tool - -import ( - "context" - "flag" - "fmt" - "runtime" - "runtime/debug" - - "github.com/ServiceWeaver/weaver/runtime/codegen" - "github.com/ServiceWeaver/weaver/runtime/version" -) - -// VersionCmd returns a command to show a deployer's version. -func VersionCmd(tool string) *Command { - return &Command{ - Name: "version", - Flags: flag.NewFlagSet("version", flag.ContinueOnError), - Description: fmt.Sprintf("Show %q version", tool), - Help: fmt.Sprintf("Usage:\n %s version", tool), - Fn: func(context.Context, []string) error { - deployerAPI := fmt.Sprintf("%d.%d.%d", version.Major, version.Minor, version.Patch) - codegenAPI := fmt.Sprintf("%d.%d.0", codegen.Major, codegen.Minor) - commit := "?" - if info, ok := debug.ReadBuildInfo(); ok { - for _, setting := range info.Settings { - // vcs.revision stores the commit at which the weaver tool - // was built. See [1] for more information. - // - // [1]: https://pkg.go.dev/runtime/debug#BuildSetting - if setting.Key == "vcs.revision" { - commit = setting.Value - break - } - } - } - fmt.Printf("tool: %s\n", tool) - fmt.Printf("commit: %s\n", commit) - fmt.Printf("target: %s/%s\n", runtime.GOOS, runtime.GOARCH) - fmt.Printf("deployer API: %s\n", deployerAPI) - fmt.Printf("codegen API: %s\n", codegenAPI) - return nil - }, - } -} diff --git a/runtime/version/testprogram/main.go b/runtime/version/testprogram/main.go deleted file mode 100644 index 48168070d..000000000 --- a/runtime/version/testprogram/main.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2023 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// testprogram is used by version tests. -package main - -import ( - "context" - - "github.com/ServiceWeaver/weaver" -) - -//go:generate ../../../cmd/weaver/weaver generate - -type app struct { - weaver.Implements[weaver.Main] -} - -func (*app) Main(context.Context) error { return nil } - -func main() {} diff --git a/runtime/version/testprogram/weaver_gen.go b/runtime/version/testprogram/weaver_gen.go deleted file mode 100644 index 345b97367..000000000 --- a/runtime/version/testprogram/weaver_gen.go +++ /dev/null @@ -1,72 +0,0 @@ -// Code generated by "weaver generate". DO NOT EDIT. -//go:build !ignoreWeaverGen - -package main - -import ( - "context" - "github.com/ServiceWeaver/weaver" - "github.com/ServiceWeaver/weaver/runtime/codegen" - "go.opentelemetry.io/otel/trace" - "reflect" -) -var _ codegen.LatestVersion = codegen.Version[[0][17]struct{}]("You used 'weaver generate' codegen version 0.17.0, but you built your code with an incompatible weaver module version. Try upgrading 'weaver generate' and re-running it.") - -func init() { - codegen.Register(codegen.Registration{ - Name: "github.com/ServiceWeaver/weaver/Main", - Iface: reflect.TypeOf((*weaver.Main)(nil)).Elem(), - Impl: reflect.TypeOf(app{}), - LocalStubFn: func(impl any, tracer trace.Tracer) any { - return main_local_stub{impl: impl.(weaver.Main), tracer: tracer} - }, - ClientStubFn: func(stub codegen.Stub, caller string) any { return main_client_stub{stub: stub} }, - ServerStubFn: func(impl any, addLoad func(uint64, float64)) codegen.Server { - return main_server_stub{impl: impl.(weaver.Main), addLoad: addLoad} - }, - RefData: "", - }) -} - -// weaver.Instance checks. -var _ weaver.InstanceOf[weaver.Main] = (*app)(nil) - -// weaver.Router checks. -var _ weaver.Unrouted = (*app)(nil) - -// Local stub implementations. - -type main_local_stub struct { - impl weaver.Main - tracer trace.Tracer -} - -// Check that main_local_stub implements the weaver.Main interface. -var _ weaver.Main = (*main_local_stub)(nil) - -// Client stub implementations. - -type main_client_stub struct { - stub codegen.Stub -} - -// Check that main_client_stub implements the weaver.Main interface. -var _ weaver.Main = (*main_client_stub)(nil) - -// Server stub implementations. - -type main_server_stub struct { - impl weaver.Main - addLoad func(key uint64, load float64) -} - -// Check that main_server_stub implements the codegen.Server interface. -var _ codegen.Server = (*main_server_stub)(nil) - -// GetStubFn implements the codegen.Server interface. -func (s main_server_stub) GetStubFn(method string) func(ctx context.Context, args []byte) ([]byte, error) { - switch method { - default: - return nil - } -} diff --git a/runtime/version/version.go b/runtime/version/version.go index 5263f1638..5226ccc03 100644 --- a/runtime/version/version.go +++ b/runtime/version/version.go @@ -12,90 +12,72 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package version contains code related to deployer API versioning. +// Package version contains the version of the weaver module and its +// constituent APIs (e.g., the pipe API, the codegen API). package version import ( "fmt" - "regexp" - "strconv" - - "github.com/ServiceWeaver/weaver/runtime/bin" ) +// TODO(mwhittaker): Write a doc explaining versioning in detail. Include +// Srdjan's comments in PR #219. + const ( - // The version of the deployer API---aka pipe API---in semantic version - // format (Major.Minor.Patch). + // The weaver module semantic version [1]. + // + // [1]: https://go.dev/doc/modules/version-numbers + ModuleMajor = 0 + ModuleMinor = 17 + ModulePatch = 0 + + // Note that there is currently no way to programmatically get the version + // of the current module [1], so we have to manually update the module + // version here whenever we release a new version. // - // Every time we make a change to deployer API, we assign it a new version. - // When an envelope spawns a weavelet, the weavelet reports this version to - // the envelope. The envelope then errors out if it is not compatible with - // the reported version. + // [1]: https://github.com/golang/go/issues/29228 + + // The version of the deployer API. // - // We could assign the deployer API versions v1, v2, v3, and so on. - // However, this makes it hard to understand the relationship between the - // deployer API version and the version of the Service Weaver module. (What - // version of Service Weaver do I need to install to get version 7 of the - // pipe?) + // Every time we make a change to the deployer API, we assign it a new + // version. We could assign the deployer API versions v1, v2, v3, and so + // on. However, this makes it hard to understand the relationship between + // the deployer API version and the version of the Service Weaver module. // // Instead, we use Service Weaver module versions as deployer API versions. // For example, if we change the deployer API in v0.12.0 of Service Weaver, // then we update the deployer API version to v0.12.0. If we don't change // the deployer API in v0.13.0 of Service Weaver, then we leave the // deployer API at v0.12.0. - // - // TODO(mwhittaker): Write a doc explaining versioning in detail. Include - // Srdjan's comments in PR #219. - Major = 0 - Minor = 14 - Patch = 0 + DeployerMajor = 0 + DeployerMinor = 14 + + // The version of the codegen API. As with the deployer API, we assign a + // new version every time we change how code is generated, and we use + // weaver module versions. + CodegenMajor = 0 + CodegenMinor = 17 ) -// version exists to embed the deployer API version into a Service Weaver -// binary. We split declaring and assigning version to prevent the compiler -// from erasing it. -// -// NOTE that version should be initialized with a hardcoded string that should -// reflect the values of Major, Minor and Patch. -// -//nolint:unused -var version string +var ( + // The weaver module version. + ModuleVersion = SemVer{ModuleMajor, ModuleMinor, ModulePatch} -func init() { - // Make sure that the hardcoded string reflects the values of Major, Minor and - // Patch. - version = "⟦wEaVeRvErSiOn:0.14.0⟧" -} + // The deployer API version. + DeployerVersion = SemVer{DeployerMajor, DeployerMinor, 0} + + // The codegen API version. + CodegenVersion = SemVer{CodegenMajor, CodegenMinor, 0} +) -// ReadVersion reads version (major, minor, patch) from the specified binary. -func ReadVersion(filename string) (int, int, int, error) { - data, err := bin.ROData(filename) - if err != nil { - return 0, 0, 0, err - } - return extractVersion(data) +// SemVer is a semantic version. See https://go.dev/doc/modules/version-numbers +// for details. +type SemVer struct { + Major int + Minor int + Patch int } -// extractVersion returns the version (major, minor, patch) corresponding to -// MakeVersionString() embedded in data. -func extractVersion(data []byte) (int, int, int, error) { - re := regexp.MustCompile(`⟦wEaVeRvErSiOn:([0-9]*?)\.([0-9]*?)\.([0-9]*?)⟧`) - m := re.FindSubmatch(data) - if m == nil { - return 0, 0, 0, fmt.Errorf("version not found") - } - major, minor, patch := string(m[1]), string(m[2]), string(m[3]) - ma, err := strconv.Atoi(major) - if err != nil { - return 0, 0, 0, fmt.Errorf("invalid major %q: %w", major, err) - } - mi, err := strconv.Atoi(minor) - if err != nil { - return 0, 0, 0, fmt.Errorf("invalid minor %q: %w", minor, err) - } - pa, err := strconv.Atoi(patch) - if err != nil { - return 0, 0, 0, fmt.Errorf("invalid patch %q: %w", patch, err) - } - return ma, mi, pa, nil +func (s SemVer) String() string { + return fmt.Sprintf("v%d.%d.%d", s.Major, s.Minor, s.Patch) } diff --git a/runtime/version/version_test.go b/runtime/version/version_test.go deleted file mode 100644 index 6cb67f30d..000000000 --- a/runtime/version/version_test.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2023 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package version - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "testing" -) - -func TestExtractVersion(t *testing.T) { - for _, test := range []struct{ major, minor, patch int }{ - {0, 0, 0}, - {10, 10, 10}, - {123, 4567, 891011}, - } { - name := fmt.Sprintf("%d.%d.%d", test.major, test.minor, test.patch) - t.Run(name, func(t *testing.T) { - // Embed the version string inside a big array of bytes. - var bytes [10000]byte - v := fmt.Sprintf("⟦wEaVeRvErSiOn:%d.%d.%d⟧", test.major, test.minor, test.patch) - copy(bytes[1234:], []byte(v)) - - // Extract the version string. - major, minor, patch, err := extractVersion(bytes[:]) - if err != nil { - t.Fatal(err) - } - if major != test.major { - t.Errorf("major: got %d, want %d", major, test.major) - } - if minor != test.minor { - t.Errorf("minor: got %d, want %d", minor, test.minor) - } - if patch != test.patch { - t.Errorf("patch: got %d, want %d", patch, test.patch) - } - }) - } -} - -func TestReadVersion(t *testing.T) { - for _, test := range []struct{ os, arch string }{ - {"linux", "amd64"}, - {"windows", "amd64"}, - {"darwin", "arm64"}, - } { - t.Run(fmt.Sprintf("%s/%s", test.os, test.arch), func(t *testing.T) { - // Build the binary for os/arch. - d := t.TempDir() - binary := filepath.Join(d, "bin") - cmd := exec.Command("go", "build", "-o", binary, "./testprogram") - cmd.Env = append(os.Environ(), "GOOS="+test.os, "GOARCH="+test.arch) - err := cmd.Run() - if err != nil { - t.Fatal(err) - } - - // Read version. - major, minor, patch, err := ReadVersion(binary) - if err != nil { - t.Fatal(err) - } - if major != Major { - t.Errorf("major: got %d, want %d", major, Major) - } - if minor != Minor { - t.Errorf("minor: got %d, want %d", minor, Minor) - } - if patch != Patch { - t.Errorf("patch: got %d, want %d", patch, Patch) - } - }) - } -}