diff --git a/internal/assets/contents/shells/bashrc_append.sh b/internal/assets/contents/shells/bashrc_append.sh index c09af1c695..dab323c79c 100644 --- a/internal/assets/contents/shells/bashrc_append.sh +++ b/internal/assets/contents/shells/bashrc_append.sh @@ -6,4 +6,7 @@ export {{$K}}="{{$V}}:$PATH" export {{$K}}="{{$V}}" {{- end}} {{- end}} +if [[ ! -z "${{.ActivatedEnv}}" && -f "${{.ActivatedEnv}}/{{.ConfigFile}}" ]]; then + echo "State Tool is operating on project ${{.ActivatedNamespaceEnv}}, located at ${{.ActivatedEnv}}" +fi # {{.Stop}} diff --git a/internal/assets/contents/shells/fishrc_append.fish b/internal/assets/contents/shells/fishrc_append.fish index 0f233db90d..e0145c2ea0 100644 --- a/internal/assets/contents/shells/fishrc_append.fish +++ b/internal/assets/contents/shells/fishrc_append.fish @@ -6,4 +6,8 @@ set -xg {{$K}} "{{$V}}:$PATH" set -xg {{$K}} "{{$V}}" {{- end}} {{- end}} -# {{.Stop}} \ No newline at end of file +if test ! -z "${{.ActivatedEnv}}" + and test -f "${{.ActivatedEnv}}/{{.ConfigFile}}" + echo "State Tool is operating on project ${{.ActivatedNamespaceEnv}}, located at ${{.ActivatedEnv}}" +end +# {{.Stop}} diff --git a/internal/assets/contents/shells/tcshrc_append.sh b/internal/assets/contents/shells/tcshrc_append.sh index a8042870fb..9b36097219 100644 --- a/internal/assets/contents/shells/tcshrc_append.sh +++ b/internal/assets/contents/shells/tcshrc_append.sh @@ -5,4 +5,7 @@ setenv {{$K}} "{{$V}}:$PATH" {{- else}} {{- end}} {{- end}} +if ( "${{.ActivatedEnv}}" != "" && -f "${{.ActivatedEnv}}/{{.ConfigFile}}" ) then + echo "State Tool is operating on project ${{.ActivatedNamespaceEnv}}, located at ${{.ActivatedEnv}}" +endif # {{.Stop}} diff --git a/internal/assets/contents/shells/zshrc_append.sh b/internal/assets/contents/shells/zshrc_append.sh index c09af1c695..dab323c79c 100644 --- a/internal/assets/contents/shells/zshrc_append.sh +++ b/internal/assets/contents/shells/zshrc_append.sh @@ -6,4 +6,7 @@ export {{$K}}="{{$V}}:$PATH" export {{$K}}="{{$V}}" {{- end}} {{- end}} +if [[ ! -z "${{.ActivatedEnv}}" && -f "${{.ActivatedEnv}}/{{.ConfigFile}}" ]]; then + echo "State Tool is operating on project ${{.ActivatedNamespaceEnv}}, located at ${{.ActivatedEnv}}" +fi # {{.Stop}} diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 9f4367442b..d62c67ba3d 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -73,6 +73,9 @@ const ActivatedStateEnvVarName = "ACTIVESTATE_ACTIVATED" // ActivatedStateIDEnvVarName is the name of the environment variable that is set when in an activated state, its value will be a unique id identifying a specific instance of an activated state const ActivatedStateIDEnvVarName = "ACTIVESTATE_ACTIVATED_ID" +// ActivatedStateProjectEnvVarName is the name of the environment variable that specifies the activated state's org/project namespace. +const ActivatedStateNamespaceEnvVarName = "ACTIVESTATE_ACTIVATED_NAMESPACE" + // ForwardedStateEnvVarName is the name of the environment variable that is set when in an activated state, its value will be the path of the project const ForwardedStateEnvVarName = "ACTIVESTATE_FORWARDED" diff --git a/internal/runbits/activation/activation.go b/internal/runbits/activation/activation.go index 865c7a9211..d6d23740d8 100644 --- a/internal/runbits/activation/activation.go +++ b/internal/runbits/activation/activation.go @@ -39,7 +39,7 @@ func ActivateAndWait( } } - ve, err := venv.GetEnv(false, true, projectDir) + ve, err := venv.GetEnv(false, true, projectDir, proj.Namespace().String()) if err != nil { return locale.WrapError(err, "error_could_not_activate_venv", "Could not retrieve environment information.") } diff --git a/internal/runners/exec/exec.go b/internal/runners/exec/exec.go index faf4624d78..e3a8fb5037 100644 --- a/internal/runners/exec/exec.go +++ b/internal/runners/exec/exec.go @@ -136,7 +136,7 @@ func (s *Exec) Run(params *Params, args ...string) (rerr error) { } venv := virtualenvironment.New(rt) - env, err := venv.GetEnv(true, false, projectDir) + env, err := venv.GetEnv(true, false, projectDir, projectNamespace) if err != nil { return locale.WrapError(err, "err_exec_env", "Could not retrieve environment information for your runtime") } diff --git a/internal/scriptrun/scriptrun.go b/internal/scriptrun/scriptrun.go index e60af28ccc..4e5d2a9034 100644 --- a/internal/scriptrun/scriptrun.go +++ b/internal/scriptrun/scriptrun.go @@ -81,7 +81,7 @@ func (s *ScriptRun) PrepareVirtualEnv() (rerr error) { venv := virtualenvironment.New(rt) projDir := filepath.Dir(s.project.Source().Path()) - env, err := venv.GetEnv(true, true, projDir) + env, err := venv.GetEnv(true, true, projDir, s.project.Namespace().String()) if err != nil { return errs.Wrap(err, "Could not get venv environment") } @@ -91,7 +91,7 @@ func (s *ScriptRun) PrepareVirtualEnv() (rerr error) { } // search the "clean" path first (PATHS that are set by venv) - env, err = venv.GetEnv(false, true, "") + env, err = venv.GetEnv(false, true, "", "") if err != nil { return errs.Wrap(err, "Could not get venv environment") } diff --git a/internal/subshell/sscommon/rcfile.go b/internal/subshell/sscommon/rcfile.go index b40d2d7e87..bf670d6533 100644 --- a/internal/subshell/sscommon/rcfile.go +++ b/internal/subshell/sscommon/rcfile.go @@ -73,9 +73,12 @@ func WriteRcFile(rcTemplateName string, path string, data RcIdentification, env } rcData := map[string]interface{}{ - "Start": data.Start, - "Stop": data.Stop, - "Env": env, + "Start": data.Start, + "Stop": data.Stop, + "Env": env, + "ActivatedEnv": constants.ActivatedStateEnvVarName, + "ConfigFile": constants.ConfigFileName, + "ActivatedNamespaceEnv": constants.ActivatedStateNamespaceEnvVarName, } if err := CleanRcFile(path, data); err != nil { diff --git a/internal/subshell/sscommon/rcfile_test.go b/internal/subshell/sscommon/rcfile_test.go index bd01eb3833..7f893a794a 100644 --- a/internal/subshell/sscommon/rcfile_test.go +++ b/internal/subshell/sscommon/rcfile_test.go @@ -43,35 +43,50 @@ func TestWriteRcFile(t *testing.T) { path string env map[string]string } + + zsh := fmt.Sprintf( + `export PATH="foo:$PATH" +if [[ ! -z "$%s" && -f "$%s/%s" ]]; then + echo "State Tool is operating on project $%s, located at $%s" +fi`, + constants.ActivatedStateEnvVarName, + constants.ActivatedStateEnvVarName, + constants.ConfigFileName, + constants.ActivatedStateNamespaceEnvVarName, + constants.ActivatedStateEnvVarName) + if runtime.GOOS == "windows" { + zsh = strings.ReplaceAll(zsh, "\n", "\r\n") + } + tests := []struct { name string args args - want error + want error wantContents string }{ { "Write RC to empty file", args{ - "fishrc_append.fish", + "zshrc_append.sh", fakeFileWithContents("", "", ""), map[string]string{ "PATH": "foo", }, }, nil, - fakeContents("", `set -xg PATH "foo:$PATH"`, ""), + fakeContents("", zsh, ""), }, { "Write RC update", args{ - "fishrc_append.fish", + "zshrc_append.sh", fakeFileWithContents("before", "SOMETHING ELSE", "after"), map[string]string{ "PATH": "foo", }, }, nil, - fakeContents(strings.Join([]string{"before", "after"}, fileutils.LineEnd), `set -xg PATH "foo:$PATH"`, ""), + fakeContents(strings.Join([]string{"before", "after"}, fileutils.LineEnd), zsh, ""), }, } for _, tt := range tests { diff --git a/internal/virtualenvironment/virtualenvironment.go b/internal/virtualenvironment/virtualenvironment.go index e6aca20fe3..055bc890fc 100644 --- a/internal/virtualenvironment/virtualenvironment.go +++ b/internal/virtualenvironment/virtualenvironment.go @@ -29,7 +29,7 @@ func New(runtime *runtime.Runtime) *VirtualEnvironment { } // GetEnv returns a map of the cumulative environment variables for all active virtual environments -func (v *VirtualEnvironment) GetEnv(inherit bool, useExecutors bool, projectDir string) (map[string]string, error) { +func (v *VirtualEnvironment) GetEnv(inherit bool, useExecutors bool, projectDir, namespace string) (map[string]string, error) { envMap := make(map[string]string) // Source runtime environment information @@ -44,6 +44,7 @@ func (v *VirtualEnvironment) GetEnv(inherit bool, useExecutors bool, projectDir if projectDir != "" { envMap[constants.ActivatedStateEnvVarName] = projectDir envMap[constants.ActivatedStateIDEnvVarName] = v.activationID + envMap[constants.ActivatedStateNamespaceEnvVarName] = namespace // Get project from explicitly defined configuration file configFile := filepath.Join(projectDir, constants.ConfigFileName) diff --git a/test/integration/shell_int_test.go b/test/integration/shell_int_test.go index 97940f367e..0a72eb211d 100644 --- a/test/integration/shell_int_test.go +++ b/test/integration/shell_int_test.go @@ -10,6 +10,8 @@ import ( "github.com/ActiveState/cli/internal/config" "github.com/ActiveState/cli/internal/fileutils" "github.com/ActiveState/cli/internal/subshell" + "github.com/ActiveState/cli/internal/subshell/bash" + "github.com/ActiveState/cli/internal/subshell/sscommon" "github.com/ActiveState/cli/internal/subshell/zsh" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" @@ -276,6 +278,53 @@ func (suite *ShellIntegrationTestSuite) SetupRCFile(ts *e2e.Session) { suite.Require().NoError(err) } +func (suite *ShellIntegrationTestSuite) TestNestedShellNotification() { + if runtime.GOOS == "windows" { + return // cmd.exe does not have an RC file to check for nested shells in + } + suite.OnlyRunForTags(tagsuite.Shell) + ts := e2e.New(suite.T(), false) + defer ts.Close() + + cfg, err := config.New() + suite.Require().NoError(err) + + ss := subshell.New(cfg) + err = subshell.ConfigureAvailableShells(ss, cfg, nil, sscommon.InstallID, true) // mimic installer + suite.Require().NoError(err) + + rcFile, err := ss.RcFile() + suite.Require().NoError(err) + suite.Require().FileExists(rcFile) + suite.Require().Contains(string(fileutils.ReadFileUnsafe(rcFile)), "State Tool is operating on project") + + cp := ts.Spawn("checkout", "ActiveState-CLI/small-python") + cp.Expect("Checked out project") + cp.ExpectExitCode(0) + + cp = ts.SpawnWithOpts( + e2e.WithArgs("shell", "small-python"), + e2e.AppendEnv("ACTIVESTATE_CLI_DISABLE_RUNTIME=false"), + ) + cp.Expect("Activated") + suite.Assert().NotContains(cp.TrimmedSnapshot(), "State Tool is operating on project") + + binary := ss.Binary() // platform-specific shell (zsh on macOS, bash on Linux, etc.) + // Cannot run bare binary because it will not load the rcFile in ts.Dirs.HomeDir. + // Instead, tell the shell to load rcFile. This is not needed in a non-test environment. + switch ss.Shell() { + case bash.Name: + binary = fmt.Sprintf("%s --rcfile %s", binary, rcFile) + case zsh.Name: + binary = fmt.Sprintf("ZDOTDIR=%s %s", filepath.Dir(rcFile), binary) + } + cp.SendLine(binary) + cp.ExpectLongString("State Tool is operating on project ActiveState-CLI/small-python") + cp.SendLine("exit") // subshell within a subshell + cp.SendLine("exit") + cp.ExpectExitCode(0) +} + func TestShellIntegrationTestSuite(t *testing.T) { suite.Run(t, new(ShellIntegrationTestSuite)) }