blob: 1fb0672573083031969cc3f83a647534671da459 [file] [log] [blame]
// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.xcode.bundlemerge;
import static com.google.devtools.build.singlejar.ZipCombiner.DOS_EPOCH;
import static com.google.devtools.build.singlejar.ZipCombiner.OutputMode.FORCE_DEFLATE;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.singlejar.ZipCombiner;
import com.google.devtools.build.xcode.bundlemerge.proto.BundleMergeProtos.BundleFile;
import com.google.devtools.build.xcode.bundlemerge.proto.BundleMergeProtos.Control;
import com.google.devtools.build.xcode.bundlemerge.proto.BundleMergeProtos.MergeZip;
import com.google.devtools.build.xcode.plmerge.PlistMerging;
import com.google.devtools.build.xcode.zip.ZipFiles;
import com.google.devtools.build.xcode.zip.ZipInputEntry;
import com.google.devtools.build.zip.ZipFileEntry;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.annotation.CheckReturnValue;
/**
* Implementation of the final steps to create an iOS application bundle.
*
* <p>TODO(bazel-team): Add asset catalog compilation and bundling to this logic.
*/
public final class BundleMerging {
@VisibleForTesting final FileSystem fileSystem;
@VisibleForTesting final Path outputZip;
@VisibleForTesting final ImmutableList<ZipInputEntry> inputs;
@VisibleForTesting final ImmutableList<MergeZip> mergeZips;
/**
* We can instantiate this class for testing purposes. For typical uses, just use
* {@link #merge(FileSystem, Control)}.
*/
private BundleMerging(FileSystem fileSystem,
Path outputZip, ImmutableList<ZipInputEntry> inputs, ImmutableList<MergeZip> mergeZips) {
this.fileSystem = Preconditions.checkNotNull(fileSystem);
this.outputZip = Preconditions.checkNotNull(outputZip);
this.inputs = Preconditions.checkNotNull(inputs);
this.mergeZips = Preconditions.checkNotNull(mergeZips);
}
/**
* Joins two paths to be used in a zip file. The {@code right} part of the path must be relative.
* The {@code left} part could or could not have a trailing slash. These paths are used in .ipa
* (.zip) files, which must use forward slashes, so they are hard-coded here.
* <p>
* TODO(bazel-team): This is messy. See if we can use some common joining function that handles
* empty paths and doesn't automatically inherit the path conventions of the host platform.
*/
private static String joinPath(String left, String right) {
Preconditions.checkArgument(!right.startsWith("/"), "'right' must be relative: %s", right);
if (left.isEmpty() || right.isEmpty() || left.endsWith("/")) {
return left + right;
} else {
return left + "/" + right;
}
}
private static final String INFOPLIST_FILENAME = "Info.plist";
private static final String PKGINFO_FILENAME = "PkgInfo";
/**
* Adds merge artifacts from the given {@code control} into builders that collect merge zips and
* individual files. {@code bundleRoot} is prepended to each path, except the paths in the merge
* zips.
*/
private static void mergeInto(
Path tempDir, FileSystem fileSystem, Control control, String bundleRoot,
ImmutableList.Builder<ZipInputEntry> packagedFilesBuilder,
ImmutableList.Builder<MergeZip> mergeZipsBuilder, boolean includePkgInfo) throws IOException {
bundleRoot = joinPath(bundleRoot, control.getBundleRoot());
if (control.hasBundleInfoPlistFile()) {
Path tempMergedPlist = Files.createTempFile(tempDir, null, INFOPLIST_FILENAME);
Path tempPkgInfo = Files.createTempFile(tempDir, null, PKGINFO_FILENAME);
Path bundleInfoPlist = fileSystem.getPath(control.getBundleInfoPlistFile());
new PlistMerging(PlistMerging.readPlistFile(bundleInfoPlist))
.setBundleIdentifier(
control.hasPrimaryBundleIdentifier() ? control.getPrimaryBundleIdentifier() : null,
control.hasFallbackBundleIdentifier() ? control.getFallbackBundleIdentifier() : null)
.writePlist(tempMergedPlist)
.writePkgInfo(tempPkgInfo);
packagedFilesBuilder
.add(new ZipInputEntry(tempMergedPlist, joinPath(bundleRoot, INFOPLIST_FILENAME)));
if (includePkgInfo) {
packagedFilesBuilder
.add(new ZipInputEntry(tempPkgInfo, joinPath(bundleRoot, PKGINFO_FILENAME)));
}
}
for (BundleFile bundleFile : control.getBundleFileList()) {
int externalFileAttribute = bundleFile.hasExternalFileAttribute()
? bundleFile.getExternalFileAttribute() : ZipInputEntry.DEFAULT_EXTERNAL_FILE_ATTRIBUTE;
packagedFilesBuilder.add(
new ZipInputEntry(
fileSystem.getPath(bundleFile.getSourceFile()),
joinPath(bundleRoot, bundleFile.getBundlePath()),
externalFileAttribute));
}
mergeZipsBuilder.addAll(control.getMergeZipList());
for (Control nestedControl : control.getNestedBundleList()) {
mergeInto(tempDir, fileSystem, nestedControl, bundleRoot, packagedFilesBuilder,
mergeZipsBuilder, /*includePkgInfo=*/false);
}
}
/**
* Returns a zipper configuration that can be executed to create the application bundle.
*/
@CheckReturnValue
@VisibleForTesting
static BundleMerging merging(Path tempDir, FileSystem fileSystem, Control control)
throws IOException {
ImmutableList.Builder<MergeZip> mergeZipsBuilder = new ImmutableList.Builder<>();
ImmutableList.Builder<ZipInputEntry> packagedFilesBuilder = new ImmutableList.Builder<>();
mergeInto(tempDir, fileSystem, control, /*bundleRoot=*/"", packagedFilesBuilder,
mergeZipsBuilder, /*includePkgInfo=*/true);
return new BundleMerging(fileSystem, fileSystem.getPath(control.getOutFile()),
packagedFilesBuilder.build(), mergeZipsBuilder.build());
}
/**
* Copies all entries from the source zip into a destination zip using the given combiner. The
* contents of the source zip can appear to be in a sub-directory of the destination zip by
* passing a non-empty string for the entry names prefix with a trailing '/'.
*/
private void addEntriesFromOtherZip(ZipCombiner combiner, Path sourceZip, String entryNamesPrefix)
throws IOException {
Map<String, Integer> externalFileAttributes = ZipFiles.unixExternalFileAttributes(sourceZip);
try (ZipInputStream zipIn = new ZipInputStream(Files.newInputStream(sourceZip))) {
while (true) {
ZipEntry zipInEntry = zipIn.getNextEntry();
if (zipInEntry == null) {
break;
}
// TODO(bazel-dev): Add support for soft links because we will need them for MacOS support
// in frameworks at the very least. https://github.com/bazelbuild/bazel/issues/289
String name = entryNamesPrefix + zipInEntry.getName();
if (zipInEntry.isDirectory()) {
// If we already have a directory entry with this name then don't attempt to
// add it again. It's not an error to attempt to merge in two zip files that contain
// the same directories. It's only an error to attempt to merge in two zip files with the
// same leaf files.
if (!combiner.containsFile(name)) {
combiner.addDirectory(name, DOS_EPOCH);
}
continue;
}
Integer externalFileAttr = externalFileAttributes.get(zipInEntry.getName());
if (externalFileAttr == null) {
externalFileAttr = ZipInputEntry.DEFAULT_EXTERNAL_FILE_ATTRIBUTE;
}
ZipFileEntry zipOutEntry = new ZipFileEntry(name);
zipOutEntry.setTime(DOS_EPOCH.getTime());
zipOutEntry.setVersion(ZipInputEntry.MADE_BY_VERSION);
zipOutEntry.setExternalAttributes(externalFileAttr);
combiner.addFile(zipOutEntry, zipIn);
}
}
}
@VisibleForTesting
void execute() throws IOException {
try (OutputStream out = Files.newOutputStream(outputZip);
ZipCombiner combiner = new ZipCombiner(FORCE_DEFLATE, out)) {
ZipInputEntry.addAll(combiner, inputs);
for (MergeZip mergeZip : mergeZips) {
addEntriesFromOtherZip(
combiner, fileSystem.getPath(mergeZip.getSourcePath()), mergeZip.getEntryNamePrefix());
}
}
}
/**
* Creates an {@code .ipa} file for an iOS application.
* @param fileSystem used to resolve paths specified in {@code control}
* @param control specifies the locations of input and output files and other parameters used to
* create the final {@code .ipa} file.
*/
public static void merge(FileSystem fileSystem, Control control) throws IOException {
merging(Files.createTempDirectory("mergebundle"), fileSystem, control).execute();
}
}