From ceb4819f9e0c3a03bb89283e5ce297ceb4f42090 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 31 Oct 2026 23:30:12 +0900 Subject: [PATCH 1/6] Chore: bump python-multiplatform and typedpython-gate to 3.15.0-alpha02 (#119) --- .github/workflows/test.yml | 2 +- AGENTS.md | 2 +- docs/SPEC.md | 4 ++-- toolchain/build.gradle.kts | 3 ++- .../multiplatform/toolchain/typedpython/TypedpythonGate.kt | 2 +- .../toolchain/typedpython/TypedpythonGateArtifactTest.kt | 4 ++-- .../toolchain/typedpython/TypedpythonGateTest.kt | 4 ++-- usage-example/gradle/libs.versions.toml | 2 +- 8 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 600e583..25dfe74 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -123,7 +123,7 @@ jobs: # usage-example applies three things from mavenLocal: packpack (through the plugin), the # toolchain plugin, and python-multiplatform's library and bindings plugin. Versions pinned in - # usage-example/gradle/libs.versions.toml (3.15.0-alpha01). + # usage-example/gradle/libs.versions.toml (3.15.0-alpha02). - name: Publish packpack, the toolchain plugin and python-multiplatform to mavenLocal run: | (cd .ci/pypackpack && ./gradlew :packpack:publishToMavenLocal --console=plain) diff --git a/AGENTS.md b/AGENTS.md index 53206a3..3e924cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -246,7 +246,7 @@ Prerequisites: `org.thisisthepy.python.multiplatform:packpack:0.1.0` in `~/.m2` `pypackpack` repository), `uv` on `PATH`, and network access: `InstallDependenciesTaskTest` and `InstallerTest` run a real `uv add` against PyPI. `usage-example` additionally needs an Android SDK (`ANDROID_HOME`) and python-multiplatform in `~/.m2`: from a python-multiplatform checkout, -`./gradlew publishAllToMavenLocal` (`org.thisisthepy:python-multiplatform*` and bindings plugin `3.15.0-alpha01`). +`./gradlew publishAllToMavenLocal` (`org.thisisthepy:python-multiplatform*` and bindings plugin `3.15.0-alpha02`). Run each module separately (rule 8), with output to a file under `.cache/`: diff --git a/docs/SPEC.md b/docs/SPEC.md index 617ebd0..c5500d7 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -61,7 +61,7 @@ Test: `PythonSdkTest` (`a CPython before 3_15 is refused as a GIL build, inside **Temporary exception: `3.15.0` without `t` (`PY3_15_0`).** It names python-multiplatform's GIL build, which is what python-multiplatform publishes today: its `gradle.properties` has `pythonFreeThreaded=false`, -its artifact version (`3.15.0-alpha01`) carries no flavour, and Android and iOS have no 3.15t build yet +its artifact version (`3.15.0-alpha02`) carries no flavour, and Android and iOS have no 3.15t build yet (python-multiplatform #163). Selecting `cp315t` wheels for that runtime would install wheels it cannot load. The exception is removed, and `3.15.0` then means `3.15.0t`, when python-multiplatform publishes free-threaded as its default with the flavour in its version or metadata, and #163 lands; #76 tracks it. @@ -542,7 +542,7 @@ bundled. Every `buildPython` and the single host `buildPytho --find-links typedpython== pyrefly==` (`@Input` versions, defaults `0.1.0` and `1.3.2`). `` must hold both wheels for the platform; nothing comes from an index. 2. The **gate artifact**, by default: the `typedpython-gate` zip python-multiplatform-ksp publishes beside - its processor jar, `org.thisisthepy:python-multiplatform-ksp:3.15.0-alpha01:typedpython-gate@zip` + its processor jar, `org.thisisthepy:python-multiplatform-ksp:3.15.0-alpha02:typedpython-gate@zip` (`DEFAULT_TYPEDPYTHON_GATE`, the one place its group and version are written; `-Ptypedpython.gate= ` overrides it). It is resolved leniently from the project's own repositories (python-multiplatform's `publishToMavenLocal` puts it in `mavenLocal()`), unzipped into diff --git a/toolchain/build.gradle.kts b/toolchain/build.gradle.kts index 321f119..6ad66f2 100644 --- a/toolchain/build.gradle.kts +++ b/toolchain/build.gradle.kts @@ -93,7 +93,8 @@ gradlePlugin { // rejected as out of scope -- `compose-multiplatform 1.7.0` (`gradle/libs.versions.toml`) is not // verified against it, and that is exactly the kind of build-wide change a single delegation should // not carry. -// `TypedpythonCheckTaskTest` installs the real `typedpython` gate, which is not on PyPI yet: +// `TypedpythonCheckTaskTest` installs the real `typedpython` gate, which is not published to PyPI +// (it ships with the GitHub Release and the distribution): // `-Ptypedpython.wheelDir=` names the directory holding its wheel. Forwarded as-is; the test // fails with that instruction when it is missing rather than skipping. tasks.test { diff --git a/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/typedpython/TypedpythonGate.kt b/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/typedpython/TypedpythonGate.kt index 6060e6e..fdce0ac 100644 --- a/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/typedpython/TypedpythonGate.kt +++ b/toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/typedpython/TypedpythonGate.kt @@ -10,7 +10,7 @@ import java.io.File * uses the same). This is the one place both are written; `-Ptypedpython.gate=` * overrides it. */ -const val DEFAULT_TYPEDPYTHON_GATE = "org.thisisthepy:python-multiplatform-ksp:3.15.0-alpha01" +const val DEFAULT_TYPEDPYTHON_GATE = "org.thisisthepy:python-multiplatform-ksp:3.15.0-alpha02" /** Project property overriding [DEFAULT_TYPEDPYTHON_GATE]. */ const val TYPEDPYTHON_GATE_PROPERTY = "typedpython.gate" diff --git a/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/typedpython/TypedpythonGateArtifactTest.kt b/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/typedpython/TypedpythonGateArtifactTest.kt index 95ad4c3..ef36566 100644 --- a/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/typedpython/TypedpythonGateArtifactTest.kt +++ b/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/typedpython/TypedpythonGateArtifactTest.kt @@ -22,8 +22,8 @@ import kotlin.test.assertTrue class TypedpythonGateArtifactTest { private val mavenLocalZip = File( System.getProperty("user.home"), - ".m2/repository/org/thisisthepy/python-multiplatform-ksp/3.15.0-alpha01/" + - "python-multiplatform-ksp-3.15.0-alpha01-typedpython-gate.zip", + ".m2/repository/org/thisisthepy/python-multiplatform-ksp/3.15.0-alpha02/" + + "python-multiplatform-ksp-3.15.0-alpha02-typedpython-gate.zip", ) @BeforeTest diff --git a/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/typedpython/TypedpythonGateTest.kt b/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/typedpython/TypedpythonGateTest.kt index 05fada3..285cde8 100644 --- a/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/typedpython/TypedpythonGateTest.kt +++ b/toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/typedpython/TypedpythonGateTest.kt @@ -41,9 +41,9 @@ class TypedpythonGateTest { @Test fun `the gate is the typedpython-gate zip of python-multiplatform-ksp, from one constant`() { - assertEquals("org.thisisthepy:python-multiplatform-ksp:3.15.0-alpha01", DEFAULT_TYPEDPYTHON_GATE) + assertEquals("org.thisisthepy:python-multiplatform-ksp:3.15.0-alpha02", DEFAULT_TYPEDPYTHON_GATE) assertEquals( - "org.thisisthepy:python-multiplatform-ksp:3.15.0-alpha01:typedpython-gate@zip", + "org.thisisthepy:python-multiplatform-ksp:3.15.0-alpha02:typedpython-gate@zip", typedpythonGateNotation(DEFAULT_TYPEDPYTHON_GATE), ) assertEquals("typedpython.gate", TYPEDPYTHON_GATE_PROPERTY) diff --git a/usage-example/gradle/libs.versions.toml b/usage-example/gradle/libs.versions.toml index 8686b1f..8c67728 100644 --- a/usage-example/gradle/libs.versions.toml +++ b/usage-example/gradle/libs.versions.toml @@ -13,7 +13,7 @@ compose-plugin = "1.11.1-ext.3" kotlin = "2.4.20-Beta2" kotlinx-coroutines = "1.10.1" # python-multiplatform's library, KSP and bindings plugin share one version (its #70, #95, #318, #333). -python-multiplatform = "3.15.0-alpha01" +python-multiplatform = "3.15.0-alpha02" # The toolchain plugin, from ../toolchain via mavenLocal (`./gradlew :toolchain:publishToMavenLocal`). toolchain = "1.0.0-alpha" From 954923bcb5081ed66c36c1aec50e8b1fe3b67d41 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sun, 1 Nov 2026 11:18:32 +0900 Subject: [PATCH 2/6] Feat: enable WASM cross-origin isolation by default and wire backend gate install (#81) --- docs/SPEC.md | 49 ++- docs/guide/guide-staging.html | 27 ++ docs/guide/status.html | 3 +- .../multiplatform/toolchain/PythonPlugin.kt | 1 + .../typedpython/TypedpythonCheckTask.kt | 65 ++-- .../wasm/WasmCrossOriginIsolation.kt | 310 ++++++++++++++++++ .../wasm/WasmCrossOriginIsolationTest.kt | 128 ++++++++ .../src/wasmJsMain/resources/index.html | 1 + 8 files changed, 546 insertions(+), 38 deletions(-) create mode 100644 toolchain/src/main/kotlin/org/thisisthepy/python/multiplatform/toolchain/wasm/WasmCrossOriginIsolation.kt create mode 100644 toolchain/src/test/kotlin/org/thisisthepy/python/multiplatform/toolchain/wasm/WasmCrossOriginIsolationTest.kt diff --git a/docs/SPEC.md b/docs/SPEC.md index c5500d7..7ea3f22 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -527,11 +527,9 @@ bundled. Every `buildPython` and the single host `buildPytho `.py` re-runs the check, a change to any other file leaves it `UP-TO-DATE`. No package directory, or no `.py` file in it: the check is skipped with a log line and the gate is not installed. - **Gate.** The `typedpython` wheel and the `pyrefly` it pins, installed into `build/typedpython/venv`: - never the project's `.venv`. The venv is created through `pypackpack`'s UV backend - (`createVirtualEnvironment`, `--no-project --clear`, Python `>=3.13`; only when the venv does not exist). - The install calls `uv` directly: `pypackpack`'s backend has no operation that installs named - requirements into an existing venv (`installDependenciesToTarget` reads `-r pyproject.toml` into - `--target`; `addDependencies` edits `pyproject.toml`): a `pypackpack` change, not made here. + never the project's `.venv`. Both venv creation (`createVirtualEnvironment`, `--no-project --clear`, + Python `>=3.13`; only when the venv does not exist) and the requirement install into it + (`installRequirementsToVenv`, `pypackpack#40`) run through `pypackpack`'s UV backend. - **Index rule.** A package whose name is **unclaimed on PyPI** is never installed from an index: `typedpython` is unclaimed, so a bare-name install would run whatever someone publishes under that name on every consumer's build. A **hash-pinned** install from PyPI is allowed: `pyrefly` comes from @@ -629,6 +627,47 @@ decision. presses Publish. A manual run does all of it except the upload, building packpack from pypackpack's develop when Central does not have it yet. +### 1.20 WASM cross-origin isolation +Source: issue [`toolchain#81`](https://github.com/thisisthepy/toolchain/issues/81) (decision 2026-10-04). + +WASM runs free-threaded CPython 3.15t with pthreads. WebAssembly threads require `SharedArrayBuffer`, +which browsers only expose to cross-origin isolated pages (`self.crossOriginIsolated === true`). Cross-origin +isolation is on by default for every WASM app `toolchain` builds: + +- **Headers.** + - `Cross-Origin-Opener-Policy`: `same-origin` + - `Cross-Origin-Embedder-Policy`: `require-corp` by default (`-Ppython.wasm.coep=credentialless` selects + `credentialless`; any other value fails with a message naming both valid policies) + - `Cross-Origin-Resource-Policy`: `same-origin` on our own assets +- **Dev server.** Before any `wasmJs*Webpack` task runs, the plugin writes + `webpack.config.d/python-cross-origin-isolation.js` into the root build's `build/wasm/packages//` + directory (`patchWasmWebpackConfigDirectory`), setting those three headers on `config.devServer.headers` + (without overwriting headers the consumer already set). +- **Distribution assets (`stageWasmCrossOriginIsolation`).** Unpacks three files into + `build/pythonWasmIsolation/`, wired into `wasmJsProcessResources` so they ship at the root of the wasm + browser distribution: + 1. `_headers`: Cloudflare Pages and Netlify header rules setting the three headers on `/*`. + 2. `coi-serviceworker.js`: our own service-worker fallback for hosts that cannot set response headers + (such as GitHub Pages). When loaded in a window where `self.crossOriginIsolated` is false, it registers + itself as a service worker and reloads once on controller activation; the worker intercepts navigation + and same-origin requests and re-serves their responses with the three headers. Its header comment states + both costs: one reload on the first visit, and no effect when service workers are unavailable. + 3. `coi-check.html`: a diagnostic test page that loads `coi-serviceworker.js` and checks + `self.crossOriginIsolated` and `typeof SharedArrayBuffer === "function"`, failing loudly in the page and + console with instructions if the page is not cross-origin isolated (matching `python-multiplatform`'s + startup check). +- **Documented limits.** `docs/guide/guide-staging.html#wasm-isolation` documents the three browser limits of + cross-origin isolation (third-party iframes must opt in via CORP/COEP; popup OAuth and payment flows lose + `window.opener` under COOP `same-origin`; external resources without CORP or CORS are blocked under + `require-corp` while `credentialless` relaxes this for no-cookie requests) alongside plain-text header + snippets for nginx, Apache, Firebase Hosting and Vercel. + +**Status: implemented**. `plugin/wasm/WasmCrossOriginIsolation.kt`, wiring in `plugin/PythonPlugin.kt`; +`ptest/wasm/WasmCrossOriginIsolationTest.kt` (unit + ProjectBuilder: default `require-corp` and +`credentialless` policies, invalid COEP rejection, `_headers` content, service-worker script content and +documented costs, `coi-check.html` diagnostic page, webpack `config.d` generation, `stageWasmCrossOriginIsolation` +task output and `wasmJsProcessResources` wiring). + ## 2. `tcl`: toolchain-lite ### 2.1 `tcl install ` diff --git a/docs/guide/guide-staging.html b/docs/guide/guide-staging.html index 8ec2862..a446939 100644 --- a/docs/guide/guide-staging.html +++ b/docs/guide/guide-staging.html @@ -60,6 +60,33 @@

iOS: the Xcode build phasei

After staging스테이징 이후

Putting the staged python/ on sys.path when the app starts is the runtime's job: python-multiplatform's side of the boundary, not this plugin's.

앱이 시작할 때 스테이징된 python/ 을 sys.path 에 올리는 것은 런타임의 일입니다: 이 플러그인이 아니라 python-multiplatform 쪽 경계입니다.

+ +
+

WASM: cross-origin isolation and its limitsWASM: 교차 출처 격리와 그 한계

+

WASM runs free-threaded CPython 3.15t with pthreads. WebAssembly threads require SharedArrayBuffer, which browsers only expose when self.crossOriginIsolated === true: Cross-Origin-Opener-Policy: same-origin together with Cross-Origin-Embedder-Policy: require-corp (or credentialless via -Ppython.wasm.coep=credentialless), plus Cross-Origin-Resource-Policy: same-origin on the app's own assets. Cross-origin isolation is on by default for every WASM app this plugin builds:

WASM 은 pthreads 기반 자유 스레드 CPython 3.15t 로 실행됩니다. WebAssembly 스레드는 SharedArrayBuffer 가 필요하며, 브라우저는 self.crossOriginIsolated === true 일 때만 이를 열어 줍니다: Cross-Origin-Opener-Policy: same-origin 과 Cross-Origin-Embedder-Policy: require-corp(또는 -Ppython.wasm.coep=credentialless 로 고르는 credentialless), 그리고 앱 자체 자산의 Cross-Origin-Resource-Policy: same-origin 입니다. 이 플러그인이 빌드하는 모든 WASM 앱에는 교차 출처 격리가 기본으로 켜집니다:

+
    +
  • Dev server headers: every wasmJs*Webpack task writes webpack.config.d/python-cross-origin-isolation.js so the local webpack dev server sends all three headers automatically.개발 서버 헤더: 모든 wasmJs*Webpack 태스크가 webpack.config.d/python-cross-origin-isolation.js 를 기록해 로컬 webpack 개발 서버가 세 헤더를 자동으로 보냅니다.
  • +
  • Static host headers (_headers): stageWasmCrossOriginIsolation emits a Cloudflare Pages and Netlify _headers file into the WASM browser distribution root.정적 호스트 헤더(_headers): stageWasmCrossOriginIsolation 이 Cloudflare Pages 와 Netlify 형식의 _headers 파일을 WASM 브라우저 배포 루트에 생성합니다.
  • +
  • Service-worker fallback (coi-serviceworker.js) and check page (coi-check.html): for hosts that cannot set HTTP headers (such as GitHub Pages), coi-serviceworker.js intercepts fetches and re-serves responses with COOP, COEP and CORP. Its cost is one reload on the first visit, and it has no effect when service workers are unavailable (non-HTTPS origins other than localhost, or webviews with service workers disabled). At startup the runtime checks self.crossOriginIsolated and fails immediately rather than failing later inside a thread; coi-check.html tests the same invariant on its own.서비스 워커 폴백(coi-serviceworker.js)과 점검 페이지(coi-check.html): GitHub Pages 처럼 HTTP 헤더를 설정할 수 없는 호스트를 위해, coi-serviceworker.js 가 요청을 가로채 COOP, COEP, CORP 헤더를 붙여 응답합니다. 그 비용은 첫 방문 시 1회 새로고침이며, 서비스 워커를 쓸 수 없는 환경에서는 효과가 없습니다(localhost 이외의 비-HTTPS 출처나 서비스 워커가 꺼진 웹뷰). 시작 시 런타임이 self.crossOriginIsolated 를 확인해 스레드 안에서 뒤늦게 죽는 대신 즉시 실패하며, coi-check.html 로도 같은 조건을 바로 점검할 수 있습니다.
  • +
+

Browser limits of cross-origin isolation교차 출처 격리의 브라우저 제약

+
Surface영역What cross-origin isolation changes교차 출처 격리로 달라지는 점What to do대응 방법
Third-party <iframe>제3자 <iframe>Embedded cross-origin documents must opt in with CORP/COEP headers, or the browser refuses to load them.내장된 교차 출처 문서가 CORP/COEP 헤더로 동의하지 않으면 브라우저가 로드를 거부합니다.Serve the framed document with Cross-Origin-Embedder-Policy and Cross-Origin-Resource-Policy: cross-origin, or open it in a top-level navigation instead of an iframe.iframe 문서에 Cross-Origin-Embedder-Policy 와 Cross-Origin-Resource-Policy: cross-origin 을 설정하거나, iframe 대신 최상위 탐색으로 여세요.
Popup OAuth and payment flows팝업 기반 OAuth 및 결제 흐름Under Cross-Origin-Opener-Policy: same-origin, cross-origin popups lose window.opener (it becomes null), so postMessage handshakes back to the opener break.Cross-Origin-Opener-Policy: same-origin 아래에서는 교차 출처 팝업의 window.opener 가 null 이 되어, 부모 창으로의 postMessage 핸드셰이크가 끊깁니다.Use full-page redirect OAuth/payment flows, or configure the host with same-origin-allow-popups where the browser's isolation rules permit it.전체 페이지 리다이렉트 방식의 OAuth/결제 흐름을 쓰거나, 브라우저 격리 규칙이 허용하는 범위에서 호스트에 same-origin-allow-popups 를 설정하세요.
External images, scripts, fonts and media외부 이미지, 스크립트, 글꼴, 미디어Under Cross-Origin-Embedder-Policy: require-corp, any cross-origin subresource without a Cross-Origin-Resource-Policy or CORS header is blocked.Cross-Origin-Embedder-Policy: require-corp 아래에서는 Cross-Origin-Resource-Policy 나 CORS 헤더가 없는 교차 출처 하위 리소스가 차단됩니다.Add crossorigin attributes and serve CORS/CORP headers on the asset host, or build with -Ppython.wasm.coep=credentialless so no-cookie cross-origin requests load without CORP.crossorigin 속성을 붙이고 자산 호스트에서 CORS/CORP 헤더를 내려주거나, 쿠키 없는 교차 출처 요청을 CORP 없이 로드하도록 -Ppython.wasm.coep=credentialless 로 빌드하세요.
+

Header snippets for other hosts다른 호스트를 위한 헤더 설정 예시

+

Cloudflare Pages and Netlify read the generated _headers file directly. For nginx, Apache, Firebase Hosting or Vercel, add the same headers in the host configuration:

Cloudflare Pages 와 Netlify 는 생성된 _headers 파일을 바로 읽습니다. nginx, Apache, Firebase Hosting, Vercel 에서는 호스트 설정에 같은 헤더를 추가하세요:

+
# nginx
+add_header Cross-Origin-Opener-Policy "same-origin" always;
+add_header Cross-Origin-Embedder-Policy "require-corp" always;
+add_header Cross-Origin-Resource-Policy "same-origin" always;
+
+# Apache (.htaccess)
+Header always set Cross-Origin-Opener-Policy "same-origin"
+Header always set Cross-Origin-Embedder-Policy "require-corp"
+Header always set Cross-Origin-Resource-Policy "same-origin"
+
+# Firebase Hosting (firebase.json) / Vercel (vercel.json)
+# Set headers on "/(.*)" -> Cross-Origin-Opener-Policy: same-origin,
+# Cross-Origin-Embedder-Policy: require-corp, Cross-Origin-Resource-Policy: same-origin
+