blob: 7d5325b158c2554ea55acec43f5adade8203589d [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package profiles
import (
"bytes"
"fmt"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"v.io/jiri/jiri"
"v.io/jiri/project"
"v.io/jiri/util"
"v.io/x/lib/envvar"
)
// GoFlags lists all of the Go environment variables and will be sorted in the
// init function for this package.
var GoFlags = []string{
"CC",
"CC_FOR_TARGET",
"CGO_ENABLED",
"CXX_FOR_TARGET",
"GO15VENDOREXPERIMENT",
"GOARCH",
"GOBIN",
"GOEXE",
"GOGCCFLAGS",
"GOHOSTARCH",
"GOHOSTOS",
"GOOS",
"GOPATH",
"GORACE",
"GOROOT",
"GOTOOLDIR",
}
type ProfilesMode bool
func (pm *ProfilesMode) Set(s string) error {
v, err := strconv.ParseBool(s)
*pm = ProfilesMode(v)
return err
}
func (pm *ProfilesMode) Get() interface{} { return bool(*pm) }
func (pm *ProfilesMode) String() string { return fmt.Sprintf("%v", *pm) }
func (pm *ProfilesMode) IsBoolFlag() bool { return true }
const (
UseProfiles ProfilesMode = false
SkipProfiles ProfilesMode = true
)
func init() {
sort.Strings(GoFlags)
}
// UnsetGoEnvVars unsets Go environment variables in the given environment.
func UnsetGoEnvVars(env *envvar.Vars) {
for _, k := range GoFlags {
env.Delete(k)
}
}
// UnsetGoEnvMap unsets Go environment variables in the given environment.
func UnsetGoEnvMap(env map[string]string) {
for _, k := range GoFlags {
delete(env, k)
}
}
// GoEnvironmentFromOS() returns the values of all Go environment variables
// as set via the OS; unset variables are omitted.
func GoEnvironmentFromOS() []string {
os := envvar.SliceToMap(os.Environ())
vars := make([]string, 0, len(GoFlags))
for _, k := range GoFlags {
v, present := os[k]
if !present {
continue
}
vars = append(vars, envvar.JoinKeyValue(k, v))
}
return vars
}
// ConfigHelper wraps the various sources of configuration and profile
// information to provide convenient methods for determing the environment
// variables to use for a given situation. It creates an initial copy of the OS
// environment that is mutated by its various methods.
type ConfigHelper struct {
*envvar.Vars
profilesMode bool
jirix *jiri.X
config *util.Config
projects project.Projects
tools project.Tools
}
// NewConfigHelper creates a new config helper. If filename is of non-zero
// length then that file will be read as a profiles manifest file, if not, the
// existing, if any, in-memory profiles information will be used. If SkipProfiles
// is specified for profilesMode, then no profiles are used.
func NewConfigHelper(jirix *jiri.X, profilesMode ProfilesMode, filename string) (*ConfigHelper, error) {
config, err := util.LoadConfig(jirix)
if err != nil {
return nil, err
}
projects, tools, err := project.ReadManifest(jirix)
if err != nil {
return nil, err
}
if profilesMode == UseProfiles && len(filename) > 0 {
if err := Read(jirix, filename); err != nil {
return nil, err
}
}
ch := &ConfigHelper{
jirix: jirix,
config: config,
projects: projects,
tools: tools,
profilesMode: bool(profilesMode),
}
ch.Vars = envvar.VarsFromOS()
if profilesMode == SkipProfiles {
return ch, nil
}
if len(os.Getenv("JIRI_PROFILE")) > 0 {
return nil, fmt.Errorf(`old style profiles are no longer supported. Please
do not set JIRI_PROFILE.`)
}
return ch, nil
}
// Root returns the root of the jiri universe.
func (ch *ConfigHelper) Root() string {
return ch.jirix.Root
}
// MergeEnv merges the embedded environment with the environment
// variables provided by the vars parameter according to the policies parameter.
func (ch *ConfigHelper) MergeEnv(policies map[string]MergePolicy, vars ...[]string) {
MergeEnv(policies, ch.Vars, vars...)
}
// MergeEnvFromProfiles merges the embedded environment with the environment
// variables stored in the requested profiles. The profiles are those read from
// the manifest and in addition the 'jiri' profile may be used which refers to
// the environment variables maintained by the jiri tool itself. It will also
// expand all instances of ${JIRI_ROOT} in the returned environment.
func (ch *ConfigHelper) MergeEnvFromProfiles(policies map[string]MergePolicy, target Target, profileNames ...string) {
envs := [][]string{}
for _, profile := range profileNames {
var e []string
if profile == "jiri" {
e = ch.JiriProfile()
} else {
e = EnvFromProfile(target, profile)
}
if e == nil {
continue
}
envs = append(envs, e)
}
MergeEnv(policies, ch.Vars, envs...)
jiri.ExpandEnv(ch.jirix, ch.Vars)
}
// SkippingProfiles returns true if no profiles are being used.
func (ch *ConfigHelper) SkippingProfiles() bool {
return ch.profilesMode == bool(SkipProfiles)
}
// ValidateRequestProfilesAndTarget checks that the supplied slice of profiles
// names is supported (including the 'jiri' profile) and that each has
// the specified target installed taking account if running using profiles
// at all or if using old-style profiles.
func (ch *ConfigHelper) ValidateRequestedProfilesAndTarget(profileNames []string, target Target) error {
if ProfilesMode(ch.profilesMode) == SkipProfiles {
return nil
}
for _, n := range profileNames {
if n == "jiri" {
continue
}
if LookupProfileTarget(n, target) == nil {
return fmt.Errorf("%q for %q is not available or not installed, use the \"list\" command to see the installed/available profiles.", target, n)
}
}
return nil
}
// PrependToPath prepends its argument to the PATH environment variable.
func (ch *ConfigHelper) PrependToPATH(path string) {
existing := ch.GetTokens("PATH", ":")
ch.SetTokens("PATH", append([]string{path}, existing...), ":")
}
// JiriProfile returns a pseudo profile that is maintained by the Jiri
// tool itself, this currently consists of the GoPath and VDLPath variables.
// It will generally be used as the last profile in the set of profiles
// passed to MergeEnv.
func (ch *ConfigHelper) JiriProfile() []string {
return []string{ch.GoPath(), ch.VDLPath()}
}
// GoPath computes and returns the GOPATH environment variable based on the
// current jiri configuration.
func (ch *ConfigHelper) GoPath() string {
path := pathHelper(ch.jirix, ch.projects, ch.config.GoWorkspaces(), "")
return "GOPATH=" + envvar.JoinTokens(path, ":")
}
// VDLPath computes and returns the VDLPATH environment variable based on the
// current jiri configuration.
func (ch *ConfigHelper) VDLPath() string {
path := pathHelper(ch.jirix, ch.projects, ch.config.VDLWorkspaces(), "src")
return "VDLPATH=" + envvar.JoinTokens(path, ":")
}
// pathHelper is a utility function for determining paths for project workspaces.
func pathHelper(jirix *jiri.X, projects project.Projects, workspaces []string, suffix string) []string {
path := []string{}
for _, workspace := range workspaces {
absWorkspace := filepath.Join(jirix.Root, workspace, suffix)
// Only append an entry to the path if the workspace is rooted
// under a jiri project that exists locally or vice versa.
for _, project := range projects {
// We check if <project.Path> is a prefix of <absWorkspace> to
// account for Go workspaces nested under a single jiri project,
// such as: $JIRI_ROOT/release/projects/chat/go.
//
// We check if <absWorkspace> is a prefix of <project.Path> to
// account for Go workspaces that span multiple jiri projects,
// such as: $JIRI_ROOT/release/go.
if strings.HasPrefix(absWorkspace, project.Path) || strings.HasPrefix(project.Path, absWorkspace) {
if _, err := jirix.NewSeq().Stat(filepath.Join(absWorkspace)); err == nil {
path = append(path, absWorkspace)
break
}
}
}
}
return path
}
// The environment variables passed to a subprocess are the result
// of merging those in the processes environment and those from
// one or more profiles according to the policies defined below.
// There is a starting environment, nominally called 'base', and one
// or profile environments. The base environment will typically be that
// inherited by the running process from its invoking shell. A policy
// consists of an 'action' and an optional separator to use when concatenating
// variables.
type MergePolicy struct {
Action MergeAction
Separator string
}
type MergeAction int
const (
// Use the first value encountered
First MergeAction = iota
// Use the last value encountered.
Last
// Ignore the variable regardless of where it occurs.
Ignore
// Append the current value to the values already accumulated.
Append
// Prepend the current value to the values already accumulated.
Prepend
// Ignore the value in the base environment, but append in the profiles.
IgnoreBaseAndAppend
// Ignore the value in the base environment, but prepend in the profiles.
IgnoreBaseAndPrepend
// Ignore the value in the base environment, but use the first value from profiles.
IgnoreBaseAndUseFirst
// Ignore the value in the base environment, but use the last value from profiles.
IgnoreBaseAndUseLast
// Ignore the values in the profiles.
IgnoreProfiles
)
var (
// A MergePolicy with a Last action.
UseLast = MergePolicy{Action: Last}
// A MergePolicy with a First action.
UseFirst = MergePolicy{Action: First}
// A MergePolicy that ignores the variable, regardless of where it occurs.
IgnoreVariable = MergePolicy{Action: Ignore}
// A MergePolicy that appends using : as a separator.
AppendPath = MergePolicy{Action: Append, Separator: ":"}
// A MergePolicy that appends using " " as a separator.
AppendFlag = MergePolicy{Action: Append, Separator: " "}
// A MergePolicy that prepends using : as a separator.
PrependPath = MergePolicy{Action: Prepend, Separator: ":"}
// A MergePolicy that prepends using " " as a separator.
PrependFlag = MergePolicy{Action: Prepend, Separator: " "}
// A MergePolicy that will ignore base, but append across profiles using ':'
IgnoreBaseAppendPath = MergePolicy{Action: IgnoreBaseAndAppend, Separator: ":"}
// A MergePolicy that will ignore base, but append across profiles using ' '
IgnoreBaseAppendFlag = MergePolicy{Action: IgnoreBaseAndAppend, Separator: " "}
// A MergePolicy that will ignore base, but prepend across profiles using ':'
IgnoreBasePrependPath = MergePolicy{Action: IgnoreBaseAndPrepend, Separator: ":"}
// A MergePolicy that will ignore base, but prepend across profiles using ' '
IgnoreBasePrependFlag = MergePolicy{Action: IgnoreBaseAndPrepend, Separator: " "}
// A MergePolicy that will ignore base, but use the last value from profiles.
IgnoreBaseUseFirst = MergePolicy{Action: IgnoreBaseAndUseFirst}
// A MergePolicy that will ignore base, but use the last value from profiles.
IgnoreBaseUseLast = MergePolicy{Action: IgnoreBaseAndUseLast}
// A MergePolicy that will always use the value from base and ignore profiles.
UseBaseIgnoreProfiles = MergePolicy{Action: IgnoreProfiles}
)
// ProfileMergePolicies returns an instance of MergePolicies that containts
// appropriate default policies for use with MergeEnv from within
// profile implementations.
func ProfileMergePolicies() MergePolicies {
values := MergePolicies{
"PATH": AppendPath,
"CCFLAGS": AppendFlag,
"CXXFLAGS": AppendFlag,
"LDFLAGS": AppendFlag,
"CGO_CFLAGS": AppendFlag,
"CGO_CXXFLAGS": AppendFlag,
"CGO_LDFLAGS": AppendFlag,
"GOPATH": IgnoreBaseAppendPath,
"GOARCH": UseBaseIgnoreProfiles,
"GOOS": UseBaseIgnoreProfiles,
}
mp := MergePolicies{}
for k, v := range values {
mp[k] = v
}
return mp
}
// JiriMergePolicies returns an instance of MergePolicies that contains
// appropriate default policies for use with MergeEnv from jiri packages
// and subcommands such as those used to build go, java etc.
func JiriMergePolicies() MergePolicies {
mp := ProfileMergePolicies()
mp["GOPATH"] = PrependPath
mp["VDLPATH"] = PrependPath
mp["GOARCH"] = UseFirst
mp["GOOS"] = UseFirst
mp["GOROOT"] = IgnoreBaseUseLast
return mp
}
// MergeEnv merges environment variables in base with those
// in vars according to the suppled policies.
func MergeEnv(policies map[string]MergePolicy, base *envvar.Vars, vars ...[]string) {
// Remove any variables that have the IgnoreBase policy.
for k, _ := range base.ToMap() {
switch policies[k].Action {
case Ignore, IgnoreBaseAndAppend, IgnoreBaseAndPrepend, IgnoreBaseAndUseFirst, IgnoreBaseAndUseLast:
base.Delete(k)
}
}
for _, ev := range vars {
for _, tmp := range ev {
k, v := envvar.SplitKeyValue(tmp)
policy := policies[k]
action := policy.Action
switch policy.Action {
case IgnoreBaseAndAppend:
action = Append
case IgnoreBaseAndPrepend:
action = Prepend
case IgnoreBaseAndUseLast:
action = Last
case IgnoreBaseAndUseFirst:
action = First
}
switch action {
case Ignore, IgnoreProfiles:
continue
case Append, Prepend:
sep := policy.Separator
ov := base.GetTokens(k, sep)
nv := envvar.SplitTokens(v, sep)
if action == Append {
base.SetTokens(k, append(ov, nv...), sep)
} else {
base.SetTokens(k, append(nv, ov...), sep)
}
case First:
if !base.Contains(k) {
base.Set(k, v)
}
case Last:
base.Set(k, v)
}
}
}
}
// EnvFromProfile obtains the environment variable settings from the specified
// profile and target. It returns nil if the target and/or profile could not
// be found.
func EnvFromProfile(target Target, profileName string) []string {
t := LookupProfileTarget(profileName, target)
if t == nil {
return nil
}
return t.Env.Vars
}
// WithDefaultVersion returns a copy of the supplied target with its
// version set to the default (i.e. emtpy string).
func WithDefaultVersion(target Target) Target {
t := &target
t.SetVersion("")
return target
}
type MergePolicies map[string]MergePolicy
func (mp *MergePolicy) String() string {
switch mp.Action {
case First:
return "use first"
case Last:
return "use last"
case Append:
return "append using '" + mp.Separator + "'"
case Prepend:
return "prepend using '" + mp.Separator + "'"
case IgnoreBaseAndAppend:
return "ignore in environment/base, append using '" + mp.Separator + "'"
case IgnoreBaseAndPrepend:
return "ignore in environment/base, prepend using '" + mp.Separator + "'"
case IgnoreBaseAndUseLast:
return "ignore in environment/base, use last value from profiles"
case IgnoreBaseAndUseFirst:
return "ignore in environment/base, use first value from profiles"
case IgnoreProfiles:
return "ignore in profiles"
}
return "unrecognised action"
}
func (mp MergePolicies) Usage() string {
return `<var>:<var>|<var>:|+<var>|<var>+|=<var>|<var>=
<var> - use the first value of <var> encountered, this is the default action.
<var>* - use the last value of <var> encountered.
-<var> - ignore the variable, regardless of where it occurs.
:<var> - append instances of <var> using : as a separator.
<var>: - prepend instances of <var> using : as a separator.
+<var> - append instances of <var> using space as a separator.
<var>+ - prepend instances of <var> using space as a separator.
^:<var> - ignore <var> from the base/inherited environment but append in profiles as per :<var>.
^<var>: - ignore <var> from the base/inherited environment but prepend in profiles as per <var>:.
^+<var> - ignore <var> from the base/inherited environment but append in profiles as per +<var>.
^<var>+ - ignore <var> from the base/inherited environment but prepend in profiles as per <var>+.
^<var> - ignore <var> from the base/inherited environment but use the first value encountered in profiles.
^<var>* - ignore <var> from the base/inherited environment but use the last value encountered in profiles.
<var>^ - ignore <var> from profiles.`
}
func separator(s string) string {
switch s {
case ":":
return ":"
default:
return "+"
}
}
// String implements flag.Value. It generates a string that can be used
// to recreate the MergePolicies value and that can be passed as a parameter
// to another process.
func (mp MergePolicies) String() string {
buf := bytes.Buffer{}
// Ensure a stable order.
keys := make([]string, 0, len(mp))
for k, _ := range mp {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v := mp[k]
var s string
switch v.Action {
case First:
s = k
case Last:
s = k + "*"
case Append:
s = separator(v.Separator) + k
case Prepend:
s = k + separator(v.Separator)
case IgnoreBaseAndAppend:
s = "^" + separator(v.Separator) + k
case IgnoreBaseAndPrepend:
s = "^" + k + separator(v.Separator)
case IgnoreBaseAndUseLast:
s = "^" + k + "*"
case IgnoreBaseAndUseFirst:
s = "^" + k
case IgnoreProfiles:
s = k + "^"
}
buf.WriteString(s)
buf.WriteString(",")
}
return strings.TrimSuffix(buf.String(), ",")
}
func (mp MergePolicies) DebugString() string {
buf := bytes.Buffer{}
for k, v := range mp {
buf.WriteString(k + ": " + v.String() + ", ")
}
return strings.TrimSuffix(buf.String(), ", ")
}
// Get implements flag.Getter
func (mp MergePolicies) Get() interface{} {
r := make(MergePolicies, len(mp))
for k, v := range mp {
r[k] = v
}
return r
}
func parseIgnoreBase(val string) (MergePolicy, string) {
if len(val) == 0 {
return IgnoreBaseUseLast, val
}
// [:+]<var>
switch val[0] {
case ':':
return IgnoreBaseAppendPath, val[1:]
case '+':
return IgnoreBaseAppendFlag, val[1:]
}
// <var>[:+]
last := len(val) - 1
switch val[last] {
case ':':
return IgnoreBasePrependPath, val[:last]
case '+':
return IgnoreBasePrependFlag, val[:last]
case '*':
return IgnoreBaseUseLast, val[:last]
}
return IgnoreBaseUseFirst, val
}
// Set implements flag.Value
func (mp MergePolicies) Set(values string) error {
if len(values) == 0 {
return fmt.Errorf("no value!")
}
for _, val := range strings.Split(values, ",") {
// [:+^-]<var>
switch val[0] {
case '^':
a, s := parseIgnoreBase(val[1:])
mp[s] = a
continue
case '-':
mp[val[1:]] = IgnoreVariable
continue
case ':':
mp[val[1:]] = AppendPath
continue
case '+':
mp[val[1:]] = AppendFlag
continue
}
// <var>[:+^]
last := len(val) - 1
switch val[last] {
case ':':
mp[val[:last]] = PrependPath
case '+':
mp[val[:last]] = PrependFlag
case '*':
mp[val[:last]] = UseLast
case '^':
mp[val[:last]] = UseBaseIgnoreProfiles
default:
mp[val] = UseFirst
}
}
return nil
}