github.com/ktr0731/dept@v0.1.4-0.20191208040738-06ee3ca97c03/deptfile/deptfile.go (about) 1 package deptfile 2 3 import ( 4 "context" 5 "io/ioutil" 6 "os" 7 "os/exec" 8 "path/filepath" 9 "strings" 10 11 "github.com/ktr0731/modfile" 12 "github.com/mitchellh/copystructure" 13 "github.com/pkg/errors" 14 ) 15 16 var ( 17 FileName = "gotool.mod" 18 FileSumName = "gotool.sum" 19 ) 20 21 var ( 22 // ErrNotFound represents deptfile not found. 23 ErrNotFound = errors.Errorf("%s not found", FileName) 24 // ErrAlreadyExist represents deptfile alredy exist. 25 ErrAlreadyExist = errors.New("already exist") 26 ) 27 28 // File represents the root struct of deptfile. 29 type File struct { 30 Require []*Require 31 f *modfile.File 32 } 33 34 // Require represents a parsed direct requirement. 35 // A Require has least one Tool. 36 type Require struct { 37 Path string 38 Version string 39 ToolPaths []*Tool 40 } 41 42 func (r *Require) format() string { 43 s := r.Path 44 if len(r.ToolPaths) == 1 && isRootToolPath(r.ToolPaths[0]) { 45 // Special case. 46 // If number of tools is 1 and it is in the module root, 47 // format without '/'. 48 // For example, 'github.com/ktr0731/evans' or 'github.com/ktr0731/evans@ev'. 49 // Not 'github.com/ktr0731/evans:/' or 'github.com/ktr0731/evans:/@ev'. 50 if r.ToolPaths[0].Name != "" { 51 s += "@" + r.ToolPaths[0].Name 52 } 53 return s 54 } 55 toolPaths := make([]string, 0, len(r.ToolPaths)) 56 for _, t := range r.ToolPaths { 57 toolPaths = append(toolPaths, t.format()) 58 } 59 s += ":" + strings.Join(toolPaths, ",") 60 return s 61 } 62 63 // Tool represents a tool that is belongs to a module. 64 // In deptfile representation, a module is represents as a Require. 65 // Path is the absolute tool path from the module root. 66 // If Path is empty, it means the package of the tool is in the module root. 67 // Name is the tool name. 68 // If Name is empty, it means Name is the same as filepath.Base(Path). 69 type Tool struct { 70 Path string 71 Name string 72 } 73 74 func (t *Tool) format() string { 75 s := t.Path 76 if n := t.Name; n != "" { 77 s += "@" + n 78 } 79 return s 80 } 81 82 // parseDeptfile parses a file which named fname as a deptfile. 83 // The differences between deptfile and go.mod is just one point, 84 // deptfile's each path has also command paths. 85 // 86 // For example: 87 // "github.com/ktr0731/evans": module is github.com/ktr0731/evans, the command path is the module root. 88 // "github.com/ktr0731/itunes-cli:/itunes": module is github.com/ktr0731/itunes-cli, the command path is /itunes. 89 // "honnef.co/go/tools:/cmd/staticcheck,/cmd/unused": module is honnef.co/go/tools, command paths are /cmd/staticcheck and /cmd/unused. 90 // 91 // Deptfile also has a rename syntax just like: 92 // "github.com/ktr0731/evans@ev" 93 // "github.com/ktr0731/itunes-cli:/itunes@it" 94 // 95 // Also parseDeptfile returns the canonical modfile. It has been removed command paths. 96 // So, it is go.mod compatible. 97 // 98 // parseDeptfile returns ErrNotFound if fname is not found. 99 func parseDeptfile(fname string) (*File, *modfile.File, error) { 100 data, err := ioutil.ReadFile(fname) 101 if os.IsNotExist(err) { 102 return nil, nil, ErrNotFound 103 } 104 if err != nil { 105 return nil, nil, errors.Wrapf(err, "failed to open %s", fname) 106 } 107 f, err := modfile.Parse(filepath.Base(fname), data, nil) 108 if err != nil { 109 return nil, nil, errors.Wrapf(err, "failed to parse %s", fname) 110 } 111 112 tmp, err := copystructure.Copy(f) 113 if err != nil { 114 return nil, nil, errors.Wrap(err, "failed to deep copy modfile.File") 115 } 116 canonical := tmp.(*modfile.File) 117 118 // Convert from modfile.File.Require to deptfile.Require. 119 requires := make([]*Require, 0, len(f.Require)) 120 for i, r := range f.Require { 121 // Skip indirect requirements because deptfile focuses on direct requirements (= managed tools) only. 122 if r.Indirect { 123 continue 124 } 125 126 var toolPaths []*Tool 127 path := r.Mod.Path 128 129 // If main package is not in the module root. 130 // Else number of tools is 1 and it is in the module root package. 131 if i := strings.LastIndex(r.Mod.Path, ":"); i != -1 { 132 path = r.Mod.Path[:i] 133 for _, toolPath := range strings.Split(r.Mod.Path[i+1:], ",") { 134 if i := strings.LastIndex(toolPath, "@"); i != -1 { 135 toolPaths = append(toolPaths, &Tool{Path: toolPath[:i], Name: toolPath[i+1:]}) 136 } else { 137 toolPaths = append(toolPaths, &Tool{Path: toolPath}) 138 } 139 } 140 } else { 141 toolPath := r.Mod.Path 142 if i := strings.LastIndex(toolPath, "@"); i != -1 { 143 toolPaths = append(toolPaths, &Tool{Path: "/", Name: toolPath[i+1:]}) 144 path = path[:i] 145 } else { 146 toolPaths = append(toolPaths, &Tool{Path: "/"}) 147 } 148 } 149 150 requires = append(requires, &Require{ 151 Path: path, 152 Version: r.Mod.Version, 153 ToolPaths: toolPaths, 154 }) 155 canonical.Require[i].Mod.Path = path 156 canonical.Require[i].Syntax.Token[0] = path 157 } 158 canonical.SetRequire(canonical.Require) 159 return &File{Require: requires, f: f}, canonical, nil 160 } 161 162 func convertGoModToDeptfile(fname string, gomod *File) (*modfile.File, error) { 163 data, err := ioutil.ReadFile(fname) 164 if os.IsNotExist(err) { 165 return nil, ErrNotFound 166 } 167 if err != nil { 168 return nil, errors.Wrapf(err, "failed to open %s", fname) 169 } 170 f, err := modfile.Parse(filepath.Base(fname), data, nil) 171 if err != nil { 172 return nil, errors.Wrapf(err, "failed to parse %s", fname) 173 } 174 175 // no any additional information 176 if gomod == nil { 177 return f, nil 178 } 179 180 path2req := map[string]*Require{} 181 for _, r := range gomod.Require { 182 path2req[r.Path] = r 183 } 184 185 for i := range f.Require { 186 if f.Require[i].Indirect { 187 continue 188 } 189 req, ok := path2req[f.Require[i].Mod.Path] 190 // new tool 191 if !ok { 192 continue 193 } 194 p := req.format() 195 f.Require[i].Mod.Path = p 196 197 // require statement is oneline. 198 if f.Require[i].Syntax.Token[0] == "require" { 199 f.Require[i].Syntax.Token[1] = p 200 } else { 201 f.Require[i].Syntax.Token[0] = p 202 } 203 } 204 205 f.SetRequire(f.Require) 206 207 return f, nil 208 } 209 210 // Create creates a new deptfile. 211 // If already created, Create returns ErrAlreadyExist. 212 func Create(ctx context.Context) error { 213 if _, err := os.Stat(FileName); err == nil { 214 return ErrAlreadyExist 215 } 216 217 var err error 218 w := &Workspace{ 219 SourcePath: ".", 220 DoNotCopy: true, 221 } 222 err = w.Do(func(string, *File) error { 223 // TODO: module name 224 err = exec.CommandContext(ctx, "go", "mod", "init", "tools").Run() 225 if err != nil { 226 return errors.Wrap(err, "failed to init Go modules") 227 } 228 return nil 229 }) 230 if err != nil { 231 return err 232 } 233 234 return nil 235 } 236 237 func isRootToolPath(p *Tool) bool { 238 return p.Path == "/" 239 }