TBR jiri: Create and populate the .jiri_root dir

Our plans are to move to a new world order where there is a
.jiri_root directory that contains all the
root-level (i.e. non-project-level) jiri metadata.  This is a
step in that direction.

This CL creates the .jiri_root directory when "jiri update" is
run.  In our final state we want the bootstrap script to do this,
but we can't force everyone to re-run the bootstrap, so this is
part of the the transition logic.

We also populate this with two directories:
  root/.jiri_root/bin            # previously root/devtools/bin
  root/.jiri_root/update_history # previously root/.update_history

It's important to move the bin directory now, since we don't want
our shim script to change very often (or at all), so we need the
new location.  The "jiri update" logic now populates the new bin
directory, and we have transitional logic that backs up an
existing devtools/bin into .jiri_root/bin.BACKUP, and drops a
symlink from devtools/bin to .jiri_root/bin.

Also added the shim script, which is very straightforward.

MultiPart: 1/2
Change-Id: Ia90c337d2ec363d12033599fd2b3e303cd518a3a
diff --git a/cmd.go b/cmd.go
index 0090f3b..863596b 100644
--- a/cmd.go
+++ b/cmd.go
@@ -41,10 +41,76 @@
 		cmdUpdate,
 	},
 	Topics: []cmdline.Topic{
+		topicLayout,
 		topicManifest,
 	},
 }
 
