Skip to content

Commit 14e217e

Browse files
asifuddin01claude
andcommitted
Officina: Java runs too, and the isolation headers come back off
Java compiles and runs in the page: ecj compiles the cell, a WebAssembly JVM runs the class. 23 worked examples, from a first program to a sieve. Two findings shaped it, both measured rather than assumed. The isolation headers added with the C/C++ toolchain were not needed and are removed. emception wants a SharedArrayBuffer for interactive stdin and does without one when it cannot have it — clang, wasm-ld and the programs they build all run on a page that is not cross-origin isolated. Keeping the headers would have bought a stdin this notebook does not offer, and it would have cost Java outright: CheerpJ runs its JVM in an iframe on its own origin that sends no COEP header of its own, so `require-corp` blocks it and every call hangs with no error at all. That was an hour of a JVM that answered nothing. CheerpJ ships a JRE, not a JDK, so `com.sun.tools.javac.Main` is not there to call. The compiler is ecj, served from this origin, pinned to the 3.26 line: later releases need Java 17 to run, and on 17 they refuse to start with "invalid location for system libraries" because a modular JDK has no rt.jar to read. Java 8 has one, 3.26 runs on 8. The loader is the first and only thing this site fetches from another origin. CheerpJ's free licence permits its runtime to be served only from the vendor's domain, so the choice was that or no Java. scripts/audit.mjs now carries the exception, scoped to that origin and that one page, and prints it on every run rather than passing in silence — and /officina credits CheerpJ, as the licence asks. Java's entry point is a class, so the file is named after whatever the cell declares: `public class Foo` must live in Foo.java, and always writing Main.java would reject good programs over a filename the reader never chose. Output is collected by borrowing console.log for the length of a run, because CheerpJ offers no stdout of its own, and ecj's diagnostics say "your code" where they would otherwise print a scratch path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 667c5d2 commit 14e217e

8 files changed

Lines changed: 632 additions & 43 deletions

File tree

