| // 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 ( |
| "archive/zip" |
| "context" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "path" |
| "path/filepath" |
| "regexp" |
| "strings" |
| |
| "golang.org/x/tools/internal/gocommand" |
| ) |
| |
| // Modules is the exporter that produces module layouts. |
| // Each "repository" is put in it's 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 := ioutil.ReadFile(gomod) |
| if err != nil { |
| return err |
| } |
| if err := ioutil.WriteFile(gomod+".temp", contents, 0644); err != nil { |
| return err |
| } |
| } |
| |
| exported.written[exported.primary]["go.mod"] = filepath.Join(primaryDir, "go.mod") |
| primaryGomod := "module " + exported.primary + "\nrequire (\n" |
| 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 |
| } |
| primaryGomod += fmt.Sprintf("\t%v %v\n", other, version) |
| } |
| primaryGomod += ")\n" |
| if err := ioutil.WriteFile(filepath.Join(primaryDir, "go.mod"), []byte(primaryGomod), 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 := ioutil.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. |
| proxyDir := 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 |
| } |
| dir := filepath.Join(proxyDir, module, "@v") |
| if err := writeModuleProxy(dir, 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"), |
| "GOPROXY="+proxyDirToURL(proxyDir), |
| "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"}, |
| Env: exported.Config.Env, |
| BuildFlags: exported.Config.BuildFlags, |
| WorkingDir: exported.Config.Dir, |
| } |
| if _, err := inv.Run(context.Background()); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // writeModuleProxy creates a directory in the proxy dir for a module. |
| func writeModuleProxy(dir, module, ver string, files map[string]string) error { |
| if err := os.MkdirAll(dir, 0755); err != nil { |
| return err |
| } |
| |
| // the modproxy checks for versions by looking at the "list" file, |
| // since we are supporting multiple versions, create the file if it does not exist or |
| // append the version number to the preexisting file. |
| f, err := os.OpenFile(filepath.Join(dir, "list"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) |
| if err != nil { |
| return err |
| } |
| if _, err := f.WriteString(ver + "\n"); err != nil { |
| return err |
| } |
| |
| // go.mod, copied from the file written in Finalize. |
| modContents, err := ioutil.ReadFile(files["go.mod"]) |
| if err != nil { |
| return err |
| } |
| if err := ioutil.WriteFile(filepath.Join(dir, ver+".mod"), modContents, 0644); err != nil { |
| return err |
| } |
| |
| // info file, just the bare bones. |
| infoContents := []byte(fmt.Sprintf(`{"Version": "%v", "Time":"2017-12-14T13:08:43Z"}`, ver)) |
| if err := ioutil.WriteFile(filepath.Join(dir, ver+".info"), infoContents, 0644); err != nil { |
| return err |
| } |
| |
| // zip of all the source files. |
| f, err = os.OpenFile(filepath.Join(dir, ver+".zip"), os.O_CREATE|os.O_WRONLY, 0644) |
| if err != nil { |
| return err |
| } |
| z := zip.NewWriter(f) |
| for name, path := range files { |
| zf, err := z.Create(module + "@" + ver + "/" + name) |
| if err != nil { |
| return err |
| } |
| contents, err := ioutil.ReadFile(path) |
| if err != nil { |
| return err |
| } |
| if _, err := zf.Write(contents); err != nil { |
| return err |
| } |
| } |
| if err := z.Close(); err != nil { |
| return err |
| } |
| if err := f.Close(); err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| 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" |
| } |