+var topicLayout = cmdline.Topic{
+	Name:  "layout",
+	Short: "Description of jiri file system layout",
+	Long: `
+All data managed by the jiri tool is located in the file system under a root
+directory, colloquially called the jiri root directory.  The file system layout
+looks like this:
+
+ [root]                              # root directory (name picked by user)
+ [root]/.jiri_root                   # root metadata directory
+ [root]/.jiri_root/bin               # contains tool binaries (jiri, etc.)
+ [root]/.jiri_root/update_history    # contains history of update snapshots
+ [root]/.manifest                    # contains jiri manifests
+ [root]/[project1]                   # project directory (name picked by user)
+ [root]/[project1]/.jiri             # project metadata directory
+ [root]/[project1]/.jiri/metadata.v2 # project metadata file
+ [root]/[project1]/.jiri/<<cls>>     # project per-cl metadata directories
+ [root]/[project1]/<<files>>         # project files
+ [root]/[project2]...
+
+The [root] and [projectN] directory names are picked by the user.  The <<cls>>
+are named via jiri cl new, and the <<files>> are named as the user adds files
+and directories to their project.  All other names above have special meaning to
+the jiri tool, and cannot be changed; you must ensure your path names don't
+collide with these special names.
+
+There are two ways to run the jiri tool:
+
+1) Shim script (recommended approach).  This is a shell script that looks for
+the [root] directory.  If the JIRI_ROOT environment variable is set, that is
+assumed to be the [root] directory.  Otherwise the script looks for the
+.jiri_root directory, starting in the current working directory and walking up
+the directory chain.  The search is terminated successfully when the .jiri_root
+directory is found; it fails after it reaches the root of the file system.  Thus
+the shim must be invoked from the [root] directory or one of its subdirectories.
+
+Once the [root] is found, the JIRI_ROOT environment variable is set to its
+location, and [root]/.jiri_root/bin/jiri is invoked.  That file contains the
+actual jiri binary.
+
+The point of the shim script is to make it easy to use the jiri tool with
+multiple [root] directories on your file system.  Keep in mind that when "jiri
+update" is run, the jiri tool itself is automatically updated along with all
+projects.  By using the shim script, you only need to remember to invoke the
+jiri tool from within the appropriate [root] directory, and the projects and
+tools under that [root] directory will be updated.
+
+The shim script is located at [root]/release/go/src/v.io/jiri/scripts/jiri
+
+2) Direct binary.  This is the jiri binary, containing all of the actual jiri
+tool logic.  The binary requires the JIRI_ROOT environment variable to point to
+the [root] directory.
+
+Note that if you have multiple [root] directories on your file system, you must
+remember to run the jiri binary corresponding to the setting of your JIRI_ROOT
+environment variable.  Things may fail if you mix things up, since the jiri
+binary is updated with each call to "jiri update", and you may encounter version
+mismatches between the jiri binary and the various metadata files or other
+logic.  This is the reason the shim script is recommended over running the
+binary directly.
+
+The binary is located at [root]/.jiri_root/bin/jiri
+`,
+}
+
 var topicManifest = cmdline.Topic{
 	Name:  "manifest",
 	Short: "Description of manifest files",
diff --git a/doc.go b/doc.go
index 40b7e29..8a5bc8c 100644
--- a/doc.go
+++ b/doc.go
@@ -21,6 +21,7 @@
    help         Display help for commands or topics
 
 The jiri additional help topics are:
+   layout      Description of jiri file system layout
    manifest    Description of manifest files
 
 The jiri flags are:
@@ -316,7 +317,7 @@
 Jiri rebuild - Rebuild all jiri tools
 
 Rebuilds all jiri tools and installs the resulting binaries into
-$JIRI_ROOT/devtools/bin. This is similar to "jiri update", but does not update
+$JIRI_ROOT/.jiri_root/bin. This is similar to "jiri update", but does not update
 any projects before building the tools. The set of tools to rebuild is described
 in the manifest.
 
@@ -432,7 +433,7 @@
 Jiri update - Update all jiri tools and projects
 
 Updates all projects, builds the latest version of all tools, and installs the
-resulting binaries into $JIRI_ROOT/devtools/bin. The sequence in which the
+resulting binaries into $JIRI_ROOT/.jiri_root/bin. The sequence in which the
 individual updates happen guarantees that we end up with a consistent set of
 tools and source code. The set of projects and tools to update is described in
 the manifest.
@@ -483,6 +484,67 @@
    Defaults to the terminal width if available.  Override the default by setting
    the CMDLINE_WIDTH environment variable.
 
+Jiri layout - Description of jiri file system layout
+
+All data managed by the jiri tool is located in the file system under a root
+directory, colloquially called the jiri root directory.  The file system layout
+looks like this:
+
+ [root]                              # root directory (name picked by user)
+ [root]/.jiri_root                   # root metadata directory
+ [root]/.jiri_root/bin               # contains tool binaries (jiri, etc.)
+ [root]/.jiri_root/update_history    # contains history of update snapshots
+ [root]/.manifest                    # contains jiri manifests
+ [root]/[project1]                   # project directory (name picked by user)
+ [root]/[project1]/.jiri             # project metadata directory
+ [root]/[project1]/.jiri/metadata.v2 # project metadata file
+ [root]/[project1]/.jiri/<<cls>>     # project per-cl metadata directories
+ [root]/[project1]/<<files>>         # project files
+ [root]/[project2]...
+
+The [root] and [projectN] directory names are picked by the user.  The <<cls>>
+are named via jiri cl new, and the <<files>> are named as the user adds files
+and directories to their project.  All other names above have special meaning to
+the jiri tool, and cannot be changed; you must ensure your path names don't
+collide with these special names.
+
+There are two ways to run the jiri tool:
+
+1) Shim script (recommended approach).  This is a shell script that looks for
+the [root] directory.  If the JIRI_ROOT environment variable is set, that is
+assumed to be the [root] directory.  Otherwise the script looks for the
+.jiri_root directory, starting in the current working directory and walking up
+the directory chain.  The search is terminated successfully when the .jiri_root
+directory is found; it fails after it reaches the root of the file system.  Thus
+the shim must be invoked from the [root] directory or one of its subdirectories.
+
+Once the [root] is found, the JIRI_ROOT environment variable is set to its
+location, and [root]/.jiri_root/bin/jiri is invoked.  That file contains the
+actual jiri binary.
+
+The point of the shim script is to make it easy to use the jiri tool with
+multiple [root] directories on your file system.  Keep in mind that when "jiri
+update" is run, the jiri tool itself is automatically updated along with all
+projects.  By using the shim script, you only need to remember to invoke the
+jiri tool from within the appropriate [root] directory, and the projects and
+tools under that [root] directory will be updated.
+
+The shim script is located at [root]/release/go/src/v.io/jiri/scripts/jiri
+
+2) Direct binary.  This is the jiri binary, containing all of the actual jiri
+tool logic.  The binary requires the JIRI_ROOT environment variable to point to
+the [root] directory.
+
+Note that if you have multiple [root] directories on your file system, you must
+remember to run the jiri binary corresponding to the setting of your JIRI_ROOT
+environment variable.  Things may fail if you mix things up, since the jiri
+binary is updated with each call to "jiri update", and you may encounter version
+mismatches between the jiri binary and the various metadata files or other
+logic.  This is the reason the shim script is recommended over running the
+binary directly.
+
+The binary is located at [root]/.jiri_root/bin/jiri
+
 Jiri manifest - Description of manifest files
 
 Jiri manifests are revisioned and stored in a "manifest" repository, that is
