github.com/jmigpin/editor@v1.6.0/core/godebug/cmd.go (about)

     1  package godebug
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"embed"
     7  	"fmt"
     8  	"go/ast"
     9  	"go/printer"
    10  	"go/token"
    11  	"go/types"
    12  	"io"
    13  	"io/ioutil"
    14  	"os"
    15  	"os/exec"
    16  	"path"
    17  	"path/filepath"
    18  	"runtime"
    19  	"strings"
    20  	"time"
    21  
    22  	"github.com/jmigpin/editor/core/godebug/debug"
    23  	"github.com/jmigpin/editor/util/astut"
    24  	"github.com/jmigpin/editor/util/goutil"
    25  	"github.com/jmigpin/editor/util/iout"
    26  	"github.com/jmigpin/editor/util/osutil"
    27  	"github.com/jmigpin/editor/util/parseutil"
    28  	"github.com/jmigpin/editor/util/pathutil"
    29  	"golang.org/x/mod/modfile"
    30  )
    31  
    32  // The godebug/debug pkg is writen to a tmp dir and used with the pkg path "godebugconfig/debug" to avoid clashes when self debugging. A config.go file is added with the annotation data. The godebug/debug pkg is included in the editor binary via //go:embed directive.
    33  var debugPkgPath = "godebugconfig/debug"
    34  
    35  //----------
    36  
    37  type Cmd struct {
    38  	Dir string // running directory
    39  
    40  	flags      flags
    41  	gopathMode bool
    42  
    43  	Stdin  io.Reader
    44  	Stdout io.Writer
    45  	Stderr io.Writer
    46  
    47  	tmpDir           string
    48  	tmpBuiltFile     string // godebug file built
    49  	tmpGoModFilename string
    50  
    51  	mainFuncFilename string // set at annotation time
    52  
    53  	fset   *token.FileSet
    54  	env    []string // set at start
    55  	annset *AnnotatorSet
    56  
    57  	debugPkgDir      string
    58  	alternativeGoMod string
    59  	overlayFilename  string
    60  	overlay          map[string]string // orig->new
    61  
    62  	Client *Client
    63  	start  struct {
    64  		network    string
    65  		address    string
    66  		cancel     context.CancelFunc
    67  		serverWait func() error // annotated program; can be nil
    68  		filesData  *debug.FilesDataMsg
    69  	}
    70  }
    71  
    72  func NewCmd() *Cmd {
    73  	cmd := &Cmd{
    74  		Stdout: os.Stdout,
    75  		Stderr: os.Stderr,
    76  	}
    77  	cmd.fset = token.NewFileSet()
    78  	cmd.annset = NewAnnotatorSet(cmd.fset)
    79  	return cmd
    80  }
    81  
    82  //------------
    83  
    84  func (cmd *Cmd) printf(f string, a ...interface{}) (int, error) {
    85  	return fmt.Fprintf(cmd.Stdout, "# "+f, a...)
    86  }
    87  func (cmd *Cmd) logf(f string, a ...interface{}) (int, error) {
    88  	if cmd.flags.verbose {
    89  		f = strings.TrimRight(f, "\n") + "\n" // ensure one newline
    90  		return cmd.printf(f, a...)
    91  	}
    92  	return 0, nil
    93  }
    94  func (cmd *Cmd) Error(err error) {
    95  	cmd.printf("error: %v\n", err)
    96  }
    97  
    98  //------------
    99  
   100  func (cmd *Cmd) Start(ctx context.Context, args []string) (bool, error) {
   101  	if err := cmd.start2(ctx, args); err != nil {
   102  		return true, err
   103  	}
   104  	if cmd.flags.mode.build {
   105  		return true, nil
   106  	}
   107  	return false, nil
   108  }
   109  func (cmd *Cmd) start2(ctx context.Context, args []string) error {
   110  	defer cmd.cleanupAfterStart()
   111  
   112  	cmd.logf("dir=%v\n", cmd.Dir)
   113  	cmd.logf("testmode=%v\n", cmd.flags.mode.test)
   114  
   115  	if err := cmd.neededGoVersion(); err != nil {
   116  		return err
   117  	}
   118  
   119  	// use absolute dir
   120  	dir0, err := filepath.Abs(cmd.Dir)
   121  	if err != nil {
   122  		return err
   123  	}
   124  	cmd.Dir = dir0
   125  
   126  	// read flags
   127  	cmd.flags.stderr = cmd.Stderr
   128  	if err := cmd.flags.parseArgs(args); err != nil {
   129  		return err
   130  	}
   131  
   132  	// setup environment
   133  	cmd.env = goutil.OsAndGoEnv(cmd.Dir)
   134  	cmd.env = osutil.SetEnvs(cmd.env, cmd.flags.env)
   135  
   136  	if err := cmd.detectGopathMode(cmd.env); err != nil {
   137  		return err
   138  	}
   139  
   140  	// REVIEW
   141  	// depends on: gopathMode, tmpDir
   142  	//cmd.env = cmd.setGoPathEnv(cmd.env)
   143  
   144  	// depends on cmd.flags.work
   145  	if err := cmd.setupTmpDir(); err != nil {
   146  		return err
   147  	}
   148  
   149  	if err := cmd.setupNetworkAddress(); err != nil {
   150  		return err
   151  	}
   152  
   153  	m := &cmd.flags.mode
   154  	if m.run || m.test || m.build {
   155  		if err := cmd.build(ctx); err != nil {
   156  			return err
   157  		}
   158  	}
   159  
   160  	switch {
   161  	case m.build:
   162  		// inform the address used in the binary
   163  		cmd.printf("build: %v (builtin address: %v, %v)\n", cmd.tmpBuiltFile, cmd.start.network, cmd.start.address)
   164  		return nil
   165  	case m.run || m.test:
   166  		return cmd.startServerClient(ctx)
   167  	case m.connect:
   168  		return cmd.startClient(ctx)
   169  	default:
   170  		panic(fmt.Sprintf("unhandled mode: %v", m))
   171  	}
   172  }
   173  
   174  //----------
   175  
   176  func (cmd *Cmd) build(ctx context.Context) error {
   177  	fa := NewFilesToAnnotate(cmd)
   178  	if err := fa.find(ctx); err != nil {
   179  		return err
   180  	}
   181  	if err := cmd.annotateFiles2(ctx, fa); err != nil {
   182  		return err
   183  	}
   184  	if err := cmd.buildDebugPkg(ctx, fa); err != nil {
   185  		return err
   186  	}
   187  	if !cmd.gopathMode {
   188  		if err := cmd.buildAlternativeGoMod(ctx, fa); err != nil {
   189  			return err
   190  		}
   191  	}
   192  	if err := cmd.buildOverlayFile(ctx); err != nil {
   193  		return err
   194  	}
   195  
   196  	// DEBUG
   197  	//cmd.printAnnotatedFilesAsts(fa)
   198  
   199  	if err := cmd.buildBinary(ctx, fa); err != nil {
   200  		// auto-set work flag to avoid cleanup; allows clicking on failing work files locations
   201  		cmd.flags.work = true
   202  
   203  		return err
   204  	}
   205  	return nil
   206  }
   207  
   208  func (cmd *Cmd) buildBinary(ctx context.Context, fa *FilesToAnnotate) error {
   209  	outFilename, err := cmd.buildOutFilename(fa)
   210  	if err != nil {
   211  		return err
   212  	}
   213  	cmd.tmpBuiltFile = outFilename
   214  
   215  	// build args
   216  	a := []string{osutil.ExecName("go")}
   217  	if cmd.flags.mode.test {
   218  		a = append(a, "test")
   219  		a = append(a, "-c") // compile binary but don't run
   220  		//a = append(a, "-vet=off")
   221  	} else {
   222  		a = append(a, "build")
   223  	}
   224  	if cmd.alternativeGoMod != "" {
   225  		a = append(a, "-modfile="+cmd.alternativeGoMod)
   226  	}
   227  	a = append(a, "-overlay="+cmd.overlayFilename)
   228  	a = append(a, "-o="+cmd.tmpBuiltFile)
   229  	a = append(a, cmd.buildArgs()...)
   230  	a = append(a, cmd.flags.unnamedArgs...)
   231  
   232  	cmd.logf("build binary: %v\n", a)
   233  	ec := cmd.newCmdI(ctx, a)
   234  	if err := ec.Start(); err != nil {
   235  		return err
   236  	}
   237  	return ec.Wait()
   238  }
   239  
   240  //------------
   241  
   242  // DEBUG
   243  func (cmd *Cmd) printAnnotatedFilesAsts(fa *FilesToAnnotate) {
   244  	for orig, _ := range cmd.overlay {
   245  		astFile, ok := fa.filesAsts[orig]
   246  		if ok {
   247  			astut.PrintNode(cmd.annset.fset, astFile)
   248  		}
   249  	}
   250  }
   251  
   252  //------------
   253  
   254  func (cmd *Cmd) startServerClient(ctx context.Context) error {
   255  	// server/client context to cancel the other when one of them ends
   256  	ctx2, cancel := context.WithCancel(ctx)
   257  	cmd.start.cancel = cancel
   258  
   259  	if err := cmd.startServer(ctx2); err != nil {
   260  		return err
   261  	}
   262  	return cmd.startClient(ctx2)
   263  }
   264  func (cmd *Cmd) cancelStart() {
   265  	if cmd.start.cancel != nil {
   266  		cmd.start.cancel()
   267  	}
   268  }
   269  func (cmd *Cmd) startServer(ctx context.Context) error {
   270  	return cmd.runBinary(ctx)
   271  }
   272  func (cmd *Cmd) runBinary(ctx context.Context) error {
   273  	// args of the built binary to run (annotated program)
   274  	args := []string{}
   275  	if cmd.flags.toolExec != "" {
   276  		args = append(args, cmd.flags.toolExec)
   277  	}
   278  	args = append(args, cmd.tmpBuiltFile)
   279  	args = append(args, cmd.flags.binaryArgs...)
   280  
   281  	// callback func to print process id and args
   282  	cb := func(cmdi osutil.CmdI) {
   283  		cmd.printf("pid %d: %v\n", cmdi.Cmd().Process.Pid, args)
   284  	}
   285  
   286  	// run the annotated program
   287  	ci := cmd.newCmdI(ctx, args)
   288  	ci = osutil.NewCallbackOnStartCmd(ci, cb)
   289  	if err := ci.Start(); err != nil {
   290  		cmd.cancelStart()
   291  		return err
   292  	}
   293  
   294  	//waitErr := error(nil)
   295  	//wg := sync.WaitGroup{}
   296  	//wg.Add(1)
   297  	//go func() {
   298  	//	defer wg.Done()
   299  	//	waitErr = ec.Wait()
   300  	//}()
   301  
   302  	//log.Println("server started")
   303  	cmd.start.serverWait = func() error {
   304  		//defer log.Println("server wait done")
   305  		//wg.Wait()
   306  		//return waitErr
   307  
   308  		return ci.Wait()
   309  	}
   310  	return nil
   311  }
   312  func (cmd *Cmd) startClient(ctx context.Context) error {
   313  	// blocks until connected
   314  	client, err := NewClient(ctx, cmd.start.network, cmd.start.address)
   315  	if err != nil {
   316  		//log.Println("client ended")
   317  		cmd.cancelStart()
   318  		if cmd.start.serverWait != nil {
   319  			cmd.start.serverWait()
   320  		}
   321  		return err
   322  	}
   323  	cmd.Client = client
   324  
   325  	// set deadline for the starting protocol
   326  	deadline := time.Now().Add(8 * time.Second)
   327  	cmd.Client.Conn.SetWriteDeadline(deadline)
   328  	defer cmd.Client.Conn.SetWriteDeadline(time.Time{}) // clear
   329  
   330  	// starting protocol
   331  	if err := cmd.requestFilesData(); err != nil {
   332  		return err
   333  	}
   334  	// wait for filesdata
   335  	msg, ok := <-cmd.Client.Messages
   336  	if !ok {
   337  		return fmt.Errorf("clients msgs chan closed")
   338  	}
   339  	if fd, ok := msg.(*debug.FilesDataMsg); !ok {
   340  		return fmt.Errorf("unexpected msg: %#v", msg)
   341  	} else {
   342  		cmd.start.filesData = fd
   343  	}
   344  	// request start
   345  	if err := cmd.requestStart(); err != nil {
   346  		return err
   347  	}
   348  	return nil
   349  }
   350  
   351  func (cmd *Cmd) Wait() error {
   352  	defer cmd.cleanupAfterWait()
   353  	defer cmd.cancelStart()
   354  	err := error(nil)
   355  	if cmd.start.serverWait != nil { // might be nil (ex: connect mode)
   356  		err = cmd.start.serverWait()
   357  	}
   358  	if cmd.Client != nil { // might be nil (ex: server failed to start)
   359  		cmd.Client.Wait()
   360  	}
   361  	return err
   362  }
   363  
   364  //------------
   365  
   366  func (cmd *Cmd) Messages() chan interface{} {
   367  	return cmd.Client.Messages
   368  }
   369  func (cmd *Cmd) FilesData() *debug.FilesDataMsg {
   370  	return cmd.start.filesData
   371  }
   372  
   373  //------------
   374  
   375  func (cmd *Cmd) requestFilesData() error {
   376  	msg := &debug.ReqFilesDataMsg{}
   377  	encoded, err := debug.EncodeMessage(msg)
   378  	if err != nil {
   379  		return err
   380  	}
   381  	_, err = cmd.Client.Conn.Write(encoded)
   382  	return err
   383  }
   384  
   385  func (cmd *Cmd) requestStart() error {
   386  	msg := &debug.ReqStartMsg{}
   387  	encoded, err := debug.EncodeMessage(msg)
   388  	if err != nil {
   389  		return err
   390  	}
   391  	_, err = cmd.Client.Conn.Write(encoded)
   392  	return err
   393  }
   394  
   395  //------------
   396  
   397  //func (cmd *Cmd) tmpDirBasedFilename(filename string) string {
   398  //	// remove volume name
   399  //	v := filepath.VolumeName(filename)
   400  //	if len(v) > 0 {
   401  //		filename = filename[len(v):]
   402  //	}
   403  
   404  //	if cmd.gopathMode {
   405  //		// trim filename when inside a src dir
   406  //		rhs := trimAtFirstSrcDir(filename)
   407  //		return filepath.Join(cmd.tmpDir, "src", rhs)
   408  //	}
   409  
   410  //	// just replicate on tmp dir
   411  //	return filepath.Join(cmd.tmpDir, filename)
   412  //}
   413  
   414  //------------
   415  
   416  //func (cmd *Cmd) setGoPathEnv(env []string) []string {
   417  //	// after cmd.flags.env such that this result won't be overriden
   418  
   419  //	s := cmd.fullGoPathStr(env)
   420  //	return osutil.SetEnv(env, "GOPATH", s)
   421  //}
   422  
   423  //func (cmd *Cmd) fullGoPathStr(env []string) string {
   424  //	u := []string{} // first has priority, use new slice
   425  
   426  //	// add tmpdir for priority to the annotated files
   427  //	if cmd.gopathMode {
   428  //		u = append(u, cmd.tmpDir)
   429  //	}
   430  
   431  //	if s := osutil.GetEnv(cmd.flags.env, "GOPATH"); s != "" {
   432  //		u = append(u, s)
   433  //	}
   434  
   435  //	// always include default gopath last (includes entry that might not be defined anywhere, needs to be set)
   436  //	u = append(u, goutil.GetGoPath(env)...)
   437  
   438  //	return goutil.JoinPathLists(u...)
   439  //}
   440  
   441  func (cmd *Cmd) addToGopathStart(dir string) {
   442  	varName := "GOPATH"
   443  	v := osutil.GetEnv(cmd.env, varName)
   444  	sep := ""
   445  	if v != "" {
   446  		sep = string(os.PathListSeparator)
   447  	}
   448  	v2 := dir + sep + v
   449  	cmd.env = osutil.SetEnv(cmd.env, varName, v2)
   450  }
   451  
   452  //------------
   453  
   454  func (cmd *Cmd) detectGopathMode(env []string) error {
   455  	modsMode, err := cmd.detectModulesMode(env)
   456  	if err != nil {
   457  		return err
   458  	}
   459  	cmd.gopathMode = !modsMode
   460  	cmd.logf("gopathmode=%v\n", cmd.gopathMode)
   461  	return nil
   462  }
   463  func (cmd *Cmd) detectModulesMode(env []string) (bool, error) {
   464  	v := osutil.GetEnv(env, "GO111MODULE")
   465  	switch v {
   466  	case "on":
   467  		return true, nil
   468  	case "off":
   469  		return false, nil
   470  	case "auto":
   471  		return cmd.detectGoMod(), nil
   472  	default:
   473  		v, err := goutil.GoVersion()
   474  		if err != nil {
   475  			return false, err
   476  		}
   477  		// < go1.16, modules mode if go.mod present
   478  		if parseutil.VersionLessThan(v, "1.16") {
   479  			return cmd.detectGoMod(), nil
   480  		}
   481  		// >= go1.16, modules mode by default
   482  		return true, nil
   483  	}
   484  }
   485  func (cmd *Cmd) detectGoMod() bool {
   486  	_, ok := goutil.FindGoMod(cmd.Dir)
   487  	return ok
   488  }
   489  func (cmd *Cmd) neededGoVersion() error {
   490  	// need go version that supports overlays
   491  	v, err := goutil.GoVersion()
   492  	if err != nil {
   493  		return err
   494  	}
   495  	if parseutil.VersionLessThan(v, "1.16") {
   496  		return fmt.Errorf("need go version >=1.16 that supports -overlay flag")
   497  	}
   498  	return nil
   499  }
   500  
   501  //------------
   502  
   503  func (cmd *Cmd) cleanupAfterStart() {
   504  	// always remove (written in src dir)
   505  	if cmd.tmpGoModFilename != "" {
   506  		_ = os.Remove(cmd.tmpGoModFilename) // best effort
   507  	}
   508  	// remove dirs
   509  	if cmd.tmpDir != "" && !cmd.flags.work {
   510  		_ = os.RemoveAll(cmd.tmpDir) // best effort
   511  	}
   512  }
   513  
   514  func (cmd *Cmd) cleanupAfterWait() {
   515  	// cleanup unix socket in case of bad stop
   516  	if cmd.start.network == "unix" {
   517  		_ = os.Remove(cmd.start.address) // best effort
   518  	}
   519  
   520  	if cmd.tmpBuiltFile != "" && !cmd.flags.mode.build {
   521  		_ = os.Remove(cmd.tmpBuiltFile) // best effort
   522  	}
   523  }
   524  
   525  //------------
   526  
   527  func (cmd *Cmd) mkdirAllWriteAstFile(filename string, astFile *ast.File) error {
   528  	buf := &bytes.Buffer{}
   529  
   530  	pcfg := &printer.Config{Tabwidth: 4}
   531  
   532  	// by default, don't print with sourcepos since it will only confuse the user. If the original code doesn't compile, the load packages should fail early before getting to output any ast file.
   533  	if cmd.flags.srcLines {
   534  		pcfg.Mode = printer.SourcePos
   535  	}
   536  
   537  	if err := pcfg.Fprint(buf, cmd.fset, astFile); err != nil {
   538  		return err
   539  	}
   540  	return mkdirAllWriteFile(filename, buf.Bytes())
   541  }
   542  
   543  //------------
   544  
   545  func (cmd *Cmd) setupTmpDir() error {
   546  	fixedDir := filepath.Join(os.TempDir(), "editor_godebug")
   547  	if err := iout.MkdirAll(fixedDir); err != nil {
   548  		return err
   549  	}
   550  	dir, err := ioutil.TempDir(fixedDir, "work*")
   551  	if err != nil {
   552  		return err
   553  	}
   554  	cmd.tmpDir = dir
   555  
   556  	// print tmp dir if work flag is present
   557  	if cmd.flags.work {
   558  		cmd.printf("tmpDir: %v\n", cmd.tmpDir)
   559  	}
   560  	return nil
   561  }
   562  
   563  //------------
   564  
   565  func (cmd *Cmd) buildArgs() []string {
   566  	u := []string{}
   567  	u = append(u, envGodebugBuildFlags(cmd.env)...)
   568  	u = append(u, cmd.flags.unknownArgs...)
   569  	return u
   570  }
   571  
   572  //------------
   573  
   574  func (cmd *Cmd) setupNetworkAddress() error {
   575  	// can't consider using stdin/out since the program could use it
   576  
   577  	if cmd.flags.address != "" {
   578  		cmd.start.network = "tcp"
   579  		cmd.start.address = cmd.flags.address
   580  		return nil
   581  	}
   582  
   583  	// OS target to choose how to connect
   584  	goOs := osutil.GetEnv(cmd.env, "GOOS")
   585  	if goOs == "" {
   586  		goOs = runtime.GOOS
   587  	}
   588  
   589  	switch goOs {
   590  	case "linux":
   591  		cmd.start.network = "unix"
   592  		cmd.start.address = filepath.Join(cmd.tmpDir, "godebug.sock")
   593  	default:
   594  		port, err := osutil.GetFreeTcpPort()
   595  		if err != nil {
   596  			return err
   597  		}
   598  		cmd.start.network = "tcp"
   599  		cmd.start.address = fmt.Sprintf("127.0.0.1:%v", port)
   600  	}
   601  	return nil
   602  }
   603  
   604  //------------
   605  
   606  func (cmd *Cmd) annotateFiles2(ctx context.Context, fa *FilesToAnnotate) error {
   607  	// annotate files
   608  	handledMain := false
   609  	cmd.overlay = map[string]string{}
   610  	mainName := mainFuncName(fa.cmd.flags.mode.test)
   611  	for filename := range fa.toAnnotate {
   612  		astFile, ok := fa.filesAsts[filename]
   613  		if !ok {
   614  			return fmt.Errorf("missing ast file: %v", filename)
   615  		}
   616  
   617  		// annotate
   618  		ti := (*types.Info)(nil)
   619  		pkg, ok := fa.filesPkgs[filename]
   620  		if ok {
   621  			ti = pkg.TypesInfo
   622  		}
   623  		if err := cmd.annset.AnnotateAstFile(astFile, ti, fa.nodeAnnTypes); err != nil {
   624  			return err
   625  		}
   626  
   627  		// setup main ast with debug.exitserver
   628  		if fd, ok := findFuncDeclWithBody(astFile, mainName); ok {
   629  			handledMain = true
   630  			cmd.mainFuncFilename = filename
   631  			cmd.annset.setupDebugExitInFuncDecl(fd, astFile)
   632  		}
   633  	}
   634  
   635  	if !handledMain {
   636  		if !cmd.flags.mode.test {
   637  			return fmt.Errorf("main func not handled")
   638  		}
   639  		// insert testmains in "*_test.go" files
   640  		seen := map[string]bool{}
   641  		for filename := range fa.toAnnotate {
   642  			if !strings.HasSuffix(filename, "_test.go") {
   643  				continue
   644  			}
   645  
   646  			// one testmain per dir
   647  			dir := filepath.Dir(filename)
   648  			if seen[dir] {
   649  				continue
   650  			}
   651  			seen[dir] = true
   652  
   653  			astFile, ok := fa.filesAsts[filename]
   654  			if !ok {
   655  				continue
   656  			}
   657  			if err := cmd.annset.insertTestMain(astFile); err != nil {
   658  				return err
   659  			}
   660  
   661  			// use dir of the first file // TODO: just use current dir?
   662  			if cmd.mainFuncFilename == "" {
   663  				dir := filepath.Dir(filename)
   664  				cmd.mainFuncFilename = filepath.Join(dir, "testmain")
   665  			}
   666  		}
   667  	}
   668  
   669  	for filename := range fa.toAnnotate {
   670  		astFile, ok := fa.filesAsts[filename]
   671  		if !ok {
   672  			return fmt.Errorf("missing ast file: %v", filename)
   673  		}
   674  
   675  		// encode filename for a flat map
   676  		ext := filepath.Ext(filename)
   677  		base := filepath.Base(filename)
   678  		base = base[:len(base)-len(ext)]
   679  		//hash := md5.New().Write([]byte(filename)).Sum(nil)
   680  		//hash := genDigitsStr(8)
   681  		hash := hashStringN(filename, 10)
   682  		name := fmt.Sprintf("%s_%s%s", base, hash, ext)
   683  
   684  		// write annotated files and keep in map for overlay
   685  		filename2 := filepath.Join(cmd.tmpDir, "annotated", name)
   686  		if err := cmd.mkdirAllWriteAstFile(filename2, astFile); err != nil {
   687  			return err
   688  		}
   689  
   690  		cmd.overlay[filename] = filename2
   691  	}
   692  
   693  	return nil
   694  }
   695  
   696  //------------
   697  
   698  func (cmd *Cmd) buildOverlayFile(ctx context.Context) error {
   699  	// build entries
   700  	w := []string{}
   701  	for src, dest := range cmd.overlay {
   702  		w = append(w, fmt.Sprintf("%q:%q", src, dest))
   703  	}
   704  	// write overlay file
   705  	src := []byte(fmt.Sprintf("{%q:{%s}}", "Replace", strings.Join(w, ",")))
   706  	cmd.overlayFilename = filepath.Join(cmd.tmpDir, "annotated_overlay.json")
   707  	return mkdirAllWriteFile(cmd.overlayFilename, src)
   708  }
   709  
   710  //------------
   711  
   712  func (cmd *Cmd) buildDebugPkg(ctx context.Context, fa *FilesToAnnotate) error {
   713  	//// detect if the editor debug pkg is used
   714  	//cmd.selfMode = false
   715  	//selfDebugPkgDir := ""
   716  	//for pkgPath, pkg := range fa.pathsPkgs {
   717  	//	if strings.HasPrefix(pkgPath, editorPkgPath+"/") {
   718  	//		// find dir
   719  	//		f := pkg.GoFiles[0]
   720  	//		k := strings.Index(f, editorPkgPath)
   721  	//		if k >= 0 {
   722  	//			cmd.selfMode = true
   723  	//			selfDebugPkgDir = filepath.Join(f[:k], debugPkgPath)
   724  	//			break
   725  	//		}
   726  	//	}
   727  	//}
   728  
   729  	//// setup current files to be empty by default (attempt to discard if debugging an old version of the editor)
   730  	//if cmd.selfMode {
   731  	//	fis, err := ioutil.ReadDir(selfDebugPkgDir)
   732  	//	if err == nil {
   733  	//		for _, fi := range fis {
   734  	//			filename := fsutil.JoinPath(selfDebugPkgDir, fi.Name())
   735  	//			cmd.overlay[filename] = ""
   736  	//		}
   737  	//	}
   738  	//}
   739  
   740  	// target dir
   741  	cmd.debugPkgDir = filepath.Join(cmd.tmpDir, "debugpkg")
   742  	if cmd.gopathMode {
   743  		cmd.addToGopathStart(cmd.debugPkgDir)
   744  		cmd.debugPkgDir = filepath.Join(cmd.debugPkgDir, "src/"+debugPkgPath)
   745  	}
   746  
   747  	// util to add file to debug pkg dir
   748  	writeFile := func(name string, src []byte) error {
   749  		filename2 := filepath.Join(cmd.debugPkgDir, name)
   750  
   751  		//if cmd.selfMode {
   752  		//	filename3 := filepath.Join(selfDebugPkgDir, name)
   753  		//	//println("overlay", filename3, filename2)
   754  		//	cmd.overlay[filename3] = filename2
   755  		//}
   756  
   757  		return mkdirAllWriteFile(filename2, src)
   758  	}
   759  
   760  	// local src pkg dir where the debug pkg is located (io/fs)
   761  	srcDir := "debug"
   762  	des, err := debugPkgFs.ReadDir(srcDir)
   763  	if err != nil {
   764  		return err
   765  	}
   766  	for _, de := range des {
   767  		// must use path.join since dealing with embedFs
   768  		filename1 := path.Join(srcDir, de.Name())
   769  		if strings.HasSuffix(filename1, "_test.go") {
   770  			continue
   771  		}
   772  		src, err := debugPkgFs.ReadFile(filename1)
   773  		if err != nil {
   774  			return err
   775  		}
   776  		if err := writeFile(de.Name(), src); err != nil {
   777  			return err
   778  		}
   779  	}
   780  
   781  	// dynamically create go.mod since go:embed doesn't allow it
   782  	if !cmd.gopathMode {
   783  		src3 := []byte(fmt.Sprintf("module %s\n", debugPkgPath))
   784  		if err := writeFile("go.mod", src3); err != nil {
   785  			return err
   786  		}
   787  	}
   788  
   789  	// init() functions declared across multiple files in a package are processed in alphabetical order of the file name. Use name starting with "a" to setup config vars as early as possible.
   790  	configFilename := "aaaconfig.go"
   791  
   792  	// build config file
   793  	src4 := cmd.annset.BuildConfigSrc(cmd.start.network, cmd.start.address, &cmd.flags)
   794  	if err := writeFile(configFilename, src4); err != nil {
   795  		return err
   796  	}
   797  
   798  	return nil
   799  }
   800  
   801  //------------
   802  
   803  func (cmd *Cmd) buildAlternativeGoMod(ctx context.Context, fa *FilesToAnnotate) error {
   804  	filename, ok := fa.GoModFilename()
   805  	if !ok {
   806  		//return fmt.Errorf("missing go.mod")
   807  
   808  		// in the case of a simple main.go without any go.mod (but in modules mode), it needs to create an artificial go.mod in order to reference the debug pkg that is located in the tmp dir
   809  
   810  		// TODO: last resort, having to create files in the src dir is to be avoided -- needs review
   811  
   812  		// create temporary go.mod in src dir based on main file
   813  		if cmd.mainFuncFilename == "" {
   814  			return fmt.Errorf("missing main func filename")
   815  		}
   816  		dir := filepath.Dir(cmd.mainFuncFilename)
   817  		fname2 := filepath.Join(dir, "go.mod")
   818  		// must not exist
   819  		if _, err := os.Stat(fname2); !os.IsNotExist(err) {
   820  			return fmt.Errorf("file should not exist because gomodfilename didn't found it: %v", fname2)
   821  		}
   822  		// create
   823  		src := []byte("module main\n")
   824  		if err := mkdirAllWriteFile(fname2, src); err != nil {
   825  			return err
   826  		}
   827  		cmd.tmpGoModFilename = fname2
   828  		cmd.logf("tmpgomodfilename: %v\n", cmd.tmpGoModFilename)
   829  		filename = fname2
   830  	}
   831  
   832  	// build based on current go.mod
   833  	src, err := ioutil.ReadFile(filename)
   834  	if err != nil {
   835  		return fmt.Errorf("unable to read mod file: %w", err)
   836  	}
   837  	f, err := modfile.ParseLax(filename, src, nil)
   838  	if err != nil {
   839  		return err
   840  	}
   841  
   842  	if cmd.flags.usePkgLinks {
   843  		if err := cmd.buildPkgLinks(f, fa); err != nil {
   844  			return err
   845  		}
   846  	}
   847  
   848  	// include debug pkg require/replace lines
   849  	f.AddNewRequire(debugPkgPath, "v0.0.0", false)
   850  	f.AddReplace(debugPkgPath, "", cmd.debugPkgDir, "")
   851  
   852  	src2, err := f.Format()
   853  	if err != nil {
   854  		return err
   855  	}
   856  	cmd.alternativeGoMod = filepath.Join(cmd.tmpDir, "alternative.mod")
   857  	if err := mkdirAllWriteFile(cmd.alternativeGoMod, src2); err != nil {
   858  		return err
   859  	}
   860  
   861  	// REVIEW: commented: using overlay for go.mod, so the original go.sum should be used(?)
   862  	//// copy as well go.sum or it will fail, just need a best effort
   863  	//dir := filepath.Dir(filename)
   864  	//gosum := filepath.Join(dir, "go.sum")
   865  	//gosumDst := pathutil.ReplaceExt(cmd.alternativeGoMod, ".sum")
   866  	//_ = copyFile(gosum, gosumDst)
   867  
   868  	cmd.overlay[filename] = cmd.alternativeGoMod
   869  	////cmd.overlay[gosum] = gosumDst
   870  	cmd.alternativeGoMod = "" // disable (using overlay)
   871  
   872  	return nil
   873  }
   874  
   875  func (cmd *Cmd) buildPkgLinks(mf *modfile.File, fa *FilesToAnnotate) error {
   876  
   877  	linksDir := filepath.Join(cmd.tmpDir, "pkglinks")
   878  	if err := iout.MkdirAll(linksDir); err != nil {
   879  		return err
   880  	}
   881  
   882  	linkFilename := func(dir string) string {
   883  		hash := hashStringN(dir, 10)
   884  		base := filepath.Base(dir)
   885  		name := fmt.Sprintf("%s-%s", base, hash)
   886  		return filepath.Join(linksDir, name)
   887  	}
   888  
   889  	seen := map[string]bool{}        // seen module
   890  	for _, req := range mf.Require { // modules being required
   891  		for filename := range fa.toAnnotate { // all annotated files
   892  			fpkg, ok := fa.filesPkgs[filename]
   893  			if !ok {
   894  				continue
   895  			}
   896  
   897  			fmod := pkgMod(fpkg)
   898  			if fmod == nil || fmod.Dir == "" {
   899  				continue
   900  			}
   901  
   902  			// visited already
   903  			if seen[fmod.Dir] {
   904  				continue
   905  			}
   906  			seen[fmod.Dir] = true
   907  
   908  			if fmod.Path != req.Mod.Path {
   909  				continue
   910  			}
   911  
   912  			// required module with annotated files
   913  
   914  			// TODO: do this only if in a gopath dir? Seemed to be the issue that only modules in a gopath dir would not honor the overlay including a new package
   915  
   916  			// make a link to the package module and use that link dir to bypass the erroneous behaviour introduced by go1.19.x
   917  			ldir := linkFilename(fmod.Dir)
   918  			if err := os.Symlink(fmod.Dir, ldir); err != nil {
   919  				return fmt.Errorf("builddirlinks: %w", err)
   920  			}
   921  
   922  			// add replace directive to go.mod
   923  			mf.AddReplace(fmod.Path, "", ldir, "")
   924  
   925  			// replace all references in the overlay map to the created link dir
   926  			for oldf, newf := range cmd.overlay {
   927  				dir2 := fmod.Dir + string(filepath.Separator)
   928  				if strings.HasPrefix(oldf, dir2) {
   929  					rest := oldf[len(dir2):]
   930  					name2 := filepath.Join(ldir, rest)
   931  					cmd.overlay[name2] = newf
   932  					delete(cmd.overlay, oldf)
   933  				}
   934  			}
   935  
   936  			// add module go.mod in overlay (ex: case of module without a go.mod, but with a built go.mod in a cache dir)
   937  			dir2 := filepath.Dir(fmod.GoMod)
   938  			if dir2 != fmod.Dir {
   939  				filename := filepath.Join(ldir, "go.mod")
   940  				cmd.overlay[filename] = fmod.GoMod
   941  			}
   942  		}
   943  	}
   944  	return nil
   945  }
   946  
   947  //------------
   948  
   949  func (cmd *Cmd) buildOutFilename(fa *FilesToAnnotate) (string, error) {
   950  	if cmd.flags.outFilename != "" {
   951  		return cmd.flags.outFilename, nil
   952  	}
   953  
   954  	if cmd.mainFuncFilename == "" {
   955  		return "", fmt.Errorf("missing main filename")
   956  	}
   957  
   958  	// commented: output to tmp dir
   959  	//fname := filepath.Base(cmd.mainFuncFilename)
   960  	//fname = fsutil.JoinPath(cmd.tmpDir, fname)
   961  
   962  	// output to main file dir
   963  	fname := cmd.mainFuncFilename
   964  	fname = pathutil.ReplaceExt(fname, "_godebug") // don't use ".godebug", not a file type
   965  
   966  	fname = osutil.ExecName(fname)
   967  	return fname, nil
   968  }
   969  
   970  //------------
   971  
   972  func (cmd *Cmd) newCmdI(ctx context.Context, args []string) osutil.CmdI {
   973  	ec := exec.CommandContext(ctx, args[0], args[1:]...)
   974  	ec.Stdin = cmd.Stdin
   975  	ec.Stdout = cmd.Stdout
   976  	ec.Stderr = cmd.Stderr
   977  	ec.Dir = cmd.Dir
   978  	ec.Env = cmd.env
   979  
   980  	ci := osutil.NewCmdI(ec)
   981  	ci = osutil.NewSetSidCmd(ctx, ci)
   982  	ci = osutil.NewShellCmd(ci)
   983  	return ci
   984  }
   985  
   986  //------------
   987  //------------
   988  //------------
   989  
   990  //go:embed debug/*
   991  var debugPkgFs embed.FS
   992  
   993  //------------
   994  
   995  func writeFile(filename string, src []byte) error {
   996  	return os.WriteFile(filename, src, 0640)
   997  }
   998  func mkdirAllWriteFile(filename string, src []byte) error {
   999  	return iout.MkdirAllWriteFile(filename, src, 0640)
  1000  }
  1001  
  1002  func mkdirAllCopyFile(src, dst string) error {
  1003  	return iout.MkdirAllCopyFile(src, dst, 0640)
  1004  }
  1005  func mkdirAllCopyFileSync(src, dst string) error {
  1006  	return iout.MkdirAllCopyFileSync(src, dst, 0640)
  1007  }
  1008  
  1009  func copyFile(src, dst string) error {
  1010  	return iout.CopyFile(src, dst, 0640)
  1011  }
  1012  
  1013  //------------
  1014  
  1015  func splitCommaList(val string) []string {
  1016  	a := strings.Split(val, ",")
  1017  	u := []string{}
  1018  	for _, s := range a {
  1019  		// don't add empty strings
  1020  		s := strings.TrimSpace(s)
  1021  		if s == "" {
  1022  			continue
  1023  		}
  1024  
  1025  		u = append(u, s)
  1026  	}
  1027  	return u
  1028  }
  1029  
  1030  //------------
  1031  
  1032  //func trimAtFirstSrcDir(filename string) string {
  1033  //	v := filename
  1034  //	w := []string{}
  1035  //	for {
  1036  //		base := filepath.Base(v)
  1037  //		if base == "src" {
  1038  //			return filepath.Join(w...) // trimmed
  1039  //		}
  1040  //		w = append([]string{base}, w...)
  1041  //		oldv := v
  1042  //		v = filepath.Dir(v)
  1043  //		isRoot := oldv == v
  1044  //		if isRoot {
  1045  //			break
  1046  //		}
  1047  //	}
  1048  //	return filename
  1049  //}
  1050  
  1051  //----------
  1052  
  1053  // TODO: remove once env vars supported in editor
  1054  func envGodebugBuildFlags(env []string) []string {
  1055  	bfs := osutil.GetEnv(env, "GODEBUG_BUILD_FLAGS")
  1056  	if len(bfs) == 0 {
  1057  		return nil
  1058  	}
  1059  	return strings.Split(bfs, ",")
  1060  }