github.com/seh/gb@v0.4.4-0.20160724065125-065d2b2b1ba1/depfile.go (about) 1 package gb 2 3 import ( 4 "compress/gzip" 5 "crypto/sha1" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "net/http" 10 "os" 11 "path/filepath" 12 "regexp" 13 "strings" 14 15 "github.com/constabulary/gb/internal/debug" 16 "github.com/constabulary/gb/internal/depfile" 17 "github.com/constabulary/gb/internal/importer" 18 "github.com/constabulary/gb/internal/untar" 19 "github.com/pkg/errors" 20 ) 21 22 const semverRegex = `^([0-9]+)\.([0-9]+)\.([0-9]+)(?:(\-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-\-\.]+)?$` 23 24 // addDepfileDeps inserts into the Context's importer list 25 // a set of importers for entries in the depfile. 26 func addDepfileDeps(ic *importer.Context, ctx *Context) (Importer, error) { 27 i := Importer(new(nullImporter)) 28 df, err := readDepfile(ctx) 29 if err != nil { 30 if !os.IsNotExist(errors.Cause(err)) { 31 return nil, errors.Wrap(err, "could not parse depfile") 32 } 33 debug.Debugf("no depfile, nothing to do.") 34 return i, nil 35 } 36 re := regexp.MustCompile(semverRegex) 37 for prefix, kv := range df { 38 if version, ok := kv["version"]; ok { 39 if !re.MatchString(version) { 40 return nil, errors.Errorf("%s: %q is not a valid SemVer 2.0.0 version", prefix, version) 41 } 42 root := filepath.Join(cachePath(), hash(prefix, version)) 43 dest := filepath.Join(root, "src", filepath.FromSlash(prefix)) 44 fi, err := os.Stat(dest) 45 if err == nil { 46 if !fi.IsDir() { 47 return nil, errors.Errorf("%s is not a directory", dest) 48 } 49 } 50 if err != nil { 51 if !os.IsNotExist(err) { 52 return nil, err 53 } 54 if err := fetchVersion(root, dest, prefix, version); err != nil { 55 return nil, err 56 } 57 } 58 i = &_importer{ 59 Importer: i, 60 im: importer.Importer{ 61 Context: ic, 62 Root: root, 63 }, 64 } 65 debug.Debugf("Add importer for %q: %v", prefix+" "+version, root) 66 } 67 68 if tag, ok := kv["tag"]; ok { 69 root := filepath.Join(cachePath(), hash(prefix, tag)) 70 dest := filepath.Join(root, "src", filepath.FromSlash(prefix)) 71 fi, err := os.Stat(dest) 72 if err == nil { 73 if !fi.IsDir() { 74 return nil, errors.Errorf("%s is not a directory", dest) 75 } 76 } 77 if err != nil { 78 if !os.IsNotExist(err) { 79 return nil, err 80 } 81 if err := fetchTag(root, dest, prefix, tag); err != nil { 82 return nil, err 83 } 84 } 85 i = &_importer{ 86 Importer: i, 87 im: importer.Importer{ 88 Context: ic, 89 Root: root, 90 }, 91 } 92 debug.Debugf("Add importer for %q: %v", prefix+" "+tag, root) 93 } 94 } 95 return i, nil 96 } 97 98 func fetchVersion(root, dest, prefix, version string) error { 99 if !strings.HasPrefix(prefix, "github.com") { 100 return errors.Errorf("unable to fetch %v", prefix) 101 } 102 103 fmt.Printf("fetching %v (%v)\n", prefix, version) 104 105 rc, err := fetchRelease(prefix, "v"+version) 106 if err != nil { 107 return err 108 } 109 defer rc.Close() 110 return unpackReleaseTarball(dest, rc) 111 } 112 113 func fetchTag(root, dest, prefix, tag string) error { 114 if !strings.HasPrefix(prefix, "github.com") { 115 return errors.Errorf("unable to fetch %v", prefix) 116 } 117 118 fmt.Printf("fetching %v (%v)\n", prefix, tag) 119 120 rc, err := fetchRelease(prefix, tag) 121 if err != nil { 122 return err 123 } 124 defer rc.Close() 125 return unpackReleaseTarball(dest, rc) 126 } 127 128 func unpackReleaseTarball(dest string, r io.Reader) error { 129 gzr, err := gzip.NewReader(r) 130 if err != nil { 131 return errors.Wrap(err, "unable to construct gzip reader") 132 } 133 134 parent, pkg := filepath.Split(dest) 135 if err := os.MkdirAll(parent, 0755); err != nil { 136 return err 137 } 138 tmpdir, err := ioutil.TempDir(parent, "tmp") 139 if err != nil { 140 return err 141 } 142 defer os.RemoveAll(tmpdir) 143 144 tmpdir = filepath.Join(tmpdir, pkg) 145 146 if err := untar.Untar(tmpdir, gzr); err != nil { 147 return err 148 } 149 150 dents, err := ioutil.ReadDir(tmpdir) 151 if err != nil { 152 os.RemoveAll(tmpdir) 153 return errors.Wrap(err, "cannot read download directory") 154 } 155 re := regexp.MustCompile(`\w+-\w+-[a-z0-9]+`) 156 for _, dent := range dents { 157 if re.MatchString(dent.Name()) { 158 if err := os.Rename(filepath.Join(tmpdir, dent.Name()), dest); err != nil { 159 os.RemoveAll(tmpdir) 160 return errors.Wrap(err, "unable to rename final cache dir") 161 } 162 return nil 163 } 164 } 165 os.RemoveAll(tmpdir) 166 return errors.New("release directory not found in tarball") 167 } 168 169 func fetchRelease(prefix, tag string) (io.ReadCloser, error) { 170 const format = "https://api.github.com/repos/%s/tarball/%s" 171 prefix = prefix[len("github.com/"):] 172 url := fmt.Sprintf(format, prefix, tag) 173 resp, err := http.Get(url) 174 if err != nil { 175 return nil, errors.Wrapf(err, "failed to fetch %q", url) 176 } 177 if resp.StatusCode != 200 { 178 return nil, errors.Errorf("failed to fetch %q: expected 200, got %d", url, resp.StatusCode) 179 } 180 return resp.Body, nil 181 } 182 183 func readDepfile(ctx *Context) (map[string]map[string]string, error) { 184 file := filepath.Join(ctx.Projectdir(), "depfile") 185 debug.Debugf("loading depfile at %q", file) 186 return depfile.ParseFile(file) 187 } 188 189 func hash(arg string, args ...string) string { 190 h := sha1.New() 191 io.WriteString(h, arg) 192 for _, arg := range args { 193 io.WriteString(h, arg) 194 } 195 return fmt.Sprintf("%x", string(h.Sum(nil))) 196 } 197 198 func cachePath() string { 199 return filepath.Join(gbhome(), "cache") 200 } 201 202 func gbhome() string { 203 return envOr("GB_HOME", filepath.Join(envOr("HOME", "/tmp"), ".gb")) 204 } 205 206 func envOr(key, def string) string { 207 v := os.Getenv(key) 208 if v == "" { 209 v = def 210 } 211 return v 212 } 213 214 func isDir(path string) bool { 215 fi, err := os.Stat(path) 216 return err == nil && fi.IsDir() 217 }