github.com/alexanderthaller/godep@v0.0.0-20141231210904-0baa7ea46402/save.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "encoding/json" 7 "errors" 8 "io" 9 "io/ioutil" 10 "log" 11 "os" 12 "path/filepath" 13 "regexp" 14 "strings" 15 16 "github.com/kr/fs" 17 ) 18 19 var cmdSave = &Command{ 20 Usage: "save [-r] [packages]", 21 Short: "list and copy dependencies into Godeps", 22 Long: ` 23 Save writes a list of the dependencies of the named packages along 24 with the exact source control revision of each dependency, and copies 25 their source code into a subdirectory. 26 27 The list is written to Godeps/Godeps.json, and source code for all 28 dependencies is copied into Godeps/_workspace. 29 30 The dependency list is a JSON document with the following structure: 31 32 type Godeps struct { 33 ImportPath string 34 GoVersion string // Abridged output of 'go version'. 35 Packages []string // Arguments to godep save, if any. 36 Deps []struct { 37 ImportPath string 38 Comment string // Tag or description of commit. 39 Rev string // VCS-specific commit ID. 40 } 41 } 42 43 Any dependencies already present in the list will be left unchanged. 44 To update a dependency to a newer revision, use 'godep update'. 45 46 If -r is given, import statements will be rewritten to refer 47 directly to the copied source code. 48 49 For more about specifying packages, see 'go help packages'. 50 `, 51 Run: runSave, 52 } 53 54 var ( 55 saveCopy = true 56 saveR = false 57 ) 58 59 func init() { 60 cmdSave.Flag.BoolVar(&saveCopy, "copy", true, "copy source code") 61 cmdSave.Flag.BoolVar(&saveR, "r", false, "rewrite import paths") 62 } 63 64 func runSave(cmd *Command, args []string) { 65 if !saveCopy { 66 log.Println("flag unsupported: -copy=false") 67 cmd.UsageExit() 68 } 69 err := save(args) 70 if err != nil { 71 log.Fatalln(err) 72 } 73 } 74 75 func save(pkgs []string) error { 76 dot, err := LoadPackages(".") 77 if err != nil { 78 return err 79 } 80 ver, err := goVersion() 81 if err != nil { 82 return err 83 } 84 manifest := filepath.Join("Godeps", "Godeps.json") 85 var gold Godeps 86 oldIsFile, err := readOldGodeps(&gold) 87 if err != nil { 88 return err 89 } 90 gnew := &Godeps{ 91 ImportPath: dot[0].ImportPath, 92 GoVersion: ver, 93 } 94 if len(pkgs) > 0 { 95 gnew.Packages = pkgs 96 } else { 97 pkgs = []string{"."} 98 } 99 a, err := LoadPackages(pkgs...) 100 if err != nil { 101 return err 102 } 103 err = gnew.Load(a) 104 if err != nil { 105 return err 106 } 107 if gnew.Deps == nil { 108 gnew.Deps = make([]Dependency, 0) // produce json [], not null 109 } 110 gdisk := copyGodeps(gnew) 111 err = carryVersions(&gold, gnew) 112 if err != nil { 113 return err 114 } 115 if oldIsFile { 116 // If we are migrating from an old format file, 117 // we require that the listed version of every 118 // dependency must be installed in GOPATH, so it's 119 // available to copy. 120 if !eqDeps(gnew.Deps, gdisk.Deps) { 121 return errors.New(strings.TrimSpace(needRestore)) 122 } 123 gold = Godeps{} 124 } 125 os.Remove("Godeps") // remove regular file if present; ignore error 126 readme := filepath.Join("Godeps", "Readme") 127 err = writeFile(readme, strings.TrimSpace(Readme)+"\n") 128 if err != nil { 129 log.Println(err) 130 } 131 f, err := os.Create(manifest) 132 if err != nil { 133 return err 134 } 135 _, err = gnew.WriteTo(f) 136 if err != nil { 137 f.Close() 138 return err 139 } 140 err = f.Close() 141 if err != nil { 142 return err 143 } 144 // We use a name starting with "_" so the go tool 145 // ignores this directory when traversing packages 146 // starting at the project's root. For example, 147 // godep go list ./... 148 workspace := filepath.Join("Godeps", "_workspace") 149 srcdir := filepath.Join(workspace, "src") 150 rem := subDeps(gold.Deps, gnew.Deps) 151 add := subDeps(gnew.Deps, gold.Deps) 152 err = removeSrc(srcdir, rem) 153 if err != nil { 154 return err 155 } 156 err = copySrc(srcdir, add) 157 if err != nil { 158 return err 159 } 160 writeVCSIgnore(workspace) 161 var rewritePaths []string 162 if saveR { 163 for _, dep := range gnew.Deps { 164 rewritePaths = append(rewritePaths, dep.ImportPath) 165 } 166 } 167 return rewrite(a, dot[0].ImportPath, rewritePaths) 168 } 169 170 func readOldGodeps(g *Godeps) (isFile bool, err error) { 171 f, err := os.Open(filepath.Join("Godeps", "Godeps.json")) 172 if err != nil { 173 isFile = true 174 f, err = os.Open("Godeps") 175 } 176 if os.IsNotExist(err) { 177 return false, nil 178 } 179 if err != nil { 180 return false, err 181 } 182 err = json.NewDecoder(f).Decode(g) 183 f.Close() 184 return isFile, err 185 } 186 187 type revError struct { 188 ImportPath string 189 HaveRev string 190 WantRev string 191 } 192 193 func (v *revError) Error() string { 194 return v.ImportPath + ": revision is " + v.HaveRev + ", want " + v.WantRev 195 } 196 197 // carryVersions copies Rev and Comment from a to b for 198 // each dependency with an identical ImportPath. For any 199 // dependency in b that appears to be from the same repo 200 // as one in a (for example, a parent or child directory), 201 // the Rev must already match - otherwise it is an error. 202 func carryVersions(a, b *Godeps) error { 203 for i := range b.Deps { 204 err := carryVersion(a, &b.Deps[i]) 205 if err != nil { 206 return err 207 } 208 } 209 return nil 210 } 211 212 func carryVersion(a *Godeps, db *Dependency) error { 213 // First see if this exact package is already in the list. 214 for _, da := range a.Deps { 215 if db.ImportPath == da.ImportPath { 216 db.Rev = da.Rev 217 db.Comment = da.Comment 218 return nil 219 } 220 } 221 // No exact match, check for child or sibling package. 222 // We can't handle mismatched versions for packages in 223 // the same repo, so report that as an error. 224 for _, da := range a.Deps { 225 switch { 226 case strings.HasPrefix(db.ImportPath, da.ImportPath+"/"): 227 if da.Rev != db.Rev { 228 return &revError{db.ImportPath, db.Rev, da.Rev} 229 } 230 case strings.HasPrefix(da.ImportPath, db.root+"/"): 231 if da.Rev != db.Rev { 232 return &revError{db.ImportPath, db.Rev, da.Rev} 233 } 234 } 235 } 236 // No related package in the list, must be a new repo. 237 return nil 238 } 239 240 // subDeps returns a - b, using ImportPath for equality. 241 func subDeps(a, b []Dependency) (diff []Dependency) { 242 Diff: 243 for _, da := range a { 244 for _, db := range b { 245 if da.ImportPath == db.ImportPath { 246 continue Diff 247 } 248 } 249 diff = append(diff, da) 250 } 251 return diff 252 } 253 254 func removeSrc(srcdir string, deps []Dependency) error { 255 for _, dep := range deps { 256 path := filepath.FromSlash(dep.ImportPath) 257 err := os.RemoveAll(filepath.Join(srcdir, path)) 258 if err != nil { 259 return err 260 } 261 } 262 return nil 263 } 264 265 func copySrc(dir string, deps []Dependency) error { 266 ok := true 267 for _, dep := range deps { 268 srcdir := filepath.Join(dep.ws, "src") 269 rel, err := filepath.Rel(srcdir, dep.dir) 270 if err != nil { // this should never happen 271 return err 272 } 273 dstpkgroot := filepath.Join(dir, rel) 274 err = os.RemoveAll(dstpkgroot) 275 if err != nil { 276 log.Println(err) 277 ok = false 278 } 279 w := fs.Walk(dep.dir) 280 for w.Step() { 281 err = copyPkgFile(dir, srcdir, w) 282 if err != nil { 283 log.Println(err) 284 ok = false 285 } 286 } 287 } 288 if !ok { 289 return errors.New("error copying source code") 290 } 291 return nil 292 } 293 294 func copyPkgFile(dstroot, srcroot string, w *fs.Walker) error { 295 if w.Err() != nil { 296 return w.Err() 297 } 298 if c := w.Stat().Name()[0]; c == '.' || c == '_' { 299 // Skip directories using a rule similar to how 300 // the go tool enumerates packages. 301 // See $GOROOT/src/cmd/go/main.go:/matchPackagesInFs 302 w.SkipDir() 303 } 304 if w.Stat().IsDir() { 305 return nil 306 } 307 rel, err := filepath.Rel(srcroot, w.Path()) 308 if err != nil { // this should never happen 309 return err 310 } 311 return copyFile(filepath.Join(dstroot, rel), w.Path()) 312 } 313 314 // copyFile copies a regular file from src to dst. 315 // dst is opened with os.Create. 316 // If the file name ends with .go, 317 // copyFile strips canonical import path annotations. 318 // These are comments of the form: 319 // package foo // import "bar/foo" 320 // package foo /* import "bar/foo" */ 321 func copyFile(dst, src string) error { 322 err := os.MkdirAll(filepath.Dir(dst), 0777) 323 if err != nil { 324 return err 325 } 326 327 linkDst, err := os.Readlink(src) 328 if err == nil { 329 return os.Symlink(linkDst, dst) 330 } 331 332 r, err := os.Open(src) 333 if err != nil { 334 return err 335 } 336 defer r.Close() 337 338 w, err := os.Create(dst) 339 if err != nil { 340 return err 341 } 342 343 if strings.HasSuffix(dst, ".go") { 344 err = copyWithoutImportComment(w, r) 345 } else { 346 _, err = io.Copy(w, r) 347 } 348 err1 := w.Close() 349 if err == nil { 350 err = err1 351 } 352 353 return err 354 } 355 356 func copyWithoutImportComment(w io.Writer, r io.Reader) error { 357 sc := bufio.NewScanner(r) 358 for sc.Scan() { 359 _, err := w.Write(append(stripImportComment(sc.Bytes()), '\n')) 360 if err != nil { 361 return err 362 } 363 } 364 return nil 365 } 366 367 const ( 368 importAnnotation = `import\s+(?:"[^"]*"|` + "`[^`]*`" + `)` 369 importComment = `(?://\s*` + importAnnotation + `\s*$|/\*\s*` + importAnnotation + `\s*\*/)` 370 ) 371 372 var ( 373 importCommentRE = regexp.MustCompile(`^\s*(package\s+\w+)\s+` + importComment + `(.*)`) 374 pkgPrefix = []byte("package ") 375 ) 376 377 // stripImportComment returns line with its import comment removed. 378 // If s is not a package statement containing an import comment, 379 // it is returned unaltered. 380 // See also http://golang.org/s/go14customimport. 381 func stripImportComment(line []byte) []byte { 382 if !bytes.HasPrefix(line, pkgPrefix) { 383 // Fast path; this will skip all but one line in the file. 384 // This assumes there is no whitespace before the keyword. 385 return line 386 } 387 if m := importCommentRE.FindSubmatch(line); m != nil { 388 return append(m[1], m[2]...) 389 } 390 return line 391 } 392 393 // Func writeVCSIgnore writes "ignore" files inside dir for known VCSs, 394 // so that dir/pkg and dir/bin don't accidentally get committed. 395 // It logs any errors it encounters. 396 func writeVCSIgnore(dir string) { 397 // Currently git is the only VCS for which we know how to do this. 398 // Mercurial and Bazaar have similar mechasims, but they apparently 399 // require writing files outside of dir. 400 const ignore = "/pkg\n/bin\n" 401 name := filepath.Join(dir, ".gitignore") 402 err := writeFile(name, ignore) 403 if err != nil { 404 log.Println(err) 405 } 406 } 407 408 // writeFile is like ioutil.WriteFile but it creates 409 // intermediate directories with os.MkdirAll. 410 func writeFile(name, body string) error { 411 err := os.MkdirAll(filepath.Dir(name), 0777) 412 if err != nil { 413 return err 414 } 415 return ioutil.WriteFile(name, []byte(body), 0666) 416 } 417 418 const ( 419 Readme = ` 420 This directory tree is generated automatically by godep. 421 422 Please do not edit. 423 424 See https://github.com/tools/godep for more information. 425 ` 426 needRestore = ` 427 mismatched versions while migrating 428 429 It looks like you are switching from the old Godeps format 430 (from flag -copy=false). The old format is just a file; it 431 doesn't contain source code. For this migration, godep needs 432 the appropriate version of each dependency to be installed in 433 GOPATH, so that the source code is available to copy. 434 435 To fix this, run 'godep restore'. 436 ` 437 )