Merge "v.io/jiri/profiles: prevent use of target Tags."
diff --git a/profiles/.api b/profiles/.api
index 4dee0b0..3e7f821 100644
--- a/profiles/.api
+++ b/profiles/.api
@@ -1,43 +1,61 @@
+pkg profiles, const Append MergeAction
+pkg profiles, const AppendJiriProfile AppendJiriProfileMode
 pkg profiles, const DefaultDirPerm os.FileMode
 pkg profiles, const DefaultFilePerm os.FileMode
+pkg profiles, const DoNotAppendJiriProfile bool
+pkg profiles, const First MergeAction
+pkg profiles, const Ignore MergeAction
+pkg profiles, const IgnoreBaseAndAppend MergeAction
+pkg profiles, const IgnoreBaseAndPrepend MergeAction
+pkg profiles, const IgnoreBaseAndUseFirst MergeAction
+pkg profiles, const IgnoreBaseAndUseLast MergeAction
+pkg profiles, const IgnoreProfiles MergeAction
 pkg profiles, const Install Action
+pkg profiles, const Last MergeAction
 pkg profiles, const Original Version
+pkg profiles, const Prepend MergeAction
 pkg profiles, const SkipProfiles ProfilesMode
 pkg profiles, const Uninstall Action
 pkg profiles, const UseProfiles ProfilesMode
 pkg profiles, const V2 Version
 pkg profiles, const V3 Version
+pkg profiles, const V4 Version
 pkg profiles, func AddProfileTarget(string, Target) error
 pkg profiles, func AtomicAction(*tool.Context, func() error, string, string) error
-pkg profiles, func CommonConcatVariables() map[string]string
-pkg profiles, func CommonIgnoreVariables() map[string]bool
 pkg profiles, func DefaultTarget() Target
-pkg profiles, func EnsureProfileTargetIsInstalled(*tool.Context, string, Target, string) error
-pkg profiles, func EnsureProfileTargetIsUninstalled(*tool.Context, string, Target, string) error
+pkg profiles, func EnsureProfileTargetIsInstalled(*tool.Context, string, RelativePath, Target) error
+pkg profiles, func EnsureProfileTargetIsUninstalled(*tool.Context, string, RelativePath, Target) error
+pkg profiles, func EnvFromProfile(Target, string) []string
+pkg profiles, func Fetch(*tool.Context, string, string) error
 pkg profiles, func FindTarget(OrderedTargets, *Target) *Target
 pkg profiles, func FindTargetByTag(OrderedTargets, *Target) *Target
 pkg profiles, func FindTargetWithDefault(OrderedTargets, *Target) *Target
 pkg profiles, func GitCloneRepo(*tool.Context, string, string, string, os.FileMode) error
 pkg profiles, func GoEnvironmentFromOS() []string
+pkg profiles, func InitProfilesFromFlag(string, AppendJiriProfileMode) []string
 pkg profiles, func InsertTarget(OrderedTargets, *Target) OrderedTargets
 pkg profiles, func InstallPackages(*tool.Context, []string) error
 pkg profiles, func InstallProfile(string, string)
+pkg profiles, func JiriMergePolicies() MergePolicies
 pkg profiles, func LookupManager(string) Manager
 pkg profiles, func LookupProfile(string) *Profile
 pkg profiles, func LookupProfileTarget(string, Target) *Target
 pkg profiles, func Managers() []string
-pkg profiles, func MergeEnv(map[string]string, map[string]bool, *envvar.Vars, ...[]string)
-pkg profiles, func MergeEnvFromProfiles(map[string]string, map[string]bool, *envvar.Vars, Target, ...string) ([]string, error)
+pkg profiles, func MergeEnv(map[string]MergePolicy, *envvar.Vars, ...[]string)
 pkg profiles, func NativeTarget() Target
 pkg profiles, func NewConfigHelper(*tool.Context, ProfilesMode, string) (*ConfigHelper, error)
+pkg profiles, func NewRelativePath(string, string) RelativePath
 pkg profiles, func NewTarget(string) (Target, error)
 pkg profiles, func NewTargetWithEnv(string, string) (Target, error)
 pkg profiles, func NewVersionInfo(string, map[string]interface{}, string) *VersionInfo
