Merge "Soong: Add relativeFilePathFlag category in CMakeList generator"
diff --git a/android/androidmk.go b/android/androidmk.go
index 44c266a..5df4a85 100644
--- a/android/androidmk.go
+++ b/android/androidmk.go
@@ -25,6 +25,7 @@
 	"strings"
 
 	"github.com/google/blueprint"
+	"github.com/google/blueprint/bootstrap"
 )
 
 func init() {
@@ -64,13 +65,13 @@
 		return
 	}
 
-	var androidMkModulesList []Module
+	var androidMkModulesList []blueprint.Module
 
-	ctx.VisitAllModules(func(module Module) {
+	ctx.VisitAllModulesBlueprint(func(module blueprint.Module) {
 		androidMkModulesList = append(androidMkModulesList, module)
 	})
 
-	sort.Sort(AndroidModulesByName{androidMkModulesList, ctx})
+	sort.Sort(ModulesByName{androidMkModulesList, ctx})
 
 	transMk := PathForOutput(ctx, "Android"+String(ctx.Config().productVariables.Make_suffix)+".mk")
 	if ctx.Failed() {
@@ -88,7 +89,7 @@
 	})
 }
 
-func translateAndroidMk(ctx SingletonContext, mkFile string, mods []Module) error {
+func translateAndroidMk(ctx SingletonContext, mkFile string, mods []blueprint.Module) error {
 	buf := &bytes.Buffer{}
 
 	fmt.Fprintln(buf, "LOCAL_MODULE_MAKEFILE := $(lastword $(MAKEFILE_LIST))")
@@ -101,8 +102,8 @@
 			return err
 		}
 
-		if ctx.PrimaryModule(mod) == mod {
-			type_stats[ctx.ModuleType(mod)] += 1
+		if amod, ok := mod.(Module); ok && ctx.PrimaryModule(amod) == amod {
+			type_stats[ctx.ModuleType(amod)] += 1
 		}
 	}
 
@@ -148,10 +149,29 @@
 		}
 	}()
 
-	provider, ok := mod.(AndroidMkDataProvider)
-	if !ok {
+	switch x := mod.(type) {
+	case AndroidMkDataProvider:
+		return translateAndroidModule(ctx, w, mod, x)
+	case bootstrap.GoBinaryTool:
+		return translateGoBinaryModule(ctx, w, mod, x)
+	default:
 		return nil
 	}
+}
+
+func translateGoBinaryModule(ctx SingletonContext, w io.Writer, mod blueprint.Module,
+	goBinary bootstrap.GoBinaryTool) error {
+
+	name := ctx.ModuleName(mod)
+	fmt.Fprintln(w, ".PHONY:", name)
+	fmt.Fprintln(w, name+":", goBinary.InstallPath())
+	fmt.Fprintln(w, "")
+
+	return nil
+}
+
+func translateAndroidModule(ctx SingletonContext, w io.Writer, mod blueprint.Module,
+	provider AndroidMkDataProvider) error {
 
 	name := provider.BaseModuleName()
 	amod := mod.(Module).base()
diff --git a/android/module.go b/android/module.go
index 4dc4e9c..92b11ed 100644
--- a/android/module.go
+++ b/android/module.go
@@ -1527,16 +1527,16 @@
 	}
 }
 
-type AndroidModulesByName struct {
-	slice []Module
+type ModulesByName struct {
+	slice []blueprint.Module
 	ctx   interface {
 		ModuleName(blueprint.Module) string
 		ModuleSubDir(blueprint.Module) string
 	}
 }
 
-func (s AndroidModulesByName) Len() int { return len(s.slice) }
-func (s AndroidModulesByName) Less(i, j int) bool {
+func (s ModulesByName) Len() int { return len(s.slice) }
+func (s ModulesByName) Less(i, j int) bool {
 	mi, mj := s.slice[i], s.slice[j]
 	ni, nj := s.ctx.ModuleName(mi), s.ctx.ModuleName(mj)
 
@@ -1546,7 +1546,7 @@
 		return s.ctx.ModuleSubDir(mi) < s.ctx.ModuleSubDir(mj)
 	}
 }
-func (s AndroidModulesByName) Swap(i, j int) { s.slice[i], s.slice[j] = s.slice[j], s.slice[i] }
+func (s ModulesByName) Swap(i, j int) { s.slice[i], s.slice[j] = s.slice[j], s.slice[i] }
 
 // Collect information for opening IDE project files in java/jdeps.go.
 type IDEInfo interface {
diff --git a/android/singleton.go b/android/singleton.go
index fa1efdc..f926435 100644
--- a/android/singleton.go
+++ b/android/singleton.go
@@ -48,6 +48,7 @@
 	// are expanded in the scope of the PackageContext.
 	Eval(pctx PackageContext, ninjaStr string) (string, error)
 
+	VisitAllModulesBlueprint(visit func(blueprint.Module))
 	VisitAllModules(visit func(Module))
 	VisitAllModulesIf(pred func(Module) bool, visit func(Module))
 	// Deprecated: use WalkDeps instead to support multiple dependency tags on the same module
@@ -138,6 +139,10 @@
 	}
 }
 
+func (s singletonContextAdaptor) VisitAllModulesBlueprint(visit func(blueprint.Module)) {
+	s.SingletonContext.VisitAllModules(visit)
+}
+
 func (s singletonContextAdaptor) VisitAllModules(visit func(Module)) {
 	s.SingletonContext.VisitAllModules(visitAdaptor(visit))
 }
diff --git a/cc/config/clang.go b/cc/config/clang.go
index 186d790..5e22d6a 100644
--- a/cc/config/clang.go
+++ b/cc/config/clang.go
@@ -178,6 +178,18 @@
 		// Disable c++98-specific warning since Android is not concerned with C++98
 		// compatibility.
 		"-Wno-c++98-compat-extra-semi",
+
+		// Disable this warning until we can fix all instances where it fails.
+		"-Wno-self-assign-overloaded",
+
+		// Disable this warning until we can fix all instances where it fails.
+		"-Wno-constant-logical-operand",
+
+		// Disable this warning because we don't care about behavior with older compilers.
+		"-Wno-return-std-move-in-c++11",
+
+		// Disable this warning until we can fix all instances where it fails.
+		"-Wno-dangling-field",
 	}, " "))
 
 	// Extra cflags for projects under external/ directory to disable warnings that are infeasible
diff --git a/cc/config/global.go b/cc/config/global.go
index 000aab6..ad63c97 100644
--- a/cc/config/global.go
+++ b/cc/config/global.go
@@ -126,8 +126,8 @@
 
 	// prebuilts/clang default settings.
 	ClangDefaultBase         = "prebuilts/clang/host"
