Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ Node, which is how the tests run.
IndexedDB and one VM per about:blank iframe. Keep these working.
- The release form is an npm package of the built files only
(`java-web.js`, `java-web-worker.js`, `node.js` (the Node entry), `cli.js` (the
`java-web` command), `boot.pack`, `jdk/<module>.jar`,
`jdk/javac.pack`, `jdk/jdk.localedata.txt`, and OpenJDK's notices in `jdk/legal/`), versioned, with
`java-web` command), `boot.pack`, `jdk/image.jar`,
`jdk/javac.pack`, and OpenJDK's notices in `jdk/legal/`), versioned, with
nothing built at install. A GitHub Actions workflow triggered by a release
publishes it to npm; never publish by hand.

Expand All @@ -42,7 +42,7 @@ Node, which is how the tests run.
- `src/classfile.ts` class file parser
- `src/runtime.ts` classes, methods, fields, objects, arrays
- `src/interp.ts` bytecode interpreter and per-thread run loop
- `src/jvm.ts` loading, linking, initialization, resolution, scheduler, boot
- `src/jvm.ts` loading, linking, initialization, resolution, scheduler, boot; `src/jimage.ts` reads the linked JDK image
- `src/indy.ts` invokedynamic, method handles
- `src/snapshot.ts` boot snapshots: saving and restoring the VM as it is after boot
- `src/library.ts` library mode in the VM: object handles, calls, field access,
Expand Down Expand Up @@ -76,12 +76,9 @@ Node, which is how the tests run.

## Glossary

- **java.home image**: `build/jdk`, the Linux x64 JDK's class library in the
exploded layout (`modules/<module>/...` instead of a `lib/modules` jimage),
built the same on every host. Mounted at
`/jdk` in the VFS. It has no `lib/modules`, which would switch the JDK to
image mode; `lib/resources.jimage` holds only the non-class resources, for
`jrt:` URLs.
- **java.home image**: `build/jdk`, a linked image with the Linux x64 JDK's
class library in `lib/modules`, built the same on every host and mounted at
`/jdk` in the VFS.
- **VFS**: the virtual file system the Java side sees (`src/fs.ts`).
- **native**: a JS function implementing a Java `native` method, keyed by
`class.name(desc)`. A plain native returns its result; a generator native
Expand Down
8 changes: 5 additions & 3 deletions docs/browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ The npm package holds the built files: `java-web.js`, `java-web-worker.js`,
and load `java-web.js` with a classic `<script>` tag. It finds the other files
next to itself, or where `cheerpjInit({ runtime: "/path/to/dist/" })` says.

- The server should answer range requests for the `jdk/<module>.jar` files,
so a page reads only the classes it needs. A host that compresses ranged
responses still works, at the cost of one full download per file.
- The server should answer range requests for `jdk/image.jar`, the whole JDK
image, so a page reads only the classes it needs. `boot.pack` holds what a
typical start reads, and `jdk/javac.pack` what javac adds. A host that
compresses ranged responses still works, at the cost of one full download of
`jdk/image.jar` (about 120 MB).
- The JIT compiles hot Java methods with `new Function`, so a page with a
Content Security Policy needs `'unsafe-eval'` in `script-src`. Without it,
Java still runs, only interpreted.
Expand Down
7 changes: 6 additions & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ same download on Linux x64) into `~/.cache/java-web`, and checks each SHA-256.
The runtime image comes from the Linux x64 JDK, so every machine, a Mac
included, builds the same image, with the Linux class library whose natives
java-web implements. The host JDK is the HotSpot reference the tests run, linked
at `build/hotspot`.
at `build/hotspot`. The build links the selected modules into `build/jdk/lib/modules`;
`lib/packages.txt` indexes packages for the boot class loader. `pnpm build`
puts it, with the files around it, in `dist/jdk/image.jar`, read by byte range.
It packs the byte ranges a typical start reads into `boot.pack`, and those javac
adds into `jdk/javac.pack`. Without `localeData`, the VM leaves `jdk.localedata`
out with `jdk.module.limitmods`, as `--limit-modules` does.

