| // 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(); |
| } |
| } |