-	ClangDefaultVersion      = "clang-r328903"
-	ClangDefaultShortVersion = "7.0.2"
+	ClangDefaultVersion      = "clang-r339409"
+	ClangDefaultShortVersion = "8.0.1"
 
 	// Directories with warnings from Android.bp files.
 	WarningAllowedProjects = []string{
diff --git a/cc/util.go b/cc/util.go
index 93cf536..bab4d32 100644
--- a/cc/util.go
+++ b/cc/util.go
@@ -105,7 +105,7 @@
 	return list
 }
 
-var shlibVersionPattern = regexp.MustCompile("(?:\\.\\d+)+")
+var shlibVersionPattern = regexp.MustCompile("(?:\\.\\d+(?:svn)?)+")
 
 // splitFileExt splits a file name into root, suffix and ext. root stands for the file name without
 // the file extension and the version number (e.g. "libexample"). suffix stands for the
diff --git a/cc/util_test.go b/cc/util_test.go
index 3108294..7c718ea 100644
--- a/cc/util_test.go
+++ b/cc/util_test.go
@@ -35,6 +35,22 @@
 		}
 	})
 
+	t.Run("soname with svn version", func(t *testing.T) {
+		root, suffix, ext := splitFileExt("libtest.so.1svn")
+		expected := "libtest"
+		if root != expected {
+			t.Errorf("root should be %q but got %q", expected, root)
+		}
+		expected = ".so.1svn"
+		if suffix != expected {
+			t.Errorf("suffix should be %q but got %q", expected, suffix)
+		}
+		expected = ".so"
+		if ext != expected {
+			t.Errorf("ext should be %q but got %q", expected, ext)
+		}
+	})
+
 	t.Run("version numbers in the middle should be ignored", func(t *testing.T) {
 		root, suffix, ext := splitFileExt("libtest.1.0.30.so")
 		expected := "libtest.1.0.30"
diff --git a/cmd/merge_zips/merge_zips.go b/cmd/merge_zips/merge_zips.go
index 95ff70b..f383de9 100644
--- a/cmd/merge_zips/merge_zips.go
+++ b/cmd/merge_zips/merge_zips.go
@@ -250,7 +250,12 @@
 			addMapping(jar.MetaDir, dirSource)
 		}
 
-		fh, buf, err := jar.ManifestFileContents(manifest)
+		contents, err := ioutil.ReadFile(manifest)
+		if err != nil {
+			return err
+		}
+
+		fh, buf, err := jar.ManifestFileContents(contents)
 		if err != nil {
 			return err
 		}
diff --git a/cmd/multiproduct_kati/main.go b/cmd/multiproduct_kati/main.go
index 374868c..9cb75fa 100644
--- a/cmd/multiproduct_kati/main.go
+++ b/cmd/multiproduct_kati/main.go
@@ -328,7 +328,7 @@
 			NumParallelJobs:  runtime.NumCPU(),
 			CompressionLevel: 5,
 		}
-		if err := zip.Run(args); err != nil {
+		if err := zip.Zip(args); err != nil {
 			log.Fatalf("Error zipping logs: %v", err)
 		}
 	}
@@ -409,13 +409,13 @@
 				NumParallelJobs:  runtime.NumCPU(),
 				CompressionLevel: 5,
 			}
