jiri: Get rid of project.JiriRoot, and many uses of JIRI_ROOT

Now that we have jiri.X to hold all our jiri-related state, we
can start putting useful stuff there.  The CL is the first step:
get rid of project.JiriRoot, and make all of our utilities use
jiri.X.Root instead of the environment variable.

There's lots of code changes scattered throughout.  Here's some
of the larger themes:

1) Replace many functions in project/paths.go with similar
methods on jiri.X that don't need to return errors.

2) Move FakeJiriRoot from jiri/project to jiritest.  That code
should never be used in production.  We can clean up
FakeJiriRoot more and use it more widely, but we'll do that
separately.

3) Also add jiritest.NewX_DeprecatedEnv, to cater to tests that
actually rely on JIRI_ROOT pointing to a valid development
tree.  As the naming suggests, we'll get rid of that function,
but for now it's good enough to isolate it so that it's
obvious it's only used in tests.

4) Remove spurious setting and resetting of the JIRI_ROOT envvar
from many tests.  We set and reset it via FakeJiriRoot, as
well as jiritest.NewX, so this behavior is now isolated.

5) Rewrite the mailer to use v.io/x/lib/cmdline, and to follow
our other conventions for jiri.X.

After this CL, the production code only refers to jiri.X.Root to
find the root directory, and that's always initialized at tool
start-up.  This means we'll be able to safely transition to the
fs-walking logic, and it'll only happen once.  All the code that
uses the root is also simpler, since we don't need error checks.

There is one exception.  The profiles code needs to access the
jiri root directory to set up default flag values, and that runs
in an init func before main, and before we have a jiri.X.  This
code now uses jiri.FindRoot, which is a best-effort that doesn't
return any errors.  It's better not to panic in tool init funcs,
since otherwise the user can't get help for a command if
JIRI_ROOT isn't set.  With the new behavior, if a user hasn't set
JIRI_ROOT the flag defaults will be wrong, but if they try to run
any command they'll still get an error that JIRI_ROOT isn't set,
so the flag defaults don't matter.

MultiPart: 1/2

Change-Id: I1368498a57b6543d6c264a1d92495395cad07f0e
diff --git a/cl.go b/cl.go
index 241d146..f5dc422 100644
--- a/cl.go
+++ b/cl.go
@@ -81,7 +81,7 @@
 	if err != nil {
 		return "", err
 	}
-	return filepath.Join(topLevel, project.MetadataDirName(), branch, commitMessageFileName), nil
+	return filepath.Join(topLevel, jiri.ProjectMetaDir, branch, commitMessageFileName), nil
 }
 
 func getDependencyPathFileName(jirix *jiri.X, branch string) (string, error) {
@@ -89,7 +89,7 @@
 	if err != nil {
 		return "", err
 	}
-	return filepath.Join(topLevel, project.MetadataDirName(), branch, dependencyPathFileName), nil
+	return filepath.Join(topLevel, jiri.ProjectMetaDir, branch, dependencyPathFileName), nil
 }
 
 func getDependentCLs(jirix *jiri.X, branch string) ([]string, error) {
@@ -209,7 +209,7 @@
 	if err != nil {
 		return err
 	}
-	metadataDir := filepath.Join(topLevel, project.MetadataDirName())
+	metadataDir := filepath.Join(topLevel, jiri.ProjectMetaDir)
 	if err := jirix.Run().RemoveAll(filepath.Join(metadataDir, branch)); err != nil {
 		return err
 	}
@@ -887,7 +887,7 @@
 	if err != nil {
 		return err
 	}
-	newMetadataDir := filepath.Join(topLevel, project.MetadataDirName(), review.CLOpts.Branch)
+	newMetadataDir := filepath.Join(topLevel, jiri.ProjectMetaDir, review.CLOpts.Branch)
 	if err := review.jirix.Run().MkdirAll(newMetadataDir, os.FileMode(0755)); err != nil {
 		return err
 	}
@@ -909,7 +909,7 @@
 new branch in the %v metadata directory. The information recorded in
 the %v metadata directory tracks dependencies between CLs and is used
 by the "jiri cl sync" and "jiri cl mail" commands.
-`, project.MetadataDirName(), project.MetadataDirName()),
+`, jiri.ProjectMetaDir, jiri.ProjectMetaDir),
 	ArgsName: "<name>",
 	ArgsLong: "<name> is the changelist name.",
 }
@@ -954,7 +954,7 @@
 		return err
 	}
 	branches = append(branches, originalBranch)
-	newMetadataDir := filepath.Join(topLevel, project.MetadataDirName(), newBranch)
+	newMetadataDir := filepath.Join(topLevel, jiri.ProjectMetaDir, newBranch)
 	if err := jirix.Run().MkdirAll(newMetadataDir, os.FileMode(0755)); err != nil {
 		return err
 	}
@@ -989,7 +989,7 @@
 changes in an ancestor into its dependent. When that occurs, the
 command is aborted and prints instructions that need to be followed
 before the command can be retried.
-`, project.MetadataDirName()),
+`, jiri.ProjectMetaDir),
 }
 
 func runCLSync(jirix *jiri.X, _ []string) error {
diff --git a/cl_test.go b/cl_test.go
index 586aef3..27de7c5 100644
--- a/cl_test.go
+++ b/cl_test.go
@@ -16,7 +16,7 @@
 	"v.io/jiri/gerrit"
 	"v.io/jiri/gitutil"
 	"v.io/jiri/jiri"
-	"v.io/jiri/project"
+	"v.io/jiri/jiritest"
 )
 
 // assertCommitCount asserts that the commit count between two
@@ -144,7 +144,7 @@
 	if err := jirix.Git().Init(repoPath); err != nil {
 		t.Fatalf("%v", err)
 	}
-	if err := jirix.Run().MkdirAll(filepath.Join(repoPath, project.MetadataDirName()), os.FileMode(0755)); err != nil {
+	if err := jirix.Run().MkdirAll(filepath.Join(repoPath, jiri.ProjectMetaDir), os.FileMode(0755)); err != nil {
 		t.Fatalf("%v", err)
 	}
 	return repoPath
@@ -221,12 +221,12 @@
 }
 
 // setupTest creates a setup for testing the review tool.