astro.config.mjs

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -118,29 +118,6 @@ export default defineConfig({
118118
vite: {
119119
plugins: [
120120
tailwindcss(),
121-
/**
122-
* The C/C++ toolchain in /officina runs clang as WebAssembly across a
123-
* worker, which needs SharedArrayBuffer, which browsers only hand to a
124-
* cross-origin-isolated page. `require-corp` is safe to apply site-wide
125-
* here for the same reason the audit exists: every subresource this
126-
* site loads is already same-origin, so there is nothing left to block.
127-
* The deployed Worker sets the same pair on its own responses.
128-
*
129-
* `vite.server.headers` is ignored under `astro dev`, so the isolation
130-
* headers go on as middleware instead. Without them SharedArrayBuffer
131-
* is undefined and the toolchain refuses to start — in dev only; the
132-
* deployed Worker sets the same pair on its own responses.
133-
*/
134-
{
135-
name: 'officina-cross-origin-isolation',
136-
configureServer(server) {
137-
server.middlewares.use((_req, res, next) => {
138-
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
139-
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
140-
next();
141-
});
142-
},
143-
},
144121
],
145122
/**
146123
* The toolchain adapter imports its Python shim as `./subprocess_shim.py?raw`.

public/_headers

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
11
# Cloudflare Pages headers.
22
#
3-
# The last two on /* are cross-origin isolation, which is what lets /officina
4-
# hand clang a SharedArrayBuffer and compile C in the page.
5-
#
6-
# Site-wide rather than on /officina alone, for two reasons. The pages are
7-
# linked with a client-side router, so arriving at /officina from another page
8-
# loads no new document and would inherit that page's headers — the toolchain
9-
# would work on a reload and not on a click. And `require-corp` blocks
10-
# subresources from other origins, which this site has none of by rule: the
11-
# audit fails the build if one appears. The isolation costs nothing here
12-
# precisely because that rule is already kept.
3+
# Deliberately no Cross-Origin-Embedder-Policy. The C/C++ toolchain in
4+
# /officina wants a SharedArrayBuffer for interactive stdin and does without
5+
# one when it cannot have it — clang, wasm-ld and the programs they build all
6+
# run on a page that is not cross-origin isolated, which was worth measuring
7+
# rather than assuming. Isolation would buy a stdin this notebook does not
8+
# offer, and it would cost Java: CheerpJ runs its JVM in an iframe on its own
9+
# origin that sends no COEP header of its own, so `require-corp` blocks it and
10+
# every call hangs with no error at all.
1311

1412
/*
1513
X-Content-Type-Options: nosniff
1614
Referrer-Policy: strict-origin-when-cross-origin
1715
X-Frame-Options: DENY
1816
Permissions-Policy: geolocation=(), microphone=(), camera=(), interest-cohort=()
1917
Strict-Transport-Security: max-age=31536000; includeSubDomains
20-
Cross-Origin-Opener-Policy: same-origin
21-
Cross-Origin-Embedder-Policy: require-corp
2218

2319
# Astro fingerprints everything under /_astro/, so it can be cached forever.
2420
/_astro/*

public/java/ecj.jar

2.99 MB
Binary file not shown.

scripts/audit.mjs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,26 @@ const SITE_ORIGIN = (
1717
await readFile('src/consts.ts', 'utf8')
1818
).match(/SITE = '([^']*)'/)?.[1] ?? '';
1919

20+
/**
21+
* The one thing on this site that is loaded from somewhere else.
22+
*
23+
* /officina runs Java, and running Java in a browser means a JVM. CheerpJ is
24+
* the only working one, and its Community Licence — the free tier, which is
25+
* what an individual gets — permits use *only* from the vendor's own domain:
26+
* self-hosting the runtime requires a commercial licence. So Java is either
27+
* this exception or nothing, and it was chosen deliberately over nothing.
28+
*
29+
* Scoped to the page that needs it and to that vendor's origin, so the rule
30+
* still holds everywhere else and this stays one visible, arguable decision
31+
* rather than a hole. The page carries the attribution the licence asks for.
32+
*/
33+
const ALLOWED_EXTERNAL = [
34+
{ origin: 'https://cjrtnc.leaningtech.com/', page: '/officina' },
35+
];
36+
37+
const isAllowed = (page, url) =>
38+
ALLOWED_EXTERNAL.some((a) => page === a.page && url.startsWith(a.origin));
39+
2040
let fail = 0;
2141
const problems = [];
2242

@@ -71,6 +91,7 @@ for await (const file of walk('dist')) {
7191
// rel=canonical/alternate/sitemap point at our own site by design
7292
if (/rel="(?:canonical|alternate|sitemap)"/i.test(m[0])) continue;
7393
if (m[1].startsWith(SITE_ORIGIN)) continue;
94+
if (isAllowed(page, m[1])) continue;
7495
problems.push(`${page} external subresource: ${m[1]}`);
7596
}
7697
}
@@ -98,6 +119,9 @@ if (problems.length) {
98119
for (const p of problems) console.log(' ✗ ' + p);
99120
fail = 1;
100121
} else {
101-
console.log('✓ headings, alt text, landmarks, lang and self-hosting all clean');
122+
console.log(
123+
'✓ headings, alt text, landmarks, lang and self-hosting all clean ' +
124+
`(${ALLOWED_EXTERNAL.length} declared exception: the Java runtime on /officina)`
125+
);
102126
}
103127
process.exit(fail);

src/content.config.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -980,15 +980,16 @@ const recipes = defineCollection({
980980
loader: glob({ pattern: '**/*.mdx', base: './src/content/recipes' }),
981981
schema: z.object({
982982
/** Must match a language the workshop knows: python, c, cpp, asm. */
983-
language: z.enum(['python', 'c', 'cpp', 'asm']),
983+
language: z.enum(['python', 'c', 'cpp', 'java', 'asm']),
984984
label: z.string(),
985985
/** Sets the order of the language tabs. */
986986
order: z.number().int().min(1),
987987
/**
988988
* Whether this language executes in the browser, and on what.
989989
*
990-
* Python runs on Pyodide, C and C++ on a clang/wasm-ld toolchain that is
991-
* fetched only when a cell is actually run — the packs are tens of
990+
* Python runs on Pyodide, C and C++ on a clang/wasm-ld toolchain, and
991+
* Java on a JVM plus the Eclipse batch compiler. All three are fetched
992+
* only when a cell is actually run — the C/C++ packs alone are tens of
992993
* megabytes, so nobody who does not compile ever pays for them. Assembly
993994
* is architecture-specific and has no assembler here, so it is written and
994995
* exported rather than run.
@@ -998,7 +999,7 @@ const recipes = defineCollection({
998999
* refinement below rejects that pairing at build time.
9991000
*/
10001001
runnable: z.boolean().default(false),
1001-
runtime: z.enum(['pyodide', 'emception', 'none']).default('none'),
1002+
runtime: z.enum(['pyodide', 'emception', 'cheerpj', 'none']).default('none'),
10021003
/** File extension for a downloaded cell. */
10031004
extension: z.string(),
10041005
/** How to run it outside the browser, shown where a Run button is not. */

src/content/recipes/asm.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
language: asm
33
label: Assembly
4-
order: 4
4+
order: 5
55
runnable: false
66
runtime: none
77
extension: .s

0 commit comments

Comments
 (0)