+pkg profiles, func ProfileMergePolicies() MergePolicies
 pkg profiles, func Profiles() []string
 pkg profiles, func Read(*tool.Context, string) error
 pkg profiles, func Register(string, Manager)
 pkg profiles, func RegisterManifestFlag(*flag.FlagSet, *string, string)
-pkg profiles, func RegisterProfileFlags(*flag.FlagSet, *ProfilesMode, *string, *string, string, *Target)
+pkg profiles, func RegisterMergePoliciesFlag(*flag.FlagSet, *MergePolicies)
+pkg profiles, func RegisterProfileFlags(*flag.FlagSet, *ProfilesMode, *string, *string, string, *MergePolicies, *Target)
+pkg profiles, func RegisterProfilesFlag(*flag.FlagSet, *string)
 pkg profiles, func RegisterTargetAndEnvFlags(*flag.FlagSet, *Target)
 pkg profiles, func RegisterTargetFlag(*flag.FlagSet, *Target)
 pkg profiles, func RemoveProfileTarget(string, Target) bool
@@ -46,25 +64,31 @@
 pkg profiles, func SchemaVersion() Version
 pkg profiles, func UnsetGoEnvMap(map[string]string)
 pkg profiles, func UnsetGoEnvVars(*envvar.Vars)
+pkg profiles, func Unzip(*tool.Context, string, string) error
 pkg profiles, func UpdateProfileTarget(string, Target) error
+pkg profiles, func WithDefaultVersion(Target) Target
 pkg profiles, func Write(*tool.Context, string) error
+pkg profiles, method (*ConfigHelper) GoPath() string
+pkg profiles, method (*ConfigHelper) JiriProfile() []string
 pkg profiles, method (*ConfigHelper) LegacyProfiles() bool
+pkg profiles, method (*ConfigHelper) MergeEnv(map[string]MergePolicy, ...[]string)
+pkg profiles, method (*ConfigHelper) MergeEnvFromProfiles(map[string]MergePolicy, Target, ...string)
 pkg profiles, method (*ConfigHelper) PrependToPATH(string)
 pkg profiles, method (*ConfigHelper) Root() string
-pkg profiles, method (*ConfigHelper) SetEnvFromProfiles(map[string]string, map[string]bool, string, Target)
-pkg profiles, method (*ConfigHelper) SetGoPath()
-pkg profiles, method (*ConfigHelper) SetVDLPath()
 pkg profiles, method (*ConfigHelper) SkippingProfiles() bool
+pkg profiles, method (*ConfigHelper) VDLPath() string
 pkg profiles, method (*ConfigHelper) ValidateRequestedProfilesAndTarget([]string, Target) error
 pkg profiles, method (*Environment) Get() interface{}
 pkg profiles, method (*Environment) Set(string) error
 pkg profiles, method (*Environment) String() string
 pkg profiles, method (*Environment) Usage() string
+pkg profiles, method (*MergePolicy) String() string
 pkg profiles, method (*Profile) Targets() OrderedTargets
 pkg profiles, method (*ProfilesMode) Get() interface{}
 pkg profiles, method (*ProfilesMode) IsBoolFlag() bool
 pkg profiles, method (*ProfilesMode) Set(string) error
 pkg profiles, method (*ProfilesMode) String() string
+pkg profiles, method (*RelativePath) String() string
 pkg profiles, method (*Target) Arch() string
 pkg profiles, method (*Target) Less(*Target) bool
 pkg profiles, method (*Target) OS() string
@@ -73,6 +97,7 @@
 pkg profiles, method (*Target) Tag() string
 pkg profiles, method (*Target) TargetSpecificDirname() string
 pkg profiles, method (*Target) Usage() string
+pkg profiles, method (*Target) UseCommandLineEnv()
 pkg profiles, method (*Target) Version() string
 pkg profiles, method (*VersionInfo) Default() string
 pkg profiles, method (*VersionInfo) IsNewerThanDefault(string) bool
