/* * Copyright 2016-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.facebook.buck.io.MorePaths; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.model.BuildTargets; import com.facebook.buck.rules.AbstractBuildRule; import com.facebook.buck.rules.BuildContext; 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.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.zip.CustomZipOutputStream; import com.facebook.buck.zip.ZipOutputStreams; import com.facebook.buck.zip.ZipScrubberStep; import com.google.common.base.Charsets; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import java.io.IOException; import java.io.OutputStream; import java.nio.file.FileVisitResult; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; /** Rule for trimming unnecessary ids from R.java files. */ class TrimUberRDotJava extends AbstractBuildRule { /** * If the app has resources, aapt will have generated an R.java in this directory. If there are no * resources, this should be empty and we'll create a placeholder R.java below. */ private final Optional<SourcePath> pathToRDotJavaDir; private final Collection<DexProducedFromJavaLibrary> allPreDexRules; private final Optional<String> keepResourcePattern; private static final Pattern R_DOT_JAVA_LINE_PATTERN = Pattern.compile("^ *public static final int(?:\\[\\])? (\\w+)="); private static final Pattern R_DOT_JAVA_PACKAGE_NAME_PATTERN = Pattern.compile("^ *package ([\\w.]+);"); TrimUberRDotJava( BuildRuleParams buildRuleParams, Optional<SourcePath> pathToRDotJavaDir, Collection<DexProducedFromJavaLibrary> allPreDexRules, Optional<String> keepResourcePattern) { super(buildRuleParams); this.pathToRDotJavaDir = pathToRDotJavaDir; this.allPreDexRules = allPreDexRules; this.keepResourcePattern = keepResourcePattern; } @Override public ImmutableList<Step> getBuildSteps( BuildContext context, BuildableContext buildableContext) { Path output = context.getSourcePathResolver().getRelativePath(getSourcePathToOutput()); Optional<Path> input = pathToRDotJavaDir.map(context.getSourcePathResolver()::getRelativePath); buildableContext.recordArtifact(output); return new ImmutableList.Builder<Step>() .addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), output.getParent())) .add(new PerformTrimStep(output, input)) .add( ZipScrubberStep.of( context.getSourcePathResolver().getAbsolutePath(getSourcePathToOutput()))) .build(); } @Override public SourcePath getSourcePathToOutput() { return new ExplicitBuildTargetSourcePath( getBuildTarget(), BuildTargets.getGenPath( getProjectFilesystem(), getBuildTarget(), "%s/_trimmed_r_dot_java.src.zip")); } private class PerformTrimStep implements Step { private final Path pathToOutput; private final Optional<Path> pathToInput; public PerformTrimStep(Path pathToOutput, Optional<Path> pathToInput) { this.pathToOutput = pathToOutput; this.pathToInput = pathToInput; } @Override public StepExecutionResult execute(ExecutionContext context) throws IOException, InterruptedException { ImmutableSet.Builder<String> allReferencedResourcesBuilder = ImmutableSet.builder(); for (DexProducedFromJavaLibrary preDexRule : allPreDexRules) { Optional<ImmutableList<String>> referencedResources = preDexRule.getReferencedResources(); if (referencedResources.isPresent()) { allReferencedResourcesBuilder.addAll(referencedResources.get()); } } final ImmutableSet<String> allReferencedResources = allReferencedResourcesBuilder.build(); final ProjectFilesystem projectFilesystem = getProjectFilesystem(); try (final CustomZipOutputStream output = ZipOutputStreams.newOutputStream(projectFilesystem.resolve(pathToOutput))) { if (!pathToInput.isPresent()) { // dx fails if its input contains no classes. Rather than add empty input handling // to DxStep, the dex merger, and every other step of this chain, just generate a // stub class. This will be stripped by ProGuard in release builds and have a minimal // effect on debug builds. output.putNextEntry(new ZipEntry("com/facebook/buck/AppWithoutResourcesStub.java")); output.write( ("package com.facebook.buck_generated;\n" + "final class AppWithoutResourcesStub {}") .getBytes()); } else { Preconditions.checkState(projectFilesystem.exists(pathToInput.get())); projectFilesystem.walkRelativeFileTree( pathToInput.get(), new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (attrs.isDirectory()) { return FileVisitResult.CONTINUE; } if (!attrs.isRegularFile()) { throw new RuntimeException( String.format( "Found unknown file type while looking for R.java: %s (%s)", file, attrs)); } if (!file.getFileName().toString().endsWith(".java")) { throw new RuntimeException( String.format("Found unknown file while looking for R.java: %s", file)); } output.putNextEntry( new ZipEntry( MorePaths.pathWithUnixSeparators(pathToInput.get().relativize(file)))); if (allPreDexRules.isEmpty()) { // If there are no pre-dexed inputs, we don't yet support trimming // R.java, so just copy it verbatim (instead of trimming it down to nothing). projectFilesystem.copyToOutputStream(file, output); } else { filterRDotJava( projectFilesystem.readLines(file), output, allReferencedResources, keepResourcePattern); } return FileVisitResult.CONTINUE; } }); } } return StepExecutionResult.SUCCESS; } @Override public String getShortName() { return "trim_uber_r_dot_java"; } @Override public String getDescription(ExecutionContext context) { return String.format("trim_uber_r_dot_java %s > %s", pathToInput, pathToOutput); } } private static void filterRDotJava( List<String> rDotJavaLines, OutputStream output, ImmutableSet<String> allReferencedResources, Optional<String> keepResourcePattern) throws IOException { String packageName = null; Matcher m; Optional<Pattern> keepPattern = keepResourcePattern.map(Pattern::compile); for (String line : rDotJavaLines) { if (packageName == null) { m = R_DOT_JAVA_PACKAGE_NAME_PATTERN.matcher(line); if (m.find()) { packageName = m.group(1); } else { continue; } } m = R_DOT_JAVA_LINE_PATTERN.matcher(line); // We match on the package name + resource name. // This can cause us to keep (for example) R.layout.foo when only R.string.foo // is referenced. That is a very rare case, though, and not worth the complexity to fix. if (m.find()) { final String resource = m.group(1); boolean shouldWriteLine = allReferencedResources.contains(packageName + "." + resource) || (keepPattern.isPresent() && keepPattern.get().matcher(resource).find()); if (!shouldWriteLine) { continue; } } output.write(line.getBytes(Charsets.UTF_8)); output.write('\n'); } } }