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 }