github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/subshell/sscommon/rcfile.go (about)

     1  package sscommon
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strings"
    11  	"text/template"
    12  
    13  	"github.com/ActiveState/cli/internal/installation/storage"
    14  	"github.com/mash/go-tempfile-suffix"
    15  
    16  	"github.com/ActiveState/cli/internal/assets"
    17  	"github.com/ActiveState/cli/internal/colorize"
    18  	"github.com/ActiveState/cli/internal/constants"
    19  	"github.com/ActiveState/cli/internal/errs"
    20  	"github.com/ActiveState/cli/internal/fileutils"
    21  	"github.com/ActiveState/cli/internal/locale"
    22  	"github.com/ActiveState/cli/internal/logging"
    23  	configMediator "github.com/ActiveState/cli/internal/mediators/config"
    24  	"github.com/ActiveState/cli/internal/osutils"
    25  	"github.com/ActiveState/cli/internal/output"
    26  	"github.com/ActiveState/cli/pkg/project"
    27  )
    28  
    29  var (
    30  	DeployID RcIdentification = RcIdentification{
    31  		constants.RCAppendDeployStartLine,
    32  		constants.RCAppendDeployStopLine,
    33  		"user_env",
    34  	}
    35  	DefaultID RcIdentification = RcIdentification{
    36  		constants.RCAppendDefaultStartLine,
    37  		constants.RCAppendDefaultStopLine,
    38  		"user_default_env",
    39  	}
    40  	InstallID RcIdentification = RcIdentification{
    41  		constants.RCAppendInstallStartLine,
    42  		constants.RCAppendInstallStopLine,
    43  		"user_install_env",
    44  	}
    45  	OfflineInstallID RcIdentification = RcIdentification{
    46  		constants.RCAppendOfflineInstallStartLine,
    47  		constants.RCAppendOfflineInstallStopLine,
    48  		"user_offlineinstall_env",
    49  	}
    50  	AutostartID RcIdentification = RcIdentification{
    51  		constants.RCAppendAutostartStartLine,
    52  		constants.RCAppendAutostartStopLine,
    53  		"user_autostart_env",
    54  	}
    55  )
    56  
    57  func init() {
    58  	configMediator.RegisterOption(constants.PreservePs1ConfigKey, configMediator.Bool, false)
    59  }
    60  
    61  // Configurable defines an interface to store and get configuration data
    62  type Configurable interface {
    63  	Set(string, interface{}) error
    64  	GetBool(string) bool
    65  	GetString(string) string
    66  	GetStringMap(string) map[string]interface{}
    67  }
    68  
    69  type RcIdentification struct {
    70  	Start string
    71  	Stop  string
    72  	Key   string
    73  }
    74  
    75  func WriteRcFile(rcTemplateName string, path string, data RcIdentification, env map[string]string) error {
    76  	if err := fileutils.Touch(path); err != nil {
    77  		return err
    78  	}
    79  
    80  	rcData := map[string]interface{}{
    81  		"Start":                 data.Start,
    82  		"Stop":                  data.Stop,
    83  		"Env":                   env,
    84  		"ActivatedEnv":          constants.ActivatedStateEnvVarName,
    85  		"ConfigFile":            constants.ConfigFileName,
    86  		"ActivatedNamespaceEnv": constants.ActivatedStateNamespaceEnvVarName,
    87  		"Default":               data == DefaultID,
    88  	}
    89  
    90  	if err := CleanRcFile(path, data); err != nil {
    91  		return err
    92  	}
    93  
    94  	tpl, err := assets.ReadFileBytes(fmt.Sprintf("shells/%s", rcTemplateName))
    95  	if err != nil {
    96  		return errs.Wrap(err, "Failed to read asset")
    97  	}
    98  	t, err := template.New("rcfile_append").Parse(string(tpl))
    99  	if err != nil {
   100  		return errs.Wrap(err, "Templating failure")
   101  	}
   102  
   103  	var out bytes.Buffer
   104  	err = t.Execute(&out, rcData)
   105  	if err != nil {
   106  		return errs.Wrap(err, "Templating failure")
   107  	}
   108  
   109  	logging.Debug("Writing to %s:\n%s", path, out.String())
   110  
   111  	return fileutils.AppendToFile(path, []byte(fileutils.LineEnd+out.String()))
   112  }
   113  
   114  func WriteRcData(data string, path string, identification RcIdentification) error {
   115  	if err := fileutils.Touch(path); err != nil {
   116  		return err
   117  	}
   118  
   119  	if err := CleanRcFile(path, identification); err != nil {
   120  		return err
   121  	}
   122  
   123  	data = identification.Start + fileutils.LineEnd + data + fileutils.LineEnd + identification.Stop
   124  	logging.Debug("Writing to %s:\n%s", path, data)
   125  	return fileutils.AppendToFile(path, []byte(fileutils.LineEnd+data))
   126  }
   127  
   128  // RemoveLegacyInstallPath removes the PATH modification statement added to the shell-rc file by the legacy install script
   129  func RemoveLegacyInstallPath(path string) error {
   130  	if err := fileutils.Touch(path); err != nil {
   131  		return err
   132  	}
   133  	readFile, err := os.Open(path)
   134  	if err != nil {
   135  		return errs.Wrap(err, "IO failure")
   136  	}
   137  
   138  	scanner := bufio.NewScanner(readFile)
   139  	scanner.Split(bufio.ScanLines)
   140  
   141  	var fileContents []string
   142  	for scanner.Scan() {
   143  		text := scanner.Text()
   144  
   145  		// remove lines with marker added by legacy install script
   146  		if strings.Contains(text, "# ActiveState State Tool") {
   147  			continue
   148  		}
   149  
   150  		// Rebuild file contents
   151  		fileContents = append(fileContents, scanner.Text())
   152  	}
   153  	if err := readFile.Close(); err != nil {
   154  		return errs.Wrap(err, "failed to close %s", path)
   155  	}
   156  
   157  	return fileutils.WriteFile(path, []byte(strings.Join(fileContents, fileutils.LineEnd)))
   158  }
   159  
   160  func CleanRcFile(path string, data RcIdentification) error {
   161  	if err := fileutils.Touch(path); err != nil {
   162  		return err
   163  	}
   164  	readFile, err := os.Open(path)
   165  	if err != nil {
   166  		return errs.Wrap(err, "IO failure")
   167  	}
   168  
   169  	scanner := bufio.NewScanner(readFile)
   170  	scanner.Split(bufio.ScanLines)
   171  
   172  	var strip bool
   173  	var fileContents []string
   174  	for scanner.Scan() {
   175  		text := scanner.Text()
   176  
   177  		// Detect start line
   178  		if strings.Contains(text, data.Start) {
   179  			logging.Debug("Cleaning previous RC lines from %s", path)
   180  			strip = true
   181  		}
   182  
   183  		// Detect stop line
   184  		if strings.Contains(text, data.Stop) {
   185  			strip = false
   186  			continue
   187  		}
   188  
   189  		// Strip line
   190  		if strip {
   191  			continue
   192  		}
   193  
   194  		// Rebuild file contents
   195  		fileContents = append(fileContents, scanner.Text())
   196  	}
   197  	readFile.Close()
   198  
   199  	return fileutils.WriteFile(path, []byte(strings.Join(fileContents, fileutils.LineEnd)))
   200  }
   201  
   202  // SetupShellRcFile create a rc file to activate a runtime (without a project being present)
   203  func SetupShellRcFile(rcFileName, templateName string, env map[string]string, namespace *project.Namespaced, cfg Configurable) error {
   204  	tpl, err := assets.ReadFileBytes(fmt.Sprintf("shells/%s", templateName))
   205  	if err != nil {
   206  		return errs.Wrap(err, "Failed to read asset")
   207  	}
   208  	t, err := template.New("rcfile").Parse(string(tpl))
   209  	if err != nil {
   210  		return errs.Wrap(err, "Failed to parse template file.")
   211  	}
   212  
   213  	projectValue := ""
   214  	if namespace != nil {
   215  		projectValue = namespace.String()
   216  	}
   217  
   218  	var out bytes.Buffer
   219  	rcData := map[string]interface{}{
   220  		"Env":         env,
   221  		"Project":     projectValue,
   222  		"PreservePs1": cfg.GetBool(constants.PreservePs1ConfigKey),
   223  	}
   224  	err = t.Execute(&out, rcData)
   225  	if err != nil {
   226  		return errs.Wrap(err, "failed to execute template.")
   227  	}
   228  
   229  	f, err := os.Create(rcFileName)
   230  	if err != nil {
   231  		return locale.WrapError(err, "sscommon_rc_file_creation_err", "Failed to create file {{.V0}}", rcFileName)
   232  	}
   233  	defer f.Close()
   234  
   235  	_, err = f.WriteString(out.String())
   236  	if err != nil {
   237  		return errs.Wrap(err, "Failed to write to output buffer.")
   238  	}
   239  
   240  	err = os.Chmod(rcFileName, 0755)
   241  	if err != nil {
   242  		return errs.Wrap(err, "Failed to set executable flag.")
   243  	}
   244  	return nil
   245  }
   246  
   247  // SetupProjectRcFile creates a temporary RC file that our shell is initiated from, this allows us to template the logic
   248  // used for initialising the subshell
   249  func SetupProjectRcFile(prj *project.Project, templateName, ext string, env map[string]string, out output.Outputer, cfg Configurable, bashifyPaths bool) (*os.File, error) {
   250  	tpl, err := assets.ReadFileBytes(fmt.Sprintf("shells/%s", templateName))
   251  	if err != nil {
   252  		return nil, errs.Wrap(err, "Failed to read asset")
   253  	}
   254  
   255  	userScripts := ""
   256  
   257  	// Yes this is awkward, issue here - https://www.pivotaltracker.com/story/show/175619373
   258  	activatedKey := fmt.Sprintf("activated_%s", prj.Namespace().String())
   259  	for _, eventType := range project.ActivateEvents() {
   260  		event := prj.EventByName(eventType.String(), bashifyPaths)
   261  		if event == nil {
   262  			continue
   263  		}
   264  
   265  		v, err := event.Value()
   266  		if err != nil {
   267  			return nil, errs.Wrap(err, "Could not get event value")
   268  		}
   269  
   270  		if strings.ToLower(event.Name()) == project.FirstActivate.String() && !cfg.GetBool(activatedKey) {
   271  			userScripts = v + "\n" + userScripts
   272  		}
   273  
   274  		if strings.ToLower(event.Name()) == project.Activate.String() {
   275  			userScripts = userScripts + "\n" + v
   276  		}
   277  	}
   278  	err = cfg.Set(activatedKey, true)
   279  	if err != nil {
   280  		return nil, errs.Wrap(err, "Could not set activatedKey in config")
   281  	}
   282  
   283  	inuse := []string{}
   284  	scripts := map[string]string{}
   285  	var explicitName string
   286  	globalBinDir := filepath.Clean(storage.GlobalBinDir())
   287  
   288  	// Prepare script map to be parsed by template
   289  	for _, cmd := range prj.Scripts() {
   290  		explicitName = fmt.Sprintf("%s_%s", prj.NormalizedName(), cmd.Name())
   291  
   292  		path, err := exec.LookPath(cmd.Name())
   293  		dir := filepath.Clean(filepath.Dir(path))
   294  		if dir == globalBinDir {
   295  			continue
   296  		}
   297  		if err == nil {
   298  			// Do not overwrite commands that are already in use and
   299  			// keep track of those commands to warn to the user
   300  			inuse = append(inuse, cmd.Name())
   301  			continue
   302  		}
   303  
   304  		scripts[cmd.Name()] = cmd.Name()
   305  		scripts[explicitName] = cmd.Name()
   306  	}
   307  
   308  	if len(inuse) > 0 {
   309  		out.Notice(locale.Tr("warn_script_name_in_use", strings.Join(inuse, "[/RESET],[NOTICE] "), inuse[0], explicitName))
   310  	}
   311  
   312  	wd, err := osutils.Getwd()
   313  	if err != nil {
   314  		return nil, locale.WrapError(err, "err_subshell_wd", "", "Could not get working directory.")
   315  	}
   316  
   317  	isConsole := ext == ".bat" // yeah this is a dirty cheat, should find something more deterministic
   318  
   319  	actualEnv := map[string]string{}
   320  	for k, v := range env {
   321  		if strings.Contains(v, "\n") {
   322  			logging.Warning("Env key %s has a multi-line value, which is not supported", k)
   323  			continue
   324  		}
   325  		actualEnv[k] = v
   326  	}
   327  
   328  	rcData := map[string]interface{}{
   329  		"Owner":       prj.Owner(),
   330  		"Name":        prj.Name(),
   331  		"Env":         actualEnv,
   332  		"WD":          wd,
   333  		"UserScripts": userScripts,
   334  		"Scripts":     scripts,
   335  		"ExecName":    constants.CommandName,
   336  		"ActivatedMessage": colorize.ColorizedOrStrip(locale.Tl("project_activated",
   337  			"[SUCCESS]✔ Project \"{{.V0}}\" Has Been Activated[/RESET]", prj.Namespace().String()), isConsole),
   338  		"PreservePs1": cfg.GetBool(constants.PreservePs1ConfigKey),
   339  	}
   340  
   341  	currExec := osutils.Executable()
   342  	currExecAbsDir := filepath.Dir(currExec)
   343  	if bashifyPaths {
   344  		currExec, err = osutils.BashifyPath(currExec)
   345  		if err != nil {
   346  			return nil, errs.Wrap(err, "Could not bashify executable: %s", currExec)
   347  		}
   348  	}
   349  
   350  	listSep := string(os.PathListSeparator)
   351  	pathList, ok := env["PATH"]
   352  	inPathList, err := fileutils.PathInList(listSep, pathList, currExecAbsDir)
   353  	if err != nil {
   354  		return nil, errs.Wrap(err, "Could not check if %s is in PATH", currExecAbsDir)
   355  	}
   356  	if !ok || !inPathList {
   357  		safeExec := currExec
   358  		if strings.ContainsAny(currExec, " ") {
   359  			safeExec = fmt.Sprintf(`"%s"`, currExec) // quote for alias
   360  		}
   361  		rcData["ExecAlias"] = safeExec // alias {ExecName}={ExecAlias}
   362  	}
   363  
   364  	t := template.New("rcfile")
   365  	t.Funcs(map[string]interface{}{
   366  		"splitLines": func(v string) []string { return strings.Split(v, "\n") },
   367  	})
   368  
   369  	t, err = t.Parse(string(tpl))
   370  	if err != nil {
   371  		return nil, errs.Wrap(err, "Templating failure")
   372  	}
   373  
   374  	var o bytes.Buffer
   375  	err = t.Execute(&o, rcData)
   376  	if err != nil {
   377  		return nil, errs.Wrap(err, "Templating failure")
   378  	}
   379  
   380  	tmpFile, err := tempfile.TempFileWithSuffix(os.TempDir(), "state-subshell-rc", ext)
   381  	if err != nil {
   382  		return nil, errs.Wrap(err, "OS failure")
   383  	}
   384  	defer tmpFile.Close()
   385  
   386  	_, err = tmpFile.WriteString(o.String())
   387  	if err != nil {
   388  		return nil, errs.Wrap(err, "Failed to write to output buffer.")
   389  	}
   390  
   391  	logging.Debug("Using project RC: (%s) %s", tmpFile.Name(), o.String())
   392  
   393  	return tmpFile, nil
   394  }
   395  
   396  func ProjectRCIdentifier(base RcIdentification, namespace *project.Namespaced) RcIdentification {
   397  	id := base
   398  	id.Start = fmt.Sprintf("%s-%s", id.Start, namespace.String())
   399  	id.Stop = fmt.Sprintf("%s-%s", id.Stop, namespace.String())
   400  	id.Key = fmt.Sprintf("%s_%s", id.Key, namespace.String())
   401  	return id
   402  }