Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
03f99ee
Evaluate @requires in the OpenJDK runner instead of skipping every te…
Project516 Nov 5, 2026
9bf8d4b
Apply the requested mode to new files and directories, and add jdk.ma…
Project516 Nov 6, 2026
f02a61f
Reject unknown user names, make File.createNewFile fail on an empty p…
Project516 Nov 6, 2026
3aa0324
Answer HotSpotDiagnosticMXBean.getVMOption for the VM options java-we…
Project516 Nov 6, 2026
a3ae7fd
Search the parent's PATH for child programs as the JDK launcher does,…
Project516 Nov 6, 2026
1772d57
Add openat, unlinkat, renameat, fstatat, fchmodat, fdopendir and dup …
Project516 Nov 6, 2026
e70cf17
Support symbolic links and hard links on host file systems, and resol…
Project516 Nov 6, 2026
08fb05d
Report EACCES for an unreadable file, ELOOP for O_NOFOLLOW on a link,…
Project516 Nov 6, 2026
edec36b
Share an inode between hard links and let renameat move a dangling link
Project516 Nov 6, 2026
68bfa01
Let File.createNewFile see a dangling link as an existing file
Project516 Nov 6, 2026
f408262
Fail an empty program name with ENOENT, split environment entries at …
Project516 Nov 6, 2026
1113ab0
Compare directory-relative file operations and program lookup with Ho…
Project516 Nov 6, 2026
615eeb5
Use the host's inode number and link count for file keys, so links to…
Project516 Nov 6, 2026
09bebd2
Treat O_CREAT with O_EXCL on a dangling link as an existing file
Project516 Nov 6, 2026
de6501d
Set the modification time of a link itself when asked not to follow it
Project516 Nov 6, 2026
35165cf
Document links, inode numbers, the runner's @requires and the VM feat…
Project516 Nov 6, 2026
591679d
Run 47 more OpenJDK tests that now pass
Project516 Nov 6, 2026
7c1acf8
Merge remote-tracking branch 'origin/master' into p516/jtreg-fixes
Project516 Nov 6, 2026
ab79fb4
Hold a host descriptor on each open file so its inode number is not r…
Project516 Nov 6, 2026
a03ae47
Close directory streams in the new test programs, release pinned desc…
Project516 Nov 6, 2026
03df77d
Merge branch 'master' into p516/jtreg-fixes
Project516 Nov 6, 2026
090ff9e
Keep the mount prefix when canonicalizing a mount point.
Project516 Nov 6, 2026
ebc7bbc
Evaluate the runner's @requires ~= as a full match, as jtreg does.
Project516 Nov 6, 2026
54c5c43
Limit the inode and mode claims in the support notes to host file sys…
Project516 Nov 6, 2026
be9cee6
Treat any non-regular file on PATH as EACCES when looking up a program.
Project516 Nov 6, 2026
961938a
Pin only the inode a file was read from without creating it, and shar…
Project516 Nov 6, 2026
2dbbc0c
Fail the directory-relative natives with the errors Linux gives for d…
Project516 Nov 6, 2026
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
4 changes: 4 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ node scripts/jtreg/run.ts <jdk-source> java/lang
without the `+build` part. A sparse checkout of `test/lib` and the test
directories is enough.

The runner evaluates a test's `@requires` as jtreg does on a release Linux
HotSpot with default options (for example `vm.continuations` is true and
`vm.debug` is false); a test that needs more is skipped.

`test/jtreg/passing.txt` lists the tests CI runs, with `--only-passing`.
`--shard k/n` splits them into n shards of about equal run time, using each
test's time on java-web from `test/jtreg/times.tsv` (the median for a test
Expand Down
13 changes: 12 additions & 1 deletion docs/support.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ java-web targets headless Java: console apps, libraries and javac.

