Skip to content

Commit

Permalink
Improve runtime/debug Go 1.18 incompat. via go.mod file parsing (#…
Browse files Browse the repository at this point in the history
…130)

As of Go 1.18 [1] the debug.ReadBuildInfo [2] function does not work for
Mage executables anymore because the way how module information is
stored changed. Therefore the fields of the returned debug.Module [3]
type only has zero values, including the module path.
The debug.Module.Version [4] field has a default value [5] (`(devel)`)
which is not Semver compatible and causes the parsing to fail.
The change in Go 1.18 [9] also came with the new `debug/buildinfo`
package [10] which allows to read the information from compiled binaries
while the `runtime/debug.ReadBuildInfo` function returns information
from within the running binary. Both are not suitable anymore which is
also described in the Go 1.18 `version` command release notes:

  "The underlying data format of the embedded build information can
  change with new `go` releases, so an older version of `go` may not
  handle the build information produced with a newer version of `go`.
  To read the version information from a binary built with `go` 1.18,
  use the `go` version command and the `debug/buildinfo` package from
  `go` 1.18+."

To get the required module information that was previously provided by
the runtime/debug [6] package the official golang.org/x/mod/modfile [7]
package is now used instead that provides the implementation for a
parser and formatter for `go.mod` files [8] [^1]. This allows to safely
get the module path without the need to depend on runtime/dynamic logic
that might change in future Go versions.

Note that this change also increased the minimum Go version from `1.17`
to `1.19`!

[1]: golang/go@9cec77ac#diff-abdadaf0d85a2e6c8e45da716909b2697d830b0c75149b9e35accda9c38622bdR2234
[2]: https://pkg.go.dev/runtime/debug@go1.18#ReadBuildInfo
[3]: https://pkg.go.dev/runtime/debug#Module
[4]: https://github.com/golang/go/blob/9cec77ac/src/runtime/debug/mod.go#L52
[5]: https://github.com/golang/go/blob/122a22e0e9eba7fe712030d429fc4bcf6f447f5e/src/cmd/go/internal/load/pkg.go#L2288
[6]: https://pkg.go.dev/runtime/debug@go1.18.8
[7]: https://pkg.go.dev/golang.org/x/mod/modfile
[8]: https://pkg.go.dev/cmd/go#hdr-The_go_mod_file
[9]: https://tip.golang.org/doc/go1.18#go-version
[10]: https://tip.golang.org/doc/go1.18#debug/buildinfo

[^1]: https://go.dev/ref/mod#go-mod-file

GH-129
  • Loading branch information
svengreb authored Nov 15, 2022
1 parent c4fe6cf commit df29129
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 14 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/svengreb/wand

go 1.17
go 1.19

