Skip to content
Open
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
3 changes: 2 additions & 1 deletion .github/workflows/native-image-wasm-spring-shell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ jobs:
- name: Run 'native-image/wasm-spring-shell'
run: |
cd native-image/wasm-spring-shell
./mvnw --no-transfer-progress -Pnative native:compile
./mvnw --no-transfer-progress -Pnative,cli package
node target/wasm-spring-shell help
node target/wasm-spring-shell hello Jane
./mvnw --no-transfer-progress -Pnative,web package
21 changes: 19 additions & 2 deletions native-image/wasm-spring-shell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ This demo requires:

## Run Spring Shell on Node

1. Build the Wasm module with the `native` profile:
1. Build the Wasm module with the `native` and `cli` profiles:
```bash
$ ./mvnw -Pnative package
$ ./mvnw -Pnative,cli package
```
The demo uses the [Native Build Tools](https://graalvm.github.io/native-build-tools/latest/index.html) for building native images with GraalVM and Maven.
This command generates a Wasm file and a corresponding JavaScript binding in the `target` directory.
Expand All @@ -25,3 +25,20 @@ This demo requires:
node target/wasm-spring-shell hello Jane
```
This requires Node.js 22 or later.

## Run Spring Shell in the Browser

1. Build the Wasm module with the `native` and `web` profiles:
```bash
./mvnw -Pnative,web package
```
This command generates a Wasm file and a corresponding JavaScript binding in the `web` directory.

2. Run a local web server in the `web` directory:
```bash
cd web
python server.py
```
This will serve the `web` directory locally on port `8000`.

3. Navigate to http://localhost:8000 to run the Spring Shell in the browser.
112 changes: 88 additions & 24 deletions native-image/wasm-spring-shell/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@
<spring-shell.version>3.4.1</spring-shell.version>
</properties>
<dependencies>
<dependency>
<groupId>org.graalvm.sdk</groupId>
<artifactId>webimage-preview</artifactId>
<version>25.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand All @@ -59,28 +63,88 @@
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<buildArgs>
<!-- Enable Web Image -->
<buildArg>--tool:svm-wasm</buildArg>
<!-- Annotate Wasm module with debug info -->
<buildArg>-g</buildArg>
<!-- macOS-specific initialization policies -->
<buildArg>--initialize-at-build-time=apple.security.AppleProvider</buildArg>
<buildArg>--initialize-at-build-time=apple.security.AppleProvider$ProviderService</buildArg>
</buildArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>cli</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<buildArgs>
<!-- Enable Web Image -->
<buildArg>--tool:svm-wasm</buildArg>
<!-- Annotate Wasm module with debug info -->
<buildArg>-g</buildArg>
<!-- macOS-specific initialization policies -->
<buildArg>--initialize-at-build-time=apple.security.AppleProvider</buildArg>
<buildArg>--initialize-at-build-time=apple.security.AppleProvider$ProviderService</buildArg>
</buildArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.example.wasm_spring_shell.cli.WasmSpringShellApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</profile>

<profile>
<id>web</id>
<properties>
<spring.profiles.active>web</spring.profiles.active>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<imageName>../web/wasm-spring-shell</imageName>
<buildArgs>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think with something like this, you can append -H:-AutoRunVM to the default list, and not repeat and override it:

Suggested change
<buildArgs>
<buildArgs combine.children="append">

<!-- Enable Web Image -->
<buildArg>--tool:svm-wasm</buildArg>
<!-- Annotate Wasm module with debug info -->
<buildArg>-g</buildArg>
<!-- macOS-specific initialization policies -->
<buildArg>--initialize-at-build-time=apple.security.AppleProvider</buildArg>
<buildArg>--initialize-at-build-time=apple.security.AppleProvider$ProviderService</buildArg>
<buildArg>-H:-AutoRunVM</buildArg>
</buildArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.example.wasm_spring_shell.web.Worker</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,18 @@
* Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL.
*/

package com.example.wasm_spring_shell;
package com.example.wasm_spring_shell.cli;

import com.example.wasm_spring_shell.common.MyCommands;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.ansi.AnsiOutput;
import org.springframework.boot.ansi.AnsiOutput.Enabled;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@SpringBootApplication(scanBasePackageClasses = {WasmSpringShellApplication.class, MyCommands.class})
public class WasmSpringShellApplication {

public static void main(String[] args) {
SpringApplication.run(WasmSpringShellApplication.class, args);
}

static {
/* Always enable colorful terminal output. */
AnsiOutput.setEnabled(Enabled.ALWAYS);
}

SpringApplication.run(WasmSpringShellApplication.class, args);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Licensed under the Universal Permissive License v 1.0 as shown at https://opensource.org/license/UPL.
*/

package com.example.wasm_spring_shell;
package com.example.wasm_spring_shell.common;

import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
Expand All @@ -17,5 +17,5 @@ public class MyCommands {
public String hello(@ShellOption(defaultValue = "Spring") String arg) {
return "Hello " + arg + " from WebAssembly built with GraalVM!";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example.wasm_spring_shell.web;

import org.jline.utils.AttributedCharSequence;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.shell.ResultHandler;
import org.springframework.stereotype.Component;

import java.io.PrintWriter;
import java.io.StringWriter;

@Component
public class CustomResultHandler implements ResultHandler<Object> {
@Override
public void handleResult(Object result) {
switch (result) {
case null -> Worker.sendErrorMessage("null result");
case AttributedCharSequence attributed -> {
System.out.println(attributed);
Worker.sendOutputMessage(attributed.toAnsi());
}
case Throwable throwable -> {
StringWriter stringWriter = new StringWriter();
throwable.printStackTrace(new PrintWriter(stringWriter));
Worker.sendErrorMessage(stringWriter.toString());
}
default -> Worker.sendOutputMessage(result.toString());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.example.wasm_spring_shell.web;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add copyright header, here and in all other .java files


import org.graalvm.webimage.api.JS;
import org.jline.reader.History;
import org.jline.reader.ParsedLine;
import org.jline.reader.impl.DefaultParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.shell.Input;
import org.springframework.shell.InputProvider;
import org.springframework.shell.Utils;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class DemoInputProvider implements InputProvider {
private static final Logger LOG = LoggerFactory.getLogger(DemoInputProvider.class);

private final History history;

public DemoInputProvider(History history) {
this.history = history;
}

@Override
public Input readInput() {
var lineParser = new DefaultParser();
Worker.sendReadyMessage();
LOG.info("Waiting for message...");
waitForMessage();
String line = getMessage();
LOG.info("Got message {}", line);
history.add(line);
ParsedLine parsedLine = lineParser.parse(line, line.length() + 1);
return new SimpleInput(parsedLine);
}

@JS.Coerce
@JS("""
const length = globalThis.sharedI32[1];
const byteArray = new Uint8Array(length);
byteArray.set(new Uint8Array(globalThis.sharedBuffer, 8, length))
return new TextDecoder().decode(byteArray);
""")
public static native String getMessage();

@JS("""
Atomics.wait(globalThis.sharedI32, 0, 0);
Atomics.store(globalThis.sharedI32, 0, 0);
""")
public static native void waitForMessage();

public static class SimpleInput implements Input {
private final ParsedLine parsedLine;

public SimpleInput(ParsedLine parsedLine) {
this.parsedLine = parsedLine;
}

@Override
public String rawText() {
return parsedLine.line();
}

@Override
public List<String> words() {
return Utils.sanitizeInput(parsedLine.words());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.wasm_spring_shell.web;

import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.context.annotation.Primary;
import org.springframework.shell.result.GenericResultHandlerService;
import org.springframework.stereotype.Component;

@Primary
@Component
public class MyResultHandlerService extends GenericResultHandlerService {
public MyResultHandlerService(CustomResultHandler handler) {
this.addResultHandler(handler);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.wasm_spring_shell.web;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.shell.Shell;
import org.springframework.shell.context.InteractionMode;
import org.springframework.shell.context.ShellContext;
import org.springframework.stereotype.Component;

@Component
public class ShellRunner implements CommandLineRunner {
private final ApplicationContext context;

public ShellRunner(ApplicationContext context) {
this.context = context;
}

@Override
public void run(String... args) throws Exception {
context.getBean(ShellContext.class).setInteractionMode(InteractionMode.INTERACTIVE);
context.getBean(Shell.class).run(context.getBean(DemoInputProvider.class));
}
}
Loading