github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/environs/tools/build.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package tools
     5  
     6  import (
     7  	"archive/tar"
     8  	"bytes"
     9  	"compress/gzip"
    10  	"crypto/sha256"
    11  	"fmt"
    12  	"io"
    13  	"io/ioutil"
    14  	"os"
    15  	"os/exec"
    16  	"path/filepath"
    17  	"strings"
    18  
    19  	"github.com/juju/errors"
    20  	"github.com/juju/version"
    21  
    22  	"github.com/juju/juju/juju/names"
    23  )
    24  
    25  // Archive writes the executable files found in the given directory in
    26  // gzipped tar format to w.
    27  func Archive(w io.Writer, dir string) error {
    28  	entries, err := ioutil.ReadDir(dir)
    29  	if err != nil {
    30  		return err
    31  	}
    32  
    33  	gzw := gzip.NewWriter(w)
    34  	defer closeErrorCheck(&err, gzw)
    35  
    36  	tarw := tar.NewWriter(gzw)
    37  	defer closeErrorCheck(&err, tarw)
    38  
    39  	for _, ent := range entries {
    40  		h := tarHeader(ent)
    41  		logger.Debugf("adding entry: %#v", h)
    42  		// ignore local umask
    43  		if isExecutable(ent) {
    44  			h.Mode = 0755
    45  		} else {
    46  			h.Mode = 0644
    47  		}
    48  		err := tarw.WriteHeader(h)
    49  		if err != nil {
    50  			return err
    51  		}
    52  		fileName := filepath.Join(dir, ent.Name())
    53  		if err := copyFile(tarw, fileName); err != nil {
    54  			return err
    55  		}
    56  	}
    57  	return nil
    58  }
    59  
    60  // archiveAndSHA256 calls Archive with the provided arguments,
    61  // and returns a hex-encoded SHA256 hash of the resulting
    62  // archive.
    63  func archiveAndSHA256(w io.Writer, dir string) (sha256hash string, err error) {
    64  	h := sha256.New()
    65  	if err := Archive(io.MultiWriter(h, w), dir); err != nil {
    66  		return "", err
    67  	}
    68  	return fmt.Sprintf("%x", h.Sum(nil)), err
    69  }
    70  
    71  // copyFile writes the contents of the given file to w.
    72  func copyFile(w io.Writer, file string) error {
    73  	f, err := os.Open(file)
    74  	if err != nil {
    75  		return err
    76  	}
    77  	defer f.Close()
    78  	_, err = io.Copy(w, f)
    79  	return err
    80  }
    81  
    82  // tarHeader returns a tar file header given the file's stat
    83  // information.
    84  func tarHeader(i os.FileInfo) *tar.Header {
    85  	return &tar.Header{
    86  		Typeflag:   tar.TypeReg,
    87  		Name:       i.Name(),
    88  		Size:       i.Size(),
    89  		Mode:       int64(i.Mode() & 0777),
    90  		ModTime:    i.ModTime(),
    91  		AccessTime: i.ModTime(),
    92  		ChangeTime: i.ModTime(),
    93  		Uname:      "ubuntu",
    94  		Gname:      "ubuntu",
    95  	}
    96  }
    97  
    98  // isExecutable returns whether the given info
    99  // represents a regular file executable by (at least) the user.
   100  func isExecutable(i os.FileInfo) bool {
   101  	return i.Mode()&(0100|os.ModeType) == 0100
   102  }
   103  
   104  // closeErrorCheck means that we can ensure that
   105  // Close errors do not get lost even when we defer them,
   106  func closeErrorCheck(errp *error, c io.Closer) {
   107  	err := c.Close()
   108  	if *errp == nil {
   109  		*errp = err
   110  	}
   111  }
   112  
   113  func setenv(env []string, val string) []string {
   114  	prefix := val[0 : strings.Index(val, "=")+1]
   115  	for i, eval := range env {
   116  		if strings.HasPrefix(eval, prefix) {
   117  			env[i] = val
   118  			return env
   119  		}
   120  	}
   121  	return append(env, val)
   122  }
   123  
   124  func findExecutable(execFile string) (string, error) {
   125  	logger.Debugf("looking for: %s", execFile)
   126  	if filepath.IsAbs(execFile) {
   127  		return execFile, nil
   128  	}
   129  
   130  	dir, file := filepath.Split(execFile)
   131  
   132  	// Now we have two possibilities:
   133  	//   file == path indicating that the PATH was searched
   134  	//   dir != "" indicating that it is a relative path
   135  
   136  	if dir == "" {
   137  		path := os.Getenv("PATH")
   138  		for _, name := range filepath.SplitList(path) {
   139  			result := filepath.Join(name, file)
   140  			// Use exec.LookPath() to check if the file exists and is executable`
   141  			f, err := exec.LookPath(result)
   142  			if err == nil {
   143  				return f, nil
   144  			}
   145  		}
   146  
   147  		return "", fmt.Errorf("could not find %q in the path", file)
   148  	}
   149  	cwd, err := os.Getwd()
   150  	if err != nil {
   151  		return "", err
   152  	}
   153  	return filepath.Clean(filepath.Join(cwd, execFile)), nil
   154  }
   155  
   156  func copyExistingJujud(dir string) error {
   157  	// Assume that the user is running juju.
   158  	jujuLocation, err := findExecutable(os.Args[0])
   159  	if err != nil {
   160  		logger.Infof("%v", err)
   161  		return err
   162  	}
   163  	jujudLocation := filepath.Join(filepath.Dir(jujuLocation), names.Jujud)
   164  	logger.Debugf("checking: %s", jujudLocation)
   165  	info, err := os.Stat(jujudLocation)
   166  	if err != nil {
   167  		logger.Infof("couldn't find existing jujud")
   168  		return err
   169  	}
   170  	logger.Infof("found existing jujud")
   171  	// TODO(thumper): break this out into a util function.
   172  	// copy the file into the dir.
   173  	source, err := os.Open(jujudLocation)
   174  	if err != nil {
   175  		logger.Infof("open source failed: %v", err)
   176  		return err
   177  	}
   178  	defer source.Close()
   179  	target := filepath.Join(dir, names.Jujud)
   180  	logger.Infof("target: %v", target)
   181  	destination, err := os.OpenFile(target, os.O_RDWR|os.O_TRUNC|os.O_CREATE, info.Mode())
   182  	if err != nil {
   183  		logger.Infof("open destination failed: %v", err)
   184  		return err
   185  	}
   186  	defer destination.Close()
   187  	_, err = io.Copy(destination, source)
   188  	if err != nil {
   189  		return err
   190  	}
   191  	return nil
   192  }
   193  
   194  func buildJujud(dir string) error {
   195  	logger.Infof("building jujud")
   196  	cmds := [][]string{
   197  		{"go", "build", "-gccgoflags=-static-libgo", "-o", filepath.Join(dir, names.Jujud), "github.com/juju/juju/cmd/jujud"},
   198  	}
   199  	for _, args := range cmds {
   200  		cmd := exec.Command(args[0], args[1:]...)
   201  		out, err := cmd.CombinedOutput()
   202  		if err != nil {
   203  			return fmt.Errorf("build command %q failed: %v; %s", args[0], err, out)
   204  		}
   205  	}
   206  	return nil
   207  }
   208  
   209  // BundleToolsFunc is a function which can bundle all the current juju tools
   210  // in gzipped tar format to the given writer.
   211  type BundleToolsFunc func(w io.Writer, forceVersion *version.Number) (version.Binary, string, error)
   212  
   213  // Override for testing.
   214  var BundleTools BundleToolsFunc = bundleTools
   215  
   216  // bundleTools bundles all the current juju tools in gzipped tar
   217  // format to the given writer.
   218  // If forceVersion is not nil, a FORCE-VERSION file is included in
   219  // the tools bundle so it will lie about its current version number.
   220  func bundleTools(w io.Writer, forceVersion *version.Number) (tvers version.Binary, sha256Hash string, err error) {
   221  	dir, err := ioutil.TempDir("", "juju-tools")
   222  	if err != nil {
   223  		return version.Binary{}, "", err
   224  	}
   225  	defer os.RemoveAll(dir)
   226  
   227  	if err := copyExistingJujud(dir); err != nil {
   228  		logger.Debugf("copy existing failed: %v", err)
   229  		if err := buildJujud(dir); err != nil {
   230  			return version.Binary{}, "", err
   231  		}
   232  	}
   233  
   234  	if forceVersion != nil {
   235  		logger.Debugf("forcing version to %s", forceVersion)
   236  		if err := ioutil.WriteFile(filepath.Join(dir, "FORCE-VERSION"), []byte(forceVersion.String()), 0666); err != nil {
   237  			return version.Binary{}, "", err
   238  		}
   239  	}
   240  
   241  	tvers, err = getVersionFromJujud(dir)
   242  	if err != nil {
   243  		return version.Binary{}, "", errors.Trace(err)
   244  	}
   245  
   246  	sha256hash, err := archiveAndSHA256(w, dir)
   247  	if err != nil {
   248  		return version.Binary{}, "", err
   249  	}
   250  	return tvers, sha256hash, err
   251  }
   252  
   253  var execCommand = exec.Command
   254  
   255  func getVersionFromJujud(dir string) (version.Binary, error) {
   256  	path := filepath.Join(dir, names.Jujud)
   257  	cmd := execCommand(path, "version")
   258  	var stdout, stderr bytes.Buffer
   259  	cmd.Stdout = &stdout
   260  	cmd.Stderr = &stderr
   261  
   262  	if err := cmd.Run(); err != nil {
   263  		return version.Binary{}, errors.Errorf("cannot get version from %q: %v; %s", path, err, stderr.String()+stdout.String())
   264  	}
   265  	tvs := strings.TrimSpace(stdout.String())
   266  	tvers, err := version.ParseBinary(tvs)
   267  	if err != nil {
   268  		return version.Binary{}, errors.Errorf("invalid version %q printed by jujud", tvs)
   269  	}
   270  	return tvers, nil
   271  }