| // 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" |
| } |