Skip to content

Investigate C++/Python support #13

Description

@mathewdunne

I'll dig into how the current Java-only pipeline is wired before assessing what multi-language would take.

Investigated the run path, container build, and control-plane image handling. Here's the assessment.

Verdict: yes, and the architecture is already most of the way there

The control plane talks to workspaces through a language-neutral contract: start-sim.sh / stop-sim.sh / a log file at $HOME/sim.log, plus HALSim-WS on 3300 and NT4 on 5810. Nothing above the container knows about Java. Confirmed:

  • [runs.ts:86-95](apps/control/src/runs.ts:86) shells out to start-sim.sh, tails sim.log, and greps for the literal string BUILD FAILED. No Gradle, no Java.
  • Readiness detection ([runs.ts:79](apps/control/src/runs.ts:79)) matches on nt4|networktables|listening…5810 — WPILib prints that in all three languages.
  • DriverStation, AdvantageScope, gamepad, telemetry all ride HALSim-WS/NT4, which are WPILib-level protocols identical across Java/C++/Python.

So the port is real work, but it's concentrated in two places: the images and one hardcoded global image name.

What has to change

1. The image name is a singleton — this is the core refactor.
config.codeImage ([config.ts:333](apps/control/src/config.ts:333)) is one string, referenced in ~13 spots in [local-docker-runtime-provider.ts](apps/control/src/containers/local-docker-runtime-provider.ts) (create, inspect, reconcile, preflight, rebuild). It becomes a map keyed by language, and the workspace row needs a language column alongside the existing current_module_kind ([storage.ts:25](apps/control/src/storage.ts:25)).

The nasty part: a container's language is fixed at creation. Switching language = destroy and recreate the container, not just swap project files. Today loadModule copies files into a live container. Cross-language switching has to route through the lifecycle path instead, and the container labels ([metadata.ts:93](apps/control/src/containers/metadata.ts:93)) need a frc-sim.language label so reconciliation doesn't adopt a Python container for a Java workspace.

2. Three Dockerfiles, one shared base. [containers/code/Dockerfile](containers/code/Dockerfile) is ~200 lines, of which the VSCodium base, webview-commit patch, gh, s6 services, and catalog copy are language-independent. Split it: a coderunner-workspace-base image, then thin Java / C++ / Python layers that differ only in toolchain, extensions, and cache priming.

3. Extension licensing is a real constraint, not a footnote. This repo already builds Spotless from source rather than pull a Marketplace VSIX, because VSCodium can't use the MS Marketplace. That same rule bites hard here:

  • C++: ms-vscode.cpptools is proprietary and Marketplace-only. Use llvm-vs-code-extensions.vscode-clangd (Apache-2.0, on Open VSX) instead.
  • Python: Pylance is proprietary and unavailable. Use the Open VSX Python extension with Jedi, or basedpyright + ruff.

Neither is a blocker, but IntelliSense quality will differ from what students see in real WPILib VS Code. wpilibsuite.vscode-wpilib is already multi-language, so that one carries over free.

4. Run scripts diverge — and Python diverges the most.

  • C++: closest to Java. run-sim.sh uses simulateExternalJavaRelease and reads build/sim/release_java.json ([run-sim.sh:44](containers/code/run-sim.sh:44)). GradleRIO has a C++ equivalent, but the exact task name and descriptor path need verification before you commit to the two-phase design of decision 025. Rather than exec java -jar, you exec the built native binary with LD_LIBRARY_PATH set.
  • Python: no Gradle at all. start-sim.sh's hard gate on build.gradle and gradlew ([start-sim.sh:23](containers/code/start-sim.sh:23)) doesn't apply; there's no build phase, so the whole two-phase build/exec split collapses to one step. The Gradle init script ([sim-headless.init.gradle](containers/code/sim-headless.init.gradle)) — which strips halsim_gui/halsim_ds_socket and enables halsim_ws_server — has no analogue; robotpy selects extensions by CLI flag. And BUILD FAILED isn't a string Python emits, so each variant must synthesize that sentinel on failure to keep the control-plane contract intact.
  • stop-sim.sh pattern-matches on GradleWrapperMain…simulate and build/libs/*.jar ([stop-sim.sh:97](containers/code/stop-sim.sh:97)). Needs per-language patterns.

5. Catalog and contracts. lessonModuleKindSchema is ["plain-java", "robot"] ([index.ts:42](packages/contracts/src/index.ts:42)) — that enum conflates language and shape. Split it into orthogonal language × kind fields. modules.json gains a language field, and the team-import path's build.gradle gate ([imports.ts:263](apps/control/src/imports.ts:263)) becomes language-aware detection (build.gradle → Java or C++ by source tree; pyproject.toml/robot.py → Python).

6. Content is the largest single cost. Every lesson triples. Note that robot-starter is AdvantageKit-based — AdvantageKit is Java-only, so the C++ and Python starters need a different telemetry approach (plain WPILib logging still feeds AdvantageScope fine, but the lesson content isn't a mechanical translation).

7. CI/ops multiplies. [release.yml](.github/workflows/release.yml) builds each image on native amd64 + arm64 runners and merges by digest. Three workspace images = six build jobs. [scripts/image.ts](scripts/image.ts) hardcodes workspace/control and needs a kind per language. Host disk for pulls roughly triples, though per-student memory budget is unchanged since a student runs one container.

Recommended end-user product

Language is a property of the project, chosen at project-switch time — never a separate setting the student manages.

The topbar Switch Project surface already exists and is the natural home. Add a language filter or grouping to the lesson catalog; each catalog module declares its language. When a student picks a Python lesson while sitting in a Java container, the control plane transparently tears down and recreates the workspace container from the Python image and loads the lesson. The student sees the existing progress UI with one extra stage ("Preparing Python workspace…") — a few extra seconds, no new concepts. Team import sniffs the repo and picks the image the same way.

Everything downstream stays identical: same editor chrome, same Run button, same DriverStation, same AdvantageScope. That's the whole point — the student learns FRC programming, and the language is a detail of the project they opened. Concretely, from the student's view the only visible changes are a language badge on catalog entries and a slightly longer switch when crossing a language boundary.

Before committing, verify

  1. The GradleRIO C++ external-sim task name and descriptor path — decision 025's two-phase design depends on a non-JavaExec task existing for C++.
  2. Whether robotpy's sim entrypoint can be made to hold a stable PID for the pid_file lifecycle contract.
  3. Image size after three toolchains — pull time and host disk are the practical ceiling, not memory.

Want me to write this up as docs/decisions/038-multi-language-support.md, or publish it as a shareable page?

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions