diff --git a/build/images/BUILD.gn b/build/images/BUILD.gn
index 7ef3758ba1c8ea5ad068872b14daa210031ae65c..997ccd86e004b4ae9ac3dc4f834e9a1ea126b709 100644
--- a/build/images/BUILD.gn
+++ b/build/images/BUILD.gn
@@ -193,6 +193,7 @@ group("package_lists") {
     ":available_packages.list",
     ":monolith_packages.list",
     ":preinstall_packages.list",
+    ":all_package_manifests.list",
   ]
 }
 
@@ -1444,19 +1445,17 @@ action("update.sources.manifest") {
   ]
 }
 
-# The amber index is the index of all requested packages, naming each meta.far
-# file instead of its merkleroot. Additionally the amber_index has the system
-# package itself, and the system update package.
-amber_index = "$target_out_dir/amber_index"
-
-package_metadata_list("amber_index") {
-  visibility = [ ":amber_publish_index" ]
+# This output is a manifest of manifests that is usable as an input to `pm
+# publish -lp`, a tool for publishing a set of packages from a build produced
+# list of package manifests.
+all_package_manifests_list = root_build_dir + "/all_package_manifests.list"
+package_metadata_list("all_package_manifests.list") {
   testonly = true
-
   outputs = [
-    amber_index,
+    all_package_manifests_list,
   ]
-  data_keys = [ "meta_far_index_entries" ]
+  data_keys = [ "package_output_manifests" ]
+  rebase = root_build_dir
   deps = [
     ":packages",
     ":system_image.meta",
@@ -1588,64 +1587,21 @@ compiled_action("system_snapshot") {
   ]
 }
 
-# Available blob manifest is a manifest of merkleroot=source_path for all blobs
-# in all packages produced by the build, including the system image and the
-# update package.
-available_blob_manifest = "$root_build_dir/available_blobs.manifest"
-
-collect_blob_manifest("available_blobs.manifest") {
-  visibility = [ ":*" ]
-  testonly = true
-  outputs = [
-    available_blob_manifest,
-  ]
-  deps = [
-    ":packages",
-    ":system_image.meta",
-    ":update.meta",
-  ]
-}
-
-# Populate the repository directory with content ID-named copies.
-action("amber_publish_blobs") {
-  testonly = true
-  outputs = [
-    "$amber_repository_dir.stamp",
-  ]
-  deps = [
-    ":available_blobs.manifest",
-  ]
-  inputs = []
-  foreach(dep, deps) {
-    inputs += get_target_outputs(dep)
-  }
-  script = "manifest.py"
-  args = [
-    "--copy-contentaddr",
-    "--output=" + rebase_path(amber_repository_blobs_dir),
-    "--stamp=" + rebase_path("$amber_repository_dir.stamp"),
-  ]
-  foreach(manifest, inputs) {
-    args += [ "--manifest=" + rebase_path(manifest, root_build_dir) ]
-  }
-}
-
-# Sign and publish the package index.
-pm_publish("amber_publish_index") {
+# publish all packages to the package repository.
+pm_publish("publish") {
   testonly = true
   deps = [
-    ":amber_index",
+    ":all_package_manifests.list",
   ]
   inputs = [
-    amber_index,
+    all_package_manifests_list,
   ]
 }
 
 group("updates") {
   testonly = true
   deps = [
-    ":amber_publish_blobs",
-    ":amber_publish_index",
+    ":publish",
     ":ids.txt",
     ":package_lists",
     ":system_snapshot",
diff --git a/build/package.gni b/build/package.gni
index 05c39c952af9daca7f56b0b74955d9772cc76fd7..7a2f5d98156fe6f852651fe5fc9a20c0ffb62184 100644
--- a/build/package.gni
+++ b/build/package.gni
@@ -50,6 +50,9 @@ declare_args() {
 #         The metafar merkle index entries are aggregated in image builds to
 #         produce package server indices for monolith serving.
 #
+#       package_output_manifests
+#         The path of each output manifest for each package.
+#
 #       package_barrier
 #         [list of labels] This is passed to walk_keys and can be used as a barrier to
 #         control dependency propgatation below a package target.
@@ -60,6 +63,7 @@ declare_args() {
 #   output_conversion (optional)
 #   outputs (optional)
 #   public_deps (optional)
+#   rebase (optional)
 #   testonly (optional)
 #   visibility (optional)
 #     Same as for any GN `generated_file()` target.
@@ -74,6 +78,7 @@ template("package_metadata_list") {
                              "output_conversion",
                              "outputs",
                              "public_deps",
+                             "rebase",
                              "testonly",
                              "visibility",
                            ])
@@ -186,6 +191,7 @@ template("pm_build_package") {
 
     depfile = "$pkg_out_dir/meta.far.d"
 
+    pkg_output_manifest = "$pkg_out_dir/package_manifest.json"
     outputs = [
       # produced by seal, must be listed first because of depfile rules.
       "$pkg_out_dir/meta.far",
@@ -205,6 +211,9 @@ template("pm_build_package") {
 
       # package blob manifest
       "$pkg_out_dir/blobs.manifest",
+
+      # package output manifest
+      pkg_output_manifest
     ]
 
     blobs_json_path = rebase_path("${pkg_out_dir}/blobs.json", root_build_dir)
@@ -237,6 +246,10 @@ template("pm_build_package") {
       meta_far_merkle_index_entries =
           [ "$package_name/$package_variant=" +
             rebase_path("$pkg_out_dir/meta.far.merkle", root_build_dir) ]
+
+      package_output_manifests = [
+        pkg_output_manifest,
+      ]
     }
 
     args = [
@@ -247,6 +260,8 @@ template("pm_build_package") {
       "-m",
       rebase_path(pkg_manifest_file, root_build_dir),
       "build",
+      "-output-package-manifest",
+      rebase_path(pkg_output_manifest, root_build_dir),
       "-depfile",
       "-blobsfile",
       "-blobs-manifest",
diff --git a/build/packages/prebuilt_package.gni b/build/packages/prebuilt_package.gni
index 6c654346a67ccaa6f7b222b65a66f5409b3baf43..821f590e01d6135efec001ddce81bd0f2a0929bf 100644
--- a/build/packages/prebuilt_package.gni
+++ b/build/packages/prebuilt_package.gni
@@ -26,6 +26,7 @@ template("prebuilt_package") {
 
   meta_dir = target_out_dir + "/" + pkg_name + ".meta"
   blobs_json = "$meta_dir/blobs.json"
+  package_manifest_json = "$meta_dir/package_manifest.json"
 
   pkg = {
     package_name = pkg_name
@@ -66,6 +67,7 @@ template("prebuilt_package") {
       blobs_manifest,
       system_rsp,
       blobs_json,
+      package_manifest_json,
       meta_merkle,
     ]
 
@@ -99,6 +101,10 @@ template("prebuilt_package") {
       meta_far_merkle_index_entries =
           [ "${pkg.package_name}/${pkg.package_version}=" +
             rebase_path("$meta_dir/meta.far.merkle", root_build_dir) ]
+
+      package_output_manifests = [
+        package_manifest_json,
+      ]
     }
   }
 
diff --git a/garnet/go/src/amber/daemon/daemon_test.go b/garnet/go/src/amber/daemon/daemon_test.go
index afc0ebaa48c5aa2044b303ec863be3a376bac188..dba8b163e0cc57913d0c0945b747183e4e16dda8 100644
--- a/garnet/go/src/amber/daemon/daemon_test.go
+++ b/garnet/go/src/amber/daemon/daemon_test.go
@@ -180,7 +180,7 @@ func TestDaemon(t *testing.T) {
 	mf, err := os.Open(store + "/" + pkgBlob)
 	panicerr(err)
 	defer mf.Close()
-	panicerr(repo.AddPackage("foo/0", mf))
+	panicerr(repo.AddPackage("foo/0", mf, ""))
 
 	for _, blob := range []string{pkgBlob, root1} {
 		b, err := os.Open(store + "/" + blob)
@@ -292,7 +292,7 @@ func TestOpenRepository(t *testing.T) {
 	mf, err := os.Open(store + "/" + pkgBlob)
 	panicerr(err)
 	defer mf.Close()
-	panicerr(repo.AddPackage("foo/0", mf))
+	panicerr(repo.AddPackage("foo/0", mf, ""))
 
 	for _, blob := range []string{pkgBlob, root1} {
 		b, err := os.Open(store + "/" + blob)
@@ -398,7 +398,7 @@ func TestDaemonWithEncryption(t *testing.T) {
 	mf, err := os.Open(store + "/" + pkgBlob)
 	panicerr(err)
 	defer mf.Close()
-	panicerr(repo.AddPackage("foo/0", mf))
+	panicerr(repo.AddPackage("foo/0", mf, ""))
 
 	for _, blob := range []string{pkgBlob, root1} {
 		b, err := os.Open(store + "/" + blob)
diff --git a/garnet/go/src/pm/build/config.go b/garnet/go/src/pm/build/config.go
index 81173f572c8b89613535fc1af0cb385557d5b879..7ce747ab00780b89167516e03995cc79c4fc6712 100644
--- a/garnet/go/src/pm/build/config.go
+++ b/garnet/go/src/pm/build/config.go
@@ -6,10 +6,12 @@ package build
 
 import (
 	"flag"
+	"fmt"
 	"io/ioutil"
 	"os"
 	"path/filepath"
 
+	"fuchsia.googlesource.com/pm/pkg"
 	"golang.org/x/crypto/ed25519"
 )
 
@@ -20,6 +22,7 @@ type Config struct {
 	KeyPath      string
 	TempDir      string
 	PkgName      string
+	PkgVersion   string
 
 	// the manifest is memoized lazily, on the first call to Manifest()
 	manifest *Manifest
@@ -33,6 +36,7 @@ func NewConfig() *Config {
 		KeyPath:      "",
 		TempDir:      os.TempDir(),
 		PkgName:      "",
+		PkgVersion:   "0",
 	}
 	return cfg
 }
@@ -51,6 +55,7 @@ func TestConfig() *Config {
 		KeyPath:      filepath.Join(d, "key"),
 		TempDir:      filepath.Join(d, "tmp"),
 		PkgName:      filepath.Join(d, "pkg"),
+		PkgVersion:   "0",
 	}
 	for _, d := range []string{cfg.OutputDir, cfg.TempDir} {
 		os.MkdirAll(d, os.ModePerm)
@@ -65,6 +70,7 @@ func (c *Config) InitFlags(fs *flag.FlagSet) {
 	fs.StringVar(&c.KeyPath, "k", c.KeyPath, "signing key")
 	fs.StringVar(&c.TempDir, "t", c.TempDir, "temporary directory")
 	fs.StringVar(&c.PkgName, "n", c.PkgName, "name of the packages")
+	fs.StringVar(&c.PkgVersion, "version", c.PkgVersion, "version of the packages")
 }
 
 // PrivateKey loads the configured private key
@@ -109,3 +115,23 @@ func (c *Config) MetaFAR() string {
 func (c *Config) MetaFARMerkle() string {
 	return filepath.Join(c.OutputDir, "meta.far.merkle")
 }
+
+func (c *Config) Package() (pkg.Package, error) {
+	p := pkg.Package{
+		Name:    c.PkgName,
+		Version: c.PkgVersion,
+	}
+
+	if p.Name == "" {
+		p.Name = filepath.Base(c.OutputDir)
+		if p.Name == "." {
+			var err error
+			p.Name, err = filepath.Abs(p.Name)
+			if err != nil {
+				return p, fmt.Errorf("build: unable to compute package name from directory: %s", err)
+			}
+			p.Name = filepath.Base(p.Name)
+		}
+	}
+	return p, nil
+}
diff --git a/garnet/go/src/pm/build/package.go b/garnet/go/src/pm/build/package.go
index a3f902e37764186d9f063826305e6613cf456cca..9210101a5316d4bc44ad53be3fb94af6c331f89a 100644
--- a/garnet/go/src/pm/build/package.go
+++ b/garnet/go/src/pm/build/package.go
@@ -23,21 +23,17 @@ import (
 	"fuchsia.googlesource.com/pm/pkg"
 )
 
+// PackageManifest is the json structure representation of a full package
+// manifest.
+type PackageManifest struct {
+	Version string            `json:"version"`
+	Package pkg.Package       `json:"package"`
+	Blobs   []PackageBlobInfo `json:"blobs"`
+}
+
 // Init initializes package metadata in the output directory. A manifest
 // is generated with a name matching the output directory name.
 func Init(cfg *Config) error {
-	pkgName := cfg.PkgName
-	if pkgName == "" {
-		pkgName = filepath.Base(cfg.OutputDir)
-		if pkgName == "." {
-			var err error
-			pkgName, err = filepath.Abs(pkgName)
-			if err != nil {
-				return fmt.Errorf("build: unable to compute package name from directory: %s", err)
-			}
-			pkgName = filepath.Base(pkgName)
-		}
-	}
 	metadir := filepath.Join(cfg.OutputDir, "meta")
 	if err := os.MkdirAll(metadir, os.ModePerm); err != nil {
 		return err
@@ -50,9 +46,9 @@ func Init(cfg *Config) error {
 			return err
 		}
 
-		p := pkg.Package{
-			Name:    pkgName,
-			Version: "0",
+		p, err := cfg.Package()
+		if err != nil {
+			return err
 		}
 
 		err = json.NewEncoder(f).Encode(&p)
diff --git a/garnet/go/src/pm/cmd/pm/build/build.go b/garnet/go/src/pm/cmd/pm/build/build.go
index 1664e5149650941658dea54b470da803b7818949..6629fcebca694af2bc788dc61e84bf964948faea 100644
--- a/garnet/go/src/pm/cmd/pm/build/build.go
+++ b/garnet/go/src/pm/cmd/pm/build/build.go
@@ -29,6 +29,7 @@ func Run(cfg *build.Config, args []string) error {
 	fs := flag.NewFlagSet("build", flag.ExitOnError)
 
 	var depfile = fs.Bool("depfile", true, "Produce a depfile")
+	var pkgManifestPath = fs.String("output-package-manifest", "", "If set, produce a package manifest at the given path")
 	var blobsfile = fs.Bool("blobsfile", false, "Produce blobs.json file")
 	var blobsmani = fs.Bool("blobs-manifest", false, "Produce blobs.manifest file")
 
@@ -68,34 +69,52 @@ func Run(cfg *build.Config, args []string) error {
 		}
 	}
 
+	if cfg.ManifestPath == "" {
+		return fmt.Errorf("the -blobsfile option requires the use of the -m manifest option")
+	}
+
+	blobs, err := buildPackageBlobInfo(cfg)
+	if err != nil {
+		return err
+	}
+
 	if *blobsfile {
-		if cfg.ManifestPath == "" {
-			return fmt.Errorf("the -blobsfile option requires the use of the -m manifest option")
+		content, err := json.Marshal(blobs)
+		if err != nil {
+			return err
+		}
+		if err := ioutil.WriteFile(filepath.Join(cfg.OutputDir, "blobs.json"), content, 0644); err != nil {
+			return err
 		}
+	}
+
+	if *blobsmani {
+		var buf bytes.Buffer
+		for _, blob := range blobs {
+			fmt.Fprintf(&buf, "%s=%s\n", blob.Merkle.String(), blob.SourcePath)
+		}
+		if err := ioutil.WriteFile(filepath.Join(cfg.OutputDir, "blobs.manifest"), buf.Bytes(), 0644); err != nil {
+			return err
+		}
+	}
 
-		blobs, err := buildPackageBlobInfo(cfg)
+	if *pkgManifestPath != "" {
+		p, err := cfg.Package()
 		if err != nil {
 			return err
 		}
+		pkgManifest := build.PackageManifest{
+			Version: "1",
+			Package: p,
+			Blobs:   blobs,
+		}
 
-		if *blobsfile {
-			content, err := json.Marshal(blobs)
-			if err != nil {
-				return err
-			}
-			if err := ioutil.WriteFile(filepath.Join(cfg.OutputDir, "blobs.json"), content, 0644); err != nil {
-				return err
-			}
-		}
-
-		if *blobsmani {
-			var buf bytes.Buffer
-			for _, blob := range blobs {
-				fmt.Fprintf(&buf, "%s=%s\n", blob.Merkle.String(), blob.SourcePath)
-			}
-			if err := ioutil.WriteFile(filepath.Join(cfg.OutputDir, "blobs.manifest"), buf.Bytes(), 0644); err != nil {
-				return err
-			}
+		content, err := json.Marshal(pkgManifest)
+		if err != nil {
+			return err
+		}
+		if err := ioutil.WriteFile(*pkgManifestPath, content, 0644); err != nil {
+			return err
 		}
 	}
 
diff --git a/garnet/go/src/pm/cmd/pm/expand/expand.go b/garnet/go/src/pm/cmd/pm/expand/expand.go
index d23b4129fb789dc48f0a974a3fd6f3a98ec62357..a733ca4bea8e266c141bef3bf5bf199dbca591f3 100644
--- a/garnet/go/src/pm/cmd/pm/expand/expand.go
+++ b/garnet/go/src/pm/cmd/pm/expand/expand.go
@@ -88,10 +88,11 @@ func merkleFor(b []byte) (build.MerkleRoot, error) {
 	return res, nil
 }
 
-// Extract the meta.far to the `outputDir`, and write out a package manifest
-// into `$outputDir/package.manifest`. for it. The format of the manifest is:
-//
+// Extract the meta.far to the `outputDir`, and write package manifests.
+// `$outputDir/package.manifest` contains:
 //     $PKG_NAME.$PKG_VERSION=$outputDir/meta.far
+// `package_manifest.json` contains a package output manifest as built by `pm
+// build -outut-package-manifest`.
 func writeMetadataAndManifest(pkgArchive *far.Reader, outputDir string) error {
 	// First, extract the package info from the archive, or error out if
 	// the meta.far is malformed.
@@ -164,6 +165,20 @@ func writeMetadataAndManifest(pkgArchive *far.Reader, outputDir string) error {
 	}
 	f.Close()
 
+	// Write out package_manifest.json
+	pkgManifest := build.PackageManifest{
+		Version: "1",
+		Package: *p,
+		Blobs:   blobs,
+	}
+	content, err := json.Marshal(pkgManifest)
+	if err != nil {
+		return err
+	}
+	if err := ioutil.WriteFile(filepath.Join(outputDir, "package_manifest.json"), content, 0644); err != nil {
+		return err
+	}
+
 	cwd, err := os.Getwd()
 	if err != nil {
 		return err
diff --git a/garnet/go/src/pm/cmd/pm/publish/publish.go b/garnet/go/src/pm/cmd/pm/publish/publish.go
index 8c62a2e4fe8b7e6eb382bc25cc79a8d277024aa3..7b603bf6afcb84182b9230bbf1adc4def1cfff1b 100644
--- a/garnet/go/src/pm/cmd/pm/publish/publish.go
+++ b/garnet/go/src/pm/cmd/pm/publish/publish.go
@@ -50,10 +50,11 @@ type manifestEntry struct {
 func Run(cfg *build.Config, args []string) error {
 	fs := flag.NewFlagSet("serve", flag.ExitOnError)
 
+	listOfPackageManifestsMode := fs.Bool("lp", false, "(mode) Publish a list of packages (and blobs) by package output manifest")
 	archiveMode := fs.Bool("a", false, "(mode) Publish an archived package.")
 	packageSetMode := fs.Bool("ps", false, "(mode) Publish a set of packages from a manifest.")
 	blobSetMode := fs.Bool("bs", false, "(mode) Publish a set of blobs from a manifest.")
-	modeFlags := []*bool{archiveMode, packageSetMode, blobSetMode}
+	modeFlags := []*bool{listOfPackageManifestsMode, archiveMode, packageSetMode, blobSetMode}
 
 	config := &repo.Config{}
 	config.Vars(fs)
@@ -150,6 +151,39 @@ func Run(cfg *build.Config, args []string) error {
 	}
 
 	switch {
+	case *listOfPackageManifestsMode:
+		if len(filePaths) != 1 {
+			return fmt.Errorf("too many file paths supplied")
+		}
+		deps = append(deps, filePaths[0])
+		f, err := os.Open(filePaths[0])
+		if err != nil {
+			return err
+		}
+		defer f.Close()
+
+		scanner := bufio.NewScanner(f)
+
+		for scanner.Scan() {
+			pkgManifestPath := scanner.Text()
+			deps = append(deps, pkgManifestPath)
+			if *verbose {
+				fmt.Printf("publishing: %s\n", pkgManifestPath)
+			}
+			if err := repo.PublishManifest(pkgManifestPath); err != nil {
+				return err
+			}
+		}
+		if err := scanner.Err(); err != nil {
+			return err
+		}
+
+		if *verbose {
+			fmt.Printf("committing updates\n")
+		}
+		if err := repo.CommitUpdates(config.TimeVersioned); err != nil {
+			log.Fatalf("error committing repository updates: %s", err)
+		}
 	case *archiveMode:
 		if len(filePaths) != 1 {
 			return fmt.Errorf("too many file paths supplied")
@@ -190,7 +224,7 @@ func Run(cfg *build.Config, args []string) error {
 		if *verbose {
 			fmt.Printf("adding package %s\n", name)
 		}
-		if err := repo.AddPackage(name, bytes.NewReader(b)); err != nil {
+		if err := repo.AddPackage(name, bytes.NewReader(b), ""); err != nil {
 			return err
 		}
 
@@ -223,7 +257,7 @@ func Run(cfg *build.Config, args []string) error {
 				return err
 			}
 			defer f.Close()
-			if err := repo.AddPackage(name, f); err != nil {
+			if err := repo.AddPackage(name, f, ""); err != nil {
 				return fmt.Errorf("failed to add package %q from %q: %s", name, src, err)
 			}
 			return nil
diff --git a/garnet/go/src/pm/pm.gni b/garnet/go/src/pm/pm.gni
index 0652032406bfa4f70868b79187e78707bcf0b8d4..7b9069d93f3da919bd645ab0723c547e8bf6a9cf 100644
--- a/garnet/go/src/pm/pm.gni
+++ b/garnet/go/src/pm/pm.gni
@@ -75,7 +75,7 @@ template("pm_publish") {
       "-C",
       "-r",
       rebase_path(amber_repository_dir, root_build_dir),
-      "-ps",
+      "-lp",
       "-f",
       rebase_path(inputs[0], root_build_dir),
       "-vt",
diff --git a/garnet/go/src/pm/repo/repo.go b/garnet/go/src/pm/repo/repo.go
index 5d8d180a75cf87eb40474f9d039a9d6b572bff06..dfa4938595ada6775b49dab4d8961139c2489c38 100644
--- a/garnet/go/src/pm/repo/repo.go
+++ b/garnet/go/src/pm/repo/repo.go
@@ -19,6 +19,7 @@ import (
 	"time"
 
 	"fuchsia.googlesource.com/merkle"
+	"fuchsia.googlesource.com/pm/build"
 
 	tuf "github.com/flynn/go-tuf"
 )
@@ -56,7 +57,20 @@ func New(path string) (*Repo, error) {
 	}
 
 	repo, err := tuf.NewRepo(tuf.FileSystemStore(path, passphrase), "sha512")
-	return &Repo{repo, path, nil}, err
+	if err != nil {
+		return nil, err
+	}
+	r := &Repo{repo, path, nil}
+
+	blobDir := filepath.Join(r.path, "repository", "blobs")
+	if err := os.MkdirAll(blobDir, os.ModePerm); err != nil {
+		return nil, err
+	}
+	if err := os.MkdirAll(r.stagedFilesPath(), os.ModePerm); err != nil {
+		return nil, err
+	}
+
+	return r, nil
 }
 
 func (r *Repo) EncryptWith(path string) error {
@@ -98,9 +112,10 @@ func (r *Repo) GenKeys() error {
 }
 
 // AddPackage adds a package with the given name with the content from the given
-// reader. The package blob is also added.
-func (r *Repo) AddPackage(name string, rd io.Reader) error {
-	root, size, err := r.AddBlob("", rd)
+// reader. The package blob is also added. If merkle is non-empty, it is used,
+// otherwise the package merkleroot is computed on the fly.
+func (r *Repo) AddPackage(name string, rd io.Reader, merkle string) error {
+	root, size, err := r.AddBlob(merkle, rd)
 	if err != nil {
 		return NewAddErr("adding package blob", err)
 	}
@@ -115,25 +130,12 @@ func (r *Repo) AddPackage(name string, rd io.Reader) error {
 		return NewAddErr(fmt.Sprintf("serializing %v", metadata), err)
 	}
 
-	// The staged package blob is copied from the path produced by AddBlob, as the
-	// AddBlob operation my have performed blob encryption.
-	dst, err := os.Create(stagingPath)
-	if err != nil {
-		return NewAddErr("creating file in staging directory", err)
-	}
 	blobDir := filepath.Join(r.path, "repository", "blobs")
-	src, err := os.Open(filepath.Join(blobDir, root))
-	if err != nil {
-		dst.Close()
-		return NewAddErr("reading blob", err)
-	}
-	if _, err := io.Copy(dst, src); err != nil {
-		dst.Close()
-		src.Close()
-		return NewAddErr("staging meta.far blob", err)
+	blobPath := filepath.Join(blobDir, root)
+
+	if err := linkOrCopy(blobPath, stagingPath); err != nil {
+		return NewAddErr("creating file in staging directory", err)
 	}
-	dst.Close()
-	src.Close()
 
 	// add file with custom JSON to repository
 	if err := r.AddTarget(name, json.RawMessage(jsonStr)); err != nil {
@@ -159,13 +161,20 @@ func cryptingWriter(dst io.Writer, key []byte) (io.WriteCloser, error) {
 	return cipher.StreamWriter{stream, dst, nil}, nil
 }
 
+// HasBlob returns true if the given merkleroot is already in the repository
+// blob store.
+func (r *Repo) HasBlob(root string) bool {
+	blobPath := filepath.Join(r.path, "repository", "blobs", root)
+	fi, err := os.Stat(blobPath)
+	return err == nil && fi.Mode().IsRegular()
+}
+
 // AddBlob writes the content of the given reader to the blob identified by the
 // given merkleroot. If merkleroot is empty string, a merkleroot is computed.
 // Addblob always returns the plaintext size of the blob that is added, even if
 // blob encryption is used.
 func (r *Repo) AddBlob(root string, rd io.Reader) (string, int64, error) {
 	blobDir := filepath.Join(r.path, "repository", "blobs")
-	os.MkdirAll(blobDir, os.ModePerm)
 
 	if root != "" {
 		dstPath := filepath.Join(blobDir, root)
@@ -255,6 +264,47 @@ func (r *Repo) CommitUpdates(dateVersioning bool) error {
 	return r.commitUpdates()
 }
 
+func (r *Repo) PublishManifest(path string) error {
+	f, err := os.Open(path)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	var packageManifest build.PackageManifest
+	if err := json.NewDecoder(f).Decode(&packageManifest); err != nil {
+		return err
+	}
+	if packageManifest.Version != "1" {
+		return fmt.Errorf("unknown version %q, can't publish", packageManifest.Version)
+	}
+
+	for _, blob := range packageManifest.Blobs {
+		if blob.Path == "meta/" {
+			p := packageManifest.Package
+			name := p.Name + "/" + p.Version
+			f, err := os.Open(blob.SourcePath)
+			if err != nil {
+				return err
+			}
+			err = r.AddPackage(name, f, blob.Merkle.String())
+			f.Close()
+		} else {
+			if !r.HasBlob(blob.Merkle.String()) {
+				f, err := os.Open(blob.SourcePath)
+				if err != nil {
+					return err
+				}
+				_, _, err = r.AddBlob(blob.Merkle.String(), f)
+				f.Close()
+			}
+		}
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func (r *Repo) commitUpdates() error {
 	if err := r.SnapshotWithExpires(tuf.CompressionTypeNone, time.Now().AddDate(0, 0, 30)); err != nil {
 		return NewAddErr("problem snapshotting repository", err)
@@ -289,3 +339,28 @@ func (r *Repo) fixupRootConsistentSnapshot() error {
 	}
 	return nil
 }
+
+func linkOrCopy(dstPath, srcPath string) error {
+	if err := os.Link(dstPath, srcPath); err != nil {
+		s, err := os.Open(srcPath)
+		if err != nil {
+			return err
+		}
+		defer s.Close()
+
+		d, err := ioutil.TempFile(filepath.Dir(dstPath), filepath.Base(dstPath))
+		if err != nil {
+			return err
+		}
+		if _, err := io.Copy(d, s); err != nil {
+			d.Close()
+			os.Remove(d.Name())
+			return err
+		}
+		if err := d.Close(); err != nil {
+			return err
+		}
+		return os.Rename(d.Name(), dstPath)
+	}
+	return nil
+}
diff --git a/garnet/go/src/pm/repo/repo_test.go b/garnet/go/src/pm/repo/repo_test.go
index 680e28f2b3c747e59f09561460162f8c22325e83..50caa329e617ab5bbba69331dfa699cc4872190f 100644
--- a/garnet/go/src/pm/repo/repo_test.go
+++ b/garnet/go/src/pm/repo/repo_test.go
@@ -92,7 +92,7 @@ func TestAddPackage(t *testing.T) {
 	}
 
 	targetName := "test-test"
-	err = r.AddPackage("test-test", io.LimitReader(rand.Reader, 8193))
+	err = r.AddPackage("test-test", io.LimitReader(rand.Reader, 8193), "")
 
 	if err != nil {
 		t.Fatalf("Problem adding repo file %v", err)