aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/objc_tools/plmerge
diff options
context:
space:
mode:
authorGravatar Han-Wen Nienhuys <hanwen@google.com>2015-02-25 16:45:20 +0100
committerGravatar Han-Wen Nienhuys <hanwen@google.com>2015-02-25 16:45:20 +0100
commitd08b27fa9701fecfdb69e1b0d1ac2459efc2129b (patch)
tree5d50963026239ca5aebfb47ea5b8db7e814e57c8 /src/objc_tools/plmerge
Update from Google.
-- MOE_MIGRATED_REVID=85702957
Diffstat (limited to 'src/objc_tools/plmerge')
-rw-r--r--src/objc_tools/plmerge/README2
-rw-r--r--src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/KeysToRemoveIfEmptyString.java37
-rw-r--r--src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/PlMerge.java84
-rw-r--r--src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/PlistMerging.java282
4 files changed, 405 insertions, 0 deletions
diff --git a/src/objc_tools/plmerge/README b/src/objc_tools/plmerge/README
new file mode 100644
index 0000000000..972409fb39
--- /dev/null
+++ b/src/objc_tools/plmerge/README
@@ -0,0 +1,2 @@
+plmerge merges plist files into one. It is similar to Xcode's built in
+builtin-infoPlistUtility.
diff --git a/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/KeysToRemoveIfEmptyString.java b/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/KeysToRemoveIfEmptyString.java
new file mode 100644
index 0000000000..5f4662f540
--- /dev/null
+++ b/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/KeysToRemoveIfEmptyString.java
@@ -0,0 +1,37 @@
+// Copyright 2014 Google Inc. 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.plmerge;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Iterator;
+
+/**
+ * A glorified {@link Iterable} which contains keys which should be automatically removed from the
+ * final plist if they are empty strings.
+ */
+public final class KeysToRemoveIfEmptyString implements Iterable<String> {
+ private final Iterable<String> keyNames;
+
+ public KeysToRemoveIfEmptyString(String... keyNames) {
+ this.keyNames = ImmutableList.copyOf(keyNames);
+ }
+
+ @Override
+ public Iterator<String> iterator() {
+ return keyNames.iterator();
+ }
+}
+
diff --git a/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/PlMerge.java b/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/PlMerge.java
new file mode 100644
index 0000000000..e61f2b908a
--- /dev/null
+++ b/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/PlMerge.java
@@ -0,0 +1,84 @@
+// Copyright 2014 Google Inc. 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.plmerge;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.common.options.Option;
+import com.google.devtools.common.options.Options;
+import com.google.devtools.common.options.OptionsBase;
+import com.google.devtools.common.options.OptionsParser;
+import com.google.devtools.common.options.OptionsParsingException;
+
+import com.dd.plist.NSObject;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Entry point for the {@code plmerge} tool, which merges the data from one or more plists into a
+ * single binary plist. This tool's functionality is similar to that of the
+ * {@code builtin-infoPlistUtility} in Xcode.
+ */
+public class PlMerge {
+ /**
+ * Options for {@link PlMerge}.
+ */
+ public static class PlMergeOptions extends OptionsBase {
+ @Option(
+ name = "source_file",
+ help = "Paths to the plist files to merge. These can be binary, XML, or ASCII format. "
+ + "Repeat this flag to specify multiple files. Required.",
+ allowMultiple = true,
+ defaultValue = "null")
+ public List<String> sourceFiles;
+
+ @Option(
+ name = "out_file",
+ help = "Path to the output file. Required.",
+ defaultValue = "null")
+ public String outFile;
+ }
+
+ public static void main(String[] args) throws IOException, OptionsParsingException {
+ OptionsParser parser = OptionsParser.newOptionsParser(PlMergeOptions.class);
+ parser.parse(args);
+ PlMergeOptions options = parser.getOptions(PlMergeOptions.class);
+ if (options.sourceFiles.isEmpty()) {
+ missingArg("At least one --source_file");
+ }
+ if (options.outFile == null) {
+ missingArg("--out_file");
+ }
+ FileSystem fileSystem = FileSystems.getDefault();
+
+ List<Path> sourceFilePaths = new ArrayList<>();
+ for (String sourceFile : options.sourceFiles) {
+ sourceFilePaths.add(fileSystem.getPath(sourceFile));
+ }
+
+ PlistMerging merging = PlistMerging.from(sourceFilePaths, ImmutableMap.<String, NSObject>of(),
+ ImmutableMap.<String, String>of(), new KeysToRemoveIfEmptyString());
+ merging.writePlist(fileSystem.getPath(options.outFile));
+ }
+
+ private static void missingArg(String flag) {
+ throw new IllegalArgumentException(flag + " is required:\n"
+ + Options.getUsage(PlMergeOptions.class));
+ }
+}
diff --git a/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/PlistMerging.java b/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/PlistMerging.java
new file mode 100644
index 0000000000..dba8f84ac3
--- /dev/null
+++ b/src/objc_tools/plmerge/java/com/google/devtools/build/xcode/plmerge/PlistMerging.java
@@ -0,0 +1,282 @@
+// Copyright 2014 Google Inc. 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.plmerge;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.ByteSource;
+import com.google.devtools.build.xcode.common.Platform;
+import com.google.devtools.build.xcode.common.TargetDeviceFamily;
+import com.google.devtools.build.xcode.util.Equaling;
+import com.google.devtools.build.xcode.util.Intersection;
+import com.google.devtools.build.xcode.util.Mapping;
+import com.google.devtools.build.xcode.util.Value;
+
+import com.dd.plist.BinaryPropertyListWriter;
+import com.dd.plist.NSArray;
+import com.dd.plist.NSDictionary;
+import com.dd.plist.NSObject;
+import com.dd.plist.NSString;
+import com.dd.plist.PropertyListFormatException;
+import com.dd.plist.PropertyListParser;
+
+import org.xml.sax.SAXException;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.ParseException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+/**
+ * Utility code for merging project files.
+ */
+public class PlistMerging extends Value<PlistMerging> {
+
+ /**
+ * Exception type thrown when validation of the plist file fails.
+ */
+ public static class ValidationException extends RuntimeException {
+ ValidationException(String message) {
+ super(message);
+ }
+ }
+
+ private final NSDictionary merged;
+
+ @VisibleForTesting
+ PlistMerging(NSDictionary merged) {
+ super(merged);
+ this.merged = merged;
+ }
+
+ /**
+ * Merges several plist files into a single {@code NSDictionary}. Each file should be a plist (of
+ * one of these formats: ASCII, Binary, or XML) that contains an NSDictionary.
+ */
+ @VisibleForTesting
+ static NSDictionary merge(Iterable<? extends Path> sourceFilePaths) throws IOException {
+ NSDictionary result = new NSDictionary();
+ for (Path sourceFilePath : sourceFilePaths) {
+ result.putAll(readPlistFile(sourceFilePath));
+ }
+ return result;
+ }
+
+ public static NSDictionary readPlistFile(final Path sourceFilePath) throws IOException {
+ ByteSource rawBytes = new Utf8BomSkippingByteSource(sourceFilePath);
+
+ try {
+ try (InputStream in = rawBytes.openStream()) {
+ return (NSDictionary) PropertyListParser.parse(in);
+ } catch (PropertyListFormatException | ParseException e) {
+ // If we failed to parse, the plist may implicitly be a map. To handle this, wrap the plist
+ // with {}.
+ // TODO(bazel-team): Do this in a cleaner way.
+ ByteSource concatenated = ByteSource.concat(
+ ByteSource.wrap(new byte[] {'{'}),
+ rawBytes,
+ ByteSource.wrap(new byte[] {'}'}));
+ try (InputStream in = concatenated.openStream()) {
+ return (NSDictionary) PropertyListParser.parse(in);
+ }
+ }
+ } catch (PropertyListFormatException | ParseException | ParserConfigurationException
+ | SAXException e) {
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Writes the results of a merge operation to a plist file.
+ * @param plistPath the path of the plist to write in binary format
+ */
+ public void writePlist(Path plistPath) throws IOException {
+ try (OutputStream out = Files.newOutputStream(plistPath)) {
+ BinaryPropertyListWriter.write(out, merged);
+ }
+ }
+
+ /**
+ * Writes a PkgInfo file based on certain keys in the merged plist.
+ * @param pkgInfoPath the path of the PkgInfo file to write. In many iOS apps, this file just
+ * contains the raw string {@code APPL????}.
+ */
+ public void writePkgInfo(Path pkgInfoPath) throws IOException {
+ String pkgInfo =
+ Mapping.of(merged, "CFBundlePackageType").or(NSObject.wrap("APPL")).toString()
+ + Mapping.of(merged, "CFBundleSignature").or(NSObject.wrap("????")).toString();
+ Files.write(pkgInfoPath, pkgInfo.getBytes(StandardCharsets.UTF_8));
+ }
+
+ /** Invokes {@link #writePlist(Path)} and {@link #writePkgInfo(Path)}. */
+ public void write(Path plistPath, Path pkgInfoPath) throws IOException {
+ writePlist(plistPath);
+ writePkgInfo(pkgInfoPath);
+ }
+
+ /**
+ * Returns a map containing entries that should be added to the merged plist. These are usually
+ * generated by Xcode automatically during the build process.
+ */
+ public static Map<String, NSObject> automaticEntries(
+ Iterable<TargetDeviceFamily> targetedDeviceFamily, Platform platform, String sdkVersion,
+ String minimumOsVersion) {
+ ImmutableMap.Builder<String, NSObject> result = new ImmutableMap.Builder<>();
+ List<Integer> uiDeviceFamily =
+ Mapping.of(
+ TargetDeviceFamily.UI_DEVICE_FAMILY_VALUES,
+ ImmutableSet.copyOf(targetedDeviceFamily))
+ .get();
+ result.put("UIDeviceFamily", NSObject.wrap(uiDeviceFamily.toArray()));
+ result.put("DTPlatformName", NSObject.wrap(platform.getLowerCaseNameInPlist()));
+ result.put("DTSDKName", NSObject.wrap(platform.getLowerCaseNameInPlist() + sdkVersion));
+ result.put("CFBundleSupportedPlatforms", new NSArray(NSObject.wrap(platform.getNameInPlist())));
+
+ if (platform == Platform.DEVICE) {
+ // TODO(bazel-team): Figure out if there are more appropriate values to put here, or if any
+ // can be omitted. These have been copied from a plist file generated by Xcode for a device
+ // build.
+ result.put("DTCompiler", NSObject.wrap("com.apple.compilers.llvm.clang.1_0"));
+ result.put("BuildMachineOSBuild", NSObject.wrap("13D65"));
+ result.put("DTPlatformBuild", NSObject.wrap("11B508"));
+ result.put("DTSDKBuild", NSObject.wrap("11B508"));
+ result.put("DTXcode", NSObject.wrap("0502"));
+ result.put("DTXcodeBuild", NSObject.wrap("5A3005"));
+ result.put("DTPlatformVersion", NSObject.wrap(sdkVersion));
+ result.put("MinimumOSVersion", NSObject.wrap(minimumOsVersion));
+ }
+ return result.build();
+ }
+
+ /**
+ * Generates final merged Plist file and PkgInfo file in the specified locations, and includes the
+ * "automatic" entries in the Plist.
+ */
+ public static PlistMerging from(List<Path> sourceFiles, Map<String, NSObject> automaticEntries,
+ Map<String, String> substitutions, KeysToRemoveIfEmptyString keysToRemoveIfEmptyString)
+ throws IOException {
+ NSDictionary merged = PlistMerging.merge(sourceFiles);
+
+ Set<String> conflictingEntries = Intersection.of(automaticEntries.keySet(), merged.keySet());
+ Preconditions.checkArgument(conflictingEntries.isEmpty(),
+ "The following plist entries are generated automatically, but are present in one of the "
+ + "input lists: %s", conflictingEntries);
+ merged.putAll(automaticEntries);
+
+ for (Map.Entry<String, NSObject> entry : merged.entrySet()) {
+ if (entry.getValue().toJavaObject() instanceof String) {
+ String newValue = substituteEnvironmentVariable(
+ substitutions, (String) entry.getValue().toJavaObject());
+ merged.put(entry.getKey(), newValue);
+ }
+ }
+
+ for (String key : keysToRemoveIfEmptyString) {
+ if (Equaling.of(Mapping.of(merged, key), Optional.<NSObject>of(new NSString("")))) {
+ merged.remove(key);
+ }
+ }
+
+ return new PlistMerging(merged);
+ }
+
+ // Assume that if an RFC 1034 format string is specified, the value is RFC 1034 compliant.
+ private static String substituteEnvironmentVariable(
+ Map<String, String> substitutions, String string) {
+ // The substitution is *not* performed recursively.
+ for (Map.Entry<String, String> variable : substitutions.entrySet()) {
+ for (String variableNameWithFormatString : withFormatStrings(variable.getKey())) {
+ string = string
+ .replace("${" + variableNameWithFormatString + "}", variable.getValue())
+ .replace("$(" + variableNameWithFormatString + ")", variable.getValue());
+ }
+ }
+
+ return string;
+ }
+
+ private static ImmutableSet<String> withFormatStrings(String variableName) {
+ return ImmutableSet.of(variableName, variableName + ":rfc1034identifier");
+ }
+
+ @VisibleForTesting
+ NSDictionary asDictionary() {
+ return merged;
+ }
+
+ /**
+ * Sets the given executable name on this merged plist in the {@code CFBundleExecutable}
+ * attribute.
+ *
+ * @param executableName name of the bundle executable
+ * @return this plist merging
+ * @throws ValidationException if the plist already contains an incompatible
+ * {@code CFBundleExecutable} entry
+ */
+ public PlistMerging setExecutableName(String executableName) {
+ NSString bundleExecutable = (NSString) merged.get("CFBundleExecutable");
+
+ if (bundleExecutable == null) {
+ merged.put("CFBundleExecutable", executableName);
+ } else if (!executableName.equals(bundleExecutable.getContent())) {
+ throw new ValidationException(String.format(
+ "Blaze generated the executable %s but the Plist CFBundleExecutable is %s",
+ executableName, bundleExecutable));
+ }
+
+ return this;
+ }
+
+ private static class Utf8BomSkippingByteSource extends ByteSource {
+
+ private static final byte[] UTF8_BOM =
+ new byte[] { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF };
+
+ private final Path path;
+
+ public Utf8BomSkippingByteSource(Path path) {
+ this.path = path;
+ }
+
+ @Override
+ public InputStream openStream() throws IOException {
+ InputStream stream = new BufferedInputStream(Files.newInputStream(path));
+ stream.mark(UTF8_BOM.length);
+ byte[] buffer = new byte[UTF8_BOM.length];
+ int read = stream.read(buffer);
+ stream.reset();
+ buffer = Arrays.copyOf(buffer, read);
+
+ if (UTF8_BOM.length == read && Arrays.equals(buffer, UTF8_BOM)) {
+ stream.skip(UTF8_BOM.length);
+ }
+
+ return stream;
+ }
+ }
+}