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  }