```sh
pnpm install
Expand Down
2 changes: 1 addition & 1 deletion docs/node.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ npx @project516/java-web [options] -jar app.jar [args]
| `--locale-data` | locales other than English (see [support.md](support.md#locales)) |
| `-Xmx` | the largest array, and what `Runtime.maxMemory()` reports (default and at most 1g, rounded up to 2 MB steps, below 2m an error); the total heap is not counted |
| `--max-rss MB` | exit when resident memory passes this (default 2048) |
| `--jdk DIR` | an exploded JDK image instead of the packaged one |
| `--jdk DIR` | a java-web JDK image instead of the packaged one |
| `--debug` | log VM events |

Other `-X` and HotSpot-only options are ignored.
Expand Down
7 changes: 3 additions & 4 deletions docs/support.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ java-web targets headless Java: console apps, libraries and javac.
- `java.desktop`'s headless parts work: 2D geometry, image pixels, `Graphics2D`
drawing on a `BufferedImage`, and JavaBeans. So does `java.xml`, including
XML Schema validation and DTDs from the JDK's built-in catalog.
- `jrt:/` URLs read the JDK's resources but not its class files.
- `Deflater` writes valid DEFLATE streams using stored blocks. Compression
levels and strategies currently do not reduce output size; compressed bytes
and compression ratios differ from HotSpot's zlib output.
Expand All @@ -19,14 +18,14 @@ java-web targets headless Java: console apps, libraries and javac.
## Locales

Locales other than English need `cheerpjInit({ localeData: true })`, or
`--locale-data` under Node (`localeData: true` for `runNode`). Turn it on
`--locale-data` under Node (`localeData: true` for `runNode`). Adding
`jdk.localedata` or `ALL-SYSTEM` with `--add-modules` turns it on too. Turn it on
when a program formats numbers, dates or currencies, or prints month and day
names, for a locale other than English: for example
`String.format(Locale.GERMANY, ...)`, or a German default locale. Without it,
those locales format as English. It is off by default because, once on, it
slows the first formatting call of every program, English ones included, by
a few hundred milliseconds. With it on, a page also fetches the locale file
list (`jdk/jdk.localedata.txt`, 160 KB) at start, then only the locales it
up to a second on slow devices. With it on, a page reads only the locales it
uses.

## Graphics and text
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"dist/boot.pack",
"dist/jdk/*.jar",
"dist/jdk/javac.pack",
"dist/jdk/jdk.localedata.txt",
"dist/jdk/legal/"
],
"publishConfig": {
Expand Down
4 changes: 1 addition & 3 deletions scripts/ExplodedModuleInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
import java.nio.file.Files;
import java.nio.file.Path;

// Drops ModuleHashes from module-info.class files, as a JDK exploded build
// has none: exploded modules cannot be hashed, and boot fails when java.base
// records hashes of other modules.
// Drop hashes of the original JDK modules before linking a different set.
void main(String[] args) throws Exception {
ClassFile cf = ClassFile.of();
for (String a : args) {
Expand Down
15 changes: 15 additions & 0 deletions scripts/HashedModules.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import java.lang.classfile.Attributes;
import java.lang.classfile.ClassFile;
import java.nio.file.Path;
import java.util.Set;
import java.util.stream.Collectors;

// Prints a jmod --hash-modules pattern for the modules a java.base
// module-info.class records hashes of, among the given ones.
// Args: <java.base module-info.class> <modules, space separated>
void main(String[] args) throws Exception {
var hashes = ClassFile.of().parse(Path.of(args[0])).findAttribute(Attributes.moduleHashes()).orElseThrow();
Set<String> linked = Set.of(args[1].split(" "));
System.out.println(hashes.hashes().stream().map(h -> h.moduleName().name().stringValue())
.filter(linked::contains).sorted().collect(Collectors.joining("|", "^(", ")$")));
}
28 changes: 1 addition & 27 deletions scripts/LinuxImage.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import java.io.DataInputStream;
import java.io.IOException;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleDescriptor.Requires;
Expand All @@ -14,9 +13,7 @@
import java.util.Set;
import java.util.TreeSet;

// Builds the java.home image from a Linux JDK without jlink: keeps the root
// modules and the modules they require, as jlink does without --bind-services,
// and copies the conf/, lib/ and legal/ files of those modules.
// Copies the conf/, lib/ and legal/ files of the linked modules.
// Args: <JDK home> <modules extracted from its lib/modules> <root modules, comma separated> <output dir>
void main(String[] args) throws IOException {
Path jdk = Path.of(args[0]), src = Path.of(args[1]), out = Path.of(args[3]);
Expand All @@ -36,7 +33,6 @@ void main(String[] args) throws IOException {
// Each module's files are listed in jdk.jlink as type|kind|hash or link target|path
Path lists = src.resolve("jdk.jlink/jdk/tools/jlink/internal/runtimelink");
for (String name : keep) {
Files.move(src.resolve(name), out.resolve("modules").resolve(name));
Path list = lists.resolve("fs_" + name + "_files");
if (Files.notExists(list)) continue;
for (String line : Files.readAllLines(list)) {
Expand All @@ -54,30 +50,8 @@ void main(String[] args) throws IOException {
}
}

restoreSystemModulesMap(src, out);

String version = Files.readAllLines(jdk.resolve("release")).stream()
.filter(l -> l.startsWith("JAVA_VERSION=")).findFirst().orElseThrow();
Files.writeString(out.resolve("release"), version + "\nMODULES=\"" + String.join(" ", keep) + "\"\n");
}

// jlink's system-modules plugin generated SystemModules classes for the whole
// JDK and pointed SystemModulesMap at them. The image has no lib/modules, so
// the JDK must find its modules by scanning, which the original map allows.
// diff_java.base holds the original bytes for undoing the plugin.
void restoreSystemModulesMap(Path src, Path out) throws IOException {
Path diff = src.resolve("jdk.jlink/jdk/tools/jlink/internal/runtimelink/diff_java.base");
String dir = "/java.base/jdk/internal/module/";
try (var in = new DataInputStream(Files.newInputStream(diff))) {
in.readInt();
for (int n = in.readInt(); n > 0; n--) {
short kind = in.readShort();
String name = new String(in.readNBytes(in.readInt()));
byte[] original = in.readNBytes(in.readInt());
if (!name.startsWith(dir)) continue;
Path file = out.resolve("modules").resolve(name.substring(1));
if (kind == 1) Files.delete(file);
else Files.write(file, original);
}
}
}
59 changes: 0 additions & 59 deletions scripts/ResourceImage.java

This file was deleted.

22 changes: 22 additions & 0 deletions scripts/UndoJlink.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import java.io.DataInputStream;
import java.nio.file.Files;
import java.nio.file.Path;

void main(String[] args) throws Exception {
Path src = Path.of(args[0]);
Path diff = src.resolve("jdk.jlink/jdk/tools/jlink/internal/runtimelink/diff_java.base");
try (var in = new DataInputStream(Files.newInputStream(diff))) {
in.readInt();
for (int n = in.readInt(); n > 0; n--) {
short kind = in.readShort();
String name = new String(in.readNBytes(in.readInt()));
byte[] original = in.readNBytes(in.readInt());
Path file = src.resolve(name.substring(1));
if (kind == 1) Files.deleteIfExists(file);
else {
Files.createDirectories(file.getParent());
Files.write(file, original);
}
}
}
}
54 changes: 32 additions & 22 deletions scripts/build-jdk.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
#!/usr/bin/env bash
# Builds build/jdk, the java.home the JVM runs on: the Linux x64 JDK 25 class
# library in the exploded layout (modules/<module>/...) instead of a
# lib/modules jimage, so classes and resources are plain files.
# Builds a linked Linux x64 java.home for the VM on every host.
set -euo pipefail

cd "$(dirname "$0")/.."
Expand Down Expand Up @@ -51,12 +49,12 @@ mkdir -p build
ln -sfn "$JAVA_HOME" build/hotspot

# The image comes from the Linux x64 JDK on every host, since the natives are
# the Linux ones. Temurin ships no jmods, so jlink cannot link it: the modules
# are extracted from its lib/modules and selected by scripts/LinuxImage.java.
# the Linux ones. Extract its modules for jlink and restore their original
# contents before linking.
linux=$(jdk_home x64_linux)
src=build/jdk-src
rm -rf "$src" "$out"
mkdir -p "$src" "$out/modules"
rm -rf "$src" "$out" build/jdk-linked
mkdir -p "$src" "$out"
"$JAVA_HOME/bin/jimage" extract --dir "$src" "$linux/lib/modules"

# javaweb.tls is java-web's own TLS pass-through provider (scripts/tls), built
Expand All @@ -65,18 +63,35 @@ mkdir -p "$src" "$out/modules"
"$JAVA_HOME/bin/javac" -d "$src/javaweb.tls" $(find scripts/tls -name '*.java')

"$JAVA_HOME/bin/java" scripts/LinuxImage.java "$linux" "$src" "$modules" "$out"
rm -rf "$src"
"$JAVA_HOME/bin/java" scripts/UndoJlink.java "$src"

# Without the generated SystemModules classes the JDK finds its modules by
# reading module-info.class files from the exploded tree.
"$JAVA_HOME/bin/java" scripts/ExplodedModuleInfo.java "$out"/modules/*/module-info.class
# java.base records a hash of each JDK module linked with it, and the JDK leaves
# those modules' versions out of stack traces. jlink cannot hash module
# directories, so the modules go into jmods (with fixed timestamps, so the image
# comes out the same every time) and java.base.jmod records their hashes anew,
# for the modules Temurin's java.base records.
linked=$(sed -n 's/^MODULES="\(.*\)"/\1/p' "$out/release")
hashed=$("$JAVA_HOME/bin/java" scripts/HashedModules.java "$src/java.base/module-info.class" "$linked")
"$JAVA_HOME/bin/java" scripts/ExplodedModuleInfo.java "$src"/*/module-info.class
jmods=build/jdk-jmods
rm -rf "$jmods"
mkdir -p "$jmods"
date=2025-01-01T00:00:00Z
for m in $linked; do
[ "$m" = java.base ] || "$JAVA_HOME/bin/jmod" create --date "$date" --class-path "$src/$m" "$jmods/$m.jmod"
done
"$JAVA_HOME/bin/jmod" create --date "$date" --class-path "$src/java.base" --module-path "$jmods" --hash-modules "$hashed" "$jmods/java.base.jmod"
"$JAVA_HOME/bin/jlink" --module-path "$jmods" --add-modules "$modules" --output build/jdk-linked
rm -rf "$jmods"
mkdir -p "$out/lib"
cp build/jdk-linked/lib/modules "$out/lib/modules"
rm -rf build/jdk-linked

# jrt: URLs read their resources through the jimage reader. Classes stay in
# modules/, so the image holds only the other resources. It is not named
# lib/modules, which would switch the JDK to image mode.
work=$(mktemp -d "$PWD/build/work.XXXXXX")
"$JAVA_HOME/bin/java" scripts/ResourceImage.java "$out/modules" "$work" "$out/lib/resources.jimage"
rm -rf "$work"
# Index the packages in the linked modules.
(cd "$src" && export LC_ALL=C && for m in $(sed -n 's/^MODULES="\(.*\)"/\1/p' "$OLDPWD/$out/release"); do
find "$m" -mindepth 2 -name '*.class' | sed 's|/[^/]*$||' | sort -u | sed "s|^$m/||;s|^|$m |"
done) > "$out/lib/packages.txt"
rm -rf "$src"

# The same provider list with JavaWebTLS first, for java.security.properties
sec=$out/conf/security
Expand Down Expand Up @@ -110,11 +125,6 @@ mkdir -p "$out/lib/fonts"
cp "$font_home/ttf/DejaVuSans.ttf" "$out/lib/fonts/"
cp "$font_home/LICENSE" "$out/legal/java.desktop/dejavu-fonts.txt"

# module -> packages index, so the boot loader can find a class without probing.
(cd "$out/modules" && export LC_ALL=C && for m in *; do
find "$m" -mindepth 2 -name '*.class' | sed 's|/[^/]*$||' | sort -u | sed "s|^$m/||;s|^|$m |"
done) > "$out/lib/packages.txt"

# a digest of the whole image, which boot snapshots are made for
(cd "$out" && node -e '
const { createHash } = require("node:crypto");
Expand Down
Loading
Loading