| // Copyright 2018 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package packagestest | |
| import ( | |
| "bytes" | |
| "context" | |
| "fmt" | |
| "os" | |
| "path" | |
| "path/filepath" | |
| "regexp" | |
| "strings" | |
| "golang.org/x/tools/internal/gocommand" | |
| "golang.org/x/tools/internal/proxydir" | |
| ) | |
| // Modules is the exporter that produces module layouts. | |
| // Each "repository" is put in its own module, and the module file generated | |
| // will have replace directives for all other modules. | |
| // Given the two files | |
| // | |
| // golang.org/repoa#a/a.go | |
| // golang.org/repob#b/b.go | |
| // | |
| // You would get the directory layout | |
| // | |
| // /sometemporarydirectory | |
| // ├── repoa | |
| // │ ├── a | |
| // │ │ └── a.go | |
| // │ └── go.mod | |
| // └── repob | |
| // ├── b | |
| // │ └── b.go | |
| // └── go.mod | |
| // | |
| // and the working directory would be | |
| // | |
| // /sometemporarydirectory/repoa | |
| var Modules = modules{} | |
| type modules struct{} | |
| type moduleAtVersion struct { | |
| module string | |
| version string | |
| } | |
| func (modules) Name() string { | |
| return "Modules" | |
| } | |
| func (modules) Filename(exported *Exported, module, fragment string) string { | |
| if module == exported.primary { | |
| return filepath.Join(primaryDir(exported), fragment) | |
| } | |
| return filepath.Join(moduleDir(exported, module), fragment) | |
| } | |
| func (modules) Finalize(exported *Exported) error { | |
| // Write out the primary module. This module can use symlinks and | |
| // other weird stuff, and will be the working dir for the go command. | |
| // It depends on all the other modules. | |
| primaryDir := primaryDir(exported) | |
| if err := os.MkdirAll(primaryDir, 0755); err != nil { | |
| return err | |
| } | |
| exported.Config.Dir = primaryDir | |
| if exported.written[exported.primary] == nil { | |
| exported.written[exported.primary] = make(map[string]string) | |
| } | |
| // Create a map of modulepath -> {module, version} for modulepaths | |
| // that are of the form `repoa/mod1@v1.1.0`. | |
| versions := make(map[string]moduleAtVersion) | |
| for module := range exported.written { | |
| if splt := strings.Split(module, "@"); len(splt) > 1 { | |
| versions[module] = moduleAtVersion{ | |
| module: splt[0], | |
| version: splt[1], | |
| } | |
| } | |
| } | |
| // If the primary module already has a go.mod, write the contents to a temp | |
| // go.mod for now and then we will reset it when we are getting all the markers. | |
| if gomod := exported.written[exported.primary]["go.mod"]; gomod != "" { | |
| contents, err := os.ReadFile(gomod) | |
| if err != nil { | |
| return err | |
| } | |
| if err := os.WriteFile(gomod+".temp", contents, 0644); err != nil { | |
| return err | |
| } | |
| } | |
| exported.written[exported.primary]["go.mod"] = filepath.Join(primaryDir, "go.mod") | |
| var primaryGomod bytes.Buffer | |
| fmt.Fprintf(&primaryGomod, "module %s\nrequire (\n", exported.primary) | |
| for other := range exported.written { | |
| if other == exported.primary { | |
| continue | |
| } | |
| version := moduleVersion(other) | |
| // If other is of the form `repo1/mod1@v1.1.0`, | |
| // then we need to extract the module and the version. | |
| if v, ok := versions[other]; ok { | |
| other = v.module | |
| version = v.version | |
| } | |
| fmt.Fprintf(&primaryGomod, "\t%v %v\n", other, version) | |
| } | |
| fmt.Fprintf(&primaryGomod, ")\n") | |
| if err := os.WriteFile(filepath.Join(primaryDir, "go.mod"), primaryGomod.Bytes(), 0644); err != nil { | |
| return err | |
| } | |
| // Create the mod cache so we can rename it later, even if we don't need it. | |
| if err := os.MkdirAll(modCache(exported), 0755); err != nil { | |
| return err | |
| } | |
| // Write out the go.mod files for the other modules. | |
| for module, files := range exported.written { | |
| if module == exported.primary { | |
| continue | |
| } | |
| dir := moduleDir(exported, module) | |
| modfile := filepath.Join(dir, "go.mod") | |
| // If other is of the form `repo1/mod1@v1.1.0`, | |
| // then we need to extract the module name without the version. | |
| if v, ok := versions[module]; ok { | |
| module = v.module | |
| } | |
| if err := os.WriteFile(modfile, []byte("module "+module+"\n"), 0644); err != nil { | |
| return err | |
| } | |
| files["go.mod"] = modfile | |
| } | |
| // Zip up all the secondary modules into the proxy dir. | |
| modProxyDir := filepath.Join(exported.temp, "modproxy") | |
| for module, files := range exported.written { | |
| if module == exported.primary { | |
| continue | |
| } | |
| version := moduleVersion(module) | |
| // If other is of the form `repo1/mod1@v1.1.0`, | |
| // then we need to extract the module and the version. | |
| if v, ok := versions[module]; ok { | |
| module = v.module | |
| version = v.version | |
| } | |
| if err := writeModuleFiles(modProxyDir, module, version, files); err != nil { | |
| return fmt.Errorf("creating module proxy dir for %v: %v", module, err) | |
| } | |
| } | |
| // Discard the original mod cache dir, which contained the files written | |
| // for us by Export. | |
| if err := os.Rename(modCache(exported), modCache(exported)+".orig"); err != nil { | |
| return err | |
| } | |
| exported.Config.Env = append(exported.Config.Env, | |
| "GO111MODULE=on", | |
| "GOPATH="+filepath.Join(exported.temp, "modcache"), | |
| "GOMODCACHE=", | |
| "GOPROXY="+proxydir.ToURL(modProxyDir), | |
| "GOSUMDB=off", | |
| ) | |
| // Run go mod download to recreate the mod cache dir with all the extra | |
| // stuff in cache. All the files created by Export should be recreated. | |
| inv := gocommand.Invocation{ | |
| Verb: "mod", | |
| Args: []string{"download", "all"}, | |
| Env: exported.Config.Env, | |
| BuildFlags: exported.Config.BuildFlags, | |
| WorkingDir: exported.Config.Dir, | |
| } | |
| _, err := new(gocommand.Runner).Run(context.Background(), inv) | |
| return err | |
| } | |
| func writeModuleFiles(rootDir, module, ver string, filePaths map[string]string) error { | |
| fileData := make(map[string][]byte) | |
| for name, path := range filePaths { | |
| contents, err := os.ReadFile(path) | |
| if err != nil { | |
| return err | |
| } | |
| fileData[name] = contents | |
| } | |
| return proxydir.WriteModuleVersion(rootDir, module, ver, fileData) | |
| } | |
| func modCache(exported *Exported) string { | |
| return filepath.Join(exported.temp, "modcache/pkg/mod") | |
| } | |
| func primaryDir(exported *Exported) string { | |
| return filepath.Join(exported.temp, path.Base(exported.primary)) | |
| } | |
| func moduleDir(exported *Exported, module string) string { | |
| if strings.Contains(module, "@") { | |
| return filepath.Join(modCache(exported), module) | |
| } | |
| return filepath.Join(modCache(exported), path.Dir(module), path.Base(module)+"@"+moduleVersion(module)) | |
| } | |
| var versionSuffixRE = regexp.MustCompile(`v\d+`) | |
| func moduleVersion(module string) string { | |
| if versionSuffixRE.MatchString(path.Base(module)) { | |
| return path.Base(module) + ".0.0" | |
| } | |
| return "v1.0.0" | |
| } |