feat: add web playground with Docker sandbox

- playground/backend/main.go — Go HTTP server with /compile endpoint
- playground/frontend/index.html — Monaco Editor with 8 code examples
- playground/sandbox/Dockerfile — isolated Docker container with buxc2
- playground/docker-compose.yml — orchestrates backend + sandbox
- playground/Makefile — build and run targets
- playground/README.md — deployment guide for VPS with nginx + SSL
This commit is contained in:
2026-06-10 20:33:26 +03:00
parent 8f68a9da1a
commit ef4aa42b0b
7 changed files with 1049 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
# Bux Playground — Makefile
# Run from project root: make -C playground <target>
.PHONY: all build-sandbox build-backend run stop clean
SANDBOX_IMAGE = bux-playground-sandbox
BACKEND_IMAGE = bux-playground-backend
PORT = 8080
all: build-sandbox build-backend
# Build the sandbox Docker image (needs buxc2 + lib + rt)
build-sandbox:
@echo "Building sandbox image..."
@docker build \
-f sandbox/Dockerfile \
-t $(SANDBOX_IMAGE) \
..
# Build the backend Go server
build-backend:
@echo "Building backend image..."
@docker build \
-f backend/Dockerfile \
-t $(BACKEND_IMAGE) \
.
# Run everything with docker-compose
run:
@echo "Starting Bux Playground on http://localhost:$(PORT)"
@docker-compose up
# Run in background
run-detached:
@echo "Starting Bux Playground in background on http://localhost:$(PORT)"
@docker-compose up -d
# Stop
stop:
@docker-compose down
# Clean images and containers
clean:
@docker-compose down --rmi all -v
@docker rmi $(SANDBOX_IMAGE) $(BACKEND_IMAGE) 2>/dev/null || true
# Development mode (local backend, no Docker sandbox)
dev:
@echo "Starting backend in dev mode..."
cd backend && go run main.go
# Quick test
test:
@echo "Testing /health endpoint..."
@curl -s http://localhost:$(PORT)/health | jq .
+165
View File
@@ -0,0 +1,165 @@
# Bux Playground
Interactive web playground for the Bux programming language. Compile and run Bux code directly in your browser.
## Architecture
```
┌─────────────┐ POST /compile ┌──────────────┐ docker run ┌─────────────┐
│ Browser │ ────────────────────► │ Go Backend │ ────────────────► │ Sandbox │
│ (Monaco) │ ◄─── JSON output │ (8080) │ │ (Docker) │
└─────────────┘ └──────────────┘ └─────────────┘
```
## Quick Start
```bash
# 1. Build both images (from project root)
make -C playground all
# 2. Start the playground
make -C playground run
# 3. Open http://localhost:8080
```
## Development Mode
```bash
# Run backend locally without Docker sandbox
# (requires buxc2 in PATH)
cd playground/backend
go run main.go
```
Then open `playground/frontend/index.html` directly in browser.
## Deployment on VPS
### 1. Build and copy to VPS
```bash
# Local machine
make selfhost # Build buxc2
make -C playground all # Build Docker images
docker save bux-playground-sandbox | gzip > sandbox.tar.gz
docker save bux-playground-backend | gzip > backend.tar.gz
# Copy to VPS
scp sandbox.tar.gz backend.tar.gz user@vps:/tmp/
```
### 2. On VPS
```bash
# Load images
docker load < /tmp/sandbox.tar.gz
docker load < /tmp/backend.tar.gz
# Run with docker-compose
cd /opt/bux-playground
docker-compose up -d
```
### 3. nginx reverse proxy + SSL
```nginx
server {
listen 80;
server_name play.bux-lang.org;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name play.bux-lang.org;
ssl_certificate /etc/letsencrypt/live/play.bux-lang.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/play.bux-lang.org/privkey.pem;
location / {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
```
### 4. systemd service
```ini
# /etc/systemd/system/bux-playground.service
[Unit]
Description=Bux Playground
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/bux-playground
ExecStart=/usr/bin/docker-compose up -d
ExecStop=/usr/bin/docker-compose down
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl enable bux-playground
sudo systemctl start bux-playground
```
## Security
- **Sandbox**: Each compile runs in a fresh Docker container with:
- No network access (`--network=none`)
- 128MB memory limit
- 1 CPU core limit
- 64 process limit
- Read-only filesystem
- `timeout 5s` for program execution
- Temporary files auto-deleted
- **Code limits**:
- Max code size: 64KB
- Max output size: 64KB
- Compile timeout: 10s
- Run timeout: 5s
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `PORT` | `8080` | HTTP server port |
| `USE_DOCKER` | `1` | Use Docker sandbox (0 = local mode) |
| `SANDBOX_IMAGE` | `bux-playground-sandbox` | Docker image name |
| `BUXC2_PATH` | `buxc2` | Path to buxc2 binary (local mode) |
## API
### POST /compile
Compile and run Bux code.
**Request:** `text/plain` — Bux source code
**Response:**
```json
{
"output": "Hello, Bux!\n",
"isError": false
}
```
### GET /health
Health check endpoint.
**Response:**
```json
{"status": "ok"}
```
+18
View File
@@ -0,0 +1,18 @@
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY main.go .
RUN go mod init bux-playground 2>/dev/null || true
RUN go build -o playground-server main.go
FROM alpine:latest
RUN apk add --no-cache ca-certificates docker-cli
WORKDIR /app
COPY --from=builder /app/playground-server .
COPY frontend ./frontend
EXPOSE 8080
CMD ["./playground-server"]
+240
View File
@@ -0,0 +1,240 @@
package main
import (
"bytes"
"context"
"encoding/json"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
const (
compileTimeout = 10 * time.Second
runTimeout = 5 * time.Second
maxOutputSize = 64 * 1024 // 64KB
maxCodeSize = 64 * 1024 // 64KB
)
type CompileRequest struct {
Code string `json:"code"`
}
type CompileResponse struct {
Output string `json:"output"`
IsError bool `json:"isError"`
}
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
mux := http.NewServeMux()
mux.HandleFunc("/compile", handleCompile)
mux.HandleFunc("/health", handleHealth)
mux.Handle("/", http.FileServer(http.Dir("./frontend")))
log.Printf("Bux Playground server starting on :%s", port)
log.Fatal(http.ListenAndServe(":"+port, cors(mux)))
}
func cors(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
func handleHealth(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func handleCompile(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Read code from body
body, err := io.ReadAll(io.LimitReader(r.Body, maxCodeSize+1))
if err != nil {
writeError(w, "Failed to read request body")
return
}
if len(body) > maxCodeSize {
writeError(w, "Code exceeds maximum size (64KB)")
return
}
code := strings.TrimSpace(string(body))
if code == "" {
writeError(w, "Empty code")
return
}
// Create temp project directory
tmpDir, err := os.MkdirTemp("", "bux-playground-*")
if err != nil {
writeError(w, "Failed to create temp directory")
return
}
defer os.RemoveAll(tmpDir)
// Write code to src/Main.bux
srcDir := filepath.Join(tmpDir, "src")
if err := os.MkdirAll(srcDir, 0755); err != nil {
writeError(w, "Failed to create src directory")
return
}
mainFile := filepath.Join(srcDir, "Main.bux")
if err := os.WriteFile(mainFile, []byte(code), 0644); err != nil {
writeError(w, "Failed to write source file")
return
}
// Write bux.toml
buxToml := `[Package]
Name = "playground"
Version = "0.1.0"
Type = "bin"
[Build]
Output = "Bin"
`
if err := os.WriteFile(filepath.Join(tmpDir, "bux.toml"), []byte(buxToml), 0644); err != nil {
writeError(w, "Failed to write bux.toml")
return
}
// Run buxc2 build inside Docker sandbox
output, isError := runInSandbox(tmpDir)
// Truncate output if too large
if len(output) > maxOutputSize {
output = output[:maxOutputSize] + "\n... (output truncated)"
}
resp := CompileResponse{
Output: output,
IsError: isError,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func runInSandbox(projectDir string) (string, bool) {
// Check if running inside Docker (sandbox container available)
sandboxImage := os.Getenv("SANDBOX_IMAGE")
if sandboxImage == "" {
sandboxImage = "bux-playground-sandbox"
}
// Determine if we use Docker or local buxc2
useDocker := os.Getenv("USE_DOCKER") != "0"
var cmd *exec.Cmd
var ctx context.Context
var cancel context.CancelFunc
if useDocker {
// Run compilation inside Docker container
ctx, cancel = context.WithTimeout(context.Background(), compileTimeout)
defer cancel()
cmd = exec.CommandContext(ctx, "docker", "run",
"--rm",
"--network=none",
"--memory=128m",
"--memory-swap=128m",
"--cpus=1.0",
"--pids-limit=64",
"--read-only",
"--tmpfs", "/tmp:noexec,nosuid,size=50m",
"-v", projectDir+":/project:ro",
"-w", "/project",
sandboxImage,
"sh", "-c",
"buxc2 build 2>&1 && timeout 5s ./build/playground 2>&1 || true",
)
} else {
// Local mode (for development)
ctx, cancel = context.WithTimeout(context.Background(), compileTimeout)
defer cancel()
buxc2Path := os.Getenv("BUXC2_PATH")
if buxc2Path == "" {
buxc2Path = "buxc2"
}
// First compile
compileCmd := exec.CommandContext(ctx, buxc2Path, "build")
compileCmd.Dir = projectDir
compileCmd.Env = append(os.Environ(),
"HOME=/tmp",
)
compileOut, compileErr := compileCmd.CombinedOutput()
output := string(compileOut)
if compileErr != nil {
return output, true
}
// Then run the binary
runCtx, runCancel := context.WithTimeout(context.Background(), runTimeout)
defer runCancel()
binPath := filepath.Join(projectDir, "build", "playground")
runCmd := exec.CommandContext(runCtx, binPath)
runCmd.Dir = projectDir
runOut, runErr := runCmd.CombinedOutput()
output += string(runOut)
if runErr != nil && runCtx.Err() != context.DeadlineExceeded {
output += "\nRuntime error: " + runErr.Error()
}
return output, false
}
// Run Docker command
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
err := cmd.Run()
output := out.String()
if ctx.Err() == context.DeadlineExceeded {
output += "\nTimeout: compilation or execution took too long"
return output, true
}
if err != nil {
return output, true
}
return output, false
}
func writeError(w http.ResponseWriter, msg string) {
resp := CompileResponse{
Output: msg,
IsError: true,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
+27
View File
@@ -0,0 +1,27 @@
version: '3.8'
services:
backend:
build:
context: .
dockerfile: backend/Dockerfile
ports:
- "8080:8080"
environment:
- PORT=8080
- USE_DOCKER=1
- SANDBOX_IMAGE=bux-playground-sandbox
volumes:
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
- sandbox
restart: unless-stopped
# Sandbox image is built separately and used by backend
sandbox:
build:
context: ..
dockerfile: playground/sandbox/Dockerfile
image: bux-playground-sandbox
# This container is not run directly — backend spawns it per-request
command: ["echo", "sandbox image ready"]
+511
View File
@@ -0,0 +1,511 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bux Playground</title>
<link rel="stylesheet" data-name="vs/editor/editor.main" href="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/editor/editor.main.min.css">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: #0d1117;
color: #c9d1d9;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: #161b22;
padding: 10px 20px;
border-bottom: 1px solid #30363d;
display: flex;
align-items: center;
gap: 16px;
flex-shrink: 0;
}
.header .logo {
font-weight: 700;
color: #58a6ff;
font-size: 18px;
display: flex;
align-items: center;
gap: 8px;
}
.header .logo::before {
content: '◈';
color: #7ee787;
}
.header .version {
color: #8b949e;
font-size: 12px;
}
.header .examples {
margin-left: auto;
display: flex;
gap: 6px;
}
.header select {
background: #21262d;
color: #c9d1d9;
border: 1px solid #30363d;
border-radius: 6px;
padding: 4px 10px;
font-size: 12px;
cursor: pointer;
}
.main {
display: flex;
flex: 1;
min-height: 0;
}
.editor-panel {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.toolbar {
display: flex;
gap: 8px;
padding: 8px 16px;
background: #161b22;
border-bottom: 1px solid #30363d;
flex-shrink: 0;
}
.toolbar button {
padding: 6px 16px;
border: 1px solid #30363d;
background: #21262d;
color: #c9d1d9;
cursor: pointer;
border-radius: 6px;
font-size: 13px;
font-family: inherit;
transition: all 0.15s;
}
.toolbar button:hover {
background: #30363d;
}
.toolbar button.run {
background: #238636;
color: #fff;
border-color: #238636;
font-weight: 600;
}
.toolbar button.run:hover {
background: #2ea043;
}
.toolbar button.run:disabled {
background: #1f4d2e;
border-color: #1f4d2e;
cursor: not-allowed;
}
#editor {
flex: 1;
min-height: 0;
}
.output-panel {
width: 45%;
min-width: 300px;
max-width: 600px;
background: #0d1117;
border-left: 1px solid #30363d;
display: flex;
flex-direction: column;
}
.output-header {
padding: 8px 16px;
background: #161b22;
border-bottom: 1px solid #30363d;
font-size: 12px;
font-weight: 600;
color: #8b949e;
text-transform: uppercase;
letter-spacing: 0.5px;
flex-shrink: 0;
}
#output {
flex: 1;
padding: 12px 16px;
font-family: 'SFMono-Regular', 'Fira Code', 'JetBrains Mono', Consolas, monospace;
font-size: 13px;
line-height: 1.6;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
color: #c9d1d9;
}
#output.error { color: #f85149; }
#output.success { color: #7ee787; }
#output.running { color: #8b949e; }
.status-bar {
padding: 4px 16px;
background: #161b22;
border-top: 1px solid #30363d;
font-size: 11px;
color: #8b949e;
flex-shrink: 0;
display: flex;
justify-content: space-between;
}
.spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid #30363d;
border-top-color: #58a6ff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-right: 6px;
vertical-align: middle;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="header">
<span class="logo">Bux Playground</span>
<span class="version">Try the Bux programming language in your browser</span>
<div class="examples">
<select id="exampleSelect">
<option value="">Load example...</option>
<option value="hello">Hello World</option>
<option value="factorial">Factorial</option>
<option value="fibonacci">Fibonacci</option>
<option value="structs">Structs & Methods</option>
<option value="generics">Generics</option>
<option value="channels">Channels</option>
<option value="async">Async/Await</option>
<option value="result">Error Handling</option>
</select>
</div>
</div>
<div class="main">
<div class="editor-panel">
<div class="toolbar">
<button class="run" id="runBtn" onclick="runCode()">▶ Run</button>
<button onclick="clearOutput()">Clear Output</button>
<button onclick="formatCode()">Format</button>
</div>
<div id="editor"></div>
</div>
<div class="output-panel">
<div class="output-header">Output</div>
<div id="output"></div>
<div class="status-bar">
<span>Ctrl+Enter to run</span>
<span id="statusText">Ready</span>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script>
<script>
const examples = {
hello: `import Std::Io::PrintLine;
func Main() -> int {
PrintLine("Hello, Bux!");
return 0;
}`,
factorial: `import Std::Io::{PrintLine, PrintInt};
import Std::Fmt::Fmt_Fmt2;
func Factorial(n: int) -> int {
if n <= 1 {
return 1;
}
return n * Factorial(n - 1);
}
func Main() -> int {
PrintLine("Factorials:");
var i: int = 1;
while i <= 10 {
let fact: int = Factorial(i);
PrintLine(Fmt_Fmt2("{0}! = {1}", String_FromInt(i), String_FromInt(fact)));
i = i + 1;
}
return 0;
}`,
fibonacci: `import Std::Io::{PrintLine, PrintInt};
func Fibonacci(n: int) -> int {
if n <= 1 {
return n;
}
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
func Main() -> int {
PrintLine("Fibonacci sequence:");
var i: int = 0;
while i < 15 {
PrintInt(Fibonacci(i));
PrintLine("");
i = i + 1;
}
return 0;
}`,
structs: `import Std::Io::{PrintLine, PrintInt};
struct Rectangle {
width: int;
height: int;
}
extend Rectangle {
func Area(self: Rectangle) -> int {
return self.width * self.height;
}
func Perimeter(self: Rectangle) -> int {
return 2 * (self.width + self.height);
}
}
func Main() -> int {
let rect: Rectangle = Rectangle { width: 10, height: 5 };
PrintLine("Rectangle:");
PrintLine(Fmt_Fmt2(" Area = {0}", String_FromInt(rect.Area())));
PrintLine(Fmt_Fmt2(" Perimeter = {0}", String_FromInt(rect.Perimeter())));
return 0;
}`,
generics: `import Std::Io::{PrintLine, PrintInt};
func Max<T>(a: T, b: T) -> T {
if a > b {
return a;
}
return b;
}
func Main() -> int {
let m1: int = Max<int>(10, 20);
PrintLine("Max of 10 and 20:");
PrintInt(m1);
PrintLine("");
return 0;
}`,
channels: `import Std::Io::{PrintLine, PrintInt};
import Std::Channel::{Channel, Channel_New, Channel_SendInt, Channel_RecvInt, Channel_Close};
func Producer(chPtr: *Channel<int>) {
var i: int = 1;
while i <= 5 {
Channel_SendInt(chPtr, i * 10);
i = i + 1;
}
Channel_Close<int>(chPtr);
}
func Consumer(chPtr: *Channel<int>) {
var total: int = 0;
while true {
let val: int = Channel_RecvInt(chPtr);
if val == 0 { break; }
total = total + val;
PrintInt(val);
PrintLine("");
}
PrintLine("Total:");
PrintInt(total);
PrintLine("");
}
func Main() -> int {
let ch: Channel<int> = Channel_New<int>(3);
let p: *void = spawn Producer(&ch);
let c: *void = spawn Consumer(&ch);
return 0;
}`,
async: `import Std::Io::{PrintLine, PrintInt};
async func Compute() -> int {
PrintLine("Compute: step 1");
bux_async_yield();
PrintLine("Compute: step 2");
return 42;
}
func Main() -> int {
let h1 = spawn Compute();
let r1: int = h1.await as int;
PrintLine("Result:");
PrintInt(r1);
PrintLine("");
return 0;
}`,
result: `import Std::Io::PrintLine;
enum Result {
Ok(int),
Err(String)
}
func Divide(a: int, b: int) -> Result {
if b == 0 {
return Result_NewErr("division by zero");
}
return Result_NewOk(a / b);
}
func Compute() -> Result {
let x: int = Divide(10, 2)?;
let y: int = Divide(x, 5)?;
return Result_NewOk(y);
}
func Main() -> int {
let result: Result = Compute();
match result {
case Ok(val) => PrintLine("Success!");
case Err(msg) => PrintLine("Error!");
}
return 0;
}`
};
// Monaco Editor setup
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' } });
let editor;
require(['vs/editor/editor.main'], function () {
// Simple Bux language definition
monaco.languages.register({ id: 'bux' });
monaco.languages.setMonarchTokensProvider('bux', {
keywords: [
'func', 'let', 'var', 'const', 'type', 'struct', 'enum', 'union',
'interface', 'extend', 'module', 'import', 'pub', 'extern',
'if', 'else', 'while', 'do', 'loop', 'for', 'in', 'break',
'continue', 'return', 'match', 'as', 'is', 'null', 'self',
'super', 'sizeof', 'async', 'await', 'spawn', 'defer',
'switch', 'case', 'default', 'checked'
],
operators: [
'=', '>', '<', '!', '~', '?', ':', '==', '<=', '>=', '!=',
'&&', '||', '++', '--', '+', '-', '*', '/', '&', '|', '^',
'%', '<<', '>>', '>>>', '+=', '-=', '*=', '/=', '&=', '|=',
'^=', '%=', '<<=', '>>=', '>>>='
],
symbols: /[=><!~?:&|+\-*\/\^%]+/,
tokenizer: {
root: [
[/[a-zA-Z_]\w*/, {
cases: {
'@keywords': 'keyword',
'@default': 'identifier'
}
}],
[/\d+/, 'number'],
[/"[^"]*"/, 'string'],
[/`[^`]*`/, 'string'],
[/[{}()\[\]]/, '@brackets'],
[/@symbols/, {
cases: {
'@operators': 'operator',
'@default': ''
}
}],
[/\/\/.*$/, 'comment'],
[/\/\*/, 'comment', '@comment'],
],
comment: [
[/[^\/*]+/, 'comment'],
[/\*\//, 'comment', '@pop'],
[/[\/*]/, 'comment']
]
}
});
editor = monaco.editor.create(document.getElementById('editor'), {
value: examples.hello,
language: 'bux',
theme: 'vs-dark',
fontSize: 14,
fontFamily: "'Fira Code', 'JetBrains Mono', monospace",
minimap: { enabled: false },
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 4,
insertSpaces: false,
lineNumbers: 'on',
renderWhitespace: 'selection',
folding: true,
wordWrap: 'on'
});
// Ctrl+Enter shortcut
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, runCode);
});
// Example selector
document.getElementById('exampleSelect').addEventListener('change', function(e) {
const key = e.target.value;
if (key && examples[key] && editor) {
editor.setValue(examples[key]);
}
e.target.value = '';
});
const outputDiv = document.getElementById('output');
const runBtn = document.getElementById('runBtn');
const statusText = document.getElementById('statusText');
async function runCode() {
if (!editor) return;
const code = editor.getValue();
outputDiv.textContent = '';
outputDiv.className = 'output running';
runBtn.disabled = true;
statusText.innerHTML = '<span class="spinner"></span>Compiling...';
try {
const resp = await fetch('/compile', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: code
});
const data = await resp.json();
let output = data.output || '';
// Filter verbose build lines
let lines = output.split('\n');
let filtered = [];
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
if (line.includes('[Package]') || line.includes('[Build]') ||
line.includes('Hint:') || line.includes('Bux compiler') ||
line.startsWith('Compiling') || line.startsWith('Linking') ||
line.startsWith('Build successful')) continue;
filtered.push(line);
}
output = filtered.join('\n').trim() || '(no output)';
outputDiv.textContent = output;
outputDiv.className = 'output' + (data.isError ? ' error' : ' success');
statusText.textContent = data.isError ? 'Error' : 'Done';
} catch (e) {
outputDiv.textContent = 'Error: ' + e.message;
outputDiv.className = 'output error';
statusText.textContent = 'Error';
} finally {
runBtn.disabled = false;
}
}
function clearOutput() {
outputDiv.textContent = '';
outputDiv.className = 'output';
statusText.textContent = 'Ready';
}
function formatCode() {
if (!editor) return;
// Simple auto-indent (Monaco formatter would be better)
const code = editor.getValue();
editor.setValue(code);
statusText.textContent = 'Formatted';
}
</script>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
# Bux Playground Sandbox
# Minimal container with Bux compiler + C toolchain
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
clang \
libc6-dev \
libssl-dev \
make \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Create sandbox user (no root)
RUN useradd -m -s /bin/bash sandbox
# Copy Bux compiler binary
COPY buxc2 /usr/local/bin/buxc2
RUN chmod +x /usr/local/bin/buxc2
# Copy stdlib and runtime
COPY lib /usr/local/share/bux/lib
COPY rt /usr/local/share/bux/rt
# Set up working directory
WORKDIR /tmp/bux-run
RUN chown sandbox:sandbox /tmp/bux-run
# Security: drop privileges, limit resources
USER sandbox
# Default command (backend will override)
CMD ["/bin/bash"]