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