| // Copyright 2018 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.importdeps; |
| |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static com.google.common.base.Preconditions.checkState; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.MoreObjects; |
| import com.google.common.base.Predicates; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.ImmutableSortedMap; |
| import com.google.common.io.Closer; |
| import com.google.devtools.build.importdeps.AbstractClassEntryState.ExistingState; |
| import com.google.devtools.build.importdeps.AbstractClassEntryState.IncompleteState; |
| import com.google.devtools.build.importdeps.AbstractClassEntryState.MissingState; |
| import com.google.devtools.build.importdeps.ClassInfo.MemberInfo; |
| import java.io.Closeable; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.PrintStream; |
| import java.nio.file.Path; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.function.Predicate; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipFile; |
| import javax.annotation.Nullable; |
| import org.objectweb.asm.ClassReader; |
| import org.objectweb.asm.ClassVisitor; |
| import org.objectweb.asm.FieldVisitor; |
| import org.objectweb.asm.MethodVisitor; |
| import org.objectweb.asm.Opcodes; |
| |
| /** A cache that stores all the accessible classes in a set of JARs. */ |
| public final class ClassCache implements Closeable { |
| |
| private final LazyClasspath lazyClasspath; |
| private boolean isClosed; |
| |
| public ClassCache( |
| ImmutableSet<Path> bootclasspath, |
| ImmutableSet<Path> directClasspath, |
| ImmutableSet<Path> regularClasspath, |
| ImmutableSet<Path> inputJars, |
| boolean populateMembers) |
| throws IOException { |
| lazyClasspath = |
| new LazyClasspath( |
| bootclasspath, directClasspath, regularClasspath, inputJars, populateMembers); |
| } |
| |
| public AbstractClassEntryState getClassState(String internalName) { |
| checkState(!isClosed, "The cache has been closed."); |
| LazyClassEntry entry = lazyClasspath.getLazyEntry(internalName); |
| if (entry == null) { |
| return MissingState.singleton(); |
| } |
| return entry.getState(lazyClasspath); |
| } |
| |
| public ImmutableMap<Path, Boolean> collectUsedJarsInRegularClasspath() { |
| return lazyClasspath.collectUsedJarsInRegularClasspath(); |
| } |
| |
| @Override |
| public void close() throws IOException { |
| lazyClasspath.close(); |
| isClosed = true; |
| } |
| |
| static class LazyClassEntry { |
| private final String internalName; |
| private final ZipFile zipFile; |
| private final Path jarPath; |
| private final boolean isDirectDep; |
| |
| /** |
| * The state of this class entry. If {@literal null}, then this class has not been resolved yet. |
| */ |
| @Nullable private AbstractClassEntryState state = null; |
| |
| private LazyClassEntry( |
| String internalName, ZipFile zipFile, Path jarPath, boolean isDirectDep) { |
| this.internalName = internalName; |
| this.zipFile = zipFile; |
| this.jarPath = jarPath; |
| this.isDirectDep = isDirectDep; |
| } |
| |
| ZipFile getZipFile() { |
| return zipFile; |
| } |
| |
| @Nullable |
| public AbstractClassEntryState getState(LazyClasspath classpath) { |
| resolveIfNot(classpath); |
| checkState( |
| state != null && !state.isMissingState(), |
| "The state cannot be null or MISSING. %s", |
| state); |
| return state; |
| } |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(this) |
| .add("internalName", internalName) |
| .add("state", state) |
| .toString(); |
| } |
| |
| boolean isResolved() { |
| return state != null; |
| } |
| |
| private void resolveIfNot(LazyClasspath lazyClasspath) { |
| if (isResolved()) { |
| return; |
| } |
| resolveClassEntry(this, lazyClasspath, /* explicitUse= */ true); |
| checkNotNull(state, "After resolution, the state cannot be null"); |
| } |
| |
| private static void resolveClassEntry( |
| LazyClassEntry classEntry, LazyClasspath lazyClasspath, boolean explicitUse) { |
| if (classEntry.state != null) { |
| // Already resolved. See if it is the existing state. |
| if (classEntry.state instanceof ExistingState) { |
| ExistingState state = (ExistingState) classEntry.state; |
| if (!state.direct() && explicitUse) { |
| // If the state was previously indirect, update now for direct dep |
| classEntry.state = ExistingState.create(state.classInfo().get(), explicitUse); |
| } |
| } |
| return; |
| } |
| |
| String entryName = classEntry.internalName + ".class"; |
| ZipEntry zipEntry = |
| checkNotNull( |
| classEntry.zipFile.getEntry(entryName), "The zip entry %s is null.", entryName); |
| try (InputStream inputStream = classEntry.zipFile.getInputStream(zipEntry)) { |
| ClassReader classReader = new ClassReader(inputStream); |
| ImmutableList.Builder<ResolutionFailureChain> resolutionFailureChainsBuilder = |
| ImmutableList.builder(); |
| for (String superName : |
| combineWithoutNull(classReader.getSuperName(), classReader.getInterfaces())) { |
| Optional<ResolutionFailureChain> failurePath = |
| resolveSuperClassEntry(superName, lazyClasspath); |
| failurePath.map(resolutionFailureChainsBuilder::add); |
| } |
| ClassInfoBuilder classInfoBuilder = |
| new ClassInfoBuilder().setJarPath(classEntry.jarPath).setDirect(classEntry.isDirectDep); |
| // Only visit the class if we need to extract its list of members. If we do visit, skip |
| // code and debug attributes since we just care about finding declarations here. |
| if (lazyClasspath.populateMembers) { |
| classReader.accept( |
| classInfoBuilder, |
| ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); |
| } else { |
| classInfoBuilder.setNames( |
| classReader.getClassName(), classReader.getSuperName(), classReader.getInterfaces()); |
| } |
| |
| ImmutableList<ResolutionFailureChain> resolutionFailureChains = |
| resolutionFailureChainsBuilder.build(); |
| if (resolutionFailureChains.isEmpty()) { |
| classEntry.state = |
| ExistingState.create( |
| classInfoBuilder.build(lazyClasspath, /*incomplete=*/ false), explicitUse); |
| } else { |
| ClassInfo classInfo = classInfoBuilder.build(lazyClasspath, /*incomplete=*/ true); |
| classEntry.state = |
| IncompleteState.create( |
| classInfo, |
| ResolutionFailureChain.createWithParent(classInfo, resolutionFailureChains)); |
| } |
| } catch (IOException e) { |
| throw new RuntimeException("Error when resolving class entry " + entryName, e); |
| } catch (RuntimeException e) { |
| System.err.println( |
| "A runtime exception occurred. The following is the content in the class index. " |
| + e.getMessage()); |
| lazyClasspath.printClasspath(System.err); |
| throw e; |
| } |
| } |
| |
| private static Optional<ResolutionFailureChain> resolveSuperClassEntry( |
| String superName, LazyClasspath lazyClasspath) { |
| LazyClassEntry superClassEntry = lazyClasspath.getLazyEntry(superName); |
| |
| if (superClassEntry == null) { |
| return Optional.of(ResolutionFailureChain.createMissingClass(superName)); |
| } else { |
| resolveClassEntry(superClassEntry, lazyClasspath, /* explicitUse= */ false); |
| AbstractClassEntryState superState = superClassEntry.state; |
| if (superState instanceof ExistingState) { |
| // Do nothing. Good to proceed. |
| return Optional.empty(); |
| } else if (superState instanceof IncompleteState) { |
| return Optional.of(superState.asIncompleteState().resolutionFailureChain()); |
| } else { |
| throw new RuntimeException("Cannot reach here. superState is " + superState); |
| } |
| } |
| } |
| } |
| |
| private static ImmutableList<String> combineWithoutNull( |
| @Nullable String first, @Nullable String[] others) { |
| ImmutableList.Builder<String> list = ImmutableList.builder(); |
| if (first != null) { |
| list.add(first); |
| } |
| if (others != null) { |
| list.add(others); |
| } |
| return list.build(); |
| } |
| |
| /** The classpath, emulating the behavior of the real classpath. */ |
| @VisibleForTesting |
| static final class LazyClasspath implements Closeable { |
| private final ClassIndex bootclasspath; |
| private final ClassIndex regularClasspath; |
| private final ClassIndex inputJars; |
| private final ImmutableList<ClassIndex> orderedClasspath; |
| final boolean populateMembers; // accessed from other inner classes |
| private final Closer closer = Closer.create(); |
| |
| public LazyClasspath( |
| ImmutableSet<Path> bootclasspath, |
| ImmutableSet<Path> directClasspath, |
| ImmutableSet<Path> regularClasspath, |
| ImmutableSet<Path> inputJars, |
| boolean populateMembers) |
| throws IOException { |
| this.populateMembers = populateMembers; |
| this.bootclasspath = new ClassIndex("boot classpath", bootclasspath, Predicates.alwaysTrue()); |
| this.inputJars = new ClassIndex("input jars", inputJars, Predicates.alwaysTrue()); |
| this.regularClasspath = |
| new ClassIndex( |
| "regular classpath", |
| regularClasspath, |
| jar -> |
| bootclasspath.contains(jar) |
| || inputJars.contains(jar) |
| || directClasspath.contains(jar)); |
| // Reflect runtime resolution order, with input before classpath similar to javac |
| this.orderedClasspath = |
| ImmutableList.of(this.bootclasspath, this.inputJars, this.regularClasspath); |
| this.orderedClasspath.forEach(closer::register); |
| } |
| |
| public LazyClassEntry getLazyEntry(String internalName) { |
| return orderedClasspath |
| .stream() |
| .map(classIndex -> classIndex.getClassEntry(internalName)) |
| .filter(Objects::nonNull) |
| .findFirst() |
| .orElse(null); |
| } |
| |
| public ImmutableMap<Path, Boolean> collectUsedJarsInRegularClasspath() { |
| return regularClasspath.collectUsedJarFiles(); |
| } |
| |
| public void printClasspath(PrintStream stream) { |
| orderedClasspath.forEach(c -> c.printClasspath(stream)); |
| } |
| |
| @Override |
| public void close() throws IOException { |
| closer.close(); |
| } |
| } |
| |
| /** |
| * Representation of a class path, composed of a list of JARs. It indexes all the class files with |
| * the class names. |
| */ |
| private static final class ClassIndex implements Closeable { |
| |
| private final String name; |
| private final ImmutableMap<String, LazyClassEntry> classIndex; |
| private final Closer closer; |
| |
| public ClassIndex(String name, ImmutableSet<Path> jarFiles, Predicate<Path> isDirect) |
| throws IOException { |
| this.name = name; |
| this.closer = Closer.create(); |
| classIndex = buildClassIndex(jarFiles, closer, isDirect); |
| } |
| |
| @Override |
| public void close() throws IOException { |
| closer.close(); |
| } |
| |
| public LazyClassEntry getClassEntry(String internalName) { |
| return classIndex.get(internalName); |
| } |
| |
| /** Second argument in the Map is if the jar is used directly (at least once). */ |
| public ImmutableMap<Path, Boolean> collectUsedJarFiles() { |
| Map<Path, Boolean> usedJars = new HashMap<>(); |
| for (Map.Entry<String, LazyClassEntry> entry : classIndex.entrySet()) { |
| LazyClassEntry clazz = entry.getValue(); |
| if (clazz.isResolved()) { |
| if (!usedJars.containsKey(clazz.jarPath) || clazz.state.direct()) { |
| usedJars.put(clazz.jarPath, clazz.state.direct()); |
| } |
| } |
| } |
| return ImmutableSortedMap.copyOf(usedJars); |
| } |
| |
| private void printClasspath(PrintStream stream) { |
| stream.println("Classpath: " + name); |
| int counter = 0; |
| for (Map.Entry<String, LazyClassEntry> entry : classIndex.entrySet()) { |
| stream.printf("%d %s\n %s\n\n", ++counter, entry.getKey(), entry.getValue()); |
| } |
| } |
| |
| private static ImmutableMap<String, LazyClassEntry> buildClassIndex( |
| ImmutableSet<Path> jars, Closer closer, Predicate<Path> isDirect) throws IOException { |
| HashMap<String, LazyClassEntry> result = new HashMap<>(); |
| for (Path jarPath : jars) { |
| boolean jarIsDirect = isDirect.test(jarPath); |
| try { |
| ZipFile zipFile = closer.register(new ZipFile(jarPath.toFile())); |
| zipFile |
| .stream() |
| .forEach( |
| entry -> { |
| String name = entry.getName(); |
| if (!name.endsWith(".class")) { |
| return; // Not a class file. |
| } |
| String internalName = name.substring(0, name.lastIndexOf('.')); |
| result.computeIfAbsent( |
| internalName, |
| key -> new LazyClassEntry(key, zipFile, jarPath, jarIsDirect)); |
| }); |
| } catch (Throwable e) { |
| throw new RuntimeException("Error in reading zip file " + jarPath, e); |
| } |
| } |
| return ImmutableMap.copyOf(result); |
| } |
| } |
| |
| /** Builder to build a ClassInfo object from the class file. */ |
| private static class ClassInfoBuilder extends ClassVisitor { |
| |
| private String internalName; |
| private final ImmutableSet.Builder<MemberInfo> members = ImmutableSet.builder(); |
| private ImmutableList<String> superClasses; |
| private Path jarPath; |
| private boolean directDep; |
| |
| public ClassInfoBuilder() { |
| super(Opcodes.ASM7); |
| } |
| |
| @Override |
| public void visit( |
| int version, |
| int access, |
| String name, |
| String signature, |
| String superName, |
| String[] interfaces) { |
| setNames(name, superName, interfaces); |
| } |
| |
| @Override |
| public FieldVisitor visitField( |
| int access, String name, String desc, String signature, Object value) { |
| members.add(MemberInfo.create(name, desc)); |
| return null; |
| } |
| |
| @Override |
| public MethodVisitor visitMethod( |
| int access, String name, String desc, String signature, String[] exceptions) { |
| members.add(MemberInfo.create(name, desc)); |
| return null; |
| } |
| |
| void setNames(String name, String superName, String[] interfaces) { |
| checkState(internalName == null && superClasses == null, "This visitor is already used."); |
| internalName = name; |
| superClasses = combineWithoutNull(superName, interfaces); |
| } |
| |
| public ClassInfoBuilder setJarPath(Path jarPath) { |
| this.jarPath = jarPath; |
| return this; |
| } |
| |
| public ClassInfoBuilder setDirect(boolean direct) { |
| this.directDep = direct; |
| return this; |
| } |
| |
| public ClassInfo build(LazyClasspath lazyClasspath, boolean incomplete) { |
| ImmutableList<ClassInfo> superClassInfos = |
| superClasses |
| .stream() |
| .map(lazyClasspath::getLazyEntry) |
| // nulls possible when building ClassInfo for an "incomplete" class |
| .filter(entry -> entry != null && entry.state != null) |
| .map(entry -> entry.state.classInfo().get()) |
| .collect(ImmutableList.toImmutableList()); |
| checkState( |
| incomplete || superClassInfos.size() == superClasses.size(), |
| "Missing class info for some of %s's super types %s: %s", |
| internalName, |
| superClasses, |
| superClassInfos); |
| return ClassInfo.create( |
| checkNotNull(internalName), |
| checkNotNull(jarPath), |
| directDep, |
| superClassInfos, |
| members.build()); |
| } |
| } |
| } |