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 }