Repository: nomad-software/vend Branch: master Commit: a1ea6c775ac2 Files: 10 Total size: 10.9 KB Directory structure: gitextract_7qwgw9ce/ ├── LICENSE ├── README.md ├── cli/ │ ├── cli.go │ └── cmd.go ├── file/ │ ├── dep.go │ ├── file.go │ └── mod.go ├── go.mod ├── main.go └── output/ └── output.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Gary Willoughby Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Vend **A small command line utility for fully vendoring module dependencies** --- ## Why? [Because Google have a different idea of what vendoring means than the rest of us.](https://github.com/golang/go/issues/26366) If you use the built-in `go mod vendor` command, it will only cherry pick certain files for inclusion in the vendor folder. This can cause problems when using [Cgo](https://blog.golang.org/c-go-cgo) because it [ignores C files that are not in the package directory](https://github.com/golang/go/issues/26366#issuecomment-405683150). Tests and examples for dependencies are ignored too. This tool copies the entire dependency tree into the vendor folder like every other package manager does and how every sane developer would expect it to work. It can be used safely in the `$GOPATH` or elsewhere. This package expects that the new [module system](https://github.com/golang/go/wiki/Modules) [introduced in v1.11](https://golang.org/doc/go1.11) is being used. ## What does it do? This tool fully copies all files from your project's imported dependencies into the `vendor` folder. This allows you to: 1. Always have access to _all_ files in your dependencies, even if they go offline 2. Always be able to build your project on a disconnected computer 3. Always be able to run the tests or benchmarks for all your dependencies ## Supported Go versions * v1.11+ ## Install ``` $ go get github.com/nomad-software/vend ``` ## Usage ``` $ cd $GOPATH/mypackage $ vend ``` ## Help Run the following command for help. ``` $ vend -help ``` ================================================ FILE: cli/cli.go ================================================ package cli import ( "flag" "fmt" ) // Options contains CLI arguments passed to the program. type Options struct { Help bool PkgOnly bool } // ParseOptions parses the command line options and returns a struct filled with // the relevant options. func ParseOptions() Options { var opt Options flag.BoolVar(&opt.Help, "help", false, "Show help.") flag.Parse() return opt } // PrintUsage prints the usage of this tool. func (opt *Options) PrintUsage() { const banner string = ` _ __ _____ _ __ __| | \ \ / / _ \ '_ \ / _' | \ V / __/ | | | (_| | \_/ \___|_| |_|\__,_| ` fmt.Println(banner) fmt.Printf("A small command line utility for fully vendoring module dependencies\n\n") flag.Usage() } ================================================ FILE: cli/cmd.go ================================================ package cli import ( "os" "os/exec" "github.com/nomad-software/vend/output" ) // UpdateModule makes sure the module is updated ready to vendor the // dependencies. func UpdateModule() { var commands = []string{"tidy", "download", "vendor"} for _, command := range commands { cmd := exec.Command("go", "mod", command) cmd.Env = buildEnv() cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err := cmd.Run() output.OnError(err, command) } } // ReadModJSON reads the module file and returns a Json string. func ReadModJSON() string { cmd := exec.Command("go", "mod", "edit", "-json") cmd.Env = buildEnv() cmd.Stderr = os.Stderr b, err := cmd.Output() output.OnError(err, "Error running 'go mod edit'") return string(b) } // ReadDownloadJSON reads dependency information and returns a Json string. func ReadDownloadJSON() string { cmd := exec.Command("go", "mod", "download", "-json") cmd.Env = buildEnv() cmd.Stderr = os.Stderr b, err := cmd.Output() output.OnError(err, "Error running 'go mod download'") return string(b) } // BuildEnv creates the environment in which to run the commands. func buildEnv() []string { env := os.Environ() env = append(env, "GO111MODULE=on") return env } ================================================ FILE: file/dep.go ================================================ package file import ( "encoding/json" "io" "strings" "github.com/nomad-software/vend/output" ) // ParseDownloadJSON parses the dependency file into a data structure. func ParseDownloadJSON(raw string) []Dep { decoder := json.NewDecoder(strings.NewReader(raw)) data := make([]Dep, 0, 10) for { var dep Dep err := decoder.Decode(&dep) if err != nil { if err == io.EOF { break } output.OnError(err, "Error decoding dependency json") } data = append(data, dep) } return data } // Dep represents parsed dependency json data. type Dep struct { Path string Version string Error string Info string GoMod string Zip string Dir string Sum string GoModSum string } ================================================ FILE: file/file.go ================================================ package file import ( "fmt" "io" "io/ioutil" "os" "path" "path/filepath" "github.com/nomad-software/vend/cli" "github.com/nomad-software/vend/output" ) // VendorDir represents a vendor directory. type VendorDir struct { basePath string modFileContent []byte mod GoMod deps []Dep } // InitVendorDir creates a new vendor directory. func InitVendorDir() VendorDir { wd, err := os.Getwd() output.OnError(err, "Error getting the current directory") v := VendorDir{ basePath: path.Join(wd, "vendor"), mod: ParseModJSON(cli.ReadModJSON()), deps: ParseDownloadJSON(cli.ReadDownloadJSON()), } if !v.exists(v.basePath) { output.Fatal("vend: no dependencies to vendor") } return v } // CopyDependencies copies remote module level dependencies transitively. func (v *VendorDir) CopyDependencies() { v.clear() for _, d := range v.deps { fmt.Printf("vend: copying %s (%s)\n", d.Path, d.Version) v.copy(d.Dir, v.vendPath(d.Path)) } for _, r := range v.mod.Replace { if r.Old.Path != r.New.Path { fmt.Printf("vend: replacing %s with %s\n", r.Old.Path, r.New.Path) newPath := v.vendPath(r.New.Path) oldPath := v.vendPath(r.Old.Path) // If the directory is in the vendor folder it was copied from the // module cache so we can just rename it. Otherwise it's a local // directory located somewhere else that needs copying in. if v.exists(newPath) { v.copy(newPath, oldPath) v.remove(newPath) } else { v.copy(r.New.Path, oldPath) } } } } // exists checks if a file exists. func (v *VendorDir) exists(file string) bool { _, err := os.Stat(file) return !os.IsNotExist(err) } // remove removes a path. func (v *VendorDir) remove(p string) { err := os.RemoveAll(p) output.OnError(err, "Error removing path") } // vendPath creates a vendor directory path. func (v *VendorDir) vendPath(p string) string { return path.Join(v.basePath, p) } // copyModFile internally copies and saves the modules.txt file. func (v *VendorDir) copyModFile() { var err error v.modFileContent, err = ioutil.ReadFile(v.vendPath("modules.txt")) output.OnError(err, "Error reading modules.txt") } // writeModFile writes the modules.txt file into the vendor directory. func (v *VendorDir) writeModFile() { err := ioutil.WriteFile(v.vendPath("modules.txt"), v.modFileContent, 0644) output.OnError(err, "Error saving modules.txt") } // clear removes all dependencies from the vendor directory. func (v *VendorDir) clear() { v.copyModFile() v.remove(v.basePath) err := os.MkdirAll(v.basePath, 0755) output.OnError(err, "Error creating vendor directory") v.writeModFile() } // copy will copy files and directories. func (v *VendorDir) copy(src string, dest string) { info, err := os.Lstat(src) output.OnError(err, "Error getting information about source") if info.Mode()&os.ModeSymlink != 0 { return // Completely ignore symlinks. } if info.IsDir() { v.copyDirectory(src, dest) } else { v.copyFile(src, dest) } } // copyDirectory will copy directories. func (v *VendorDir) copyDirectory(src string, dest string) { err := os.MkdirAll(dest, 0755) output.OnError(err, "Error creating directories") contents, err := ioutil.ReadDir(src) output.OnError(err, "Error reading source directory") for _, content := range contents { s := filepath.Join(src, content.Name()) d := filepath.Join(dest, content.Name()) v.copy(s, d) } } // copyFile will copy files. func (v *VendorDir) copyFile(src string, dest string) { err := os.MkdirAll(filepath.Dir(dest), 0755) output.OnError(err, "Error creating directories") d, err := os.Create(dest) output.OnError(err, "Error creating file") defer d.Close() s, err := os.Open(src) output.OnError(err, "Error opening file") defer s.Close() _, err = io.Copy(d, s) output.OnError(err, "Error copying file") } ================================================ FILE: file/mod.go ================================================ package file import ( "encoding/json" "github.com/nomad-software/vend/output" ) // ParseModJSON parses the mode file into a data structure. func ParseModJSON(raw string) GoMod { data := GoMod{ Module: Module{}, Require: make([]Require, 0, 10), Exclude: make([]Module, 0, 10), Replace: make([]Replace, 0, 10), Retract: make([]Retract, 0, 10), } err := json.Unmarshal([]byte(raw), &data) output.OnError(err, "Error parsing module json") return data } // GoMod represents parsed module json data. type GoMod struct { Module Module Go string Require []Require Exclude []Module Replace []Replace Retract []Retract } // Module represents parsed module json data. type Module struct { Path string Version string } // Require represents parsed module json data. type Require struct { Path string Version string Indirect bool } // Replace represents parsed module json data. type Replace struct { Old Module New Module } // Retract represents dependency version that are retracted. type Retract struct { Low string High string Rationale string } ================================================ FILE: go.mod ================================================ module github.com/nomad-software/vend go 1.16 ================================================ FILE: main.go ================================================ package main import ( "github.com/nomad-software/vend/cli" "github.com/nomad-software/vend/file" ) func main() { options := cli.ParseOptions() if options.Help { options.PrintUsage() } else { cli.UpdateModule() dir := file.InitVendorDir() dir.CopyDependencies() } } ================================================ FILE: output/output.go ================================================ package output import ( "fmt" "os" ) // OnError prints an error if err is not nil and exits the program. func OnError(err error, text string) { if err != nil { Fatal("%s:, %s", text, err.Error()) } } // Fatal prints an error and exits the program. func Fatal(format string, args ...interface{}) { fmt.Fprintf(os.Stderr, format+"\n", args...) os.Exit(1) } // Info prints information. func Info(format string, args ...interface{}) { fmt.Printf(format+"\n", args...) }