diff --git a/jiri/.api b/jiri/.api
index c76f601..951333b 100644
--- a/jiri/.api
+++ b/jiri/.api
@@ -7,6 +7,7 @@
 pkg jiri, func NewRelPath(...string) RelPath
 pkg jiri, func NewX(*cmdline.Env) (*X, error)
 pkg jiri, func RunnerFunc(func(*X, []string) error) cmdline.Runner
+pkg jiri, method (*X) BinDir() string
 pkg jiri, method (*X) Clone(tool.ContextOpts) *X
 pkg jiri, method (*X) LocalManifestFile() string
 pkg jiri, method (*X) LocalSnapshotDir() string
@@ -14,6 +15,8 @@
 pkg jiri, method (*X) ManifestFile(string) string
 pkg jiri, method (*X) RemoteSnapshotDir() string
 pkg jiri, method (*X) ResolveManifestPath(string) (string, error)
+pkg jiri, method (*X) RootMetaDir() string
+pkg jiri, method (*X) UpdateHistoryDir() string
 pkg jiri, method (*X) UsageErrorf(string, ...interface{}) error
 pkg jiri, method (RelPath) Abs(*X) string
 pkg jiri, method (RelPath) Join(...string) RelPath
diff --git a/jiri/x.go b/jiri/x.go
index 22e463c..c5735f6 100644
--- a/jiri/x.go
+++ b/jiri/x.go
@@ -107,6 +107,21 @@
 	return nil
 }
 
+// RootMetaDir returns the path to the root metadata directory.
+func (x *X) RootMetaDir() string {
+	return filepath.Join(x.Root, RootMetaDir)
+}
+
+// BinDir returns the path to the bin directory.
+func (x *X) BinDir() string {
+	return filepath.Join(x.RootMetaDir(), "bin")
+}
+
+// UpdateHistoryDir returns the path to the update history directory.
+func (x *X) UpdateHistoryDir() string {
+	return filepath.Join(x.RootMetaDir(), "update_history")
+}
+
 // LocalManifestFile returns the path to the local manifest file.
 func (x *X) LocalManifestFile() string {
 	return filepath.Join(x.Root, ".local_manifest")
diff --git a/project/.api b/project/.api
new file mode 100644
index 0000000..7558eaa
--- /dev/null
+++ b/project/.api
@@ -0,0 +1,84 @@
+pkg project, const FastScan ScanMode
+pkg project, const FullScan ScanMode
+pkg project, func ApplyToLocalMaster(*jiri.X, Projects, func() error) error
+pkg project, func BuildTools(*jiri.X, Tools, string) error
+pkg project, func CleanupProjects(*jiri.X, Projects, bool) error
+pkg project, func CreateSnapshot(*jiri.X, string) error
+pkg project, func CurrentManifest(*jiri.X) (*Manifest, error)
+pkg project, func CurrentProjectName(*jiri.X) (string, error)
+pkg project, func DataDirPath(*jiri.X, string) (string, error)
+pkg project, func GerritHost(*jiri.X) (string, error)
+pkg project, func GetProjectStates(*jiri.X, bool) (map[string]*ProjectState, error)
+pkg project, func GitHost(*jiri.X) (string, error)
+pkg project, func InstallTools(*jiri.X, string) error
+pkg project, func LocalProjects(*jiri.X, ScanMode) (Projects, error)
+pkg project, func ParseNames(*jiri.X, []string, map[string]struct{}) (map[string]Project, error)
+pkg project, func PollProjects(*jiri.X, map[string]struct{}) (Update, error)
+pkg project, func ReadManifest(*jiri.X) (Projects, Tools, error)
+pkg project, func TransitionBinDir(*jiri.X) error
+pkg project, func UpdateUniverse(*jiri.X, bool) error
+pkg project, method (UnsupportedProtocolErr) Error() string
+pkg project, type BranchState struct
+pkg project, type BranchState struct, HasGerritMessage bool
+pkg project, type BranchState struct, Name string
+pkg project, type CL struct
+pkg project, type CL struct, Author string
+pkg project, type CL struct, Description string
+pkg project, type CL struct, Email string
+pkg project, type GitHook struct
+pkg project, type GitHook struct, Name string
+pkg project, type GitHook struct, Path string
+pkg project, type Hook struct
+pkg project, type Hook struct, Args []HookArg
+pkg project, type Hook struct, Exclude bool
+pkg project, type Hook struct, Interpreter string
+pkg project, type Hook struct, Name string
+pkg project, type Hook struct, Path string
+pkg project, type Hook struct, Project string
+pkg project, type HookArg struct
+pkg project, type HookArg struct, Arg string
+pkg project, type Hooks map[string]Hook
+pkg project, type Host struct
+pkg project, type Host struct, GitHooks []GitHook
+pkg project, type Host struct, Location string
+pkg project, type Host struct, Name string
+pkg project, type Hosts map[string]Host
+pkg project, type Import struct
+pkg project, type Import struct, Name string
+pkg project, type Imports map[string]Import
+pkg project, type Manifest struct
+pkg project, type Manifest struct, Hooks []Hook
+pkg project, type Manifest struct, Hosts []Host
+pkg project, type Manifest struct, Imports []Import
+pkg project, type Manifest struct, Label string
+pkg project, type Manifest struct, Projects []Project
+pkg project, type Manifest struct, Tools []Tool
+pkg project, type Manifest struct, XMLName struct{}
+pkg project, type Project struct
+pkg project, type Project struct, Exclude bool
+pkg project, type Project struct, Name string
+pkg project, type Project struct, Path string
+pkg project, type Project struct, Protocol string
+pkg project, type Project struct, Remote string
+pkg project, type Project struct, RemoteBranch string
+pkg project, type Project struct, Revision string
+pkg project, type ProjectState struct
+pkg project, type ProjectState struct, Branches []BranchState
+pkg project, type ProjectState struct, CurrentBranch string
+pkg project, type ProjectState struct, HasUncommitted bool
+pkg project, type ProjectState struct, HasUntracked bool
+pkg project, type ProjectState struct, Project Project
+pkg project, type Projects map[string]Project
+pkg project, type ScanMode bool
+pkg project, type Tool struct
+pkg project, type Tool struct, Data string
+pkg project, type Tool struct, Exclude bool
+pkg project, type Tool struct, Name string
+pkg project, type Tool struct, Package string
+pkg project, type Tool struct, Project string
+pkg project, type Tools map[string]Tool
+pkg project, type UnsupportedProtocolErr string
+pkg project, type Update map[string][]CL
+pkg project, var JiriName string
+pkg project, var JiriPackage string
+pkg project, var JiriProject string
diff --git a/project/project.go b/project/project.go
index fc40d27..d5cc5c7 100644
--- a/project/project.go
+++ b/project/project.go
@@ -179,8 +179,6 @@
 // project names to a collections of commits.
 type Update map[string][]CL
 
-var devtoolsBinDir = filepath.Join("devtools", "bin")
-
 // CreateSnapshot creates a manifest that encodes the current state of
 // master branches of all projects and writes this snapshot out to the
 // given file.
@@ -545,7 +543,7 @@
 	if err := buildToolsFromMaster(jirix, remoteTools, tmpDir); err != nil {
 		return err
 	}
-	// 3. Install the tools into $JIRI_ROOT/devtools/bin.
+	// 3. Install the tools into $JIRI_ROOT/.jiri_root/bin.
 	if err := InstallTools(jirix, tmpDir); err != nil {
 		return err
 	}
@@ -852,7 +850,7 @@
 }
 
 // InstallTools installs the tools from the given directory into
