Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ build/
__pycache__/
.cache/
compile_commands.json
serve.log
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,13 @@ The CLI is tested using `python`. From the top-level directory:
```bash
pytest -v
```

# WebAssembly build and deployment

The `wasm` directory contains everything needed to build the local `git2cpp` source code as an
WebAssembly [Emscripten-forge](https://emscripten-forge.org/) package, create local
[cockle](https://github.com/jupyterlite/cockle) and
[JupyterLite terminal](https://github.com/jupyterlite/terminal) deployments that run in a browser,
and test the WebAssembly build.

See the `README.md` in the `wasm` directory for further details.
Empty file added test/__init__.py
Empty file.
10 changes: 8 additions & 2 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
import pytest
import subprocess

GIT2CPP_TEST_WASM = os.getenv('GIT2CPP_TEST_WASM') == "1"

if GIT2CPP_TEST_WASM:
from .conftest_wasm import *

# Fixture to run test in current tmp_path
@pytest.fixture
Expand All @@ -15,8 +19,10 @@ def run_in_tmp_path(tmp_path):

@pytest.fixture(scope="session")
def git2cpp_path():
return Path(__file__).parent.parent / "build" / "git2cpp"

if GIT2CPP_TEST_WASM:
return 'git2cpp'
else:
return Path(__file__).parent.parent / 'build' / 'git2cpp'

@pytest.fixture
def xtl_clone(git2cpp_path, tmp_path, run_in_tmp_path):
Expand Down
55 changes: 55 additions & 0 deletions test/conftest_wasm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Extra fixtures used for wasm testing.
from functools import partial
from pathlib import Path
from playwright.sync_api import Page
import pytest
import subprocess
import time

@pytest.fixture(scope="session", autouse=True)
def run_web_server():
with open('serve.log', 'w') as f:
cwd = Path(__file__).parent.parent / 'wasm/test'
proc = subprocess.Popen(
['npm', 'run', 'serve'], stdout=f, stderr=f, cwd=cwd
)
# Wait a bit until server ready to receive connections.
time.sleep(0.3)
yield
proc.terminate()

@pytest.fixture(scope="function", autouse=True)
def load_page(page: Page):
# Load web page at start of every test.
page.goto("http://localhost:8000")
page.locator("#loaded").wait_for()

def subprocess_run(
page: Page,
cmd: list[str],
*,
capture_output: bool = False,
cwd: str | None = None,
text: bool | None = None
) -> subprocess.CompletedProcess:
if cwd is not None:
raise RuntimeError('cwd is not yet supported')

proc = page.evaluate("async cmd => window.cockle.shellRun(cmd)", cmd)
# TypeScript object is auto converted to Python dict.
# Want to return subprocess.CompletedProcess, consider namedtuple if this fails in future.
stdout = proc['stdout'] if capture_output else ''
stderr = proc['stderr'] if capture_output else ''
if not text:
stdout = stdout.encode("utf-8")
stderr = stderr.encode("utf-8")
return subprocess.CompletedProcess(
args=cmd,
returncode=proc['returncode'],
stdout=stdout,
stderr=stderr
)

@pytest.fixture(scope="function", autouse=True)
def mock_subprocess_run(page: Page, monkeypatch):
monkeypatch.setattr(subprocess, "run", partial(subprocess_run, page))
14 changes: 14 additions & 0 deletions wasm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
CMakeCache.txt
CMakeFiles/
Makefile
cmake_install.cmake
cockle-config.json
cockle_wasm_env/
node_modules/
package-lock.json
.jupyterlite.doit.db

recipe/em-forge-recipes/
serve/*/
test/assets/*/
test/lib/
16 changes: 16 additions & 0 deletions wasm/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
cmake_minimum_required(VERSION 3.28)
project(git2cpp-wasm)

add_subdirectory(recipe)
add_subdirectory(cockle-deploy)
add_subdirectory(lite-deploy)
add_subdirectory(test)

# Build everything (package, cockle and lite deployments, tests).
add_custom_target(build ALL DEPENDS build-recipe build-cockle build-lite build-test)

# Rebuild after change in C++ code.
add_custom_target(rebuild DEPENDS rebuild-recipe rebuild-cockle rebuild-lite rebuild-test)

# Serve both cockle and JupyterLite deployments.
add_custom_target(serve COMMAND npx static-handler --cors --coop --coep --corp serve)
80 changes: 80 additions & 0 deletions wasm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Building and testing git2cpp in WebAssembly

This directory contains everything needed to build the local `git2cpp` source code as an
WebAssembly [Emscripten-forge](https://emscripten-forge.org/) package, create local
[cockle](https://github.com/jupyterlite/cockle) and
[JupyterLite terminal](https://github.com/jupyterlite/terminal) deployments that run in a browser,
and test the WebAssembly build.

It works on Linux and macOS but not Windows.

There are 5 sub-directories:

- `recipe`: build local `git2cpp` source code into an Emscripten-forge package.
- `cockle-deploy`: create a `cockle` deployment in the `serve` directory.
- `lite-deploy`: create a JupyterLite `terminal` deployment in the `serve` directory.
- `serve`: where the two deployments are served from.
- `test`: test the WebAssembly build.

## Build and deploy

The build, deploy and test process uses a separate `micromamba` environment defined in
`wasm-environment.yml`. To set this up use from within this directory:

```bash
micromamba create -f wasm-environment.yml
micromamba activate git2cpp-wasm
```

Then to build the WebAssembly package, both deployments and the testing resources use:

```bash
cmake .
make
```

The built emscripten-forge package will be file named something like `git2cpp-0.0.5-h7223423_1.tar.bz2`
in the directory `recipe/em-force-recipes/output/emscripten-wasm32`.

The local deployments in the `serve` directory can be manually checked using:

```bash
make serve
```

and open a web browser at http://localhost:8080/. Confirm that the local build of `git2cpp` is being
used in the two deployments by running `cockle-config package` at the command line, the output
should be something like:

<img alt="cockle-config output" src="cockle-config.png">

Note that the `source` for the `git2cpp` package is the local filesystem rather than from
`prefix.dev`. The version number of `git2cpp` in this table is not necessarily correct as it is the
version number of the current Emscripten-forge recipe rather than the version of the local `git2cpp`
source code which can be checked using `git2cpp -v` at the `cockle`/`terminal` command line.

## Test

To test the WebAssembly build use:

```bash
make test
```

This runs (some of) the tests in the top-level `test` directory with various monkey patching so that
`git2cpp` commands are executed in the browser. If there are problems running the tests then ensure
you have the latest `playwright` browser installed:


```bash
playwright install chromium
```

## Rebuild

After making changes to the local `git2cpp` source code you can rebuild the WebAssembly package,
both deployments and test code using:

```bash
make rebuild
```
Binary file added wasm/cockle-config.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions wasm/cockle-deploy/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.28)
project(git2cpp-wasm-cockle-deploy)

include("../common.cmake")

set(SERVE_DIR "../serve/cockle")

add_custom_target(build-cockle
DEPENDS build-recipe
COMMAND npm install
COMMAND COCKLE_WASM_EXTRA_CHANNEL=${BUILT_PACKAGE_DIR} npm run build
BYPRODUCTS cockle_wasm_env node_modules ${SERVE_DIR}
)

add_custom_target(rebuild-cockle
DEPENDS rebuild-recipe clean-env-cockle build-cockle
)

add_custom_target(clean-env-cockle
COMMAND ${CMAKE_COMMAND} -E remove_directory cockle_wasm_env
)
10 changes: 10 additions & 0 deletions wasm/cockle-deploy/assets/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>cockle-deployment for git2cpp</title>
<script src="bundle.js"></script>
</head>
<body>
<div id="targetdiv"></div>
</body>
</html>
19 changes: 19 additions & 0 deletions wasm/cockle-deploy/cockle-config-in.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"packages": {
"git2cpp": {},
"nano": {},
"tree": {},
"vim": {}
},
"aliases": {
"git": "git2cpp",
"vi": "vim"
},
"environment": {
"GIT_CORS_PROXY": "https://corsproxy.io/?url=",
"GIT_AUTHOR_NAME": "Jane Doe",
"GIT_AUTHOR_EMAIL": "[email protected]",
"GIT_COMMITTER_NAME": "Jane Doe",
"GIT_COMMITTER_EMAIL": "[email protected]"
}
}
28 changes: 28 additions & 0 deletions wasm/cockle-deploy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "cockle-deploy",
"scripts": {
"build": "rspack build",
"postbuild": "npm run postbuild:wasm && npm run postbuild:index",
"postbuild:wasm": "node node_modules/@jupyterlite/cockle/lib/tools/prepare_wasm.js --copy ../serve/cockle/",
"postbuild:index": "cp assets/index.html ../serve/cockle/"
},
"main": "../serve/cockle/index.js",
"types": "../serve/cockle/index.d.ts",
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@rspack/cli": "^1.0.4",
"@rspack/core": "^1.0.4",
"css-loader": "^7.1.2",
"style-loader": "^4.0.0",
"ts-loader": "^9.5.1",
"typescript": "^5.4.5"
},
"dependencies": {
"@jupyterlite/cockle": "^1.3.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"deepmerge-ts": "^7.1.4"
}
}
29 changes: 29 additions & 0 deletions wasm/cockle-deploy/rspack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const path = require('path');

module.exports = {
mode: 'development',
entry: './src/index.ts',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, '../serve/cockle'),
}
};
10 changes: 10 additions & 0 deletions wasm/cockle-deploy/src/defs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { IShellManager } from '@jupyterlite/cockle';

