github.com/gernest/nezuko@v0.1.2/internal/modload/init.go (about) 1 // Copyright 2018 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package modload 6 7 import ( 8 "bytes" 9 "encoding/json" 10 "fmt" 11 "io/ioutil" 12 "os" 13 "path" 14 "path/filepath" 15 "regexp" 16 "runtime/debug" 17 "sort" 18 "strconv" 19 "strings" 20 "text/template" 21 22 "github.com/gernest/nezuko/internal/base" 23 "github.com/gernest/nezuko/internal/cache" 24 "github.com/gernest/nezuko/internal/cfg" 25 "github.com/gernest/nezuko/internal/load" 26 "github.com/gernest/nezuko/internal/modfetch" 27 "github.com/gernest/nezuko/internal/modfetch/codehost" 28 "github.com/gernest/nezuko/internal/modfile" 29 "github.com/gernest/nezuko/internal/module" 30 "github.com/gernest/nezuko/internal/mvs" 31 "github.com/gernest/nezuko/internal/renameio" 32 "github.com/gernest/nezuko/internal/search" 33 ) 34 35 var ( 36 cwd string // TODO(bcmills): Is this redundant with base.Cwd? 37 initialized bool 38 39 modRoot string 40 modFile *modfile.File 41 modFileData []byte 42 excluded map[module.Version]bool 43 Target module.Version 44 45 gopath string 46 47 CmdModInit bool // running 'z mod init' 48 CmdModModule string // module argument for 'z mod init' 49 ) 50 51 // ModFile returns the parsed z.mod file. 52 // 53 // Note that after calling ImportPaths or LoadBuildList, 54 // the require statements in the modfile.File are no longer 55 // the source of truth and will be ignored: edits made directly 56 // will be lost at the next call to WriteGoMod. 57 // To make permanent changes to the require statements 58 // in z.mod, edit it before calling ImportPaths or LoadBuildList. 59 func ModFile() *modfile.File { 60 Init() 61 if modFile == nil { 62 die() 63 } 64 return modFile 65 } 66 67 func BinDir() string { 68 Init() 69 return filepath.Join(gopath, "bin") 70 } 71 72 // mustUseModules reports whether we are invoked as vgo 73 // (as opposed to go). 74 // If so, we only support builds with z.mod files. 75 func mustUseModules() bool { 76 name := os.Args[0] 77 name = name[strings.LastIndex(name, "/")+1:] 78 name = name[strings.LastIndex(name, `\`)+1:] 79 return strings.HasPrefix(name, "vgo") 80 } 81 82 // Init determines whether module mode is enabled, locates the root of the 83 // current module (if any), sets environment variables for Git subprocesses, and 84 // configures the cfg, codehost, load, modfetch, and search packages for use 85 // with modules. 86 func Init() { 87 if initialized { 88 return 89 } 90 initialized = true 91 92 // Disable any prompting for passwords by Git. 93 // Only has an effect for 2.3.0 or later, but avoiding 94 // the prompt in earlier versions is just too hard. 95 // If user has explicitly set GIT_TERMINAL_PROMPT=1, keep 96 // prompting. 97 // See golang.org/issue/9341 and golang.org/issue/12706. 98 if os.Getenv("GIT_TERMINAL_PROMPT") == "" { 99 os.Setenv("GIT_TERMINAL_PROMPT", "0") 100 } 101 102 // Disable any ssh connection pooling by Git. 103 // If a Git subprocess forks a child into the background to cache a new connection, 104 // that child keeps stdout/stderr open. After the Git subprocess exits, 105 // os /exec expects to be able to read from the stdout/stderr pipe 106 // until EOF to get all the data that the Git subprocess wrote before exiting. 107 // The EOF doesn't come until the child exits too, because the child 108 // is holding the write end of the pipe. 109 // This is unfortunate, but it has come up at least twice 110 // (see golang.org/issue/13453 and golang.org/issue/16104) 111 // and confuses users when it does. 112 // If the user has explicitly set GIT_SSH or GIT_SSH_COMMAND, 113 // assume they know what they are doing and don't step on it. 114 // But default to turning off ControlMaster. 115 if os.Getenv("GIT_SSH") == "" && os.Getenv("GIT_SSH_COMMAND") == "" { 116 os.Setenv("GIT_SSH_COMMAND", "ssh -o ControlMaster=no") 117 } 118 119 var err error 120 cwd, err = os.Getwd() 121 if err != nil { 122 base.Fatalf("z: %v", err) 123 } 124 125 if CmdModInit { 126 // Running 'z mod init': z.mod will be created in current directory. 127 modRoot = cwd 128 } else { 129 modRoot, _ = FindModuleRoot(cwd, "", true) 130 if modRoot == "" { 131 } else if search.InDir(modRoot, os.TempDir()) == "." { 132 // If you create /tmp/z.mod for experimenting, 133 // then any tests that create work directories under /tmp 134 // will find it and get modules when they're not expecting them. 135 // It's a bit of a peculiar thing to disallow but quite mysterious 136 // when it happens. See golang.org/issue/26708. 137 modRoot = "" 138 fmt.Fprintf(os.Stderr, "z: warning: ignoring z.mod in system temp root %v\n", os.TempDir()) 139 } 140 } 141 142 // We're in module mode. Install the hooks to make it work. 143 144 if c := cache.Default(); c == nil { 145 // With modules, there are no install locations for packages 146 // other than the build cache. 147 base.Fatalf("z: cannot use modules with build cache disabled") 148 } 149 150 list := filepath.SplitList(cfg.BuildContext.ZIGPATH) 151 if len(list) == 0 || list[0] == "" { 152 base.Fatalf("missing $ZIGPATH") 153 } 154 gopath = list[0] 155 if _, err := os.Stat(filepath.Join(gopath, "z.mod")); err == nil { 156 base.Fatalf("$GOPATH/z.mod exists but should not") 157 } 158 159 oldSrcMod := filepath.Join(list[0], "src/mod") 160 pkgMod := filepath.Join(list[0], "pkg/mod") 161 infoOld, errOld := os.Stat(oldSrcMod) 162 _, errMod := os.Stat(pkgMod) 163 if errOld == nil && infoOld.IsDir() && errMod != nil && os.IsNotExist(errMod) { 164 os.Rename(oldSrcMod, pkgMod) 165 } 166 167 modfetch.PkgMod = pkgMod 168 codehost.WorkRoot = filepath.Join(pkgMod, "cache/vcs") 169 170 cfg.ModulesEnabled = true 171 load.ModBinDir = BinDir 172 load.ModLookup = Lookup 173 load.ModPackageModuleInfo = PackageModuleInfo 174 load.ModImportPaths = ImportPaths 175 load.ModPackageBuildInfo = PackageBuildInfo 176 load.ModInfoProg = ModInfoProg 177 load.ModImportFromFiles = ImportFromFiles 178 load.ModDirImportPath = DirImportPath 179 180 if modRoot == "" { 181 // We're in module mode, but not inside a module. 182 // 183 // If the command is 'go get' or 'go list' and all of the args are in the 184 // same existing module, we could use that module's download directory in 185 // the module cache as the module root, applying any replacements and/or 186 // exclusions specified by that module. However, that would leave us in a 187 // strange state: we want 'go get' to be consistent with 'go list', and 'go 188 // list' should be able to operate on multiple modules. Moreover, the 'get' 189 // target might specify relative file paths (e.g. in the same repository) as 190 // replacements, and we would not be able to apply those anyway: we would 191 // need to either error out or ignore just those replacements, when a build 192 // from an empty module could proceed without error. 193 // 194 // Instead, we'll operate as though we're in some ephemeral external module, 195 // ignoring all replacements and exclusions uniformly. 196 197 // Normally we check sums using the go.sum file from the main module, but 198 // without a main module we do not have an authoritative go.sum file. 199 // 200 // TODO(bcmills): In Go 1.13, check sums when outside the main module. 201 // 202 // One possible approach is to merge the go.sum files from all of the 203 // modules we download: that doesn't protect us against bad top-level 204 // modules, but it at least ensures consistency for transitive dependencies. 205 } else { 206 modfetch.ZigSumFile = filepath.Join(modRoot, "z.sum") 207 search.SetModRoot(modRoot) 208 } 209 } 210 211 func init() { 212 load.ModInit = Init 213 214 // Set modfetch.PkgMod unconditionally, so that go clean -modcache can run even without modules enabled. 215 if list := filepath.SplitList(cfg.BuildContext.ZIGPATH); len(list) > 0 && list[0] != "" { 216 modfetch.PkgMod = filepath.Join(list[0], "pkg/mod") 217 } 218 } 219 220 // ModRoot returns the root of the main module. 221 // It calls base.Fatalf if there is no main module. 222 func ModRoot() string { 223 if !HasModRoot() { 224 die() 225 } 226 return modRoot 227 } 228 229 // HasModRoot reports whether a main module is present. 230 // HasModRoot may return false even if Enabled returns true: for example, 'get' 231 // does not require a main module. 232 func HasModRoot() bool { 233 Init() 234 return modRoot != "" 235 } 236 237 // printStackInDie causes die to print a stack trace. 238 // 239 // It is enabled by the testgo tag, and helps to diagnose paths that 240 // unexpectedly require a main module. 241 var printStackInDie = false 242 243 func die() { 244 if printStackInDie { 245 debug.PrintStack() 246 } 247 base.Fatalf("z: cannot find main module; see 'z help modules'") 248 } 249 250 // InitMod sets Target and, if there is a main module, parses the initial build 251 // list from its z.mod file, creating and populating that file if needed. 252 func InitMod() { 253 if len(buildList) > 0 { 254 return 255 } 256 257 Init() 258 if modRoot == "" { 259 Target = module.Version{Path: "command-line-arguments"} 260 buildList = []module.Version{Target} 261 return 262 } 263 264 if CmdModInit { 265 // Running z mod init: do legacy module conversion 266 legacyModInit() 267 modFileToBuildList() 268 WriteGoMod() 269 return 270 } 271 272 gomod := filepath.Join(modRoot, "z.mod") 273 data, err := ioutil.ReadFile(gomod) 274 if err != nil { 275 if os.IsNotExist(err) { 276 legacyModInit() 277 modFileToBuildList() 278 WriteGoMod() 279 return 280 } 281 base.Fatalf("z: %v", err) 282 } 283 284 f, err := modfile.Parse(gomod, data, fixVersion) 285 if err != nil { 286 // Errors returned by modfile.Parse begin with file:line. 287 base.Fatalf("z: errors parsing z.mod:\n%s\n", err) 288 } 289 modFile = f 290 modFileData = data 291 292 if len(f.Syntax.Stmt) == 0 || f.Module == nil { 293 // Empty mod file. Must add module path. 294 path, err := FindModulePath(modRoot) 295 if err != nil { 296 base.Fatalf("z: %v", err) 297 } 298 f.AddModuleStmt(path) 299 } 300 301 if len(f.Syntax.Stmt) == 1 && f.Module != nil { 302 // Entire file is just a module statement. 303 // Populate require if possible. 304 legacyModInit() 305 } 306 307 excluded = make(map[module.Version]bool) 308 for _, x := range f.Exclude { 309 excluded[x.Mod] = true 310 } 311 modFileToBuildList() 312 WriteGoMod() 313 } 314 315 // modFileToBuildList initializes buildList from the modFile. 316 func modFileToBuildList() { 317 Target = modFile.Module.Mod 318 list := []module.Version{Target} 319 for _, r := range modFile.Require { 320 list = append(list, r.Mod) 321 } 322 buildList = list 323 } 324 325 // Allowed reports whether module m is allowed (not excluded) by the main module's z.mod. 326 func Allowed(m module.Version) bool { 327 return !excluded[m] 328 } 329 330 func legacyModInit() { 331 if modFile == nil { 332 path, err := FindModulePath(modRoot) 333 if err != nil { 334 base.Fatalf("z: %v", err) 335 } 336 fmt.Fprintf(os.Stderr, "z: creating new z.mod: module %s\n", path) 337 modFile = new(modfile.File) 338 modFile.AddModuleStmt(path) 339 } 340 341 addExportsStmt() 342 } 343 344 // InitGoStmt adds a go statement, unless there already is one. 345 func InitGoStmt() { 346 if modFile.Exports == nil { 347 addExportsStmt() 348 } 349 } 350 351 // addGoStmt adds a go statement referring to the current version. 352 func addExportsStmt() { 353 name := path.Base(modFile.Module.Mod.Path) 354 if !modfile.IsValidExport(name) { 355 base.Fatalf("z: unrecognized export name %q", name) 356 } 357 if err := modFile.AddExportsStmt(name); err != nil { 358 base.Fatalf("z: internal error: %v", err) 359 } 360 } 361 362 // Exported only for testing. 363 func FindModuleRoot(dir, limit string, legacyConfigOK bool) (root, file string) { 364 dir = filepath.Clean(dir) 365 limit = filepath.Clean(limit) 366 367 // Look for enclosing z.mod. 368 for { 369 if fi, err := os.Stat(filepath.Join(dir, "z.mod")); err == nil && !fi.IsDir() { 370 return dir, "z.mod" 371 } 372 if dir == limit { 373 break 374 } 375 d := filepath.Dir(dir) 376 if d == dir { 377 break 378 } 379 dir = d 380 } 381 return "", "" 382 } 383 384 // Exported only for testing. 385 func FindModulePath(dir string) (string, error) { 386 if CmdModModule != "" { 387 // Running z mod init x/y/z; return x/y/z. 388 return CmdModModule, nil 389 } 390 391 // Cast about for import comments, 392 // first in top-level directory, then in subdirectories. 393 list, _ := ioutil.ReadDir(dir) 394 for _, info := range list { 395 if info.Mode().IsRegular() && strings.HasSuffix(info.Name(), ".go") { 396 if com := findImportComment(filepath.Join(dir, info.Name())); com != "" { 397 return com, nil 398 } 399 } 400 } 401 for _, info1 := range list { 402 if info1.IsDir() { 403 files, _ := ioutil.ReadDir(filepath.Join(dir, info1.Name())) 404 for _, info2 := range files { 405 if info2.Mode().IsRegular() && strings.HasSuffix(info2.Name(), ".go") { 406 if com := findImportComment(filepath.Join(dir, info1.Name(), info2.Name())); com != "" { 407 return path.Dir(com), nil 408 } 409 } 410 } 411 } 412 } 413 414 // Look for Godeps.json declaring import path. 415 data, _ := ioutil.ReadFile(filepath.Join(dir, "Godeps/Godeps.json")) 416 var cfg1 struct{ ImportPath string } 417 json.Unmarshal(data, &cfg1) 418 if cfg1.ImportPath != "" { 419 return cfg1.ImportPath, nil 420 } 421 422 // Look for vendor.json declaring import path. 423 data, _ = ioutil.ReadFile(filepath.Join(dir, "vendor/vendor.json")) 424 var cfg2 struct{ RootPath string } 425 json.Unmarshal(data, &cfg2) 426 if cfg2.RootPath != "" { 427 return cfg2.RootPath, nil 428 } 429 430 // Look for path in GOPATH. 431 for _, gpdir := range filepath.SplitList(cfg.BuildContext.ZIGPATH) { 432 if gpdir == "" { 433 continue 434 } 435 if rel := search.InDir(dir, filepath.Join(gpdir, "src")); rel != "" && rel != "." { 436 return filepath.ToSlash(rel), nil 437 } 438 } 439 440 // Look for .git/config with github origin as last resort. 441 data, _ = ioutil.ReadFile(filepath.Join(dir, ".git/config")) 442 if m := gitOriginRE.FindSubmatch(data); m != nil { 443 return "github.com/" + string(m[1]), nil 444 } 445 446 return "", fmt.Errorf("cannot determine module path for source directory %s (outside GOPATH, no import comments)", dir) 447 } 448 449 var ( 450 gitOriginRE = regexp.MustCompile(`(?m)^\[remote "origin"\]\r?\n\turl = (?:https://github.com/|git@github.com:|gh:)([^/]+/[^/]+?)(\.git)?\r?\n`) 451 importCommentRE = regexp.MustCompile(`(?m)^package[ \t]+[^ \t\r\n/]+[ \t]+//[ \t]+import[ \t]+(\"[^"]+\")[ \t]*\r?\n`) 452 ) 453 454 func findImportComment(file string) string { 455 data, err := ioutil.ReadFile(file) 456 if err != nil { 457 return "" 458 } 459 m := importCommentRE.FindSubmatch(data) 460 if m == nil { 461 return "" 462 } 463 path, err := strconv.Unquote(string(m[1])) 464 if err != nil { 465 return "" 466 } 467 return path 468 } 469 470 var allowWriteGoMod = true 471 472 // DisallowWriteGoMod causes future calls to WriteGoMod to do nothing at all. 473 func DisallowWriteGoMod() { 474 allowWriteGoMod = false 475 } 476 477 // AllowWriteGoMod undoes the effect of DisallowWriteGoMod: 478 // future calls to WriteGoMod will update z.mod if needed. 479 // Note that any past calls have been discarded, so typically 480 // a call to AlowWriteGoMod should be followed by a call to WriteGoMod. 481 func AllowWriteGoMod() { 482 allowWriteGoMod = true 483 } 484 485 // MinReqs returns a Reqs with minimal dependencies of Target, 486 // as will be written to z.mod. 487 func MinReqs() mvs.Reqs { 488 var direct []string 489 for _, m := range buildList[1:] { 490 if loaded.direct[m.Path] { 491 direct = append(direct, m.Path) 492 } 493 } 494 min, err := mvs.Req(Target, buildList, direct, Reqs()) 495 if err != nil { 496 base.Fatalf("z: %v", err) 497 } 498 return &mvsReqs{buildList: append([]module.Version{Target}, min...)} 499 } 500 501 // WriteGoMod writes the current build list back to z.mod. 502 func WriteGoMod() { 503 // If we're using -mod=vendor we basically ignored 504 // z.mod, so definitely don't try to write back our 505 // incomplete view of the world. 506 if !allowWriteGoMod || cfg.BuildMod == "vendor" { 507 return 508 } 509 510 // If we aren't in a module, we don't have anywhere to write a z.mod file. 511 if modRoot == "" { 512 return 513 } 514 515 if loaded != nil { 516 reqs := MinReqs() 517 min, err := reqs.Required(Target) 518 if err != nil { 519 base.Fatalf("z: %v", err) 520 } 521 var list []*modfile.Require 522 for _, m := range min { 523 list = append(list, &modfile.Require{ 524 Mod: m, 525 Indirect: !loaded.direct[m.Path], 526 }) 527 } 528 modFile.SetRequire(list) 529 } 530 531 modFile.Cleanup() // clean file after edits 532 new, err := modFile.Format() 533 if err != nil { 534 base.Fatalf("z: %v", err) 535 } 536 537 // Always update z.sum, even if we didn't change z.mod: we may have 538 // downloaded modules that we didn't have before. 539 modfetch.WriteZigSum() 540 541 if bytes.Equal(new, modFileData) { 542 // We don't need to modify z.mod from what we read previously. 543 // Ignore any intervening edits. 544 return 545 } 546 if cfg.BuildMod == "readonly" { 547 base.Fatalf("z: updates to z.mod needed, disabled by -mod=readonly") 548 } 549 550 unlock := modfetch.SideLock() 551 defer unlock() 552 553 file := filepath.Join(modRoot, "z.mod") 554 old, err := ioutil.ReadFile(file) 555 if !bytes.Equal(old, modFileData) { 556 if bytes.Equal(old, new) { 557 // Some other process wrote the same z.mod file that we were about to write. 558 modFileData = new 559 return 560 } 561 if err != nil { 562 base.Fatalf("z: can't determine whether z.mod has changed: %v", err) 563 } 564 // The contents of the z.mod file have changed. In theory we could add all 565 // of the new modules to the build list, recompute, and check whether any 566 // module in *our* build list got bumped to a different version, but that's 567 // a lot of work for marginal benefit. Instead, fail the command: if users 568 // want to run concurrent commands, they need to start with a complete, 569 // consistent module definition. 570 base.Fatalf("z: updates to z.mod needed, but contents have changed") 571 572 } 573 574 if err := renameio.WriteFile(file, new); err != nil { 575 base.Fatalf("error writing z.mod: %v", err) 576 } 577 modFileData = new 578 } 579 580 const buildFileTpl = `const build_pkg = @import("std").build; 581 const Builder = build_pkg.Builder; 582 const LibExeObjStep = build_pkg.LibExeObjStep; 583 const mod = @import("mod.zig"); 584 const ward = @import("std").debug.warn; 585 586 pub fn build(b: *Builder) void { 587 mod.build(b); 588 } 589 590 const Pkg = struct { 591 name: []const u8, 592 path: []const u8, 593 }; 594 const packages = [_]Pkg{ 595 <<range . ->> 596 Pkg{.name= "<<.Export>>", .path="<<.Path>>"}, 597 <<end>> 598 }; 599 600 // addPackages looks for all top level steps and adds packages to them. 601 fn addPackages(b: *Builder) void { 602 if (packages.len == 0) return; 603 var top_steps = b.top_level_steps.toSlice(); 604 var i: usize = 0; 605 while (i < top_steps.len) : (i += 1) { 606 var step_ptr = &top_steps[i].step; 607 var step = @fieldParentPtr(LibExeObjStep, "step", step_ptr); 608 for (packages) |pkg| { 609 step.addPackagePath(pkg.name, pkg.path); 610 } 611 } 612 } 613 ` 614 615 var bTpl = template.Must(template.New("build").Delims("<<", ">>").Parse(buildFileTpl)) 616 617 type Pkg struct { 618 Export string 619 Path string 620 } 621 622 func WriteZigBuildFile() { 623 loaded.load(func() []string { 624 return []string{Target.Path} 625 }) 626 var pkgs []*Pkg 627 var dir string 628 for _, pkg := range loaded.pkgs { 629 if pkg.mod.Path == Target.Path { 630 dir = pkg.dir 631 continue 632 } 633 pkgs = append(pkgs, &Pkg{ 634 Export: pkg.exports, 635 Path: pkg.entryFile, 636 }) 637 } 638 sort.Slice(pkgs, func(i, j int) bool { 639 return pkgs[i].Export < pkgs[j].Export 640 }) 641 var buf bytes.Buffer 642 err := bTpl.Execute(&buf, pkgs) 643 if err != nil { 644 base.Fatalf("error generating build.zig: %v", err) 645 return 646 } 647 file := cfg.BuildContext.JoinPath(dir, "build.zig") 648 if err := renameio.WriteFile(file, buf.Bytes()); err != nil { 649 base.Fatalf("error writing build.zig: %v", err) 650 } 651 } 652 653 func getEntryFile() { 654 655 } 656 657 func fixVersion(path, vers string) (string, error) { 658 // Special case: remove the old -gopkgin- hack. 659 if strings.HasPrefix(path, "gopkg.in/") && strings.Contains(vers, "-gopkgin-") { 660 vers = vers[strings.Index(vers, "-gopkgin-")+len("-gopkgin-"):] 661 } 662 663 // fixVersion is called speculatively on every 664 // module, version pair from every z.mod file. 665 // Avoid the query if it looks OK. 666 _, pathMajor, ok := module.SplitPathVersion(path) 667 if !ok { 668 return "", fmt.Errorf("malformed module path: %s", path) 669 } 670 if vers != "" && module.CanonicalVersion(vers) == vers && module.MatchPathMajor(vers, pathMajor) { 671 return vers, nil 672 } 673 674 info, err := Query(path, vers, nil) 675 if err != nil { 676 return "", err 677 } 678 return info.Version, nil 679 }