-// $JIRI_ROOT/devtools/bin.
+// $JIRI_ROOT/.jiri_root/bin.
 func InstallTools(jirix *jiri.X, dir string) error {
 	jirix.TimerPush("install tools")
 	defer jirix.TimerPop()
@@ -860,20 +858,20 @@
 		// In "dry run" mode, no binaries are built.
 		return nil
 	}
-	binDir := toAbs(jirix, devtoolsBinDir)
 	fis, err := ioutil.ReadDir(dir)
 	if err != nil {
 		return fmt.Errorf("ReadDir(%v) failed: %v", dir, err)
 	}
+	binDir := jirix.BinDir()
+	if err := jirix.NewSeq().MkdirAll(binDir, 0755).Done(); err != nil {
+		return fmt.Errorf("MkdirAll(%v) failed: %v", binDir, err)
+	}
 	failed := false
 	for _, fi := range fis {
 		installFn := func() error {
 			src := filepath.Join(dir, fi.Name())
 			dst := filepath.Join(binDir, fi.Name())
-			if err := jirix.Run().Rename(src, dst); err != nil {
-				return err
-			}
-			return nil
+			return jirix.Run().Rename(src, dst)
 		}
 		opts := runutil.Opts{Verbose: true}
 		if err := jirix.Run().FunctionWithOpts(opts, installFn, "install tool %q", fi.Name()); err != nil {
@@ -884,19 +882,57 @@
 	if failed {
 		return cmdline.ErrExitCode(2)
 	}
+	return nil
+}
 
-	// Delete any old subcommands.
-	v23SubCmds := []string{
-		"jiri-profile",
-		"jiri-env",
-	}
-	for _, subCmd := range v23SubCmds {
-		subCmdPath := filepath.Join(binDir, subCmd)
-		if err := jirix.Run().RemoveAll(subCmdPath); err != nil {
-			return err
+// TransitionBinDir handles the transition from the old location
+// $JIRI_ROOT/devtools/bin to the new $JIRI_ROOT/.jiri_root/bin.  In
+// InstallTools above we've already installed the tools to the new location.
+//
+// For now we want $JIRI_ROOT/devtools/bin symlinked to the new location, so
+// that users won't perceive a difference in behavior.  In addition, we want to
+// save the old binaries to $JIRI_ROOT/.jiri_root/bin.BACKUP the first time this
+// is run.  That way if we screwed something up, the user can recover their old
+// binaries.
+//
+// TODO(toddw): Remove this logic after the transition to .jiri_root is done.
+func TransitionBinDir(jirix *jiri.X) error {
+	oldDir, newDir := filepath.Join(jirix.Root, "devtools", "bin"), jirix.BinDir()
+	switch info, err := jirix.Run().Lstat(oldDir); {
+	case os.IsNotExist(err):
+		// Drop down to create the symlink below.
+	case err != nil:
+		return fmt.Errorf("Failed to stat old bin dir: %v", err)
+	case info.Mode() == os.ModeSymlink:
+		link, err := jirix.NewSeq().Readlink(oldDir)
+		if err != nil {
+			return fmt.Errorf("Failed to read link from old bin dir: %v", err)
+		}
+		if link == newDir {
+			// The old dir is already correctly symlinked to the new dir.
+			return nil
+		}
+		fallthrough
+	default:
+		// The old dir exists, and either it's not a symlink, or it's a symlink that
+		// doesn't point to the new dir.  Move the old dir to the backup location.
+		backupDir := newDir + ".BACKUP"
+		switch _, err := jirix.Run().Stat(backupDir); {
+		case os.IsNotExist(err):
+			if err := jirix.NewSeq().Rename(oldDir, backupDir).Done(); err != nil {
+				return fmt.Errorf("Failed to backup old bin dir %v to %v: %v", oldDir, backupDir, err)
+			}
+			// Drop down to create the symlink below.
+		case err != nil:
+			return fmt.Errorf("Failed to stat backup bin dir: %v", err)
+		default:
+			return fmt.Errorf("Backup bin dir %v already exists", backupDir)
 		}
 	}
-
+	// Create the symlink.
+	if err := jirix.NewSeq().MkdirAll(filepath.Dir(oldDir), 0755).Symlink(newDir, oldDir).Done(); err != nil {
+		return fmt.Errorf("Failed to symlink to new bin dir %v from %v: %v", newDir, oldDir, err)
+	}
 	return nil
 }
 
diff --git a/project/project_test.go b/project/project_test.go
index ea9c799..5683ced 100644
--- a/project/project_test.go
+++ b/project/project_test.go
@@ -2,8 +2,6 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// TODO(jsimsa): Switch this test to using FakeJiriRoot.
-
 package project_test
 
 import (
@@ -602,3 +600,167 @@
 	err := project.UnsupportedProtocolErr("foo")
 	_ = err.Error()
 }
+
+type binDirTest struct {
+	Name     string
+	Setup    func(old, new string) error
+	Teardown func(old, new string) error
+	Error    string
+}
+
+func TestTransitionBinDir(t *testing.T) {
+	tests := []binDirTest{
+		{
+			"No old dir",
+			func(old, new string) error { return nil },
+			nil,
+			"",
+		},
+		{
+			"Empty old dir",
+			func(old, new string) error {
+				return os.MkdirAll(old, 0777)
+			},
+			nil,
+			"",
+		},
+		{
+			"Populated old dir",
+			func(old, new string) error {
+				if err := os.MkdirAll(old, 0777); err != nil {
+					return err
+				}
+				return ioutil.WriteFile(filepath.Join(old, "tool"), []byte("foo"), 0777)
+			},
+			nil,
+			"",
+		},
+		{
+			"Symlinked old dir",
+			func(old, new string) error {
+				if err := os.MkdirAll(filepath.Dir(old), 0777); err != nil {
+					return err
+				}
+				return os.Symlink(new, old)
+			},
+			nil,
+			"",
+		},
+		{
+			"Symlinked old dir pointing nowhere",
+			func(old, new string) error {
+				if err := os.MkdirAll(filepath.Dir(old), 0777); err != nil {
+					return err
+				}
+				return os.Symlink(filepath.Join(new, "noexist"), old)
+			},
+			nil,
+			"",
+		},
+		{
+			"Unreadable old dir parent",
+			func(old, new string) error {
+				if err := os.MkdirAll(old, 0777); err != nil {
+					return err
+				}
+				return os.Chmod(filepath.Dir(old), 0222)
+			},
+			func(old, new string) error {
+				return os.Chmod(filepath.Dir(old), 0777)
+			},
+			"Failed to stat old bin dir",
+		},
+		{
+			"Unwritable old dir",
+			func(old, new string) error {
+				if err := os.MkdirAll(old, 0777); err != nil {
+					return err
+				}
+				return os.Chmod(old, 0444)
+			},
+			func(old, new string) error {
+				return os.Chmod(old, 0777)
+			},
+			"Failed to backup old bin dir",
+		},
+		{
+			"Unreadable backup dir parent",
+			func(old, new string) error {
+				if err := os.MkdirAll(old, 0777); err != nil {
+					return err
+				}
+				return os.Chmod(filepath.Dir(new), 0222)
+			},
+			func(old, new string) error {
+				return os.Chmod(filepath.Dir(new), 0777)
+			},
+			"Failed to stat backup bin dir",
+		},
+		{
+			"Existing backup dir",
+			func(old, new string) error {
+				if err := os.MkdirAll(old, 0777); err != nil {
+					return err
+				}
+				return os.MkdirAll(new+".BACKUP", 0777)
+			},
+			nil,
+			"Backup bin dir",
+		},
+	}
+	for _, test := range tests {
+		jirix, cleanup := jiritest.NewX(t)
+		if err := testTransitionBinDir(jirix, test); err != nil {
+			t.Errorf("%s: %v", test.Name, err)
+		}
+		cleanup()
+	}
+}
+
+func testTransitionBinDir(jirix *jiri.X, test binDirTest) (e error) {
+	oldDir, newDir := filepath.Join(jirix.Root, "devtools", "bin"), jirix.BinDir()
+	// The new bin dir always exists.
+	if err := os.MkdirAll(newDir, 0777); err != nil {
+		return fmt.Errorf("make new dir failed: %v", err)
+	}
+	if err := test.Setup(oldDir, newDir); err != nil {
+		return fmt.Errorf("setup failed: %v", err)
+	}
+	if test.Teardown != nil {
+		defer func() {
+			if err := test.Teardown(oldDir, newDir); err != nil && e == nil {
+				e = fmt.Errorf("teardown failed: %v", err)
+			}
+		}()
+	}
+	oldInfo, _ := os.Stat(oldDir)
+	switch err := project.TransitionBinDir(jirix); {
+	case err != nil && test.Error == "":
+		return fmt.Errorf("got error %q, want success", err)
+	case err != nil && !strings.Contains(fmt.Sprint(err), test.Error):
+		return fmt.Errorf("got error %q, want prefix %q", err, test.Error)
+	case err == nil && test.Error != "":
+		return fmt.Errorf("got no error, want %q", test.Error)
+	case err == nil && test.Error == "":
+		// Make sure the symlink exists and is correctly linked.
+		link, err := os.Readlink(oldDir)
+		if err != nil {
+			return fmt.Errorf("old dir isn't a symlink: %v", err)
+		}
+		if got, want := link, newDir; got != want {
+			return fmt.Errorf("old dir symlink got %v, want %v", got, want)
+		}
+		if oldInfo != nil {
+			// Make sure the oldDir was backed up correctly.
+			backupDir := filepath.Join(jirix.RootMetaDir(), "bin.BACKUP")
+			backupInfo, err := os.Stat(backupDir)
+			if err != nil {
+				return fmt.Errorf("stat backup dir failed: %v", err)
+			}
+			if !os.SameFile(oldInfo, backupInfo) {
+				return fmt.Errorf("old dir wasn't backed up correctly")
+			}
+		}
+	}
+	return nil
+}
diff --git a/rebuild.go b/rebuild.go
index 2ca2de0..608805b 100644
--- a/rebuild.go
+++ b/rebuild.go
@@ -20,7 +20,7 @@
 	Short:  "Rebuild all jiri tools",
 	Long: `
 Rebuilds all jiri tools and installs the resulting binaries into
-$JIRI_ROOT/devtools/bin. This is similar to "jiri update", but does not update
+$JIRI_ROOT/.jiri_root/bin. This is similar to "jiri update", but does not update
 any projects before building the tools. The set of tools to rebuild is described
 in the manifest.
 
diff --git a/runutil/.api b/runutil/.api
index a818038..318b8e8 100644
--- a/runutil/.api
+++ b/runutil/.api
@@ -14,6 +14,7 @@
 pkg runutil, method (*Run) Function(func() error, string, ...interface{}) error
 pkg runutil, method (*Run) FunctionWithOpts(Opts, func() error, string, ...interface{}) error
 pkg runutil, method (*Run) IsDir(string) (bool, error)
+pkg runutil, method (*Run) Lstat(string) (os.FileInfo, error)
 pkg runutil, method (*Run) MkdirAll(string, os.FileMode) error
 pkg runutil, method (*Run) Open(string) (*os.File, error)
 pkg runutil, method (*Run) OpenFile(string, int, os.FileMode) (*os.File, error)
@@ -21,6 +22,7 @@
 pkg runutil, method (*Run) OutputWithOpts(Opts, []string)
 pkg runutil, method (*Run) ReadDir(string) ([]os.FileInfo, error)
 pkg runutil, method (*Run) ReadFile(string) ([]byte, error)
+pkg runutil, method (*Run) Readlink(string) (string, error)
 pkg runutil, method (*Run) Remove(string) error
 pkg runutil, method (*Run) RemoveAll(string) error
 pkg runutil, method (*Run) Rename(string, string) error
@@ -44,6 +46,7 @@
 pkg runutil, method (*Sequence) FileExists(string) (bool, error)
 pkg runutil, method (*Sequence) IsDir(string) (bool, error)
 pkg runutil, method (*Sequence) Last(string, ...string) error
+pkg runutil, method (*Sequence) Lstat(string) (os.FileInfo, error)
 pkg runutil, method (*Sequence) MkdirAll(string, os.FileMode) *Sequence
 pkg runutil, method (*Sequence) Open(string) (*os.File, error)
 pkg runutil, method (*Sequence) OpenFile(string, int, os.FileMode) (*os.File, error)
@@ -53,6 +56,7 @@
 pkg runutil, method (*Sequence) Read(io.Reader) *Sequence
 pkg runutil, method (*Sequence) ReadDir(string) ([]os.FileInfo, error)
 pkg runutil, method (*Sequence) ReadFile(string) ([]byte, error)
+pkg runutil, method (*Sequence) Readlink(string) (string, error)
 pkg runutil, method (*Sequence) Remove(string) *Sequence
 pkg runutil, method (*Sequence) RemoveAll(string) *Sequence
 pkg runutil, method (*Sequence) Rename(string, string) *Sequence
diff --git a/runutil/sequence.go b/runutil/sequence.go
index 44f2c68..257c3fd 100644
--- a/runutil/sequence.go
+++ b/runutil/sequence.go
@@ -145,6 +145,9 @@
 
 func (s *Sequence) Error() error {
 	if s.err != nil && len(s.caller) > 0 {
+		// TODO(toddw): Wrapping the error here is bad, since some callers require
+		// the original error to be returned.  E.g. it's common to call os.Stat()
+		// and check against os.IsNotExist, which breaks with wrapped errors.
 		return fmt.Errorf("%s: %v", s.caller, s.err)
 	}
 	return s.err
@@ -548,6 +551,28 @@
 	return fi, s.Done()
 }
 
+// Lstat is a wrapper around os.Lstat that handles options such as
+// "verbose" or "dry run". Lstat is a terminating function.
+func (s *Sequence) Lstat(name string) (os.FileInfo, error) {
+	if s.err != nil {
+		return nil, s.Done()
+	}
+	fi, err := s.r.Lstat(name)
+	s.setError(err, fmt.Sprintf("Lstat(%s)", name))
+	return fi, s.Done()
+}
+
+// Readlink is a wrapper around os.Readlink that handles options such as
+// "verbose" or "dry run". Lstat is a terminating function.
+func (s *Sequence) Readlink(name string) (string, error) {
+	if s.err != nil {
+		return "", s.Done()
+	}
+	link, err := s.r.Readlink(name)
+	s.setError(err, fmt.Sprintf("Readlink(%s)", name))
+	return link, s.Done()
+}
+
 // TempDir is a wrapper around ioutil.TempDir that handles options
 // such as "verbose" or "dry run". TempDir is a terminating function.
 func (s *Sequence) TempDir(dir, prefix string) (string, error) {
diff --git a/runutil/wrapper.go b/runutil/wrapper.go
index d4c63b3..ad0c914 100644
--- a/runutil/wrapper.go
+++ b/runutil/wrapper.go
@@ -166,6 +166,26 @@
 	return
 }
 
+// Lstat is a wrapper around os.Lstat that handles options such as
+// "verbose" or "dry run".
+func (r *Run) Lstat(name string) (fileInfo os.FileInfo, err error) {
+	r.dryRun(func() error {
+		fileInfo, err = os.Lstat(name)
+		return err
+	}, fmt.Sprintf("lstat %q", name))
+	return
+}
+
+// Readlink is a wrapper around os.Readlink that handles options such as
+// "verbose" or "dry run".
+func (r *Run) Readlink(name string) (link string, err error) {
+	r.dryRun(func() error {
+		link, err = os.Readlink(name)
+		return err
+	}, fmt.Sprintf("readlink %q", name))
+	return
+}
+
 // IsDir is a wrapper around os.Stat that handles options such as
 // "verbose" or "dry run".
 func (r *Run) IsDir(name string) (bool, error) {
diff --git a/scripts/jiri b/scripts/jiri
new file mode 100755
index 0000000..8059229
--- /dev/null
+++ b/scripts/jiri
@@ -0,0 +1,31 @@
+#!/bin/sh
+# 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.
+
+fatal() {
+  echo "ERROR: $@" 1>&2
+  exit 1
+}
+
+# If $JIRI_ROOT is set we always use it, otherwise look for a .jiri_root
+# directory starting with the current working directory, and walking up.
+if [ "${JIRI_ROOT}" == "" ]; then
+  while [ ! -d  "$(pwd)/.jiri_root" ]; do
+    if [ "$(pwd)" == "/" ]; then
+      fatal "could not find .jiri_root directory"
+    fi
+    cd ..
+  done
+  export JIRI_ROOT="$(pwd)"
+fi
+
+# Make sure the jiri binary exists and is executable.
+if [ ! -e "${JIRI_ROOT}/.jiri_root/bin/jiri" ]; then
+  fatal "${JIRI_ROOT}/.jiri_root/bin/jiri does not exist"
+elif [ ! -x "${JIRI_ROOT}/.jiri_root/bin/jiri" ]; then
+  fatal "${JIRI_ROOT}/.jiri_root/bin/jiri is not executable"
+fi
+
+# Execute the jiri binary.
+exec "${JIRI_ROOT}/.jiri_root/bin/jiri" "$@"
diff --git a/update.go b/update.go
index a1511bb..9e126ad 100644
--- a/update.go
+++ b/update.go
@@ -34,7 +34,7 @@
 	Short:  "Update all jiri tools and projects",
 	Long: `
 Updates all projects, builds the latest version of all tools, and installs the
-resulting binaries into $JIRI_ROOT/devtools/bin. The sequence in which the
+resulting binaries into $JIRI_ROOT/.jiri_root/bin. The sequence in which the
 individual updates happen guarantees that we end up with a consistent set of
 tools and source code. The set of projects and tools to update is described in
 the manifest.
@@ -44,9 +44,18 @@
 }
 
 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.
-	snapshotFile := filepath.Join(jirix.Root, ".update_history", time.Now().Format(time.RFC3339))
+	// Create the $JIRI_ROOT/.jiri_root directory if it doesn't already exist.
+	//
+	// TODO(toddw): Remove this logic after the transition to .jiri_root is done.
+	// The bootstrapping logic should create this directory, and jiri should fail
+	// if the directory doesn't exist.
+	if err := jirix.NewSeq().MkdirAll(jirix.RootMetaDir(), 0755).Done(); err != nil {
+		return err
+	}
+
+	// Create a snapshot of the current state of all projects and write it to the
+	// update history directory.
+	snapshotFile := filepath.Join(jirix.UpdateHistoryDir(), time.Now().Format(time.RFC3339))
 	if err := project.CreateSnapshot(jirix, snapshotFile); err != nil {
 		return err
 	}
@@ -54,7 +63,12 @@
 	// Update all projects to their latest version.
 	// Attempt <attemptsFlag> times before failing.
 	updateFn := func() error {
-		return project.UpdateUniverse(jirix, gcFlag)
+		if err := project.UpdateUniverse(jirix, gcFlag); err != nil {
+			return err
+		}
+		// We're careful to only attempt the bin dir transition after the update has
+		// succeeded, to avoid messy partial states.
+		return project.TransitionBinDir(jirix)
 	}
 	return retry.Function(jirix.Context, updateFn, retry.AttemptsOpt(attemptsFlag))
 }