export namespace IDeployment {
export interface IOptions {
baseUrl: string;
browsingContextId: string;
shellManager: IShellManager;
targetDiv: HTMLElement;
}
}
63 changes: 63 additions & 0 deletions wasm/cockle-deploy/src/deployment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Shell } from '@jupyterlite/cockle'
import { FitAddon } from '@xterm/addon-fit'
import { Terminal } from '@xterm/xterm'
import { IDeployment } from './defs'

export class Deployment {
constructor(options: IDeployment.IOptions) {
this._targetDiv = options.targetDiv;

const termOptions = {
rows: 50,
theme: {
foreground: "ivory",
background: "#111111",
cursor: "silver"
},
}
this._term = new Terminal(termOptions)

this._fitAddon = new FitAddon()
this._term.loadAddon(this._fitAddon)

const { baseUrl, browsingContextId, shellManager } = options;

this._shell = new Shell({
browsingContextId,
baseUrl,
wasmBaseUrl: baseUrl,
shellManager,
outputCallback: this.outputCallback.bind(this),
})
}

async start(): Promise<void> {
this._term!.onResize(async (arg: any) => await this.onResize(arg))
this._term!.onData(async (data: string) => await this.onData(data))

const resizeObserver = new ResizeObserver((entries) => {
this._fitAddon!.fit()
})

this._term!.open(this._targetDiv)
await this._shell.start()
resizeObserver.observe(this._targetDiv)
}

async onData(data: string): Promise<void> {
await this._shell.input(data)
}

async onResize(arg: any): Promise<void> {
await this._shell.setSize(arg.rows, arg.cols)
}

private outputCallback(text: string): void {
this._term!.write(text)
}

private _targetDiv: HTMLElement;
private _term: Terminal
private _fitAddon: FitAddon
private _shell: Shell
}
Loading