1
0
mirror of https://github.com/golang/go synced 2024-11-18 08:44:43 -07:00

go/buildutil, cmd/guru: extract overlay context into buildutil

This extracts the overlay context and archive parsing from guru into
buildutil.

At least one tool (gogetdoc) has a vendored copy of this code already,
and more tools implementing the same functionality will follow.

The new code in buildutil is an almost identical copy of the code in
guru (names aside), except for the following changes:

- Instead of reading into a bytes.Buffer, we read directly into a []byte
  of appropriate size

- sameFile first attempts a simple comparison of path.Clean'ed paths.

Change-Id: I97cd978ccc10722e3648e5e10625fa7f1407f202
Reviewed-on: https://go-review.googlesource.com/21805
Reviewed-by: Alan Donovan <adonovan@google.com>
This commit is contained in:
Dominik Honnef 2016-04-11 20:58:33 +02:00 committed by Alan Donovan
parent fb9c7fc43c
commit 446a18e709
4 changed files with 192 additions and 71 deletions

View File

@ -13,18 +13,14 @@ package main // import "golang.org/x/tools/cmd/guru"
import (
"bufio"
"bytes"
"flag"
"fmt"
"go/build"
"go/token"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime/pprof"
"strconv"
"strings"
"sync"
@ -168,7 +164,7 @@ func main() {
// read them from the standard input and
// overlay them on the build context.
if *modifiedFlag {
modified, err := parseArchive(os.Stdin)
modified, err := buildutil.ParseOverlayArchive(os.Stdin)
if err != nil {
log.Fatal(err)
}
@ -178,7 +174,7 @@ func main() {
// but the loader's cgo preprocessing currently does not.
if len(modified) > 0 {
ctxt = useModifiedFiles(ctxt, modified)
ctxt = buildutil.OverlayContext(ctxt, modified)
}
}
@ -212,68 +208,3 @@ func main() {
log.Fatal(err)
}
}
func parseArchive(archive io.Reader) (map[string][]byte, error) {
modified := make(map[string][]byte)
r := bufio.NewReader(archive)
for {
// Read file name.
filename, err := r.ReadString('\n')
if err != nil {
if err == io.EOF {
break // OK
}
return nil, fmt.Errorf("reading modified file name: %v", err)
}
filename = filepath.Clean(strings.TrimSpace(filename))
// Read file size.
sz, err := r.ReadString('\n')
if err != nil {
return nil, fmt.Errorf("reading size of modified file %s: %v", filename, err)
}
sz = strings.TrimSpace(sz)
size, err := strconv.ParseInt(sz, 10, 32)
if err != nil {
return nil, fmt.Errorf("parsing size of modified file %s: %v", filename, err)
}
// Read file content.
var content bytes.Buffer
content.Grow(int(size))
if _, err := io.CopyN(&content, r, size); err != nil {
return nil, fmt.Errorf("reading modified file %s: %v", filename, err)
}
modified[filename] = content.Bytes()
}
return modified, nil
}
// useModifiedFiles augments the provided build.Context by the
// mapping from file names to alternative contents.
func useModifiedFiles(orig *build.Context, modified map[string][]byte) *build.Context {
rc := func(data []byte) (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewBuffer(data)), nil
}
copy := *orig // make a copy
ctxt := &copy
ctxt.OpenFile = func(path string) (io.ReadCloser, error) {
// Fast path: names match exactly.
if content, ok := modified[path]; ok {
return rc(content)
}
// Slow path: check for same file under a different
// alias, perhaps due to a symbolic link.
for filename, content := range modified {
if sameFile(path, filename) {
return rc(content)
}
}
return buildutil.OpenFile(orig, path)
}
return ctxt
}

103
go/buildutil/overlay.go Normal file
View File

@ -0,0 +1,103 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package buildutil
import (
"bufio"
"bytes"
"fmt"
"go/build"
"io"
"io/ioutil"
"path/filepath"
"strconv"
"strings"
)
// OverlayContext overlays a build.Context with additional files from
// a map. Files in the map take precedence over other files.
//
// In addition to plain string comparison, two file names are
// considered equal if their base names match and their directory
// components point at the same directory on the file system. That is,
// symbolic links are followed for directories, but not files.
//
// A common use case for OverlayContext is to allow editors to pass in
// a set of unsaved, modified files.
//
// Currently, only the Context.OpenFile function will respect the
// overlay. This may change in the future.
func OverlayContext(orig *build.Context, overlay map[string][]byte) *build.Context {
// TODO(dominikh): Implement IsDir, HasSubdir and ReadDir
rc := func(data []byte) (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewBuffer(data)), nil
}
copy := *orig // make a copy
ctxt := &copy
ctxt.OpenFile = func(path string) (io.ReadCloser, error) {
// Fast path: names match exactly.
if content, ok := overlay[path]; ok {
return rc(content)
}
// Slow path: check for same file under a different
// alias, perhaps due to a symbolic link.
for filename, content := range overlay {
if sameFile(path, filename) {
return rc(content)
}
}
return OpenFile(orig, path)
}
return ctxt
}
// ParseOverlayArchive parses an archive containing Go files and their
// contents. The result is intended to be used with Overlay.
//
//
// Archive format
//
// The archive consists of a series of files. Each file consists of a
// name, a decimal file size and the file contents, separated by
// newlinews. No newline follows after the file contents.
func ParseOverlayArchive(archive io.Reader) (map[string][]byte, error) {
overlay := make(map[string][]byte)
r := bufio.NewReader(archive)
for {
// Read file name.
filename, err := r.ReadString('\n')
if err != nil {
if err == io.EOF {
break // OK
}
return nil, fmt.Errorf("reading archive file name: %v", err)
}
filename = filepath.Clean(strings.TrimSpace(filename))
// Read file size.
sz, err := r.ReadString('\n')
if err != nil {
return nil, fmt.Errorf("reading size of archive file %s: %v", filename, err)
}
sz = strings.TrimSpace(sz)
size, err := strconv.ParseUint(sz, 10, 32)
if err != nil {
return nil, fmt.Errorf("parsing size of archive file %s: %v", filename, err)
}
// Read file content.
content := make([]byte, size)
if _, err := io.ReadFull(r, content); err != nil {
return nil, fmt.Errorf("reading archive file %s: %v", filename, err)
}
overlay[filename] = content
}
return overlay, nil
}