- The image has every Java SE module (`java.se`) plus `jdk.compiler`,
`jdk.httpserver`, `jdk.jartool` (the `jar` tool), `jdk.security.jgss`,
`jdk.unsupported`, `jdk.zipfs`, `jdk.internal.md` (Markdown in doc comments,
`jdk.management` (`UnixOperatingSystemMXBean`), `jdk.unsupported`,
`jdk.zipfs`, `jdk.internal.md` (Markdown in doc comments,
for javac's doc comment API) and `jdk.localedata` (see Locales).
- `java.desktop`'s headless parts work: 2D geometry, image pixels, `Graphics2D`
drawing on a `BufferedImage`, and JavaBeans. So does `java.xml`, including
Expand All @@ -21,6 +22,16 @@ java-web targets headless Java: console apps, libraries and javac.
java-web VM on the same image. A child cannot use a directory that exists only
in the VM, such as `/jdk`, as its working directory. `ProcessHandle` reads
other processes from Linux's `/proc`; elsewhere it sees only its own children.
- Symbolic and hard links work on file systems that come from the host, as
under Node. Elsewhere, such as the browser's in-memory files, creating a link
fails with "Function not implemented". Files on host file systems have the
host's inode numbers and link counts, and the modes `open` and `mkdir` ask
for, less the process's umask. In-memory files get synthetic inode numbers, a
link count of 1 and default modes.
- `-XX:OnOutOfMemoryError` runs no command, and `StackWalker`
has no live locals or operands. `ThreadMXBean.getThreadInfo` and
`dumpThreads` are not implemented, and a virtual thread that yields while it
holds a monitor stays pinned to its carrier.
- `WatchService` reports files the program itself, or another thread in the
same VM, creates, changes or deletes. It does not see changes made outside
the VM, including by child processes the program starts.
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-jdk.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ set -euo pipefail

cd "$(dirname "$0")/.."
out=build/jdk
modules=${JDK_MODULES:-java.se,jdk.compiler,jdk.httpserver,jdk.jartool,jdk.security.jgss,jdk.unsupported,jdk.zipfs,jdk.localedata,jdk.internal.md,javaweb.tls}
modules=${JDK_MODULES:-java.se,jdk.compiler,jdk.httpserver,jdk.jartool,jdk.security.jgss,jdk.unsupported,jdk.zipfs,jdk.localedata,jdk.management,jdk.internal.md,javaweb.tls}

# Builds from one pinned JDK release so every machine makes the same image.
# Renovate bumps the release.
Expand Down
94 changes: 92 additions & 2 deletions scripts/jtreg/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { createHash } from "node:crypto";
import { spawn, spawnSync } from "node:child_process";
import type { ChildProcess } from "node:child_process";
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
import { totalmem } from "node:os";
import { dirname, join, relative, resolve } from "node:path";
import { hotspot } from "../hotspot.ts";

Expand Down Expand Up @@ -55,6 +56,89 @@ if (!existsSync(testRoot)) {
}
const javaHome = dirname(dirname(realpathSync(hotspot("java"))));

// ---- @requires --------------------------------------------------------------

// What jtreg reports for a release Linux HotSpot with default options. A
// property not listed makes the test skip.
const required: Record<string, string> = {
"os.family": "linux",
"os.arch": process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "amd64" : process.arch,
"os.maxMemory": String(totalmem()),
"sun.arch.data.model": "64",
"vm.bits": "64",
"vm.continuations": "true",
"vm.flagless": "true",
"vm.debug": "false",
"vm.musl": "false",
"vm.compMode": "Xmixed",
"vm.gc": "null",
"vm.gc.G1": "true",
"vm.gc.Serial": "true",
"vm.gc.Parallel": "true",
"vm.gc.Z": "false",
"vm.hasJFR": "true",
"vm.graal.enabled": "false",
"vm.cpu.features": "",
};

function size(s: string): number {
const m = s.match(/^(\d+)([kmgt]?)$/i);
return m ? Number(m[1]) * 1024 ** (m[2] ? "kmgt".indexOf(m[2].toLowerCase()) + 1 : 0) : NaN;
}

// Evaluates a jtreg @requires expression: & | ! ( ) and == != >= <= ~= over properties.
function requiresHolds(expr: string): boolean {
const toks = expr.match(/"[^"]*"|==|!=|>=|<=|~=|[()&|!<>]|[^\s()&|!=<>~"]+/g) ?? [];
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
let i = 0;
const value = (): string | null => {
const t = toks[i++];
if (t === undefined) throw new Error("incomplete");
if (t.startsWith('"')) return t.slice(1, -1);
if (/^[\d.]+[kmgt]?$/i.test(t) || t === "true" || t === "false") return t;
if (t.startsWith("vm.opt.")) return null;
if (t.startsWith("vm.gc.") && !(t in required)) return "false";
if (t in required) return required[t];
throw new Error(`unknown property ${t}`);
};
const atom = (): boolean => {
if (toks[i] === "(") {
i++;
const v = or();
if (toks[i++] !== ")") throw new Error("missing )");
return v;
}
if (toks[i] === "!") {
i++;
return !atom();
}
const a = value();
const op = toks[i];
if (!op || !/^(==|!=|>=|<=|~=|<|>)$/.test(op)) return a === "true";
i++;
const b = value();
switch (op) {
case "==": return a === b;
case "!=": return a !== b;
case "~=": return a !== null && new RegExp(`^(?:${b ?? ""})$`).test(a);
}
const x = size(a ?? ""), y = size(b ?? "");
Comment thread
project516-review-bot[bot] marked this conversation as resolved.
Resolved
return op === ">=" ? x >= y : op === "<=" ? x <= y : op === ">" ? x > y : x < y;
};
const and = (): boolean => {
let v = atom();
while (toks[i] === "&") { i++; const r = atom(); v = v && r; }
return v;
};
const or = (): boolean => {
let v = and();
while (toks[i] === "|") { i++; const r = and(); v = v || r; }
return v;
};
const v = or();
if (i < toks.length) throw new Error("trailing input");
return v;
}

