github.com/Kasita-Inc/glide@v0.13.2-0.20171213220317-0274b9278c36/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/Masterminds/cookoo/io has a root repo 54 // at github.com/Masterminds/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 }