@@ -81,10 +106,20 @@
 pkg profiles, method (*VersionInfo) Select(string) (string, error)
 pkg profiles, method (*VersionInfo) String() string
 pkg profiles, method (*VersionInfo) Supported() []string
+pkg profiles, method (MergePolicies) DebugString() string
+pkg profiles, method (MergePolicies) Get() interface{}
+pkg profiles, method (MergePolicies) Set(string) error
+pkg profiles, method (MergePolicies) String() string
+pkg profiles, method (MergePolicies) Usage() string
 pkg profiles, method (OrderedTargets) Len() int
 pkg profiles, method (OrderedTargets) Less(int, int) bool
 pkg profiles, method (OrderedTargets) Sort()
 pkg profiles, method (OrderedTargets) Swap(int, int)
+pkg profiles, method (RelativePath) Expand() string
+pkg profiles, method (RelativePath) ExpandEnv(*envvar.Vars)
+pkg profiles, method (RelativePath) Join(...string) RelativePath
+pkg profiles, method (RelativePath) RelativePath() string
+pkg profiles, method (RelativePath) RootJoin(...string) RelativePath
 pkg profiles, method (Target) CommandLineEnv() Environment
 pkg profiles, method (Target) CrossCompiling() bool
 pkg profiles, method (Target) DebugString() string
@@ -93,29 +128,48 @@
 pkg profiles, method (Target) Match(*Target) bool
 pkg profiles, method (Target) String() string
 pkg profiles, type Action int
+pkg profiles, type AppendJiriProfileMode bool
 pkg profiles, type ConfigHelper struct
 pkg profiles, type ConfigHelper struct, embedded *envvar.Vars
 pkg profiles, type Environment struct
 pkg profiles, type Environment struct, Vars []string
-pkg profiles, type Manager interface { AddFlags, Info, Install, Name, Root, SetRoot, String, Uninstall, VersionInfo }
+pkg profiles, type Manager interface { AddFlags, Info, Install, Name, String, Uninstall, VersionInfo }
 pkg profiles, type Manager interface, AddFlags(*flag.FlagSet, Action)
 pkg profiles, type Manager interface, Info() string
-pkg profiles, type Manager interface, Install(*tool.Context, Target) error
+pkg profiles, type Manager interface, Install(*tool.Context, RelativePath, Target) error
 pkg profiles, type Manager interface, Name() string
-pkg profiles, type Manager interface, Root() string
-pkg profiles, type Manager interface, SetRoot(string)
 pkg profiles, type Manager interface, String() string
-pkg profiles, type Manager interface, Uninstall(*tool.Context, Target) error
+pkg profiles, type Manager interface, Uninstall(*tool.Context, RelativePath, Target) error
 pkg profiles, type Manager interface, VersionInfo() *VersionInfo
+pkg profiles, type MergeAction int
+pkg profiles, type MergePolicies map[string]MergePolicy
+pkg profiles, type MergePolicy struct
+pkg profiles, type MergePolicy struct, Action MergeAction
+pkg profiles, type MergePolicy struct, Separator string
 pkg profiles, type OrderedTargets []*Target
 pkg profiles, type Profile struct
 pkg profiles, type Profile struct, Name string
 pkg profiles, type Profile struct, Root string
 pkg profiles, type ProfilesMode bool
+pkg profiles, type RelativePath struct
 pkg profiles, type Target struct
 pkg profiles, type Target struct, Env Environment
 pkg profiles, type Target struct, InstallationDir string
 pkg profiles, type Target struct, UpdateTime time.Time
 pkg profiles, type Version int
 pkg profiles, type VersionInfo struct
+pkg profiles, var AppendFlag MergePolicy
+pkg profiles, var AppendPath MergePolicy
 pkg profiles, var GoFlags []string
