/*
* Copyright 2015-present Facebook, Inc.
*
* 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.facebook.buck.apple;
import com.facebook.buck.file.WriteFile;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.rules.AbstractBuildRule;
import com.facebook.buck.rules.BuildContext;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleParams;
import com.facebook.buck.rules.BuildableContext;
import com.facebook.buck.rules.ExplicitBuildTargetSourcePath;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.fs.CopyStep;
import com.facebook.buck.step.fs.MakeCleanDirectoryStep;
import com.facebook.buck.step.fs.MkdirStep;
import com.facebook.buck.step.fs.RmStep;
import com.facebook.buck.step.fs.WriteFileStep;
import com.facebook.buck.zip.ZipCompressionLevel;
import com.facebook.buck.zip.ZipStep;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteSource;
import java.nio.file.Path;
import java.util.Optional;
public class BuiltinApplePackage extends AbstractBuildRule {
private final Path pathToOutputFile;
private final Path temp;
private final BuildRule bundle;
public BuiltinApplePackage(BuildRuleParams params, BuildRule bundle) {
super(params);
BuildTarget buildTarget = params.getBuildTarget();
// TODO(markwang): This will be different for Mac apps.
this.pathToOutputFile = BuildTargets.getGenPath(getProjectFilesystem(), buildTarget, "%s.ipa");
this.temp = BuildTargets.getScratchPath(getProjectFilesystem(), buildTarget, "__temp__%s");
this.bundle = bundle;
}
@Override
public ImmutableList<Step> getBuildSteps(
BuildContext context, BuildableContext buildableContext) {
ImmutableList.Builder<Step> commands = ImmutableList.builder();
// Remove the output .ipa file if it exists already
commands.add(RmStep.of(getProjectFilesystem(), pathToOutputFile));
// Create temp folder to store the files going to be zipped
commands.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), temp));
Path payloadDir = temp.resolve("Payload");
commands.add(MkdirStep.of(getProjectFilesystem(), payloadDir));
// Recursively copy the .app directory into the Payload folder
Path bundleOutputPath =
context
.getSourcePathResolver()
.getRelativePath(Preconditions.checkNotNull(bundle.getSourcePathToOutput()));
appendAdditionalAppleWatchSteps(commands);
commands.add(
CopyStep.forDirectory(
getProjectFilesystem(),
bundleOutputPath,
payloadDir,
CopyStep.DirectoryMode.DIRECTORY_AND_CONTENTS));
appendAdditionalSwiftSteps(context.getSourcePathResolver(), commands);
// do the zipping
commands.add(MkdirStep.of(getProjectFilesystem(), pathToOutputFile.getParent()));
commands.add(
new ZipStep(
getProjectFilesystem(),
pathToOutputFile,
ImmutableSet.of(),
false,
ZipCompressionLevel.DEFAULT_COMPRESSION_LEVEL,
temp));
buildableContext.recordArtifact(
context.getSourcePathResolver().getRelativePath(getSourcePathToOutput()));
return commands.build();
}
private void appendAdditionalSwiftSteps(
SourcePathResolver resolver, ImmutableList.Builder<Step> commands) {
// For .ipas containing Swift code, Apple requires the following for App Store submissions:
// 1. Copy the Swift standard libraries to SwiftSupport/{platform}
if (bundle instanceof AppleBundle) {
AppleBundle appleBundle = (AppleBundle) bundle;
Path swiftSupportDir = temp.resolve("SwiftSupport").resolve(appleBundle.getPlatformName());
appleBundle.addSwiftStdlibStepIfNeeded(
resolver, swiftSupportDir, Optional.empty(), commands, true /* is for packaging? */);
}
}
private void appendAdditionalAppleWatchSteps(ImmutableList.Builder<Step> commands) {
// For .ipas with WatchOS2 support, Apple apparently requires the following for App Store
// submissions:
// 1. Have a empty "Symbols" directory on the top level.
// 2. Copy the unmodified WatchKit stub binary for WatchOS2 apps to WatchKitSupport2/WK
// We can't use the copy of the binary in the bundle because that has already been re-signed
// with our own identity.
//
// For WatchOS1 support: same as above, except:
// 1. No "Symbols" directory needed.
// 2. WatchKitSupport instead of WatchKitSupport2.
for (BuildRule rule : bundle.getBuildDeps()) {
if (rule instanceof AppleBundle) {
AppleBundle appleBundle = (AppleBundle) rule;
if (appleBundle.getBinary().isPresent()) {
BuildRule binary = appleBundle.getBinary().get();
if (binary instanceof WriteFile && appleBundle.getPlatformName().startsWith("watch")) {
commands.add(MkdirStep.of(getProjectFilesystem(), temp.resolve("Symbols")));
Path watchKitSupportDir = temp.resolve("WatchKitSupport2");
commands.add(MkdirStep.of(getProjectFilesystem(), watchKitSupportDir));
commands.add(
new WriteFileStep(
getProjectFilesystem(),
ByteSource.wrap(((WriteFile) binary).getFileContents()),
watchKitSupportDir.resolve("WK"),
true /* executable */));
} else {
Optional<WriteFile> legacyWatchStub = getLegacyWatchStubFromDeps(appleBundle);
if (legacyWatchStub.isPresent()) {
Path watchKitSupportDir = temp.resolve("WatchKitSupport");
commands.add(MkdirStep.of(getProjectFilesystem(), watchKitSupportDir));
commands.add(
new WriteFileStep(
getProjectFilesystem(),
ByteSource.wrap(legacyWatchStub.get().getFileContents()),
watchKitSupportDir.resolve("WK"),
true /* executable */));
}
}
}
}
}
}
/**
* Get the stub binary rule from a legacy Apple Watch Extension build rule.
*
* @return the WatchOS 1 stub binary if appleBundle represents a legacy Watch Extension.
* Otherwise, return absent.
*/
private Optional<WriteFile> getLegacyWatchStubFromDeps(AppleBundle appleBundle) {
for (BuildRule rule : appleBundle.getBuildDeps()) {
if (rule instanceof AppleBundle
&& rule.getBuildTarget()
.getFlavors()
.contains(AppleBinaryDescription.LEGACY_WATCH_FLAVOR)) {
AppleBundle legacyWatchApp = (AppleBundle) rule;
if (legacyWatchApp.getBinary().isPresent()) {
BuildRule legacyWatchStub = legacyWatchApp.getBinary().get();
if (legacyWatchStub instanceof WriteFile) {
return Optional.of((WriteFile) legacyWatchStub);
}
}
}
}
return Optional.empty();
}
@Override
public SourcePath getSourcePathToOutput() {
return new ExplicitBuildTargetSourcePath(getBuildTarget(), pathToOutputFile);
}
@Override
public boolean isCacheable() {
return bundle.isCacheable();
}
}