jiri: Add bootstrap_jiri and local "jiri import"

The idea behind bootstrap_jiri is that it's how to initialize
your jiri root directory.  It sets up the jiri binary and shim
script, and prompts you to add the script directory to your PATH.

The "jiri import" command updates the .jiri_manifest file.
Previously it only handled remote imports.  The new form handles
both remote and local imports, by flipping the order of the
[remote] and [manifest] args.  The -mode flag is also renamed to
-overwrite.

MultiPart: 1/2

Change-Id: I8538d537a73b6fd718b482aa35d337b5d2a95cfa
diff --git a/bootstrap_jiri_test.go b/bootstrap_jiri_test.go
new file mode 100644
index 0000000..f5fce52
--- /dev/null
+++ b/bootstrap_jiri_test.go
@@ -0,0 +1,67 @@
+// 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 main
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"v.io/x/lib/gosh"
+)
+
+func TestBootstrapJiri(t *testing.T) {
+	sh := gosh.NewShell(gosh.Opts{Fatalf: t.Fatalf, Logf: t.Logf})
+	defer sh.Cleanup()
+
+	bootstrap, err := filepath.Abs("./scripts/bootstrap_jiri")
+	if err != nil {
+		t.Fatalf("couldn't determine absolute path to bootstrap_jiri script")
+	}
+	rootDir := filepath.Join(sh.MakeTempDir(), "root")
+	c := sh.Cmd(bootstrap, []string{rootDir}...)
+	c.AddStdoutWriter(gosh.NopWriteCloser(os.Stdout))
+	c.AddStderrWriter(gosh.NopWriteCloser(os.Stderr))
+	stdout, stderr := c.StdoutStderr()
+	if got, want := stdout, fmt.Sprintf("Please add %s to your PATH.\n", filepath.Join(rootDir, ".jiri_root", "scripts")); got != want {
+		t.Errorf("stdout got %q, want %q", got, want)
+	}
+	if got, want := stderr, ""; got != want {
+		t.Errorf("stderr got %q, want %q", got, want)
+	}
+	if _, err := os.Stat(filepath.Join(rootDir, ".jiri_root", "bin", "jiri")); err != nil {
+		t.Error(err)
+	}
+	if _, err := os.Stat(filepath.Join(rootDir, ".jiri_root", "scripts", "jiri")); err != nil {
+		t.Error(err)
+	}
+}
+
+func TestBootstrapJiriAlreadyExists(t *testing.T) {
+	sh := gosh.NewShell(gosh.Opts{Fatalf: t.Fatalf, Logf: t.Logf})
+	defer sh.Cleanup()
+
+	bootstrap, err := filepath.Abs("./scripts/bootstrap_jiri")
+	if err != nil {
+		t.Fatalf("couldn't determine absolute path to bootstrap_jiri script")
+	}
+	rootDir := sh.MakeTempDir()
+	c := sh.Cmd(bootstrap, []string{rootDir}...)
+	c.AddStdoutWriter(gosh.NopWriteCloser(os.Stdout))
+	c.AddStderrWriter(gosh.NopWriteCloser(os.Stderr))
+	c.ExitErrorIsOk = true
+	stdout, stderr := c.StdoutStderr()
+	if c.Err == nil {
+		t.Errorf("error got %q, want nil", c.Err)
+	}
+	if got, want := stdout, ""; got != want {
+		t.Errorf("stdout got %q, want %q", got, want)
+	}
+	if got, want := stderr, rootDir+" already exists"; !strings.Contains(got, want) {
+		t.Errorf("stderr got %q, want substr %q", got, want)
+	}
+}
diff --git a/cmd.go b/cmd.go
index afcbc6e..d06bc63 100644
--- a/cmd.go
+++ b/cmd.go
@@ -41,6 +41,7 @@
 		cmdSnapshot,
 		cmdUpdate,
 		cmdUpgrade,
+		cmdWhich,
 	},
 	Topics: []cmdline.Topic{
 		topicFileSystem,
diff --git a/doc.go b/doc.go
index 9fd3cad..5948cf8 100644
--- a/doc.go
+++ b/doc.go
@@ -20,6 +20,7 @@
    snapshot     Manage project snapshots
    update       Update all jiri tools and projects
    upgrade      Upgrade jiri to new-style manifests
+   which        Show path to the jiri tool
    help         Display help for commands or topics
 
 The jiri additional help topics are:
@@ -211,29 +212,45 @@
 Command "import" adds imports to the $JIRI_ROOT/.jiri_manifest file, which
 specifies manifest information for the jiri tool.  The file is created if it
 doesn't already exist, otherwise additional imports are added to the existing
-file.  The arguments and flags configure the <import> element that is added to
-the manifest.
+file.
+
+<manifest> specifies the manifest file to use.
+
+[remote] optionally specifies the remote manifest repository.
+
+If [remote] is not specified, a <fileimport> element is added to the manifest,
+representing a local file import.  The manifest file may be an absolute path, or
+relative to the current working directory.  The resulting path must be a
+subdirectory of $JIRI_ROOT.
+
+If [remote] is specified, an <import> element is added to the manifest,
+representing a remote manifest import.  The remote manifest repository is
+treated similar to regular projects; "jiri update" will update all remote
+manifest repository projects before updating regular projects.  The manifest
+file path is relative to the root directory of the remote import repository.
+
+Example of a local file import:
+  $ jiri import $JIRI_ROOT/path/to/manifest/file
+
+Example of a remote manifest import:
+  $ jiri import myfile https://foo.com/bar.git
 
 Run "jiri help manifest" for details on manifests.
 
 Usage:
-   jiri import [flags] <remote> <manifest>
-
-<remote> specifies the remote repository that contains your manifest project.
-
-<manifest> specifies the manifest file to use from the manifest project.
+   jiri import [flags] <manifest> [remote]
 
 The jiri import flags are:
- -mode=append
-   The import mode:
-      append    - Create file if it doesn't exist, or append to existing file.
-      overwrite - Write file regardless of whether it already exists.
  -name=
    The name of the remote manifest project, used to disambiguate manifest
    projects with the same remote.  Typically empty.
  -out=
    The output file.  Uses $JIRI_ROOT/.jiri_manifest if unspecified.  Uses stdout
    if set to "-".
+ -overwrite=false
+   Write a new .jiri_manifest file with the given specification.  If it already
+   exists, the existing content will be ignored and the file will be
+   overwritten.
  -path=
    Path to store the manifest project locally.  Uses "manifest" if unspecified.
  -protocol=git
@@ -551,6 +568,33 @@
  -v=false
    Print verbose output.
 
+Jiri which - Show path to the jiri tool
+
+Which behaves similarly to the unix commandline tool.  It is useful in
+determining whether the jiri binary is being run directly, or run via the jiri
+shim script.
+
+If the binary is being run directly, the output looks like this:
+
+  # binary
+  /path/to/binary/jiri
+
+If the script is being run, the output looks like this:
+
+  # script
+  /path/to/script/jiri
+
+Usage:
+   jiri which [flags]
+
+The jiri which flags are:
+ -color=true
+   Use color to format output.
+ -n=false
+   Show what commands will run but do not execute them.
+ -v=false
+   Print verbose output.
+
 Jiri help - Display help for commands or topics
 
 Help with no args displays the usage of the parent command.
diff --git a/import.go b/import.go
index 48c092b..a7dbb2c 100644
--- a/import.go
+++ b/import.go
@@ -7,6 +7,8 @@
 import (
 	"fmt"
 	"os"
+	"path/filepath"
+	"strings"
 
 	"v.io/jiri/jiri"
 	"v.io/jiri/project"
@@ -15,11 +17,11 @@
 )
 
 var (
-	// Flags for configuring project attributes.
+	// Flags for configuring project attributes for remote imports.
 	flagImportName, flagImportPath, flagImportProtocol, flagImportRemoteBranch, flagImportRevision, flagImportRoot string
 	// Flags for controlling the behavior of the command.
-	flagImportMode importMode
-	flagImportOut  string
+	flagImportOverwrite bool
+	flagImportOut       string
 )
 
 func init() {
@@ -30,47 +32,10 @@
 	cmdImport.Flags.StringVar(&flagImportRevision, "revision", "HEAD", `The revision of the remote manifest project to reset to during "jiri update".`)
 	cmdImport.Flags.StringVar(&flagImportRoot, "root", "", `Root to store the manifest project locally.`)
 
-	cmdImport.Flags.Var(&flagImportMode, "mode", `
-The import mode:
-   append    - Create file if it doesn't exist, or append to existing file.
-   overwrite - Write file regardless of whether it already exists.
-`)
+	cmdImport.Flags.BoolVar(&flagImportOverwrite, "overwrite", false, `Write a new .jiri_manifest file with the given specification.  If it already exists, the existing content will be ignored and the file will be overwritten.`)
 	cmdImport.Flags.StringVar(&flagImportOut, "out", "", `The output file.  Uses $JIRI_ROOT/.jiri_manifest if unspecified.  Uses stdout if set to "-".`)
 }
 
-type importMode int
-
-const (
-	importAppend importMode = iota
-	importOverwrite
-)
-
-func (m *importMode) Set(s string) error {
-	switch s {
-	case "append":
-		*m = importAppend
-		return nil
-	case "overwrite":
-		*m = importOverwrite
-		return nil
-	}
-	return fmt.Errorf("unknown import mode %q", s)
-}
-
-func (m importMode) String() string {
-	switch m {
-	case importAppend:
-		return "append"
-	case importOverwrite:
-		return "overwrite"
-	}
-	return "UNKNOWN"
-}
-
-func (m importMode) Get() interface{} {
-	return m
-}
-
 var cmdImport = &cmdline.Command{
 	Runner: jiri.RunnerFunc(runImport),
 	Name:   "import",
@@ -79,26 +44,41 @@
 Command "import" adds imports to the $JIRI_ROOT/.jiri_manifest file, which
 specifies manifest information for the jiri tool.  The file is created if it
 doesn't already exist, otherwise additional imports are added to the existing
-file.  The arguments and flags configure the <import> element that is added to
-the manifest.
+file.
+
+<manifest> specifies the manifest file to use.
+
+[remote] optionally specifies the remote manifest repository.
+
+If [remote] is not specified, a <fileimport> element is added to the manifest,
+representing a local file import.  The manifest file may be an absolute path, or
+relative to the current working directory.  The resulting path must be a
+subdirectory of $JIRI_ROOT.
+
+If [remote] is specified, an <import> element is added to the manifest,
+representing a remote manifest import.  The remote manifest repository is
+treated similar to regular projects; "jiri update" will update all remote
+manifest repository projects before updating regular projects.  The manifest
+file path is relative to the root directory of the remote import repository.
+
+Example of a local file import:
+  $ jiri import $JIRI_ROOT/path/to/manifest/file
+
+Example of a remote manifest import:
+  $ jiri import myfile https://foo.com/bar.git
 
 Run "jiri help manifest" for details on manifests.
 `,
-	ArgsName: "<remote> <manifest>",
-	ArgsLong: `
-<remote> specifies the remote repository that contains your manifest project.
-
-<manifest> specifies the manifest file to use from the manifest project.
-`,
+	ArgsName: "<manifest> [remote]",
 }
 
 func runImport(jirix *jiri.X, args []string) error {
-	if len(args) != 2 || args[0] == "" || args[1] == "" {
-		return jirix.UsageErrorf("must specify non-empty <remote> and <manifest>")
+	if len(args) == 0 || len(args) > 2 {
+		return jirix.UsageErrorf("wrong number of arguments")
 	}
 	// Initialize manifest.
 	var manifest *project.Manifest
-	if flagImportMode == importAppend {
+	if !flagImportOverwrite {
 		m, err := project.ManifestFromFile(jirix, jirix.JiriManifestFile())
 		if err != nil && !runutil.IsNotExist(err) {
 			return err
@@ -108,19 +88,44 @@
 	if manifest == nil {
 		manifest = &project.Manifest{}
 	}
-	// Add remote import.
-	manifest.Imports = append(manifest.Imports, project.Import{
-		Manifest: args[1],
-		Root:     flagImportRoot,
-		Project: project.Project{
-			Name:         flagImportName,
-			Path:         flagImportPath,
-			Protocol:     flagImportProtocol,
-			Remote:       args[0],
-			RemoteBranch: flagImportRemoteBranch,
-			Revision:     flagImportRevision,
-		},
-	})
+	// Add the local or remote import.
+	if len(args) == 1 {
+		// FileImport.File is relative to the directory containing the manifest
+		// file; since the .jiri_manifest file is in JIRI_ROOT, that's what it
+		// should be relative to.
+		if _, err := os.Stat(args[0]); err != nil {
+			return err
+		}
+		abs, err := filepath.Abs(args[0])
+		if err != nil {
+			return err
+		}
+		rel, err := filepath.Rel(jirix.Root, abs)
+		if err != nil {
+			return err
+		}
+		if strings.HasPrefix(rel, "..") {
+			return fmt.Errorf("%s is not a subdirectory of JIRI_ROOT %s", abs, jirix.Root)
+		}
+		manifest.FileImports = append(manifest.FileImports, project.FileImport{
+			File: rel,
+		})
+	} else {
+		// There's not much error checking when writing the .jiri_manifest file;
+		// errors will be reported when "jiri update" is run.
+		manifest.Imports = append(manifest.Imports, project.Import{
+			Manifest: args[0],
+			Root:     flagImportRoot,
+			Project: project.Project{
+				Name:         flagImportName,
+				Path:         flagImportPath,
+				Protocol:     flagImportProtocol,
+				Remote:       args[1],
+				RemoteBranch: flagImportRemoteBranch,
+				Revision:     flagImportRevision,
+			},
+		})
+	}
 	// Write output to stdout or file.
 	outFile := flagImportOut
 	if outFile == "" {
diff --git a/import_test.go b/import_test.go
index e9f4bf2..6f2a4de 100644
--- a/import_test.go
+++ b/import_test.go
@@ -7,6 +7,8 @@
 import (
 	"fmt"
 	"io/ioutil"
+	"os"
+	"path/filepath"
 	"strings"
 	"testing"
 
@@ -24,15 +26,58 @@
 func TestImport(t *testing.T) {
 	tests := []importTestCase{
 		{
-			Stderr: `must specify non-empty`,
+			Stderr: `wrong number of arguments`,
 		},
 		{
-			Args:   []string{"https://github.com/new.git"},
-			Stderr: `must specify non-empty`,
+			Args:   []string{"a", "b", "c"},
+			Stderr: `wrong number of arguments`,
 		},
-		// Default mode = append
+		// Local file imports, default append behavior
 		{
-			Args: []string{"-name=name", "-path=path", "-remotebranch=remotebranch", "-revision=revision", "-root=root", "https://github.com/new.git", "foo"},
+			Args: []string{"manfile"},
+			Want: `<manifest>
+  <imports>
+    <fileimport file="manfile"/>
+  </imports>
+</manifest>
+`,
+		},
+		{
+			Args: []string{"./manfile"},
+			Want: `<manifest>
+  <imports>
+    <fileimport file="manfile"/>
+  </imports>
+</manifest>
+`,
+		},
+		{
+			Args: []string{"manfile"},
+			Exist: `<manifest>
+  <imports>
+    <import manifest="bar" remote="https://github.com/orig.git"/>
+  </imports>
+</manifest>
+`,
+			Want: `<manifest>
+  <imports>
+    <import manifest="bar" remote="https://github.com/orig.git"/>
+    <fileimport file="manfile"/>
+  </imports>
+</manifest>
+`,
+		},
+		{
+			Args:   []string{"../manfile"},
+			Stderr: `not a subdirectory of JIRI_ROOT`,
+		},
+		{
+			Args:   []string{"noexist"},
+			Stderr: `no such file`,
+		},
+		// Remote imports, default append behavior
+		{
+			Args: []string{"-name=name", "-path=path", "-remotebranch=remotebranch", "-revision=revision", "-root=root", "foo", "https://github.com/new.git"},
 			Want: `<manifest>
   <imports>
     <import manifest="foo" root="root" name="name" path="path" remote="https://github.com/new.git" remotebranch="remotebranch" revision="revision"/>
@@ -41,7 +86,7 @@
 `,
 		},
 		{
-			Args: []string{"https://github.com/new.git", "foo"},
+			Args: []string{"foo", "https://github.com/new.git"},
 			Want: `<manifest>
   <imports>
     <import manifest="foo" remote="https://github.com/new.git"/>
@@ -50,7 +95,7 @@
 `,
 		},
 		{
-			Args:     []string{"-out=file", "https://github.com/new.git", "foo"},
+			Args:     []string{"-out=file", "foo", "https://github.com/new.git"},
 			Filename: `file`,
 			Want: `<manifest>
   <imports>
@@ -60,7 +105,7 @@
 `,
 		},
 		{
-			Args: []string{"-out=-", "https://github.com/new.git", "foo"},
+			Args: []string{"-out=-", "foo", "https://github.com/new.git"},
 			Stdout: `<manifest>
   <imports>
     <import manifest="foo" remote="https://github.com/new.git"/>
@@ -69,24 +114,66 @@
 `,
 		},
 		{
-			Args: []string{"https://github.com/new.git", "foo"},
+			Args: []string{"foo", "https://github.com/new.git"},
 			Exist: `<manifest>
   <imports>
-    <import manifest="bar" remote="https://github.com/exist.git"/>
+    <import manifest="bar" remote="https://github.com/orig.git"/>
   </imports>
 </manifest>
 `,
 			Want: `<manifest>
   <imports>
-    <import manifest="bar" remote="https://github.com/exist.git"/>
+    <import manifest="bar" remote="https://github.com/orig.git"/>
     <import manifest="foo" remote="https://github.com/new.git"/>
   </imports>
 </manifest>
 `,
 		},
-		// Explicit mode = append
+		// Local file imports, explicit overwrite behavior
 		{
-			Args: []string{"-mode=append", "https://github.com/new.git", "foo"},
+			Args: []string{"-overwrite", "manfile"},
+			Want: `<manifest>
+  <imports>
+    <fileimport file="manfile"/>
+  </imports>
+</manifest>
+`,
+		},
+		{
+			Args: []string{"-overwrite", "./manfile"},
+			Want: `<manifest>
+  <imports>
+    <fileimport file="manfile"/>
+  </imports>
+</manifest>
+`,
+		},
+		{
+			Args: []string{"-overwrite", "manfile"},
+			Exist: `<manifest>
+  <imports>
+    <import manifest="bar" remote="https://github.com/orig.git"/>
+  </imports>
+</manifest>
+`,
+			Want: `<manifest>
+  <imports>
+    <fileimport file="manfile"/>
+  </imports>
+</manifest>
+`,
+		},
+		{
+			Args:   []string{"-overwrite", "../manfile"},
+			Stderr: `not a subdirectory of JIRI_ROOT`,
+		},
+		{
+			Args:   []string{"-overwrite", "noexist"},
+			Stderr: `no such file`,
+		},
+		// Remote imports, explicit overwrite behavior
+		{
+			Args: []string{"-overwrite", "foo", "https://github.com/new.git"},
 			Want: `<manifest>
   <imports>
     <import manifest="foo" remote="https://github.com/new.git"/>
@@ -95,7 +182,7 @@
 `,
 		},
 		{
-			Args:     []string{"-mode=append", "-out=file", "https://github.com/new.git", "foo"},
+			Args:     []string{"-overwrite", "-out=file", "foo", "https://github.com/new.git"},
 			Filename: `file`,
 			Want: `<manifest>
   <imports>
@@ -105,7 +192,7 @@
 `,
 		},
 		{
-			Args: []string{"-mode=append", "-out=-", "https://github.com/new.git", "foo"},
+			Args: []string{"-overwrite", "-out=-", "foo", "https://github.com/new.git"},
 			Stdout: `<manifest>
   <imports>
     <import manifest="foo" remote="https://github.com/new.git"/>
@@ -114,55 +201,10 @@
 `,
 		},
 		{
-			Args: []string{"-mode=append", "https://github.com/new.git", "foo"},
+			Args: []string{"-overwrite", "foo", "https://github.com/new.git"},
 			Exist: `<manifest>
   <imports>
-    <import manifest="bar" remote="https://github.com/exist.git"/>
-  </imports>
-</manifest>
-`,
-			Want: `<manifest>
-  <imports>
-    <import manifest="bar" remote="https://github.com/exist.git"/>
-    <import manifest="foo" remote="https://github.com/new.git"/>
-  </imports>
-</manifest>
-`,
-		},
-		// Explicit mode = overwrite
-		{
-			Args: []string{"-mode=overwrite", "https://github.com/new.git", "foo"},
-			Want: `<manifest>
-  <imports>
-    <import manifest="foo" remote="https://github.com/new.git"/>
-  </imports>
-</manifest>
-`,
-		},
-		{
-			Args:     []string{"-mode=overwrite", "-out=file", "https://github.com/new.git", "foo"},
-			Filename: `file`,
-			Want: `<manifest>
-  <imports>
-    <import manifest="foo" remote="https://github.com/new.git"/>
-  </imports>
-</manifest>
-`,
-		},
-		{
-			Args: []string{"-mode=overwrite", "-out=-", "https://github.com/new.git", "foo"},
-			Stdout: `<manifest>
-  <imports>
-    <import manifest="foo" remote="https://github.com/new.git"/>
-  </imports>
-</manifest>
-`,
-		},
-		{
-			Args: []string{"-mode=overwrite", "https://github.com/new.git", "foo"},
-			Exist: `<manifest>
-  <imports>
-    <import manifest="bar" remote="https://github.com/exist.git"/>
+    <import manifest="bar" remote="https://github.com/orig.git"/>
   </imports>
 </manifest>
 `,
@@ -188,13 +230,23 @@
 func testImport(opts gosh.Opts, jiriTool string, test importTestCase) error {
 	sh := gosh.NewShell(opts)
 	defer sh.Cleanup()
-	jiriRoot := sh.MakeTempDir()
+	tmpDir := sh.MakeTempDir()
+	jiriRoot := filepath.Join(tmpDir, "root")
+	if err := os.Mkdir(jiriRoot, 0755); err != nil {
+		return err
+	}
 	sh.Pushd(jiriRoot)
-	defer sh.Popd()
 	filename := test.Filename
 	if filename == "" {
 		filename = ".jiri_manifest"
 	}
+	// Set up manfile for the local file import tests.  It should exist in both
+	// the tmpDir (for ../manfile tests) and jiriRoot.
+	for _, dir := range []string{tmpDir, jiriRoot} {
+		if err := ioutil.WriteFile(filepath.Join(dir, "manfile"), nil, 0644); err != nil {
+			return err
+		}
+	}
 	// Set up an existing file if it was specified.
 	if test.Exist != "" {
 		if err := ioutil.WriteFile(filename, []byte(test.Exist), 0644); err != nil {
diff --git a/scripts/bootstrap_jiri b/scripts/bootstrap_jiri
new file mode 100755
index 0000000..d02e2e6
--- /dev/null
+++ b/scripts/bootstrap_jiri
@@ -0,0 +1,75 @@
+#!/bin/bash
+# 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.
+
+# bootstrap_jiri initializes a root directory for jiri.  The following
+# directories and files will be created:
+#   <root_dir>                         - root directory (picked by user)
+#   <root_dir>/.jiri_root              - root metadata directory
+#   <root_dir>/.jiri_root/bin/jiri     - jiri binary
+#   <root_dir>/.jiri_root/scripts/jiri - jiri script
+#
+# The jiri sources are downloaded and built into a temp directory, which is
+# always deleted when this script finishes.  The <root_dir> is deleted on any
+# failure.
+
+set -euf -o pipefail
+
+# fatal prints an error message, followed by the usage string, and then exits.
+fatal() {
+  usage='
+
+Usage:
+   bootstrap_jiri <root_dir>
+
+A typical bootstrap workflow looks like this:
+
+$ curl -s https://raw.githubusercontent.com/vanadium/go.jiri/master/scripts/bootstrap_jiri | bash -s myroot
+$ export PATH=myroot/scripts:$PATH
+$ cd myroot
+$ jiri import public https://vanadium.googlesource.com/manifest
+$ jiri update'
+  echo "ERROR: $@${usage}" 1>&2
+  exit 1
+}
+
+# toabs converts the possibly relative argument into an absolute path.  Run in a
+# subshell to avoid changing the caller's working directory.
+toabs() (
+  cd $(dirname $1)
+  echo ${PWD}/$(basename $1)
+)
+
+# Check the <root_dir> argument is supplied and doesn't already exist.
+if [[ $# -ne 1 ]]; then
+  fatal "need <root_dir> argument"
+fi
+root_dir=$(toabs $1)
+if [[ -e ${root_dir} ]]; then
+  fatal "${root_dir} already exists"
+fi
+# Check that go is on the PATH.
+if ! go version >& /dev/null ; then
+  fatal 'ERROR: "go" tool not found, see https://golang.org/doc/install'
+fi
+
+trap "rm -rf ${root_dir}" INT TERM EXIT
+
+# Make the output directories.
+tmp_dir="${root_dir}/.jiri_root/tmp"
+bin_dir="${root_dir}/.jiri_root/bin"
+scripts_dir="${root_dir}/.jiri_root/scripts"
+mkdir -p "${tmp_dir}" "${bin_dir}" "${scripts_dir}"
+
+# Go get the jiri source files, build the jiri binary, and copy the jiri shim
+# script from the sources.
+GOPATH="${tmp_dir}" go get -d v.io/jiri
+GOPATH="${tmp_dir}" go build -o "${bin_dir}/jiri" v.io/jiri
+cp "${tmp_dir}/src/v.io/jiri/scripts/jiri" "${scripts_dir}/jiri"
+
+# Clean up the tmp_dir.
+rm -rf "${tmp_dir}"
+
+echo "Please add ${scripts_dir} to your PATH."
+trap - EXIT
diff --git a/scripts/jiri b/scripts/jiri
index 8059229..24593ed 100755
--- a/scripts/jiri
+++ b/scripts/jiri
@@ -1,18 +1,46 @@
-#!/bin/sh
+#!/bin/bash
 # 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.
 
+# jiri is a shim script that determines the jiri root directory and invokes
+# $JIRI_ROOT/.jiri_root/bin/jiri with the given arguments.
+#
+# If the JIRI_ROOT environment variable is set, that is assumed to be the jiri
+# 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.
+#
+# This script should be invoked from the jiri root directory or one of its
+# subdirectories, unless the JIRI_ROOT environment variable is set.
+
+set -euf -o pipefail
+
+# fatal prints an error message and exits.
 fatal() {
   echo "ERROR: $@" 1>&2
   exit 1
 }
 
+# Handle "jiri which" without any arguments.  This is handy to determine whether
+# the PATH is set up pointing at this shim script, or a regular binary.  If
+# there are any arguments, we pass the command through to the binary.
+if [[ $# -eq 1  ]]; then
+  if [[ "$1" == "which" ]]; then
+    echo "# script"
+    type -p $0
+    exit 0
+  fi
+fi
+
 # 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
+if [[ "${JIRI_ROOT}" == "" ]]; then
+  while [[ ! -d  "$(pwd)/.jiri_root" ]]; do
+    if [[ "$(pwd)" == "/" ]]; then
       fatal "could not find .jiri_root directory"
     fi
     cd ..
@@ -21,9 +49,9 @@
 fi
 
 # Make sure the jiri binary exists and is executable.
-if [ ! -e "${JIRI_ROOT}/.jiri_root/bin/jiri" ]; then
+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
+elif [[ ! -x "${JIRI_ROOT}/.jiri_root/bin/jiri" ]]; then
   fatal "${JIRI_ROOT}/.jiri_root/bin/jiri is not executable"
 fi
 
diff --git a/upgrade_test.go b/upgrade_test.go
index 49690d9..a94e276 100644
--- a/upgrade_test.go
+++ b/upgrade_test.go
@@ -214,7 +214,6 @@
 	defer sh.Cleanup()
 	jiriRoot := sh.MakeTempDir()
 	sh.Pushd(jiriRoot)
-	defer sh.Popd()
 	// Set up an existing file or local_manifest, if they were specified
 	if test.Exist {
 		if err := ioutil.WriteFile(".jiri_manifest", []byte("<manifest/>"), 0644); err != nil {
@@ -264,7 +263,6 @@
 	defer sh.Cleanup()
 	jiriRoot := sh.MakeTempDir()
 	sh.Pushd(jiriRoot)
-	defer sh.Popd()
 	jiriTool := sh.BuildGoPkg("v.io/jiri")
 	localData := `<manifest/>`
 	jiriData := `<manifest>
diff --git a/which.go b/which.go
new file mode 100644
index 0000000..9dee05c
--- /dev/null
+++ b/which.go
@@ -0,0 +1,54 @@
+// 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 main
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+
+	"v.io/x/lib/cmdline"
+)
+
+var cmdWhich = &cmdline.Command{
+	Runner: cmdline.RunnerFunc(runWhich),
+	Name:   "which",
+	Short:  "Show path to the jiri tool",
+	Long: `
+Which behaves similarly to the unix commandline tool.  It is useful in
+determining whether the jiri binary is being run directly, or run via the jiri
+shim script.
+
+If the binary is being run directly, the output looks like this:
+
+  # binary
+  /path/to/binary/jiri
+
+If the script is being run, the output looks like this:
+
+  # script
+  /path/to/script/jiri
+`,
+}
+
+func runWhich(env *cmdline.Env, args []string) error {
+	if len(args) == 0 {
+		fmt.Fprintln(env.Stdout, "# binary")
+		path, err := exec.LookPath(os.Args[0])
+		if err != nil {
+			return err
+		}
+		abs, err := filepath.Abs(path)
+		if err != nil {
+			return err
+		}
+		fmt.Fprintln(env.Stdout, abs)
+		return nil
+	}
+	// TODO(toddw): Look up the path to each argument.  This will only be helpful
+	// after the profiles are moved back into the main jiri tool.
+	return fmt.Errorf("unexpected arguments")
+}
diff --git a/which_test.go b/which_test.go
new file mode 100644
index 0000000..1e5383d
--- /dev/null
+++ b/which_test.go
@@ -0,0 +1,48 @@
+// 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 main
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"v.io/x/lib/gosh"
+)
+
+func TestWhich(t *testing.T) {
+	sh := gosh.NewShell(gosh.Opts{Fatalf: t.Fatalf, Logf: t.Logf})
+	defer sh.Cleanup()
+	jiriBinary := sh.BuildGoPkg("v.io/jiri")
+	stdout, stderr := sh.Cmd(jiriBinary, []string{"which"}...).StdoutStderr()
+	if got, want := stdout, fmt.Sprintf("# binary\n%s\n", jiriBinary); got != want {
+		t.Errorf("stdout got %q, want %q", got, want)
+	}
+	if got, want := stderr, ""; got != want {
+		t.Errorf("stderr got %q, want %q", got, want)
+	}
+}
+
+// TestWhichScript tests the behavior of "jiri which" for the shim script.
+func TestWhichScript(t *testing.T) {
+	sh := gosh.NewShell(gosh.Opts{Fatalf: t.Fatalf, Logf: t.Logf})
+	defer sh.Cleanup()
+
+	jiriScript, err := filepath.Abs("./scripts/jiri")
+	if err != nil {
+		t.Fatalf("couldn't determine absolute path to jiri script")
+	}
+	c := sh.Cmd(jiriScript, []string{"which"}...)
+	c.AddStdoutWriter(gosh.NopWriteCloser(os.Stdout))
+	c.AddStderrWriter(gosh.NopWriteCloser(os.Stderr))
+	stdout, stderr := c.StdoutStderr()
+	if got, want := stdout, fmt.Sprintf("# script\n%s\n", jiriScript); got != want {
+		t.Errorf("stdout got %q, want %q", got, want)
+	}
+	if got, want := stderr, ""; got != want {
+		t.Errorf("stderr got %q, want %q", got, want)
+	}
+}