github.com/andrewlunde/glide@v0.20.1/util/util.go (about)

     1  package util
     2  
     3  import (
     4  	"encoding/xml"
     5  	"fmt"
     6  	"go/build"
     7  	"io"
     8  	"net/http"
     9  	"net/url"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"regexp"
    14  	"strings"
    15  
    16  	"github.com/Masterminds/vcs"
    17  )
    18  
    19  // ResolveCurrent selects whether the package should only the dependencies for
    20  // the current OS/ARCH instead of all possible permutations.
    21  // This is not concurrently safe which is ok for the current application. If
    22  // other needs arise it may need to be re-written.
    23  var ResolveCurrent = false
    24  
    25  // goRoot caches the GOROOT variable for build contexts. If $GOROOT is not set in
    26  // the user's environment, then the context's root path is 'go env GOROOT'.
    27  var goRoot string
    28  
    29  func init() {
    30  	// Precompile the regular expressions used to check VCS locations.
    31  	for _, v := range vcsList {
    32  		v.regex = regexp.MustCompile(v.pattern)
    33  	}
    34  	if goRoot = os.Getenv("GOROOT"); len(goRoot) == 0 {
    35  		goExecutable := os.Getenv("GLIDE_GO_EXECUTABLE")
    36  		if len(goExecutable) <= 0 {
    37  			goExecutable = "go"
    38  		}
    39  		out, err := exec.Command(goExecutable, "env", "GOROOT").Output()
    40  		if err == nil {
    41  			goRoot = strings.TrimSpace(string(out))
    42  		}
    43  	}
    44  }
    45  
    46  func toSlash(v string) string {
    47  	return strings.Replace(v, "\\", "/", -1)
    48  }
    49  
    50  // GetRootFromPackage retrives the top level package from a name.
    51  //
    52  // From a package name find the root repo. For example,
    53  // the package github.com/andrewlunde/cookoo/io has a root repo
    54  // at github.com/andrewlunde/cookoo
    55  func GetRootFromPackage(pkg string) string {
    56  	pkg = toSlash(pkg)
    57  	for _, v := range vcsList {
    58  		m := v.regex.FindStringSubmatch(pkg)
    59  		if m == nil {
    60  			continue
    61  		}
    62  
    63  		if m[1] != "" {
    64  			return m[1]
    65  		}
    66  	}
    67  
    68  	// There are cases where a package uses the special go get magic for
    69  	// redirects. If we've not discovered the location already try that.
    70  	pkg = getRootFromGoGet(pkg)
    71  
    72  	return pkg
    73  }
    74  
    75  // Pages like https://golang.org/x/net provide an html document with
    76  // meta tags containing a location to work with. The go tool uses
    77  // a meta tag with the name go-import which is what we use here.
    78  // godoc.org also has one call go-source that we do not need to use.
    79  // The value of go-import is in the form "prefix vcs repo". The prefix
    80  // should match the vcsURL and the repo is a location that can be
    81  // checked out. Note, to get the html document you you need to add
    82  // ?go-get=1 to the url.
    83  func getRootFromGoGet(pkg string) string {
    84  
    85  	p, found := checkRemotePackageCache(pkg)
    86  	if found {
    87  		return p
    88  	}
    89  
    90  	vcsURL := "https://" + pkg
    91  	u, err := url.Parse(vcsURL)
    92  	if err != nil {
    93  		return pkg
    94  	}
    95  	if u.RawQuery == "" {
    96  		u.RawQuery = "go-get=1"
    97  	} else {
    98  		u.RawQuery = u.RawQuery + "&go-get=1"
    99  	}
   100  	checkURL := u.String()
   101  	resp, err := http.Get(checkURL)
   102  	if err != nil {
   103  		addToRemotePackageCache(pkg, pkg)
   104  		return pkg
   105  	}
   106  	defer resp.Body.Close()
   107  
   108  	nu, err := parseImportFromBody(u, resp.Body)
   109  	if err != nil {
   110  		addToRemotePackageCache(pkg, pkg)
   111  		return pkg
   112  	} else if nu == "" {
   113  		addToRemotePackageCache(pkg, pkg)
   114  		return pkg
   115  	}
   116  
   117  	addToRemotePackageCache(pkg, nu)
   118  	return nu
   119  }
   120  
   121  // The caching is not concurrency safe but should be made to be that way.
   122  // This implementation is far too much of a hack... rewrite needed.
   123  var remotePackageCache = make(map[string]string)
   124  
   125  func checkRemotePackageCache(pkg string) (string, bool) {
   126  	for k, v := range remotePackageCache {
   127  		if pkg == k || strings.HasPrefix(pkg, k+"/") {
   128  			return v, true
   129  		}
   130  	}
   131  
   132  	return pkg, false
   133  }
   134  
   135  func addToRemotePackageCache(pkg, v string) {
   136  	remotePackageCache[pkg] = v
   137  }
   138  
   139  func parseImportFromBody(ur *url.URL, r io.ReadCloser) (u string, err error) {
   140  	d := xml.NewDecoder(r)
   141  	d.CharsetReader = charsetReader
   142  	d.Strict = false
   143  	var t xml.Token
   144  	for {
   145  		t, err = d.Token()
   146  		if err != nil {
   147  			if err == io.EOF {
   148  				// If we hit the end of the markup and don't have anything
   149  				// we return an error.
   150  				err = vcs.ErrCannotDetectVCS
   151  			}
   152  			return
   153  		}
   154  		if e, ok := t.(xml.StartElement); ok && strings.EqualFold(e.Name.Local, "body") {
   155  			return
   156  		}
   157  		if e, ok := t.(xml.EndElement); ok && strings.EqualFold(e.Name.Local, "head") {
   158  			return
   159  		}
   160  		e, ok := t.(xml.StartElement)
   161  		if !ok || !strings.EqualFold(e.Name.Local, "meta") {
   162  			continue
   163  		}
   164  		if attrValue(e.Attr, "name") != "go-import" {
   165  			continue
   166  		}
   167  		if f := strings.Fields(attrValue(e.Attr, "content")); len(f) == 3 {
   168  
   169  			// If the prefix supplied by the remote system isn't a prefix to the
   170  			// url we're fetching return continue looking for more go-imports.
   171  			// This will work for exact matches and prefixes. For example,
   172  			// golang.org/x/net as a prefix will match for golang.org/x/net and
   173  			// golang.org/x/net/context.
   174  			vcsURL := ur.Host + ur.Path
   175  			if !strings.HasPrefix(vcsURL, f[0]) {
   176  				continue
   177  			} else {
   178  				u = f[0]
   179  				return
   180  			}
   181  
   182  		}
   183  	}
   184  }
   185  
   186  func charsetReader(charset string, input io.Reader) (io.Reader, error) {
   187  	switch strings.ToLower(charset) {
   188  	case "ascii":
   189  		return input, nil
   190  	default:
   191  		return nil, fmt.Errorf("can't decode XML document using charset %q", charset)
   192  	}
   193  }
   194  
   195  func attrValue(attrs []xml.Attr, name string) string {
   196  	for _, a := range attrs {
   197  		if strings.EqualFold(a.Name.Local, name) {
   198  			return a.Value
   199  		}
   200  	}
   201  	return ""
   202  }
   203  
   204  type vcsInfo struct {
   205  	host    string
   206  	pattern string
   207  	regex   *regexp.Regexp
   208  }
   209  
   210  var vcsList = []*vcsInfo{
   211  	{
   212  		host:    "github.com",
   213  		pattern: `^(?P<rootpkg>github\.com/[A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+)(/[A-Za-z0-9_.\-]+)*$`,
   214  	},
   215  	{
   216  		host:    "bitbucket.org",
   217  		pattern: `^(?P<rootpkg>bitbucket\.org/([A-Za-z0-9_.\-]+/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`,
   218  	},
   219  	{
   220  		host:    "launchpad.net",
   221  		pattern: `^(?P<rootpkg>launchpad\.net/(([A-Za-z0-9_.\-]+)(/[A-Za-z0-9_.\-]+)?|~[A-Za-z0-9_.\-]+/(\+junk|[A-Za-z0-9_.\-]+)/[A-Za-z0-9_.\-]+))(/[A-Za-z0-9_.\-]+)*$`,
   222  	},
   223  	{
   224  		host:    "git.launchpad.net",
   225  		pattern: `^(?P<rootpkg>git\.launchpad\.net/(([A-Za-z0-9_.\-]+)|~[A-Za-z0-9_.\-]+/(\+git|[A-Za-z0-9_.\-]+)/[A-Za-z0-9_.\-]+))$`,
   226  	},
   227  	{
   228  		host:    "hub.jazz.net",
   229  		pattern: `^(?P<rootpkg>hub\.jazz\.net/git/[a-z0-9]+/[A-Za-z0-9_.\-]+)(/[A-Za-z0-9_.\-]+)*$`,
   230  	},
   231  	{
   232  		host:    "go.googlesource.com",
   233  		pattern: `^(?P<rootpkg>go\.googlesource\.com/[A-Za-z0-9_.\-]+/?)$`,
   234  	},
   235  	// TODO: Once Google Code becomes fully deprecated this can be removed.
   236  	{
   237  		host:    "code.google.com",
   238  		pattern: `^(?P<rootpkg>code\.google\.com/[pr]/([a-z0-9\-]+)(\.([a-z0-9\-]+))?)(/[A-Za-z0-9_.\-]+)*$`,
   239  	},
   240  	// Alternative Google setup for SVN. This is the previous structure but it still works... until Google Code goes away.
   241  	{
   242  		pattern: `^(?P<rootpkg>[a-z0-9_\-.]+\.googlecode\.com/svn(/.*)?)$`,
   243  	},
   244  	// Alternative Google setup. This is the previous structure but it still works... until Google Code goes away.
   245  	{
   246  		pattern: `^(?P<rootpkg>[a-z0-9_\-.]+\.googlecode\.com/(git|hg))(/.*)?$`,
   247  	},
   248  	// If none of the previous detect the type they will fall to this looking for the type in a generic sense
   249  	// by the extension to the path.
   250  	{
   251  		pattern: `^(?P<rootpkg>(?P<repo>([a-z0-9.\-]+\.)+[a-z0-9.\-]+(:[0-9]+)?/[A-Za-z0-9_.\-/]*?)\.(bzr|git|hg|svn))(/[A-Za-z0-9_.\-]+)*$`,
   252  	},
   253  }
   254  
   255  // BuildCtxt is a convenience wrapper for not having to import go/build
   256  // anywhere else
   257  type BuildCtxt struct {
   258  	build.Context
   259  }
   260  
   261  // PackageName attempts to determine the name of the base package.
   262  //
   263  // If resolution fails, this will return "main".
   264  func (b *BuildCtxt) PackageName(base string) string {
   265  	cwd, err := os.Getwd()
   266  	if err != nil {
   267  		return "main"
   268  	}
   269  
   270  	pkg, err := b.Import(base, cwd, 0)
   271  	if err != nil {
   272  		// There may not be any top level Go source files but the project may
   273  		// still be within the GOPATH.
   274  		if strings.HasPrefix(base, b.GOPATH) {
   275  			p := strings.TrimPrefix(base, filepath.Join(b.GOPATH, "src"))
   276  			return strings.Trim(p, string(os.PathSeparator))
   277  		}
   278  	}
   279  
   280  	return pkg.ImportPath
   281  }
   282  
   283  // GetBuildContext returns a build context from go/build. When the $GOROOT
   284  // variable is not set in the users environment it sets the context's root
   285  // path to the path returned by 'go env GOROOT'.
   286  //
   287  // TODO: This should be moved to the `dependency` package.
   288  func GetBuildContext() (*BuildCtxt, error) {
   289  	if len(goRoot) == 0 {
   290  		return nil, fmt.Errorf("GOROOT value not found. Please set the GOROOT " +
   291  			"environment variable to use this command")
   292  	}
   293  
   294  	buildContext := &BuildCtxt{build.Default}
   295  
   296  	// If we aren't resolving for the current system set to look at all
   297  	// build modes.
   298  	if !ResolveCurrent {
   299  		// This tells the context scanning to skip filtering on +build flags or
   300  		// file names.
   301  		buildContext.UseAllFiles = true
   302  	}
   303  
   304  	buildContext.GOROOT = goRoot
   305  	return buildContext, nil
   306  }
   307  
   308  // NormalizeName takes a package name and normalizes it to the top level package.
   309  //
   310  // For example, golang.org/x/crypto/ssh becomes golang.org/x/crypto. 'ssh' is
   311  // returned as extra data.
   312  //
   313  // FIXME: Is this deprecated?
   314  func NormalizeName(name string) (string, string) {
   315  	// Fastpath check if a name in the GOROOT. There is an issue when a pkg
   316  	// is in the GOROOT and GetRootFromPackage tries to look it up because it
   317  	// expects remote names.
   318  	b, err := GetBuildContext()
   319  	if err == nil {
   320  		p := filepath.Join(b.GOROOT, "src", name)
   321  		if _, err := os.Stat(p); err == nil {
   322  			return toSlash(name), ""
   323  		}
   324  	}
   325  
   326  	name = toSlash(name)
   327  	root := GetRootFromPackage(name)
   328  	extra := strings.TrimPrefix(name, root)
   329  	if len(extra) > 0 && extra != "/" {
   330  		extra = strings.TrimPrefix(extra, "/")
   331  	} else {
   332  		// If extra is / (which is what it would be here) we want to return ""
   333  		extra = ""
   334  	}
   335  
   336  	return root, extra
   337  }