summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid AppBundle <noreply@google.com>2017-08-17 15:30:34 +0100
committerTom Dobek <tdobek@google.com>2017-08-17 15:37:17 +0100
commit7c394fa23d0e67a11e7da0bcb00e5c65affe4751 (patch)
tree6fb07636df456cfea2cfe6937ff226e5d9947f18
parent48e13052143e05f264f53f9286b3cff861ef5aac (diff)
downloadappbundle-7c394fa23d0e67a11e7da0bcb00e5c65affe4751.tar.gz
Project import generated by Copybara.
PiperOrigin-RevId: 165576084 Change-Id: I5bc76c2688f5a9fa18ea9801308c8b8e690b1f66
-rw-r--r--bundle2installable/.idea/scopes/scope_settings.xml5
-rw-r--r--bundletool/java/com/android/tools/appbundle/bundletool/AppBundle.java71
-rw-r--r--bundletool/java/com/android/tools/appbundle/bundletool/BuildModuleCommand.java197
-rw-r--r--bundletool/java/com/android/tools/appbundle/bundletool/BundleModule.java70
-rw-r--r--bundletool/java/com/android/tools/appbundle/bundletool/BundleToolMain.java104
-rw-r--r--bundletool/java/com/android/tools/appbundle/bundletool/Command.java35
-rw-r--r--bundletool/java/com/android/tools/appbundle/bundletool/SplitModuleCommand.java80
-rw-r--r--bundletool/java/com/android/tools/appbundle/bundletool/utils/FlagParser.java147
-rw-r--r--bundletool/javatests/com/android/tools/appbundle/bundletool/AllTests.java35
-rw-r--r--bundletool/javatests/com/android/tools/appbundle/bundletool/AppBundleTest.java80
-rw-r--r--bundletool/javatests/com/android/tools/appbundle/bundletool/BuildModuleCommandTest.java471
-rw-r--r--bundletool/javatests/com/android/tools/appbundle/bundletool/BundleToolTest.java32
-rw-r--r--bundletool/javatests/com/android/tools/appbundle/bundletool/SplitModuleCommandTest.java59
-rw-r--r--bundletool/javatests/com/android/tools/appbundle/bundletool/utils/FlagParserTest.java134
14 files changed, 1515 insertions, 5 deletions
diff --git a/bundle2installable/.idea/scopes/scope_settings.xml b/bundle2installable/.idea/scopes/scope_settings.xml
deleted file mode 100644
index 922003b..0000000
--- a/bundle2installable/.idea/scopes/scope_settings.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<component name="DependencyValidationManager">
- <state>
- <option name="SKIP_IMPORT_STATEMENTS" value="false" />
- </state>
-</component> \ No newline at end of file
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");
+ }
+ }
+}