/*
* 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.android;
import com.android.annotations.VisibleForTesting;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.model.BuildTargets;
import com.facebook.buck.rules.AbstractBuildRule;
import com.facebook.buck.rules.AddToRuleKey;
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.SourcePathRuleFinder;
import com.facebook.buck.rules.coercer.ManifestEntries;
import com.facebook.buck.shell.ShellStep;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.StepExecutionResult;
import com.facebook.buck.step.fs.MakeCleanDirectoryStep;
import com.facebook.buck.util.RichStream;
import com.facebook.buck.zip.ZipScrubberStep;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedSet;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import javax.annotation.Nullable;
/** Perform the "aapt2 link" step of building an Android app. */
public class Aapt2Link extends AbstractBuildRule {
@AddToRuleKey private final ImmutableList<Aapt2Compile> compileRules;
@AddToRuleKey private final SourcePath manifest;
@AddToRuleKey private final ManifestEntries manifestEntries;
Aapt2Link(
BuildRuleParams buildRuleParams,
SourcePathRuleFinder ruleFinder,
ImmutableList<Aapt2Compile> compileRules,
ImmutableList<HasAndroidResourceDeps> resourceRules,
SourcePath manifest,
ManifestEntries manifestEntries) {
super(
buildRuleParams.copyReplacingDeclaredAndExtraDeps(
Suppliers.ofInstance(
ImmutableSortedSet.<BuildRule>naturalOrder()
.addAll(compileRules)
.addAll(RichStream.from(resourceRules).filter(BuildRule.class).toOnceIterable())
.addAll(ruleFinder.filterBuildRuleInputs(manifest))
.build()),
Suppliers.ofInstance(ImmutableSortedSet.of())));
this.compileRules = compileRules;
this.manifest = manifest;
this.manifestEntries = manifestEntries;
}
@Override
public ImmutableList<Step> getBuildSteps(
BuildContext context, BuildableContext buildableContext) {
ImmutableList.Builder<Step> steps = ImmutableList.builder();
steps.addAll(
MakeCleanDirectoryStep.of(getProjectFilesystem(), getResourceApkPath().getParent()));
AaptPackageResources.prepareManifestForAapt(
steps,
getProjectFilesystem(),
getFinalManifestPath(),
context.getSourcePathResolver().getAbsolutePath(manifest),
manifestEntries);
steps.add(
new Aapt2LinkStep(
getProjectFilesystem().getRootPath(),
compileRules
.stream()
.map(Aapt2Compile::getSourcePathToOutput)
.map(context.getSourcePathResolver()::getAbsolutePath)));
steps.add(ZipScrubberStep.of(getProjectFilesystem().resolve(getResourceApkPath())));
steps.add(
new MakeRDotTxtStep(getProjectFilesystem(), getInitialRDotJavaDir(), getRDotTxtPath()));
buildableContext.recordArtifact(getFinalManifestPath());
buildableContext.recordArtifact(getResourceApkPath());
buildableContext.recordArtifact(getProguardConfigPath());
buildableContext.recordArtifact(getRDotTxtPath());
// Don't really need this, but it's small and might help with debugging.
buildableContext.recordArtifact(getInitialRDotJavaDir());
return steps.build();
}
@Nullable
@Override
public SourcePath getSourcePathToOutput() {
return null;
}
private Path getFinalManifestPath() {
return BuildTargets.getGenPath(
getProjectFilesystem(), getBuildTarget(), "%s/AndroidManifest.xml");
}
private Path getResourceApkPath() {
return BuildTargets.getGenPath(getProjectFilesystem(), getBuildTarget(), "%s/resource-apk.ap_");
}
private Path getProguardConfigPath() {
return BuildTargets.getGenPath(
getProjectFilesystem(), getBuildTarget(), "%s/proguard-for-resources.pro");
}
private Path getRDotTxtPath() {
return BuildTargets.getGenPath(getProjectFilesystem(), getBuildTarget(), "%s/R.txt");
}
/** Directory containing R.java files produced by aapt2 link. */
private Path getInitialRDotJavaDir() {
return BuildTargets.getGenPath(getProjectFilesystem(), getBuildTarget(), "%s/initial-rdotjava");
}
public AaptOutputInfo getAaptOutputInfo() {
return AaptOutputInfo.builder()
.setPathToRDotTxt(new ExplicitBuildTargetSourcePath(getBuildTarget(), getRDotTxtPath()))
.setPrimaryResourcesApkPath(
new ExplicitBuildTargetSourcePath(getBuildTarget(), getResourceApkPath()))
.setAndroidManifestXml(
new ExplicitBuildTargetSourcePath(getBuildTarget(), getFinalManifestPath()))
.setAaptGeneratedProguardConfigFile(
new ExplicitBuildTargetSourcePath(getBuildTarget(), getProguardConfigPath()))
.build();
}
class Aapt2LinkStep extends ShellStep {
private final Stream<Path> compiledResourcePaths;
Aapt2LinkStep(Path workingDirectory, Stream<Path> compiledResourcePaths) {
super(workingDirectory);
this.compiledResourcePaths = compiledResourcePaths;
}
@Override
public String getShortName() {
return "aapt2_link";
}
@Override
protected ImmutableList<String> getShellCommandInternal(ExecutionContext context) {
AndroidPlatformTarget androidPlatformTarget = context.getAndroidPlatformTarget();
ImmutableList.Builder<String> builder = ImmutableList.builder();
builder.add(androidPlatformTarget.getAapt2Executable().toString());
builder.add("link");
if (context.getVerbosity().shouldUseVerbosityFlagIfAvailable()) {
builder.add("-v");
}
builder.add("--no-auto-version");
builder.add("--auto-add-overlay");
builder.add("-o", getResourceApkPath().toString());
builder.add("--proguard", getProguardConfigPath().toString());
builder.add("--manifest", getFinalManifestPath().toString());
builder.add("-I", androidPlatformTarget.getAndroidJar().toString());
builder.add("--java", getInitialRDotJavaDir().toString());
// Generate a custom-package R.java just for the purpose of generating R.txt.
builder.add("--custom-package", "make.r.txt");
compiledResourcePaths.forEach(r -> builder.add("-R", r.toString()));
return builder.build();
}
}
@VisibleForTesting
static class MakeRDotTxtStep implements Step {
private static final Pattern NO_OP_LINE =
Pattern.compile(
"^package |"
+ // Don't care about package.
"^public final class R \\{|"
+ // Don't care about top-level class.
"^[/*]" // Comment.
);
private static final Pattern CLASS_LINE =
Pattern.compile("public static final class (\\w+) \\{");
private static final Pattern INT_LINE =
Pattern.compile("public static final int (\\w+)=(\\w+);");
private static final Pattern ARRAY_LINE =
Pattern.compile("public static final int\\[\\] (\\w+)=\\{");
private static final Pattern VALUES_LINE = Pattern.compile("[x0-9a-f, ]*");
private final ProjectFilesystem projectFilesystem;
private final Path initialRDotJavaDir;
private final Path rDotTxtPath;
public MakeRDotTxtStep(
ProjectFilesystem projectFilesystem, Path initialRDotJavaDir, Path rDotTxtPath) {
this.projectFilesystem = projectFilesystem;
this.initialRDotJavaDir = initialRDotJavaDir;
this.rDotTxtPath = rDotTxtPath;
}
@Override
public StepExecutionResult execute(ExecutionContext context)
throws IOException, InterruptedException {
List<String> javaLines =
projectFilesystem.readLines(initialRDotJavaDir.resolve("make/r/txt/R.java"));
List<String> txtLines = convertLines(javaLines);
projectFilesystem.writeLinesToPath(txtLines, rDotTxtPath);
return StepExecutionResult.SUCCESS;
}
@VisibleForTesting
static List<String> convertLines(List<String> javaLines) {
ImmutableList.Builder<String> txtLines = ImmutableList.builder();
@Nullable String inClass = null;
@Nullable String currentArrayName = null;
@Nullable List<String> currentArrayValues = null;
for (String line : javaLines) {
line = line.trim();
if (line.isEmpty() || NO_OP_LINE.matcher(line).lookingAt()) {
continue;
}
if (currentArrayName != null) {
if (line.equals("};")) {
txtLines.add(
String.format(
"int[] %s %s { %s }",
inClass, currentArrayName, Joiner.on(',').join(currentArrayValues)));
currentArrayName = null;
currentArrayValues = null;
continue;
}
if (VALUES_LINE.matcher(line).matches()) {
RichStream.of(line.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.forEach(currentArrayValues::add);
continue;
}
throw new IllegalArgumentException("Got unexpected line in array data: " + line);
}
Matcher classMatch = CLASS_LINE.matcher(line);
if (classMatch.matches()) {
inClass = classMatch.group(1);
continue;
}
if (line.equals("}")) {
inClass = null;
continue;
}
if (inClass == null) {
throw new IllegalArgumentException("Got unexpected line with no active class: " + line);
}
Matcher intMatch = INT_LINE.matcher(line);
if (intMatch.matches()) {
txtLines.add(
String.format("int %s %s %s", inClass, intMatch.group(1), intMatch.group(2)));
continue;
}
Matcher arrayMatch = ARRAY_LINE.matcher(line);
if (arrayMatch.matches()) {
currentArrayName = arrayMatch.group(1);
currentArrayValues = new ArrayList<>();
continue;
}
throw new IllegalArgumentException("Got unexpected line: " + line);
}
return txtLines.build();
}
public static void main(String[] args) throws IOException {
List<String> javaLines = Files.readAllLines(Paths.get(args[0]), Charsets.UTF_8);
List<String> txtLines = convertLines(javaLines);
txtLines.forEach(System.out::println);
}
@Override
public String getShortName() {
return "make_r_dot_txt";
}
@Override
public String getDescription(ExecutionContext context) {
return String.format("make_r_dot_txt %s > %s", initialRDotJavaDir, rDotTxtPath);
}
}
}