View File

@ -0,0 +1,70 @@
package buildutil_test
import (
"go/build"
"io/ioutil"
"reflect"
"strings"
"testing"
"golang.org/x/tools/go/buildutil"
)
func TestParseOverlayArchive(t *testing.T) {
var tt = []struct {
in string
out map[string][]byte
hasErr bool
}{
{
"a.go\n5\n12345",
map[string][]byte{"a.go": []byte("12345")},
false,
},
{
"a.go\n5\n1234",
nil,
true,
},
{
"a.go\n5\n12345b.go\n4\n1234",
map[string][]byte{"a.go": []byte("12345"), "b.go": []byte("1234")},
false,
},
}
for _, test := range tt {
got, err := buildutil.ParseOverlayArchive(strings.NewReader(test.in))
if err == nil && test.hasErr {
t.Errorf("expected error for %q", test.in)
}
if err != nil && !test.hasErr {
t.Errorf("unexpected error %v for %q", err, test.in)
}
if !reflect.DeepEqual(got, test.out) {
t.Errorf("got %#v, want %#v", got, test.out)
}
}
}
func TestOverlay(t *testing.T) {
ctx := &build.Default
ov := map[string][]byte{
"/somewhere/a.go": []byte("file contents"),
}
names := []string{"/somewhere/a.go", "/somewhere//a.go"}
ctx = buildutil.OverlayContext(ctx, ov)
for _, name := range names {
f, err := buildutil.OpenFile(ctx, name)
if err != nil {
t.Errorf("unexpected error %v", err)
}
b, err := ioutil.ReadAll(f)
if err != nil {
t.Errorf("unexpected error %v", err)
}
if got, expected := string(b), string(ov["/somewhere/a.go"]); got != expected {
t.Errorf("read %q, expected %q", got, expected)
}
}
}

View File

@ -165,3 +165,20 @@ func SplitPathList(ctxt *build.Context, s string) []string {
}
return filepath.SplitList(s)
}
// sameFile returns true if x and y have the same basename and denote
// the same file.
//
func sameFile(x, y string) bool {
if path.Clean(x) == path.Clean(y) {
return true
}
if filepath.Base(x) == filepath.Base(y) { // (optimisation)
if xi, err := os.Stat(x); err == nil {
if yi, err := os.Stat(y); err == nil {
return os.SameFile(xi, yi)
}
}
}
return false
}