+pkg profiles, var IgnoreBaseAppendFlag MergePolicy
+pkg profiles, var IgnoreBaseAppendPath MergePolicy
+pkg profiles, var IgnoreBasePrependFlag MergePolicy
+pkg profiles, var IgnoreBasePrependPath MergePolicy
+pkg profiles, var IgnoreBaseUseFirst MergePolicy
+pkg profiles, var IgnoreBaseUseLast MergePolicy
+pkg profiles, var IgnoreVariable MergePolicy
+pkg profiles, var PrependFlag MergePolicy
+pkg profiles, var PrependPath MergePolicy
+pkg profiles, var UseBaseIgnoreProfiles MergePolicy
+pkg profiles, var UseFirst MergePolicy
+pkg profiles, var UseLast MergePolicy
diff --git a/profiles/commandline/driver.go b/profiles/commandline/driver.go
index 1b26baf..5f27d52 100644
--- a/profiles/commandline/driver.go
+++ b/profiles/commandline/driver.go
@@ -12,6 +12,7 @@
 	"bytes"
 	"flag"
 	"fmt"
+	"path/filepath"
 	"strings"
 	"text/template"
 
@@ -111,6 +112,7 @@
 }
 
 var (
+	rootPath             profiles.RelativePath
 	targetFlag           profiles.Target
 	manifestFlag         string
 	showManifestFlag     bool
@@ -123,6 +125,7 @@
 	mergePoliciesFlag    profiles.MergePolicies
 	specificVersionsFlag bool
 	cleanupFlag          bool
+	rmAllFlag            bool
 )
 
 func Main(name string) {
@@ -140,6 +143,8 @@
 		panic(err)
 	}
 
+	rootPath = profiles.NewRelativePath("JIRI_ROOT", rootDir).Join("profiles")
+
 	// Every sub-command accepts: --manifest
 	for _, fs := range []*flag.FlagSet{
 		&cmdInstall.Flags,
@@ -191,6 +196,7 @@
 	// cleanup accepts the following flags:
 	cmdCleanup.Flags.BoolVar(&cleanupFlag, "gc", false, "uninstall profile targets that are older than the current default")
 	cmdCleanup.Flags.BoolVar(&specificVersionsFlag, "ensure-specific-versions-are-set", false, "ensure that profile targets have a specific version set")
+	cmdCleanup.Flags.BoolVar(&rmAllFlag, "rm-all", false, "remove profile manifest and all profile generated output files.")
 }
 
 func runList(env *cmdline.Env, args []string) error {
@@ -329,6 +335,14 @@
 	return out.String()
 }
 
+func handleRelativePath(root profiles.RelativePath, s string) string {
+	// Handle the transition from absolute to relative paths.
+	if filepath.IsAbs(s) {
+		return s
+	}
+	return root.RootJoin(s).Expand()
+}
+
 func fmtInfo(ctx *tool.Context, mgr profiles.Manager, profile *profiles.Profile, target *profiles.Target) (string, error) {
 	if len(infoFlag) > 0 {
 		// Populate an instance listInfo
@@ -348,7 +362,7 @@
 		}
 		info.SchemaVersion = profiles.SchemaVersion()
 		if target != nil {
-			info.Target.InstallationDir = target.InstallationDir
+			info.Target.InstallationDir = handleRelativePath(rootPath, target.InstallationDir)
 			info.Target.CommandLineEnv = target.CommandLineEnv().Vars
 			info.Target.Env = target.Env.Vars
 			clenv := ""
@@ -358,7 +372,7 @@
 			info.Target.Command = fmt.Sprintf("jiri v23-profile install --target=%s %s%s", target, clenv, name)
 		}
 		if profile != nil {
-			info.Profile.Root = profile.Root
+			info.Profile.Root = handleRelativePath(rootPath, profile.Root)
 		}
 
 		// Use a template to print out any field in our instance of listInfo.
@@ -454,14 +468,13 @@
 			continue
 		}
 		vi := mgr.VersionInfo()
