blob: fa6d9bbf7ce9738c3159492a4a0b45efd3c16156 [file] [log] [blame]
// Copyright 2015 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.android;
import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hasher;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.logging.Logger;
/**
* Deduplicates identical files in the provided directories.
* <p>
* This is necessary for the andorid_resources deprecation -- the old style of inheritance
* required all relevant resources to be copied from each dependency. This means each resource is
* duplicated for each resource set. This modifier creates a sym link forest for each unique file
* on a first come, first serve basis. Which makes aapt and the merging code loads happier.
*/
public class FileDeDuplicator implements DirectoryModifier {
private static final Logger LOGGER = Logger.getLogger(FileDeDuplicator.class.getName());
private static final class ConditionalCopyVisitor extends SimpleFileVisitor<Path> {
private final Path newRoot;
private final Path workingDir;
private Multimap<Path, HashCode> seen;
private HashFunction hashFunction;
private ConditionalCopyVisitor(Path newRoot, Path workingDir,
Multimap<Path, HashCode> seen, HashFunction hashFunction) {
this.newRoot = newRoot;
this.workingDir = workingDir;
this.seen = seen;
this.hashFunction = hashFunction;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
Files.createDirectories(newRoot.resolve(workingDir.relativize(dir)));
return super.preVisitDirectory(dir, attrs);
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Path relativePath = workingDir.relativize(file);
final HashCode fileHash = hashPath(file, hashFunction.newHasher());
if (!seen.get(relativePath).contains(fileHash)) {
seen.get(relativePath).add(fileHash);
// TODO(bazel-team): Change to a symlink when the AOSP merge code supports symlinks.
Files.copy(file, newRoot.resolve(relativePath));
// Files.createSymbolicLink(newRoot.resolve(workingDir.relativize(file)), file);
} else {
LOGGER.warning(String.format("Duplicated file %s [%s]", relativePath, file));
}
return super.visitFile(file, attrs);
}
}
private static HashCode hashPath(Path file, final Hasher hasher) throws IOException {
byte[] tmpBuffer = new byte[512];
final InputStream in = Files.newInputStream(file);
for (int read = in.read(tmpBuffer); read > 0; read = in.read(tmpBuffer)) {
hasher.putBytes(tmpBuffer, 0, read);
}
final HashCode fileHash = hasher.hash();
in.close();
return fileHash;
}
private final Multimap<Path, HashCode> seen;
private final HashFunction hashFunction;
private final Path out;
private final Path workingDirectory;
public FileDeDuplicator(HashFunction hashFunction, Path out, Path workingDirectory) {
this.hashFunction = hashFunction;
this.workingDirectory = workingDirectory;
this.seen = HashMultimap.create();
this.out = out;
}
private ImmutableList<Path> conditionallyCopy(ImmutableList<Path> roots)
throws IOException {
final Builder<Path> builder = ImmutableList.builder();
for (Path root : roots) {
Preconditions.checkArgument(root.startsWith(workingDirectory),
root + " must start with root " + workingDirectory + " from " + roots);
Preconditions.checkArgument(!root.equals(workingDirectory),
"Cannot deduplicate root directory: " + root + " from " + roots);
if (!seen.containsKey(root)) {
seen.put(root, null);
final Path newRoot = out.resolve(workingDirectory.relativize(root));
Files.walkFileTree(root, ImmutableSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
new ConditionalCopyVisitor(newRoot, root, seen, hashFunction));
builder.add(newRoot);
} else {
// Duplicated directories are ok -- multiple files from different libraries
// can reside in the same directory, but duplicate files should not be seen mulitple times.
}
}
return builder.build();
}
@Override
public ImmutableList<Path> modify(ImmutableList<Path> directories) {
try {
return conditionallyCopy(directories);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}