github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/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 "launchpad.net/juju-core/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", "install", "launchpad.net/juju-core/cmd/jujud"}, 190 {"strip", dir + "/jujud"}, 191 } 192 env := setenv(os.Environ(), "GOBIN="+dir) 193 for _, args := range cmds { 194 cmd := exec.Command(args[0], args[1:]...) 195 cmd.Env = env 196 out, err := cmd.CombinedOutput() 197 if err != nil { 198 return fmt.Errorf("build command %q failed: %v; %s", args[0], err, out) 199 } 200 } 201 return nil 202 } 203 204 // BundleTools bundles all the current juju tools in gzipped tar 205 // format to the given writer. 206 // If forceVersion is not nil, a FORCE-VERSION file is included in 207 // the tools bundle so it will lie about its current version number. 208 func BundleTools(w io.Writer, forceVersion *version.Number) (tvers version.Binary, sha256Hash string, err error) { 209 dir, err := ioutil.TempDir("", "juju-tools") 210 if err != nil { 211 return version.Binary{}, "", err 212 } 213 defer os.RemoveAll(dir) 214 215 if err := copyExistingJujud(dir); err != nil { 216 logger.Debugf("copy existing failed: %v", err) 217 if err := buildJujud(dir); err != nil { 218 return version.Binary{}, "", err 219 } 220 } 221 222 if forceVersion != nil { 223 logger.Debugf("forcing version to %s", forceVersion) 224 if err := ioutil.WriteFile(filepath.Join(dir, "FORCE-VERSION"), []byte(forceVersion.String()), 0666); err != nil { 225 return version.Binary{}, "", err 226 } 227 } 228 cmd := exec.Command(filepath.Join(dir, "jujud"), "version") 229 out, err := cmd.CombinedOutput() 230 if err != nil { 231 return version.Binary{}, "", fmt.Errorf("cannot get version from %q: %v; %s", cmd.Args[0], err, out) 232 } 233 tvs := strings.TrimSpace(string(out)) 234 tvers, err = version.ParseBinary(tvs) 235 if err != nil { 236 return version.Binary{}, "", fmt.Errorf("invalid version %q printed by jujud", tvs) 237 } 238 sha256Hash, err = archive(w, dir) 239 if err != nil { 240 return version.Binary{}, "", err 241 } 242 return tvers, sha256Hash, err 243 }