require (
github.com/Masterminds/semver/v3 v3.1.1
Expand All @@ -10,6 +10,7 @@ require (
github.com/stretchr/testify v1.7.0
github.com/svengreb/golib v0.1.0
github.com/svengreb/nib v0.2.0
golang.org/x/mod v0.7.0
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
Expand Down
64 changes: 64 additions & 0 deletions pkg/project/gomodule.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,30 @@ package project

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

"github.com/Masterminds/semver/v3"
"golang.org/x/mod/modfile"

glFS "github.com/svengreb/golib/pkg/io/fs"
)

const (
// GoModuleDefaultBuildInfoVersion is the default version for the build info of a Go module when it is not set or no
// when no version was detected by the [runtime/debug.ReadBuildInfo] function. [As of Go 1.18] this defaults to [this
// value] instead of an empty string and [the `go version -m` documentation] also describes the meaning of the value.
//
// [runtime/debug.ReadBuildInfo]: https://pkg.go.dev/runtime/debug@go1.18#ReadBuildInfo
// [As of Go 1.18]: https://github.com/golang/go/commit/9cec77ac11b012283e654b423cf85cf9976bedd9#diff-abdadaf0d85a2e6c8e45da716909b2697d830b0c75149b9e35accda9c38622bdR2234
// [this value]: https://github.com/golang/go/blob/122a22e0e9eba7fe712030d429fc4bcf6f447f5e/src/cmd/go/internal/load/pkg.go#L2288
// [the `go version -m` documentation]: https://go.dev/ref/mod#modules-overview#go-version-m
GoModuleDefaultBuildInfoVersion = "(devel)"

// GoModuleDefaultFileName is the default name for a Go module file.
GoModuleDefaultFileName = "go.mod"

// GoModuleVersionLatest is the "version query suffix" for the latest version of a Go module.
// See https://golang.org/ref/mod#version-queries for more details.
GoModuleVersionLatest = "latest"
Expand Down Expand Up @@ -88,3 +105,50 @@ func GoModuleFromImportPath(importPath string) (*GoModuleID, error) {
gm.Version = version
return gm, nil
}

// GoModuleFromFile parses a Go module file ([GoModuleDefaultBuildInfoVersion]).
// This is required because [as of Go 1.18] the [debug.ReadBuildInfo] function does not work for Mage executables
// anymore because the way how module information is stored changed. Therefore the fields of the returned
// [debug.Module] type only has zero values, including the module path. The [debug.Module.Version] field has a
// [default value] ([GoModuleDefaultBuildInfoVersion]) which is not Semver compatible and causes the parsing to fail.
//
// To get the required module information that was previously provided by the [runtime/debug] package the official
// [golang.org/x/mod/modfile] package is used instead.
//
// [debug.ReadBuildInfo]: https://pkg.go.dev/runtime/debug@go1.18#ReadBuildInfo
// [as of Go 1.18]: https://github.com/golang/go/commit/9cec77ac11b012283e654b423cf85cf9976bedd9#diff-abdadaf0d85a2e6c8e45da716909b2697d830b0c75149b9e35accda9c38622bdR2234
// [default value]: https://github.com/golang/go/blob/122a22e0e9eba7fe712030d429fc4bcf6f447f5e/src/cmd/go/internal/load/pkg.go#L2288
func GoModuleFromFile(dirAbs string) (*GoModuleID, error) {
goModFilePath := filepath.Join(dirAbs, GoModuleDefaultFileName)
hasModFile, fsErr := glFS.RegularFileExists(goModFilePath)
if fsErr != nil {
return nil, fsErr
}
if !hasModFile {
return nil, fmt.Errorf("no %q file in project root directory path %q", GoModuleDefaultFileName, dirAbs)
}
goModFileData, osErr := os.ReadFile(goModFilePath)
if osErr != nil {
return nil, fmt.Errorf("read Go module file %q: %w", goModFilePath, osErr)
}
goModFile, goModFileErr := modfile.Parse(goModFilePath, goModFileData, nil)
if goModFileErr != nil {
return nil, fmt.Errorf("parse Go module file %q: %w", goModFilePath, goModFileErr)
}

gmfv := goModFile.Module.Mod.Version
gm := &GoModuleID{Path: goModFile.Module.Mod.Path}
// Handle as latest Go module version when the module version equals the default value or is not set/detected and set
// a valid Semver version string as fallback.
if gmfv == "" || goModFile.Module.Mod.Version == GoModuleDefaultBuildInfoVersion {
gm.IsLatest = true
gmfv = DefaultVersion
}

version, semVerErr := semver.NewVersion(gmfv)
if semVerErr != nil {
return nil, fmt.Errorf("parse version %q from Go module %q: %w", gmfv, gm.Path, semVerErr)
}
gm.Version = version
return gm, nil
}
17 changes: 4 additions & 13 deletions pkg/project/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package project
import (
"os"
"path/filepath"
"runtime/debug"

"github.com/Masterminds/semver/v3"

Expand Down Expand Up @@ -138,24 +137,16 @@ func newOptions(opts ...Option) (*Options, error) {
}
}

buildInfo, ok := debug.ReadBuildInfo()
if !ok {
return nil, &ErrProject{Kind: ErrDetermineGoModuleInformation}
}

goModuleVersion, goModuleVersionErr := semver.NewVersion(buildInfo.Main.Version)
if goModuleVersionErr == nil {
return nil, &ErrProject{Err: goModuleVersionErr, Kind: ErrDetermineGoModuleInformation}
gm, gmErr := GoModuleFromFile(rootDirPath)
if gmErr != nil {
return nil, &ErrProject{Err: gmErr, Kind: ErrDetermineGoModuleInformation}
}

opt := &Options{
BaseOutputDir: DefaultBaseOutputDir,
DefaultVersion: DefaultVersion,
DisplayName: filepath.Base(rootDirPath),
GoModule: &GoModuleID{
Path: buildInfo.Main.Path,
Version: goModuleVersion,
},
GoModule: gm,
Name: filepath.Base(rootDirPath),
Repository: vcsNone.New(),
RootDirPathAbs: rootDirPath,
Expand Down
1 change: 1 addition & 0 deletions pkg/task/golang/golang.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"os/exec"

"github.com/magefile/mage/sh"

glFS "github.com/svengreb/golib/pkg/io/fs"

"github.com/svengreb/wand/pkg/task"
Expand Down

0 comments on commit df29129

Please sign in to comment.