-			if err := zip.Run(args); err != nil {
+			if err := zip.Zip(args); err != nil {
 				log.Fatalf("Error zipping artifacts: %v", err)
 			}
 		}
 		if *incremental {
 			// Save space, Kati doesn't notice
-			if f := config.KatiNinjaFile(); f != "" {
+			if f := config.KatiBuildNinjaFile(); f != "" {
 				os.Truncate(f, 0)
 			}
 		} else {
@@ -436,7 +436,7 @@
 
 	// Save std_full.log if Kati re-read the makefiles
 	if buildWhat&build.BuildKati != 0 {
-		if after, err := os.Stat(config.KatiNinjaFile()); err == nil && after.ModTime().After(before) {
+		if after, err := os.Stat(config.KatiBuildNinjaFile()); err == nil && after.ModTime().After(before) {
 			err := copyFile(stdLog, filepath.Join(filepath.Dir(stdLog), "std_full.log"))
 			if err != nil {
 				log.Fatalf("Error copying log file: %s", err)
diff --git a/jar/jar.go b/jar/jar.go
index 653e5ee..fa0e693 100644
--- a/jar/jar.go
+++ b/jar/jar.go
@@ -17,7 +17,6 @@
 import (
 	"bytes"
 	"fmt"
-	"io/ioutil"
 	"os"
 	"strings"
 	"time"
@@ -81,10 +80,9 @@
 	return dirHeader
 }
 
-// Convert manifest source path to zip header and contents.  If path is empty uses a default
-// manifest.
-func ManifestFileContents(src string) (*zip.FileHeader, []byte, error) {
-	b, err := manifestContents(src)
+// Create a manifest zip header and contents using the provided contents if any.
+func ManifestFileContents(contents []byte) (*zip.FileHeader, []byte, error) {
+	b, err := manifestContents(contents)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -100,26 +98,16 @@
 	return fh, b, nil
 }
 
-// Convert manifest source path to contents.  If path is empty uses a default manifest.
-func manifestContents(src string) ([]byte, error) {
-	var givenBytes []byte
-	var err error
-
-	if src != "" {
-		givenBytes, err = ioutil.ReadFile(src)
-		if err != nil {
-			return nil, err
-		}
-	}
-
+// Create manifest contents, using the provided contents if any.
+func manifestContents(contents []byte) ([]byte, error) {
 	manifestMarker := []byte("Manifest-Version:")
 	header := append(manifestMarker, []byte(" 1.0\nCreated-By: soong_zip\n")...)
 
 	var finalBytes []byte
-	if !bytes.Contains(givenBytes, manifestMarker) {
-		finalBytes = append(append(header, givenBytes...), byte('\n'))
+	if !bytes.Contains(contents, manifestMarker) {
+		finalBytes = append(append(header, contents...), byte('\n'))
 	} else {
-		finalBytes = givenBytes
+		finalBytes = contents
 	}
 
 	return finalBytes, nil
diff --git a/java/dex.go b/java/dex.go
index c612a0c..03316f6 100644
--- a/java/dex.go
+++ b/java/dex.go
@@ -82,13 +82,17 @@
 	return flags
 }
 
-func (j *Module) d8Flags(ctx android.ModuleContext, flags javaBuilderFlags) []string {
+func (j *Module) d8Flags(ctx android.ModuleContext, flags javaBuilderFlags) ([]string, android.Paths) {
 	d8Flags := j.dexCommonFlags(ctx)
 
 	d8Flags = append(d8Flags, flags.bootClasspath.FormTurbineClasspath("--lib")...)
 	d8Flags = append(d8Flags, flags.classpath.FormTurbineClasspath("--lib")...)
 
-	return d8Flags
+	var d8Deps android.Paths
+	d8Deps = append(d8Deps, flags.bootClasspath...)
+	d8Deps = append(d8Deps, flags.classpath...)
+
+	return d8Flags, d8Deps
 }
 
 func (j *Module) r8Flags(ctx android.ModuleContext, flags javaBuilderFlags) (r8Flags []string, r8Deps android.Paths) {
@@ -113,6 +117,10 @@
 	r8Flags = append(r8Flags, flags.classpath.FormJavaClassPath("-libraryjars"))
 	r8Flags = append(r8Flags, "-forceprocessing")
 
+	r8Deps = append(r8Deps, proguardRaiseDeps...)
+	r8Deps = append(r8Deps, flags.bootClasspath...)
+	r8Deps = append(r8Deps, flags.classpath...)
+
 	flagFiles := android.Paths{
 		android.PathForSource(ctx, "build/make/core/proguard.flags"),
 	}
@@ -182,12 +190,13 @@
 			},
 		})
 	} else {
-		d8Flags := j.d8Flags(ctx, flags)
+		d8Flags, d8Deps := j.d8Flags(ctx, flags)
 		ctx.Build(pctx, android.BuildParams{
 			Rule:        d8,
 			Description: "d8",
 			Output:      javalibJar,
 			Input:       classesJar,
+			Implicits:   d8Deps,
 			Args: map[string]string{
 				"d8Flags": strings.Join(d8Flags, " "),
 				"outDir":  outDir.String(),
diff --git a/java/java.go b/java/java.go
index 0bd7857..7fd5344 100644
--- a/java/java.go
+++ b/java/java.go
@@ -95,6 +95,9 @@
 	// list of java libraries that will be compiled into the resulting jar
 	Static_libs []string `android:"arch_variant"`
 
+	// list of native libraries that will be provided in or alongside the resulting jar
+	Jni_libs []string `android:"arch_variant"`
+
 	// manifest file to be included in resulting jar
 	Manifest *string
 
diff --git a/scripts/strip.sh b/scripts/strip.sh
index bfc66ee..29594dc 100755
--- a/scripts/strip.sh
+++ b/scripts/strip.sh
@@ -54,7 +54,7 @@
     # ${CROSS_COMPILE}strip --strip-all does not strip .ARM.attributes,
     # so we tell llvm-strip to keep it too.
     if [ ! -z "${use_llvm_strip}" ]; then
-        "${CLANG_BIN}/llvm-strip" --strip-all -keep=.ARM.attributes "${infile}" "${outfile}.tmp"
+        "${CLANG_BIN}/llvm-strip" --strip-all -keep=.ARM.attributes "${infile}" -o "${outfile}.tmp"
     else
         "${CROSS_COMPILE}strip" --strip-all "${infile}" -o "${outfile}.tmp"
     fi
@@ -75,7 +75,7 @@
     rm -f "${outfile}.dynsyms" "${outfile}.funcsyms" "${outfile}.keep_symbols" "${outfile}.debug" "${outfile}.mini_debuginfo" "${outfile}.mini_debuginfo.xz"
     local fail=
     if [ ! -z "${use_llvm_strip}" ]; then
-        "${CLANG_BIN}/llvm-strip" --strip-all -keep=.ARM.attributes -remove-section=.comment "${infile}" "${outfile}.tmp" || fail=true
+        "${CLANG_BIN}/llvm-strip" --strip-all -keep=.ARM.attributes -remove-section=.comment "${infile}" -o "${outfile}.tmp" || fail=true
     else
         "${CROSS_COMPILE}strip" --strip-all -R .comment "${infile}" -o "${outfile}.tmp" || fail=true
     fi
diff --git a/ui/build/build.go b/ui/build/build.go
index 52ef005..377481b 100644
--- a/ui/build/build.go
+++ b/ui/build/build.go
@@ -37,7 +37,10 @@
 
 var combinedBuildNinjaTemplate = template.Must(template.New("combined").Parse(`
 builddir = {{.OutDir}}
-{{if .HasKatiSuffix}}include {{.KatiNinjaFile}}
+pool local_pool
+ depth = {{.Parallel}}
+build _kati_always_build_: phony
+{{if .HasKatiSuffix}}include {{.KatiBuildNinjaFile}}
 {{end -}}
 include {{.SoongNinjaFile}}
 `))
@@ -174,7 +177,9 @@
 
 	if what&BuildKati != 0 {
 		// Run ckati
-		runKati(ctx, config)
+		genKatiSuffix(ctx, config)
+		runKatiCleanSpec(ctx, config)
+		runKatiBuild(ctx, config)
 
 		ioutil.WriteFile(config.LastKatiSuffixFile(), []byte(config.KatiSuffix()), 0777)
 	} else {
diff --git a/ui/build/config.go b/ui/build/config.go
index fdeb8f2..d470b96 100644
--- a/ui/build/config.go
+++ b/ui/build/config.go
@@ -501,8 +501,8 @@
 	return filepath.Join(c.OutDir(), "env"+c.KatiSuffix()+".sh")
 }
 
-func (c *configImpl) KatiNinjaFile() string {
-	return filepath.Join(c.OutDir(), "build"+c.KatiSuffix()+".ninja")
+func (c *configImpl) KatiBuildNinjaFile() string {
+	return filepath.Join(c.OutDir(), "build"+c.KatiSuffix()+katiBuildSuffix+".ninja")
 }
 
 func (c *configImpl) SoongNinjaFile() string {
diff --git a/ui/build/kati.go b/ui/build/kati.go
index b26d673..546fd1a 100644
--- a/ui/build/kati.go
+++ b/ui/build/kati.go
@@ -19,7 +19,6 @@
 	"fmt"
 	"io/ioutil"
 	"path/filepath"
-	"strconv"
 	"strings"
 
 	"android/soong/ui/status"
@@ -27,6 +26,9 @@
 
 var spaceSlashReplacer = strings.NewReplacer("/", "_", " ", "_")
 
+const katiBuildSuffix = ""
+const katiCleanspecSuffix = "-cleanspec"
+
 // genKatiSuffix creates a suffix for kati-generated files so that we can cache
 // them based on their inputs. So this should encode all common changes to Kati
 // inputs. Currently that includes the TARGET_PRODUCT, kati-processed command
@@ -49,7 +51,7 @@
 		ctx.Verbosef("Kati ninja suffix too long: %q", katiSuffix)
 		ctx.Verbosef("Replacing with: %q", shortSuffix)
 
-		if err := ioutil.WriteFile(strings.TrimSuffix(config.KatiNinjaFile(), "ninja")+"suf", []byte(katiSuffix), 0777); err != nil {
+		if err := ioutil.WriteFile(strings.TrimSuffix(config.KatiBuildNinjaFile(), "ninja")+"suf", []byte(katiSuffix), 0777); err != nil {
 			ctx.Println("Error writing suffix file:", err)
 		}
 	} else {
@@ -57,30 +59,49 @@
 	}
 }
 
-func runKati(ctx Context, config Config) {
-	genKatiSuffix(ctx, config)
-
-	runKatiCleanSpec(ctx, config)
-
-	ctx.BeginTrace("kati")
-	defer ctx.EndTrace()
-
+func runKati(ctx Context, config Config, extraSuffix string, args []string) {
 	executable := config.PrebuiltBuildTool("ckati")
-	args := []string{
+	args = append([]string{
 		"--ninja",
 		"--ninja_dir=" + config.OutDir(),
-		"--ninja_suffix=" + config.KatiSuffix(),
+		"--ninja_suffix=" + config.KatiSuffix() + extraSuffix,
+		"--no_ninja_prelude",
 		"--regen",
 		"--ignore_optional_include=" + filepath.Join(config.OutDir(), "%.P"),
 		"--detect_android_echo",
 		"--color_warnings",
 		"--gen_all_targets",
+		"--use_find_emulator",
 		"--werror_find_emulator",
 		"--no_builtin_rules",
 		"--werror_suffix_rules",
 		"--warn_real_to_phony",
 		"--warn_phony_looks_real",
 		"--kati_stats",
+	}, args...)
+
+	args = append(args,
+		"SOONG_MAKEVARS_MK="+config.SoongMakeVarsMk(),
+		"TARGET_DEVICE_DIR="+config.TargetDeviceDir())
+
+	cmd := Command(ctx, config, "ckati", executable, args...)
+	cmd.Sandbox = katiSandbox
+	pipe, err := cmd.StdoutPipe()
+	if err != nil {
+		ctx.Fatalln("Error getting output pipe for ckati:", err)
+	}
+	cmd.Stderr = cmd.Stdout
+
+	cmd.StartOrFatal()
+	status.KatiReader(ctx.Status.StartTool(), pipe)
+	cmd.WaitOrFatal()
+}
+
+func runKatiBuild(ctx Context, config Config) {
+	ctx.BeginTrace("kati build")
+	defer ctx.EndTrace()
+
+	args := []string{
 		"--writable", config.OutDir() + "/",
 		"--writable", config.DistDir() + "/",
 		"-f", "build/make/core/main.mk",
@@ -102,67 +123,20 @@
 			"--werror_writable")
 	}
 
-	if !config.Environment().IsFalse("KATI_EMULATE_FIND") {
-		args = append(args, "--use_find_emulator")
-	}
-
 	args = append(args, config.KatiArgs()...)
 
-	args = append(args,
-		"BUILDING_WITH_NINJA=true",
-		"SOONG_ANDROID_MK="+config.SoongAndroidMk(),
-		"SOONG_MAKEVARS_MK="+config.SoongMakeVarsMk(),
-		"TARGET_DEVICE_DIR="+config.TargetDeviceDir())
+	args = append(args, "SOONG_ANDROID_MK="+config.SoongAndroidMk())
 
-	if config.UseGoma() {
-		args = append(args, "-j"+strconv.Itoa(config.Parallel()))
-	}
-
-	cmd := Command(ctx, config, "ckati", executable, args...)
-	cmd.Sandbox = katiSandbox
-	pipe, err := cmd.StdoutPipe()
-	if err != nil {
-		ctx.Fatalln("Error getting output pipe for ckati:", err)
-	}
-	cmd.Stderr = cmd.Stdout
-
-	cmd.StartOrFatal()
-	status.KatiReader(ctx.Status.StartTool(), pipe)
-	cmd.WaitOrFatal()
+	runKati(ctx, config, katiBuildSuffix, args)
 }
 
 func runKatiCleanSpec(ctx Context, config Config) {
 	ctx.BeginTrace("kati cleanspec")
 	defer ctx.EndTrace()
 
-	executable := config.PrebuiltBuildTool("ckati")
-	args := []string{
-		"--ninja",
-		"--ninja_dir=" + config.OutDir(),
-		"--ninja_suffix=" + config.KatiSuffix() + "-cleanspec",
-		"--regen",
-		"--detect_android_echo",
-		"--color_warnings",
-		"--gen_all_targets",
-		"--werror_find_emulator",
+	runKati(ctx, config, katiCleanspecSuffix, []string{
+		"--werror_implicit_rules",
 		"--werror_overriding_commands",
-		"--use_find_emulator",
-		"--kati_stats",
 		"-f", "build/make/core/cleanbuild.mk",
-		"BUILDING_WITH_NINJA=true",
-		"SOONG_MAKEVARS_MK=" + config.SoongMakeVarsMk(),
-		"TARGET_DEVICE_DIR=" + config.TargetDeviceDir(),
-	}
-
-	cmd := Command(ctx, config, "ckati", executable, args...)
-	cmd.Sandbox = katiCleanSpecSandbox
-	pipe, err := cmd.StdoutPipe()
-	if err != nil {
-		ctx.Fatalln("Error getting output pipe for ckati:", err)
-	}
-	cmd.Stderr = cmd.Stdout
-
-	cmd.StartOrFatal()
-	status.KatiReader(ctx.Status.StartTool(), pipe)
-	cmd.WaitOrFatal()
+	})
 }
diff --git a/zip/cmd/main.go b/zip/cmd/main.go
index dfd56dc..1125602 100644
--- a/zip/cmd/main.go
+++ b/zip/cmd/main.go
@@ -15,29 +15,19 @@
 package main
 
 import (
-	"bytes"
 	"flag"
 	"fmt"
-	"io"
 	"io/ioutil"
 	"os"
 	"runtime"
+	"runtime/pprof"
+	"runtime/trace"
 	"strconv"
 	"strings"
 
 	"android/soong/zip"
 )
 
-type byteReaderCloser struct {
-	*bytes.Reader
-	io.Closer
-}
-
-type pathMapping struct {
-	dest, src string
-	zipMethod uint16
-}
-
 type uniqueSet map[string]bool
 
 func (u *uniqueSet) String() string {
@@ -56,106 +46,67 @@
 
 type file struct{}
 
+func (file) String() string { return `""` }
+
+func (file) Set(s string) error {
+	fileArgsBuilder.File(s)
+	return nil
+}
+
 type listFiles struct{}
 
+func (listFiles) String() string { return `""` }
+
+func (listFiles) Set(s string) error {
+	fileArgsBuilder.List(s)
+	return nil
+}
+
 type dir struct{}
 
-func (f *file) String() string {
-	return `""`
-}
+func (dir) String() string { return `""` }
 
-func (f *file) Set(s string) error {
-	if relativeRoot == "" && !junkPaths {
-		return fmt.Errorf("must pass -C or -j before -f")
-	}
-
-	fArgs = append(fArgs, zip.FileArg{
-		PathPrefixInZip:     *rootPrefix,
-		SourcePrefixToStrip: relativeRoot,
-		JunkPaths:           junkPaths,
-		SourceFiles:         []string{s},
-	})
-
+func (dir) Set(s string) error {
+	fileArgsBuilder.Dir(s)
 	return nil
 }
 
-func (l *listFiles) String() string {
-	return `""`
-}
+type relativeRoot struct{}
 
-func (l *listFiles) Set(s string) error {
-	if relativeRoot == "" && !junkPaths {
-		return fmt.Errorf("must pass -C or -j before -l")
-	}
+func (relativeRoot) String() string { return "" }
 
-	list, err := ioutil.ReadFile(s)
-	if err != nil {
-		return err
-	}
-
-	fArgs = append(fArgs, zip.FileArg{
-		PathPrefixInZip:     *rootPrefix,
-		SourcePrefixToStrip: relativeRoot,
-		JunkPaths:           junkPaths,
-		SourceFiles:         strings.Split(string(list), "\n"),
-	})
-
+func (relativeRoot) Set(s string) error {
+	fileArgsBuilder.SourcePrefixToStrip(s)
 	return nil
 }
 
-func (d *dir) String() string {
-	return `""`
-}
+type junkPaths struct{}
 
-func (d *dir) Set(s string) error {
-	if relativeRoot == "" && !junkPaths {
-		return fmt.Errorf("must pass -C or -j before -D")
-	}
+func (junkPaths) IsBoolFlag() bool { return true }
+func (junkPaths) String() string   { return "" }
 
-	fArgs = append(fArgs, zip.FileArg{
-		PathPrefixInZip:     *rootPrefix,
-		SourcePrefixToStrip: relativeRoot,
-		JunkPaths:           junkPaths,
-		GlobDir:             s,
-	})
-
-	return nil
-}
-
-type relativeRootImpl struct{}
-
-func (*relativeRootImpl) String() string { return relativeRoot }
-
-func (*relativeRootImpl) Set(s string) error {
-	relativeRoot = s
-	junkPaths = false
-	return nil
-}
-
-type junkPathsImpl struct{}
-
-func (*junkPathsImpl) IsBoolFlag() bool { return true }
-
-func (*junkPathsImpl) String() string { return relativeRoot }
-
-func (*junkPathsImpl) Set(s string) error {
-	var err error
-	junkPaths, err = strconv.ParseBool(s)
-	relativeRoot = ""
+func (junkPaths) Set(s string) error {
+	v, err := strconv.ParseBool(s)
+	fileArgsBuilder.JunkPaths(v)
 	return err
 }
 
-var (
-	rootPrefix   *string
-	relativeRoot string
-	junkPaths    bool
+type rootPrefix struct{}
 
-	fArgs            zip.FileArgs
+func (rootPrefix) String() string { return "" }
+
+func (rootPrefix) Set(s string) error {
+	fileArgsBuilder.PathPrefixInZip(s)
+	return nil
+}
+
+var (
+	fileArgsBuilder  = zip.NewFileArgsBuilder()
 	nonDeflatedFiles = make(uniqueSet)
 )
 
 func usage() {
-	fmt.Fprintf(os.Stderr, "usage: zip -o zipfile [-m manifest] -C dir [-f|-l file]...\n")
+	fmt.Fprintf(os.Stderr, "usage: soong_zip -o zipfile [-m manifest] [-C dir] [-f|-l file] [-D dir]...\n")
 	flag.PrintDefaults()
 	os.Exit(2)
 }
@@ -177,11 +128,11 @@
 	}
 
 	flags := flag.NewFlagSet("flags", flag.ExitOnError)
+	flags.Usage = usage
 
 	out := flags.String("o", "", "file to write zip file to")
 	manifest := flags.String("m", "", "input jar manifest file name")
 	directories := flags.Bool("d", false, "include directories in zip")
-	rootPrefix = flags.String("P", "", "path prefix within the zip at which to place files")
 	compLevel := flags.Int("L", 5, "deflate compression level (0-9)")
 	emulateJar := flags.Bool("jar", false, "modify the resultant .zip to emulate the output of 'jar'")
 	writeIfChanged := flags.Bool("write_if_changed", false, "only update resultant .zip if it has changed")
@@ -190,20 +141,55 @@
 	cpuProfile := flags.String("cpuprofile", "", "write cpu profile to file")
 	traceFile := flags.String("trace", "", "write trace to file")
 
+	flags.Var(&rootPrefix{}, "P", "path prefix within the zip at which to place files")
 	flags.Var(&listFiles{}, "l", "file containing list of .class files")
 	flags.Var(&dir{}, "D", "directory to include in zip")
 	flags.Var(&file{}, "f", "file to include in zip")
 	flags.Var(&nonDeflatedFiles, "s", "file path to be stored within the zip without compression")
-	flags.Var(&relativeRootImpl{}, "C", "path to use as relative root of files in following -f, -l, or -D arguments")
-	flags.Var(&junkPathsImpl{}, "j", "junk paths, zip files without directory names")
+	flags.Var(&relativeRoot{}, "C", "path to use as relative root of files in following -f, -l, or -D arguments")
+	flags.Var(&junkPaths{}, "j", "junk paths, zip files without directory names")
 
 	flags.Parse(expandedArgs[1:])
 
-	err := zip.Run(zip.ZipArgs{
-		FileArgs:                 fArgs,
+	if flags.NArg() > 0 {
+		fmt.Fprintf(os.Stderr, "unexpected arguments %s\n", strings.Join(flags.Args(), " "))
+		flags.Usage()
+	}
+
+	if *cpuProfile != "" {
+		f, err := os.Create(*cpuProfile)
+		if err != nil {
+			fmt.Fprintln(os.Stderr, err.Error())
+			os.Exit(1)
+		}
+		defer f.Close()
+		pprof.StartCPUProfile(f)
+		defer pprof.StopCPUProfile()
+	}
+
+	if *traceFile != "" {
+		f, err := os.Create(*traceFile)
+		if err != nil {
+			fmt.Fprintln(os.Stderr, err.Error())
+			os.Exit(1)
+		}
+		defer f.Close()
+		err = trace.Start(f)
+		if err != nil {
+			fmt.Fprintln(os.Stderr, err.Error())
+			os.Exit(1)
+		}
+		defer trace.Stop()
+	}
+
+	if fileArgsBuilder.Error() != nil {
+		fmt.Fprintln(os.Stderr, fileArgsBuilder.Error())
+		os.Exit(1)
+	}
+
+	err := zip.Zip(zip.ZipArgs{
+		FileArgs:                 fileArgsBuilder.FileArgs(),
 		OutputFilePath:           *out,
-		CpuProfileFilePath:       *cpuProfile,
-		TraceFilePath:            *traceFile,
 		EmulateJar:               *emulateJar,
 		AddDirectoryEntriesToZip: *directories,
 		CompressionLevel:         *compLevel,
diff --git a/zip/zip.go b/zip/zip.go
index 6b36e10..e7de6f8 100644
--- a/zip/zip.go
+++ b/zip/zip.go
@@ -22,11 +22,8 @@
 	"hash/crc32"
 	"io"
 	"io/ioutil"
-	"log"
 	"os"
 	"path/filepath"
-	"runtime/pprof"
-	"runtime/trace"
 	"sort"
 	"strings"
 	"sync"
@@ -68,22 +65,6 @@
 	zipMethod uint16
 }
 
-type uniqueSet map[string]bool
-
-func (u *uniqueSet) String() string {
-	return `""`
-}
-
-func (u *uniqueSet) Set(s string) error {
-	if _, found := (*u)[s]; found {
-		return fmt.Errorf("File %q was specified twice as a file to not deflate", s)
-	} else {
-		(*u)[s] = true
-	}
-
-	return nil
-}
-
 type FileArg struct {
 	PathPrefixInZip, SourcePrefixToStrip string
 	SourceFiles                          []string
@@ -91,7 +72,96 @@
 	GlobDir                              string
 }
 
-type FileArgs []FileArg
+type FileArgsBuilder struct {
+	state FileArg
+	err   error
+	fs    pathtools.FileSystem
+
+	fileArgs []FileArg
+}
+
+func NewFileArgsBuilder() *FileArgsBuilder {
+	return &FileArgsBuilder{
+		fs: pathtools.OsFs,
+	}
+}
+
+func (b *FileArgsBuilder) JunkPaths(v bool) *FileArgsBuilder {
+	b.state.JunkPaths = v
+	b.state.SourcePrefixToStrip = ""
+	return b
+}
+
+func (b *FileArgsBuilder) SourcePrefixToStrip(prefixToStrip string) *FileArgsBuilder {
+	b.state.JunkPaths = false
+	b.state.SourcePrefixToStrip = prefixToStrip
+	return b
+}
+
+func (b *FileArgsBuilder) PathPrefixInZip(rootPrefix string) *FileArgsBuilder {
+	b.state.PathPrefixInZip = rootPrefix
+	return b
+}
+
+func (b *FileArgsBuilder) File(name string) *FileArgsBuilder {
+	if b.err != nil {
+		return b
+	}
+
+	arg := b.state
+	arg.SourceFiles = []string{name}
+	b.fileArgs = append(b.fileArgs, arg)
+	return b
+}
+
+func (b *FileArgsBuilder) Dir(name string) *FileArgsBuilder {
+	if b.err != nil {
+		return b
+	}
+
+	arg := b.state
+	arg.GlobDir = name
+	b.fileArgs = append(b.fileArgs, arg)
+	return b
+}
+
+func (b *FileArgsBuilder) List(name string) *FileArgsBuilder {
+	if b.err != nil {
+		return b
+	}
+
+	f, err := b.fs.Open(name)
+	if err != nil {
+		b.err = err
+		return b
+	}
+	defer f.Close()
+
+	list, err := ioutil.ReadAll(f)
+	if err != nil {
+		b.err = err
+		return b
+	}
+
+	arg := b.state
+	arg.SourceFiles = strings.Split(string(list), "\n")
+	b.fileArgs = append(b.fileArgs, arg)
+	return b
+}
+
+func (b *FileArgsBuilder) Error() error {
+	if b == nil {
+		return nil
+	}
+	return b.err
+}
+
+func (b *FileArgsBuilder) FileArgs() []FileArg {
+	if b == nil {
+		return nil
+	}
+	return b.fileArgs
+}
 
 type ZipWriter struct {
 	time         time.Time
@@ -107,6 +177,8 @@
 
 	compressorPool sync.Pool
 	compLevel      int
+
+	fs pathtools.FileSystem
 }
 
 type zipEntry struct {
@@ -121,10 +193,8 @@
 }
 
 type ZipArgs struct {
-	FileArgs                 FileArgs
+	FileArgs                 []FileArg
 	OutputFilePath           string
-	CpuProfileFilePath       string
-	TraceFilePath            string
 	EmulateJar               bool
 	AddDirectoryEntriesToZip bool
 	CompressionLevel         int
@@ -132,6 +202,7 @@
 	NumParallelJobs          int
 	NonDeflatedFiles         map[string]bool
 	WriteIfChanged           bool
+	Filesystem               pathtools.FileSystem
 }
 
 const NOQUOTE = '\x00'
@@ -177,48 +248,24 @@
 	return args
 }
 
-func Run(args ZipArgs) (err error) {
-	if args.CpuProfileFilePath != "" {
-		f, err := os.Create(args.CpuProfileFilePath)
-		if err != nil {
-			fmt.Fprintln(os.Stderr, err.Error())
-			os.Exit(1)
-		}
-		defer f.Close()
-		pprof.StartCPUProfile(f)
-		defer pprof.StopCPUProfile()
-	}
-
-	if args.TraceFilePath != "" {
-		f, err := os.Create(args.TraceFilePath)
-		if err != nil {
-			fmt.Fprintln(os.Stderr, err.Error())
-			os.Exit(1)
-		}
-		defer f.Close()
-		err = trace.Start(f)
-		if err != nil {
-			fmt.Fprintln(os.Stderr, err.Error())
-			os.Exit(1)
-		}
-		defer trace.Stop()
-	}
-
-	if args.OutputFilePath == "" {
-		return fmt.Errorf("output file path must be nonempty")
-	}
-
+func ZipTo(args ZipArgs, w io.Writer) error {
 	if args.EmulateJar {
 		args.AddDirectoryEntriesToZip = true
 	}
 
-	w := &ZipWriter{
+	z := &ZipWriter{
 		time:         jar.DefaultTime,
 		createdDirs:  make(map[string]string),
 		createdFiles: make(map[string]string),
 		directories:  args.AddDirectoryEntriesToZip,
 		compLevel:    args.CompressionLevel,
+		fs:           args.Filesystem,
 	}
+
+	if z.fs == nil {
+		z.fs = pathtools.OsFs
+	}
+
 	pathMappings := []pathMapping{}
 
 	noCompression := args.CompressionLevel == 0
@@ -231,11 +278,19 @@
 		for _, src := range srcs {
 			err := fillPathPairs(fa, src, &pathMappings, args.NonDeflatedFiles, noCompression)
 			if err != nil {
-				log.Fatal(err)
+				return err
 			}
 		}
 	}
 
+	return z.write(w, pathMappings, args.ManifestSourcePath, args.EmulateJar, args.NumParallelJobs)
+}
+
+func Zip(args ZipArgs) error {
+	if args.OutputFilePath == "" {
+		return fmt.Errorf("output file path must be nonempty")
+	}
+
 	buf := &bytes.Buffer{}
 	var out io.Writer = buf
 
@@ -255,7 +310,7 @@
 		out = f
 	}
 
-	err = w.write(out, pathMappings, args.ManifestSourcePath, args.EmulateJar, args.NumParallelJobs)
+	err := ZipTo(args, out)
 	if err != nil {
 		return err
 	}
@@ -308,13 +363,6 @@
 	sort.SliceStable(mappings, less)
 }
 
-type readerSeekerCloser interface {
-	io.Reader
-	io.ReaderAt
-	io.Closer
-	io.Seeker
-}
-
 func (z *ZipWriter) write(f io.Writer, pathMappings []pathMapping, manifest string, emulateJar bool, parallelJobs int) error {
 	z.errors = make(chan error)
 	defer close(z.errors)
@@ -461,7 +509,7 @@
 	var fileSize int64
 	var executable bool
 
-	if s, err := os.Lstat(src); err != nil {
+	if s, err := z.fs.Lstat(src); err != nil {
 		return err
 	} else if s.IsDir() {
 		if z.directories {
@@ -492,7 +540,7 @@
 		executable = s.Mode()&0100 != 0
 	}
 
-	r, err := os.Open(src)
+	r, err := z.fs.Open(src)
 	if err != nil {
 		return err
 	}
@@ -522,7 +570,21 @@
 		return err
 	}
 
-	fh, buf, err := jar.ManifestFileContents(src)
+	var contents []byte
+	if src != "" {
+		f, err := z.fs.Open(src)
+		if err != nil {
+			return err
+		}
+
+		contents, err = ioutil.ReadAll(f)
+		f.Close()
+		if err != nil {
+			return err
+		}
+	}
+
+	fh, buf, err := jar.ManifestFileContents(contents)
 	if err != nil {
 		return err
 	}
@@ -532,7 +594,7 @@
 	return z.writeFileContents(fh, reader)
 }
 
-func (z *ZipWriter) writeFileContents(header *zip.FileHeader, r readerSeekerCloser) (err error) {
+func (z *ZipWriter) writeFileContents(header *zip.FileHeader, r pathtools.ReaderAtSeekerCloser) (err error) {
 
 	header.SetModTime(z.time)
 
@@ -802,7 +864,7 @@
 	fileHeader.SetModTime(z.time)
 	fileHeader.SetMode(0777 | os.ModeSymlink)
 
-	dest, err := os.Readlink(file)
+	dest, err := z.fs.Readlink(file)
 	if err != nil {
 		return err
 	}
diff --git a/zip/zip_test.go b/zip/zip_test.go
index 03e7958..0c2105c 100644
--- a/zip/zip_test.go
+++ b/zip/zip_test.go
@@ -15,10 +15,395 @@
 package zip
 
 import (
+	"bytes"
+	"hash/crc32"
+	"io"
+	"os"
 	"reflect"
+	"syscall"
 	"testing"
+
+	"android/soong/third_party/zip"
+
+	"github.com/google/blueprint/pathtools"
 )
 
+var (
+	fileA        = []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
+	fileB        = []byte("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")
+	fileC        = []byte("CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC")
+	fileEmpty    = []byte("")
+	fileManifest = []byte("Manifest-Version: 1.0\nCreated-By: soong_zip\n\n")
+
+	fileCustomManifest  = []byte("Custom manifest: true\n")
+	customManifestAfter = []byte("Manifest-Version: 1.0\nCreated-By: soong_zip\nCustom manifest: true\n\n")
+)
+
+var mockFs = pathtools.MockFs(map[string][]byte{
+	"a/a/a":            fileA,
+	"a/a/b":            fileB,
+	"a/a/c -> ../../c": nil,
+	"a/a/d -> b":       nil,
+	"c":                fileC,
+	"l":                []byte("a/a/a\na/a/b\nc\n"),
+	"l2":               []byte("missing\n"),
+	"manifest.txt":     fileCustomManifest,
+})
+
+func fh(name string, contents []byte, method uint16) zip.FileHeader {
+	return zip.FileHeader{
+		Name:               name,
+		Method:             method,
+		CRC32:              crc32.ChecksumIEEE(contents),
+		UncompressedSize64: uint64(len(contents)),
+		ExternalAttrs:      0,
+	}
+}
+
+func fhManifest(contents []byte) zip.FileHeader {
+	return zip.FileHeader{
+		Name:               "META-INF/MANIFEST.MF",
+		Method:             zip.Store,
+		CRC32:              crc32.ChecksumIEEE(contents),
+		UncompressedSize64: uint64(len(contents)),
+		ExternalAttrs:      (syscall.S_IFREG | 0700) << 16,
+	}
+}
+
+func fhLink(name string, to string) zip.FileHeader {
+	return zip.FileHeader{
+		Name:               name,
+		Method:             zip.Store,
+		CRC32:              crc32.ChecksumIEEE([]byte(to)),
+		UncompressedSize64: uint64(len(to)),
+		ExternalAttrs:      (syscall.S_IFLNK | 0777) << 16,
+	}
+}
+
+func fhDir(name string) zip.FileHeader {
+	return zip.FileHeader{
+		Name:               name,
+		Method:             zip.Store,
+		CRC32:              crc32.ChecksumIEEE(nil),
+		UncompressedSize64: 0,
+		ExternalAttrs:      (syscall.S_IFDIR|0700)<<16 | 0x10,
+	}
+}
+
+func fileArgsBuilder() *FileArgsBuilder {
+	return &FileArgsBuilder{
+		fs: mockFs,
+	}
+}
+
+func TestZip(t *testing.T) {
+	testCases := []struct {
+		name             string
+		args             *FileArgsBuilder
+		compressionLevel int
+		emulateJar       bool
+		nonDeflatedFiles map[string]bool
+		dirEntries       bool
+		manifest         string
+
+		files []zip.FileHeader
+		err   error
+	}{
+		{
+			name: "empty args",
+			args: fileArgsBuilder(),
+
+			files: []zip.FileHeader{},
+		},
+		{
+			name: "files",
+			args: fileArgsBuilder().
+				File("a/a/a").
+				File("a/a/b").
+				File("c"),
+			compressionLevel: 9,
+
+			files: []zip.FileHeader{
+				fh("a/a/a", fileA, zip.Deflate),
+				fh("a/a/b", fileB, zip.Deflate),
+				fh("c", fileC, zip.Deflate),
+			},
+		},
+		{
+			name: "stored files",
+			args: fileArgsBuilder().
+				File("a/a/a").
+				File("a/a/b").
+				File("c"),
+			compressionLevel: 0,
+
+			files: []zip.FileHeader{
+				fh("a/a/a", fileA, zip.Store),
+				fh("a/a/b", fileB, zip.Store),
+				fh("c", fileC, zip.Store),
+			},
+		},
+		{
+			name: "symlinks in zip",
+			args: fileArgsBuilder().
+				File("a/a/a").
+				File("a/a/b").
+				File("a/a/c").
+				File("a/a/d"),
+			compressionLevel: 9,
+
+			files: []zip.FileHeader{
+				fh("a/a/a", fileA, zip.Deflate),
+				fh("a/a/b", fileB, zip.Deflate),
+				fhLink("a/a/c", "../../c"),
+				fhLink("a/a/d", "b"),
+			},
+		},
+		{
+			name: "list",
+			args: fileArgsBuilder().
+				List("l"),
+			compressionLevel: 9,
+
+			files: []zip.FileHeader{
+				fh("a/a/a", fileA, zip.Deflate),
+				fh("a/a/b", fileB, zip.Deflate),
+				fh("c", fileC, zip.Deflate),
+			},
+		},
+		{
+			name: "prefix in zip",
+			args: fileArgsBuilder().
+				PathPrefixInZip("foo").
+				File("a/a/a").
+				File("a/a/b").
+				File("c"),
+			compressionLevel: 9,
+
+			files: []zip.FileHeader{
+				fh("foo/a/a/a", fileA, zip.Deflate),
+				fh("foo/a/a/b", fileB, zip.Deflate),
+				fh("foo/c", fileC, zip.Deflate),
+			},
+		},
+		{
+			name: "relative root",
+			args: fileArgsBuilder().
+				SourcePrefixToStrip("a").
+				File("a/a/a").
+				File("a/a/b"),
+			compressionLevel: 9,
+
+			files: []zip.FileHeader{
+				fh("a/a", fileA, zip.Deflate),
+				fh("a/b", fileB, zip.Deflate),
+			},
+		},
+		{
+			name: "multiple relative root",
+			args: fileArgsBuilder().
+				SourcePrefixToStrip("a").
+				File("a/a/a").
+				SourcePrefixToStrip("a/a").
+				File("a/a/b"),
+			compressionLevel: 9,
+
+			files: []zip.FileHeader{
+				fh("a/a", fileA, zip.Deflate),
+				fh("b", fileB, zip.Deflate),
+			},
+		},
+		{
+			name: "emulate jar",
+			args: fileArgsBuilder().
+				File("a/a/a").
+				File("a/a/b"),
+			compressionLevel: 9,
+			emulateJar:       true,
+
+			files: []zip.FileHeader{
+				fhDir("META-INF/"),
+				fhManifest(fileManifest),
+				fhDir("a/"),
+				fhDir("a/a/"),
+				fh("a/a/a", fileA, zip.Deflate),
+				fh("a/a/b", fileB, zip.Deflate),
+			},
+		},
+		{
+			name: "emulate jar with manifest",
+			args: fileArgsBuilder().
+				File("a/a/a").
+				File("a/a/b"),
+			compressionLevel: 9,
+			emulateJar:       true,
+			manifest:         "manifest.txt",
+
+			files: []zip.FileHeader{
+				fhDir("META-INF/"),
+				fhManifest(customManifestAfter),
+				fhDir("a/"),
+				fhDir("a/a/"),
+				fh("a/a/a", fileA, zip.Deflate),
+				fh("a/a/b", fileB, zip.Deflate),
+			},
+		},
+		{
+			name: "dir entries",
+			args: fileArgsBuilder().
+				File("a/a/a").
+				File("a/a/b"),
+			compressionLevel: 9,
+			dirEntries:       true,
+
+			files: []zip.FileHeader{
+				fhDir("a/"),
+				fhDir("a/a/"),
+				fh("a/a/a", fileA, zip.Deflate),
+				fh("a/a/b", fileB, zip.Deflate),
+			},
+		},
+		{
+			name: "junk paths",
+			args: fileArgsBuilder().
+				JunkPaths(true).
+				File("a/a/a").
+				File("a/a/b"),
+			compressionLevel: 9,
+
+			files: []zip.FileHeader{
+				fh("a", fileA, zip.Deflate),
+				fh("b", fileB, zip.Deflate),
+			},
+		},
+		{
+			name: "non deflated files",
+			args: fileArgsBuilder().
+				File("a/a/a").
+				File("a/a/b"),
+			compressionLevel: 9,
+			nonDeflatedFiles: map[string]bool{"a/a/a": true},
+
+			files: []zip.FileHeader{
+				fh("a/a/a", fileA, zip.Store),
+				fh("a/a/b", fileB, zip.Deflate),
+			},
+		},
+
+		// errors
+		{
+			name: "error missing file",
+			args: fileArgsBuilder().
+				File("missing"),
+			err: os.ErrNotExist,
+		},
+		{
+			name: "error missing file in list",
+			args: fileArgsBuilder().
+				List("l2"),
+			err: os.ErrNotExist,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			if test.args.Error() != nil {
+				t.Fatal(test.args.Error())
+			}
+
+			args := ZipArgs{}
+			args.FileArgs = test.args.FileArgs()
+			args.CompressionLevel = test.compressionLevel
+			args.EmulateJar = test.emulateJar
+			args.AddDirectoryEntriesToZip = test.dirEntries
+			args.NonDeflatedFiles = test.nonDeflatedFiles
+			args.ManifestSourcePath = test.manifest
+			args.Filesystem = mockFs
+
+			buf := &bytes.Buffer{}
+			err := ZipTo(args, buf)
+
+			if (err != nil) != (test.err != nil) {
+				t.Fatalf("want error %v, got %v", test.err, err)
+			} else if test.err != nil {
+				if os.IsNotExist(test.err) {
+					if !os.IsNotExist(test.err) {
+						t.Fatalf("want error %v, got %v", test.err, err)
+					}
+				} else {
+					t.Fatalf("want error %v, got %v", test.err, err)
+				}
+				return
+			}
+
+			br := bytes.NewReader(buf.Bytes())
+			zr, err := zip.NewReader(br, int64(br.Len()))
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			var files []zip.FileHeader
+			for _, f := range zr.File {
+				r, err := f.Open()
+				if err != nil {
+					t.Fatalf("error when opening %s: %s", f.Name, err)
+				}
+
+				crc := crc32.NewIEEE()
+				len, err := io.Copy(crc, r)
+				r.Close()
+				if err != nil {
+					t.Fatalf("error when reading %s: %s", f.Name, err)
+				}
+
+				if uint64(len) != f.UncompressedSize64 {
+					t.Errorf("incorrect length for %s, want %d got %d", f.Name, f.UncompressedSize64, len)
+				}
+
+				if crc.Sum32() != f.CRC32 {
+					t.Errorf("incorrect crc for %s, want %x got %x", f.Name, f.CRC32, crc)
+				}
+
+				files = append(files, f.FileHeader)
+			}
+
+			if len(files) != len(test.files) {
+				t.Fatalf("want %d files, got %d", len(test.files), len(files))
+			}
+
+			for i := range files {
+				want := test.files[i]
+				got := files[i]
+
+				if want.Name != got.Name {
+					t.Errorf("incorrect file %d want %q got %q", i, want.Name, got.Name)
+					continue
+				}
+
+				if want.UncompressedSize64 != got.UncompressedSize64 {
+					t.Errorf("incorrect file %s length want %v got %v", want.Name,
+						want.UncompressedSize64, got.UncompressedSize64)
+				}
+
+				if want.ExternalAttrs != got.ExternalAttrs {
+					t.Errorf("incorrect file %s attrs want %x got %x", want.Name,
+						want.ExternalAttrs, got.ExternalAttrs)
+				}
+
+				if want.CRC32 != got.CRC32 {
+					t.Errorf("incorrect file %s crc want %v got %v", want.Name,
+						want.CRC32, got.CRC32)
+				}
+
+				if want.Method != got.Method {
+					t.Errorf("incorrect file %s method want %v got %v", want.Name,
+						want.Method, got.Method)
+				}
+			}
+		})
+	}
+}
+
 func TestReadRespFile(t *testing.T) {
 	testCases := []struct {
 		name, in string