release-request-38d4765e-6779-4912-a79f-25f77b8184a7-for-git_pi-release-4288638 snap-temp-L71800000095143664

Change-Id: Ib90ff39e13ba63cf8f326b06c9d5d619473bf4e7
diff --git a/bundletool/java/com/android/tools/appbundle/bundletool/AppBundle.java b/bundletool/java/com/android/tools/appbundle/bundletool/AppBundle.java
new file mode 100644
index 0000000..cbdf4e1
--- /dev/null
+++ b/bundletool/java/com/android/tools/appbundle/bundletool/AppBundle.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tools.appbundle.bundletool;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/** Represents an app bundle. */
+public class AppBundle {
+
+  private ZipFile bundleFile;
+  private Map<String, BundleModule> modules;
+
+  public AppBundle(ZipFile bundleFile) {
+    this.bundleFile = bundleFile;
+    this.modules = new HashMap<>();
+    open();
+  }
+
+  private void open() {
+    Map<String, BundleModule.Builder> moduleBuilders = new HashMap<>();
+    Enumeration<? extends ZipEntry> entries = bundleFile.entries();
+    while (entries.hasMoreElements()) {
+      ZipEntry entry = entries.nextElement();
+      Path path = Paths.get(entry.getName());
+      if (path.getNameCount() > 1) {
+        String moduleName = path.getName(0).toString();
+        BundleModule.Builder moduleBuilder =
+            moduleBuilders.computeIfAbsent(
+                moduleName, name -> new BundleModule.Builder(name, this));
+        moduleBuilder.addZipEntry(entry);
+      }
+    }
+    modules.putAll(Maps.transformValues(moduleBuilders, BundleModule.Builder::build));
+  }
+
+  public Map<String, BundleModule> getModules() {
+    return ImmutableMap.copyOf(modules);
+  }
+
+  public BundleModule getModule(String moduleName) {
+    return modules.get(moduleName);
+  }
+
+  public InputStream getEntryInputStream(ZipEntry entry) throws IOException {
+    return bundleFile.getInputStream(entry);
+  }
+}
diff --git a/bundletool/java/com/android/tools/appbundle/bundletool/BuildModuleCommand.java b/bundletool/java/com/android/tools/appbundle/bundletool/BuildModuleCommand.java
new file mode 100644
index 0000000..8e30d9a
--- /dev/null
+++ b/bundletool/java/com/android/tools/appbundle/bundletool/BuildModuleCommand.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tools.appbundle.bundletool;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.android.tools.appbundle.bundletool.utils.FlagParser;
+import com.google.auto.value.AutoValue;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Optional;
+
+/** Command responsible for building an App Bundle module. */
+@AutoValue
+public abstract class BuildModuleCommand {
+
+  public static final String COMMAND_NAME = "build-module";
+
+  private static final String OUTPUT_FLAG = "output";
+  private static final String MANIFEST_FLAG = "manifest";
+  private static final String MANIFEST_DIR_FLAG = "manifest-dir";
+  private static final String DEX_FLAG = "dex";
+  private static final String DEX_DIR_FLAG = "dex-dir";
+  private static final String RESOURCES_DIR_FLAG = "resources-dir";
+  private static final String ASSETS_DIR_FLAG = "assets-dir";
+  private static final String NATIVE_DIR_FLAG = "native-dir";
+
+  abstract Path getOutputPath();
+
+  abstract Optional<Path> getManifestPath();
+
+  abstract Optional<Path> getManifestDirPath();
+
+  abstract Optional<Path> getDexPath();
+
+  abstract Optional<Path> getDexDirPath();
+
+  abstract Optional<Path> getResourcesDirPath();
+
+  abstract Optional<Path> getAssetsDirPath();
+
+  abstract Optional<Path> getNativeDirPath();
+
+  public static Builder builder() {
+    return new AutoValue_BuildModuleCommand.Builder();
+  }
+
+  /** Builder for the {@link BuildModuleCommand} */
+  @AutoValue.Builder
+  public abstract static class Builder {
+    abstract Builder setOutputPath(Path outputPath);
+
+    abstract Builder setManifestPath(Path manifestPath);
+
+    abstract Builder setManifestDirPath(Path manifestDirPath);
+
+    abstract Builder setDexPath(Path dexPath);
+
+    abstract Builder setDexDirPath(Path dexDirPath);
+
+    abstract Builder setResourcesDirPath(Path resourcesDirPath);
+
+    abstract Builder setAssetsDirPath(Path assetsDirPath);
+
+    abstract Builder setNativeDirPath(Path nativeDirPath);
+
+    abstract BuildModuleCommand build();
+  }
+
+  static BuildModuleCommand fromFlags(FlagParser flagParser) {
+    Builder builder =
+        builder().setOutputPath(Paths.get(flagParser.getRequiredFlagValue(OUTPUT_FLAG)));
+    flagParser.getFlagValueAsPath(MANIFEST_FLAG).ifPresent(builder::setManifestPath);
+    flagParser.getFlagValueAsPath(MANIFEST_DIR_FLAG).ifPresent(builder::setManifestDirPath);
+    flagParser.getFlagValueAsPath(DEX_FLAG).ifPresent(builder::setDexPath);
+    flagParser.getFlagValueAsPath(DEX_DIR_FLAG).ifPresent(builder::setDexDirPath);
+    flagParser.getFlagValueAsPath(RESOURCES_DIR_FLAG).ifPresent(builder::setResourcesDirPath);
+    flagParser.getFlagValueAsPath(ASSETS_DIR_FLAG).ifPresent(builder::setAssetsDirPath);
+    flagParser.getFlagValueAsPath(NATIVE_DIR_FLAG).ifPresent(builder::setNativeDirPath);
+
+    return builder.build();
+  }
+
+  public void execute() {
+    validateInput();
+
+
+  }
+
+  private void validateInput() {
+    checkArgument(
+        getManifestPath().isPresent() || getManifestDirPath().isPresent(),
+        "One of --%s or --%s is required.",
+        MANIFEST_FLAG,
+        MANIFEST_DIR_FLAG);
+    checkArgument(
+        !getManifestPath().isPresent() || !getManifestDirPath().isPresent(),
+        "Cannot set both --%s and --%s flags.",
+        MANIFEST_FLAG,
+        MANIFEST_DIR_FLAG);
+    checkArgument(
+        !getDexPath().isPresent() || !getDexDirPath().isPresent(),
+        "Cannot set both --%s and --%s flags.",
+        DEX_FLAG,
+        DEX_DIR_FLAG);
+
+    checkArgument(!Files.exists(getOutputPath()), "File %s already exists.", getOutputPath());
+    checkFileExistsAndReadable(getManifestPath());
+    checkDirectoryExists(getManifestDirPath());
+    checkFileExistsAndReadable(getDexPath());
+    checkDirectoryExists(getDexDirPath());
+    checkDirectoryExists(getResourcesDirPath());
+    checkDirectoryExists(getAssetsDirPath());
+    checkDirectoryExists(getNativeDirPath());
+  }
+
+  private static void checkFileExistsAndReadable(Optional<Path> pathOptional) {
+    if (pathOptional.isPresent()) {
+      Path path = pathOptional.get();
+      checkArgument(Files.exists(path), "File '%s' was not found.", path);
+      checkArgument(Files.isReadable(path), "File '%s' is not readable.", path);
+    }
+  }
+
+  private static void checkDirectoryExists(Optional<Path> pathOptional) {
+    if (pathOptional.isPresent()) {
+      Path path = pathOptional.get();
+      checkArgument(Files.exists(path), "Directory '%s' was not found.", path);
+      checkArgument(Files.isDirectory(path), "'%s' is not a directory.");
+    }
+  }
+
+  public static void help() {
+    System.out.println(
+        String.format(
+            "bundletool %s --output=<path/to/module.zip> "
+                + "[--%s=<path/to/AndroidManifest.flat>|--%s=<path/to/manifest-dir/>] "
+                + "[--%s=<path/to/classes.dex>|--%s=<path/to/dex-dir/>] "
+                + "[--%s=<path/to/res/>] "
+                + "[--%s=<path/to/assets/>] "
+                + "[--%s=<path/to/lib/>] ",
+            COMMAND_NAME,
+            MANIFEST_FLAG,
+            MANIFEST_DIR_FLAG,
+            DEX_FLAG,
+            DEX_DIR_FLAG,
+            RESOURCES_DIR_FLAG,
+            ASSETS_DIR_FLAG,
+            NATIVE_DIR_FLAG));
+    System.out.println();
+    System.out.println(
+        "Builds a module as a zip from an app's project. Note that the resources and the "
+            + "AndroidManifest.xml must already have been compiled with aapt2.");
+    System.out.println();
+    System.out.println("--output: Path to the zip file to build.");
+    System.out.printf(
+        "--%s: Path to the AndroidManifest.flat compiled by aapt2. Use --%s if there "
+            + "are more than one.\n",
+        MANIFEST_FLAG, MANIFEST_DIR_FLAG);
+    System.out.printf(
+        "--%s: Path to the directory containing multiple Android manifests compiled by aapt2. "
+            + "A file named 'manifest-targeting.xml' must be present in the directory "
+            + "describing the targeting of each manifest present.\n",
+        MANIFEST_DIR_FLAG);
+    System.out.printf(
+        "--%s: Path to the dex file. Use --%s if there are more than one.\n",
+        DEX_FLAG, DEX_DIR_FLAG);
+    System.out.printf(
+        "--%s: Path to the directory containing multiple dex files. Unless all dex files must "
+            + "be included in the generated APKs (for MultiDex), a file named "
+            + "'dex-targeting.xml' must be present in the directory describing the targeting "
+            + "of the different dex files.\n",
+        DEX_DIR_FLAG);
+    System.out.printf(
+        "--%s: Path to the directory containing the resources file(s). A file named "
+            + "'resources.flat' must be present in that directory corresponding to the output "
+            + "of the aapt2 compilation of the resources.\n",
+        RESOURCES_DIR_FLAG);
+    System.out.printf("--%s: Path to the directory containing the assets.\n", ASSETS_DIR_FLAG);
+    System.out.printf(
+        "--%s: Path to the directory containing the native libraries.\n", NATIVE_DIR_FLAG);
+  }
+}
diff --git a/bundletool/java/com/android/tools/appbundle/bundletool/BundleModule.java b/bundletool/java/com/android/tools/appbundle/bundletool/BundleModule.java
new file mode 100644
index 0000000..c88d287
--- /dev/null
+++ b/bundletool/java/com/android/tools/appbundle/bundletool/BundleModule.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tools.appbundle.bundletool;
+
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.ZipEntry;
+
+/** Represents a single module inside App Bundle. */
+public class BundleModule {
+
+  private AppBundle parent;
+  private String name;
+  private List<ZipEntry> entries;
+
+  private BundleModule(String name, AppBundle parent, List<ZipEntry> entries) {
+    this.parent = parent;
+    this.name = name;
+    this.entries = entries;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public AppBundle getParent() {
+    return parent;
+  }
+
+  public List<ZipEntry> getEntries() {
+    return ImmutableList.copyOf(entries);
+  }
+
+  /** Builder for BundleModule. */
+  public static class Builder {
+    private List<ZipEntry> entries;
+    private String name;
+    private AppBundle parent;
+
+    public Builder(String name, AppBundle parent) {
+      this.name = name;
+      this.parent = parent;
+      this.entries = new ArrayList<>();
+    }
+
+    public Builder addZipEntry(ZipEntry entry) {
+      this.entries.add(entry);
+      return this;
+    }
+
+    public BundleModule build() {
+      return new BundleModule(name, parent, entries);
+    }
+  }
+}
diff --git a/bundletool/java/com/android/tools/appbundle/bundletool/BundleToolMain.java b/bundletool/java/com/android/tools/appbundle/bundletool/BundleToolMain.java
new file mode 100644
index 0000000..330a991
--- /dev/null
+++ b/bundletool/java/com/android/tools/appbundle/bundletool/BundleToolMain.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tools.appbundle.bundletool;
+
+import com.android.tools.appbundle.bundletool.utils.FlagParser;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Main entry point of the bundle tool.
+ *
+ * <p>Consider running with -Dsun.zip.disableMemoryMapping when dealing with large bundles.
+ */
+public class BundleToolMain {
+
+  public static final String LINK_CMD = "link";
+  public static final String HELP_CMD = "help";
+
+  /** Parses the flags and routes to the appropriate command handler */
+  public static void main(String[] args) throws IOException {
+
+    FlagParser flagParser = new FlagParser();
+    try {
+      flagParser.parse(args);
+    } catch (FlagParser.ParseException e) {
+      System.out.println(String.format("Error while parsing the flags: %s", e.getMessage()));
+      return;
+    }
+    List<String> commands = flagParser.getCommands();
+
+    if (commands.isEmpty()) {
+      System.out.println("Error: you have to specify a command");
+      help();
+      return;
+    }
+
+    try {
+      switch (commands.get(0)) {
+        case BuildModuleCommand.COMMAND_NAME:
+          BuildModuleCommand.fromFlags(flagParser).execute();
+          break;
+        case SplitModuleCommand.COMMAND_NAME:
+          new SplitModuleCommand(flagParser).execute();
+          break;
+        case LINK_CMD:
+          throw new UnsupportedOperationException("Not implemented.");
+        case HELP_CMD:
+          if (commands.size() > 1) {
+            help(commands.get(1));
+          } else {
+            help();
+          }
+          return;
+        default:
+          System.out.println("Error: unrecognized command.");
+          help();
+      }
+    } catch (Exception e) {
+      System.out.println("Error: " + e.getMessage());
+    }
+  }
+
+  /** Displays a general help. */
+  public static void help() {
+    System.out.println(
+        String.format(
+            "bundletool [%s|%s|%s|%s] ...",
+            BuildModuleCommand.COMMAND_NAME, SplitModuleCommand.COMMAND_NAME, LINK_CMD, HELP_CMD));
+    System.out.println("Type: bundletool help [command] to learn more about a given command.");
+  }
+
+  /** Displays help about a given command. */
+  public static void help(String commandName) {
+    switch (commandName) {
+      case BuildModuleCommand.COMMAND_NAME:
+        BuildModuleCommand.help();
+        break;
+      case SplitModuleCommand.COMMAND_NAME:
+        SplitModuleCommand.help();
+        break;
+      case LINK_CMD:
+        System.out.println("Help is not yet available.");
+        break;
+      default:
+        System.out.println("Unrecognized command.");
+        help();
+        break;
+    }
+  }
+}
diff --git a/bundletool/java/com/android/tools/appbundle/bundletool/Command.java b/bundletool/java/com/android/tools/appbundle/bundletool/Command.java
new file mode 100644
index 0000000..670c0dc
--- /dev/null
+++ b/bundletool/java/com/android/tools/appbundle/bundletool/Command.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tools.appbundle.bundletool;
+
+/** Interface for command implementations. */
+public interface Command {
+
+  void execute() throws ExecutionException;
+
+  /** Error indicating something went wrong during executing the command. */
+  class ExecutionException extends RuntimeException {
+
+    public ExecutionException(String message) {
+      super(message);
+    }
+
+    public ExecutionException(String message, Throwable cause) {
+      super(message, cause);
+    }
+  }
+}
diff --git a/bundletool/java/com/android/tools/appbundle/bundletool/SplitModuleCommand.java b/bundletool/java/com/android/tools/appbundle/bundletool/SplitModuleCommand.java
new file mode 100644
index 0000000..f6909af
--- /dev/null
+++ b/bundletool/java/com/android/tools/appbundle/bundletool/SplitModuleCommand.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tools.appbundle.bundletool;
+
+import com.android.tools.appbundle.bundletool.utils.FlagParser;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+
+/** Implementation of the command to generate module splits. */
+public class SplitModuleCommand implements Command {
+
+  private final String bundleLocation;
+  private final String outputDirectory;
+  private final String moduleName;
+
+  public static final String COMMAND_NAME = "split-module";
+
+  private static final String BUNDLE_LOCATION_FLAG = "bundle";
+  private static final String OUTPUT_DIRECTORY_FLAG = "output";
+  private static final String MODULE_FLAG = "module";
+
+  public SplitModuleCommand(FlagParser parsedFlags) {
+    bundleLocation = parsedFlags.getRequiredFlagValue(BUNDLE_LOCATION_FLAG);
+    outputDirectory = parsedFlags.getRequiredFlagValue(OUTPUT_DIRECTORY_FLAG);
+    moduleName = parsedFlags.getRequiredFlagValue(MODULE_FLAG);
+  }
+
+  @Override
+  public void execute() throws ExecutionException {
+    try {
+      AppBundle appBundle = new AppBundle(new ZipFile(bundleLocation));
+      BundleModule module = appBundle.getModule(moduleName);
+      if (module == null) {
+        throw new ExecutionException(
+            String.format("Cannot find the %s module in the bundle", moduleName));
+      }
+      splitModule(moduleName, module, outputDirectory);
+    } catch (ZipException e) {
+      throw new ExecutionException("Zip error while opening the bundle " + e.getMessage(), e);
+    } catch (FileNotFoundException e) {
+      throw new ExecutionException("Bundle file not found", e);
+    } catch (IOException e) {
+      throw new ExecutionException("I/O error while processing the bundle " + e.getMessage(), e);
+    }
+  }
+
+  private void splitModule(String moduleName, BundleModule module, String outputDirectory) {
+    throw new UnsupportedOperationException("Not implemented");
+  }
+
+  public static void help() {
+    System.out.printf(
+        "bundletool %s --%s=[bundle.zip] --%s=[module-name] --%s=[output-dir]\n",
+        BUNDLE_LOCATION_FLAG, MODULE_FLAG, OUTPUT_DIRECTORY_FLAG, COMMAND_NAME);
+    System.out.println("Generates module splits for the given module of the bundle.");
+    System.out.println("For now, one split is generated containing all module's resources.");
+    System.out.println();
+    System.out.printf("--%s: the zip file containing an App Bundle.\n", BUNDLE_LOCATION_FLAG);
+    System.out.printf("--%s: module for which generate the splits.\n", MODULE_FLAG);
+    System.out.printf(
+        "--%s: the directory where the module zip files should be written to.\n",
+        OUTPUT_DIRECTORY_FLAG);
+  }
+}
diff --git a/bundletool/java/com/android/tools/appbundle/bundletool/utils/FlagParser.java b/bundletool/java/com/android/tools/appbundle/bundletool/utils/FlagParser.java
new file mode 100644
index 0000000..5f42165
--- /dev/null
+++ b/bundletool/java/com/android/tools/appbundle/bundletool/utils/FlagParser.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tools.appbundle.bundletool.utils;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Utility for flag parsing, specific to the Bundle Tool.
+ *
+ * <p>The flags follow the below convention:
+ *
+ * <p>[bundle-tool] [command1] [command2] .. [command-n] [--flag1] [--flag2=v2].. [--flagn] where:
+ *
+ * <ul>
+ *   <li>commands: cannot start with "-".
+ *   <li>flags: have to start with "--". If they have "=" anything after the first occurrence is
+ *       considered a flag value. By default the flag value is an empty string.
+ * </ul>
+ */
+public class FlagParser {
+
+  private List<String> commands = new ArrayList<>();
+  private Map<String, String> flags = new HashMap<>();
+
+  /**
+   * Parses the given arguments populating the structures.
+   *
+   * <p>Calling this function removes any previous parsing results.
+   */
+  public FlagParser parse(String[] args) throws ParseException {
+    this.commands.clear();
+    // Need to wrap it into a proper list implementation to be able to remove elements.
+    List<String> argsToProcess = new ArrayList<>(Arrays.asList(args));
+    while (argsToProcess.size() > 0 && !argsToProcess.get(0).startsWith("-")) {
+      commands.add(argsToProcess.get(0));
+      argsToProcess.remove(0);
+    }
+    this.flags = parseFlags(argsToProcess);
+    return this;
+  }
+
+  private Map<String, String> parseFlags(List<String> args) throws ParseException {
+    Map<String, String> flagMap = new HashMap<>();
+    for (String arg : args) {
+      if (!arg.startsWith("--")) {
+        throw new ParseException(
+            String.format("Syntax error: flags should start with -- (%s)", arg));
+      }
+      String[] segments = arg.substring(2).split("=", 2);
+      String value = "";
+      if (segments.length == 2) {
+        value = segments[1];
+      }
+      if (flagMap.putIfAbsent(segments[0], value) != null) {
+        throw new ParseException(
+            String.format("Flag %s has been set more than once.", segments[0]));
+      }
+    }
+    return flagMap;
+  }
+
+  /** Returns true if a given flag has been set. */
+  public boolean isFlagSet(String flagName) {
+    return flags.containsKey(flagName);
+  }
+
+  /** Returns the flag value wrapped in the Optional class. */
+  public Optional<String> getFlagValue(String flagName) {
+    return Optional.ofNullable(flags.get(flagName));
+  }
+
+  /**
+   * Returns a flag value. If absent throws IllegalStateException.
+   *
+   * @param flagName name of the flag to fetch
+   * @return string, the value of the flag
+   * @throws IllegalStateException if the flag was not set.
+   */
+  public String getRequiredFlagValue(String flagName) {
+    return getFlagValue(flagName)
+        .orElseThrow(
+            () ->
+                new IllegalArgumentException(
+                    String.format("Missing the required --%s flag.", flagName)));
+  }
+
+  /** Returns the string value of the flag or the default if has not been set. */
+  public String getFlagValueOrDefault(String flagName, String defaultValue) {
+    return flags.getOrDefault(flagName, defaultValue);
+  }
+
+  public Optional<Path> getFlagValueAsPath(String flagName) {
+    return Optional.ofNullable(flags.get(flagName)).map(Paths::get);
+  }
+
+  /**
+   * Returns the value of the flag as list of strings.
+   *
+   * <p>It converts the string flag value to the list assuming it's delimited by a comma. The list
+   * is empty if the flag has not been set.
+   */
+  public List<String> getFlagListValue(String flagName) {
+    if (!isFlagSet(flagName)) {
+      return Collections.emptyList();
+    }
+    return Arrays.asList(flags.get(flagName).split(","));
+  }
+
+  /**
+   * Returns the list of commands that were parsed.
+   *
+   * @return the immutable list of commands.
+   */
+  public List<String> getCommands() {
+    return Collections.unmodifiableList(commands);
+  }
+
+  /** Exception encapsulating any flag parsing errors. */
+  public static class ParseException extends RuntimeException {
+
+    public ParseException(String message) {
+      super(message);
+    }
+  }
+}
diff --git a/bundletool/javatests/com/android/tools/appbundle/bundletool/AllTests.java b/bundletool/javatests/com/android/tools/appbundle/bundletool/AllTests.java
new file mode 100644
index 0000000..acfc60b
--- /dev/null
+++ b/bundletool/javatests/com/android/tools/appbundle/bundletool/AllTests.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tools.appbundle.bundletool;
+
+import org.junit.runner.JUnitCore;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+  AppBundleTest.class,
+  BuildModuleCommandTest.class,
+  BundleToolTest.class,
+  SplitModuleCommandTest.class,
+})
+public class AllTests {
+
+  public static void main(String[] args) throws Exception {
+    JUnitCore.main("com.android.tools.appbundle.bundletool.AllTests");
+  }
+}
diff --git a/bundletool/javatests/com/android/tools/appbundle/bundletool/AppBundleTest.java b/bundletool/javatests/com/android/tools/appbundle/bundletool/AppBundleTest.java
new file mode 100644
index 0000000..d5a352b
--- /dev/null
+++ b/bundletool/javatests/com/android/tools/appbundle/bundletool/AppBundleTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tools.appbundle.bundletool;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import java.util.Vector;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for the AppBundle class. */
+@RunWith(JUnit4.class)
+public class AppBundleTest {
+
+  private ZipFile bundleFile;
+  private Vector<ZipEntry> entries = new Vector<>();
+
+  @Before
+  public void setUp() {
+    bundleFile = mock(ZipFile.class);
+  }
+
+  @Test
+  public void testSingleModuleBundle() {
+    putEntry("/module1/classes.dex");
+    doReturn(entries.elements()).when(bundleFile).entries();
+
+    AppBundle appBundle = new AppBundle(bundleFile);
+    assertThat(appBundle.getModules().keySet()).containsExactly("module1");
+  }
+
+  @Test
+  public void testNoModulesWhenFilesAtRoot() {
+    putEntry("/deliverables.pb");
+    putEntry("variants.pb");
+    doReturn(entries.elements()).when(bundleFile).entries();
+
+    AppBundle appBundle = new AppBundle(bundleFile);
+    assertThat(appBundle.getModules().keySet()).isEmpty();
+  }
+
+  @Test
+  public void testMultipleModules() {
+    putEntry("base/AndroidManifest.flat");
+    putEntry("base/Format.flat");
+    putEntry("base/classes.dex");
+    putEntry("base/assets/textures.etc1");
+    putEntry("base/res/drawable-hdpi/title.jpg");
+    putEntry("detail/AndroidManifest.flat");
+    putEntry("detail/Format.flat");
+    doReturn(entries.elements()).when(bundleFile).entries();
+
+    AppBundle appBundle = new AppBundle(bundleFile);
+    assertThat(appBundle.getModules().keySet()).containsExactly("base", "detail");
+  }
+
+  private void putEntry(String fakeFile) {
+    entries.add(new ZipEntry(fakeFile));
+  }
+}
diff --git a/bundletool/javatests/com/android/tools/appbundle/bundletool/BuildModuleCommandTest.java b/bundletool/javatests/com/android/tools/appbundle/bundletool/BuildModuleCommandTest.java
new file mode 100644
index 0000000..7f6b107
--- /dev/null
+++ b/bundletool/javatests/com/android/tools/appbundle/bundletool/BuildModuleCommandTest.java
@@ -0,0 +1,471 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.tools.appbundle.bundletool;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.expectThrows;
+
+import com.android.tools.appbundle.bundletool.utils.FlagParser;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class BuildModuleCommandTest {
+
+  @Rule public TemporaryFolder tmp = new TemporaryFolder();
+
+  private Path outputPath;
+  private Path manifestPath;
+  private Path manifestDirPath;
+  private Path dexPath;
+  private Path dexDirPath;
+  private Path resourcesDirPath;
+  private Path assetsDirPath;
+  private Path nativeDirPath;
+
+  private Path pathThatDoesNotExist;
+
+  @Before
+  public void setUp() throws IOException {
+    outputPath = Paths.get(tmp.getRoot().getPath(), "bundle");
+    manifestPath = tmp.newFile("AndroidManifest.flat").toPath();
+    manifestDirPath = tmp.newFolder("manifest").toPath();
+    dexPath = tmp.newFile("classes.dex").toPath();
+    dexDirPath = tmp.newFolder("dex").toPath();
+    resourcesDirPath = tmp.newFolder("resources").toPath();
+    assetsDirPath = tmp.newFolder("assets").toPath();
+    nativeDirPath = tmp.newFolder("native").toPath();
+
+    pathThatDoesNotExist = Paths.get(tmp.getRoot().getPath(), "path-that-does-not-exist");
+  }
+
+  @Test
+  public void validConfig_viaFlags() {
+    FlagParser flagParser =
+        new FlagParser()
+            .parse(
+                new String[] {
+                  "--output=" + outputPath,
+                  "--manifest=" + manifestPath,
+                  "--dexPath=" + dexPath,
+                  "--resourcesDirPath=" + resourcesDirPath,
+                  "--assetsDirPath=" + assetsDirPath,
+                  "--nativeDirPath=" + nativeDirPath,
+                });
+    BuildModuleCommand.fromFlags(flagParser).execute();
+  }
+
+  @Test
+  public void validConfig_viaBuilder() {
+    BuildModuleCommand.builder()
+        .setOutputPath(outputPath)
+        .setManifestPath(manifestPath)
+        .setDexPath(dexPath)
+        .setResourcesDirPath(resourcesDirPath)
+        .setAssetsDirPath(assetsDirPath)
+        .setNativeDirPath(nativeDirPath)
+        .build()
+        .execute();
+  }
+
+  @Test
+  public void outputPathNotSetThrowsException_viaFlags() {
+    FlagParser flagParser = new FlagParser().parse(new String[] {"--manifest=" + manifestPath});
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () -> BuildModuleCommand.fromFlags(flagParser).execute());
+
+    assertThat(exception).hasMessageThat().contains("--output");
+  }
+
+  @Test
+  public void outputPathNotSetThrowsException_viaBuilder() {
+    assertThrows(
+        IllegalStateException.class,
+        () -> BuildModuleCommand.builder().setManifestPath(manifestPath).build().execute());
+  }
+
+  @Test
+  public void manifestAndManifestDirNotSetThrowsException_viaFlags() {
+    FlagParser flagParser = new FlagParser().parse(new String[] {"--output=" + outputPath});
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () -> BuildModuleCommand.fromFlags(flagParser).execute());
+
+    assertThat(exception).hasMessageThat().contains("--manifest");
+  }
+
+  @Test
+  public void manifestAndManifestDirNotSetThrowsException_viaBuilder() {
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () -> BuildModuleCommand.builder().setOutputPath(outputPath).build().execute());
+
+    assertThat(exception).hasMessageThat().contains("--manifest");
+  }
+
+  @Test
+  public void manifestAndManifestDirBothSetThrowsException_viaFlags() {
+    FlagParser flagParser =
+        new FlagParser()
+            .parse(
+                new String[] {
+                  "--output=" + outputPath,
+                  "--manifest=" + manifestPath,
+                  "--manifest-dir=" + manifestDirPath
+                });
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () -> BuildModuleCommand.fromFlags(flagParser).execute());
+
+    assertThat(exception).hasMessageThat().contains("--manifest");
+  }
+
+  @Test
+  public void manifestAndManifestDirBothSetThrowsException_viaBuilder() {
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () ->
+                BuildModuleCommand.builder()
+                    .setOutputPath(outputPath)
+                    .setManifestPath(manifestPath)
+                    .setManifestDirPath(manifestDirPath)
+                    .build()
+                    .execute());
+
+    assertThat(exception).hasMessageThat().contains("--manifest");
+  }
+
+  @Test
+  public void dexAndDexDirBothSetThrowsException_viaFlags() {
+    FlagParser flagParser =
+        new FlagParser()
+            .parse(
+                new String[] {
+                  "--output=" + outputPath,
+                  "--manifest=" + manifestPath,
+                  "--dex=" + dexPath,
+                  "--dex-dir=" + dexDirPath
+                });
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () -> BuildModuleCommand.fromFlags(flagParser).execute());
+
+    assertThat(exception).hasMessageThat().contains("--dex");
+  }
+
+  @Test
+  public void dexAndDexDirBothSetThrowsException_viaBuilder() {
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () ->
+                BuildModuleCommand.builder()
+                    .setOutputPath(outputPath)
+                    .setManifestPath(manifestPath)
+                    .setDexPath(dexPath)
+                    .setDexDirPath(dexDirPath)
+                    .build()
+                    .execute());
+
+    assertThat(exception).hasMessageThat().contains("--dex");
+  }
+
+  @Test
+  public void outputExistsThrowsException_viaFlags() throws IOException {
+    outputPath = tmp.newFile("bundle").toPath();
+    FlagParser flagParser =
+        new FlagParser()
+            .parse(
+                new String[] {
+                  "--output=" + outputPath, "--manifest=" + manifestPath, "--dex=" + dexPath
+                });
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () -> BuildModuleCommand.fromFlags(flagParser).execute());
+
+    assertThat(exception).hasMessageThat().contains("already exists");
+  }
+
+  @Test
+  public void outputExistsThrowsException_viaBuilder() throws IOException {
+    outputPath = tmp.newFile("bundle").toPath();
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () ->
+                BuildModuleCommand.builder()
+                    .setOutputPath(outputPath)
+                    .setManifestPath(manifestPath)
+                    .setDexPath(dexPath)
+                    .build()
+                    .execute());
+
+    assertThat(exception).hasMessageThat().contains("already exists");
+  }
+
+  @Test
+  public void manifestDoesNotExistThrowsException_viaFlags() throws IOException {
+    FlagParser flagParser =
+        new FlagParser()
+            .parse(
+                new String[] {
+                  "--output=" + outputPath, "--manifest=" + pathThatDoesNotExist, "--dex=" + dexPath
+                });
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () -> BuildModuleCommand.fromFlags(flagParser).execute());
+
+    assertThat(exception).hasMessageThat().containsMatch("File '.*' was not found");
+  }
+
+  @Test
+  public void manifestDoesNotExistThrowsException_viaBuilder() throws IOException {
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () ->
+                BuildModuleCommand.builder()
+                    .setOutputPath(outputPath)
+                    .setManifestPath(pathThatDoesNotExist)
+                    .setDexPath(dexPath)
+                    .build()
+                    .execute());
+
+    assertThat(exception).hasMessageThat().containsMatch("File '.*' was not found");
+  }
+
+  @Test
+  public void manifestDirDoesNotExistThrowsException_viaFlags() throws IOException {
+    FlagParser flagParser =
+        new FlagParser()
+            .parse(
+                new String[] {
+                  "--output=" + outputPath,
+                  "--manifest-dir=" + pathThatDoesNotExist,
+                  "--dex=" + dexPath
+                });
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () -> BuildModuleCommand.fromFlags(flagParser).execute());
+
+    assertThat(exception).hasMessageThat().containsMatch("Directory '.*' was not found");
+  }
+
+  @Test
+  public void manifestDirDoesNotExistThrowsException_viaBuilder() throws IOException {
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () ->
+                BuildModuleCommand.builder()
+                    .setOutputPath(outputPath)
+                    .setManifestDirPath(pathThatDoesNotExist)
+                    .setDexPath(dexPath)
+                    .build()
+                    .execute());
+
+    assertThat(exception).hasMessageThat().containsMatch("Directory '.*' was not found");
+  }
+
+  @Test
+  public void dexDoesNotExistThrowsException_viaFlags() throws IOException {
+    FlagParser flagParser =
+        new FlagParser()
+            .parse(
+                new String[] {
+                  "--output=" + outputPath,
+                  "--manifest=" + manifestPath,
+                  "--dex=" + pathThatDoesNotExist
+                });
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () -> BuildModuleCommand.fromFlags(flagParser).execute());
+
+    assertThat(exception).hasMessageThat().containsMatch("File '.*' was not found");
+  }
+
+  @Test
+  public void dexDoesNotExistThrowsException_viaBuilder() throws IOException {
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () ->
+                BuildModuleCommand.builder()
+                    .setOutputPath(outputPath)
+                    .setManifestPath(manifestPath)
+                    .setDexPath(pathThatDoesNotExist)
+                    .build()
+                    .execute());
+
+    assertThat(exception).hasMessageThat().containsMatch("File '.*' was not found");
+  }
+
+  @Test
+  public void dexDirDoesNotExistThrowsException_viaFlags() throws IOException {
+    dexDirPath = Paths.get(tmp.getRoot().getPath(), "dir-that-does-not-exist");
+    FlagParser flagParser =
+        new FlagParser()
+            .parse(
+                new String[] {
+                  "--output=" + outputPath,
+                  "--manifest=" + manifestPath,
+                  "--dex-dir=" + pathThatDoesNotExist
+                });
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () -> BuildModuleCommand.fromFlags(flagParser).execute());
+
+    assertThat(exception).hasMessageThat().containsMatch("Directory '.*' was not found");
+  }
+
+  @Test
+  public void dexDirDoesNotExistThrowsException_viaBuilder() throws IOException {
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () ->
+                BuildModuleCommand.builder()
+                    .setOutputPath(outputPath)
+                    .setManifestPath(manifestPath)
+                    .setDexDirPath(pathThatDoesNotExist)
+                    .build()
+                    .execute());
+
+    assertThat(exception).hasMessageThat().containsMatch("Directory '.*' was not found");
+  }
+
+  @Test
+  public void resourcesDirDoesNotExistThrowsException_viaFlags() throws IOException {
+    FlagParser flagParser =
+        new FlagParser()
+            .parse(
+                new String[] {
+                  "--output=" + outputPath,
+                  "--manifest=" + manifestPath,
+                  "--resources-dir=" + pathThatDoesNotExist
+                });
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () -> BuildModuleCommand.fromFlags(flagParser).execute());
+
+    assertThat(exception).hasMessageThat().containsMatch("Directory '.*' was not found");
+  }
+
+  @Test
+  public void resourcesDirDoesNotExistThrowsException_viaBuilder() throws IOException {
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () ->
+                BuildModuleCommand.builder()
+                    .setOutputPath(outputPath)
+                    .setManifestPath(manifestPath)
+                    .setResourcesDirPath(pathThatDoesNotExist)
+                    .build()
+                    .execute());
+
+    assertThat(exception).hasMessageThat().containsMatch("Directory '.*' was not found");
+  }
+
+  @Test
+  public void assetsDirDoesNotExistThrowsException_viaFlags() throws IOException {
+    FlagParser flagParser =
+        new FlagParser()
+            .parse(
+                new String[] {
+                  "--output=" + outputPath,
+                  "--manifest=" + manifestPath,
+                  "--assets-dir=" + pathThatDoesNotExist
+                });
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () -> BuildModuleCommand.fromFlags(flagParser).execute());
+
+    assertThat(exception).hasMessageThat().containsMatch("Directory '.*' was not found");
+  }
+
+  @Test
+  public void assetsDirDoesNotExistThrowsException_viaBuilder() throws IOException {
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () ->
+                BuildModuleCommand.builder()
+                    .setOutputPath(outputPath)
+                    .setManifestPath(manifestPath)
+                    .setAssetsDirPath(pathThatDoesNotExist)
+                    .build()
+                    .execute());
+
+    assertThat(exception).hasMessageThat().containsMatch("Directory '.*' was not found");
+  }
+
+  @Test
+  public void nativeDirDoesNotExistThrowsException_viaFlags() throws IOException {
+    FlagParser flagParser =
+        new FlagParser()
+            .parse(
+                new String[] {
+                  "--output=" + outputPath,
+                  "--manifest=" + manifestPath,
+                  "--native-dir=" + pathThatDoesNotExist
+                });
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () -> BuildModuleCommand.fromFlags(flagParser).execute());
+
+    assertThat(exception).hasMessageThat().containsMatch("Directory '.*' was not found");
+  }
+
+  @Test
+  public void nativeDirDoesNotExistThrowsException_viaBuilder() throws IOException {
+    IllegalArgumentException exception =
+        expectThrows(
+            IllegalArgumentException.class,
+            () ->
+                BuildModuleCommand.builder()
+                    .setOutputPath(outputPath)
+                    .setManifestPath(manifestPath)
+                    .setNativeDirPath(pathThatDoesNotExist)
+                    .build()
+                    .execute());
+
+    assertThat(exception).hasMessageThat().containsMatch("Directory '.*' was not found");
+  }
+}
diff --git a/bundletool/javatests/com/android/tools/appbundle/bundletool/BundleToolTest.java b/bundletool/javatests/com/android/tools/appbundle/bundletool/BundleToolTest.java
new file mode 100644
index 0000000..2c69b01
--- /dev/null
+++ b/bundletool/javatests/com/android/tools/appbundle/bundletool/BundleToolTest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tools.appbundle.bundletool;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests for Bundle tool.
+ */
+@RunWith(JUnit4.class)
+public final class BundleToolTest {
+
+  @Test
+  public void dummyTest() {
+  }
+}
diff --git a/bundletool/javatests/com/android/tools/appbundle/bundletool/SplitModuleCommandTest.java b/bundletool/javatests/com/android/tools/appbundle/bundletool/SplitModuleCommandTest.java
new file mode 100644
index 0000000..0006cb0
--- /dev/null
+++ b/bundletool/javatests/com/android/tools/appbundle/bundletool/SplitModuleCommandTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tools.appbundle.bundletool;
+
+import static org.junit.Assert.assertThrows;
+
+import com.android.tools.appbundle.bundletool.Command.ExecutionException;
+import com.android.tools.appbundle.bundletool.utils.FlagParser;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for the SplitModuleCommand implementation. */
+@RunWith(JUnit4.class)
+public class SplitModuleCommandTest {
+
+  @Test
+  public void testMissingOutputFlag() throws Exception {
+    expectFlagException(new String[] {"--bundle=b.zip", "--module=m1"});
+  }
+
+  @Test
+  public void testMissingBundleFlag() throws Exception {
+    expectFlagException(new String[] {"--output=/some/dir", "--module=m1"});
+  }
+
+  @Test
+  public void testMissingModuleFlag() throws Exception {
+    expectFlagException(new String[] {"--output=/some/dir", "--bundle=b.zip"});
+  }
+
+  @Test
+  public void testMissingBundleFileFailsCommand() throws Exception {
+    FlagParser flagParser = new FlagParser();
+    flagParser.parse(new String[] {"--bundle=b.zip", "--output=/some/dir", "--module=m1"});
+    SplitModuleCommand command = new SplitModuleCommand(flagParser);
+    assertThrows(ExecutionException.class, () -> command.execute());
+  }
+
+  private void expectFlagException(String[] flags) throws Exception {
+    FlagParser flagParser = new FlagParser();
+    flagParser.parse(flags);
+    assertThrows(IllegalArgumentException.class, () -> new SplitModuleCommand(flagParser));
+  }
+}
diff --git a/bundletool/javatests/com/android/tools/appbundle/bundletool/utils/FlagParserTest.java b/bundletool/javatests/com/android/tools/appbundle/bundletool/utils/FlagParserTest.java
new file mode 100644
index 0000000..f9fc6d5
--- /dev/null
+++ b/bundletool/javatests/com/android/tools/appbundle/bundletool/utils/FlagParserTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.tools.appbundle.bundletool.utils;
+
+import static com.google.common.truth.Truth.assertThat;
+import static junit.framework.TestCase.fail;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for the FlagParser class. */
+@RunWith(JUnit4.class)
+public class FlagParserTest {
+
+  @Test
+  public void testParsesSingleCommand() throws Exception {
+    String[] args = {"command1"};
+    FlagParser fp = new FlagParser();
+    fp.parse(args);
+    assertThat(fp.getCommands()).containsExactly("command1");
+  }
+
+  @Test
+  public void testEmptyCommandLine() throws Exception {
+    String[] args = {};
+    FlagParser fp = new FlagParser();
+    fp.parse(args);
+    assertThat(fp.getCommands()).isEmpty();
+  }
+
+  @Test
+  public void testParsesCommandLineFlags() throws Exception {
+    String[] args = {"command", "--flag1=value1", "--flag2=value2"};
+    FlagParser fp = new FlagParser();
+    fp.parse(args);
+    assertThat(fp.getCommands()).containsExactly("command");
+    assertThat(fp.getFlagValueOrDefault("flag1", "")).isEqualTo("value1");
+    assertThat(fp.getFlagValueOrDefault("flag2", "")).isEqualTo("value2");
+  }
+
+  @Test
+  public void testIncorrectCommandLineFlagsThrows() throws Exception {
+    String[] args = {"command", "--flag1=value1", "-flag2=value2"};
+    FlagParser fp = new FlagParser();
+    try {
+      fp.parse(args);
+      fail("Expected ParseException but nothing was thrown.");
+    } catch (FlagParser.ParseException e) {
+      assertThat(e.getMessage()).contains("-flag2=value2");
+    }
+  }
+
+  @Test
+  public void testFlagListValuesNotSet() throws Exception {
+    String[] args = {"command"};
+    FlagParser fp = new FlagParser();
+    fp.parse(args);
+    assertThat(fp.getCommands()).containsExactly("command");
+    assertThat(fp.getFlagListValue("flag1")).isEmpty();
+  }
+
+  @Test
+  public void testFlagListValuesSetWithDefault() throws Exception {
+    String[] args = {"command", "--flag1"};
+    FlagParser fp = new FlagParser();
+    fp.parse(args);
+    assertThat(fp.getCommands()).containsExactly("command");
+    assertThat(fp.getFlagListValue("flag1")).containsExactly("");
+  }
+
+  @Test
+  public void testFlagListValuesSingleValue() throws Exception {
+    String[] args = {"command", "--flag1=val1"};
+    FlagParser fp = new FlagParser();
+    fp.parse(args);
+    assertThat(fp.getFlagListValue("flag1")).containsExactly("val1");
+  }
+
+  @Test
+  public void testFlagListValuesMultiple() throws Exception {
+    String[] args = {"command", "--flag1=v1,v2,value3"};
+    FlagParser fp = new FlagParser();
+    fp.parse(args);
+    assertThat(fp.getCommands()).containsExactly("command");
+    assertThat(fp.getFlagValueOrDefault("flag1", "")).isEqualTo("v1,v2,value3");
+    assertThat(fp.getFlagListValue("flag1")).containsExactly("v1", "v2", "value3").inOrder();
+  }
+
+  @Test
+  public void testHandlingAbsentFlags() throws Exception {
+    String[] args = {"command", "--flag1=v1"};
+    FlagParser fp = new FlagParser();
+    fp.parse(args);
+    assertThat(fp.getCommands()).containsExactly("command");
+    assertThat(fp.getFlagValueOrDefault("flag2", "default")).isEqualTo("default");
+    assertThat(fp.getFlagListValue("flag2")).isEmpty();
+    assertThat(fp.isFlagSet("flag2")).isFalse();
+  }
+
+  @Test
+  public void testHandlingMultipleCommands() throws Exception {
+    String[] args = {"help", "command1"};
+    FlagParser fp = new FlagParser();
+    fp.parse(args);
+    assertThat(fp.getCommands()).containsExactly("help", "command1").inOrder();
+  }
+
+  @Test
+  public void testUsingFlagMoreThanOnceThrows() throws Exception {
+    String[] args = {"command", "--flag1=v1", "--flag1=v2"};
+    FlagParser fp = new FlagParser();
+    try {
+      fp.parse(args);
+      fail("Expected ParseException but nothing was thrown.");
+    } catch (FlagParser.ParseException e) {
+      assertThat(e.getMessage()).contains("flag1");
+    }
+  }
+}