-		mgr.SetRoot(rootDir)
 		for _, target := range profile.Targets() {
 			if vi.IsNewerThanDefault(target.Version()) {
 				if verboseFlag {
 					fmt.Fprintf(ctx.Stdout(), "Updating %s %s from %q to %s\n", n, target, target.Version(), vi)
 				}
 				target.SetVersion(vi.Default())
-				err := mgr.Install(ctx, *target)
+				err := mgr.Install(ctx, rootPath, *target)
 				logResult(ctx, "Update", mgr, *target, err)
 				if err != nil {
 					return err
@@ -483,7 +496,7 @@
 		profile := profiles.LookupProfile(n)
 		for _, target := range profile.Targets() {
 			if vi.IsOlderThanDefault(target.Version()) {
-				err := mgr.Uninstall(ctx, *target)
+				err := mgr.Uninstall(ctx, rootPath, *target)
 				logResult(ctx, "gc", mgr, *target, err)
 				if err != nil {
 					return err
@@ -519,6 +532,16 @@
 	return nil
 }
 
+func runRmAll(ctx *tool.Context) error {
+	if err := ctx.Run().Remove(manifestFlag); err != nil {
+		return err
+	}
+	if err := ctx.Run().RemoveAll(rootPath.Expand()); err != nil {
+		return err
+	}
+	return nil
+}
+
 func runCleanup(env *cmdline.Env, args []string) error {
 	ctx := tool.NewContextFromEnv(env)
 	if err := profiles.Read(ctx, manifestFlag); err != nil {
@@ -547,6 +570,16 @@
 		}
 		dirty = true
 	}
+	if rmAllFlag {
+		if verboseFlag {
+			fmt.Fprintf(ctx.Stdout(), "Removing profile manifest and all profile output files\n")
+		}
+		if err := runRmAll(ctx); err != nil {
+			return err
+		}
+		// Don't write out the profiles manifest file again.
+		return nil
+	}
 	if !dirty {
 		return fmt.Errorf("at least one option must be specified")
 	}
@@ -570,7 +603,6 @@
 			return err
 		}
 		target.SetVersion(version)
-		mgr.SetRoot(rootDir)
 		if err := fn(mgr, ctx, target); err != nil {
 			return err
 		}
@@ -599,7 +631,7 @@
 	}
 	if err := applyCommand(names, env, ctx, targetFlag,
 		func(mgr profiles.Manager, ctx *tool.Context, target profiles.Target) error {
-			err := mgr.Install(ctx, target)
+			err := mgr.Install(ctx, rootPath, target)
 			logResult(ctx, "Install:", mgr, target, err)
 			return err
 		}); err != nil {
@@ -623,9 +655,8 @@
 			if profile == nil || mgr == nil {
 				continue
 			}
-			mgr.SetRoot(rootDir)
 			for _, target := range profile.Targets() {
-				if err := mgr.Uninstall(ctx, *target); err != nil {
+				if err := mgr.Uninstall(ctx, rootPath, *target); err != nil {
 					logResult(ctx, "Uninstall", mgr, *target, err)
 					return err
 				}
@@ -635,7 +666,7 @@
 	} else {
 		applyCommand(args, env, ctx, targetFlag,
 			func(mgr profiles.Manager, ctx *tool.Context, target profiles.Target) error {
-				err := mgr.Uninstall(ctx, target)
+				err := mgr.Uninstall(ctx, rootPath, target)
 				logResult(ctx, "Uninstall", mgr, target, err)
 				return err
 			})
diff --git a/profiles/env.go b/profiles/env.go
index b848d57..0d1bacb 100644
--- a/profiles/env.go
+++ b/profiles/env.go
@@ -174,7 +174,8 @@
 // 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.
+// 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) {
 	if ch.legacyMode {
 		return
@@ -193,6 +194,8 @@
 		envs = append(envs, e)
 	}
 	MergeEnv(policies, ch.Vars, envs...)
+	rp := NewRelativePath("JIRI_ROOT", ch.root)
+	rp.ExpandEnv(ch.Vars)
 }
 
 // SkippingProfiles returns true if no profiles are being used.
diff --git a/profiles/manager.go b/profiles/manager.go
index ba40ecd..5314174 100644
--- a/profiles/manager.go
+++ b/profiles/manager.go
@@ -38,10 +38,13 @@
 
 import (
 	"flag"
+	"path/filepath"
 	"sort"
+	"strings"
 	"sync"
 
 	"v.io/jiri/tool"
+	"v.io/x/lib/envvar"
 )
 
 var (
@@ -86,6 +89,72 @@
 	return registry.managers[name]
 }
 
+// RelativePath represents a relative path whose root is specified
+// by an environment variable, eg. ${JIRI_ROOT}/profiles/go. It provides
+// access to the 'expanded' value of this variable along with any
+// path components appended to it.
+type RelativePath struct {
+	name  string
+	value string
+	path  string
+}
+
+// NewRelativePath creates a new instance of RelativePath with
+// the variable name as its root and value as the value of the variable.
+func NewRelativePath(name, value string) RelativePath {
+	return RelativePath{name: name, value: value}
+}
+
+// Join returns a copy of RelativePath with the specified components
+// appended to it using filepath.Join.
+func (rp RelativePath) Join(components ...string) RelativePath {
+	nrp := rp
+	nrp.path = filepath.Join(append([]string{nrp.path}, components...)...)
+	return nrp
+}
+
+// RootJoin creates a new RelativePath that has the same variable
+// and associated value as its receiver and then appends components to it
+// using filepath.Join.
+func (rp RelativePath) RootJoin(components ...string) RelativePath {
+	nrp := RelativePath{name: rp.name, value: rp.value}
+	nrp.path = filepath.Join(append([]string{nrp.path}, components...)...)
+	return nrp
+}
+
+// Expand returns the path with the root variable expanded.
+func (rp RelativePath) Expand() string {
+	return filepath.Join(rp.value, rp.path)
+}
+
+// String returns the RelativePath with the root variable name as the
+// root - i.e. ${name}[/<any append components>].
+func (rp *RelativePath) String() string {
+	root := "${" + rp.name + "}"
+	if len(rp.path) == 0 {
+		return root
+	}
+	return root + string(filepath.Separator) + rp.path
+}
+
+// RelativePath returns just the relative path component of RelativePath.
+func (rp RelativePath) RelativePath() string {
+	return rp.path
+}
+
+// ExpandEnv expands all instances of the root variable in the supplied
+// environment.
+func (rp RelativePath) ExpandEnv(env *envvar.Vars) {
+	e := env.ToMap()
+	root := "${" + rp.name + "}"
+	for k, v := range e {
+		n := strings.Replace(v, root, rp.value, -1)
+		if n != v {
+			env.Set(k, n)
+		}
+	}
+}
+
 type Action int
 
 const (
@@ -100,11 +169,6 @@
 	// to the supplied FlagSet for the specified Action.
 	// They should be named <profile-name>.<flag>.
 	AddFlags(*flag.FlagSet, Action)
-	// SetRoot sets the top level directory for the installation. It must be
-	// called once and before Install/Uninstall/Update are called.
-	SetRoot(dir string)
-	// Root returns the top level directory for the installation.
-	Root() string
 	// Name returns the name of this profile.
 	Name() string
 	// Info returns an informative description of the profile.
@@ -115,9 +179,9 @@
 	// is its name and version.
 	String() string
 	// Install installs the profile for the specified build target.
-	Install(ctx *tool.Context, target Target) error
+	Install(ctx *tool.Context, root RelativePath, target Target) error
 	// Uninstall uninstalls the profile for the specified build target. When
 	// the last target for any given profile is uninstalled, then the profile
 	// itself (i.e. the source code) will be uninstalled.
-	Uninstall(ctx *tool.Context, target Target) error
+	Uninstall(ctx *tool.Context, root RelativePath, target Target) error
 }
diff --git a/profiles/manager_test.go b/profiles/manager_test.go
index f7ec878..400fe7e 100644
--- a/profiles/manager_test.go
+++ b/profiles/manager_test.go
@@ -9,11 +9,39 @@
 	"fmt"
 	"os"
 	"path/filepath"
+	"testing"
 
 	"v.io/jiri/profiles"
 	"v.io/jiri/tool"
 )
 
+func TestRelativePath(t *testing.T) {
+	rp := profiles.NewRelativePath("VAR", "var")
+	if got, want := rp.Expand(), "var"; got != want {
+		t.Errorf("got %v, want %v", got, want)
+	}
+	if got, want := rp.String(), "${VAR}"; got != want {
+		t.Errorf("got %v, want %v", got, want)
+	}
+	rp = rp.Join("a", "b")
+	if got, want := rp.Expand(), filepath.Join("var", "a", "b"); got != want {
+		t.Errorf("got %v, want %v", got, want)
+	}
+	if got, want := rp.String(), "${VAR}"+string(filepath.Separator)+filepath.Join("a", "b"); got != want {
+		t.Errorf("got %v, want %v", got, want)
+	}
+	rp = rp.Join("x")
+	if got, want := rp.RootJoin("a").Expand(), filepath.Join("var", "a"); got != want {
+		t.Errorf("got %v, want %v", got, want)
+	}
+	if got, want := rp.Join("y").Expand(), filepath.Join("var", "a", "b", "x", "y"); got != want {
+		t.Errorf("got %v, want %v", got, want)
+	}
+	if got, want := rp.RelativePath(), filepath.Join("a", "b", "x"); got != want {
+		t.Errorf("got %v, want %v", got, want)
+	}
+}
+
 type myNewProfile struct {
 	name, root, status string
 	versionInfo        *profiles.VersionInfo
@@ -32,14 +60,6 @@
 	return p.name
 }
 
-func (p *myNewProfile) SetRoot(root string) {
-	p.root = root
-}
-
-func (p *myNewProfile) Root() string {
-	return p.root
-}
-
 func (p *myNewProfile) Info() string {
 	return `
 The myNewProfile is for testing purposes only
@@ -61,13 +81,13 @@
 func (p *myNewProfile) AddFlags(*flag.FlagSet, profiles.Action) {
 }
 
-func (p *myNewProfile) Install(ctx *tool.Context, target profiles.Target) error {
+func (p *myNewProfile) Install(ctx *tool.Context, root profiles.RelativePath, target profiles.Target) error {
 	p.status = "installed"
 	profiles.AddProfileTarget(p.name, target)
 	return nil
 }
 
-func (p *myNewProfile) Uninstall(ctx *tool.Context, target profiles.Target) error {
+func (p *myNewProfile) Uninstall(ctx *tool.Context, root profiles.RelativePath, target profiles.Target) error {
 	profiles.RemoveProfileTarget(p.name, target)
 	if profiles.LookupProfile(p.name) == nil {
 		p.status = "uninstalled"
@@ -87,6 +107,7 @@
 	}
 	init()
 
+	rootPath := profiles.NewRelativePath("JIRI_ROOT", ".").Join("profiles")
 	mgr := profiles.LookupManager(myProfile)
 	if mgr == nil {
 		panic("manager not found for: " + myProfile)
@@ -94,7 +115,7 @@
 
 	ctx := tool.NewDefaultContext()
 	// Install myNewProfile for target.
-	if err := mgr.Install(ctx, target); err != nil {
+	if err := mgr.Install(ctx, rootPath, target); err != nil {
 		panic("failed to find manager for: " + myProfile)
 	}
 
@@ -120,7 +141,7 @@
 	}
 
 	fmt.Println(mgr.String())
-	mgr.Uninstall(ctx, target)
+	mgr.Uninstall(ctx, rootPath, target)
 	fmt.Println(mgr.String())
 	fmt.Println(mgr.VersionInfo().Supported())
 	fmt.Println(mgr.VersionInfo().Default())
diff --git a/profiles/manifest.go b/profiles/manifest.go
index 4c54ce1..91f798c 100644
--- a/profiles/manifest.go
+++ b/profiles/manifest.go
@@ -22,9 +22,14 @@
 type Version int
 
 const (
+	// Original, old-style profiles without a version #
 	Original Version = 0
-	V2       Version = 2
-	V3       Version = 3
+	// First version of new-style profiles.
+	V2 Version = 2
+	// V3 added support for recording the options that were used to install profiles.
+	V3 Version = 3
+	// V4 adds support for relative path names in profiles and environment variables.
+	V4 Version = 4
 )
 
 // Profile represents a suite of software that is managed by an implementation
diff --git a/profiles/util.go b/profiles/util.go
index 2ce3e91..248fdba 100644
--- a/profiles/util.go
+++ b/profiles/util.go
@@ -18,7 +18,7 @@
 
 	"v.io/jiri/collect"
 	"v.io/jiri/project"
-  "v.io/jiri/runutil"
+	"v.io/jiri/runutil"
 	"v.io/jiri/tool"
 )
 
@@ -106,7 +106,9 @@
 			if !ctx.Run().FileExists(completionLogPath) {
 				ctx.Run().RemoveAll(dir)
 			} else {
-				fmt.Fprintf(ctx.Stdout(), "AtomicAction: %s already completed in %s\n", message, dir)
+				if ctx.Verbose() {
+					fmt.Fprintf(ctx.Stdout(), "AtomicAction: %s already completed in %s\n", message, dir)
+				}
 				return nil
 			}
 		}
@@ -209,12 +211,22 @@
 	return ctx.Run().Function(installDepsFn, "Install dependencies")
 }
 
-// EnsureProfileTargetIsInstalled ensures that the requested profile and target
-// is installed, installing it if only if necessary.
-func EnsureProfileTargetIsInstalled(ctx *tool.Context, profile string, target Target, root string) error {
+// ensureAction ensures that the requested profile and target
+// is installed/uninstalled, installing/uninstalling it if only if necessary.
+func ensureAction(ctx *tool.Context, action Action, profile string, root RelativePath, target Target) error {
+	verb := ""
+	switch action {
+	case Install:
+		verb = "install"
+	case Uninstall:
+		verb = "uninstall"
+	default:
+		return fmt.Errorf("unrecognised action %v", action)
+	}
+
 	if t := LookupProfileTarget(profile, target); t != nil {
 		if ctx.Run().Opts().Verbose {
-			fmt.Fprintf(ctx.Stdout(), "%v %v is already installed as %v\n", profile, target, t)
+			fmt.Fprintf(ctx.Stdout(), "%v %v is already %sed as %v\n", profile, target, verb, t)
 		}
 		return nil
 	}
@@ -227,42 +239,25 @@
 		return err
 	}
 	target.SetVersion(version)
-	mgr.SetRoot(root)
 	if ctx.Run().Opts().Verbose || ctx.Run().Opts().DryRun {
-		fmt.Fprintf(ctx.Stdout(), "install %s %s\n", profile, target.DebugString())
+		fmt.Fprintf(ctx.Stdout(), "%s %s %s\n", verb, profile, target.DebugString())
 	}
-	if err := mgr.Install(ctx, target); err != nil {
-		return err
+	if action == Install {
+		return mgr.Install(ctx, root, target)
 	}
-	return nil
+	return mgr.Uninstall(ctx, root, target)
+}
+
+// EnsureProfileTargetIsInstalled ensures that the requested profile and target
+// is installed, installing it if only if necessary.
+func EnsureProfileTargetIsInstalled(ctx *tool.Context, profile string, root RelativePath, target Target) error {
+	return ensureAction(ctx, Install, profile, root, target)
 }
 
 // EnsureProfileTargetIsUninstalled ensures that the requested profile and target
 // are no longer installed.
-func EnsureProfileTargetIsUninstalled(ctx *tool.Context, profile string, target Target, root string) error {
-	if LookupProfileTarget(profile, target) != nil {
-		if ctx.Run().Opts().Verbose {
-			fmt.Fprintf(ctx.Stdout(), "%v is not installed: %v", profile, target)
-		}
-		return nil
-	}
-	mgr := LookupManager(profile)
-	if mgr == nil {
-		return fmt.Errorf("profile %v is not supported", profile)
-	}
-	version, err := mgr.VersionInfo().Select(target.Version())
-	if err != nil {
-		return err
-	}
-	target.SetVersion(version)
-	mgr.SetRoot(root)
-	if ctx.Run().Opts().Verbose || ctx.Run().Opts().DryRun {
-		fmt.Fprintf(ctx.Stdout(), "uninstall %s %s\n", profile, target.DebugString())
-	}
-	if err := mgr.Uninstall(ctx, target); err != nil {
-		return err
-	}
-	return nil
+func EnsureProfileTargetIsUninstalled(ctx *tool.Context, profile string, root RelativePath, target Target) error {
+	return ensureAction(ctx, Uninstall, profile, root, target)
 }
 
 // Fetch downloads the specified url and saves it to dst.