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..5d64abb0c1 100644 --- a/internal/assets/contents/shells/fishrc_append.fish +++ b/internal/assets/contents/shells/fishrc_append.fish @@ -6,4 +6,7 @@ set -xg {{$K}} "{{$V}}:$PATH" set -xg {{$K}} "{{$V}}" {{- end}} {{- end}} -# {{.Stop}} \ No newline at end of file +if test ! -z "${{.ActivatedEnv}}"; 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..19162f7e7b 100644 --- a/internal/subshell/sscommon/rcfile_test.go +++ b/internal/subshell/sscommon/rcfile_test.go @@ -3,6 +3,7 @@ package sscommon import ( "fmt" "reflect" + "runtime" "strings" "testing" @@ -43,10 +44,25 @@ func TestWriteRcFile(t *testing.T) { path string env map[string]string } + + fish := fmt.Sprintf( + `set -xg PATH "foo:$PATH" +if test ! -z "$%s"; test -f "$%s/%s" + echo "State Tool is operating on project $%s, located at $%s" +end`, + constants.ActivatedStateEnvVarName, + constants.ActivatedStateEnvVarName, + constants.ConfigFileName, + constants.ActivatedStateNamespaceEnvVarName, + constants.ActivatedStateEnvVarName) + if runtime.GOOS == "windows" { + fish = strings.ReplaceAll(fish, "\n", "\r\n") + } + tests := []struct { name string args args - want error + want error wantContents string }{ { @@ -59,7 +75,7 @@ func TestWriteRcFile(t *testing.T) { }, }, nil, - fakeContents("", `set -xg PATH "foo:$PATH"`, ""), + fakeContents("", fish, ""), }, { "Write RC update", @@ -71,7 +87,7 @@ func TestWriteRcFile(t *testing.T) { }, }, nil, - fakeContents(strings.Join([]string{"before", "after"}, fileutils.LineEnd), `set -xg PATH "foo:$PATH"`, ""), + fakeContents(strings.Join([]string{"before", "after"}, fileutils.LineEnd), fish, ""), }, } 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..889c96380f 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" @@ -263,6 +265,8 @@ func (suite *ShellIntegrationTestSuite) SetupRCFile(ts *e2e.Session) { rcFile, err := subshell.RcFile() suite.Require().NoError(err) + err = fileutils.TouchFileUnlessExists(rcFile) + suite.Require().NoError(err) err = fileutils.CopyFile(rcFile, filepath.Join(ts.Dirs.HomeDir, filepath.Base(rcFile))) suite.Require().NoError(err) @@ -276,6 +280,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() + + var ss subshell.SubShell + var rcFile string + env := []string{"ACTIVESTATE_CLI_DISABLE_RUNTIME=false"} + switch runtime.GOOS { + case "darwin": + ss = &zsh.SubShell{} + ss.SetBinary("zsh") + rcFile = filepath.Join(ts.Dirs.HomeDir, ".zshrc") + suite.Require().NoError(sscommon.WriteRcFile("zshrc_append.sh", rcFile, sscommon.DefaultID, nil)) + env = append(env, "SHELL=zsh") // override since CI tests are running on bash + case "linux": + ss = &bash.SubShell{} + ss.SetBinary("bash") + rcFile = filepath.Join(ts.Dirs.HomeDir, ".bashrc") + suite.Require().NoError(sscommon.WriteRcFile("bashrc_append.sh", rcFile, sscommon.DefaultID, nil)) + default: + suite.Fail("Unsupported OS") + } + suite.Require().Equal(filepath.Dir(rcFile), ts.Dirs.HomeDir, "rc file not in test suite homedir") + 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(env...)) + cp.Expect("Activated") + suite.Assert().NotContains(cp.TrimmedSnapshot(), "State Tool is operating on project") + cp.SendLine(fmt.Sprintf(`export HOME="%s"`, ts.Dirs.HomeDir)) // some shells do not forward this + + cp.SendLine(ss.Binary()) // platform-specific shell (zsh on macOS, bash on Linux, etc.) + 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)) }