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