diff --git a/compiler/wasm-pack/Cargo.toml b/compiler/wasm-pack/Cargo.toml new file mode 100644 index 000000000..8b27f0c79 --- /dev/null +++ b/compiler/wasm-pack/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "hello_world" +version = "0.1.0" +authors = ["The wasm-bindgen Developers"] +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "^0.2" +web-sys = "^0.3" +js-sys = "^0.3" +wasm-bindgen-futures = "^0.4" +yew = "0.17" +seed = "0.8.0" + +[package.metadata.wasm-pack.profile.release] +wasm-opt = false \ No newline at end of file diff --git a/compiler/wasm-pack/Dockerfile b/compiler/wasm-pack/Dockerfile new file mode 100644 index 000000000..ae2dd8a55 --- /dev/null +++ b/compiler/wasm-pack/Dockerfile @@ -0,0 +1,17 @@ +# syntax = docker/dockerfile:experimental + +# fetch dependencies to local +FROM shepmaster/rust-nightly as sources +RUN cargo install wasm-pack +ADD --chown=playground src/lib.rs /playground/src/lib.rs +# TODO support top 100 crates +ADD --chown=playground Cargo.toml /playground/Cargo.toml +RUN cargo fetch + +# build dependencies +FROM sources +RUN wasm-pack build --target web --out-name package --dev +RUN rm src/*.rs + +ADD --chown=playground cargo-pack /playground/.cargo/bin/ +ENTRYPOINT ["/playground/tools/entrypoint.sh"] diff --git a/compiler/wasm-pack/cargo-pack b/compiler/wasm-pack/cargo-pack new file mode 100755 index 000000000..64f14c25e --- /dev/null +++ b/compiler/wasm-pack/cargo-pack @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -eu + +# Rewrite our arguments to be `cargo build` instead of `cargo wasm`; +# this assumes that the command will always be `cargo wasm ...`. We +# capture the output directory in order to place the result file. +shift # Ignore "wasm" +args=() +while (( "$#" )); do + if [[ "$1" == "--" ]] ; then + : # Ignore + elif [[ "$1" == "-o" ]] ; then + shift + output="$1" + else + args+="$1" + fi + + shift +done +# Greatly inspired from https://gitlab.com/strwrite/seed-playground +# --dev flag disables the wasm-opt for optimization downloaded from networks +wasm-pack build --target web --out-name package --dev + +cat pkg/package_bg.wasm | base64 > "${output}.wasm" +cat pkg/package.js | base64 > "${output}.js" + diff --git a/compiler/wasm-pack/src/lib.rs b/compiler/wasm-pack/src/lib.rs new file mode 100644 index 000000000..8f6dcb2f2 --- /dev/null +++ b/compiler/wasm-pack/src/lib.rs @@ -0,0 +1,19 @@ +use wasm_bindgen::prelude::*; + +// Called by our JS entry point to run the example +#[wasm_bindgen(start)] +pub fn run() -> Result<(), JsValue> { + // Use `web_sys`'s global `window` function to get a handle on the global + // window object. + let window = web_sys::window().expect("no global `window` exists"); + let document = window.document().expect("should have a document on window"); + let body = document.body().expect("document should have a body"); + + // Manufacture the element we're gonna append + let val = document.create_element("p")?; + val.set_text_content(Some("Hello from Rust!")); + + body.append_child(&val)?; + + Ok(()) +} \ No newline at end of file diff --git a/ui/Cargo.lock b/ui/Cargo.lock index 4b8225a0e..c5c75ff2b 100644 --- a/ui/Cargo.lock +++ b/ui/Cargo.lock @@ -1,7 +1,5 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 - [[package]] name = "aho-corasick" version = "0.7.15" diff --git a/ui/frontend/BuildMenu.tsx b/ui/frontend/BuildMenu.tsx index 9a4ea349a..c75985868 100644 --- a/ui/frontend/BuildMenu.tsx +++ b/ui/frontend/BuildMenu.tsx @@ -26,6 +26,7 @@ const useDispatchAndClose = (action: () => void, close: () => void) => { const BuildMenu: React.SFC = props => { const isHirAvailable = useSelector(selectors.isHirAvailable); const isWasmAvailable = useSelector(selectors.isWasmAvailable); + const isWasmPackAvailable = useSelector(selectors.isWasmPackAvailable); const compile = useDispatchAndClose(actions.performCompile, props.close); const compileToAssembly = useDispatchAndClose(actions.performCompileToAssembly, props.close); @@ -33,6 +34,7 @@ const BuildMenu: React.SFC = props => { const compileToMir = useDispatchAndClose(actions.performCompileToMir, props.close); const compileToHir = useDispatchAndClose(actions.performCompileToNightlyHir, props.close); const compileToWasm = useDispatchAndClose(actions.performCompileToNightlyWasm, props.close); + const compileToWasmPack = useDispatchAndClose(actions.performCompileToNightlyWasmPack, props.close); const execute = useDispatchAndClose(actions.performExecute, props.close); const test = useDispatchAndClose(actions.performTest, props.close); @@ -67,6 +69,10 @@ const BuildMenu: React.SFC = props => { Build a WebAssembly module for web browsers, in the .WAT textual representation. {!isWasmAvailable && } + + Build a WebAssembly frontend for web browsers. Rendering in Iframe + {!isWasmPackAvailable && } + ); }; @@ -85,4 +91,10 @@ const WasmAside: React.SFC = () => (

); +const WasmPackAside: React.SFC = () => ( +

+ Note: WASM PACK currently requires using the Nightly channel, selecting this + option will switch to Nightly. +

+); export default BuildMenu; diff --git a/ui/frontend/Output.tsx b/ui/frontend/Output.tsx index ae5535e2d..736dea9fa 100644 --- a/ui/frontend/Output.tsx +++ b/ui/frontend/Output.tsx @@ -10,6 +10,7 @@ import Gist from './Output/Gist'; import Section from './Output/Section'; import SimplePane, { SimplePaneProps } from './Output/SimplePane'; import PaneWithMir from './Output/PaneWithMir'; +import PaneWithWasmPack from './Output/PaneWithWasmPack'; import * as selectors from './selectors'; const Tab: React.SFC = ({ kind, focus, label, onClick, tabProps }) => { @@ -46,7 +47,10 @@ interface PaneWithCodeProps extends SimplePaneProps { const Output: React.SFC = () => { const somethingToShow = useSelector(selectors.getSomethingToShow); - const { meta: { focus }, execute, format, clippy, miri, macroExpansion, assembly, llvmIr, mir, hir, wasm, gist } = + const { meta: { focus }, + execute, format, clippy, miri, + macroExpansion, assembly, llvmIr, mir, + hir, wasm, gist, wasmPack } = useSelector((state: State) => state.output); const dispatch = useDispatch(); @@ -62,6 +66,7 @@ const Output: React.SFC = () => { const focusHir = useCallback(() => dispatch(actions.changeFocus(Focus.Hir)), [dispatch]); const focusWasm = useCallback(() => dispatch(actions.changeFocus(Focus.Wasm)), [dispatch]); const focusGist = useCallback(() => dispatch(actions.changeFocus(Focus.Gist)), [dispatch]); + const focusWasmPack = useCallback(() => dispatch(actions.changeFocus(Focus.WasmPack)), [dispatch]); if (!somethingToShow) { return null; @@ -88,6 +93,7 @@ const Output: React.SFC = () => { {focus === Focus.Hir && } {focus === Focus.Wasm && } {focus === Focus.Gist && } + {focus === Focus.WasmPack && } ); } @@ -139,6 +145,10 @@ const Output: React.SFC = () => { label="Share" onClick={focusGist} tabProps={gist} /> + {close} {body} diff --git a/ui/frontend/Output/Container.tsx b/ui/frontend/Output/Container.tsx new file mode 100644 index 000000000..cad0f1a29 --- /dev/null +++ b/ui/frontend/Output/Container.tsx @@ -0,0 +1,35 @@ +import React, { useState, useEffect } from 'react' +import { createPortal } from 'react-dom' + +export const FunctionalIFrameComponent = ({ + children, + url, + ...props +}) => { + const [contentRef, setContentRef] = useState(null) + const document = + contentRef?.contentWindow?.document; + const mountNode = document?.body; + + useEffect(() => { + if (document) { + const script = document.createElement('script'); + script.src = url; + script.type = 'module'; + script.async = true; + mountNode && mountNode.appendChild(script); + } + + return () => { + if (mountNode) { + mountNode.innerHTML = '' + } + }; + }, [mountNode, document, url]); + + return ( + + ) +} diff --git a/ui/frontend/Output/PaneWithWasmPack.tsx b/ui/frontend/Output/PaneWithWasmPack.tsx new file mode 100644 index 000000000..0353cd600 --- /dev/null +++ b/ui/frontend/Output/PaneWithWasmPack.tsx @@ -0,0 +1,52 @@ +import React from 'react'; + +import Header from './Header'; +import SimplePane, { SimplePaneProps } from './SimplePane'; +import { FunctionalIFrameComponent } from './Container'; + +interface PaneWithWasmPackProps extends SimplePaneProps { + success?: boolean; + wasm_js?: string; + wasm_bg?: string; +} + +function base64ToByteArray(src: string) { + const decode = atob(src); + const byteNumbers = new Array(decode.length); + for (let i = 0; i < decode.length; i++) { + byteNumbers[i] = decode.charCodeAt(i); + } + return new Uint8Array(byteNumbers); +} + +function createObjectURL(src: ArrayBuffer | string, mime: string) { + return URL.createObjectURL(new Blob([src], { type: mime })); +} + +function createEntryJS(wasm_js: string, wasm_bg: string, success: boolean) { + if (!success) { + return ''; + } + const wasmJS = atob(wasm_js); + const bgWasm = base64ToByteArray(wasm_bg); + const wasmJSBlob = createObjectURL(wasmJS, 'application/javascript'); + const bgWasmBlob = createObjectURL(bgWasm, 'application/wasm'); + const entryJS = ` + import init from '${wasmJSBlob}'; + await init('${bgWasmBlob}'); + `; + + return createObjectURL(entryJS, 'application/javascript'); +} + +const PaneWithWasmPack: React.SFC = ({ wasm_js, wasm_bg, success, ...rest }) => ( + +
+
+ + +
+
+); + +export default PaneWithWasmPack; diff --git a/ui/frontend/actions.ts b/ui/frontend/actions.ts index 8b2e30d2d..19be55ab2 100644 --- a/ui/frontend/actions.ts +++ b/ui/frontend/actions.ts @@ -38,6 +38,7 @@ const routes = { clippy: { pathname: '/clippy' }, miri: { pathname: '/miri' }, macroExpansion: { pathname: '/macro-expansion' }, + wasmPack: { pathname: '/wasm-pack' }, meta: { crates: { pathname: '/meta/crates' }, version: { @@ -92,6 +93,9 @@ export enum ActionType { CompileWasmRequest = 'COMPILE_WASM_REQUEST', CompileWasmSucceeded = 'COMPILE_WASM_SUCCEEDED', CompileWasmFailed = 'COMPILE_WASM_FAILED', + CompileWasmPackRequest = 'COMPILE_WASM_PACK_REQUEST', + CompileWasmPackSucceeded = 'COMPILE_WASM_PACK_SUCCEEDED', + CompileWasmPackFailed = 'COMPILE_WASM_PACK_FAILED', EditCode = 'EDIT_CODE', AddMainFunction = 'ADD_MAIN_FUNCTION', AddImport = 'ADD_IMPORT', @@ -260,7 +264,7 @@ const performCommonExecute = (crateType, tests): ThunkAction => (dispatch, getSt }; function performAutoOnly(): ThunkAction { - return function(dispatch, getState) { + return function (dispatch, getState) { const state = getState(); const crateType = getCrateType(state); const tests = runAsTest(state); @@ -282,7 +286,7 @@ interface CompileRequestBody extends ExecuteRequestBody { function performCompileShow(target, { request, success, failure }): ThunkAction { // TODO: Check a cache - return function(dispatch, getState) { + return function (dispatch, getState) { dispatch(request()); const state = getState(); @@ -406,7 +410,45 @@ const performCompileToNightlyWasmOnly = (): ThunkAction => dispatch => { dispatch(changeChannel(Channel.Nightly)); dispatch(performCompileToWasm()); }; +interface WasmPackRequestBody { + code: string; +} + +function performCompileToWasmPack({ request, success, failure }): ThunkAction { + // TODO: Check a cache + return function (dispatch, getState) { + dispatch(request()); + const state = getState(); + const { code } = state; + const body: WasmPackRequestBody = { + code, + }; + return jsonPost(routes.wasmPack, body) + .then(json => dispatch(success(json))) + .catch(json => dispatch(failure(json))); + }; +} + +const requestCompileWasmPack = () => + createAction(ActionType.CompileWasmPackRequest); + +const receiveCompileWasmPackSuccess = ({ wasm_js, wasm_bg, stdout, stderr, success }) => + createAction(ActionType.CompileWasmPackSucceeded, { wasm_js, wasm_bg, stdout, stderr, success }); + +const receiveCompileWasmPackFailure = ({ error }) => + createAction(ActionType.CompileWasmPackFailed, { error }); + +const performCompileToNightlyWasmPackOnly = (): ThunkAction => dispatch => { + dispatch(changeChannel(Channel.Nightly)); + dispatch(performCompileToWasmPack( + { + request: requestCompileWasmPack, + success: receiveCompileWasmPackSuccess, + failure: receiveCompileWasmPackFailure, + } + )); +}; const PRIMARY_ACTIONS: { [index in PrimaryAction]: () => ThunkAction } = { [PrimaryActionCore.Asm]: performCompileToAssemblyOnly, [PrimaryActionCore.Compile]: performCompileOnly, @@ -417,6 +459,7 @@ const PRIMARY_ACTIONS: { [index in PrimaryAction]: () => ThunkAction } = { [PrimaryActionCore.Hir]: performCompileToHirOnly, [PrimaryActionCore.Mir]: performCompileToMirOnly, [PrimaryActionCore.Wasm]: performCompileToNightlyWasmOnly, + [PrimaryActionCore.WasmPack]: performCompileToNightlyWasmPackOnly, }; export const performPrimaryAction = (): ThunkAction => (dispatch, getState) => { @@ -446,6 +489,8 @@ export const performCompileToNightlyHir = performAndSwitchPrimaryAction(performCompileToNightlyHirOnly, PrimaryActionCore.Hir); export const performCompileToNightlyWasm = performAndSwitchPrimaryAction(performCompileToNightlyWasmOnly, PrimaryActionCore.Wasm); +export const performCompileToNightlyWasmPack = + performAndSwitchPrimaryAction(performCompileToNightlyWasmPackOnly, PrimaryActionCore.WasmPack); export const editCode = (code: string) => createAction(ActionType.EditCode, { code }); @@ -488,7 +533,7 @@ const receiveFormatFailure = (body: FormatResponseBody) => export function performFormat(): ThunkAction { // TODO: Check a cache - return function(dispatch, getState) { + return function (dispatch, getState) { dispatch(requestFormat()); const body: FormatRequestBody = formatRequestSelector(getState()); @@ -522,7 +567,7 @@ const receiveClippyFailure = ({ error }) => export function performClippy(): ThunkAction { // TODO: Check a cache - return function(dispatch, getState) { + return function (dispatch, getState) { dispatch(requestClippy()); const body: ClippyRequestBody = clippyRequestSelector(getState()); @@ -549,7 +594,7 @@ const receiveMiriFailure = ({ error }) => export function performMiri(): ThunkAction { // TODO: Check a cache - return function(dispatch, getState) { + return function (dispatch, getState) { dispatch(requestMiri()); const { code, configuration: { @@ -579,7 +624,7 @@ const receiveMacroExpansionFailure = ({ error }) => export function performMacroExpansion(): ThunkAction { // TODO: Check a cache - return function(dispatch, getState) { + return function (dispatch, getState) { dispatch(requestMacroExpansion()); const { code, configuration: { @@ -617,7 +662,7 @@ type PerformGistLoadProps = Pick>; export function performGistLoad({ id, channel, mode, edition }: PerformGistLoadProps): ThunkAction { - return function(dispatch, _getState) { + return function (dispatch, _getState) { dispatch(requestGistLoad()); const u = url.resolve(routes.meta.gist.pathname, id); jsonGet(u) @@ -636,7 +681,7 @@ const receiveGistSaveFailure = ({ error }) => // eslint-disable-line no-unused-v createAction(ActionType.GistSaveFailed, { error }); export function performGistSave(): ThunkAction { - return function(dispatch, getState) { + return function (dispatch, getState) { dispatch(requestGistSave()); const { code, configuration: { channel, mode, edition }, output: { execute: { stdout, stderr } } } = getState(); @@ -654,7 +699,7 @@ const receiveCratesLoadSuccess = ({ crates }) => createAction(ActionType.CratesLoadSucceeded, { crates }); export function performCratesLoad(): ThunkAction { - return function(dispatch) { + return function (dispatch) { dispatch(requestCratesLoad()); return jsonGet(routes.meta.crates) @@ -670,7 +715,7 @@ const receiveVersionsLoadSuccess = ({ stable, beta, nightly, rustfmt, clippy, mi createAction(ActionType.VersionsLoadSucceeded, { stable, beta, nightly, rustfmt, clippy, miri }); export function performVersionsLoad(): ThunkAction { - return function(dispatch) { + return function (dispatch) { dispatch(requestVersionsLoad()); const stable = jsonGet(routes.meta.version.stable); @@ -742,7 +787,7 @@ export function indexPageLoad({ mode: modeString = 'debug', edition: editionString, }): ThunkAction { - return function(dispatch) { + return function (dispatch) { const channel = parseChannel(version); const mode = parseMode(modeString); let edition = parseEdition(editionString); @@ -783,7 +828,7 @@ export function helpPageLoad() { } export function showExample(code): ThunkAction { - return function(dispatch) { + return function (dispatch) { dispatch(navigateToIndex()); dispatch(editCode(code)); }; @@ -823,6 +868,9 @@ export type Action = | ReturnType | ReturnType | ReturnType + | ReturnType + | ReturnType + | ReturnType | ReturnType | ReturnType | ReturnType diff --git a/ui/frontend/index.scss b/ui/frontend/index.scss index e06a60f24..fed78bede 100644 --- a/ui/frontend/index.scss +++ b/ui/frontend/index.scss @@ -894,3 +894,12 @@ $header-transition: 0.2s ease-in-out; cursor: pointer; } } + +.container { + height: 100%; + width: 100%; + margin: 0; + border: 0; + padding: 0; + overflow: hidden; +} \ No newline at end of file diff --git a/ui/frontend/reducers/output/index.ts b/ui/frontend/reducers/output/index.ts index 8942a967a..61ff8d4a7 100644 --- a/ui/frontend/reducers/output/index.ts +++ b/ui/frontend/reducers/output/index.ts @@ -12,6 +12,7 @@ import meta from './meta'; import mir from './mir'; import miri from './miri'; import wasm from './wasm'; +import wasmPack from './wasmPack'; const output = combineReducers({ meta, @@ -26,6 +27,7 @@ const output = combineReducers({ wasm, execute, gist, + wasmPack, }); export type State = ReturnType; diff --git a/ui/frontend/reducers/output/meta.ts b/ui/frontend/reducers/output/meta.ts index 37689c903..f0423871b 100644 --- a/ui/frontend/reducers/output/meta.ts +++ b/ui/frontend/reducers/output/meta.ts @@ -35,6 +35,9 @@ export default function meta(state = DEFAULT, action: Action) { case ActionType.CompileWasmRequest: return { ...state, focus: Focus.Wasm }; + case ActionType.CompileWasmPackRequest: + return { ...state, focus: Focus.WasmPack }; + case ActionType.CompileAssemblyRequest: return { ...state, focus: Focus.Asm }; diff --git a/ui/frontend/reducers/output/wasmPack.ts b/ui/frontend/reducers/output/wasmPack.ts new file mode 100644 index 000000000..db3221682 --- /dev/null +++ b/ui/frontend/reducers/output/wasmPack.ts @@ -0,0 +1,37 @@ +import { Action, ActionType } from '../../actions'; +import { finish, start } from './sharedStateManagement'; + +const DEFAULT: State = { + requestsInProgress: 0, + success: false, + stdout: null, + stderr: null, + error: null, + wasm_js: null, + wasm_bg: null, +}; + +interface State { + requestsInProgress: number; + success?: boolean; + stdout?: string; + stderr?: string; + error?: string; + wasm_js?: string; + wasm_bg?: string; +} + +export default function wasmPack(state = DEFAULT, action: Action) { + switch (action.type) { + case ActionType.CompileWasmPackRequest: + return start(DEFAULT, state); + case ActionType.CompileWasmPackSucceeded: { + const { stdout = '', stderr = '', wasm_js = '', wasm_bg = '', success = false } = action; + return finish(state, { stdout, stderr, wasm_js, wasm_bg, success }); + } + case ActionType.CompileWasmPackFailed: + return finish(state, { error: action.error }); + default: + return state; + } +} diff --git a/ui/frontend/selectors/index.ts b/ui/frontend/selectors/index.ts index 90e3e52ef..370748896 100644 --- a/ui/frontend/selectors/index.ts +++ b/ui/frontend/selectors/index.ts @@ -75,6 +75,7 @@ const LABELS: { [index in PrimaryActionCore]: string } = { [PrimaryActionCore.Mir]: 'Show MIR', [PrimaryActionCore.Test]: 'Test', [PrimaryActionCore.Wasm]: 'Show WASM', + [PrimaryActionCore.WasmPack]: 'Show WASM PACK', }; export const getExecutionLabel = createSelector(primaryActionSelector, primaryAction => LABELS[primaryAction]); @@ -108,6 +109,7 @@ export const isNightlyChannel = (state: State) => ( ); export const isWasmAvailable = isNightlyChannel; export const isHirAvailable = isNightlyChannel; +export const isWasmPackAvailable = isNightlyChannel; export const getModeLabel = (state: State) => { const { configuration: { mode } } = state; @@ -149,6 +151,7 @@ const getOutputs = (state: State) => [ state.output.miri, state.output.macroExpansion, state.output.wasm, + state.output.wasmPack, ]; export const getSomethingToShow = createSelector( diff --git a/ui/frontend/types.ts b/ui/frontend/types.ts index a21518986..ac94847c5 100644 --- a/ui/frontend/types.ts +++ b/ui/frontend/types.ts @@ -78,6 +78,7 @@ export enum PrimaryActionCore { Mir = 'mir', Test = 'test', Wasm = 'wasm', + WasmPack = 'wasm-pack', } export type PrimaryAction = PrimaryActionCore | PrimaryActionAuto; @@ -115,6 +116,7 @@ export enum Focus { Execute = 'execute', Format = 'format', Gist = 'gist', + WasmPack = 'wasm-pack', } export enum Notification { diff --git a/ui/src/main.rs b/ui/src/main.rs index 2421bb8f2..a337087b0 100644 --- a/ui/src/main.rs +++ b/ui/src/main.rs @@ -85,6 +85,7 @@ fn main() { mount.mount("/meta/version/miri", meta_version_miri); mount.mount("/meta/gist", gist_router); mount.mount("/evaluate.json", evaluate); + mount.mount("/wasm-pack", wasm_pack); let mut chain = Chain::new(mount); let file_logger = FileLogger::new(logfile).expect("Unable to create file logger"); @@ -281,6 +282,16 @@ fn evaluate(req: &mut Request<'_, '_>) -> IronResult { }) } +fn wasm_pack(req: &mut Request<'_, '_>) -> IronResult{ + with_sandbox(req, |sandbox, req: WasmPackRequest| { + let req = req.try_into()?; + sandbox + .wasm_pack(&req) + .map(WasmPackResponse::from) + .context(WasmPack) + }) +} + fn with_sandbox(req: &mut Request<'_, '_>, f: F) -> IronResult where F: FnOnce(Sandbox, Req) -> Result, @@ -488,6 +499,8 @@ pub enum Error { Execution { source: sandbox::Error }, #[snafu(display("Evaluation operation failed: {}", source))] Evaluation { source: sandbox::Error }, + #[snafu(display("wasm-pack operation failed: {}", source))] + WasmPack { source: sandbox::Error }, #[snafu(display("Linting operation failed: {}", source))] Linting { source: sandbox::Error }, #[snafu(display("Expansion operation failed: {}", source))] @@ -691,6 +704,20 @@ struct EvaluateResponse { error: Option, } +#[derive(Debug, Clone, Deserialize)] +struct WasmPackRequest { + code: String +} + +#[derive(Debug, Clone, Serialize)] +struct WasmPackResponse { + success: bool, + wasm_js: String, + wasm_bg: String, + stdout: String, + stderr: String, +} + impl TryFrom for sandbox::CompileRequest { type Error = Error; @@ -927,6 +954,29 @@ impl From for EvaluateResponse { } } +impl TryFrom for sandbox::WasmPackRequest { + type Error = Error; + + fn try_from(me: WasmPackRequest) -> Result { + Ok(sandbox::WasmPackRequest { + code: me.code, + ..sandbox::WasmPackRequest::default() + }) + } +} + +impl From for WasmPackResponse { + fn from(me: sandbox::WasmPackResponse) -> Self { + WasmPackResponse { + success: me.success, + wasm_bg: me.wasm_bg, + wasm_js: me.wasm_js, + stdout: me.stdout, + stderr: me.stderr, + } + } +} + fn parse_target(s: &str) -> Result { Ok(match s { "asm" => sandbox::CompileTarget::Assembly(sandbox::AssemblyFlavor::Att, diff --git a/ui/src/sandbox.rs b/ui/src/sandbox.rs index 2b529d088..fac6624ae 100644 --- a/ui/src/sandbox.rs +++ b/ui/src/sandbox.rs @@ -194,6 +194,43 @@ impl Sandbox { }) } + // Greatly inspired from https://gitlab.com/strwrite/seed-playground + pub fn wasm_pack(&self, req: &WasmPackRequest) -> Result { + use CompileTarget::*; + use CrateType::*; + + let compile_req = CompileRequest{ + backtrace: false, + channel: Channel::WasmPack, + code: req.code.clone(), + crate_type: Library(LibraryType::Cdylib), + edition: Some(Edition::Rust2018), + mode: Mode::Debug, + target: WasmPack, + tests: false, + }; + let res = self.compile(&compile_req)?; + let js_file = + fs::read_dir(&self.output_dir) + .context(UnableToReadOutput)? + .flat_map(|entry| entry) + .map(|entry| entry.path()) + .find(|path| path.extension() == Some(OsStr::new("js"))); + + let js_code = match js_file { + Some(file) => read(&file)?.unwrap_or_else(String::new), + None => String::new() // TODO: return proper error? + }; + + Ok(WasmPackResponse { + success: res.success, + stdout: res.stdout, + stderr: res.stderr, + wasm_bg: res.code, + wasm_js: js_code + }) + } + pub fn format(&self, req: &FormatRequest) -> Result { self.write_source_code(&req.code)?; let command = self.format_command(req); @@ -419,10 +456,14 @@ impl Sandbox { mount_output_dir.push(":"); mount_output_dir.push("/playground-result"); + // let mut mount_output_wasm = self.scratch.as_path().join("pkg").as_os_str().to_os_string(); + // mount_output_wasm.push(":"); + // mount_output_wasm.push("/playground/pkg"); let mut cmd = basic_secure_docker_command(); cmd .arg("--volume").arg(&mount_input_file) + // .arg("--volume").arg(&mount_output_wasm) .arg("--volume").arg(&mount_output_dir); cmd @@ -469,6 +510,7 @@ fn build_execution_command(target: Option, channel: Channel, mode let mut cmd = vec!["cargo"]; match (target, req.crate_type(), tests) { + (Some(WasmPack), _, _) => cmd.push("pack"), (Some(Wasm), _, _) => cmd.push("wasm"), (Some(_), _, _) => cmd.push("rustc"), (_, _, true) => cmd.push("test"), @@ -511,12 +553,14 @@ fn build_execution_command(target: Option, channel: Channel, mode Mir => cmd.push("--emit=mir"), Hir => cmd.push("-Zunpretty=hir"), Wasm => { /* handled by cargo-wasm wrapper */ }, + WasmPack => { /* handled by cargo-wasmpack wrapper */ }, } - } - + } + log::debug!("{:?}", &cmd); cmd } + fn set_execution_environment(cmd: &mut Command, target: Option, req: impl CrateTypeRequest + EditionRequest + BacktraceRequest) { use self::CompileTarget::*; @@ -623,6 +667,7 @@ pub enum CompileTarget { Mir, Hir, Wasm, + WasmPack, } impl CompileTarget { @@ -633,6 +678,7 @@ impl CompileTarget { CompileTarget::Mir => "mir", CompileTarget::Hir => "hir", CompileTarget::Wasm => "wat", + CompileTarget::WasmPack => "wasm", }; OsStr::new(ext) } @@ -648,6 +694,7 @@ impl fmt::Display for CompileTarget { Mir => "Rust MIR".fmt(f), Hir => "Rust HIR".fmt(f), Wasm => "WebAssembly".fmt(f), + WasmPack => "WasmPack".fmt(f), } } } @@ -657,6 +704,7 @@ pub enum Channel { Stable, Beta, Nightly, + WasmPack, } impl Channel { @@ -667,6 +715,7 @@ impl Channel { Stable => "rust-stable", Beta => "rust-beta", Nightly => "rust-nightly", + WasmPack => "rust-wasm-pack", } } } @@ -924,6 +973,35 @@ pub struct MacroExpansionResponse { pub stderr: String, } +#[derive(Debug, Clone)] +pub struct WasmPackRequest { + pub code: String, + pub crate_type: CrateType, + pub output_name: String, +} + +impl Default for WasmPackRequest { + fn default() -> Self { + WasmPackRequest { + code: String::from(""), + crate_type: CrateType::Library(LibraryType::Rlib), + output_name: "wasm".to_string(), + } + } +} + +impl CrateTypeRequest for WasmPackRequest { + fn crate_type(&self) -> CrateType { self.crate_type } +} + +#[derive(Debug, Clone)] +pub struct WasmPackResponse { + pub wasm_js: String, + pub wasm_bg: String, + pub success: bool, + pub stdout: String, + pub stderr: String, +} #[cfg(test)] mod test { use super::*;