type Kind = "main" | "testng" | "junit" | "compile";
// a compile action keeps its javac options in opts and its files in args
interface Action { kind: Kind; opts: string[]; main: string; args: string[]; timeout: number }
Expand Down Expand Up @@ -140,7 +224,13 @@ function parseFile(file: string): TestCase[] {
case "build": tc.build.push(...value.split(/\s+/).filter(Boolean)); break;
case "modules": tc.modules.push(...value.split(/\s+/).filter(Boolean)); break;
case "enablePreview": tc.preview = true; break;
case "requires": tc.skip ??= "requires"; break;
case "requires":
try {
if (!requiresHolds(value)) tc.skip ??= "requires";
} catch (e) {
tc.skip ??= `requires: ${(e as Error).message}`;
}
break;
case "ignore": tc.skip ??= "ignore"; break;
case "clean": tc.skip ??= "@clean"; break;
case "compile": {
Expand Down Expand Up @@ -341,7 +431,7 @@ function exec(cmd: string, args: string[], cwd: string, limit: number): Promise<
const start = Date.now();
const child = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"], detached: true });
let text = "";
const keep = (d: Buffer) => { text = (text + d.toString()).slice(-4000); };
const keep = (d: Buffer) => { text = (text + d.toString()).slice(-65536); };
child.stdout.on("data", keep);
child.stderr.on("data", keep);
let timedOut = false;
Expand Down
60 changes: 57 additions & 3 deletions src/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export interface Stat {
mode?: number;
// a character device; it streams and is never read whole
device?: boolean;
// lstat of a symbolic link: the length of its target, which is also its size
link?: number;
// the host's inode number and link count, so hard links and links to a file share a key
ino?: number;
nlink?: number;
}

// The same in every mount layout, so SecureRandom and the like work in the
Expand All @@ -29,6 +34,7 @@ export const DEVICES = new Set(["/dev/null", "/dev/zero", "/dev/random", "/dev/u
// Errors carry an errno name in `code`, as Node's do.
export interface FileHandle {
size(): number;
ino(): number;
read(dst: Uint8Array, off: number, len: number, pos: number): number;
write(src: Uint8Array, pos: number): void;
truncate(size: number): void;
Expand All @@ -55,14 +61,21 @@ export interface Backend {
// the file system holding path: block size and block counts, as statvfs gives them
space?(path: string): Space | null;
// sets the modification time, in milliseconds since the epoch
setMtime?(path: string, ms: number): boolean;
setMtime?(path: string, ms: number, noFollow?: boolean): boolean;
// whether the host refuses to write the file, or to create it when it is missing
denied?(path: string): boolean;
// sets the permission bits to f(current bits)
chmod?(path: string, f: (mode: number) => number): boolean;
chmodError?(path: string): string | undefined;
// whether the host allows reading (4) and executing (1), as access(2)
access?(path: string, mode: number): boolean;
// symbolic and hard links, for a host that has them; failures return the errno code
lstat?(path: string): Stat | null;
readlink?(path: string): { target: string } | { code: string };
symlink?(target: string, path: string): string | undefined;
link?(existing: string, path: string): string | undefined;
// the path with every link resolved, where the missing tail stays as written
realpath?(path: string): string | null;
}

export interface WarmTrace {
Expand Down Expand Up @@ -309,6 +322,7 @@ export class VFS {
// writes through the handle are changes watchers see, as writes through VFS.write are
return {
size: () => h.size(),
ino: () => h.ino(),
read: (dst, off, len, pos) => h.read(dst, off, len, pos),
close: () => h.close(),
write: (src, pos) => { h.write(src, pos); this.changed(path, "modify"); },
Expand Down Expand Up @@ -407,6 +421,45 @@ export class VFS {
return !!r && r[0].writable;
}

// like stat, but a symbolic link reports itself
lstat(path: string): Stat | null {
const r = this.resolve(path);
return (r && r[0].lstat?.(r[1])) || this.stat(path);
}

readlink(path: string): { target: string } | { code: string } {
const r = this.resolve(path);
return r?.[0].readlink?.(r[1]) ?? { code: this.stat(path) ? "EINVAL" : "ENOENT" };
}

// undefined on success, else the errno code; ENOSYS where the backend has no links
symlink(target: string, path: string): string | undefined {
const r = this.resolve(path);
if (!r || !r[0].writable || !r[0].symlink) return "ENOSYS";
const code = r[0].symlink(target, r[1]);
if (!code) this.changed(path, "create");
return code;
}

link(existing: string, path: string): string | undefined {
const a = this.resolve(existing), b = this.resolve(path);
if (!a || !b || a[0] !== b[0] || !b[0].writable || !b[0].link) return "ENOSYS";
const code = b[0].link(a[1], b[1]);
if (!code) this.changed(path, "create");
return code;
}

realpath(path: string): string | null {
const r = this.resolve(path);
if (!r?.[0].realpath) return null;
const real = r[0].realpath(r[1]);
if (real === null) return null;
const n = normalize(path);
// a mount point itself maps to "/" in its backend
const base = n.slice(0, n.length - (r[1] === "/" ? 0 : r[1].length));
return normalize(base + real);
}

// A file system without its own figures reports 4 KB blocks and no space,
// as Linux does for virtual file systems such as /proc.
space(path: string): Space {
Expand All @@ -417,10 +470,11 @@ export class VFS {
// ns, when given, is the exact time asked for. Hosts keep times as floating
// point seconds or milliseconds, which lose nanoseconds, so stat reports ns
// for as long as the file keeps the time this set.
setMtime(path: string, ms: number, ns?: bigint): boolean {
setMtime(path: string, ms: number, ns?: bigint, noFollow = false): boolean {
const r = this.resolve(path);
if (!r || !r[0].writable || !r[0].setMtime?.(r[1], ms)) return false;
if (!r || !r[0].writable || !r[0].setMtime?.(r[1], ms, noFollow)) return false;
this.changed(path, "modify");
if (noFollow) return true;
const n = normalize(path);
const st = ns === undefined ? null : r[0].stat(r[1]);
// only a time the host kept, give or take its rounding; a host can clamp it
Expand Down
Loading
Loading