github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/platform/runtime/envdef/environment.go (about) 1 package envdef 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strings" 9 10 "github.com/ActiveState/cli/internal/errs" 11 "github.com/ActiveState/cli/internal/osutils" 12 "github.com/thoas/go-funk" 13 14 "github.com/ActiveState/cli/internal/fileutils" 15 "github.com/ActiveState/cli/internal/locale" 16 ) 17 18 // EnvironmentDefinition provides all the information needed to set up an 19 // environment in which the packaged artifact contents can be used. 20 type EnvironmentDefinition struct { 21 // Env is a list of environment variables to be set 22 Env []EnvironmentVariable `json:"env"` 23 24 // Transforms is a list of file transformations 25 Transforms []FileTransform `json:"file_transforms"` 26 27 // InstallDir is the directory (inside the artifact tarball) that needs to be installed on the user's computer 28 InstallDir string `json:"installdir"` 29 } 30 31 // EnvironmentVariable defines a single environment variable and its values 32 type EnvironmentVariable struct { 33 Name string `json:"env_name"` 34 Values []string `json:"values"` 35 Join VariableJoin `json:"join"` 36 Inherit bool `json:"inherit"` 37 Separator string `json:"separator"` 38 } 39 40 // VariableJoin defines a strategy to join environment variables together 41 type VariableJoin int 42 43 const ( 44 // Prepend indicates that new variables should be prepended 45 Prepend VariableJoin = iota 46 // Append indicates that new variables should be prepended 47 Append 48 // Disallowed indicates that there must be only one value for an environment variable 49 Disallowed 50 ) 51 52 // MarshalText marshals a join directive for environment variables 53 func (j VariableJoin) MarshalText() ([]byte, error) { 54 var res string 55 switch j { 56 default: 57 res = "prepend" 58 case Append: 59 res = "append" 60 case Disallowed: 61 res = "disallowed" 62 } 63 return []byte(res), nil 64 } 65 66 // UnmarshalText un-marshals a join directive for environment variables 67 func (j *VariableJoin) UnmarshalText(text []byte) error { 68 switch string(text) { 69 case "prepend": 70 *j = Prepend 71 case "append": 72 *j = Append 73 case "disallowed": 74 *j = Disallowed 75 default: 76 return fmt.Errorf("Invalid join directive %s", string(text)) 77 } 78 return nil 79 } 80 81 // UnmarshalJSON unmarshals an environment variable 82 // It sets default values for Inherit, Join and Separator if they are not specified 83 func (ev *EnvironmentVariable) UnmarshalJSON(data []byte) error { 84 type evAlias EnvironmentVariable 85 v := &evAlias{ 86 Inherit: true, 87 Separator: ":", 88 Join: Prepend, 89 } 90 91 err := json.Unmarshal(data, v) 92 if err != nil { 93 return err 94 } 95 96 *ev = EnvironmentVariable(*v) 97 return nil 98 } 99 100 // NewEnvironmentDefinition returns an environment definition unmarshaled from a 101 // file 102 func NewEnvironmentDefinition(fp string) (*EnvironmentDefinition, error) { 103 blob, err := os.ReadFile(fp) 104 if err != nil { 105 return nil, locale.WrapError(err, "envdef_file_not_found", "", fp) 106 } 107 ed := &EnvironmentDefinition{} 108 err = json.Unmarshal(blob, ed) 109 if err != nil { 110 return nil, locale.WrapError(err, "envdef_unmarshal_error", "", fp) 111 } 112 return ed, nil 113 } 114 115 // WriteFile marshals an environment definition to a file 116 func (ed *EnvironmentDefinition) WriteFile(filepath string) error { 117 blob, err := ed.Marshal() 118 if err != nil { 119 return err 120 } 121 return os.WriteFile(filepath, blob, 0666) 122 } 123 124 // WriteFile marshals an environment definition to a file 125 func (ed *EnvironmentDefinition) Marshal() ([]byte, error) { 126 blob, err := json.MarshalIndent(ed, "", " ") 127 if err != nil { 128 return []byte(""), err 129 } 130 return blob, nil 131 } 132 133 // ExpandVariables expands substitution strings specified in the environment variable values. 134 // Right now, the only valid substition string is `${INSTALLDIR}` which is being replaced 135 // with the base of the installation directory for a given project 136 func (ed *EnvironmentDefinition) ExpandVariables(constants Constants) *EnvironmentDefinition { 137 res := ed 138 for k, v := range constants { 139 res = ed.ReplaceString(fmt.Sprintf("${%s}", k), v) 140 } 141 return res 142 } 143 144 // ReplaceString replaces the string `from` with its `replacement` value 145 // in every environment variable value 146 func (ed *EnvironmentDefinition) ReplaceString(from string, replacement string) *EnvironmentDefinition { 147 res := ed 148 newEnv := make([]EnvironmentVariable, 0, len(ed.Env)) 149 for _, ev := range ed.Env { 150 newEnv = append(newEnv, ev.ReplaceString(from, replacement)) 151 } 152 res.Env = newEnv 153 return res 154 } 155 156 // Merge merges two environment definitions according to the join strategy of 157 // the second one. 158 // - Environment variables that are defined in both definitions, are merged with 159 // EnvironmentVariable.Merge() and added to the result 160 // - Environment variables that are defined in only one of the two definitions, 161 // are added to the result directly 162 func (ed EnvironmentDefinition) Merge(other *EnvironmentDefinition) (*EnvironmentDefinition, error) { 163 res := ed 164 if other == nil { 165 return &res, nil 166 } 167 168 newEnv := []EnvironmentVariable{} 169 170 thisEnvNames := funk.Map( 171 ed.Env, 172 func(x EnvironmentVariable) string { return x.Name }, 173 ).([]string) 174 175 newKeys := make([]string, 0, len(other.Env)) 176 otherEnvMap := map[string]EnvironmentVariable{} 177 for _, ev := range other.Env { 178 if !funk.ContainsString(thisEnvNames, ev.Name) { 179 newKeys = append(newKeys, ev.Name) 180 } 181 otherEnvMap[ev.Name] = ev 182 } 183 184 // add new keys to environment 185 for _, k := range newKeys { 186 oev := otherEnvMap[k] 187 newEnv = append(newEnv, oev) 188 } 189 190 // merge keys 191 for _, ev := range ed.Env { 192 otherEv, ok := otherEnvMap[ev.Name] 193 if !ok { 194 // if key exists only in this variable, use it 195 newEnv = append(newEnv, ev) 196 } else { 197 // otherwise: merge this variable and the other environment variable 198 mev, err := ev.Merge(otherEv) 199 if err != nil { 200 return &res, err 201 } 202 newEnv = append(newEnv, *mev) 203 } 204 } 205 res.Env = newEnv 206 return &res, nil 207 } 208 209 // ReplaceString replaces the string 'from' with 'replacement' in 210 // environment variable values 211 func (ev EnvironmentVariable) ReplaceString(from string, replacement string) EnvironmentVariable { 212 res := ev 213 values := make([]string, 0, len(ev.Values)) 214 215 for _, v := range ev.Values { 216 values = append(values, strings.ReplaceAll(v, "${INSTALLDIR}", replacement)) 217 } 218 res.Values = values 219 return res 220 } 221 222 // Merge merges two environment variables according to the join strategy defined by 223 // the second environment variable 224 // If join strategy of the second variable is "prepend" or "append", the values 225 // are prepended or appended to the first variable. 226 // If join strategy is set to "disallowed", the variables need to have exactly 227 // one value, and both merged values need to be identical, otherwise an error is 228 // returned. 229 func (ev EnvironmentVariable) Merge(other EnvironmentVariable) (*EnvironmentVariable, error) { 230 res := ev 231 232 // separators and inherit strategy always need to match for two merged variables 233 if ev.Separator != other.Separator || ev.Inherit != other.Inherit { 234 return nil, fmt.Errorf("cannot merge environment definitions: incompatible `separator` or `inherit` directives") 235 } 236 237 // 'disallowed' join strategy needs to be set for both or none of the variables 238 if (ev.Join == Disallowed || other.Join == Disallowed) && ev.Join != other.Join { 239 return nil, fmt.Errorf("cannot merge environment definitions: incompatible `join` directives") 240 } 241 242 switch other.Join { 243 case Prepend: 244 res.Values = filterValuesUniquely(append(other.Values, ev.Values...), true) 245 case Append: 246 res.Values = filterValuesUniquely(append(ev.Values, other.Values...), false) 247 case Disallowed: 248 if len(ev.Values) != 1 || len(other.Values) != 1 || (ev.Values[0] != other.Values[0]) { 249 sep := string(ev.Separator) 250 return nil, fmt.Errorf( 251 "cannot merge environment definitions: no join strategy for variable %s with values %s and %s", 252 ev.Name, 253 strings.Join(ev.Values, sep), strings.Join(other.Values, sep), 254 ) 255 256 } 257 default: 258 return nil, fmt.Errorf("could not join environment variable %s: invalid `join` directive %v", ev.Name, other.Join) 259 } 260 res.Join = other.Join 261 return &res, nil 262 } 263 264 // filterValuesUniquely removes duplicate entries from a list of strings 265 // If `keepFirst` is true, only the first occurrence is kept, otherwise the last 266 // one. 267 func filterValuesUniquely(values []string, keepFirst bool) []string { 268 nvs := make([]*string, len(values)) 269 posMap := map[string][]int{} 270 271 for i, v := range values { 272 pmv, ok := posMap[v] 273 if !ok { 274 pmv = []int{} 275 } 276 pmv = append(pmv, i) 277 posMap[v] = pmv 278 } 279 280 var getPos func([]int) int 281 if keepFirst { 282 getPos = func(x []int) int { return x[0] } 283 } else { 284 getPos = func(x []int) int { return x[len(x)-1] } 285 } 286 287 for v, positions := range posMap { 288 pos := getPos(positions) 289 cv := v 290 nvs[pos] = &cv 291 } 292 293 res := make([]string, 0, len(values)) 294 for _, nv := range nvs { 295 if nv != nil { 296 res = append(res, *nv) 297 } 298 } 299 return res 300 } 301 302 // ValueString joins the environment variable values into a single string 303 // If duplicate values are found, only one of them is considered: for join 304 // strategy `prepend` only the first occurrence, for join strategy `append` only 305 // the last one. 306 func (ev *EnvironmentVariable) ValueString() string { 307 return strings.Join( 308 filterValuesUniquely(ev.Values, ev.Join == Prepend), 309 string(ev.Separator)) 310 } 311 312 // GetEnvBasedOn returns the environment variable names and values defined by 313 // the EnvironmentDefinition. 314 // If an environment variable is configured to inherit from the base 315 // environment (`Inherit==true`), the base environment defined by the 316 // `envLookup` method is joined with these environment variables. 317 // This function is mostly used for testing. Use GetEnv() in production. 318 func (ed *EnvironmentDefinition) GetEnvBasedOn(envLookup func(string) (string, bool)) (map[string]string, error) { 319 res := map[string]string{} 320 321 for _, ev := range ed.Env { 322 pev := &ev 323 if pev.Inherit { 324 osValue, hasOsValue := envLookup(pev.Name) 325 if hasOsValue { 326 osEv := ev 327 osEv.Values = []string{osValue} 328 var err error 329 pev, err = osEv.Merge(ev) 330 if err != nil { 331 return nil, err 332 333 } 334 } 335 } else if _, hasOsValue := os.LookupEnv(pev.Name); hasOsValue { 336 res[pev.Name] = "" // unset 337 } 338 // only add environment variable if at least one value is set (This allows us to remove variables from the environment.) 339 if len(ev.Values) > 0 { 340 res[pev.Name] = pev.ValueString() 341 } 342 } 343 return res, nil 344 } 345 346 // GetEnv returns the environment variable names and values defined by 347 // the EnvironmentDefinition. 348 // If an environment variable is configured to inherit from the OS 349 // environment (`Inherit==true`), the base environment defined by the 350 // `envLookup` method is joined with these environment variables. 351 func (ed *EnvironmentDefinition) GetEnv(inherit bool) map[string]string { 352 lookupEnv := os.LookupEnv 353 if !inherit { 354 lookupEnv = func(_ string) (string, bool) { return "", false } 355 } 356 res, err := ed.GetEnvBasedOn(lookupEnv) 357 if err != nil { 358 panic(fmt.Sprintf("Could not inherit OS environment variable: %v", err)) 359 } 360 return res 361 } 362 363 func FilterPATH(env map[string]string, excludes ...string) { 364 PATH, exists := env["PATH"] 365 if !exists { 366 return 367 } 368 369 newPaths := []string{} 370 paths := strings.Split(PATH, string(os.PathListSeparator)) 371 for _, p := range paths { 372 pc := filepath.Clean(p) 373 includePath := true 374 for _, exclude := range excludes { 375 if pc == filepath.Clean(exclude) { 376 includePath = false 377 break 378 } 379 } 380 if includePath { 381 newPaths = append(newPaths, p) 382 } 383 } 384 385 env["PATH"] = strings.Join(newPaths, string(os.PathListSeparator)) 386 } 387 388 type ExecutablePaths []string 389 390 func (ed *EnvironmentDefinition) ExecutablePaths() (ExecutablePaths, error) { 391 env := ed.GetEnv(false) 392 393 // Retrieve artifact binary directory 394 var bins []string 395 if p, ok := env["PATH"]; ok { 396 bins = strings.Split(p, string(os.PathListSeparator)) 397 } 398 399 exes, err := osutils.Executables(bins) 400 if err != nil { 401 return nil, errs.Wrap(err, "Could not detect executables") 402 } 403 404 // Remove duplicate executables as per PATH and PATHEXT 405 exes, err = osutils.UniqueExes(exes, os.Getenv("PATHEXT")) 406 if err != nil { 407 return nil, errs.Wrap(err, "Could not detect unique executables, make sure your PATH and PATHEXT environment variables are properly configured.") 408 } 409 410 return exes, nil 411 } 412 413 func (ed *EnvironmentDefinition) ExecutableDirs() (ExecutablePaths, error) { 414 exes, err := ed.ExecutablePaths() 415 if err != nil { 416 return nil, errs.Wrap(err, "Could not get executable paths") 417 } 418 419 var dirs ExecutablePaths 420 for _, p := range exes { 421 dirs = append(dirs, filepath.Dir(p)) 422 } 423 dirs = funk.UniqString(dirs) 424 425 return dirs, nil 426 } 427 428 // FindBinPathFor returns the PATH directory in which the executable can be found. 429 // If the executable cannot be found, an empty string is returned. 430 // This function should be called after variables names are expanded with ExpandVariables() 431 func (ed *EnvironmentDefinition) FindBinPathFor(executable string) string { 432 for _, ev := range ed.Env { 433 if ev.Name == "PATH" { 434 for _, dir := range ev.Values { 435 if fileutils.TargetExists(filepath.Join(dir, executable)) { 436 return filepath.Clean(filepath.FromSlash(dir)) 437 } 438 } 439 } 440 } 441 return "" 442 }