diff --git a/README.md b/README.md new file mode 100644 index 0000000..64d1a73 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# BootstrapLauncher + +> Allows bootstrapping a modularized environment from a classpath one. + +BootstrapLauncher (BSL for short) uses the following information: + +- The **(legacy) classpath information.** This is retrieved from the following, in descending order of priority: + - The `legacyClassPath.file` system property, containing a _path list_ (paths separated by `;` on Windows and `:` on + UNIX, as defined by [`File.pathSeparatorChar`][path_separator]) + - The `legacyClassPath` system property, containing a path list + - The `java.class.path` system property. + + If none of the above is present, then an exception is thrown. + +- The **ignore list**, specified by the `ignoreList` system property, as a comma-separated list of values. For any path + within the classpath (as retrieved above) whose filename begins with any value in the ignore list, the path is ignored + by BSL and not included in the bootstrap module layer created by BSL. + + By default, the ignore list is set to ignore filenames that start with `asm` or `securejarhandler` (the dependencies + of BSL). + +- The optional **module merge information**, specified by the `mergeModules` system property. This is used to combine + multiple JAR files into a single logical module in the eyes of the module system. This property is a list of groups of + comma-separated paths, where each group is separated by semicolons and denotes one module. + + For example: `a.jar,b.jar;b.jar,c.jar` means `a.jar` and `b.jar` are combined into one module, and `b.jar` and `c.jar` + are combined into another module. + +- The **bootstrap service**, which is a `Consumer` service provided by a module in the bootstrap module layer. + At least one such bootstrap service must exist, otherwise an exception is thrown. [ModLauncher][modlauncher] provides + one such service: `BootstrapLaunchConsumer`. + +Each JAR (unless included in the above module merge information) maps to one module in the bootstrap module layer. +Because all modules share the same classloader, no module may share a package with another module. Therefore, packages +are tracked and the first JAR which contains the module effectively 'owns' that package, and later JARs will not be +searched for the same package. + +BSL creates a new module layer which has the following properties: + +- The name of the module layer is `MC-BOOTSTRAP`. +- Its parent layer and configuration is the boot configuration (from [`ModuleLayer#boot()`][bootmodule]). +- It contains all the modules as provided in the classpath information (excluded from which the JARs who match the + ignore list) and mapped according to the optional module merge information. + +For easier debugging, additional debugging information is printed to `System.out` if the `bsl.debug` system property is +defined (regardless of its actual value). + +[path_separator]: https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/io/File.html#pathSeparatorChar +[modlauncher]: https://github.com/McModLauncher/modlauncher +[bootmodule]: https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/lang/ModuleLayer.html#boot() \ No newline at end of file diff --git a/build.gradle b/build.gradle index ddab79a..9cf69a1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ plugins { id 'net.minecraftforge.gradleutils' version '2.+' - id 'com.github.ben-manes.versions' version '0.38.0' - id 'org.javamodularity.moduleplugin' version '1.8.3' + id 'com.github.ben-manes.versions' version '0.39.0' + id 'org.javamodularity.moduleplugin' version '1.8.10' id 'java-library' id 'maven-publish' id 'eclipse' @@ -10,7 +10,7 @@ plugins { group 'cpw.mods' version = gradleutils.getTagOffsetVersion() -logger.lifecycle('Version: ' + version) +logger.lifecycle("Version: $version") repositories { mavenLocal() @@ -19,14 +19,14 @@ repositories { url 'https://maven.minecraftforge.net/' } } + dependencies { implementation('cpw.mods:securejarhandler:0.9.54') } - -task sourcesJar(type: Jar) { - archiveClassifier = 'sources' - from sourceSets.main.allSource +java { + toolchain.languageVersion = JavaLanguageVersion.of(16) + withSourcesJar() } changelog { @@ -34,21 +34,20 @@ changelog { } jar { - manifest { - attributes( - 'Specification-Title': 'bootstraplauncher', - 'Specification-Vendor': 'forge', - 'Specification-Version': '1', // We are version 1 of ourselves - 'Implementation-Title': project.name, - 'Implementation-Version': "${project.version}+${System.getenv("BUILD_NUMBER")?:0}+${gradleutils.gitInfo.branch}.${gradleutils.gitInfo.abbreviatedId}", - 'Implementation-Vendor':'forge', - 'Implementation-Timestamp': java.time.Instant.now().toString(), - 'Git-Commit': gradleutils.gitInfo.abbreviatedId, - 'Git-Branch': gradleutils.gitInfo.branch, - 'Build-Number': "${System.getenv("BUILD_NUMBER")?:0}", - 'Main-Class': 'cpw.mods.bootstraplauncher.BootstrapLauncher' - ) - } + //noinspection UnnecessaryQualifiedReference + manifest.attributes( + 'Specification-Title': 'bootstraplauncher', + 'Specification-Vendor': 'forge', + 'Specification-Version': '1', // We are version 1 of ourselves + 'Implementation-Title': project.name, + 'Implementation-Version': "${project.version}+${System.getenv("BUILD_NUMBER") ?: 0}+${gradleutils.gitInfo.branch}.${gradleutils.gitInfo.abbreviatedId}", + 'Implementation-Vendor': 'forge', + 'Implementation-Timestamp': java.time.Instant.now().toString(), + 'Git-Commit': gradleutils.gitInfo.abbreviatedId, + 'Git-Branch': gradleutils.gitInfo.branch, + 'Build-Number': "${System.getenv("BUILD_NUMBER") ?: 0}", + 'Main-Class': 'cpw.mods.bootstraplauncher.BootstrapLauncher' + ) } artifacts { @@ -57,29 +56,26 @@ artifacts { } publishing { - publications { - mavenJava(MavenPublication) { - from components.java - artifact sourcesJar - pom { - name = 'Bootstrap launcher' - description = 'Allows bootstrapping a modularized environment from a classpath one' + publications.register('mavenJava', MavenPublication) { + from components.java + artifact sourcesJar + pom { + name = 'Bootstrap launcher' + description = 'Allows bootstrapping a modularized environment from a classpath one' + url = 'https://github.com/McModLauncher/bootstraplauncher' + scm { url = 'https://github.com/McModLauncher/bootstraplauncher' - scm { - url = 'https://github.com/McModLauncher/bootstraplauncher' - connection = 'scm:git:git@github.com:McModLauncher/bootstraplauncher.git' - developerConnection = 'scm:git:git@github.com:McModLauncher/bootstraplauncher.git' - } - issueManagement { - system = 'github' - url = 'https://github.com/McModLauncher/bootstraplauncher/issues' - } - - developers { - developer { - id = 'cpw' - name = 'cpw' - } + connection = 'scm:git:git@github.com:McModLauncher/bootstraplauncher.git' + developerConnection = 'scm:git:git@github.com:McModLauncher/bootstraplauncher.git' + } + issueManagement { + system = 'github' + url = 'https://github.com/McModLauncher/bootstraplauncher/issues' + } + developers { + developer { + id = 'cpw' + name = 'cpw' } } } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c..7454180 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 05679dc..ffed3a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0..744e882 100755 --- a/gradlew +++ b/gradlew @@ -72,7 +72,7 @@ case "`uname`" in Darwin* ) darwin=true ;; - MINGW* ) + MSYS* | MINGW* ) msys=true ;; NONSTOP* ) diff --git a/src/main/java/cpw/mods/bootstraplauncher/BootstrapLauncher.java b/src/main/java/cpw/mods/bootstraplauncher/BootstrapLauncher.java index bda8fc5..83ee2fb 100644 --- a/src/main/java/cpw/mods/bootstraplauncher/BootstrapLauncher.java +++ b/src/main/java/cpw/mods/bootstraplauncher/BootstrapLauncher.java @@ -8,7 +8,6 @@ import java.io.IOException; import java.lang.module.ModuleFinder; import java.nio.file.Files; -import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -25,76 +24,120 @@ public class BootstrapLauncher { private static final boolean DEBUG = System.getProperties().containsKey("bsl.debug"); + @SuppressWarnings("unchecked") public static void main(String[] args) { - var legacyCP = loadLegacyClassPath(); - System.setProperty("legacyClassPath", String.join(File.pathSeparator, legacyCP)); //Ensure backwards compatibility if somebody reads this value later on. - var ignoreList = System.getProperty("ignoreList", "asm,securejarhandler"); //TODO: find existing modules automatically instead of taking in an ignore list. + var legacyClasspath = loadLegacyClassPath(); + // Ensure backwards compatibility if somebody reads this value later on. + System.setProperty("legacyClassPath", String.join(File.pathSeparator, legacyClasspath)); + + // TODO: find existing modules automatically instead of taking in an ignore list. + // The ignore list exempts files that start with certain listed keywords from being turned into modules (like existing modules) + var ignoreList = System.getProperty("ignoreList", "asm,securejarhandler"); var ignores = ignoreList.split(","); - var previousPkgs = new HashSet(); - var jars = new ArrayList<>(); + // Tracks all previously encountered packages + // This prevents subsequent modules from including packages from previous modules, which is disallowed by the module system + var previousPackages = new HashSet(); + // The list of all SecureJars, which represent one module + var jars = new ArrayList(); + // Map of filenames to their 'module number', where all filenames sharing the same 'module number' is combined into one var filenameMap = getMergeFilenameMap(); + // Map of 'module number' to the list of paths which are combined into that module var mergeMap = new HashMap>(); outer: - for (var legacy : legacyCP) { + for (var legacy : legacyClasspath) { var path = Paths.get(legacy); var filename = path.getFileName().toString(); for (var filter : ignores) { if (filename.startsWith(filter)) { - if (DEBUG) - System.out.println(legacy + " IGNORED: " + filter); + if (DEBUG) { + System.out.println("bsl: file '" + legacy + "' ignored because filename starts with '" + filter + "'"); + } continue outer; } } - if (DEBUG) - System.out.println(path); + if (DEBUG) { + System.out.println("bsl: encountered path '" + legacy + "'"); + } if (filenameMap.containsKey(filename)) { + if (DEBUG) { + System.out.println("bsl: path is contained with module #" + filenameMap.get(filename) + ", skipping for now"); + } mergeMap.computeIfAbsent(filenameMap.get(filename), k -> new ArrayList<>()).add(path); continue; } - var jar = SecureJar.from(new PkgTracker(Set.copyOf(previousPkgs), path), path); - var pkgs = jar.getPackages(); - if (DEBUG) - pkgs.forEach(p -> System.out.println(" " + p)); - previousPkgs.addAll(pkgs); + + var jar = SecureJar.from(new PackageTracker(Set.copyOf(previousPackages), path), path); + var packages = jar.getPackages(); + + if (DEBUG) { + System.out.println("bsl: list of packages for file '" + legacy + "'"); + packages.forEach(p -> System.out.println("bsl: " + p)); + } + + previousPackages.addAll(packages); jars.add(jar); } + + // Iterate over merged modules map and combine them into one SecureJar each mergeMap.forEach((idx, paths) -> { var pathsArray = paths.toArray(Path[]::new); - var jar = SecureJar.from(new PkgTracker(Set.copyOf(previousPkgs), pathsArray), pathsArray); - var pkgs = jar.getPackages(); + var jar = SecureJar.from(new PackageTracker(Set.copyOf(previousPackages), pathsArray), pathsArray); + var packages = jar.getPackages(); + if (DEBUG) { - paths.forEach(System.out::println); - pkgs.forEach(p -> System.out.println(" " + p)); + System.out.println("bsl: the following paths are merged together in module #" + idx); + paths.forEach(path -> System.out.println("bsl: " + path)); + System.out.println("bsl: list of packages for module #" + idx); + packages.forEach(p -> System.out.println("bsl: " + p)); } - previousPkgs.addAll(pkgs); + + previousPackages.addAll(packages); jars.add(jar); }); - var finder = jars.toArray(SecureJar[]::new); - - var alltargets = Arrays.stream(finder).map(SecureJar::name).toList(); - var jf = JarModuleFinder.of(finder); - var cf = ModuleLayer.boot().configuration(); - var newcf = cf.resolveAndBind(jf, ModuleFinder.ofSystem(), alltargets); - var mycl = new ModuleClassLoader("MC-BOOTSTRAP", newcf, List.of(ModuleLayer.boot())); - var layer = ModuleLayer.defineModules(newcf, List.of(ModuleLayer.boot()), m->mycl); - Thread.currentThread().setContextClassLoader(mycl); + var secureJarsArray = jars.toArray(SecureJar[]::new); + + // Gather all the module names from the SecureJars + var allTargets = Arrays.stream(secureJarsArray).map(SecureJar::name).toList(); + // Creates a module finder which uses the list of SecureJars to find modules from + var jarModuleFinder = JarModuleFinder.of(secureJarsArray); + // Retrieve the boot layer's configuration + var bootModuleConfiguration = ModuleLayer.boot().configuration(); + + // Creates the module layer configuration for the bootstrap layer module + // The parent configuration is the boot layer configuration (above) + // The `before` module finder, used to find modules "in" this layer, and is the jar module finder above + // The `after` module finder, used to find modules that aren't in the jar module finder or the parent configuration, + // is the system module finder (which is probably in the boot configuration :hmmm:) + // And the list of root modules for this configuration (that is, the modules that 'belong' to the configuration) are + // the above modules from the SecureJars + var bootstrapConfiguration = bootModuleConfiguration.resolveAndBind(jarModuleFinder, ModuleFinder.ofSystem(), allTargets); + // Creates the module class loader, which does the loading of classes and resources from the bootstrap module layer/configuration, + // falling back to the boot layer if not in the bootstrap layer + var moduleClassLoader = new ModuleClassLoader("MC-BOOTSTRAP", bootstrapConfiguration, List.of(ModuleLayer.boot())); + // Actually create the module layer, using the bootstrap configuration above, the boot layer as the parent layer (as configured), + // and mapping all modules to the module class loader + var layer = ModuleLayer.defineModules(bootstrapConfiguration, List.of(ModuleLayer.boot()), m -> moduleClassLoader); + // Set the context class loader to the module class loader from this point forward + Thread.currentThread().setContextClassLoader(moduleClassLoader); final var loader = ServiceLoader.load(layer.layer(), Consumer.class); // This *should* find the service exposed by ModLauncher's BootstrapLaunchConsumer {This doc is here to help find that class next time we go looking} - ((Consumer)loader.stream().findFirst().orElseThrow().get()).accept(args); + ((Consumer) loader.stream().findFirst().orElseThrow().get()).accept(args); } private static Map getMergeFilenameMap() { - // filename1.jar,filename2.jar;filename2.jar,filename3.jar var mergeModules = System.getProperty("mergeModules"); if (mergeModules == null) return Map.of(); + // `mergeModules` is a semicolon-separated set of comma-separated set of paths, where each (comma) set of paths is + // combined into a single modules + // example: filename1.jar,filename2.jar;filename2.jar,filename3.jar Map filenameMap = new HashMap<>(); int i = 0; @@ -109,17 +152,19 @@ private static Map getMergeFilenameMap() { return filenameMap; } - private record PkgTracker(Set packages, Path... paths) implements BiPredicate { + private record PackageTracker(Set packages, Path... paths) implements BiPredicate { @Override public boolean test(final String path, final String basePath) { - if (packages.isEmpty() || // the first jar, nothing is claimed yet - path.startsWith("META-INF/")) // Every module can have a meta-inf + // This method returns true if the given path is allowed within the JAR (filters out 'bad' paths) + + if (packages.isEmpty() || // This is the first jar, nothing is claimed yet, so allow everything + path.startsWith("META-INF/")) // Every module can have their own META-INF return true; int idx = path.lastIndexOf('/'); - return idx < 0 || // Something in the root of the module. + return idx < 0 || // Resources at the root are allowed to co-exist idx == path.length() - 1 || // All directories can have a potential to exist without conflict, we only care about real files. - !packages.contains(path.substring(0, idx).replace('/', '.')); + !packages.contains(path.substring(0, idx).replace('/', '.')); // If the package hasn't been used by a previous JAR } } @@ -138,7 +183,8 @@ private static List loadLegacyClassPath() { } } - return Arrays.asList(Objects.requireNonNull(System.getProperty("legacyClassPath", System.getProperty("java.class.path")), "Missing legacyClassPath, cannot bootstrap") - .split(File.pathSeparator)); + var legacyClasspath = System.getProperty("legacyClassPath", System.getProperty("java.class.path")); + Objects.requireNonNull(legacyClasspath, "Missing legacyClassPath, cannot bootstrap"); + return Arrays.asList(legacyClasspath.split(File.pathSeparator)); } }