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