Skip to content

Commit

Permalink
Update webR documentation for building R packages and mounting filesy…
Browse files Browse the repository at this point in the history
…stem images (#316)

* Update documentation for pkg building and mounting

* Add mounting example

* Minor tweaks
  • Loading branch information
georgestagg authored Nov 14, 2023
1 parent 6fd7fac commit 415b937
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 17 deletions.
6 changes: 5 additions & 1 deletion src/docs/_quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ website:
- objects.qmd
- convert-r-to-js.qmd
- convert-js-to-r.qmd
- packages.qmd
- section: R Packages and Data
contents:
- packages.qmd
- building.qmd
- mounting.qmd
- section: WebR API
contents:
- api/r.qmd
Expand Down
28 changes: 28 additions & 0 deletions src/docs/building.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
title: "Building R Packages"
format: html
toc: true
---

Many R packages contain C, C++ or Fortran code that must be compiled^[Prerequisite system libraries must also be compiled for WebAssembly. The webR build process optionally includes building a collection of system libraries for Wasm.] for WebAssembly (Wasm) before they can be loaded in webR. While the [webR binary repository](packages.qmd#downloading-packages-from-a-webr-binary-repository) provides pre-compiled Wasm binaries^[Other R package repositories providing WebAssembly binaries are also available, such as [R-universe](https://r-universe.dev/).], custom R packages must be independently compiled from source.

::: callout-note
It is not possible to directly install packages from source in webR. This is likely to remain the case in the future, as such a process would depend on an entire R development toolchain running in the browser. Loading pre-compiled WebAssembly binaries is the only supported way to install packages in webR.
:::

## Compiling R packages using GitHub Actions

GitHub Actions can be used to remotely build R packages for Wasm by making use of the workflows provided by [r‑wasm/actions](https://github.com/r-wasm/actions). This is a convenient method that does not require the setup of a local WebAssembly compiler toolchain or webR development environment.

The workflow can also be configured to automatically upload the resulting R package binaries to GitHub Pages, ready to be used with webR's [`install()`](packages.qmd#downloading-packages-from-a-webr-binary-repository) or [`mount()`](packages.qmd#mounting-an-r-library-filesystem-image) functions.

Further details can be found in the documentation and `examples` directory of [r‑wasm/actions](https://github.com/r-wasm/actions).

## Compiling R packages locally

To locally build R packages for webR we recommend using the [rwasm](https://r-wasm.github.io/rwasm/) package in a native R installation. The package automates the use of R's [`Makevars`](https://cran.r-project.org/doc/manuals/r-devel/R-exts.html#Using-Makevars) mechanism^[While it is possible to manually build R packages using R's `Makevars` mechanism alone, the process can be quite involved and error-prone depending on the specific R package involved.] to cross-compile R packages for Wasm, by setting up the host environment so that the [Emscripten](https://emscripten.org/index.html) compiler toolchain is used for compilation and applying a selection of configuration overrides for certain packages.

The [rwasm](https://r-wasm.github.io/rwasm/) package can also be used to deploy the resulting R package binaries as a CRAN-like repository, or as an Emscripten filesystem image containing an R package library. The resulting collection of R packages can then be made available through static file hosting^[e.g. GitHub Pages, Netlify, AWS S3, etc.].

Running the [rwasm](https://r-wasm.github.io/rwasm/) package requires pre-setup of a configured webR development environment, either by running the package under a [pre-prepared webR Docker container](https://github.com/r-wasm/webr/pkgs/container/webr), or by configuring a local webR development build. See the [Getting Started](https://r-wasm.github.io/rwasm/articles/rwasm.html) section of the [rwasm](https://r-wasm.github.io/rwasm/) package documentation for further details.

135 changes: 135 additions & 0 deletions src/docs/mounting.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
---
title: "Mounting Filesystem Data"
format: html
toc: true
---

## The virtual filesystem

The [Emscripten filesystem API](https://emscripten.org/docs/api_reference/Filesystem-API.html) provides a Unix-like virtual filesystem for the WebAssembly (Wasm) R process running in webR. This virtual filesystem has the ability to [mount](https://emscripten.org/docs/api_reference/Filesystem-API.html#FS.mount) filesystem images or host directories so that the associated file and directory data is accessible to the Wasm R process.

Mounting images and directories in this way gives the Wasm R process access to arbitrary external data, potentially including datasets, scripts, or R packages [pre-compiled for WebAssembly](building.qmd).

Emscripten's API provides several types of virtual filesystem, but for technical reasons^[Currently, webR blocks in the JavaScript worker thread while it waits for R input to be evaluated. This blocking means that Emscripten filesystems that depend on asynchronous browser APIs, such as [`IDBFS`](https://emscripten.org/docs/api_reference/Filesystem-API.html#filesystem-api-idbfs), do not work.] only the following filesystems are available for use with webR.

| Filesystem | Description | Web Browser | Node.js |
|------|-----|------|------|
| `WORKERFS` | Mount filesystem images. |||
| `NODEFS` | Mount existing host directories. |||

## Emscripten filesystem images

The [`file_packager`](https://emscripten.org/docs/porting/files/packaging_files.html#packaging-using-the-file-packager-tool) tool, provided by Emscripten, takes in a directory structure as input and produces webR compatible filesystem images as output. The [`file_packager`](https://emscripten.org/docs/porting/files/packaging_files.html#packaging-using-the-file-packager-tool) tool may be invoked from R using the [rwasm](https://r-wasm.github.io/rwasm/) R package:

```{r eval=FALSE}
> rwasm::file_packager("./input", out_dir = ".", out_name = "output")
```

It can also be invoked directly using its CLI^[See the [`file_packager`](https://emscripten.org/docs/porting/files/packaging_files.html#packaging-using-the-file-packager-tool) Emscripten documentation for details. ], if you prefer:

```bash
$ file_packager output.data --preload ./input@/ \
--separate-metadata --js-output=output.js
```

In the above examples, the files in the directory `./input` are packaged and an output filesystem image is created^[When using the `file_packager` CLI, a third file named `output.js` will also be created. If you only plan to mount the image using webR, this file may be discarded.] consisting of a data file, `output.data`, and a metadata file, `output.js.metadata`.

To prepare for mounting the filesystem image with webR, ensure that both files have the same basename (in this example, `output`) and are deployed to static file hosting^[e.g. GitHub Pages, Netlify, AWS S3, etc.]. The resulting URLs for the two files should differ only by the file extension.


## Mount a filesystem image from URL

By default, the [`webr::mount()`](api/r.qmd#mount) function downloads and mounts a filesystem image from a URL source, using the `WORKERFS` filesystem type.

```{r eval=FALSE}
webr::mount(
mountpoint = "/data",
source = "https://example.com/output.data"
)
```

A URL for the filesystem image `.data` file should be provided as the source argument, and the image will be mounted in the virtual filesystem under the path given by the `mountpoint` argument. If the `mountpoint` directory does not exist, it will be created prior to mounting.

### JavaScript API

WebR's JavaScript API includes the [`WebR.FS.mount()`](api/js/classes/WebR.WebR.md#fs) function, a thin wrapper around Emscripten's own [`FS.mount()`](https://emscripten.org/docs/api_reference/Filesystem-API.html#FS.mount). The JavaScript API provides more flexibility but requires a little more set up, including creating the `mountpoint` directory if it does not already exist.

The filesystem type should be provided as a `string`, with the `options` argument a JavaScript object of type [`FSMountOptions`](api/js/modules/WebR.md#fsmountoptions). The filesystem image data should be provided as a JavaScript `Blob` and the metadata as a JavaScript object deserialised from the underlying JSON content.

::: {.panel-tabset}
## JavaScript

``` javascript
// Create mountpoint
await webR.FS.mkdir('/data')

// Download image data
const data = await fetch('https://example.com/output.data');
const metadata = await fetch('https://example.com/output.js.metadata');

// Mount image data
const options = {
packages: [{
blob: await data.blob(),
metadata: await metadata.json(),
}],
}
await webR.FS.mount("WORKERFS", options, '/data');
```

## TypeScript

``` typescript
import { FSMountOptions } from 'webr';

// Create mountpoint
await webR.FS.mkdir('/data')

// Download image data
const data = await fetch('https://example.com/output.data');
const metadata = await fetch('https://example.com/output.js.metadata');

// Mount image data
const options: FSMountOptions = {
packages: [{
blob: await data.blob(),
metadata: await metadata.json(),
}],
}
await webR.FS.mount("WORKERFS", options, '/data');
```

:::

See the [Emscripten `FS.mount()` documentation](https://emscripten.org/docs/api_reference/Filesystem-API.html#FS.mount) for further details about the structure of the `options` argument.

## Mount an existing host directory

::: callout-warning
`NODEFS` is only available when running webR under Node.js.
:::

The `NODEFS` filesystem type maps directories that exist on the host machine so that they are accessible in the WebAssembly process.

To mount the directory `./extra` on the virtual filesystem at `/data`, use either the JavaScript or R mount API with the filesystem type set to `"NODEFS"`.

::: {.panel-tabset}
## JavaScript

``` javascript
await webR.FS.mkdir('/data')
await webR.FS.mount('NODEFS', { root: './extra' }, '/data');
```

## R
```{r eval=FALSE}
webr::mount(
mountpoint = "/data",
source = "./extra",
type = "NODEFS"
)
```


:::

54 changes: 38 additions & 16 deletions src/docs/packages.qmd
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
title: "R Packages"
title: "Installing R Packages"
format: html
toc: true
---

WebR supports the loading of R packages that have been pre-compiled targeting WebAssembly and for use with webR.
WebR supports the loading of R packages that have been pre-compiled targeting WebAssembly.

WebR packages must be installed before use, which in practice means copying binaries into the webR virtual filesystem in some way, either as part of the webR build process or during runtime by downloading packages from a repository.
WebR packages must be installed before use, which in practice means copying binaries into the webR virtual filesystem in some way, either as part of the webR build process or during runtime by downloading packages from a repository or [mounting filesystem data](packages.qmd#mounting-an-r-library-filesystem-image).

## Downloading packages from a webR binary repository

Expand All @@ -16,7 +16,7 @@ A collection of supported packages are publicly hosted via a CDN in a CRAN-like

The public CDN build of webR has been distributed with a pre-installed [webr support package](api/r.qmd). This R package provides a helper function [`install()`](api/r.qmd#install-one-or-more-packages-from-a-webr-binary-package-repo), which can be used to install further R packages from a compatible webR binary repository.

The repository URL is set using the `repos` argument and the public build of webR defaults the `repo` argument to the repository URL shown above.
The repository URL is set using the `repos` argument and the public build of webR defaults this argument to the repository URL shown above^[Other R package repositories providing WebAssembly binaries are also available, such as [R-universe](https://r-universe.dev/).].

### Example: Installing the Matrix package

Expand All @@ -32,17 +32,26 @@ If the package is available for webR and downloads successfully, you can then lo
library("Matrix")
```

## Shimming `install.packages()` for webR

The [webr support package](api/r.qmd) includes the function [`shim_install()`](api/r.html#shim_install) which optionally replaces specific base R functions with implementations that work in the webR environment. Once executed, the base R `install.packages()` function can be used to install webR packages.

```{r eval=FALSE}
webr::shim_install()
install.packages("Matrix")
```

The [webR REPL demo](https://webr.r-wasm.org/latest/) performs this base R function replacement as part of its startup procedure.

## Interactively installing packages

As part of startup, the [webR REPL demo](https://webr.r-wasm.org/latest/) website runs the following R code:
The [webr support package](api/r.qmd) provides an optional global handler that will show an interactive prompt whenever loading a library fails, asking the user if they would like to try downloading the package from the configured default webR binary package repository.

```{r eval=FALSE}
webr::global_prompt_install()
```

This function, also provided by the pre-installed [webr support package](api/r.qmd), installs a global handler that will show an interactive prompt whenever loading a library fails, asking the user if they would like to try downloading the package from the configured default webR binary package repository.

Once the global handler has been installed in this way, it is possible to install and load packages interactively via a prompt. The process is as follows,
As part of its startup, the [webR REPL demo](https://webr.r-wasm.org/latest/) enables this handler. Once enabled, it is possible to install and load packages interactively via a prompt. The process is as follows,

``` r
library(Matrix)
Expand All @@ -60,22 +69,35 @@ Once the package has been downloaded in this way, the next call to `library()` w

## Installing packages from JavaScript

The webR JavaScript API provides the function [`WebR.installpackages()`](api/js/classes/WebR.WebR.md#installpackages). This convenience function takes an array of packages as its only argument and for each calls [`install()`](api/r.qmd#install-one-or-more-packages-from-a-webr-binary-package-repo) with the default webR binary package repository.
The webR JavaScript API provides the function [`WebR.installPackages()`](api/js/classes/WebR.WebR.md#installpackages). This convenience function takes an array of packages as its only argument and for each calls [`install()`](api/r.qmd#install-one-or-more-packages-from-a-webr-binary-package-repo) with the default webR binary package repository.

``` javascript
await webR.installPackages(['Matrix', 'cli'])
```

Once the promise returned by [`WebR.installPackages()`](api/js/classes/WebR.WebR.md#installpackages) has resolved, the packages can be loaded in the usual way using `library()`.

## Building R packages for webR
## Mounting an R library filesystem image

R libraries that have been packaged with Emscripten's [`file_packager`](https://emscripten.org/docs/porting/files/packaging_files.html#packaging-using-the-file-packager-tool) tool may be loaded into the webR virtual filesystem (VFS) by mounting the image using [`webr::mount()`](api/r.qmd#mount). When using webR in a browser, `type = "WORKERFS"` (the default) should be used so that the filesystem image is downloaded from the URL given by `source` and mounted on the VFS at `mountpoint`.

::: callout-note
It is not possible to install packages from source in webR. This is not likely to change in the near future, as such a process would require an entire C and Fortran compiler toolchain to run inside the browser. For the moment, providing pre-compiled WebAssembly binaries is the only supported way to install R packages in webR.
:::
```{r eval=FALSE}
webr::mount(
mountpoint = "/library",
source = "https://example.com/library.data"
)
```

Once the library filesystem image has been mounted, the base R `.libPaths()` function should be used to ensure `mountpoint` is included in R's package library search path.

```{r eval=FALSE}
.libPaths(c(.libPaths(), "/library"))
```

Many R packages contain C or C++ code and so must be compiled for WebAssembly (along with any prerequisite system libraries) before they can be loaded in webR. One way to do this is by using the [`Makevars`](https://cran.r-project.org/doc/manuals/r-devel/R-exts.html#Using-Makevars) system to instruct R to compile R packages with the [Emscripten](https://emscripten.org/index.html) compiler toolchain, setting variables such as `CC` and `CXX` to point to the relevant Emscripten compilers.
Packages that exist in the R library filesystem image can then be loaded as normal.

Once the R package has been compiled and binaries targeting WebAssembly are available, they can be loaded into webR by providing a CRAN-like repo to the `webr::install()` command.
```{r eval=FALSE}
library(mypackage)
```

More detailed information about the repository and package building process will be made available in the future, when the infrastructure is more stable.
Details on how to build a WebAssembly R package library can be found in the [Building R Packages](building.qmd) section, and further information about mounting data to the VFS (including when running under Node.js) can be found in the [Mounting Filesystem Data](mounting.qmd) section.
13 changes: 13 additions & 0 deletions src/examples/mount/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<html>
<head>
<title>WebR Mounting Example</title>
</head>
<body>
<div>
<h1>WebR Mounting Example</h1>
<p id="loading">Loading webR, please wait...</p>
<pre><code id="out"></code></pre>
</div>
<script type="module" src="./mount.js"></script>
</body>
</html>
30 changes: 30 additions & 0 deletions src/examples/mount/mount.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { WebR } from 'https://webr.r-wasm.org/latest/webr.mjs';
const webR = new WebR();

// Remove the loading message once webR is ready
await webR.init();
document.getElementById('loading').remove();

// Download a filesystem image
await webR.FS.mkdir('/data')
const data = await fetch('./output.data');
const metadata = await fetch('./output.js.metadata');

// Mount the filesystem image data
const options = {
packages: [{
blob: await data.blob(),
metadata: await metadata.json(),
}],
}
await webR.FS.mount("WORKERFS", options, '/data');

// Read the contents of a file from the filesystem image
const result = await webR.evalR('readLines("/data/abc.txt")');
try {
let output = await result.toArray();
let text = output.join('\n');
document.getElementById('out').innerText += `Contents of the filesystem image file at /data/abc.txt:\n${text}\n\n`;
} finally {
webR.destroy(result);
}
1 change: 1 addition & 0 deletions src/examples/mount/output.data
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
abcd1234
1 change: 1 addition & 0 deletions src/examples/mount/output.js.metadata
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"files":[{"filename":"/abc.txt","start":0,"end":9}],"remote_package_size":9}

0 comments on commit 415b937

Please sign in to comment.