1 /* 2 * Copyright 2018 Google Inc. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.turbine.binder; 18 19 import static com.google.common.base.StandardSystemProperty.JAVA_HOME; 20 import static java.util.Objects.requireNonNull; 21 22 import com.google.common.base.Joiner; 23 import com.google.common.base.Supplier; 24 import com.google.common.base.Suppliers; 25 import com.google.common.collect.HashBasedTable; 26 import com.google.common.collect.ImmutableList; 27 import com.google.common.collect.ImmutableMap; 28 import com.google.common.collect.ImmutableMultimap; 29 import com.google.common.collect.Multimap; 30 import com.google.common.collect.Table; 31 import com.google.turbine.binder.bound.ModuleInfo; 32 import com.google.turbine.binder.bytecode.BytecodeBinder; 33 import com.google.turbine.binder.bytecode.BytecodeBoundClass; 34 import com.google.turbine.binder.env.Env; 35 import com.google.turbine.binder.lookup.LookupKey; 36 import com.google.turbine.binder.lookup.LookupResult; 37 import com.google.turbine.binder.lookup.PackageScope; 38 import com.google.turbine.binder.lookup.Scope; 39 import com.google.turbine.binder.lookup.TopLevelIndex; 40 import com.google.turbine.binder.sym.ClassSymbol; 41 import com.google.turbine.binder.sym.ModuleSymbol; 42 import com.google.turbine.tree.Tree.Ident; 43 import java.io.IOException; 44 import java.io.UncheckedIOException; 45 import java.net.URI; 46 import java.nio.file.DirectoryStream; 47 import java.nio.file.FileSystem; 48 import java.nio.file.FileSystems; 49 import java.nio.file.Files; 50 import java.nio.file.Path; 51 import java.util.Collection; 52 import java.util.HashMap; 53 import java.util.HashSet; 54 import java.util.Map; 55 import java.util.Set; 56 import org.jspecify.nullness.Nullable; 57 58 /** Constructs a platform {@link ClassPath} from the current JDK's jimage file using jrtfs. */ 59 public class JimageClassBinder { 60 create(FileSystem fileSystem)61 static JimageClassBinder create(FileSystem fileSystem) throws IOException { 62 Path modules = fileSystem.getPath("/modules"); 63 Path packages = fileSystem.getPath("/packages"); 64 ImmutableMultimap.Builder<String, String> packageMap = ImmutableMultimap.builder(); 65 try (DirectoryStream<Path> ps = Files.newDirectoryStream(packages)) { 66 for (Path p : ps) { 67 String packageName = packages.relativize(p).toString().replace('.', '/'); 68 try (DirectoryStream<Path> ms = Files.newDirectoryStream(p)) { 69 for (Path m : ms) { 70 packageMap.put(packageName, p.relativize(m).toString()); 71 } 72 } 73 } 74 } 75 return new JimageClassBinder(packageMap.build(), modules); 76 } 77 78 /** Returns a platform classpath for the host JDK's jimage file. */ bindDefault()79 public static ClassPath bindDefault() throws IOException { 80 return JimageClassBinder.create(FileSystems.getFileSystem(URI.create("jrt:/"))) 81 .new JimageClassPath(); 82 } 83 84 /** Returns a platform classpath for the given JDK's jimage file. */ bind(String javaHome)85 public static ClassPath bind(String javaHome) throws IOException { 86 if (javaHome.equals(JAVA_HOME.value())) { 87 return bindDefault(); 88 } 89 FileSystem fileSystem = 90 FileSystems.newFileSystem(URI.create("jrt:/"), ImmutableMap.of("java.home", javaHome)); 91 return JimageClassBinder.create(fileSystem).new JimageClassPath(); 92 } 93 94 private final Multimap<String, String> packageMap; 95 private final Path modulesRoot; 96 97 private final Set<String> loadedPackages = new HashSet<>(); 98 private final Table<String, String, ClassSymbol> packageClassesBySimpleName = 99 HashBasedTable.create(); 100 private final Map<String, ModuleInfo> moduleMap = new HashMap<>(); 101 private final Map<ClassSymbol, BytecodeBoundClass> env = new HashMap<>(); 102 JimageClassBinder(ImmutableMultimap<String, String> packageMap, Path modules)103 public JimageClassBinder(ImmutableMultimap<String, String> packageMap, Path modules) { 104 this.packageMap = packageMap; 105 this.modulesRoot = modules; 106 } 107 108 @Nullable modulePath(String moduleName)109 Path modulePath(String moduleName) { 110 Path path = modulesRoot.resolve(moduleName); 111 return Files.exists(path) ? path : null; 112 } 113 114 @Nullable module(String moduleName)115 ModuleInfo module(String moduleName) { 116 ModuleInfo result = moduleMap.get(moduleName); 117 if (result == null) { 118 Path path = modulePath(moduleName); 119 if (path == null) { 120 return null; 121 } 122 path = path.resolve("module-info.class"); 123 result = BytecodeBinder.bindModuleInfo(path.toString(), toByteArrayOrDie(path)); 124 moduleMap.put(moduleName, result); 125 } 126 return result; 127 } 128 initPackage(String packageName)129 boolean initPackage(String packageName) { 130 Collection<String> moduleNames = packageMap.get(packageName); 131 if (moduleNames.isEmpty()) { 132 return false; 133 } 134 if (!loadedPackages.add(packageName)) { 135 return true; 136 } 137 Env<ClassSymbol, BytecodeBoundClass> env = 138 new Env<ClassSymbol, BytecodeBoundClass>() { 139 @Override 140 public @Nullable BytecodeBoundClass get(ClassSymbol sym) { 141 return JimageClassBinder.this.env.get(sym); 142 } 143 }; 144 for (String moduleName : moduleNames) { 145 if (moduleName != null) { 146 // TODO(cushon): is this requireNonNull safe? 147 Path modulePath = requireNonNull(modulePath(moduleName), moduleName); 148 Path modulePackagePath = modulePath.resolve(packageName); 149 try (DirectoryStream<Path> ds = Files.newDirectoryStream(modulePackagePath)) { 150 for (Path path : ds) { 151 if (!Files.isRegularFile(path) 152 || path.getFileName().toString().equals("module-info.class")) { 153 continue; 154 } 155 String binaryName = modulePath.relativize(path).toString(); 156 binaryName = binaryName.substring(0, binaryName.length() - ".class".length()); 157 ClassSymbol sym = new ClassSymbol(binaryName); 158 packageClassesBySimpleName.put(packageName, sym.simpleName(), sym); 159 JimageClassBinder.this.env.put( 160 sym, new BytecodeBoundClass(sym, toByteArrayOrDie(path), env, path.toString())); 161 } 162 } catch (IOException e) { 163 throw new UncheckedIOException(e); 164 } 165 } 166 } 167 return true; 168 } 169 toByteArrayOrDie(Path path)170 private static Supplier<byte[]> toByteArrayOrDie(Path path) { 171 return Suppliers.memoize( 172 new Supplier<byte[]>() { 173 @Override 174 public byte[] get() { 175 try { 176 return Files.readAllBytes(path); 177 } catch (IOException e) { 178 throw new UncheckedIOException(e); 179 } 180 } 181 }); 182 } 183 184 private class JimageTopLevelIndex implements TopLevelIndex { 185 186 final Scope topLevelScope = 187 new Scope() { 188 @Override 189 public @Nullable LookupResult lookup(LookupKey lookupKey) { 190 // Find the longest prefix of the key that corresponds to a package name. 191 // TODO(cushon): SimpleTopLevelIndex uses a prefix map for this, does it matter? 192 Scope scope = null; 193 ImmutableList<Ident> names = lookupKey.simpleNames(); 194 ImmutableList.Builder<String> flatNamesBuilder = ImmutableList.builder(); 195 for (Ident name : names) { 196 flatNamesBuilder.add(name.value()); 197 } 198 ImmutableList<String> flatNames = flatNamesBuilder.build(); 199 int idx = -1; 200 for (int i = 1; i < names.size(); i++) { 201 Scope cand = lookupPackage(flatNames.subList(0, i)); 202 if (cand != null) { 203 scope = cand; 204 idx = i; 205 } 206 } 207 return scope != null 208 ? scope.lookup(new LookupKey(names.subList(idx, names.size()))) 209 : null; 210 } 211 }; 212 213 @Override 214 public Scope scope() { 215 return topLevelScope; 216 } 217 218 @Override 219 public @Nullable PackageScope lookupPackage(Iterable<String> name) { 220 String packageName = Joiner.on('/').join(name); 221 if (!initPackage(packageName)) { 222 return null; 223 } 224 return new PackageScope() { 225 @Override 226 public @Nullable LookupResult lookup(LookupKey lookupKey) { 227 ClassSymbol sym = packageClassesBySimpleName.get(packageName, lookupKey.first().value()); 228 return sym != null ? new LookupResult(sym, lookupKey) : null; 229 } 230 231 @Override 232 public Iterable<ClassSymbol> classes() { 233 return packageClassesBySimpleName.row(packageName).values(); 234 } 235 }; 236 } 237 } 238 239 private class JimageClassPath implements ClassPath { 240 241 final TopLevelIndex index = new JimageTopLevelIndex(); 242 243 @Override 244 public Env<ClassSymbol, BytecodeBoundClass> env() { 245 return new Env<ClassSymbol, BytecodeBoundClass>() { 246 @Override 247 public @Nullable BytecodeBoundClass get(ClassSymbol sym) { 248 return initPackage(sym.packageName()) ? env.get(sym) : null; 249 } 250 }; 251 } 252 253 @Override 254 public Env<ModuleSymbol, ModuleInfo> moduleEnv() { 255 return new Env<ModuleSymbol, ModuleInfo>() { 256 @Override 257 public @Nullable ModuleInfo get(ModuleSymbol module) { 258 return module(module.name()); 259 } 260 }; 261 } 262 263 @Override 264 public TopLevelIndex index() { 265 return index; 266 } 267 268 @Override 269 public @Nullable Supplier<byte[]> resource(String input) { 270 return null; 271 } 272 } 273 } 274