-func setupTest(t *testing.T, installHook bool) (cwd string, root *project.FakeJiriRoot, repoPath, originPath, gerritPath string) {
+func setupTest(t *testing.T, installHook bool) (cwd string, root *jiritest.FakeJiriRoot, repoPath, originPath, gerritPath string) {
 	var err error
 	if cwd, err = os.Getwd(); err != nil {
 		t.Fatalf("Getwd() failed: %v", err)
 	}
-	if root, err = project.NewFakeJiriRoot(); err != nil {
+	if root, err = jiritest.NewFakeJiriRoot(); err != nil {
 		t.Fatalf("%v", err)
 	}
 	repoPath, originPath, gerritPath = createTestRepos(t, root.X, root.Dir)
@@ -240,7 +240,7 @@
 }
 
 // teardownTest cleans up the setup for testing the review tool.
-func teardownTest(t *testing.T, oldWorkDir string, root *project.FakeJiriRoot) {
+func teardownTest(t *testing.T, oldWorkDir string, root *jiritest.FakeJiriRoot) {
 	chdir(t, root.X, oldWorkDir)
 	if err := root.Cleanup(); err != nil {
 		t.Fatalf("%v", err)
diff --git a/jiri/jiritest/x.go b/jiri/jiritest/x.go
deleted file mode 100644
index 3f680b6..0000000
--- a/jiri/jiritest/x.go
+++ /dev/null
@@ -1,32 +0,0 @@
-// 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 jiritest provides utilities for testing jiri functionality.
-package jiritest
-
-import (
-	"os"
-	"testing"
-
-	"v.io/jiri/jiri"
-	"v.io/jiri/tool"
-)
-
-// NewX is similar to jiri.NewX, but is meant for usage in a testing environment.
-func NewX(t *testing.T) (*jiri.X, func()) {
-	ctx := tool.NewDefaultContext()
-	root, err := ctx.NewSeq().TempDir("", "")
-	if err != nil {
-		t.Fatalf("TempDir() failed: %v", err)
-	}
-	oldEnv := os.Getenv("JIRI_ROOT")
-	if err := os.Setenv("JIRI_ROOT", root); err != nil {
-		t.Fatalf("Setenv(JIRI_ROOT) failed: %v", err)
-	}
-	cleanup := func() {
-		os.Setenv("JIRI_ROOT", oldEnv)
-		ctx.NewSeq().RemoveAll(root).Done()
-	}
-	return &jiri.X{Context: ctx, Root: root}, cleanup
-}
diff --git a/jiri/x.go b/jiri/x.go
index 5b5e0c9..22e463c 100644
--- a/jiri/x.go
+++ b/jiri/x.go
@@ -15,18 +15,19 @@
 
 	"v.io/jiri/tool"
 	"v.io/x/lib/cmdline"
+	"v.io/x/lib/timing"
 )
 
 const (
-	rootEnv = "JIRI_ROOT"
+	RootEnv         = "JIRI_ROOT"
+	RootMetaDir     = ".jiri_root"
+	ProjectMetaDir  = ".jiri"
+	ProjectMetaFile = "metadata.v2"
 )
 
 // X holds the execution environment for the jiri tool and related tools.  This
 // includes the jiri filesystem root directory.
 //
-// TODO(toddw): The Root is not currently used; we still use project.JiriRoot
-// everywhere.  Transition those uses gradually over to use this instead.
-//
 // TODO(toddw): Other jiri state should be transitioned to this struct,
 // including the manifest and related operations.
 type X struct {
@@ -38,7 +39,7 @@
 // NewX returns a new execution environment, given a cmdline env.
 func NewX(env *cmdline.Env) (*X, error) {
 	ctx := tool.NewContextFromEnv(env)
-	root, err := findJiriRoot(ctx)
+	root, err := findJiriRoot(ctx.Timer())
 	if err != nil {
 		return nil, err
 	}
@@ -49,22 +50,42 @@
 	}, nil
 }
 
-func findJiriRoot(ctx *tool.Context) (string, error) {
-	ctx.TimerPush("find JIRI_ROOT")
-	defer ctx.TimerPop()
-	if root := os.Getenv(rootEnv); root != "" {
+func findJiriRoot(timer *timing.Timer) (string, error) {
+	if timer != nil {
+		timer.Push("find JIRI_ROOT")
+		defer timer.Pop()
+	}
+	if root := os.Getenv(RootEnv); root != "" {
 		// Always use JIRI_ROOT if it's set.
 		result, err := filepath.EvalSymlinks(root)
 		if err != nil {
-			return "", fmt.Errorf("%v EvalSymlinks(%v) failed: %v", rootEnv, root, err)
+			return "", fmt.Errorf("%v EvalSymlinks(%v) failed: %v", RootEnv, root, err)
 		}
 		if !filepath.IsAbs(result) {
-			return "", fmt.Errorf("%v isn't an absolute path: %v", rootEnv, result)
+			return "", fmt.Errorf("%v isn't an absolute path: %v", RootEnv, result)
 		}
 		return filepath.Clean(result), nil
 	}
 	// TODO(toddw): Try to find the root by walking up the filesystem.
-	return "", fmt.Errorf("%v is not set", rootEnv)
+	return "", fmt.Errorf("%v is not set", RootEnv)
+}
+
+// FindRoot returns the root directory of the jiri environment.  All state
+// managed by jiri resides under this root.
+//
+// If the RootEnv environment variable is non-empty, we always attempt to use
+// it.  It must point to an absolute path, after symlinks are evaluated.
+// TODO(toddw): Walk up the filesystem too.
+//
+// Returns an empty string if the root directory cannot be determined, or if any
+// errors are encountered.
+//
+// FindRoot should be rarely used; typically you should use NewX to create a new
+// execution environment, and handle errors.  An example of a valid usage is to
+// initialize default flag values in an init func before main.
+func FindRoot() string {
+	root, _ := findJiriRoot(nil)
+	return root
 }
 
 // Clone returns a clone of the environment.
@@ -86,6 +107,51 @@
 	return nil
 }
 
+// LocalManifestFile returns the path to the local manifest file.
+func (x *X) LocalManifestFile() string {
+	return filepath.Join(x.Root, ".local_manifest")
+}
+
+// LocalSnapshotDir returns the path to the local snapshot directory.
+func (x *X) LocalSnapshotDir() string {
+	return filepath.Join(x.Root, ".snapshot")
+}
+
+// RemoteSnapshotDir returns the path to the remote snapshot directory.
+func (x *X) RemoteSnapshotDir() string {
+	return filepath.Join(x.ManifestDir(), "snapshot")
+}
+
+// ManifestDir returns the path to the manifest directory.
+func (x *X) ManifestDir() string {
+	return filepath.Join(x.Root, ".manifest", "v2")
+}
+
+// ManifestFile returns the path to the manifest file with the given name.
+func (x *X) ManifestFile(name string) string {
+	return filepath.Join(x.ManifestDir(), name)
+}
+
+// ResolveManifestPath resolves the given manifest name to an absolute path in
+// the local filesystem.
+func (x *X) ResolveManifestPath(name string) (string, error) {
+	if name != "" {
+		if filepath.IsAbs(name) {
+			return name, nil
+		}
+		return x.ManifestFile(name), nil
+	}
+	path := x.LocalManifestFile()
+	switch _, err := os.Stat(path); {
+	case err == nil:
+		return path, nil
+	case os.IsNotExist(err):
+		return x.ManifestFile("default"), nil
+	default:
+		return "", fmt.Errorf("Stat(%v) failed: %v", path, err)
+	}
+}
+
 // RunnerFunc is an adapter that turns regular functions into cmdline.Runner.
 // This is similar to cmdline.RunnerFunc, but the first function argument is
 // jiri.X, rather than cmdline.Env.
diff --git a/jiri/x_test.go b/jiri/x_test.go
new file mode 100644
index 0000000..340a4ba
--- /dev/null
+++ b/jiri/x_test.go
@@ -0,0 +1,52 @@
+// 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 jiri
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"v.io/jiri/tool"
+)
+
+// TestFindRootEnvSymlink checks that FindRoot interprets the value of the
+// JIRI_ROOT environment variable as a path, evaluates any symlinks the path
+// might contain, and returns the result.
+func TestFindRootEnvSymlink(t *testing.T) {
+	ctx := tool.NewDefaultContext()
+
+	// Create a temporary directory.
+	tmpDir, err := ctx.NewSeq().TempDir("", "")
+	if err != nil {
+		t.Fatalf("TempDir() failed: %v", err)
+	}
+	defer func() { ctx.NewSeq().RemoveAll(tmpDir).Done() }()
+
+	// Make sure tmpDir is not a symlink itself.
+	tmpDir, err = filepath.EvalSymlinks(tmpDir)
+	if err != nil {
+		t.Fatalf("EvalSymlinks(%v) failed: %v", tmpDir, err)
+	}
+
+	// Create a directory and a symlink to it.
+	root, perm := filepath.Join(tmpDir, "root"), os.FileMode(0700)
+	symRoot := filepath.Join(tmpDir, "sym_root")
+	seq := ctx.NewSeq().MkdirAll(root, perm).Symlink(root, symRoot)
+	if err := seq.Done(); err != nil {
+		t.Fatalf("%v", err)
+	}
+
+	// Set the JIRI_ROOT to the symlink created above and check that FindRoot()
+	// evaluates the symlink.
+	oldRoot := os.Getenv("JIRI_ROOT")
+	if err := os.Setenv("JIRI_ROOT", symRoot); err != nil {
+		t.Fatalf("%v", err)
+	}
+	defer os.Setenv("JIRI_ROOT", oldRoot)
+	if got, want := FindRoot(), root; got != want {
+		t.Fatalf("unexpected output: got %v, want %v", got, want)
+	}
+}
diff --git a/project/fake.go b/jiritest/fake.go
similarity index 81%
rename from project/fake.go
rename to jiritest/fake.go
index fcbf5c7..5e971fa 100644
--- a/project/fake.go
+++ b/jiritest/fake.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package project
+package jiritest
 
 import (
 	"encoding/xml"
@@ -12,14 +12,19 @@
 
 	"v.io/jiri/collect"
 	"v.io/jiri/jiri"
+	"v.io/jiri/project"
 	"v.io/jiri/tool"
 )
 
+// FakeJiriRoot sets up a fake JIRI_ROOT under a tmp directory.
+//
+// TODO(toddw): Simplify usage, and consolidate with jiritest.NewX.
 type FakeJiriRoot struct {
 	X        *jiri.X
 	Dir      string
 	Projects map[string]string
 	remote   string
+	oldRoot  string
 }
 
 const (
@@ -34,15 +39,20 @@
 func NewFakeJiriRoot() (*FakeJiriRoot, error) {
 	// Create a fake JIRI_ROOT.
 	ctx := tool.NewDefaultContext()
-	rootDir, err := ctx.Run().TempDir("", "")
+	rootDir, err := ctx.NewSeq().TempDir("", "")
 	if err != nil {
 		return nil, err
 	}
+	oldRoot := os.Getenv(jiri.RootEnv)
+	if err := os.Setenv(jiri.RootEnv, rootDir); err != nil {
+		return nil, err
+	}
 	jirix := &jiri.X{Context: ctx, Root: rootDir}
 	root := &FakeJiriRoot{
 		X:        jirix,
 		Dir:      rootDir,
 		Projects: map[string]string{},
+		oldRoot:  oldRoot,
 	}
 
 	// Create fake remote manifest and tools projects.
@@ -69,7 +79,7 @@
 	if err := jirix.Run().MkdirAll(manifestDir, os.FileMode(0700)); err != nil {
 		return nil, err
 	}
-	if err := root.WriteRemoteManifest(&Manifest{}); err != nil {
+	if err := root.WriteRemoteManifest(&project.Manifest{}); err != nil {
 		return nil, err
 	}
 	if err := jirix.Git().Clone(root.Projects[manifestProject], filepath.Join(root.Dir, manifestProject)); err != nil {
@@ -80,14 +90,14 @@
 	// manifests. This is necessary to make sure that the commonly
 	// invoked DataDirPath() function, which uses the "jiri" tool
 	// configuration for its default, works.
-	if err := root.AddProject(Project{
+	if err := root.AddProject(project.Project{
 		Name:   toolsProject,
 		Path:   toolsProject,
 		Remote: root.Projects[toolsProject],
 	}); err != nil {
 		return nil, err
 	}
-	if err := root.AddTool(Tool{
+	if err := root.AddTool(project.Tool{
 		Name:    "jiri",
 		Data:    defaultDataDir,
 		Project: toolsProject,
@@ -97,13 +107,13 @@
 
 	// Add "gerrit" and "git" hosts to the manifest, as required by the "jiri"
 	// tool.
-	if err := root.AddHost(Host{
+	if err := root.AddHost(project.Host{
 		Name:     "gerrit",
 		Location: "git://example.com/gerrit",
 	}); err != nil {
 		return nil, err
 	}
-	if err := root.AddHost(Host{
+	if err := root.AddHost(project.Host{
 		Name:     "git",
 		Location: "git://example.com/git",
 	}); err != nil {
@@ -129,7 +139,15 @@
 		}
 		return root.X.Run().RemoveAll(root.Dir)
 	}, &errs)
-	collect.Errors(func() error { return root.X.Run().RemoveAll(root.remote) }, &errs)
+	collect.Errors(func() error {
+		return root.X.Run().RemoveAll(root.remote)
+	}, &errs)
+	collect.Errors(func() error {
+		if root.oldRoot == "" {
+			return nil
+		}
+		return os.Setenv(jiri.RootEnv, root.oldRoot)
+	}, &errs)
 	if len(errs) != 0 {
 		return fmt.Errorf("Cleanup() failed: %v", errs)
 	}
@@ -137,7 +155,7 @@
 }
 
 // AddHost adds the given host to a remote manifest.
-func (root FakeJiriRoot) AddHost(host Host) error {
+func (root FakeJiriRoot) AddHost(host project.Host) error {
 	manifest, err := root.ReadRemoteManifest()
 	if err != nil {
 		return err
@@ -150,7 +168,7 @@
 }
 
 // AddProject adds the given project to a remote manifest.
-func (root FakeJiriRoot) AddProject(project Project) error {
+func (root FakeJiriRoot) AddProject(project project.Project) error {
 	manifest, err := root.ReadRemoteManifest()
 	if err != nil {
 		return err
@@ -163,7 +181,7 @@
 }
 
 // AddTool adds the given tool to a remote manifest.
-func (root FakeJiriRoot) AddTool(tool Tool) error {
+func (root FakeJiriRoot) AddTool(tool project.Tool) error {
 	manifest, err := root.ReadRemoteManifest()
 	if err != nil {
 		return err
@@ -225,23 +243,23 @@
 }
 
 // ReadLocalManifest read a manifest from the local manifest project.
-func (root FakeJiriRoot) ReadLocalManifest() (*Manifest, error) {
+func (root FakeJiriRoot) ReadLocalManifest() (*project.Manifest, error) {
 	path := filepath.Join(root.Dir, manifestProject, manifestVersion, getManifest(root.X))
 	return root.readManifest(path)
 }
 
 // ReadRemoteManifest read a manifest from the remote manifest project.
-func (root FakeJiriRoot) ReadRemoteManifest() (*Manifest, error) {
+func (root FakeJiriRoot) ReadRemoteManifest() (*project.Manifest, error) {
 	path := filepath.Join(root.remote, manifestProject, manifestVersion, getManifest(root.X))
 	return root.readManifest(path)
 }
 
-func (root FakeJiriRoot) readManifest(path string) (*Manifest, error) {
+func (root FakeJiriRoot) readManifest(path string) (*project.Manifest, error) {
 	bytes, err := root.X.Run().ReadFile(path)
 	if err != nil {
 		return nil, err
 	}
-	var manifest Manifest
+	var manifest project.Manifest
 	if err := xml.Unmarshal(bytes, &manifest); err != nil {
 		return nil, fmt.Errorf("Unmarshal(%v) failed: %v", string(bytes), err)
 	}
@@ -256,7 +274,7 @@
 		return fmt.Errorf("Setenv() failed: %v", err)
 	}
 	defer os.Setenv("JIRI_ROOT", oldRoot)
-	if err := UpdateUniverse(root.X, gc); err != nil {
+	if err := project.UpdateUniverse(root.X, gc); err != nil {
 		return err
 	}
 	return nil
@@ -264,7 +282,7 @@
 
 // WriteLocalManifest writes the given manifest to the local
 // manifest project.
-func (root FakeJiriRoot) WriteLocalManifest(manifest *Manifest) error {
+func (root FakeJiriRoot) WriteLocalManifest(manifest *project.Manifest) error {
 	dir := filepath.Join(root.Dir, manifestProject)
 	path := filepath.Join(dir, manifestVersion, getManifest(root.X))
 	return root.writeManifest(manifest, dir, path)
@@ -272,13 +290,13 @@
 
 // WriteRemoteManifest writes the given manifest to the remote
 // manifest project.
-func (root FakeJiriRoot) WriteRemoteManifest(manifest *Manifest) error {
+func (root FakeJiriRoot) WriteRemoteManifest(manifest *project.Manifest) error {
 	dir := filepath.Join(root.remote, manifestProject)
 	path := filepath.Join(dir, manifestVersion, getManifest(root.X))
 	return root.writeManifest(manifest, dir, path)
 }
 
-func (root FakeJiriRoot) writeManifest(manifest *Manifest, dir, path string) error {
+func (root FakeJiriRoot) writeManifest(manifest *project.Manifest, dir, path string) error {
 	bytes, err := xml.Marshal(manifest)
 	if err != nil {
 		return fmt.Errorf("Marshal(%v) failed: %v", manifest, err)
diff --git a/jiritest/x.go b/jiritest/x.go
new file mode 100644
index 0000000..3cee229
--- /dev/null
+++ b/jiritest/x.go
@@ -0,0 +1,49 @@
+// 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 jiritest provides utilities for testing jiri functionality.
+package jiritest
+
+import (
+	"os"
+	"testing"
+
+	"v.io/jiri/jiri"
+	"v.io/jiri/tool"
+)
+
+// NewX is similar to jiri.NewX, but is meant for usage in a testing environment.
+func NewX(t *testing.T) (*jiri.X, func()) {
+	ctx := tool.NewDefaultContext()
+	root, err := ctx.NewSeq().TempDir("", "")
+	if err != nil {
+		t.Fatalf("TempDir() failed: %v", err)
+	}
+	oldEnv := os.Getenv(jiri.RootEnv)
+	if err := os.Setenv(jiri.RootEnv, root); err != nil {
+		t.Fatalf("Setenv(%v) failed: %v", jiri.RootEnv, err)
+	}
+	cleanup := func() {
+		os.Setenv(jiri.RootEnv, oldEnv)
+		ctx.NewSeq().RemoveAll(root).Done()
+	}
+	return &jiri.X{Context: ctx, Root: root}, cleanup
+}
+
+// NewX_DeprecatedEnv relies on the deprecated JIRI_ROOT environment variable to
+// set up a new jiri.X.  Tests relying on this function need to be updated to
+// not rely on the environment variable.
+func NewX_DeprecatedEnv(t *testing.T, opts *tool.ContextOpts) *jiri.X {
+	root := os.Getenv(jiri.RootEnv)
+	if root == "" {
+		t.Fatalf("%v isn't set", jiri.RootEnv)
+	}
+	var ctx *tool.Context
+	if opts != nil {
+		ctx = tool.NewContext(*opts)
+	} else {
+		ctx = tool.NewDefaultContext()
+	}
+	return &jiri.X{Context: ctx, Root: root}
+}
diff --git a/profiles/commandline/driver.go b/profiles/commandline/driver.go
index 9681c7b..1e9d3f4 100644
--- a/profiles/commandline/driver.go
+++ b/profiles/commandline/driver.go
@@ -18,7 +18,6 @@
 
 	"v.io/jiri/jiri"
 	"v.io/jiri/profiles"
-	"v.io/jiri/project"
 	"v.io/jiri/tool"
 	"v.io/x/lib/cmdline"
 	"v.io/x/lib/textutil"
@@ -118,7 +117,6 @@
 	manifestFlag         string
 	showManifestFlag     bool
 	profilesFlag         string
-	rootDir              string
 	availableFlag        bool
 	verboseFlag          bool
 	allFlag              bool
@@ -138,14 +136,8 @@
 func Init(defaultManifestFilename string) {
 	targetFlag = profiles.DefaultTarget()
 	mergePoliciesFlag = profiles.JiriMergePolicies()
-
-	var err error
-	rootDir, err = project.JiriRoot()
-	if err != nil {
-		panic(err)
-	}
-
-	rootPath = profiles.NewRelativePath("JIRI_ROOT", rootDir).Join("profiles")
+	// TODO(toddw): Change logic to derive rootPath from jirix.Root.
+	rootPath = profiles.NewRelativePath("JIRI_ROOT", jiri.FindRoot()).Join("profiles")
 
 	// Every sub-command accepts: --manifest
 	for _, fs := range []*flag.FlagSet{
diff --git a/profiles/env.go b/profiles/env.go
index 4f34cf8..3f12f5d 100644
--- a/profiles/env.go
+++ b/profiles/env.go
@@ -99,7 +99,6 @@
 type ConfigHelper struct {
 	*envvar.Vars
 	profilesMode bool
-	root         string
 	jirix        *jiri.X
 	config       *util.Config
 	projects     project.Projects
@@ -111,10 +110,6 @@
 // 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) {
-	root, err := project.JiriRoot()
-	if err != nil {
-		return nil, err
-	}
 	config, err := util.LoadConfig(jirix)
 	if err != nil {
 		return nil, err
@@ -130,7 +125,6 @@
 	}
 	ch := &ConfigHelper{
 		jirix:        jirix,
-		root:         root,
 		config:       config,
 		projects:     projects,
 		tools:        tools,
@@ -149,7 +143,7 @@
 
 // Root returns the root of the jiri universe.
 func (ch *ConfigHelper) Root() string {
-	return ch.root
+	return ch.jirix.Root
 }
 
 // MergeEnv merges the embedded environment with the environment
@@ -178,7 +172,7 @@
 		envs = append(envs, e)
 	}
 	MergeEnv(policies, ch.Vars, envs...)
-	rp := NewRelativePath("JIRI_ROOT", ch.root)
+	rp := NewRelativePath("JIRI_ROOT", ch.jirix.Root)
 	rp.ExpandEnv(ch.Vars)
 }
 
@@ -223,22 +217,22 @@
 // GoPath computes and returns the GOPATH environment variable based on the
 // current jiri configuration.
 func (ch *ConfigHelper) GoPath() string {
-	path := pathHelper(ch.jirix, ch.root, ch.projects, ch.config.GoWorkspaces(), "")
+	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.root, ch.projects, ch.config.VDLWorkspaces(), "src")
+	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, root string, projects project.Projects, workspaces []string, suffix string) []string {
+func pathHelper(jirix *jiri.X, projects project.Projects, workspaces []string, suffix string) []string {
 	path := []string{}
 	for _, workspace := range workspaces {
-		absWorkspace := filepath.Join(root, workspace, suffix)
+		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 {
diff --git a/profiles/env_test.go b/profiles/env_test.go
index e0c3423..15f1e94 100644
--- a/profiles/env_test.go
+++ b/profiles/env_test.go
@@ -13,21 +13,16 @@
 	"sort"
 	"testing"
 
-	"v.io/jiri/jiri"
+	"v.io/jiri/jiritest"
 	"v.io/jiri/profiles"
 	"v.io/jiri/project"
-	"v.io/jiri/tool"
 	"v.io/jiri/util"
 	"v.io/x/lib/envvar"
 )
 
 func TestConfigHelper(t *testing.T) {
-	root, err := project.JiriRoot()
-	if err != nil {
-		t.Fatal(err)
-	}
-	jirix := &jiri.X{Context: tool.NewDefaultContext(), Root: root}
-	ch, err := profiles.NewConfigHelper(jirix, profiles.UseProfiles, filepath.Join(root, "release/go/src/v.io/jiri/profiles/testdata/m2.xml"))
+	jirix := jiritest.NewX_DeprecatedEnv(t, nil)
+	ch, err := profiles.NewConfigHelper(jirix, profiles.UseProfiles, filepath.Join(jirix.Root, "release/go/src/v.io/jiri/profiles/testdata/m2.xml"))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -45,11 +40,7 @@
 
 func TestEnvFromTarget(t *testing.T) {
 	profiles.Clear()
-	root, err := project.JiriRoot()
-	if err != nil {
-		t.Fatal(err)
-	}
-	jirix := &jiri.X{Context: tool.NewDefaultContext(), Root: root}
+	jirix := jiritest.NewX_DeprecatedEnv(t, nil)
 	profiles.InstallProfile("a", "root")
 	profiles.InstallProfile("b", "root")
 	t1, t2 := &profiles.Target{}, &profiles.Target{}
@@ -63,7 +54,7 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	filename := filepath.Join(root, "release", "go", "src", "v.io", "jiri", "profiles", tmpdir, "manifest")
+	filename := filepath.Join(jirix.Root, "release", "go", "src", "v.io", "jiri", "profiles", tmpdir, "manifest")
 	if err := profiles.Write(jirix, filename); err != nil {
 		t.Fatal(err)
 	}
@@ -150,7 +141,7 @@
 func testSetPathHelper(t *testing.T, name string) {
 	profiles.Clear()
 	// Setup a fake JIRI_ROOT.
-	root, err := project.NewFakeJiriRoot()
+	root, err := jiritest.NewFakeJiriRoot()
 	if err != nil {
 		t.Fatalf("%v", err)
 	}
@@ -182,12 +173,6 @@
 		config = util.NewConfig(util.VDLWorkspacesOpt([]string{"test", "does/not/exist"}))
 	}
 
-	oldRoot, err := project.JiriRoot()
-	if err := os.Setenv("JIRI_ROOT", root.Dir); err != nil {
-		t.Fatalf("%v", err)
-	}
-	defer os.Setenv("JIRI_ROOT", oldRoot)
-
 	if err := profiles.Write(root.X, filepath.Join(root.Dir, "profiles-manifest")); err != nil {
 		t.Fatal(err)
 	}
@@ -196,13 +181,7 @@
 		t.Fatalf("%v", err)
 	}
 
-	// Retrieve Jiri_ROOT through JiriRoot() to account for symlinks.
-	jiriRoot, err := project.JiriRoot()
-	if err != nil {
-		t.Fatalf("%v", err)
-	}
-
-	ch, err := profiles.NewConfigHelper(root.X, profiles.UseProfiles, filepath.Join(jiriRoot, "profiles-manifest"))
+	ch, err := profiles.NewConfigHelper(root.X, profiles.UseProfiles, filepath.Join(root.Dir, "profiles-manifest"))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -210,11 +189,11 @@
 	var got, want string
 	switch name {
 	case "GOPATH":
-		want = "GOPATH=" + filepath.Join(jiriRoot, "test")
+		want = "GOPATH=" + filepath.Join(root.Dir, "test")
 		got = ch.GoPath()
 	case "VDLPATH":
 		// Make a fake src directory.
-		want = filepath.Join(jiriRoot, "test", "src")
+		want = filepath.Join(root.Dir, "test", "src")
 		if err := root.X.Run().MkdirAll(want, 0755); err != nil {
 			t.Fatalf("%v", err)
 		}
diff --git a/profiles/manifest_test.go b/profiles/manifest_test.go
index 99a2a86..f6a7c61 100644
--- a/profiles/manifest_test.go
+++ b/profiles/manifest_test.go
@@ -16,8 +16,8 @@
 	"testing"
 
 	"v.io/jiri/jiri"
+	"v.io/jiri/jiritest"
 	"v.io/jiri/profiles"
-	"v.io/jiri/project"
 	"v.io/jiri/tool"
 )
 
@@ -192,17 +192,13 @@
 }
 
 func TestReadingV3AndV4(t *testing.T) {
-	root, err := project.JiriRoot()
-	if err != nil {
-		t.Fatal(err)
-	}
-	jirix := &jiri.X{Context: tool.NewDefaultContext(), Root: root}
+	jirix := jiritest.NewX_DeprecatedEnv(t, nil)
 	for i, c := range []struct {
 		filename, prefix, variable string
 		version                    profiles.Version
 	}{
 		{"v3.xml", "", "", profiles.V3},
-		{"v4.xml", root, "${JIRI_ROOT}", profiles.V4},
+		{"v4.xml", jirix.Root, "${JIRI_ROOT}", profiles.V4},
 	} {
 		ch, err := profiles.NewConfigHelper(jirix, profiles.UseProfiles, filepath.Join("testdata", c.filename))
 		if err != nil {
diff --git a/profiles/util.go b/profiles/util.go
index 170f897..92b7e26 100644
--- a/profiles/util.go
+++ b/profiles/util.go
@@ -17,7 +17,6 @@
 	"strings"
 
 	"v.io/jiri/jiri"
-	"v.io/jiri/project"
 	"v.io/jiri/runutil"
 	"v.io/jiri/tool"
 )
@@ -48,7 +47,7 @@
 // RegisterManifestFlag registers the commonly used --profiles-manifest
 // flag with the supplied FlagSet.
 func RegisterManifestFlag(flags *flag.FlagSet, manifest *string, defaultManifest string) {
-	root, _ := project.JiriRoot()
+	root := jiri.FindRoot()
 	flags.StringVar(manifest, "profiles-manifest", filepath.Join(root, defaultManifest), "specify the profiles XML manifest filename.")
 	flags.Lookup("profiles-manifest").DefValue = filepath.Join("$JIRI_ROOT", defaultManifest)
 }
diff --git a/project/internal_test.go b/project/internal_test.go
new file mode 100644
index 0000000..6ff656e
--- /dev/null
+++ b/project/internal_test.go
@@ -0,0 +1,8 @@
+// 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 project
+
+// InternalWriteMetadata exports writeMetadata for tests.
+var InternalWriteMetadata = writeMetadata
diff --git a/project/paths.go b/project/paths.go
index 6586fe3..51ed88b 100644
--- a/project/paths.go
+++ b/project/paths.go
@@ -6,19 +6,11 @@
 
 import (
 	"fmt"
-	"os"
 	"path/filepath"
 
 	"v.io/jiri/jiri"
 )
 
-const (
-	rootEnv              = "JIRI_ROOT"
-	metadataDirName      = ".jiri"
-	metadataFileName     = "metadata.v2"
-	metadataProfilesFile = ".jiri_profiles"
-)
-
 // DataDirPath returns the path to the data directory of the given tool.
 // TODO(nlacasse): DataDirPath is currently broken because we don't set the
 // tool.Name variable when building each tool.  Luckily, only the jiri tool has
@@ -47,80 +39,6 @@
 	return filepath.Join(project.Path, tool.Data), nil
 }
 
-// LocalManifestFile returns the path to the local manifest.
-func LocalManifestFile() (string, error) {
-	root, err := JiriRoot()
-	if err != nil {
-		return "", err
-	}
-	return filepath.Join(root, ".local_manifest"), nil
-}
-
-// LocalSnapshotDir returns the path to the local snapshot directory.
-func LocalSnapshotDir() (string, error) {
-	root, err := JiriRoot()
-	if err != nil {
-		return "", err
-	}
-	return filepath.Join(root, ".snapshot"), nil
-}
-
-// ManifestDir returns the path to the manifest directory.
-func ManifestDir() (string, error) {
-	root, err := JiriRoot()
-	if err != nil {
-		return "", err
-	}
-	return filepath.Join(root, ".manifest", "v2"), nil
-}
-
-// ManifestFile returns the path to the manifest file with the given
-// relative path.
-func ManifestFile(name string) (string, error) {
-	dir, err := ManifestDir()
-	if err != nil {
-		return "", err
-	}
-	return filepath.Join(dir, name), nil
-}
-
-// MetadataDir returns the name of the directory in which jiri stores
-// project specific metadata.
-func MetadataDirName() string {
-	return metadataDirName
-}
-
-// RemoteSnapshotDir returns the path to the remote snapshot directory.
-func RemoteSnapshotDir() (string, error) {
-	manifestDir, err := ManifestDir()
-	if err != nil {
-		return "", err
-	}
-	return filepath.Join(manifestDir, "snapshot"), nil
-}
-
-// ResolveManifestPath resolves the given manifest name to an absolute
-// path in the local filesystem.
-func ResolveManifestPath(name string) (string, error) {
-	if name != "" {
-		if filepath.IsAbs(name) {
-			return name, nil
-		}
-		return ManifestFile(name)
-	}
-	path, err := LocalManifestFile()
-	if err != nil {
-		return "", err
-	}
-	if _, err := os.Stat(path); err != nil {
-		if os.IsNotExist(err) {
-			return ResolveManifestPath("default")
-		}
-		return "", fmt.Errorf("Stat(%v) failed: %v", path, err)
-	}
-	return path, nil
-}
-
 func getHost(jirix *jiri.X, name string) (string, error) {
 	hosts, _, _, _, err := readManifest(jirix, false)
 	if err != nil {
@@ -143,44 +61,20 @@
 	return getHost(jirix, "git")
 }
 
-// JiriRoot returns the root of the jiri universe.
-func JiriRoot() (string, error) {
-	root := os.Getenv(rootEnv)
-	if root == "" {
-		return "", fmt.Errorf("%v is not set", rootEnv)
-	}
-	result, err := filepath.EvalSymlinks(root)
-	if err != nil {
-		return "", fmt.Errorf("EvalSymlinks(%v) failed: %v", root, err)
-	}
-	if !filepath.IsAbs(result) {
-		return "", fmt.Errorf("JIRI_ROOT must be absolute path: %v", rootEnv)
-	}
-	return filepath.Clean(result), nil
-}
-
-// ToAbs returns the given path rooted in JIRI_ROOT, if it is not already an
+// toAbs returns the given path rooted in JIRI_ROOT, if it is not already an
 // absolute path.
-func ToAbs(path string) (string, error) {
+func toAbs(jirix *jiri.X, path string) string {
 	if filepath.IsAbs(path) {
-		return path, nil
+		return path
 	}
-	root, err := JiriRoot()
-	if err != nil {
-		return "", err
-	}
-	return filepath.Join(root, path), nil
+	return filepath.Join(jirix.Root, path)
 }
 
-// ToRel returns the given path relative to JIRI_ROOT, if it is not already a
+// toRel returns the given path relative to JIRI_ROOT, if it is not already a
 // relative path.
-func ToRel(path string) (string, error) {
+func toRel(jirix *jiri.X, path string) (string, error) {
 	if !filepath.IsAbs(path) {
 		return path, nil
 	}
-	root, err := JiriRoot()
-	if err != nil {
-		return "", err
-	}
-	return filepath.Rel(root, path)
+	return filepath.Rel(jirix.Root, path)
 }
diff --git a/project/project.go b/project/project.go
index 5fb7b6e..fc40d27 100644
--- a/project/project.go
+++ b/project/project.go
@@ -196,7 +196,7 @@
 		return err
 	}
 	for _, project := range localProjects {
-		relPath, err := ToRel(project.Path)
+		relPath, err := toRel(jirix, project.Path)
 		if err != nil {
 			return err
 		}
@@ -240,10 +240,7 @@
 // CurrentManifest returns a manifest that identifies the result of
 // the most recent "jiri update" invocation.
 func CurrentManifest(jirix *jiri.X) (*Manifest, error) {
-	currentManifestPath, err := ToAbs(currentManifestFileName)
-	if err != nil {
-		return nil, err
-	}
+	currentManifestPath := toAbs(jirix, currentManifestFileName)
 	bytes, err := jirix.Run().ReadFile(currentManifestPath)
 	if err != nil {
 		if os.IsNotExist(err) {
@@ -265,10 +262,7 @@
 // writeCurrentManifest writes the given manifest to a file that
 // stores the result of the most recent "jiri update" invocation.
 func writeCurrentManifest(jirix *jiri.X, manifest *Manifest) error {
-	currentManifestPath, err := ToAbs(currentManifestFileName)
-	if err != nil {
-		return err
-	}
+	currentManifestPath := toAbs(jirix, currentManifestFileName)
 	bytes, err := xml.MarshalIndent(manifest, "", "  ")
 	if err != nil {
 		return fmt.Errorf("MarshalIndent(%v) failed: %v", manifest, err)
@@ -287,9 +281,9 @@
 	if err != nil {
 		return "", nil
 	}
-	metadataDir := filepath.Join(topLevel, metadataDirName)
+	metadataDir := filepath.Join(topLevel, jiri.ProjectMetaDir)
 	if _, err := jirix.Run().Stat(metadataDir); err == nil {
-		metadataFile := filepath.Join(metadataDir, metadataFileName)
+		metadataFile := filepath.Join(metadataDir, jiri.ProjectMetaFile)
 		bytes, err := jirix.Run().ReadFile(metadataFile)
 		if err != nil {
 			return "", err
@@ -340,7 +334,6 @@
 	jirix.TimerPush("local projects")
 	defer jirix.TimerPop()
 
-	projects := Projects{}
 	if scanMode == FastScan {
 		// Fast path:  Full scan was not requested, and all projects in
 		// manifest exist on local filesystem.  We just use the projects
@@ -359,17 +352,11 @@
 	}
 
 	// Slow path: Either full scan was not requested, or projects exist in
-	// manifest that were not found locally.   Do a scan of all projects in
-	// JIRI_ROOT.
-	root, err := JiriRoot()
-	if err != nil {
-		return nil, err
-	}
-
-	// Initial call to findLocalProjects -- it will recursively search all the
-	// directories under JiriRoot.
+	// manifest that were not found locally.  Do a recursive scan of all projects
+	// under JIRI_ROOT.
+	projects := Projects{}
 	jirix.TimerPush("scan fs")
-	err = findLocalProjects(jirix, root, projects)
+	err := findLocalProjects(jirix, jirix.Root, projects)
 	jirix.TimerPop()
 	if err != nil {
 		return nil, err
@@ -507,10 +494,7 @@
 	jirix.TimerPush("read manifest")
 	defer jirix.TimerPop()
 	if update {
-		manifestPath, err := ToAbs(".manifest")
-		if err != nil {
-			return nil, nil, nil, nil, err
-		}
+		manifestPath := toAbs(jirix, ".manifest")
 		manifestRemote, err := getManifestRemote(jirix, manifestPath)
 		if err != nil {
 			return nil, nil, nil, nil, err
@@ -526,7 +510,7 @@
 			return nil, nil, nil, nil, err
 		}
 	}
-	path, err := ResolveManifestPath(jirix.Manifest())
+	path, err := jirix.ResolveManifestPath(jirix.Manifest())
 	if err != nil {
 		return nil, nil, nil, nil, err
 	}
@@ -800,15 +784,11 @@
 
 // isLocalProject returns true if there is a project at the given path.
 func isLocalProject(jirix *jiri.X, path string) (bool, error) {
-	absPath, err := ToAbs(path)
-	if err != nil {
-		return false, err
-	}
+	absPath := toAbs(jirix, path)
 	// Existence of a metadata directory is how we know we've found a
 	// Jiri-maintained project.
-	metadataDir := filepath.Join(absPath, metadataDirName)
-	_, err = jirix.Run().Stat(metadataDir)
-	if err != nil {
+	metadataDir := filepath.Join(absPath, jiri.ProjectMetaDir)
+	if _, err := jirix.Run().Stat(metadataDir); err != nil {
 		if os.IsNotExist(err) {
 			return false, nil
 		}
@@ -821,11 +801,8 @@
 // path in the filesystem.
 func projectAtPath(jirix *jiri.X, path string) (Project, error) {
 	var project Project
-	absPath, err := ToAbs(path)
-	if err != nil {
-		return project, err
-	}
-	metadataFile := filepath.Join(absPath, metadataDirName, metadataFileName)
+	absPath := toAbs(jirix, path)
+	metadataFile := filepath.Join(absPath, jiri.ProjectMetaDir, jiri.ProjectMetaFile)
 	bytes, err := jirix.Run().ReadFile(metadataFile)
 	if err != nil {
 		return project, err
@@ -833,21 +810,14 @@
 	if err := xml.Unmarshal(bytes, &project); err != nil {
 		return project, fmt.Errorf("Unmarshal() failed: %v\n%s", err, string(bytes))
 	}
-	projectPath, err := ToAbs(project.Path)
-	if err != nil {
-		return project, err
-	}
-	project.Path = projectPath
+	project.Path = toAbs(jirix, project.Path)
 	return project, nil
 }
 
 // findLocalProjects scans the filesystem for all projects.  Note that project
 // directories can be nested recursively.
 func findLocalProjects(jirix *jiri.X, path string, projects Projects) error {
-	absPath, err := ToAbs(path)
-	if err != nil {
-		return err
-	}
+	absPath := toAbs(jirix, path)
 	isLocal, err := isLocalProject(jirix, absPath)
 	if err != nil {
 		return err
@@ -890,10 +860,7 @@
 		// In "dry run" mode, no binaries are built.
 		return nil
 	}
-	binDir, err := ToAbs(devtoolsBinDir)
-	if err != nil {
-		return err
-	}
+	binDir := toAbs(jirix, devtoolsBinDir)
 	fis, err := ioutil.ReadDir(dir)
 	if err != nil {
 		return fmt.Errorf("ReadDir(%v) failed: %v", dir, err)
@@ -1006,7 +973,7 @@
 		if _, ok := stack[manifest.Name]; ok {
 			return fmt.Errorf("import cycle encountered")
 		}
-		path, err := ResolveManifestPath(manifest.Name)
+		path, err := jirix.ResolveManifestPath(manifest.Name)
 		if err != nil {
 			return err
 		}
@@ -1025,11 +992,7 @@
 			continue
 		}
 		// Replace the relative path with an absolute one.
-		absPath, err := ToAbs(project.Path)
-		if err != nil {
-			return err
-		}
-		project.Path = absPath
+		project.Path = toAbs(jirix, project.Path)
 		// Use git as the default protocol.
 		if project.Protocol == "" {
 			project.Protocol = "git"
@@ -1216,7 +1179,7 @@
 // writeMetadata stores the given project metadata in the directory
 // identified by the given path.
 func writeMetadata(jirix *jiri.X, project Project, dir string) (e error) {
-	metadataDir := filepath.Join(dir, metadataDirName)
+	metadataDir := filepath.Join(dir, jiri.ProjectMetaDir)
 	cwd, err := os.Getwd()
 	if err != nil {
 		return err
@@ -1230,7 +1193,7 @@
 	}
 	// Replace absolute project paths with relative paths to make it
 	// possible to move the $JIRI_ROOT directory locally.
-	relPath, err := ToRel(project.Path)
+	relPath, err := toRel(jirix, project.Path)
 	if err != nil {
 		return err
 	}
@@ -1239,7 +1202,7 @@
 	if err != nil {
 		return fmt.Errorf("Marhsal() failed: %v", err)
 	}
-	metadataFile := filepath.Join(metadataDir, metadataFileName)
+	metadataFile := filepath.Join(metadataDir, jiri.ProjectMetaFile)
 	tmpMetadataFile := metadataFile + ".tmp"
 	if err := jirix.Run().WriteFile(tmpMetadataFile, bytes, os.FileMode(0644)); err != nil {
 		return err
@@ -1271,7 +1234,7 @@
 	default:
 		return UnsupportedProtocolErr(project.Protocol)
 	}
-	relPath, err := ToRel(project.Path)
+	relPath, err := toRel(jirix, project.Path)
 	if err != nil {
 		return err
 	}
@@ -1345,10 +1308,7 @@
 		if found && strings.HasPrefix(op.project.Remote, host.Location) {
 			gitHookDir := filepath.Join(tmpDir, ".git", "hooks")
 			for _, githook := range host.GitHooks {
-				mdir, err := ManifestDir()
-				if err != nil {
-					return err
-				}
+				mdir := jirix.ManifestDir()
 				src, err := jirix.Run().ReadFile(filepath.Join(mdir, githook.Path))
 				if err != nil {
 					return err
diff --git a/project/project_test.go b/project/project_test.go
index b15b445..ea9c799 100644
--- a/project/project_test.go
+++ b/project/project_test.go
@@ -4,7 +4,7 @@
 
 // TODO(jsimsa): Switch this test to using FakeJiriRoot.
 
-package project
+package project_test
 
 import (
 	"bytes"
@@ -19,7 +19,8 @@
 	"testing"
 
 	"v.io/jiri/jiri"
-	"v.io/jiri/jiri/jiritest"
+	"v.io/jiri/jiritest"
+	"v.io/jiri/project"
 )
 
 func addRemote(t *testing.T, jirix *jiri.X, localProject, name, remoteProject string) {
@@ -73,7 +74,7 @@
 	if err != nil {
 		t.Fatalf("ReadFile(%v) failed: %v", manifestFile, err)
 	}
-	manifest := Manifest{}
+	manifest := project.Manifest{}
 	if err := xml.Unmarshal(data, &manifest); err != nil {
 		t.Fatalf("Unmarshal() failed: %v\n%v", err, data)
 	}
@@ -91,8 +92,8 @@
 
 func createLocalManifestStub(t *testing.T, jirix *jiri.X, dir string) {
 	// Create a manifest stub.
-	manifest := Manifest{}
-	manifest.Imports = append(manifest.Imports, Import{Name: "default"})
+	manifest := project.Manifest{}
+	manifest.Imports = append(manifest.Imports, project.Import{Name: "default"})
 
 	// Store the manifest locally.
 	data, err := xml.Marshal(manifest)
@@ -110,9 +111,9 @@
 	if err := jirix.Run().MkdirAll(manifestDir, perm); err != nil {
 		t.Fatalf("%v", err)
 	}
-	manifest := Manifest{}
+	manifest := project.Manifest{}
 	for i, remote := range remotes {
-		project := Project{
+		project := project.Project{
 			Name:     remote,
 			Path:     localProjectName(i),
 			Protocol: "git",
@@ -120,12 +121,12 @@
 		}
 		manifest.Projects = append(manifest.Projects, project)
 	}
-	manifest.Hosts = []Host{
-		Host{
+	manifest.Hosts = []project.Host{
+		{
 			Name:     "gerrit",
 			Location: "git://example.com/gerrit",
 		},
-		Host{
+		{
 			Name:     "git",
 			Location: "git://example.com/git",
 		},
@@ -133,7 +134,7 @@
 	commitManifest(t, jirix, &manifest, dir)
 }
 
-func commitManifest(t *testing.T, jirix *jiri.X, manifest *Manifest, manifestDir string) {
+func commitManifest(t *testing.T, jirix *jiri.X, manifest *project.Manifest, manifestDir string) {
 	data, err := xml.Marshal(*manifest)
 	if err != nil {
 		t.Fatalf("%v", err)
@@ -155,28 +156,28 @@
 	}
 }
 
-func deleteProject(t *testing.T, jirix *jiri.X, manifestDir, project string) {
+func deleteProject(t *testing.T, jirix *jiri.X, manifestDir, name string) {
 	manifestFile := filepath.Join(manifestDir, "v2", "default")
 	data, err := ioutil.ReadFile(manifestFile)
 	if err != nil {
 		t.Fatalf("ReadFile(%v) failed: %v", manifestFile, err)
 	}
-	manifest := Manifest{}
+	manifest := project.Manifest{}
 	if err := xml.Unmarshal(data, &manifest); err != nil {
 		t.Fatalf("Unmarshal() failed: %v\n%v", err, data)
 	}
-	manifest.Projects = append(manifest.Projects, Project{Exclude: true, Name: project})
+	manifest.Projects = append(manifest.Projects, project.Project{Exclude: true, Name: name})
 	commitManifest(t, jirix, &manifest, manifestDir)
 }
 
 // Identify the current revision for a given project.
-func currentRevision(t *testing.T, jirix *jiri.X, project string) string {
+func currentRevision(t *testing.T, jirix *jiri.X, name string) string {
 	cwd, err := os.Getwd()
 	if err != nil {
 		t.Fatalf("%v", err)
 	}
 	defer jirix.Run().Chdir(cwd)
-	if err := jirix.Run().Chdir(project); err != nil {
+	if err := jirix.Run().Chdir(name); err != nil {
 		t.Fatalf("%v", err)
 	}
 	revision, err := jirix.Git().CurrentRevision()
@@ -187,19 +188,19 @@
 }
 
 // Fix the revision in the manifest file.
-func setRevisionForProject(t *testing.T, jirix *jiri.X, manifestDir, project, revision string) {
+func setRevisionForProject(t *testing.T, jirix *jiri.X, manifestDir, name, revision string) {
 	manifestFile := filepath.Join(manifestDir, "v2", "default")
 	data, err := ioutil.ReadFile(manifestFile)
 	if err != nil {
 		t.Fatalf("ReadFile(%v) failed: %v", manifestFile, err)
 	}
-	manifest := Manifest{}
+	manifest := project.Manifest{}
 	if err := xml.Unmarshal(data, &manifest); err != nil {
 		t.Fatalf("Unmarshal() failed: %v\n%v", err, data)
 	}
 	updated := false
 	for i, p := range manifest.Projects {
-		if p.Name == project {
+		if p.Name == name {
 			p.Revision = revision
 			manifest.Projects[i] = p
 			updated = true
@@ -207,33 +208,33 @@
 		}
 	}
 	if !updated {
-		t.Fatalf("failed to fix revision for project %v", project)
+		t.Fatalf("failed to fix revision for project %v", name)
 	}
 	commitManifest(t, jirix, &manifest, manifestDir)
 }
 
-func holdProjectBack(t *testing.T, jirix *jiri.X, manifestDir, project string) {
-	revision := currentRevision(t, jirix, project)
-	setRevisionForProject(t, jirix, manifestDir, project, revision)
+func holdProjectBack(t *testing.T, jirix *jiri.X, manifestDir, name string) {
+	revision := currentRevision(t, jirix, name)
+	setRevisionForProject(t, jirix, manifestDir, name, revision)
 }
 
 func localProjectName(i int) string {
 	return "test-local-project-" + fmt.Sprintf("%d", i)
 }
 
-func moveProject(t *testing.T, jirix *jiri.X, manifestDir, project, dst string) {
+func moveProject(t *testing.T, jirix *jiri.X, manifestDir, name, dst string) {
 	manifestFile := filepath.Join(manifestDir, "v2", "default")
 	data, err := ioutil.ReadFile(manifestFile)
 	if err != nil {
 		t.Fatalf("ReadFile(%v) failed: %v", manifestFile, err)
 	}
-	manifest := Manifest{}
+	manifest := project.Manifest{}
 	if err := xml.Unmarshal(data, &manifest); err != nil {
 		t.Fatalf("Unmarshal() failed: %v\n%v", err, data)
 	}
 	updated := false
 	for i, p := range manifest.Projects {
-		if p.Name == project {
+		if p.Name == name {
 			p.Path = dst
 			manifest.Projects[i] = p
 			updated = true
@@ -241,7 +242,7 @@
 		}
 	}
 	if !updated {
-		t.Fatalf("failed to set path for project %v", project)
+		t.Fatalf("failed to set path for project %v", name)
 	}
 	commitManifest(t, jirix, &manifest, manifestDir)
 }
@@ -268,7 +269,7 @@
 	}
 	if ignore {
 		ignoreFile := filepath.Join(projectDir, ".gitignore")
-		if err := jirix.Run().WriteFile(ignoreFile, []byte(metadataDirName), os.FileMode(0644)); err != nil {
+		if err := jirix.Run().WriteFile(ignoreFile, []byte(jiri.ProjectMetaDir), os.FileMode(0644)); err != nil {
 			t.Fatalf("%v", err)
 		}
 		if err := jirix.Git().Add(ignoreFile); err != nil {
@@ -285,15 +286,15 @@
 	if err := jirix.Run().Chdir(projectDir); err != nil {
 		t.Fatalf("%v", err)
 	}
-	metadataDir := filepath.Join(projectDir, metadataDirName)
+	metadataDir := filepath.Join(projectDir, jiri.ProjectMetaDir)
 	if err := jirix.Run().MkdirAll(metadataDir, os.FileMode(0755)); err != nil {
 		t.Fatalf("%v", err)
 	}
-	bytes, err := xml.Marshal(Project{})
+	bytes, err := xml.Marshal(project.Project{})
 	if err != nil {
 		t.Fatalf("Marshal() failed: %v", err)
 	}
-	metadataFile := filepath.Join(metadataDir, metadataFileName)
+	metadataFile := filepath.Join(metadataDir, jiri.ProjectMetaFile)
 	if err := jirix.Run().WriteFile(metadataFile, bytes, os.FileMode(0644)); err != nil {
 		t.Fatalf("%v", err)
 	}
@@ -345,7 +346,7 @@
 	}
 }
 
-func checkProjectsMatchPaths(t *testing.T, gotProjects Projects, wantProjectPaths []string) {
+func checkProjectsMatchPaths(t *testing.T, gotProjects project.Projects, wantProjectPaths []string) {
 	gotProjectPaths := []string{}
 	for _, p := range gotProjects {
 		gotProjectPaths = append(gotProjectPaths, p.Path)
@@ -368,17 +369,17 @@
 	// Create some projects.
 	numProjects, projectPaths := 3, []string{}
 	for i := 0; i < numProjects; i++ {
-		projectName := localProjectName(i)
-		projectPath := setupNewProject(t, jirix, jirix.Root, projectName, true)
-		project := Project{
-			Path:     projectPath,
-			Name:     projectName,
+		name := localProjectName(i)
+		path := setupNewProject(t, jirix, jirix.Root, name, true)
+		p := project.Project{
+			Path:     path,
+			Name:     name,
 			Protocol: "git",
 		}
-		if err := writeMetadata(jirix, project, projectPath); err != nil {
-			t.Fatalf("writeMetadata %v %v) failed: %v\n", project, projectPath, err)
+		if err := project.InternalWriteMetadata(jirix, p, path); err != nil {
+			t.Fatalf("writeMetadata %v %v) failed: %v\n", p, path, err)
 		}
-		projectPaths = append(projectPaths, projectPath)
+		projectPaths = append(projectPaths, path)
 	}
 
 	// Create manifest but only tell it about the first project.
@@ -386,16 +387,16 @@
 
 	// LocalProjects with scanMode = FastScan should only find the first
 	// project.
-	foundProjects, err := LocalProjects(jirix, FastScan)
+	foundProjects, err := project.LocalProjects(jirix, project.FastScan)
 	if err != nil {
-		t.Fatalf("LocalProjects(%v) failed: %v", FastScan, err)
+		t.Fatalf("LocalProjects(%v) failed: %v", project.FastScan, err)
 	}
 	checkProjectsMatchPaths(t, foundProjects, projectPaths[:1])
 
 	// LocalProjects with scanMode = FullScan should find all projects.
-	foundProjects, err = LocalProjects(jirix, FullScan)
+	foundProjects, err = project.LocalProjects(jirix, project.FullScan)
 	if err != nil {
-		t.Fatalf("LocalProjects(%v) failed: %v", FastScan, err)
+		t.Fatalf("LocalProjects(%v) failed: %v", project.FastScan, err)
 	}
 	checkProjectsMatchPaths(t, foundProjects, projectPaths[:])
 
@@ -404,9 +405,9 @@
 	if err := jirix.Run().RemoveAll(projectPaths[0]); err != nil {
 		t.Fatalf("RemoveAll(%v) failed: %v", projectPaths[0])
 	}
-	foundProjects, err = LocalProjects(jirix, FastScan)
+	foundProjects, err = project.LocalProjects(jirix, project.FastScan)
 	if err != nil {
-		t.Fatalf("LocalProjects(%v) failed: %v", FastScan, err)
+		t.Fatalf("LocalProjects(%v) failed: %v", project.FastScan, err)
 	}
 	checkProjectsMatchPaths(t, foundProjects, projectPaths[1:])
 }
@@ -422,11 +423,8 @@
 	jirix, cleanup := jiritest.NewX(t)
 	defer cleanup()
 
-	localDir := filepath.Join(jirix.Root, "local")
-	remoteDir := filepath.Join(jirix.Root, "remote")
-	if err := os.Setenv("JIRI_ROOT", localDir); err != nil {
-		t.Fatalf("%v", err)
-	}
+	localDir := jirix.Root
+	remoteDir := filepath.Join(jirix.Root, ".remote")
 
 	localManifest := setupNewProject(t, jirix, localDir, ".manifest", false)
 	writeEmptyMetadata(t, jirix, localManifest)
@@ -449,7 +447,7 @@
 	for _, remoteProject := range remoteProjects {
 		writeReadme(t, jirix, remoteProject, "revision 2")
 	}
-	if err := UpdateUniverse(jirix, false); err != nil {
+	if err := project.UpdateUniverse(jirix, false); err != nil {
 		t.Fatalf("%v", err)
 	}
 	checkCreateFn := func(i int, revision string) {
@@ -472,7 +470,7 @@
 	for _, remoteProject := range remoteProjects {
 		writeReadme(t, jirix, remoteProject, "revision 3")
 	}
-	if err := UpdateUniverse(jirix, false); err != nil {
+	if err := project.UpdateUniverse(jirix, false); err != nil {
 		t.Fatalf("%v", err)
 	}
 	checkUpdateFn := func(i int, revision string) {
@@ -494,7 +492,7 @@
 	if err := ioutil.WriteFile(file, want, perm); err != nil {
 		t.Fatalf("WriteFile(%v, %v) failed: %v", file, err, perm)
 	}
-	if err := UpdateUniverse(jirix, false); err != nil {
+	if err := project.UpdateUniverse(jirix, false); err != nil {
 		t.Fatalf("%v", err)
 	}
 	got, err := ioutil.ReadFile(file)
@@ -510,7 +508,7 @@
 	// copy of the project.
 	destination := filepath.Join("test", localProjectName(2))
 	moveProject(t, jirix, remoteManifest, remoteProjects[2], destination)
-	if err := UpdateUniverse(jirix, false); err != nil {
+	if err := project.UpdateUniverse(jirix, false); err != nil {
 		t.Fatalf("%v", err)
 	}
 	checkMoveFn := func(i int, revision string) {
@@ -527,7 +525,7 @@
 	// Delete a remote project and check that UpdateUniverse()
 	// deletes the local copy of the project.
 	deleteProject(t, jirix, remoteManifest, remoteProjects[3])
-	if err := UpdateUniverse(jirix, true); err != nil {
+	if err := project.UpdateUniverse(jirix, true); err != nil {
 		t.Fatalf("%v", err)
 	}
 	checkDeleteFn := func(i int, revision string) {
@@ -556,7 +554,7 @@
 	writeReadme(t, jirix, remoteProjects[4], "non master commit")
 	remoteBranchRevision := currentRevision(t, jirix, remoteProjects[4])
 	setRevisionForProject(t, jirix, remoteManifest, remoteProjects[4], remoteBranchRevision)
-	if err := UpdateUniverse(jirix, true); err != nil {
+	if err := project.UpdateUniverse(jirix, true); err != nil {
 		t.Fatalf("%v", err)
 	}
 	localProject := filepath.Join(localDir, localProjectName(4))
@@ -570,7 +568,7 @@
 	// Create a local manifest that imports the remote manifest
 	// and check that UpdateUniverse() has no effect.
 	createLocalManifestStub(t, jirix, localDir)
-	if err := UpdateUniverse(jirix, true); err != nil {
+	if err := project.UpdateUniverse(jirix, true); err != nil {
 		t.Fatalf("%v", err)
 	}
 
@@ -590,7 +588,7 @@
 	// check that UpdateUniverse() has no effect.
 	createLocalManifestCopy(t, jirix, localDir, remoteManifest)
 	createRemoteManifest(t, jirix, remoteManifest, remoteProjects)
-	if err := UpdateUniverse(jirix, true); err != nil {
+	if err := project.UpdateUniverse(jirix, true); err != nil {
 		t.Fatalf("%v", err)
 	}
 	for i, _ := range remoteProjects {
@@ -601,6 +599,6 @@
 // TestUnsupportedProtocolErr checks that calling
 // UnsupportedPrototoclErr.Error() does not result in an infinite loop.
 func TestUnsupportedPrototocolErr(t *testing.T) {
-	err := UnsupportedProtocolErr("foo")
+	err := project.UnsupportedProtocolErr("foo")
 	_ = err.Error()
 }
diff --git a/project/state.go b/project/state.go
index 2f86aa9..3b19dd7 100644
--- a/project/state.go
+++ b/project/state.go
@@ -37,7 +37,7 @@
 			return
 		}
 		for _, branch := range branches {
-			file := filepath.Join(state.Project.Path, MetadataDirName(), branch, ".gerrit_commit_message")
+			file := filepath.Join(state.Project.Path, jiri.ProjectMetaDir, branch, ".gerrit_commit_message")
 			hasFile := true
 			if _, err := jirix.Run().Stat(file); err != nil {
 				if !os.IsNotExist(err) {
diff --git a/snapshot.go b/snapshot.go
index 0afc338..21c0032 100644
--- a/snapshot.go
+++ b/snapshot.go
@@ -88,10 +88,7 @@
 		return jirix.UsageErrorf("unexpected number of arguments")
 	}
 	label := args[0]
-	if err := checkSnapshotDir(jirix); err != nil {
-		return err
-	}
-	snapshotDir, err := getSnapshotDir()
+	snapshotDir, err := getSnapshotDir(jirix)
 	if err != nil {
 		return err
 	}
@@ -125,49 +122,48 @@
 	return nil
 }
 
-// checkSnapshotDir makes sure that he local snapshot directory exists
-// and is initialized properly.
-func checkSnapshotDir(jirix *jiri.X) (e error) {
-	snapshotDir, err := getSnapshotDir()
-	if err != nil {
-		return err
+// getSnapshotDir returns the path to the snapshot directory, respecting the
+// value of the "-remote" command-line flag.  It performs validation that the
+// directory exists and is initialized.
+func getSnapshotDir(jirix *jiri.X) (string, error) {
+	dir := jirix.LocalSnapshotDir()
+	if remoteFlag {
+		dir = jirix.RemoteSnapshotDir()
 	}
-	if _, err := jirix.Run().Stat(snapshotDir); err != nil {
-		if !os.IsNotExist(err) {
+	switch _, err := jirix.Run().Stat(dir); {
+	case err == nil:
+		return dir, nil
+	case !os.IsNotExist(err):
+		return "", err
+	case remoteFlag:
+		if err := jirix.Run().MkdirAll(dir, 0755); err != nil {
+			return "", err
+		}
+		return dir, nil
+	}
+	// Create a new local snapshot directory.
+	createFn := func() (e error) {
+		if err := jirix.Run().MkdirAll(dir, 0755); err != nil {
 			return err
 		}
-		if remoteFlag {
-			if err := jirix.Run().MkdirAll(snapshotDir, 0755); err != nil {
-				return err
-			}
-			return nil
-		}
-		createFn := func() (err error) {
-			if err := jirix.Run().MkdirAll(snapshotDir, 0755); err != nil {
-				return err
-			}
-			if err := jirix.Git().Init(snapshotDir); err != nil {
-				return err
-			}
-			cwd, err := os.Getwd()
-			if err != nil {
-				return err
-			}
-			defer collect.Error(func() error { return jirix.Run().Chdir(cwd) }, &e)
-			if err := jirix.Run().Chdir(snapshotDir); err != nil {
-				return err
-			}
-			if err := jirix.Git().Commit(); err != nil {
-				return err
-			}
-			return nil
-		}
-		if err := createFn(); err != nil {
-			jirix.Run().RemoveAll(snapshotDir)
+		if err := jirix.Git().Init(dir); err != nil {
 			return err
 		}
+		cwd, err := os.Getwd()
+		if err != nil {
+			return err
+		}
+		defer collect.Error(func() error { return jirix.Run().Chdir(cwd) }, &e)
+		if err := jirix.Run().Chdir(dir); err != nil {
+			return err
+		}
+		return jirix.Git().Commit()
 	}
-	return nil
+	if err := createFn(); err != nil {
+		jirix.Run().RemoveAll(dir)
+		return "", err
+	}
+	return dir, nil
 }
 
 func createSnapshot(jirix *jiri.X, snapshotDir, snapshotFile, label string) error {
@@ -199,23 +195,6 @@
 	return nil
 }
 
-// getSnapshotDir returns the path to the snapshot directory,
-// respecting the value of the "-remote" command-line flag.
-func getSnapshotDir() (string, error) {
-	if remoteFlag {
-		snapshotDir, err := project.RemoteSnapshotDir()
-		if err != nil {
-			return "", err
-		}
-		return snapshotDir, nil
-	}
-	snapshotDir, err := project.LocalSnapshotDir()
-	if err != nil {
-		return "", err
-	}
-	return snapshotDir, nil
-}
-
 // revisionChanges commits changes identified by the given manifest
 // file and label to the manifest repository and (if applicable)
 // pushes these changes to the remote repository.
@@ -262,11 +241,7 @@
 }
 
 func runSnapshotList(jirix *jiri.X, args []string) error {
-	if err := checkSnapshotDir(jirix); err != nil {
-		return err
-	}
-
-	snapshotDir, err := getSnapshotDir()
+	snapshotDir, err := getSnapshotDir(jirix)
 	if err != nil {
 		return err
 	}
diff --git a/snapshot_test.go b/snapshot_test.go
index bb97ed9..e3e86c8 100644
--- a/snapshot_test.go
+++ b/snapshot_test.go
@@ -12,6 +12,7 @@
 	"testing"
 
 	"v.io/jiri/jiri"
+	"v.io/jiri/jiritest"
 	"v.io/jiri/project"
 	"v.io/jiri/tool"
 )
@@ -59,7 +60,7 @@
 
 func TestList(t *testing.T) {
 	// Setup a fake JIRI_ROOT.
-	root, err := project.NewFakeJiriRoot()
+	root, err := jiritest.NewFakeJiriRoot()
 	if err != nil {
 		t.Fatalf("%v", err)
 	}
@@ -68,20 +69,9 @@
 			t.Fatalf("%v", err)
 		}
 	}()
-	oldRoot, err := project.JiriRoot()
-	if err := os.Setenv("JIRI_ROOT", root.Dir); err != nil {
-		t.Fatalf("%v", err)
-	}
-	defer os.Setenv("JIRI_ROOT", oldRoot)
 
-	remoteSnapshotDir, err := project.RemoteSnapshotDir()
-	if err != nil {
-		t.Fatalf("%v", err)
-	}
-	localSnapshotDir, err := project.LocalSnapshotDir()
-	if err != nil {
-		t.Fatalf("%v", err)
-	}
+	remoteSnapshotDir := root.X.RemoteSnapshotDir()
+	localSnapshotDir := root.X.LocalSnapshotDir()
 
 	// Create a test suite.
 	tests := []config{
@@ -191,7 +181,7 @@
 
 func TestCreate(t *testing.T) {
 	// Setup a fake JIRI_ROOT instance.
-	root, err := project.NewFakeJiriRoot()
+	root, err := jiritest.NewFakeJiriRoot()
 	if err != nil {
 		t.Fatalf("%v", err)
 	}
@@ -216,13 +206,6 @@
 		}
 	}
 
-	// Point the JIRI_ROOT environment variable to the fake.
-	oldRoot, err := project.JiriRoot()
-	if err := os.Setenv("JIRI_ROOT", root.Dir); err != nil {
-		t.Fatalf("%v", err)
-	}
-	defer os.Setenv("JIRI_ROOT", oldRoot)
-
 	// Create initial commits in the remote projects and use
 	// UpdateUniverse() to mirror them locally.
 	for i := 0; i < numProjects; i++ {
@@ -250,10 +233,7 @@
 
 	// Check that invoking the UpdateUniverse() with the local
 	// snapshot restores the local repositories.
-	snapshotDir, err := project.LocalSnapshotDir()
-	if err != nil {
-		t.Fatalf("%v", err)
-	}
+	snapshotDir := root.X.LocalSnapshotDir()
 	snapshotFile := filepath.Join(snapshotDir, "test-local")
 	localX := root.X.Clone(tool.ContextOpts{
 		Manifest: &snapshotFile,
diff --git a/update.go b/update.go
index ab1f60d..a1511bb 100644
--- a/update.go
+++ b/update.go
@@ -46,11 +46,7 @@
 func runUpdate(jirix *jiri.X, _ []string) error {
 	// Create a snapshot of the current state of all projects and
 	// write it to the $JIRI_ROOT/.update_history folder.
-	root, err := project.JiriRoot()
-	if err != nil {
-		return err
-	}
-	snapshotFile := filepath.Join(root, ".update_history", time.Now().Format(time.RFC3339))
+	snapshotFile := filepath.Join(jirix.Root, ".update_history", time.Now().Format(time.RFC3339))
 	if err := project.CreateSnapshot(jirix, snapshotFile); err != nil {
 		return err
 	}
diff --git a/util/config_test.go b/util/config_test.go
index 87204f5..b43afa9 100644
--- a/util/config_test.go
+++ b/util/config_test.go
@@ -5,11 +5,10 @@
 package util
 
 import (
-	"os"
 	"reflect"
 	"testing"
 
-	"v.io/jiri/project"
+	"v.io/jiri/jiritest"
 )
 
 var (
@@ -111,7 +110,7 @@
 }
 
 func TestConfigSerialization(t *testing.T) {
-	root, err := project.NewFakeJiriRoot()
+	root, err := jiritest.NewFakeJiriRoot()
 	if err != nil {
 		t.Fatalf("%v", err)
 	}
@@ -120,11 +119,6 @@
 			t.Fatalf("%v", err)
 		}
 	}()
-	oldRoot, err := project.JiriRoot()
-	if err := os.Setenv("JIRI_ROOT", root.Dir); err != nil {
-		t.Fatalf("%v", err)
-	}
-	defer os.Setenv("JIRI_ROOT", oldRoot)
 
 	config := NewConfig(
 		APICheckProjectsOpt(apiCheckProjects),
diff --git a/util/env_test.go b/util/env_test.go
deleted file mode 100644
index 5e2d0c4..0000000
--- a/util/env_test.go
+++ /dev/null
@@ -1,59 +0,0 @@
-// 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 util
-
-import (
-	"os"
-	"path/filepath"
-	"testing"
-
-	"v.io/jiri/project"
-	"v.io/jiri/tool"
-)
-
-// TestJiriRootSymlink checks that JiriRoot interprets the value
-// of the JIRI_ROOT environment variable as a path, evaluates any
-// symlinks the path might contain, and returns the result.
-func TestJiriRootSymlink(t *testing.T) {
-	jirix := tool.NewDefaultContext()
-
-	// Create a temporary directory.
-	tmpDir, err := jirix.Run().TempDir("", "")
-	if err != nil {
-		t.Fatalf("TempDir() failed: %v", err)
-	}
-	defer jirix.Run().RemoveAll(tmpDir)
-
-	// Make sure tmpDir is not a symlink itself.
-	tmpDir, err = filepath.EvalSymlinks(tmpDir)
-	if err != nil {
-		t.Fatalf("EvalSymlinks(%v) failed: %v", tmpDir, err)
-	}
-
-	// Create a directory and a symlink to it.
-	root, perm := filepath.Join(tmpDir, "root"), os.FileMode(0700)
-	if err := jirix.Run().MkdirAll(root, perm); err != nil {
-		t.Fatalf("%v", err)
-	}
-	symRoot := filepath.Join(tmpDir, "sym_root")
-	if err := jirix.Run().Symlink(root, symRoot); err != nil {
-		t.Fatalf("%v", err)
-	}
-
-	// Set the JIRI_ROOT to the symlink created above and check
-	// that JiriRoot() evaluates the symlink.
-	oldRoot := os.Getenv("JIRI_ROOT")
-	if err := os.Setenv("JIRI_ROOT", symRoot); err != nil {
-		t.Fatalf("%v", err)
-	}
-	defer os.Setenv("JIRI_ROOT", oldRoot)
-	got, err := project.JiriRoot()
-	if err != nil {
-		t.Fatalf("%v", err)
-	}
-	if want := root; got != want {
-		t.Fatalf("unexpected output: got %v, want %v", got, want)
-	}
-}
diff --git a/util/oncall_test.go b/util/oncall_test.go
index 85b0d39..43d04f6 100644
--- a/util/oncall_test.go
+++ b/util/oncall_test.go
@@ -13,7 +13,7 @@
 	"time"
 
 	"v.io/jiri/jiri"
-	"v.io/jiri/project"
+	"v.io/jiri/jiritest"
 )
 
 func createOncallFile(t *testing.T, jirix *jiri.X) {
@@ -51,7 +51,7 @@
 }
 
 func TestOncall(t *testing.T) {
-	root, err := project.NewFakeJiriRoot()
+	root, err := jiritest.NewFakeJiriRoot()
 	if err != nil {
 		t.Fatalf("%v", err)
 	}
@@ -60,14 +60,6 @@
 			t.Fatalf("%v", err)
 		}
 	}()
-	oldRoot, err := project.JiriRoot()
-	if err != nil {
-		t.Fatalf("%v", err)
-	}
-	if err := os.Setenv("JIRI_ROOT", root.Dir); err != nil {
-		t.Fatalf("%v", err)
-	}
-	defer os.Setenv("JIRI_ROOT", oldRoot)
 
 	// Create a oncall.v1.xml file.
 	createOncallFile(t, root.X)
diff --git a/util/paths.go b/util/paths.go
index a68d10d..d028f15 100644
--- a/util/paths.go
+++ b/util/paths.go
@@ -36,29 +36,15 @@
 
 // ThirdPartyBinPath returns the path to the given third-party tool
 // taking into account the host and the target Go architecture.
-func ThirdPartyBinPath(name string) (string, error) {
-	root, err := project.JiriRoot()
-	if err != nil {
-		return "", err
-	}
-	bin := filepath.Join(root, "third_party", "go", "bin", name)
+func ThirdPartyBinPath(jirix *jiri.X, name string) (string, error) {
+	bin := filepath.Join(jirix.Root, "third_party", "go", "bin", name)
 	goArch := os.Getenv("GOARCH")
 	machineArch, err := host.Arch()
 	if err != nil {
 		return "", err
 	}
 	if goArch != "" && goArch != machineArch {
-		bin = filepath.Join(root, "third_party", "go", "bin", fmt.Sprintf("%s_%s", runtime.GOOS, goArch), name)
+		bin = filepath.Join(jirix.Root, "third_party", "go", "bin", fmt.Sprintf("%s_%s", runtime.GOOS, goArch), name)
 	}
 	return bin, nil
 }
-
-// ThirdPartyCCodePath returns that path to the directory containing built
-// binaries for the target OS and architecture.
-func ThirdPartyCCodePath(os, arch string) (string, error) {
-	root, err := project.JiriRoot()
-	if err != nil {
-		return "", err
-	}
-	return filepath.Join(root, "third_party", "cout", fmt.Sprintf("%s_%s", os, arch)), nil
-}