Skip to content

Commit

Permalink
Cleanup and add documentation (#7)
Browse files Browse the repository at this point in the history
* Configure JVM toolchain to Java 16

Prevents any errors arising from accidentally loading the project in a
Java 11 environment.

* Cleanup buildscript

Removes some pesky warnings in IDEA, uses java.withSourcesJar instead
of manually defining the sources task (so the proper Gradle module
information is included and created).

* Update dependencies

 - securejarhandler, from 0.9.44 to 0.9.54
 - Gradle Versions Plugin, from 0.38.0 to 0.39.0
 - gradle-modules-plugin, from 1.8.3 to 1.8.10

* Update to Gradle 7.2

* Improve debug messages, cleanup code readability

Uses proper names instead of shorthands when possible.
Debug messages are more structured, for improved readability.

* Add documentation in the form of comments

Some areas which are more self-explanatory (being simple code or having
nearby debug messages which explain it) are not commented.

* Add README

* Fix publishing block

Somehow the developers block was moved outside the pom block.

* tweak readme

Signed-off-by: cpw <[email protected]>

Co-authored-by: Marc Hermans <[email protected]>
Co-authored-by: cpw <[email protected]>
  • Loading branch information
3 people authored Dec 29, 2021
1 parent fde1128 commit 8ea1310
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 85 deletions.
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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<String[]>` 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()
84 changes: 40 additions & 44 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -10,7 +10,7 @@ plugins {
group 'cpw.mods'

version = gradleutils.getTagOffsetVersion()
logger.lifecycle('Version: ' + version)
logger.lifecycle("Version: $version")

repositories {
mavenLocal()
Expand All @@ -19,36 +19,35 @@ 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 {
fromTag "0.1"
}

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 {
Expand All @@ -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:[email protected]:McModLauncher/bootstraplauncher.git'
developerConnection = 'scm:git:[email protected]:McModLauncher/bootstraplauncher.git'
}
issueManagement {
system = 'github'
url = 'https://github.com/McModLauncher/bootstraplauncher/issues'
}

developers {
developer {
id = 'cpw'
name = 'cpw'
}
connection = 'scm:git:[email protected]:McModLauncher/bootstraplauncher.git'
developerConnection = 'scm:git:[email protected]:McModLauncher/bootstraplauncher.git'
}
issueManagement {
system = 'github'
url = 'https://github.com/McModLauncher/bootstraplauncher/issues'
}
developers {
developer {
id = 'cpw'
name = 'cpw'
}
}
}
Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion gradlew
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ case "`uname`" in
Darwin* )
darwin=true
;;
MINGW* )
MSYS* | MINGW* )
msys=true
;;
NONSTOP* )
Expand Down
124 changes: 85 additions & 39 deletions src/main/java/cpw/mods/bootstraplauncher/BootstrapLauncher.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String>();
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<String>();
// The list of all SecureJars, which represent one module
var jars = new ArrayList<SecureJar>();
// 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<Integer, List<Path>>();

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<String[]>)loader.stream().findFirst().orElseThrow().get()).accept(args);
((Consumer<String[]>) loader.stream().findFirst().orElseThrow().get()).accept(args);
}

private static Map<String, Integer> 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<String, Integer> filenameMap = new HashMap<>();
int i = 0;
Expand All @@ -109,17 +152,19 @@ private static Map<String, Integer> getMergeFilenameMap() {
return filenameMap;
}

private record PkgTracker(Set<String> packages, Path... paths) implements BiPredicate<String, String> {
private record PackageTracker(Set<String> packages, Path... paths) implements BiPredicate<String, String> {
@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
}
}

Expand All @@ -138,7 +183,8 @@ private static List<String> 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));
}
}

0 comments on commit 8ea1310

Please sign in to comment.