Cheng Shao pushed to branch wip/wasm-ghci-file-server at Glasgow Haskell Compiler / GHC
Commits:
-
c951fef1
by Cheng Shao at 2026-02-25T20:58:28+00:00
8 changed files:
- compiler/GHC/Driver/Config/Interpreter.hs
- compiler/GHC/Driver/DynFlags.hs
- compiler/GHC/Driver/Session.hs
- compiler/GHC/Runtime/Interpreter/Init.hs
- compiler/GHC/Runtime/Interpreter/Types.hs
- compiler/GHC/Runtime/Interpreter/Wasm.hs
- docs/users_guide/wasm.rst
- utils/jsffi/dyld.mjs
Changes:
| ... | ... | @@ -30,6 +30,7 @@ initInterpOpts dflags = do |
| 30 | 30 | , interpBrowser = gopt Opt_GhciBrowser dflags
|
| 31 | 31 | , interpBrowserHost = ghciBrowserHost dflags
|
| 32 | 32 | , interpBrowserPort = ghciBrowserPort dflags
|
| 33 | + , interpBrowserAssetsDir = ghciBrowserAssetsDir dflags
|
|
| 33 | 34 | , interpBrowserRedirectWasiConsole = gopt Opt_GhciBrowserRedirectWasiConsole dflags
|
| 34 | 35 | , interpBrowserPuppeteerLaunchOpts = ghciBrowserPuppeteerLaunchOpts dflags
|
| 35 | 36 | , interpBrowserPlaywrightBrowserType = ghciBrowserPlaywrightBrowserType dflags
|
| ... | ... | @@ -43,4 +44,3 @@ initInterpOpts dflags = do |
| 43 | 44 | , interpCcConfig = configureCc dflags
|
| 44 | 45 | , interpExecutableLinkOpts = initExecutableLinkOpts dflags Dynamic
|
| 45 | 46 | } |
| 46 | - |
| ... | ... | @@ -426,6 +426,7 @@ data DynFlags = DynFlags { |
| 426 | 426 | -- wasm ghci browser mode
|
| 427 | 427 | ghciBrowserHost :: !String,
|
| 428 | 428 | ghciBrowserPort :: !Int,
|
| 429 | + ghciBrowserAssetsDir :: !(Maybe FilePath),
|
|
| 429 | 430 | ghciBrowserPuppeteerLaunchOpts :: !(Maybe String),
|
| 430 | 431 | ghciBrowserPlaywrightBrowserType :: !(Maybe String),
|
| 431 | 432 | ghciBrowserPlaywrightLaunchOpts :: !(Maybe String),
|
| ... | ... | @@ -727,6 +728,7 @@ defaultDynFlags mySettings = |
| 727 | 728 | |
| 728 | 729 | ghciBrowserHost = "127.0.0.1",
|
| 729 | 730 | ghciBrowserPort = 0,
|
| 731 | + ghciBrowserAssetsDir = Nothing,
|
|
| 730 | 732 | ghciBrowserPuppeteerLaunchOpts = Nothing,
|
| 731 | 733 | ghciBrowserPlaywrightBrowserType = Nothing,
|
| 732 | 734 | ghciBrowserPlaywrightLaunchOpts = Nothing,
|
| ... | ... | @@ -1896,6 +1896,8 @@ dynamic_flags_deps = [ |
| 1896 | 1896 | $ hasArg $ \f d -> d { ghciBrowserHost = f }
|
| 1897 | 1897 | , make_ord_flag defGhciFlag "fghci-browser-port"
|
| 1898 | 1898 | $ intSuffix $ \n d -> d { ghciBrowserPort = n }
|
| 1899 | + , make_ord_flag defGhciFlag "fghci-browser-assets-dir"
|
|
| 1900 | + $ hasArg $ \f d -> d { ghciBrowserAssetsDir = Just f }
|
|
| 1899 | 1901 | , make_ord_flag defGhciFlag "fghci-browser-puppeteer-launch-opts"
|
| 1900 | 1902 | $ hasArg $ \f d -> d { ghciBrowserPuppeteerLaunchOpts = Just f }
|
| 1901 | 1903 | , make_ord_flag defGhciFlag "fghci-browser-playwright-browser-type"
|
| ... | ... | @@ -49,6 +49,7 @@ data InterpOpts = InterpOpts |
| 49 | 49 | , interpBrowser :: Bool
|
| 50 | 50 | , interpBrowserHost :: String
|
| 51 | 51 | , interpBrowserPort :: Int
|
| 52 | + , interpBrowserAssetsDir :: !(Maybe FilePath)
|
|
| 52 | 53 | , interpBrowserRedirectWasiConsole :: Bool
|
| 53 | 54 | , interpBrowserPuppeteerLaunchOpts :: Maybe String
|
| 54 | 55 | , interpBrowserPlaywrightBrowserType :: Maybe String
|
| ... | ... | @@ -89,6 +90,7 @@ initInterpreter dflags tmpfs logger platform finder_cache unit_env opts = do |
| 89 | 90 | , wasmInterpBrowser = interpBrowser opts
|
| 90 | 91 | , wasmInterpBrowserHost = interpBrowserHost opts
|
| 91 | 92 | , wasmInterpBrowserPort = interpBrowserPort opts
|
| 93 | + , wasmInterpBrowserAssetsDir = interpBrowserAssetsDir opts
|
|
| 92 | 94 | , wasmInterpBrowserRedirectWasiConsole = interpBrowserRedirectWasiConsole opts
|
| 93 | 95 | , wasmInterpBrowserPuppeteerLaunchOpts = interpBrowserPuppeteerLaunchOpts opts
|
| 94 | 96 | , wasmInterpBrowserPlaywrightBrowserType = interpBrowserPlaywrightBrowserType opts
|
| ... | ... | @@ -220,6 +220,7 @@ data WasmInterpConfig = WasmInterpConfig |
| 220 | 220 | , wasmInterpBrowser :: !Bool
|
| 221 | 221 | , wasmInterpBrowserHost :: !String
|
| 222 | 222 | , wasmInterpBrowserPort :: !Int
|
| 223 | + , wasmInterpBrowserAssetsDir :: !(Maybe FilePath)
|
|
| 223 | 224 | , wasmInterpBrowserRedirectWasiConsole :: !Bool
|
| 224 | 225 | , wasmInterpBrowserPuppeteerLaunchOpts :: !(Maybe String)
|
| 225 | 226 | , wasmInterpBrowserPlaywrightBrowserType :: !(Maybe String)
|
| ... | ... | @@ -52,6 +52,7 @@ spawnWasmInterp WasmInterpConfig {..} = do |
| 52 | 52 | let dyld_env =
|
| 53 | 53 | [("GHCI_BROWSER", "1") | wasmInterpBrowser]
|
| 54 | 54 | ++ [("GHCI_BROWSER_HOST", wasmInterpBrowserHost), ("GHCI_BROWSER_PORT", show wasmInterpBrowserPort)]
|
| 55 | + ++ [("GHCI_BROWSER_ASSETS_DIR", f) | f <- maybeToList wasmInterpBrowserAssetsDir]
|
|
| 55 | 56 | ++ [("GHCI_BROWSER_REDIRECT_WASI_CONSOLE", "1") | wasmInterpBrowserRedirectWasiConsole]
|
| 56 | 57 | ++ [("GHCI_BROWSER_PUPPETEER_LAUNCH_OPTS", f) | f <- maybeToList wasmInterpBrowserPuppeteerLaunchOpts]
|
| 57 | 58 | ++ [("GHCI_BROWSER_PLAYWRIGHT_BROWSER_TYPE", f) | f <- maybeToList wasmInterpBrowserPlaywrightBrowserType]
|
| ... | ... | @@ -193,6 +193,18 @@ See below for other optional GHC flags of wasm ghci browser mode: |
| 193 | 193 | Specify the port that the ``dyld`` HTTP server should listen on.
|
| 194 | 194 | Defaults to a random idle port.
|
| 195 | 195 | |
| 196 | +.. ghc-flag:: -fghci-browser-assets-dir
|
|
| 197 | + :shortdesc: User-specified assets root directory
|
|
| 198 | + :type: dynamic
|
|
| 199 | + |
|
| 200 | + :default: ``$PWD``
|
|
| 201 | + |
|
| 202 | + The HTTP server also exposes an ``/assets`` endpoint that allows
|
|
| 203 | + the users to fetch custom assets with sensible default MIME type,
|
|
| 204 | + e.g. `http://127.0.0.1:8080/assets/index.html` would fetch
|
|
| 205 | + `index.html` in the assets root directory with ``text/html`` MIME
|
|
| 206 | + type.
|
|
| 207 | + |
|
| 196 | 208 | .. ghc-flag:: -fghci-browser-redirect-wasi-console
|
| 197 | 209 | :shortdesc: Redirect wasi console stdout/stderr back to host ghci.
|
| 198 | 210 | :type: dynamic
|
| ... | ... | @@ -560,6 +560,7 @@ export class DyLDRPC { |
| 560 | 560 | |
| 561 | 561 | // Actual implementation of endpoints used by DyLDRPC
|
| 562 | 562 | class DyLDRPCServer {
|
| 563 | + #mimeDb;
|
|
| 563 | 564 | #dyldHost;
|
| 564 | 565 | #server;
|
| 565 | 566 | #wss;
|
| ... | ... | @@ -567,6 +568,7 @@ class DyLDRPCServer { |
| 567 | 568 | constructor({
|
| 568 | 569 | host,
|
| 569 | 570 | port,
|
| 571 | + assetsDir,
|
|
| 570 | 572 | dyldPath,
|
| 571 | 573 | searchDirs,
|
| 572 | 574 | mainSoPath,
|
| ... | ... | @@ -575,6 +577,20 @@ class DyLDRPCServer { |
| 575 | 577 | args,
|
| 576 | 578 | redirectWasiConsole,
|
| 577 | 579 | }) {
|
| 580 | + this.#mimeDb = fetch("https://cdn.jsdelivr.net/npm/mime-db@1.54.0/db.json")
|
|
| 581 | + .then((resp) => resp.json())
|
|
| 582 | + .then((db) => {
|
|
| 583 | + const ext2mime = {};
|
|
| 584 | + for (const mime in db) {
|
|
| 585 | + if (db[mime].extensions) {
|
|
| 586 | + for (const ext of db[mime].extensions) {
|
|
| 587 | + ext2mime[`.${ext}`] = mime;
|
|
| 588 | + }
|
|
| 589 | + }
|
|
| 590 | + }
|
|
| 591 | + return ext2mime;
|
|
| 592 | + });
|
|
| 593 | + |
|
| 578 | 594 | this.#dyldHost = new DyLDHost({ outFd, inFd });
|
| 579 | 595 | |
| 580 | 596 | this.#server = http.createServer(async (req, res) => {
|
| ... | ... | @@ -634,6 +650,33 @@ args.rpc.opened.then(() => main(args)); |
| 634 | 650 | return;
|
| 635 | 651 | }
|
| 636 | 652 | |
| 653 | + if (req.url.startsWith("/assets")) {
|
|
| 654 | + const p = path.resolve(
|
|
| 655 | + assetsDir,
|
|
| 656 | + new URL(req.url, origin).pathname.replace("/assets/", ""),
|
|
| 657 | + );
|
|
| 658 | + try {
|
|
| 659 | + await fs.promises.access(p, fs.promises.constants.R_OK);
|
|
| 660 | + |
|
| 661 | + res.setHeader(
|
|
| 662 | + "Content-Type",
|
|
| 663 | + (await this.#mimeDb)[path.extname(p)] || "application/octet-stream",
|
|
| 664 | + );
|
|
| 665 | + |
|
| 666 | + res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
| 667 | + |
|
| 668 | + res.writeHead(200);
|
|
| 669 | + fs.createReadStream(p).pipe(res);
|
|
| 670 | + } catch {
|
|
| 671 | + res.writeHead(404, {
|
|
| 672 | + "Content-Type": "text/plain",
|
|
| 673 | + });
|
|
| 674 | + res.end("not found");
|
|
| 675 | + }
|
|
| 676 | + |
|
| 677 | + return;
|
|
| 678 | + }
|
|
| 679 | + |
|
| 637 | 680 | if (req.url.startsWith("/rpc")) {
|
| 638 | 681 | const endpoint = req.url.replace("/rpc/", "");
|
| 639 | 682 | |
| ... | ... | @@ -1373,6 +1416,7 @@ async function nodeMain({ searchDirs, mainSoPath, outFd, inFd, args }) { |
| 1373 | 1416 | const server = new DyLDRPCServer({
|
| 1374 | 1417 | host: process.env.GHCI_BROWSER_HOST || "127.0.0.1",
|
| 1375 | 1418 | port: process.env.GHCI_BROWSER_PORT || 0,
|
| 1419 | + assetsDir: process.env.GHCI_BROWSER_ASSETS_DIR || process.cwd(),
|
|
| 1376 | 1420 | dyldPath: import.meta.filename,
|
| 1377 | 1421 | searchDirs,
|
| 1378 | 1422 | mainSoPath,
|