Skip to content

Commit

Permalink
cmd/go: stamp the version for binaries built with go build
Browse files Browse the repository at this point in the history
This CL will set the binary version using local tag information if
present.

For #50603

Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest,gotip-windows-amd64-longtest
Change-Id: I58bed345c7eea20e51a7b24ff6e943d9d1ed240d
Reviewed-on: https://go-review.googlesource.com/c/go/+/596035
Reviewed-by: Michael Matloob <matloob@golang.org>
Reviewed-by: Karam Moore <bigjimhilljameel@gmail.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
  • Loading branch information
samthanawalla committed Aug 12, 2024
1 parent b674434 commit 8aa2eed
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 118 deletions.
2 changes: 1 addition & 1 deletion src/cmd/go/go_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ func (tg *testgoData) unsetenv(name string) {
tg.env = append([]string(nil), os.Environ()...)
tg.env = append(tg.env, "GO111MODULE=off", "TESTGONETWORK=panic")
if testing.Short() {
tg.env = append(tg.env, "TESTGOVCS=panic")
tg.env = append(tg.env, "TESTGOVCSREMOTE=panic")
}
}
for i, v := range tg.env {
Expand Down
13 changes: 13 additions & 0 deletions src/cmd/go/internal/load/pkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -2494,6 +2494,19 @@ func (p *Package) setBuildInfo(ctx context.Context, autoVCS bool) {
appendSetting("vcs.time", stamp)
}
appendSetting("vcs.modified", strconv.FormatBool(st.Uncommitted))
// Determine the correct version of this module at the current revision and update the build metadata accordingly.
repo := modfetch.LookupLocal(ctx, p.Module.Dir)
revInfo, err := repo.Stat(ctx, st.Revision)
if err != nil {
goto omitVCS
}
vers := revInfo.Version
if vers != "" {
if st.Uncommitted {
vers += "+dirty"
}
info.Main.Version = vers
}
}
omitVCS:

Expand Down
42 changes: 27 additions & 15 deletions src/cmd/go/internal/modfetch/codehost/codehost.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,39 +296,52 @@ func (e *RunError) Error() string {

var dirLock sync.Map

type RunArgs struct {
cmdline []any // the command to run
dir string // the directory to run the command in
local bool // true if the VCS information is local
env []string // environment variables for the command
stdin io.Reader
}

// Run runs the command line in the given directory
// (an empty dir means the current directory).
// It returns the standard output and, for a non-zero exit,
// a *RunError indicating the command, exit status, and standard error.
// Standard error is unavailable for commands that exit successfully.
func Run(ctx context.Context, dir string, cmdline ...any) ([]byte, error) {
return RunWithStdin(ctx, dir, nil, cmdline...)
return run(ctx, RunArgs{cmdline: cmdline, dir: dir})
}

// RunWithArgs is the same as Run but it also accepts additional arguments.
func RunWithArgs(ctx context.Context, args RunArgs) ([]byte, error) {
return run(ctx, args)
}

// bashQuoter escapes characters that have special meaning in double-quoted strings in the bash shell.
// See https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html.
var bashQuoter = strings.NewReplacer(`"`, `\"`, `$`, `\$`, "`", "\\`", `\`, `\\`)

func RunWithStdin(ctx context.Context, dir string, stdin io.Reader, cmdline ...any) ([]byte, error) {
if dir != "" {
muIface, ok := dirLock.Load(dir)
func run(ctx context.Context, args RunArgs) ([]byte, error) {
if args.dir != "" {
muIface, ok := dirLock.Load(args.dir)
if !ok {
muIface, _ = dirLock.LoadOrStore(dir, new(sync.Mutex))
muIface, _ = dirLock.LoadOrStore(args.dir, new(sync.Mutex))
}
mu := muIface.(*sync.Mutex)
mu.Lock()
defer mu.Unlock()
}

cmd := str.StringList(cmdline...)
if os.Getenv("TESTGOVCS") == "panic" {
panic(fmt.Sprintf("use of vcs: %v", cmd))
cmd := str.StringList(args.cmdline...)
if os.Getenv("TESTGOVCSREMOTE") == "panic" && !args.local {
panic(fmt.Sprintf("use of remote vcs: %v", cmd))
}
if xLog, ok := cfg.BuildXWriter(ctx); ok {
text := new(strings.Builder)
if dir != "" {
if args.dir != "" {
text.WriteString("cd ")
text.WriteString(dir)
text.WriteString(args.dir)
text.WriteString("; ")
}
for i, arg := range cmd {
Expand Down Expand Up @@ -362,15 +375,14 @@ func RunWithStdin(ctx context.Context, dir string, stdin io.Reader, cmdline ...a
var stdout bytes.Buffer
c := exec.CommandContext(ctx, cmd[0], cmd[1:]...)
c.Cancel = func() error { return c.Process.Signal(os.Interrupt) }
c.Dir = dir
c.Stdin = stdin
c.Dir = args.dir
c.Stdin = args.stdin
c.Stderr = &stderr
c.Stdout = &stdout
// For Git commands, manually supply GIT_DIR so Git works with safe.bareRepository=explicit set. Noop for other commands.
c.Env = append(c.Environ(), "GIT_DIR="+dir)
c.Env = append(c.Environ(), args.env...)
err := c.Run()
if err != nil {
err = &RunError{Cmd: strings.Join(cmd, " ") + " in " + dir, Stderr: stderr.Bytes(), Err: err}
err = &RunError{Cmd: strings.Join(cmd, " ") + " in " + args.dir, Stderr: stderr.Bytes(), Err: err}
}
return stdout.Bytes(), err
}
165 changes: 77 additions & 88 deletions src/cmd/go/internal/modfetch/codehost/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,6 @@ import (
"golang.org/x/mod/semver"
)

// LocalGitRepo is like Repo but accepts both Git remote references
// and paths to repositories on the local file system.
func LocalGitRepo(ctx context.Context, remote string) (Repo, error) {
return newGitRepoCached(ctx, remote, true)
}

// A notExistError wraps another error to retain its original text
// but makes it opaquely equivalent to fs.ErrNotExist.
type notExistError struct {
Expand All @@ -50,88 +44,73 @@ func (notExistError) Is(err error) bool { return err == fs.ErrNotExist }

const gitWorkDirType = "git3"

var gitRepoCache par.ErrCache[gitCacheKey, Repo]

type gitCacheKey struct {
remote string
localOK bool
}

func newGitRepoCached(ctx context.Context, remote string, localOK bool) (Repo, error) {
return gitRepoCache.Do(gitCacheKey{remote, localOK}, func() (Repo, error) {
return newGitRepo(ctx, remote, localOK)
})
}

func newGitRepo(ctx context.Context, remote string, localOK bool) (Repo, error) {
r := &gitRepo{remote: remote}
if strings.Contains(remote, "://") {
// This is a remote path.
var err error
r.dir, r.mu.Path, err = WorkDir(ctx, gitWorkDirType, r.remote)
if err != nil {
return nil, err
func newGitRepo(ctx context.Context, remote string, local bool) (Repo, error) {
r := &gitRepo{remote: remote, local: local}
if local {
if strings.Contains(remote, "://") { // Local flag, but URL provided
return nil, fmt.Errorf("git remote (%s) lookup disabled", remote)
}

unlock, err := r.mu.Lock()
info, err := os.Stat(remote)
if err != nil {
return nil, err
}
defer unlock()

if _, err := os.Stat(filepath.Join(r.dir, "objects")); err != nil {
if _, err := Run(ctx, r.dir, "git", "init", "--bare"); err != nil {
os.RemoveAll(r.dir)
return nil, err
}
// We could just say git fetch https://whatever later,
// but this lets us say git fetch origin instead, which
// is a little nicer. More importantly, using a named remote
// avoids a problem with Git LFS. See golang.org/issue/25605.
if _, err := Run(ctx, r.dir, "git", "remote", "add", "origin", "--", r.remote); err != nil {
os.RemoveAll(r.dir)
return nil, err
}
if runtime.GOOS == "windows" {
// Git for Windows by default does not support paths longer than
// MAX_PATH (260 characters) because that may interfere with navigation
// in some Windows programs. However, cmd/go should be able to handle
// long paths just fine, and we expect people to use 'go clean' to
// manipulate the module cache, so it should be harmless to set here,
// and in some cases may be necessary in order to download modules with
// long branch names.
//
// See https://github.com/git-for-windows/git/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path.
if _, err := Run(ctx, r.dir, "git", "config", "core.longpaths", "true"); err != nil {
os.RemoveAll(r.dir)
return nil, err
}
}
if !info.IsDir() {
return nil, fmt.Errorf("%s exists but is not a directory", remote)
}
r.remoteURL = r.remote
r.remote = "origin"
} else {
// Local path.
// Disallow colon (not in ://) because sometimes
// that's rcp-style host:path syntax and sometimes it's not (c:\work).
// The go command has always insisted on URL syntax for ssh.
r.dir = remote
r.mu.Path = r.dir + ".lock"
return r, nil
}
// This is a remote path lookup.
if !strings.Contains(remote, "://") { // No URL scheme, could be host:path
if strings.Contains(remote, ":") {
return nil, fmt.Errorf("git remote cannot use host:path syntax")
return nil, fmt.Errorf("git remote (%s) must not be local directory (use URL syntax not host:path syntax)", remote)
}
if !localOK {
return nil, fmt.Errorf("git remote must not be local directory")
return nil, fmt.Errorf("git remote (%s) must not be local directory", remote)
}
var err error
r.dir, r.mu.Path, err = WorkDir(ctx, gitWorkDirType, r.remote)
if err != nil {
return nil, err
}

unlock, err := r.mu.Lock()
if err != nil {
return nil, err
}
defer unlock()

if _, err := os.Stat(filepath.Join(r.dir, "objects")); err != nil {
if _, err := Run(ctx, r.dir, "git", "init", "--bare"); err != nil {
os.RemoveAll(r.dir)
return nil, err
}
r.local = true
info, err := os.Stat(remote)
if err != nil {
// We could just say git fetch https://whatever later,
// but this lets us say git fetch origin instead, which
// is a little nicer. More importantly, using a named remote
// avoids a problem with Git LFS. See golang.org/issue/25605.
if _, err := r.runGit(ctx, "git", "remote", "add", "origin", "--", r.remote); err != nil {
os.RemoveAll(r.dir)
return nil, err
}
if !info.IsDir() {
return nil, fmt.Errorf("%s exists but is not a directory", remote)
if runtime.GOOS == "windows" {
// Git for Windows by default does not support paths longer than
// MAX_PATH (260 characters) because that may interfere with navigation
// in some Windows programs. However, cmd/go should be able to handle
// long paths just fine, and we expect people to use 'go clean' to
// manipulate the module cache, so it should be harmless to set here,
// and in some cases may be necessary in order to download modules with
// long branch names.
//
// See https://github.com/git-for-windows/git/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path.
if _, err := r.runGit(ctx, "git", "config", "core.longpaths", "true"); err != nil {
os.RemoveAll(r.dir)
return nil, err
}
}
r.dir = remote
r.mu.Path = r.dir + ".lock"
}
r.remoteURL = r.remote
r.remote = "origin"
return r, nil
}

Expand Down Expand Up @@ -171,7 +150,7 @@ func (r *gitRepo) loadLocalTags(ctx context.Context) {
// The git protocol sends all known refs and ls-remote filters them on the client side,
// so we might as well record both heads and tags in one shot.
// Most of the time we only care about tags but sometimes we care about heads too.
out, err := Run(ctx, r.dir, "git", "tag", "-l")
out, err := r.runGit(ctx, "git", "tag", "-l")
if err != nil {
return
}
Expand Down Expand Up @@ -246,7 +225,7 @@ func (r *gitRepo) loadRefs(ctx context.Context) (map[string]string, error) {
r.refsErr = err
return
}
out, gitErr := Run(ctx, r.dir, "git", "ls-remote", "-q", r.remote)
out, gitErr := r.runGit(ctx, "git", "ls-remote", "-q", r.remote)
release()

if gitErr != nil {
Expand Down Expand Up @@ -509,7 +488,7 @@ func (r *gitRepo) stat(ctx context.Context, rev string) (info *RevInfo, err erro
if fromTag && !slices.Contains(info.Tags, tag) {
// The local repo includes the commit hash we want, but it is missing
// the corresponding tag. Add that tag and try again.
_, err := Run(ctx, r.dir, "git", "tag", tag, hash)
_, err := r.runGit(ctx, "git", "tag", tag, hash)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -554,7 +533,7 @@ func (r *gitRepo) stat(ctx context.Context, rev string) (info *RevInfo, err erro
// an apparent Git bug introduced in Git 2.21 (commit 61c771),
// which causes the handler for protocol version 1 to sometimes miss
// tags that point to the requested commit (see https://go.dev/issue/56881).
_, err = Run(ctx, r.dir, "git", "-c", "protocol.version=2", "fetch", "-f", "--depth=1", r.remote, refspec)
_, err = r.runGit(ctx, "git", "-c", "protocol.version=2", "fetch", "-f", "--depth=1", r.remote, refspec)
release()

if err == nil {
Expand Down Expand Up @@ -597,12 +576,12 @@ func (r *gitRepo) fetchRefsLocked(ctx context.Context) error {
}
defer release()

if _, err := Run(ctx, r.dir, "git", "fetch", "-f", r.remote, "refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"); err != nil {
if _, err := r.runGit(ctx, "git", "fetch", "-f", r.remote, "refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"); err != nil {
return err
}

if _, err := os.Stat(filepath.Join(r.dir, "shallow")); err == nil {
if _, err := Run(ctx, r.dir, "git", "fetch", "--unshallow", "-f", r.remote); err != nil {
if _, err := r.runGit(ctx, "git", "fetch", "--unshallow", "-f", r.remote); err != nil {
return err
}
}
Expand All @@ -615,7 +594,7 @@ func (r *gitRepo) fetchRefsLocked(ctx context.Context) error {
// statLocal returns a new RevInfo describing rev in the local git repository.
// It uses version as info.Version.
func (r *gitRepo) statLocal(ctx context.Context, version, rev string) (*RevInfo, error) {
out, err := Run(ctx, r.dir, "git", "-c", "log.showsignature=false", "log", "--no-decorate", "-n1", "--format=format:%H %ct %D", rev, "--")
out, err := r.runGit(ctx, "git", "-c", "log.showsignature=false", "log", "--no-decorate", "-n1", "--format=format:%H %ct %D", rev, "--")
if err != nil {
// Return info with Origin.RepoSum if possible to allow caching of negative lookup.
var info *RevInfo
Expand Down Expand Up @@ -691,7 +670,7 @@ func (r *gitRepo) ReadFile(ctx context.Context, rev, file string, maxSize int64)
if err != nil {
return nil, err
}
out, err := Run(ctx, r.dir, "git", "cat-file", "blob", info.Name+":"+file)
out, err := r.runGit(ctx, "git", "cat-file", "blob", info.Name+":"+file)
if err != nil {
return nil, fs.ErrNotExist
}
Expand All @@ -709,7 +688,7 @@ func (r *gitRepo) RecentTag(ctx context.Context, rev, prefix string, allowed fun
// result is definitive.
describe := func() (definitive bool) {
var out []byte
out, err = Run(ctx, r.dir, "git", "for-each-ref", "--format", "%(refname)", "refs/tags", "--merged", rev)
out, err = r.runGit(ctx, "git", "for-each-ref", "--format", "%(refname)", "refs/tags", "--merged", rev)
if err != nil {
return true
}
Expand Down Expand Up @@ -793,7 +772,7 @@ func (r *gitRepo) DescendsFrom(ctx context.Context, rev, tag string) (bool, erro
//
// git merge-base --is-ancestor exits with status 0 if rev is an ancestor, or
// 1 if not.
_, err := Run(ctx, r.dir, "git", "merge-base", "--is-ancestor", "--", tag, rev)
_, err := r.runGit(ctx, "git", "merge-base", "--is-ancestor", "--", tag, rev)

// Git reports "is an ancestor" with exit code 0 and "not an ancestor" with
// exit code 1.
Expand Down Expand Up @@ -837,7 +816,7 @@ func (r *gitRepo) DescendsFrom(ctx context.Context, rev, tag string) (bool, erro
}
}

_, err = Run(ctx, r.dir, "git", "merge-base", "--is-ancestor", "--", tag, rev)
_, err = r.runGit(ctx, "git", "merge-base", "--is-ancestor", "--", tag, rev)
if err == nil {
return true, nil
}
Expand Down Expand Up @@ -873,7 +852,7 @@ func (r *gitRepo) ReadZip(ctx context.Context, rev, subdir string, maxSize int64
// text file line endings. Setting -c core.autocrlf=input means only
// translate files on the way into the repo, not on the way out (archive).
// The -c core.eol=lf should be unnecessary but set it anyway.
archive, err := Run(ctx, r.dir, "git", "-c", "core.autocrlf=input", "-c", "core.eol=lf", "archive", "--format=zip", "--prefix=prefix/", info.Name, args)
archive, err := r.runGit(ctx, "git", "-c", "core.autocrlf=input", "-c", "core.eol=lf", "archive", "--format=zip", "--prefix=prefix/", info.Name, args)
if err != nil {
if bytes.Contains(err.(*RunError).Stderr, []byte("did not match any files")) {
return nil, fs.ErrNotExist
Expand Down Expand Up @@ -923,3 +902,13 @@ func ensureGitAttributes(repoDir string) (err error) {

return nil
}

func (r *gitRepo) runGit(ctx context.Context, cmdline ...any) ([]byte, error) {
args := RunArgs{cmdline: cmdline, dir: r.dir, local: r.local}
if !r.local {
// Manually supply GIT_DIR so Git works with safe.bareRepository=explicit set.
// This is necessary only for remote repositories as they are initialized with git init --bare.
args.env = []string{"GIT_DIR=" + r.dir}
}
return RunWithArgs(ctx, args)
}
Loading

0 comments on commit 8aa2eed

Please sign in to comment.