github.com/kekek/gb@v0.4.5-0.20170222120241-d4ba64b0b297/depfile.go (about)

     1  package gb
     2  
     3  import (
     4  	"compress/gzip"
     5  	"crypto/sha1"
     6  	"fmt"
     7  	"go/build"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"os"
    12  	"path/filepath"
    13  	"regexp"
    14  	"strings"
    15  
    16  	"github.com/constabulary/gb/internal/debug"
    17  	"github.com/constabulary/gb/internal/depfile"
    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(bc *build.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{
    61  					Context: bc,
    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{
    88  					Context: bc,
    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  }