From 4d668f439e397b8f193ec595a173adfd8f0f3ff0 Mon Sep 17 00:00:00 2001 From: Hardy Jonck Date: Tue, 23 Jun 2026 02:21:53 +0200 Subject: [PATCH] =?UTF-8?q?feat(crates):=20mxbuild=20build=20crates=20(7?= =?UTF-8?q?=E2=80=9311)=20+=20Mendix=208=20runtime/build=20crates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a build crate under crates/mendix-/build/ for every Mendix major (7/9/10/11): a version-pinned mxbuild + mx toolchain image that compiles an .mpr to an .mda and runs `mx check`, recipe-pulling mxbuild-.tar.gz from cdn.mendix.com at build (no committed binary; guard.sh-compliant). The mxbuild invocation mirrors the production AIDE aide-mxtools entrypoint (--java-home / --java-exe-path / --gradle-home / --loose-version-check), retargeted at the baked /opt/mxtools/modeler. Adds Mendix 8 coverage (runtime + build). The earlier scope assumed MX8 was portal-only, but the final 8.18 LTS patch (8.18.35.97) is CDN-hosted (verified HTTP 200), so MX8 ships as a first-class recipe-pull crate. PORTAL-DOWNLOAD.md documents the portal fallback + agent download-instructions for the older MX8 patches the public CDN no longer serves. Verification (2026-06-23): mendix-11/build is image-verified — it builds clean (amd64, ~650 MB), the CDN toolchain pull succeeds, mxbuild/mx/gradle resolve at /opt/mxtools/modeler, and the entrypoint runs (version/unknown-command). The other crates are authored to the same proven pattern with their CDN source URLs verified reachable; the full .mpr -> .mda compile smoke is recorded honestly as pending in each provenance.yaml (no committed false verification). - docs/building-mendix-apps-in-docker.md: consolidated build guide - README: split runtime/build version tables, MX8 row, updated repo layout Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 67 +++- crates/mendix-10/build/CHANGELOG.md | 29 ++ crates/mendix-10/build/Dockerfile | 113 ++++++ crates/mendix-10/build/README.md | 102 ++++++ crates/mendix-10/build/build.sh | 134 +++++++ crates/mendix-10/build/provenance.yaml | 31 ++ crates/mendix-10/build/tests/smoke-test.sh | 60 ++++ crates/mendix-10/build/versions.yaml | 18 + crates/mendix-11/build/CHANGELOG.md | 35 ++ crates/mendix-11/build/Dockerfile | 113 ++++++ crates/mendix-11/build/README.md | 102 ++++++ crates/mendix-11/build/build.sh | 134 +++++++ crates/mendix-11/build/provenance.yaml | 38 ++ crates/mendix-11/build/tests/smoke-test.sh | 60 ++++ crates/mendix-11/build/versions.yaml | 19 + crates/mendix-7/build/CHANGELOG.md | 29 ++ crates/mendix-7/build/Dockerfile | 113 ++++++ crates/mendix-7/build/README.md | 102 ++++++ crates/mendix-7/build/build.sh | 134 +++++++ crates/mendix-7/build/provenance.yaml | 31 ++ crates/mendix-7/build/tests/smoke-test.sh | 60 ++++ crates/mendix-7/build/versions.yaml | 18 + crates/mendix-8/CHANGELOG.md | 35 ++ crates/mendix-8/Dockerfile | 123 +++++++ crates/mendix-8/PORTAL-DOWNLOAD.md | 103 ++++++ crates/mendix-8/README.md | 166 +++++++++ crates/mendix-8/build/CHANGELOG.md | 29 ++ crates/mendix-8/build/Dockerfile | 113 ++++++ crates/mendix-8/build/README.md | 106 ++++++ crates/mendix-8/build/build.sh | 134 +++++++ crates/mendix-8/build/provenance.yaml | 31 ++ crates/mendix-8/build/tests/smoke-test.sh | 60 ++++ crates/mendix-8/build/versions.yaml | 18 + crates/mendix-8/provenance.yaml | 30 ++ crates/mendix-8/start.sh | 333 ++++++++++++++++++ .../mendix-8/tests/docker-compose.smoke.yml | 48 +++ crates/mendix-8/tests/smoke-test.sh | 58 +++ crates/mendix-8/versions.yaml | 27 ++ crates/mendix-9/build/CHANGELOG.md | 29 ++ crates/mendix-9/build/Dockerfile | 113 ++++++ crates/mendix-9/build/README.md | 102 ++++++ crates/mendix-9/build/build.sh | 134 +++++++ crates/mendix-9/build/provenance.yaml | 31 ++ crates/mendix-9/build/tests/smoke-test.sh | 60 ++++ crates/mendix-9/build/versions.yaml | 18 + docs/building-mendix-apps-in-docker.md | 122 +++++++ 46 files changed, 3550 insertions(+), 15 deletions(-) create mode 100644 crates/mendix-10/build/CHANGELOG.md create mode 100644 crates/mendix-10/build/Dockerfile create mode 100644 crates/mendix-10/build/README.md create mode 100644 crates/mendix-10/build/build.sh create mode 100644 crates/mendix-10/build/provenance.yaml create mode 100644 crates/mendix-10/build/tests/smoke-test.sh create mode 100644 crates/mendix-10/build/versions.yaml create mode 100644 crates/mendix-11/build/CHANGELOG.md create mode 100644 crates/mendix-11/build/Dockerfile create mode 100644 crates/mendix-11/build/README.md create mode 100644 crates/mendix-11/build/build.sh create mode 100644 crates/mendix-11/build/provenance.yaml create mode 100644 crates/mendix-11/build/tests/smoke-test.sh create mode 100644 crates/mendix-11/build/versions.yaml create mode 100644 crates/mendix-7/build/CHANGELOG.md create mode 100644 crates/mendix-7/build/Dockerfile create mode 100644 crates/mendix-7/build/README.md create mode 100644 crates/mendix-7/build/build.sh create mode 100644 crates/mendix-7/build/provenance.yaml create mode 100644 crates/mendix-7/build/tests/smoke-test.sh create mode 100644 crates/mendix-7/build/versions.yaml create mode 100644 crates/mendix-8/CHANGELOG.md create mode 100644 crates/mendix-8/Dockerfile create mode 100644 crates/mendix-8/PORTAL-DOWNLOAD.md create mode 100644 crates/mendix-8/README.md create mode 100644 crates/mendix-8/build/CHANGELOG.md create mode 100644 crates/mendix-8/build/Dockerfile create mode 100644 crates/mendix-8/build/README.md create mode 100644 crates/mendix-8/build/build.sh create mode 100644 crates/mendix-8/build/provenance.yaml create mode 100644 crates/mendix-8/build/tests/smoke-test.sh create mode 100644 crates/mendix-8/build/versions.yaml create mode 100644 crates/mendix-8/provenance.yaml create mode 100644 crates/mendix-8/start.sh create mode 100644 crates/mendix-8/tests/docker-compose.smoke.yml create mode 100755 crates/mendix-8/tests/smoke-test.sh create mode 100644 crates/mendix-8/versions.yaml create mode 100644 crates/mendix-9/build/CHANGELOG.md create mode 100644 crates/mendix-9/build/Dockerfile create mode 100644 crates/mendix-9/build/README.md create mode 100644 crates/mendix-9/build/build.sh create mode 100644 crates/mendix-9/build/provenance.yaml create mode 100644 crates/mendix-9/build/tests/smoke-test.sh create mode 100644 crates/mendix-9/build/versions.yaml create mode 100644 docs/building-mendix-apps-in-docker.md diff --git a/README.md b/README.md index 8acffad..ed41f6a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # Mendix Runtime Crates -> Model-agnostic, app-agnostic, version-pinned Docker recipes for running **any** -> Mendix application — Mendix 7 through 11 — without baking the model into the image. -> Bind-mount your unzipped MDA, set a few env vars, get a working Mendix runtime. +> Model-agnostic, app-agnostic, version-pinned Docker recipes for **running** and +> **building** any Mendix application — Mendix 7 through 11 — without baking the +> model into the image. Bind-mount your unzipped MDA, set a few env vars, get a +> working Mendix runtime; or bind-mount a project and compile it to an `.mda`, +> with no Studio Pro on the host. [![no-mendix-binaries](https://github.com/ontologylabs/mendix-runtime-crates/actions/workflows/no-mendix-binaries.yml/badge.svg)](https://github.com/ontologylabs/mendix-runtime-crates/actions/workflows/no-mendix-binaries.yml) [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) @@ -19,11 +21,14 @@ zero dangling layers. **[Jump to Quick start ↓](#quick-start)** ## What this is -A set of **recipes** — `Dockerfile`, `start.sh`, and supporting scaffolding — that -build a clean, reusable Docker image for each Mendix major version. They contain -**no Mendix binaries**: the Mendix runtime tarball is downloaded from the official -Mendix CDN (`cdn.mendix.com`) *at build time, on your machine*. You bring the -licensed runtime; we bring the build recipe. +A set of **recipes** — `Dockerfile`, `start.sh` / `build.sh`, and supporting +scaffolding — that build a clean, reusable Docker image for each Mendix major +version, in two flavours: a **runtime crate** (`crates/mendix-/`) that runs an +app, and a **build crate** (`crates/mendix-/build/`) that compiles a project to +an `.mda` and runs `mx check`. They contain **no Mendix binaries**: the Mendix +runtime/toolchain tarball is downloaded from the official Mendix CDN +(`cdn.mendix.com`) *at build time, on your machine*. You bring the licensed +runtime; we bring the build recipe. ## The problem it solves @@ -50,17 +55,44 @@ multiple bind-mounts. ## Supported versions +### Runtime crates — *run* an app + | Crate | Verified Mendix version | JRE | Base image | Status | |---|---|---|---|---| | [`crates/mendix-11`](crates/mendix-11) | `11.6.4` | Java 21 | `eclipse-temurin:21-jre-jammy` | verified | | [`crates/mendix-10`](crates/mendix-10) | `10.24.13.86719` (LTS) | Java 21 | `eclipse-temurin:21-jre-jammy` | verified | | [`crates/mendix-9`](crates/mendix-9) | `9.24.20.33307` | Java 11 | `eclipse-temurin:11-jre-jammy` | verified | +| [`crates/mendix-8`](crates/mendix-8) | `8.18.35.97` (final LTS) | Java 11 | `eclipse-temurin:11-jre-jammy` | recipe (CDN 200) | | [`crates/mendix-7`](crates/mendix-7) | `7.23.8.58888` | Java 8 | `eclipse-temurin:8-jre-jammy` | verified | -Each crate's `versions.yaml` lists the exact versions it has been built and -smoke-tested against, plus planned versions. The boot path -(`runtimelauncher.jar` + the m2ee admin protocol) is identical from Mendix 7 -through 11. +### Build crates — *compile* an `.mpr` → `.mda` + +Each version also ships a **build crate** under `crates/mendix-/build/`: a +version-pinned `mxbuild` + `mx` toolchain image that compiles a project and runs +`mx check`, with nothing on the host but Docker (no Studio Pro). The toolchain is +pulled from the same CDN as `mxbuild-.tar.gz`. See +**[Building MDAs in Docker](docs/building-mendix-apps-in-docker.md)**. + +| Build crate | mxbuild version | JDK | Base image | Status | +|---|---|---|---|---| +| [`crates/mendix-11/build`](crates/mendix-11/build) | `11.6.4` | Java 21 | `eclipse-temurin:21-jdk-jammy` | image-verified¹ | +| [`crates/mendix-10/build`](crates/mendix-10/build) | `10.24.13.86719` | Java 21 | `eclipse-temurin:21-jdk-jammy` | recipe (CDN 200) | +| [`crates/mendix-9/build`](crates/mendix-9/build) | `9.24.20.33307` | Java 11 | `eclipse-temurin:11-jdk-jammy` | recipe (CDN 200) | +| [`crates/mendix-8/build`](crates/mendix-8/build) | `8.18.35.97` | Java 11 | `eclipse-temurin:11-jdk-jammy` | recipe (CDN 200) | +| [`crates/mendix-7/build`](crates/mendix-7/build) | `7.23.8.58888` | Java 8 | `eclipse-temurin:8-jdk-jammy` | recipe (CDN 200) | + +¹ *image-verified* = the image builds, the CDN toolchain pull succeeds, and the +binaries + entrypoint resolve and run; a full `.mpr → .mda` compile against a +licensed project is the remaining smoke gate (per crate `provenance.yaml`). +*recipe (CDN 200)* = authored to the same proven pattern with the CDN source +URL verified reachable, image build pending. + +Each crate's `versions.yaml` lists the exact versions it targets. The runtime +boot path (`runtimelauncher.jar` + the m2ee admin protocol) and the build +invocation are identical from Mendix 7 through 11. **Mendix 8** is out of +standard support but its final LTS patch (`8.18.35.97`) is still CDN-hosted; +older MX8 patches that aren't are covered by +[`crates/mendix-8/PORTAL-DOWNLOAD.md`](crates/mendix-8/PORTAL-DOWNLOAD.md). ## Quick start @@ -118,11 +150,16 @@ mendix-runtime-crates/ ├── LICENSE # Apache-2.0 ├── guard.sh # the no-Mendix-binary CI check ├── .github/workflows/no-mendix-binaries.yml # runs guard.sh on push + PR +├── docs/ +│ ├── running-mendix--in-docker.md # per-version run guides +│ └── building-mendix-apps-in-docker.md # compile an .mpr → .mda └── crates/ ├── mendix-7/ { Dockerfile, start.sh, versions.yaml, provenance.yaml, README.md, CHANGELOG.md, tests/ } - ├── mendix-9/ - ├── mendix-10/ - └── mendix-11/ + │ └── build/ { Dockerfile, build.sh, versions.yaml, provenance.yaml, README.md, CHANGELOG.md, tests/ } + ├── mendix-8/ { …runtime… , PORTAL-DOWNLOAD.md, build/ } # final 8.18 LTS; portal fallback for older patches + ├── mendix-9/ { …runtime… , build/ } + ├── mendix-10/ { …runtime… , build/ } + └── mendix-11/ { …runtime… , build/ } ``` ## Contributing a new version diff --git a/crates/mendix-10/build/CHANGELOG.md b/crates/mendix-10/build/CHANGELOG.md new file mode 100644 index 0000000..0d72a16 --- /dev/null +++ b/crates/mendix-10/build/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog — Mendix 10 Build Crate + +## [0.1.0] — 2026-06-23 + +Initial release. The build (mxbuild + mx) companion to the mendix-10 runtime crate. + +* `Dockerfile`: model-agnostic; JDK 21 base (`eclipse-temurin:21-jdk-jammy`); + downloads the Mendix 10 build toolchain from the official CDN at build + (`mxbuild-${MENDIX_VERSION}.tar.gz`); never commits a Mendix binary + (recipe-pull, D-DOCKER-LIB-001/002). apt deps for the .NET/Mono toolchain: + `libgdiplus libicu70 libssl3 sqlite3`. +* `build.sh`: `build` / `check` / `version` dispatch. Auto-injects + `--java-home` / `--java-exe-path` / `--gradle-home` / `--loose-version-check` + and defaults `--output=/workspace/.mda`. `mx check` exit codes normalised + to the CI convention (0 clean / 1 errors / 2 warnings). Derived from the + production AIDE aide-mxtools entrypoint. +* Pre-creates `$HOME/.local/share/Mendix` for the MX 10 `.NET` mxbuild whitelist + FailFast (same fix as aide-mxtools). +* Default Mendix version: 10.24.13.86719 (override via `--build-arg MENDIX_VERSION`). + +### Verification status +* CDN source `mxbuild-10.24.13.86719.tar.gz`: **HTTP 200 verified (2026-06-23)**. +* Image build + `mx version` / MDA smoke: **pending** — recorded honestly in + `provenance.yaml` (`smoke_verified: pending`). The mxbuild invocation itself is + the production-proven aide-mxtools path; the remaining gate is a release build. + +### Why +Lets a Docker-only environment (CI, a contributor without Studio Pro) compile and +validate a Mendix 10 app, producing the `.mda` the runtime crate runs. diff --git a/crates/mendix-10/build/Dockerfile b/crates/mendix-10/build/Dockerfile new file mode 100644 index 0000000..00c049d --- /dev/null +++ b/crates/mendix-10/build/Dockerfile @@ -0,0 +1,113 @@ +# Mendix 10 Build Crate — model-agnostic mxbuild + mx toolchain (recipe-pull). +# +# Image: ontologylabs/mendix-mxbuild:10 +# ontologylabs/mendix-mxbuild:10.24.13.86719 (immutable) +# +# Contract: +# * Bake: JDK 21 + the Mendix 10.x build toolchain (mxbuild + mx), pulled from +# the official Mendix CDN at `docker build` time. +# * NEVER commit a Mendix binary to the repo. The toolchain tarball is curl'd +# from cdn.mendix.com on the licensed user's own machine — exactly like the +# runtime crate downloads the runtime tarball (D-DOCKER-LIB-001 recipe-pull; +# D-DOCKER-LIB-002 no-binaries guard). You bring the licensed toolchain; we +# bring the build recipe. +# * Bind-mount the project directory (containing the `.mpr`) at /workspace. +# * Compile : `docker run -v :/workspace ... build /workspace/App.mpr` +# → writes App.mda next to the .mpr in /workspace. +# * Validate: `docker run -v :/workspace ... check /workspace/App.mpr` +# → `mx check` with CI-normalised exit codes (0 clean / 1 error / 2 warn). +# +# Why a "build crate" (companion to the runtime crate): +# The AIDE pipeline drives mxbuild via a bind-mounted Studio Pro modeler +# (the aide-mxtools image). This crate makes the toolchain self-contained and +# version-pinned: one image per Mendix major, the matching mxbuild fetched +# from the CDN at build, so a contributor with only Docker can compile any app +# at that version without a local Studio Pro install. Runtime crate runs the +# app; build crate produces the .mda the runtime crate runs. +# +# Provenance: +# * mxbuild + mx from https://cdn.mendix.com/runtime/mxbuild-${MENDIX_VERSION}.tar.gz +# * Tarball top-level is `modeler/` → modeler/mxbuild, modeler/mx, +# modeler/tools/gradle (verified against the AIDE build-server cache layout). +# * Java: Eclipse Temurin 21 JDK — Mendix 10 compiles Java actions at release 21. +# A full JDK (not JRE) is required: mxbuild invokes javac to compile the +# app's javasource and Java actions. + +FROM --platform=linux/amd64 eclipse-temurin:21-jdk-jammy + +ARG MENDIX_VERSION=10.24.13.86719 +ARG MXBUILD_CDN_BASE=https://cdn.mendix.com/runtime + +LABEL ai.ayios.purpose="Mendix 10 build crate (mxbuild + mx, model-agnostic)" +LABEL ai.ayios.crate="mendix-10/build" +LABEL ai.ayios.mendix-version="${MENDIX_VERSION}" +LABEL maintainer="hardy@agileworks.co.za" + +# Layer 1 — apt deps for the mxbuild toolchain. mxbuild is a .NET/Mono program: +# libgdiplus — System.Drawing (image/PDF generation during build) +# libicu70 — globalization/ICU (jammy ships ICU 70) +# libssl3 — TLS for any build-time fetch +# sqlite3 — read the .mpr (modern Mendix projects are SQLite databases) +# curl — pull the toolchain tarball +# Stable layer; survives churn in the heavier toolchain layer below. +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + libgdiplus \ + libicu70 \ + libssl3 \ + sqlite3 \ + && rm -rf /var/lib/apt/lists/* + +# Layer 2 — Mendix build toolchain tarball (mxbuild + mx). ~300-500 MB download. +# Recipe-pull: curl'd from the CDN at build, never committed. Cached unless +# MENDIX_VERSION or MXBUILD_CDN_BASE changes. +# +# Tarball top-level is `modeler/`, so extracting at /opt/mxtools produces the +# same layout the AIDE build-server caches and mounts at /opt/mxtools: +# /opt/mxtools/modeler/mxbuild +# /opt/mxtools/modeler/mx +# /opt/mxtools/modeler/tools/gradle +RUN set -eux; \ + mkdir -p /opt/mxtools /workspace; \ + echo "Downloading Mendix build toolchain ${MENDIX_VERSION} from ${MXBUILD_CDN_BASE}..."; \ + curl -fsSL "${MXBUILD_CDN_BASE}/mxbuild-${MENDIX_VERSION}.tar.gz" -o /tmp/mxbuild.tar.gz; \ + echo "Extracting..."; \ + tar -xzf /tmp/mxbuild.tar.gz -C /opt/mxtools; \ + rm /tmp/mxbuild.tar.gz; \ + test -f /opt/mxtools/modeler/mxbuild || { \ + echo "FATAL: modeler/mxbuild not found at expected path /opt/mxtools/modeler/mxbuild."; \ + echo "Tarball top-level may have changed. Inspecting:"; \ + find /opt/mxtools -maxdepth 3 -type f -name 'mx*'; \ + exit 1; \ + }; \ + chmod +x /opt/mxtools/modeler/mxbuild /opt/mxtools/modeler/mx 2>/dev/null || true; \ + echo "Mendix ${MENDIX_VERSION} build toolchain ready at /opt/mxtools/modeler/" + +# Mendix mxbuild — MX 11's .NET toolchain in particular — creates settings under +# $HOME/.local/share/Mendix via GetFolderPath(LocalApplicationData). In a fresh +# container that directory does not exist, and mxbuild's WhitelistAware +# filesystem FailFast-rejects *creating* it ("UnauthorizedAccessException … +# EnsurePathSafety") before the build starts. Pre-create it for HOME=/root so +# mxbuild writes settings without tripping the create-time whitelist check (a +# harmless no-op on older Mono-based mxbuild). Proven in AIDE aide-mxtools. +RUN mkdir -p /root/.local/share/Mendix /root/.config/Mendix /root/.cache/Mendix + +# Crate entrypoint. +COPY build.sh /opt/mxtools/build.sh +RUN chmod +x /opt/mxtools/build.sh + +# Runs as root by design — an ephemeral, single-shot build container (not a +# long-running service). Root avoids the bind-mount uid-mismatch that would +# otherwise block writing the produced .mda back into the operator's mounted +# /workspace. On a Linux host where root-owned output is undesirable, pass +# `--user $(id -u)`; build.sh defensively re-creates the Mendix settings dir +# under whatever $HOME resolves to. +ENV JAVA_HOME=/opt/java/openjdk \ + MENDIX_VERSION=${MENDIX_VERSION} + +VOLUME ["/workspace"] +WORKDIR /workspace + +ENTRYPOINT ["/opt/mxtools/build.sh"] +CMD ["--help"] diff --git a/crates/mendix-10/build/README.md b/crates/mendix-10/build/README.md new file mode 100644 index 0000000..ceea3f1 --- /dev/null +++ b/crates/mendix-10/build/README.md @@ -0,0 +1,102 @@ +# Mendix 10 Build Crate + +> A model-agnostic, version-pinned Docker image that compiles **any** Mendix 10.x +> project (`.mpr`) into a deployable `.mda` — and runs `mx check` — with nothing +> on the host but Docker. The Mendix build toolchain (`mxbuild` + `mx`) is pulled +> from the official Mendix CDN at build time; no Studio Pro install, no committed +> binaries. + +This is the **build** companion to the [`mendix-10` runtime crate](../). The build +crate *produces* the `.mda`; the runtime crate *runs* it. + +## Why this exists + +The AIDE pipeline compiles models with `mxbuild` driven from a bind-mounted Studio +Pro install. That requires a licensed modeler on the host. This crate makes the +toolchain self-contained and version-pinned: one image per Mendix major, the +matching `mxbuild` fetched from `cdn.mendix.com` at `docker build` — so a CI runner +or a contributor with only Docker can compile a Mendix 10 app without Studio Pro. + +| | Studio Pro on host | aide-mxtools (bind-mount) | This build crate | +|---|---|---|---| +| Needs Studio Pro installed | yes | yes (modeler bind-mounted) | **no** | +| Toolchain source | local install | local install | **CDN at build** | +| Version pinning | manual | manual | **one image per version** | +| Runs in plain CI | no | partial | **yes** | + +## Image tags + +| Tag | Pins | Use | +|---|---|---| +| `ontologylabs/mendix-mxbuild:10.24.13.86719` | Exact version (immutable) | Reproducible CI | +| `ontologylabs/mendix-mxbuild:10` | Latest 10.x in this crate | Dev, demo | + +## Build the image + +```bash +cd crates/mendix-10/build +docker build \ + --platform linux/amd64 \ + --build-arg MENDIX_VERSION=10.24.13.86719 \ + -t ontologylabs/mendix-mxbuild:10.24.13.86719 . +# mxbuild + mx are pulled from cdn.mendix.com/runtime/mxbuild-10.24.13.86719.tar.gz at build. +``` + +## Compile an app + +```bash +# /path/to/project contains App.mpr +docker run --rm \ + --platform linux/amd64 \ + -v /path/to/project:/workspace \ + ontologylabs/mendix-mxbuild:10.24.13.86719 \ + build /workspace/App.mpr +# → writes /path/to/project/App.mda +``` + +Then run it with the runtime crate: + +```bash +unzip /path/to/project/App.mda -d /path/to/project/app +cd ../ # the mendix-10 runtime crate +docker compose -f tests/docker-compose.smoke.yml up # or your own compose +``` + +## Validate a model (`mx check`) + +```bash +docker run --rm -v /path/to/project:/workspace \ + ontologylabs/mendix-mxbuild:10.24.13.86719 \ + check /workspace/App.mpr +# exit 0 = clean · 1 = errors · 2 = warnings only +``` + +## What the entrypoint auto-injects + +`build.sh` supplies the toolchain paths `mxbuild` needs unless you pass them: + +* `--java-home` / `--java-exe-path` → the baked JDK (`$JAVA_HOME`) +* `--gradle-home` → the toolchain's bundled Gradle +* `--loose-version-check` → tolerate patch drift between toolchain and model +* `--output=/workspace/.mda` → default output beside the `.mpr` + +Pass any of these explicitly to override. + +## Notes & gotchas + +* **Runs as root by design.** A single-shot build container, not a service. Root + avoids the bind-mount uid mismatch that would block writing the `.mda` back into + your mounted `/workspace`. On Linux, pass `--user $(id -u)` if you want + host-uid-owned output; the entrypoint re-creates the Mendix settings dir under + the resulting `$HOME`. +* **`.NET` whitelist (MX 11 origin).** mxbuild creates settings under + `$HOME/.local/share/Mendix` and FailFast-rejects *creating* that path itself; the + image pre-creates it. (Same fix as the AIDE aide-mxtools image.) +* **amd64 emulation.** On Apple Silicon, `mxbuild` runs x86_64 under emulation. + Enable Colima Rosetta (`colima start --vm-type vz --vz-rosetta`) for speed; a + hung emulated build is the classic symptom of Rosetta being off. +* **No Mendix binary is committed.** The toolchain is fetched from the CDN at build, + on your licensed machine (D-DOCKER-LIB-002). See repo `guard.sh`. + +See [`provenance.yaml`](provenance.yaml) for the exact CDN source and +[`versions.yaml`](versions.yaml) for verified versions. diff --git a/crates/mendix-10/build/build.sh b/crates/mendix-10/build/build.sh new file mode 100644 index 0000000..c8b9214 --- /dev/null +++ b/crates/mendix-10/build/build.sh @@ -0,0 +1,134 @@ +#!/bin/bash +# Mendix Build Crate — entrypoint. Dispatches to mxbuild / mx from the toolchain +# baked at /opt/mxtools/modeler (pulled from the Mendix CDN at image build). +# +# Commands: +# build [mxbuild-options] Compile an .mpr → .mda +# check [mx-check-options] Run `mx check` (CI-normalised exit) +# version Print the mx toolchain version +# --help Usage +# +# Mount: -v :/workspace (the dir containing your App.mpr) +# Output: build writes .mda into /workspace next to the .mpr. +# +# Derived from the AIDE aide-mxtools entrypoint (production-proven mxbuild +# invocation) — same find-tool + auto-inject(--java-home/--gradle-home/ +# --loose-version-check) logic, retargeted at the baked /opt/mxtools/modeler. + +set -euo pipefail + +MXTOOLS_DIR="/opt/mxtools" + +# Defensive: re-create the Mendix settings dir under whatever HOME resolves to. +# The image bakes these for HOME=/root; this covers a runtime `--user`/`-e HOME=` +# override. MX 11's .NET mxbuild FailFast-rejects creating these; harmless elsewhere. Idempotent. +mkdir -p "${HOME:-/root}/.local/share/Mendix" \ + "${HOME:-/root}/.config/Mendix" \ + "${HOME:-/root}/.cache/Mendix" 2>/dev/null || true + +# find_tool — locate mxbuild/mx in the baked toolchain. Strict candidates +# first (the `modeler/` layout the CDN tarball extracts to), then a glob fallback +# so a future tarball-layout change degrades gracefully instead of failing hard. +find_tool() { + local tool_name="$1" + for candidate in \ + "${MXTOOLS_DIR}/modeler/${tool_name}" \ + "${MXTOOLS_DIR}/${tool_name}" \ + "${MXTOOLS_DIR}/runtime/${tool_name}" \ + "${MXTOOLS_DIR}/tools/${tool_name}"; do + if [ -x "$candidate" ]; then echo "$candidate"; return 0; fi + done + local found + found=$(find "${MXTOOLS_DIR}" -name "${tool_name}" -type f -executable 2>/dev/null | head -1) + [ -n "$found" ] && { echo "$found"; return 0; } + return 1 +} + +case "${1:-}" in + build) + shift + [ $# -ge 1 ] || { echo "Usage: build [mxbuild-options]" >&2; exit 1; } + MXBUILD_BIN=$(find_tool "mxbuild") || { echo "ERROR: mxbuild not found under ${MXTOOLS_DIR}" >&2; exit 1; } + echo "Using: ${MXBUILD_BIN}" >&2 + + # Auto-inject the toolchain paths mxbuild needs, unless the caller set them. + EXTRA_ARGS=() + HAS_JAVA_HOME=false; HAS_GRADLE_HOME=false; HAS_LOOSE=false; HAS_OUTPUT=false + for arg in "$@"; do + case "$arg" in + --java-home=*) HAS_JAVA_HOME=true ;; + --gradle-home=*) HAS_GRADLE_HOME=true ;; + --loose-version-check) HAS_LOOSE=true ;; + --output=*|--output) HAS_OUTPUT=true ;; + esac + done + if [ "$HAS_JAVA_HOME" = false ] && [ -n "${JAVA_HOME:-}" ]; then + EXTRA_ARGS+=(--java-home="${JAVA_HOME}" --java-exe-path="${JAVA_HOME}/bin/java") + fi + if [ "$HAS_GRADLE_HOME" = false ]; then + for g in "${MXTOOLS_DIR}/modeler/tools/gradle" "${MXTOOLS_DIR}/tools/gradle"; do + [ -d "$g" ] && { EXTRA_ARGS+=(--gradle-home="$g"); break; } + done + fi + # Tolerate patch-version drift between the toolchain and the model + # (SDK commits may bump the model's product version). + [ "$HAS_LOOSE" = false ] && EXTRA_ARGS+=(--loose-version-check) + + # Default the output beside the .mpr in /workspace if the caller didn't + # pass --output. mpr-path is the first positional after `build`. + if [ "$HAS_OUTPUT" = false ]; then + MPR_PATH="$1" + MPR_BASE="$(basename "${MPR_PATH%.mpr}")" + EXTRA_ARGS+=(--output="/workspace/${MPR_BASE}.mda") + fi + + exec "$MXBUILD_BIN" "${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}" "$@" + ;; + + check) + shift + [ $# -ge 1 ] || { echo "Usage: check [mx-check-options]" >&2; exit 1; } + MX_BIN=$(find_tool "mx") || { echo "ERROR: mx not found under ${MXTOOLS_DIR}" >&2; exit 1; } + echo "Using: ${MX_BIN}" >&2 + # mx check returns an OR'd bitmask: 1=errors, 2=warnings, 4=deprecations. + # Normalise to the CI convention: 0=clean, 1=errors, 2=warnings-only. + set +e + "$MX_BIN" check "$@" + MX_EXIT=$? + set -e + if [ $((MX_EXIT & 1)) -ne 0 ]; then exit 1 + elif [ $((MX_EXIT & 2)) -ne 0 ]; then exit 2 + else exit 0 + fi + ;; + + version) + # mx has no toolchain --version flag; the pinned version is the image's + # MENDIX_VERSION. (mx show-* verbs report an *app's* version, not the toolchain's.) + echo "Mendix build crate — toolchain version ${MENDIX_VERSION:-unknown} (mxbuild + mx)" + ;; + + --help|help|"") + cat <<'USAGE' +Mendix Build Crate — mxbuild + mx in a version-pinned container + +Commands: + build [options] Compile an .mpr → .mda (output → /workspace) + check [options] Run mx check (exit 0=clean, 1=errors, 2=warnings) + version Show the mx toolchain version + +Mount: + -v :/workspace Directory containing your App.mpr + +Examples: + docker run --rm -v "$PWD":/workspace ontologylabs/mendix-mxbuild:10 build /workspace/App.mpr + docker run --rm -v "$PWD":/workspace ontologylabs/mendix-mxbuild:10 check /workspace/App.mpr +USAGE + ;; + + *) + echo "Unknown command: ${1:-}" >&2 + echo "Run with --help for usage." >&2 + exit 1 + ;; +esac diff --git a/crates/mendix-10/build/provenance.yaml b/crates/mendix-10/build/provenance.yaml new file mode 100644 index 0000000..6028c08 --- /dev/null +++ b/crates/mendix-10/build/provenance.yaml @@ -0,0 +1,31 @@ +# Per-image provenance for the mendix-10 BUILD crate (mxbuild + mx). +# Recipe-pull distribution model (D-DOCKER-LIB-001): no registry, no published +# Mendix binaries — the toolchain tarball is curl'd from the Mendix CDN at build. +# +# Stable provenance anchor: cdn_source_url (anyone can re-fetch + verify the same +# tarball). image_digest is the local image-config ID for a given build; it is +# build-environment-specific (apt package versions drift) and recorded for +# traceability, not byte-for-byte reproducibility. It is populated once the image +# has been built + smoke-verified on a release machine. + +crate: mendix-10/build +crate_version: "0.1.0" + +images: + - mendix_version: "10.24.13.86719" + cdn_source_url: "https://cdn.mendix.com/runtime/mxbuild-10.24.13.86719.tar.gz" + base_image: "eclipse-temurin:21-jdk-jammy" + java_version: 21 + platform: "linux/amd64" + image_tags: + - "ontologylabs/mendix-mxbuild:10.24.13.86719" + - "ontologylabs/mendix-mxbuild:10" + image_digest: null # filled after a verified release build + cdn_verified: "2026-06-23 — HTTP 200 (HEAD)" + smoke_verified: "pending" + notes: > + Recipe authored from the production AIDE aide-mxtools image (proven mxbuild + invocation: --java-home / --java-exe-path / --gradle-home / --loose-version-check) + and the verified runtime-crate recipe-pull pattern. The CDN source URL is + verified reachable; an image build + `mx version`/MDA smoke is the remaining + release gate. diff --git a/crates/mendix-10/build/tests/smoke-test.sh b/crates/mendix-10/build/tests/smoke-test.sh new file mode 100644 index 0000000..93ae74e --- /dev/null +++ b/crates/mendix-10/build/tests/smoke-test.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Smoke test for the Mendix 10 build crate. +# +# Compiles a Mendix 10 project (.mpr) into an .mda using the build-crate image, +# then asserts the .mda was produced. Exits 0 on pass, non-zero on fail. +# +# Usage: ./smoke-test.sh /path/to/project/App.mpr +# +# You supply the licensed .mpr — none is committed to this repo (guard.sh blocks +# .mpr/.mda/.tar.gz patterns; D-DOCKER-LIB-002). The image's mxbuild toolchain is +# pulled from the Mendix CDN at `docker build`. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +BUILD_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +MENDIX_VERSION="${MENDIX_VERSION:-10.24.13.86719}" +IMAGE="${BUILD_IMAGE:-ontologylabs/mendix-mxbuild:${MENDIX_VERSION}}" + +MPR_PATH="${1:-}" +if [ -z "${MPR_PATH}" ] || [ ! -f "${MPR_PATH}" ]; then + echo "[FATAL] Pass the path to a Mendix 10 .mpr as the first argument." + echo " e.g. $0 /path/to/MyApp/MyApp.mpr" + exit 78 +fi + +# Colima mounts only \$HOME into the VM; a project outside \$HOME is invisible to +# the container. Warn rather than silently mount an empty dir. +case "${MPR_PATH}" in + "${HOME}"/*) : ;; + *) echo "[WARN] ${MPR_PATH} is not under \$HOME — on Colima it may be invisible to the container." ;; +esac + +PROJECT_DIR="$(cd "$(dirname "${MPR_PATH}")" && pwd)" +MPR_NAME="$(basename "${MPR_PATH}")" +MDA_NAME="$(basename "${MPR_NAME%.mpr}").mda" + +# Build the image if absent. +if ! docker image inspect "${IMAGE}" >/dev/null 2>&1; then + echo "=== Building image ${IMAGE} (mxbuild pulled from CDN) ===" + docker build --platform linux/amd64 \ + --build-arg MENDIX_VERSION="${MENDIX_VERSION}" \ + -t "${IMAGE}" "${BUILD_DIR}" +fi + +echo "=== Compiling ${MPR_NAME} → ${MDA_NAME} ===" +rm -f "${PROJECT_DIR}/${MDA_NAME}" +docker run --rm --platform linux/amd64 \ + -v "${PROJECT_DIR}:/workspace" \ + "${IMAGE}" \ + build "/workspace/${MPR_NAME}" + +if [ -f "${PROJECT_DIR}/${MDA_NAME}" ]; then + SIZE=$(du -h "${PROJECT_DIR}/${MDA_NAME}" | cut -f1) + echo "=== PASS — ${MDA_NAME} produced (${SIZE}) ===" + exit 0 +fi + +echo "=== FAIL — ${MDA_NAME} was not produced. Inspect mxbuild output above. ===" +exit 1 diff --git a/crates/mendix-10/build/versions.yaml b/crates/mendix-10/build/versions.yaml new file mode 100644 index 0000000..0ee0234 --- /dev/null +++ b/crates/mendix-10/build/versions.yaml @@ -0,0 +1,18 @@ +# Mendix v10 build-toolchain versions verified against this crate. +# When adding a new version: build the image, smoke-test (build a known .mpr + +# mx check), then add the row. The build toolchain (mxbuild + mx) ships on the +# same CDN as the runtime, as mxbuild-.tar.gz. + +crate_version: "0.1.0" +default_mendix_version: "10.24.13.86719" + +supported: + - mendix_version: "10.24.13.86719" + cdn_url: "https://cdn.mendix.com/runtime/mxbuild-10.24.13.86719.tar.gz" + java_version: 21 # JDK (mxbuild compiles javasource at release 21) + cdn_status: "200 (verified 2026-06-23)" + image_smoke: "pending" # image build + `mx version` not yet run in CI + notes: "Pairs with the mendix-10 runtime crate (same default version)." + +planned: + - mendix_version: "10.18.0" diff --git a/crates/mendix-11/build/CHANGELOG.md b/crates/mendix-11/build/CHANGELOG.md new file mode 100644 index 0000000..67ceeb4 --- /dev/null +++ b/crates/mendix-11/build/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog — Mendix 11 Build Crate + +## [0.1.0] — 2026-06-23 + +Initial release. The build (mxbuild + mx) companion to the mendix-11 runtime crate. + +* `Dockerfile`: model-agnostic; JDK 21 base (`eclipse-temurin:21-jdk-jammy`); + downloads the Mendix 11 build toolchain from the official CDN at build + (`mxbuild-${MENDIX_VERSION}.tar.gz`); never commits a Mendix binary + (recipe-pull, D-DOCKER-LIB-001/002). apt deps for the .NET/Mono toolchain: + `libgdiplus libicu70 libssl3 sqlite3`. +* `build.sh`: `build` / `check` / `version` dispatch. Auto-injects + `--java-home` / `--java-exe-path` / `--gradle-home` / `--loose-version-check` + and defaults `--output=/workspace/.mda`. `mx check` exit codes normalised + to the CI convention (0 clean / 1 errors / 2 warnings). Derived from the + production AIDE aide-mxtools entrypoint. +* Pre-creates `$HOME/.local/share/Mendix` for the MX 11 `.NET` mxbuild whitelist + FailFast (same fix as aide-mxtools). +* Default Mendix version: 11.6.4 (override via `--build-arg MENDIX_VERSION`). + +### Verification status (2026-06-23) +* CDN source `mxbuild-11.6.4.tar.gz`: **HTTP 200 verified**. +* **Image build verified**: builds clean (exit 0, ~650 MB, linux/amd64 under + Colima); the recipe-pull curl of the toolchain from the CDN succeeds; + `modeler/{mxbuild,mx,tools/gradle}` resolve at `/opt/mxtools/modeler`; + `build.sh` runs (`version` → `11.6.4`, unknown-command errors); the `mx` + binary executes under emulation. +* **Full MDA compile smoke: pending** — a real `.mpr → .mda` build needs a + licensed MX11 project; recorded honestly in `provenance.yaml` + (`smoke_verified: pending`). The mxbuild invocation is the production-proven + aide-mxtools path. + +### Why +Lets a Docker-only environment (CI, a contributor without Studio Pro) compile and +validate a Mendix 11 app, producing the `.mda` the runtime crate runs. diff --git a/crates/mendix-11/build/Dockerfile b/crates/mendix-11/build/Dockerfile new file mode 100644 index 0000000..445cce0 --- /dev/null +++ b/crates/mendix-11/build/Dockerfile @@ -0,0 +1,113 @@ +# Mendix 11 Build Crate — model-agnostic mxbuild + mx toolchain (recipe-pull). +# +# Image: ontologylabs/mendix-mxbuild:11 +# ontologylabs/mendix-mxbuild:11.6.4 (immutable) +# +# Contract: +# * Bake: JDK 21 + the Mendix 11.x build toolchain (mxbuild + mx), pulled from +# the official Mendix CDN at `docker build` time. +# * NEVER commit a Mendix binary to the repo. The toolchain tarball is curl'd +# from cdn.mendix.com on the licensed user's own machine — exactly like the +# runtime crate downloads the runtime tarball (D-DOCKER-LIB-001 recipe-pull; +# D-DOCKER-LIB-002 no-binaries guard). You bring the licensed toolchain; we +# bring the build recipe. +# * Bind-mount the project directory (containing the `.mpr`) at /workspace. +# * Compile : `docker run -v :/workspace ... build /workspace/App.mpr` +# → writes App.mda next to the .mpr in /workspace. +# * Validate: `docker run -v :/workspace ... check /workspace/App.mpr` +# → `mx check` with CI-normalised exit codes (0 clean / 1 error / 2 warn). +# +# Why a "build crate" (companion to the runtime crate): +# The AIDE pipeline drives mxbuild via a bind-mounted Studio Pro modeler +# (the aide-mxtools image). This crate makes the toolchain self-contained and +# version-pinned: one image per Mendix major, the matching mxbuild fetched +# from the CDN at build, so a contributor with only Docker can compile any app +# at that version without a local Studio Pro install. Runtime crate runs the +# app; build crate produces the .mda the runtime crate runs. +# +# Provenance: +# * mxbuild + mx from https://cdn.mendix.com/runtime/mxbuild-${MENDIX_VERSION}.tar.gz +# * Tarball top-level is `modeler/` → modeler/mxbuild, modeler/mx, +# modeler/tools/gradle (verified against the AIDE build-server cache layout). +# * Java: Eclipse Temurin 21 JDK — Mendix 11 compiles Java actions at release 21. +# A full JDK (not JRE) is required: mxbuild invokes javac to compile the +# app's javasource and Java actions. + +FROM --platform=linux/amd64 eclipse-temurin:21-jdk-jammy + +ARG MENDIX_VERSION=11.6.4 +ARG MXBUILD_CDN_BASE=https://cdn.mendix.com/runtime + +LABEL ai.ayios.purpose="Mendix 11 build crate (mxbuild + mx, model-agnostic)" +LABEL ai.ayios.crate="mendix-11/build" +LABEL ai.ayios.mendix-version="${MENDIX_VERSION}" +LABEL maintainer="hardy@agileworks.co.za" + +# Layer 1 — apt deps for the mxbuild toolchain. mxbuild is a .NET/Mono program: +# libgdiplus — System.Drawing (image/PDF generation during build) +# libicu70 — globalization/ICU (jammy ships ICU 70) +# libssl3 — TLS for any build-time fetch +# sqlite3 — read the .mpr (modern Mendix projects are SQLite databases) +# curl — pull the toolchain tarball +# Stable layer; survives churn in the heavier toolchain layer below. +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + libgdiplus \ + libicu70 \ + libssl3 \ + sqlite3 \ + && rm -rf /var/lib/apt/lists/* + +# Layer 2 — Mendix build toolchain tarball (mxbuild + mx). ~300-500 MB download. +# Recipe-pull: curl'd from the CDN at build, never committed. Cached unless +# MENDIX_VERSION or MXBUILD_CDN_BASE changes. +# +# Tarball top-level is `modeler/`, so extracting at /opt/mxtools produces the +# same layout the AIDE build-server caches and mounts at /opt/mxtools: +# /opt/mxtools/modeler/mxbuild +# /opt/mxtools/modeler/mx +# /opt/mxtools/modeler/tools/gradle +RUN set -eux; \ + mkdir -p /opt/mxtools /workspace; \ + echo "Downloading Mendix build toolchain ${MENDIX_VERSION} from ${MXBUILD_CDN_BASE}..."; \ + curl -fsSL "${MXBUILD_CDN_BASE}/mxbuild-${MENDIX_VERSION}.tar.gz" -o /tmp/mxbuild.tar.gz; \ + echo "Extracting..."; \ + tar -xzf /tmp/mxbuild.tar.gz -C /opt/mxtools; \ + rm /tmp/mxbuild.tar.gz; \ + test -f /opt/mxtools/modeler/mxbuild || { \ + echo "FATAL: modeler/mxbuild not found at expected path /opt/mxtools/modeler/mxbuild."; \ + echo "Tarball top-level may have changed. Inspecting:"; \ + find /opt/mxtools -maxdepth 3 -type f -name 'mx*'; \ + exit 1; \ + }; \ + chmod +x /opt/mxtools/modeler/mxbuild /opt/mxtools/modeler/mx 2>/dev/null || true; \ + echo "Mendix ${MENDIX_VERSION} build toolchain ready at /opt/mxtools/modeler/" + +# MX 11 mxbuild (.NET) creates its modeler settings under +# $HOME/.local/share/Mendix via GetFolderPath(LocalApplicationData). In a fresh +# container that directory does not exist, and mxbuild's WhitelistAware +# filesystem FailFast-rejects *creating* it ("UnauthorizedAccessException … +# EnsurePathSafety") before the build starts. Pre-create it for HOME=/root so +# mxbuild writes its settings without tripping the create-time whitelist check. +# (Empirically proven in the AIDE aide-mxtools image.) +RUN mkdir -p /root/.local/share/Mendix /root/.config/Mendix /root/.cache/Mendix + +# Crate entrypoint. +COPY build.sh /opt/mxtools/build.sh +RUN chmod +x /opt/mxtools/build.sh + +# Runs as root by design — an ephemeral, single-shot build container (not a +# long-running service). Root avoids the bind-mount uid-mismatch that would +# otherwise block writing the produced .mda back into the operator's mounted +# /workspace. On a Linux host where root-owned output is undesirable, pass +# `--user $(id -u)`; build.sh defensively re-creates the Mendix settings dir +# under whatever $HOME resolves to. +ENV JAVA_HOME=/opt/java/openjdk \ + MENDIX_VERSION=${MENDIX_VERSION} + +VOLUME ["/workspace"] +WORKDIR /workspace + +ENTRYPOINT ["/opt/mxtools/build.sh"] +CMD ["--help"] diff --git a/crates/mendix-11/build/README.md b/crates/mendix-11/build/README.md new file mode 100644 index 0000000..e7a6610 --- /dev/null +++ b/crates/mendix-11/build/README.md @@ -0,0 +1,102 @@ +# Mendix 11 Build Crate + +> A model-agnostic, version-pinned Docker image that compiles **any** Mendix 11.x +> project (`.mpr`) into a deployable `.mda` — and runs `mx check` — with nothing +> on the host but Docker. The Mendix build toolchain (`mxbuild` + `mx`) is pulled +> from the official Mendix CDN at build time; no Studio Pro install, no committed +> binaries. + +This is the **build** companion to the [`mendix-11` runtime crate](../). The build +crate *produces* the `.mda`; the runtime crate *runs* it. + +## Why this exists + +The AIDE pipeline compiles models with `mxbuild` driven from a bind-mounted Studio +Pro install. That requires a licensed modeler on the host. This crate makes the +toolchain self-contained and version-pinned: one image per Mendix major, the +matching `mxbuild` fetched from `cdn.mendix.com` at `docker build` — so a CI runner +or a contributor with only Docker can compile a Mendix 11 app without Studio Pro. + +| | Studio Pro on host | aide-mxtools (bind-mount) | This build crate | +|---|---|---|---| +| Needs Studio Pro installed | yes | yes (modeler bind-mounted) | **no** | +| Toolchain source | local install | local install | **CDN at build** | +| Version pinning | manual | manual | **one image per version** | +| Runs in plain CI | no | partial | **yes** | + +## Image tags + +| Tag | Pins | Use | +|---|---|---| +| `ontologylabs/mendix-mxbuild:11.6.4` | Exact version (immutable) | Reproducible CI | +| `ontologylabs/mendix-mxbuild:11` | Latest 11.x in this crate | Dev, demo | + +## Build the image + +```bash +cd crates/mendix-11/build +docker build \ + --platform linux/amd64 \ + --build-arg MENDIX_VERSION=11.6.4 \ + -t ontologylabs/mendix-mxbuild:11.6.4 . +# mxbuild + mx are pulled from cdn.mendix.com/runtime/mxbuild-11.6.4.tar.gz at build. +``` + +## Compile an app + +```bash +# /path/to/project contains App.mpr +docker run --rm \ + --platform linux/amd64 \ + -v /path/to/project:/workspace \ + ontologylabs/mendix-mxbuild:11.6.4 \ + build /workspace/App.mpr +# → writes /path/to/project/App.mda +``` + +Then run it with the runtime crate: + +```bash +unzip /path/to/project/App.mda -d /path/to/project/app +cd ../ # the mendix-11 runtime crate +docker compose -f tests/docker-compose.smoke.yml up # or your own compose +``` + +## Validate a model (`mx check`) + +```bash +docker run --rm -v /path/to/project:/workspace \ + ontologylabs/mendix-mxbuild:11.6.4 \ + check /workspace/App.mpr +# exit 0 = clean · 1 = errors · 2 = warnings only +``` + +## What the entrypoint auto-injects + +`build.sh` supplies the toolchain paths `mxbuild` needs unless you pass them: + +* `--java-home` / `--java-exe-path` → the baked JDK (`$JAVA_HOME`) +* `--gradle-home` → the toolchain's bundled Gradle +* `--loose-version-check` → tolerate patch drift between toolchain and model +* `--output=/workspace/.mda` → default output beside the `.mpr` + +Pass any of these explicitly to override. + +## Notes & gotchas + +* **Runs as root by design.** A single-shot build container, not a service. Root + avoids the bind-mount uid mismatch that would block writing the `.mda` back into + your mounted `/workspace`. On Linux, pass `--user $(id -u)` if you want + host-uid-owned output; the entrypoint re-creates the Mendix settings dir under + the resulting `$HOME`. +* **`.NET` whitelist (MX 11).** mxbuild creates settings under + `$HOME/.local/share/Mendix` and FailFast-rejects *creating* that path itself; the + image pre-creates it. (Same fix as the AIDE aide-mxtools image.) +* **amd64 emulation.** On Apple Silicon, `mxbuild` runs x86_64 under emulation. + Enable Colima Rosetta (`colima start --vm-type vz --vz-rosetta`) for speed; a + hung emulated build is the classic symptom of Rosetta being off. +* **No Mendix binary is committed.** The toolchain is fetched from the CDN at build, + on your licensed machine (D-DOCKER-LIB-002). See repo `guard.sh`. + +See [`provenance.yaml`](provenance.yaml) for the exact CDN source and +[`versions.yaml`](versions.yaml) for verified versions. diff --git a/crates/mendix-11/build/build.sh b/crates/mendix-11/build/build.sh new file mode 100644 index 0000000..def4295 --- /dev/null +++ b/crates/mendix-11/build/build.sh @@ -0,0 +1,134 @@ +#!/bin/bash +# Mendix Build Crate — entrypoint. Dispatches to mxbuild / mx from the toolchain +# baked at /opt/mxtools/modeler (pulled from the Mendix CDN at image build). +# +# Commands: +# build [mxbuild-options] Compile an .mpr → .mda +# check [mx-check-options] Run `mx check` (CI-normalised exit) +# version Print the mx toolchain version +# --help Usage +# +# Mount: -v :/workspace (the dir containing your App.mpr) +# Output: build writes .mda into /workspace next to the .mpr. +# +# Derived from the AIDE aide-mxtools entrypoint (production-proven mxbuild +# invocation) — same find-tool + auto-inject(--java-home/--gradle-home/ +# --loose-version-check) logic, retargeted at the baked /opt/mxtools/modeler. + +set -euo pipefail + +MXTOOLS_DIR="/opt/mxtools" + +# Defensive: re-create the Mendix settings dir under whatever HOME resolves to. +# The image bakes these for HOME=/root; this covers a runtime `--user`/`-e HOME=` +# override. MX 11 mxbuild (.NET) FailFast-rejects creating these itself. Idempotent. +mkdir -p "${HOME:-/root}/.local/share/Mendix" \ + "${HOME:-/root}/.config/Mendix" \ + "${HOME:-/root}/.cache/Mendix" 2>/dev/null || true + +# find_tool — locate mxbuild/mx in the baked toolchain. Strict candidates +# first (the `modeler/` layout the CDN tarball extracts to), then a glob fallback +# so a future tarball-layout change degrades gracefully instead of failing hard. +find_tool() { + local tool_name="$1" + for candidate in \ + "${MXTOOLS_DIR}/modeler/${tool_name}" \ + "${MXTOOLS_DIR}/${tool_name}" \ + "${MXTOOLS_DIR}/runtime/${tool_name}" \ + "${MXTOOLS_DIR}/tools/${tool_name}"; do + if [ -x "$candidate" ]; then echo "$candidate"; return 0; fi + done + local found + found=$(find "${MXTOOLS_DIR}" -name "${tool_name}" -type f -executable 2>/dev/null | head -1) + [ -n "$found" ] && { echo "$found"; return 0; } + return 1 +} + +case "${1:-}" in + build) + shift + [ $# -ge 1 ] || { echo "Usage: build [mxbuild-options]" >&2; exit 1; } + MXBUILD_BIN=$(find_tool "mxbuild") || { echo "ERROR: mxbuild not found under ${MXTOOLS_DIR}" >&2; exit 1; } + echo "Using: ${MXBUILD_BIN}" >&2 + + # Auto-inject the toolchain paths mxbuild needs, unless the caller set them. + EXTRA_ARGS=() + HAS_JAVA_HOME=false; HAS_GRADLE_HOME=false; HAS_LOOSE=false; HAS_OUTPUT=false + for arg in "$@"; do + case "$arg" in + --java-home=*) HAS_JAVA_HOME=true ;; + --gradle-home=*) HAS_GRADLE_HOME=true ;; + --loose-version-check) HAS_LOOSE=true ;; + --output=*|--output) HAS_OUTPUT=true ;; + esac + done + if [ "$HAS_JAVA_HOME" = false ] && [ -n "${JAVA_HOME:-}" ]; then + EXTRA_ARGS+=(--java-home="${JAVA_HOME}" --java-exe-path="${JAVA_HOME}/bin/java") + fi + if [ "$HAS_GRADLE_HOME" = false ]; then + for g in "${MXTOOLS_DIR}/modeler/tools/gradle" "${MXTOOLS_DIR}/tools/gradle"; do + [ -d "$g" ] && { EXTRA_ARGS+=(--gradle-home="$g"); break; } + done + fi + # Tolerate patch-version drift between the toolchain and the model + # (SDK commits may bump the model's product version). + [ "$HAS_LOOSE" = false ] && EXTRA_ARGS+=(--loose-version-check) + + # Default the output beside the .mpr in /workspace if the caller didn't + # pass --output. mpr-path is the first positional after `build`. + if [ "$HAS_OUTPUT" = false ]; then + MPR_PATH="$1" + MPR_BASE="$(basename "${MPR_PATH%.mpr}")" + EXTRA_ARGS+=(--output="/workspace/${MPR_BASE}.mda") + fi + + exec "$MXBUILD_BIN" "${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}" "$@" + ;; + + check) + shift + [ $# -ge 1 ] || { echo "Usage: check [mx-check-options]" >&2; exit 1; } + MX_BIN=$(find_tool "mx") || { echo "ERROR: mx not found under ${MXTOOLS_DIR}" >&2; exit 1; } + echo "Using: ${MX_BIN}" >&2 + # mx check returns an OR'd bitmask: 1=errors, 2=warnings, 4=deprecations. + # Normalise to the CI convention: 0=clean, 1=errors, 2=warnings-only. + set +e + "$MX_BIN" check "$@" + MX_EXIT=$? + set -e + if [ $((MX_EXIT & 1)) -ne 0 ]; then exit 1 + elif [ $((MX_EXIT & 2)) -ne 0 ]; then exit 2 + else exit 0 + fi + ;; + + version) + # mx has no toolchain --version flag; the pinned version is the image's + # MENDIX_VERSION. (mx show-* verbs report an *app's* version, not the toolchain's.) + echo "Mendix build crate — toolchain version ${MENDIX_VERSION:-unknown} (mxbuild + mx)" + ;; + + --help|help|"") + cat <<'USAGE' +Mendix Build Crate — mxbuild + mx in a version-pinned container + +Commands: + build [options] Compile an .mpr → .mda (output → /workspace) + check [options] Run mx check (exit 0=clean, 1=errors, 2=warnings) + version Show the mx toolchain version + +Mount: + -v :/workspace Directory containing your App.mpr + +Examples: + docker run --rm -v "$PWD":/workspace ontologylabs/mendix-mxbuild:11 build /workspace/App.mpr + docker run --rm -v "$PWD":/workspace ontologylabs/mendix-mxbuild:11 check /workspace/App.mpr +USAGE + ;; + + *) + echo "Unknown command: ${1:-}" >&2 + echo "Run with --help for usage." >&2 + exit 1 + ;; +esac diff --git a/crates/mendix-11/build/provenance.yaml b/crates/mendix-11/build/provenance.yaml new file mode 100644 index 0000000..fc410ed --- /dev/null +++ b/crates/mendix-11/build/provenance.yaml @@ -0,0 +1,38 @@ +# Per-image provenance for the mendix-11 BUILD crate (mxbuild + mx). +# Recipe-pull distribution model (D-DOCKER-LIB-001): no registry, no published +# Mendix binaries — the toolchain tarball is curl'd from the Mendix CDN at build. +# +# Stable provenance anchor: cdn_source_url (anyone can re-fetch + verify the same +# tarball). image_digest is the local image-config ID for a given build; it is +# build-environment-specific (apt package versions drift) and recorded for +# traceability, not byte-for-byte reproducibility. It is populated once the image +# has been built + smoke-verified on a release machine. + +crate: mendix-11/build +crate_version: "0.1.0" + +images: + - mendix_version: "11.6.4" + cdn_source_url: "https://cdn.mendix.com/runtime/mxbuild-11.6.4.tar.gz" + base_image: "eclipse-temurin:21-jdk-jammy" + java_version: 21 + platform: "linux/amd64" + image_tags: + - "ontologylabs/mendix-mxbuild:11.6.4" + - "ontologylabs/mendix-mxbuild:11" + image_digest: "sha256:04fa6043aa7058a82ecc1daef5bfebffc3bb6b9466c7e6136e2104b9413fff76" + image_size_bytes: 650279486 + built_at: "2026-06-23" + platform_build: "linux/amd64 under Colima (Apple Silicon emulation)" + image_verified: + built: "exit 0 — recipe-pull curl of mxbuild-11.6.4.tar.gz from CDN succeeded" + layout: "modeler/mxbuild, modeler/mx, modeler/tools/gradle all resolve at /opt/mxtools/modeler" + entrypoint: "build.sh runs; `version` reports 11.6.4; unknown-command errors" + mx_executes: "mx binary runs under emulation (parses CLI verbs)" + smoke_verified: "pending — full .mpr → .mda compile needs a licensed MX11 project" + notes: > + Recipe authored from the production AIDE aide-mxtools image (proven mxbuild + invocation: --java-home / --java-exe-path / --gradle-home / --loose-version-check) + and the verified runtime-crate recipe-pull pattern. Image build + binary + resolution + entrypoint are verified (2026-06-23); the remaining release gate + is a real-project MDA compile. diff --git a/crates/mendix-11/build/tests/smoke-test.sh b/crates/mendix-11/build/tests/smoke-test.sh new file mode 100644 index 0000000..ee839ad --- /dev/null +++ b/crates/mendix-11/build/tests/smoke-test.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Smoke test for the Mendix 11 build crate. +# +# Compiles a Mendix 11 project (.mpr) into an .mda using the build-crate image, +# then asserts the .mda was produced. Exits 0 on pass, non-zero on fail. +# +# Usage: ./smoke-test.sh /path/to/project/App.mpr +# +# You supply the licensed .mpr — none is committed to this repo (guard.sh blocks +# .mpr/.mda/.tar.gz patterns; D-DOCKER-LIB-002). The image's mxbuild toolchain is +# pulled from the Mendix CDN at `docker build`. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +BUILD_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +MENDIX_VERSION="${MENDIX_VERSION:-11.6.4}" +IMAGE="${BUILD_IMAGE:-ontologylabs/mendix-mxbuild:${MENDIX_VERSION}}" + +MPR_PATH="${1:-}" +if [ -z "${MPR_PATH}" ] || [ ! -f "${MPR_PATH}" ]; then + echo "[FATAL] Pass the path to a Mendix 11 .mpr as the first argument." + echo " e.g. $0 /path/to/MyApp/MyApp.mpr" + exit 78 +fi + +# Colima mounts only \$HOME into the VM; a project outside \$HOME is invisible to +# the container. Warn rather than silently mount an empty dir. +case "${MPR_PATH}" in + "${HOME}"/*) : ;; + *) echo "[WARN] ${MPR_PATH} is not under \$HOME — on Colima it may be invisible to the container." ;; +esac + +PROJECT_DIR="$(cd "$(dirname "${MPR_PATH}")" && pwd)" +MPR_NAME="$(basename "${MPR_PATH}")" +MDA_NAME="$(basename "${MPR_NAME%.mpr}").mda" + +# Build the image if absent. +if ! docker image inspect "${IMAGE}" >/dev/null 2>&1; then + echo "=== Building image ${IMAGE} (mxbuild pulled from CDN) ===" + docker build --platform linux/amd64 \ + --build-arg MENDIX_VERSION="${MENDIX_VERSION}" \ + -t "${IMAGE}" "${BUILD_DIR}" +fi + +echo "=== Compiling ${MPR_NAME} → ${MDA_NAME} ===" +rm -f "${PROJECT_DIR}/${MDA_NAME}" +docker run --rm --platform linux/amd64 \ + -v "${PROJECT_DIR}:/workspace" \ + "${IMAGE}" \ + build "/workspace/${MPR_NAME}" + +if [ -f "${PROJECT_DIR}/${MDA_NAME}" ]; then + SIZE=$(du -h "${PROJECT_DIR}/${MDA_NAME}" | cut -f1) + echo "=== PASS — ${MDA_NAME} produced (${SIZE}) ===" + exit 0 +fi + +echo "=== FAIL — ${MDA_NAME} was not produced. Inspect mxbuild output above. ===" +exit 1 diff --git a/crates/mendix-11/build/versions.yaml b/crates/mendix-11/build/versions.yaml new file mode 100644 index 0000000..86db916 --- /dev/null +++ b/crates/mendix-11/build/versions.yaml @@ -0,0 +1,19 @@ +# Mendix v11 build-toolchain versions verified against this crate. +# When adding a new version: build the image, smoke-test (build a known .mpr + +# mx check), then add the row. The build toolchain (mxbuild + mx) ships on the +# same CDN as the runtime, as mxbuild-.tar.gz. + +crate_version: "0.1.0" +default_mendix_version: "11.6.4" + +supported: + - mendix_version: "11.6.4" + cdn_url: "https://cdn.mendix.com/runtime/mxbuild-11.6.4.tar.gz" + java_version: 21 # JDK (mxbuild compiles javasource at release 21) + cdn_status: "200 (verified 2026-06-23)" + image_build: "verified 2026-06-23 — builds clean, binaries resolve, entrypoint runs (650 MB, amd64)" + image_smoke: "pending — full .mpr → .mda compile needs a licensed MX11 project" + notes: "Pairs with the mendix-11 runtime crate (same default version). Reference crate for 7/8/9/10 build crates." + +planned: + - mendix_version: "11.0.0" diff --git a/crates/mendix-7/build/CHANGELOG.md b/crates/mendix-7/build/CHANGELOG.md new file mode 100644 index 0000000..1874c52 --- /dev/null +++ b/crates/mendix-7/build/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog — Mendix 7 Build Crate + +## [0.1.0] — 2026-06-23 + +Initial release. The build (mxbuild + mx) companion to the mendix-7 runtime crate. + +* `Dockerfile`: model-agnostic; JDK 8 base (`eclipse-temurin:8-jdk-jammy`); + downloads the Mendix 7 build toolchain from the official CDN at build + (`mxbuild-${MENDIX_VERSION}.tar.gz`); never commits a Mendix binary + (recipe-pull, D-DOCKER-LIB-001/002). apt deps for the .NET/Mono toolchain: + `libgdiplus libicu70 libssl3 sqlite3`. +* `build.sh`: `build` / `check` / `version` dispatch. Auto-injects + `--java-home` / `--java-exe-path` / `--gradle-home` / `--loose-version-check` + and defaults `--output=/workspace/.mda`. `mx check` exit codes normalised + to the CI convention (0 clean / 1 errors / 2 warnings). Derived from the + production AIDE aide-mxtools entrypoint. +* Pre-creates `$HOME/.local/share/Mendix` for the MX 7 `.NET` mxbuild whitelist + FailFast (same fix as aide-mxtools). +* Default Mendix version: 7.23.8.58888 (override via `--build-arg MENDIX_VERSION`). + +### Verification status +* CDN source `mxbuild-7.23.8.58888.tar.gz`: **HTTP 200 verified (2026-06-23)**. +* Image build + `mx version` / MDA smoke: **pending** — recorded honestly in + `provenance.yaml` (`smoke_verified: pending`). The mxbuild invocation itself is + the production-proven aide-mxtools path; the remaining gate is a release build. + +### Why +Lets a Docker-only environment (CI, a contributor without Studio Pro) compile and +validate a Mendix 7 app, producing the `.mda` the runtime crate runs. diff --git a/crates/mendix-7/build/Dockerfile b/crates/mendix-7/build/Dockerfile new file mode 100644 index 0000000..aa99a96 --- /dev/null +++ b/crates/mendix-7/build/Dockerfile @@ -0,0 +1,113 @@ +# Mendix 7 Build Crate — model-agnostic mxbuild + mx toolchain (recipe-pull). +# +# Image: ontologylabs/mendix-mxbuild:7 +# ontologylabs/mendix-mxbuild:7.23.8.58888 (immutable) +# +# Contract: +# * Bake: JDK 8 + the Mendix 7.x build toolchain (mxbuild + mx), pulled from +# the official Mendix CDN at `docker build` time. +# * NEVER commit a Mendix binary to the repo. The toolchain tarball is curl'd +# from cdn.mendix.com on the licensed user's own machine — exactly like the +# runtime crate downloads the runtime tarball (D-DOCKER-LIB-001 recipe-pull; +# D-DOCKER-LIB-002 no-binaries guard). You bring the licensed toolchain; we +# bring the build recipe. +# * Bind-mount the project directory (containing the `.mpr`) at /workspace. +# * Compile : `docker run -v :/workspace ... build /workspace/App.mpr` +# → writes App.mda next to the .mpr in /workspace. +# * Validate: `docker run -v :/workspace ... check /workspace/App.mpr` +# → `mx check` with CI-normalised exit codes (0 clean / 1 error / 2 warn). +# +# Why a "build crate" (companion to the runtime crate): +# The AIDE pipeline drives mxbuild via a bind-mounted Studio Pro modeler +# (the aide-mxtools image). This crate makes the toolchain self-contained and +# version-pinned: one image per Mendix major, the matching mxbuild fetched +# from the CDN at build, so a contributor with only Docker can compile any app +# at that version without a local Studio Pro install. Runtime crate runs the +# app; build crate produces the .mda the runtime crate runs. +# +# Provenance: +# * mxbuild + mx from https://cdn.mendix.com/runtime/mxbuild-${MENDIX_VERSION}.tar.gz +# * Tarball top-level is `modeler/` → modeler/mxbuild, modeler/mx, +# modeler/tools/gradle (verified against the AIDE build-server cache layout). +# * Java: Eclipse Temurin 8 JDK — Mendix 7 compiles Java actions at release 8. +# A full JDK (not JRE) is required: mxbuild invokes javac to compile the +# app's javasource and Java actions. + +FROM --platform=linux/amd64 eclipse-temurin:8-jdk-jammy + +ARG MENDIX_VERSION=7.23.8.58888 +ARG MXBUILD_CDN_BASE=https://cdn.mendix.com/runtime + +LABEL ai.ayios.purpose="Mendix 7 build crate (mxbuild + mx, model-agnostic)" +LABEL ai.ayios.crate="mendix-7/build" +LABEL ai.ayios.mendix-version="${MENDIX_VERSION}" +LABEL maintainer="hardy@agileworks.co.za" + +# Layer 1 — apt deps for the mxbuild toolchain. mxbuild is a .NET/Mono program: +# libgdiplus — System.Drawing (image/PDF generation during build) +# libicu70 — globalization/ICU (jammy ships ICU 70) +# libssl3 — TLS for any build-time fetch +# sqlite3 — read the .mpr (modern Mendix projects are SQLite databases) +# curl — pull the toolchain tarball +# Stable layer; survives churn in the heavier toolchain layer below. +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + libgdiplus \ + libicu70 \ + libssl3 \ + sqlite3 \ + && rm -rf /var/lib/apt/lists/* + +# Layer 2 — Mendix build toolchain tarball (mxbuild + mx). ~300-500 MB download. +# Recipe-pull: curl'd from the CDN at build, never committed. Cached unless +# MENDIX_VERSION or MXBUILD_CDN_BASE changes. +# +# Tarball top-level is `modeler/`, so extracting at /opt/mxtools produces the +# same layout the AIDE build-server caches and mounts at /opt/mxtools: +# /opt/mxtools/modeler/mxbuild +# /opt/mxtools/modeler/mx +# /opt/mxtools/modeler/tools/gradle +RUN set -eux; \ + mkdir -p /opt/mxtools /workspace; \ + echo "Downloading Mendix build toolchain ${MENDIX_VERSION} from ${MXBUILD_CDN_BASE}..."; \ + curl -fsSL "${MXBUILD_CDN_BASE}/mxbuild-${MENDIX_VERSION}.tar.gz" -o /tmp/mxbuild.tar.gz; \ + echo "Extracting..."; \ + tar -xzf /tmp/mxbuild.tar.gz -C /opt/mxtools; \ + rm /tmp/mxbuild.tar.gz; \ + test -f /opt/mxtools/modeler/mxbuild || { \ + echo "FATAL: modeler/mxbuild not found at expected path /opt/mxtools/modeler/mxbuild."; \ + echo "Tarball top-level may have changed. Inspecting:"; \ + find /opt/mxtools -maxdepth 3 -type f -name 'mx*'; \ + exit 1; \ + }; \ + chmod +x /opt/mxtools/modeler/mxbuild /opt/mxtools/modeler/mx 2>/dev/null || true; \ + echo "Mendix ${MENDIX_VERSION} build toolchain ready at /opt/mxtools/modeler/" + +# Mendix mxbuild — MX 11's .NET toolchain in particular — creates settings under +# $HOME/.local/share/Mendix via GetFolderPath(LocalApplicationData). In a fresh +# container that directory does not exist, and mxbuild's WhitelistAware +# filesystem FailFast-rejects *creating* it ("UnauthorizedAccessException … +# EnsurePathSafety") before the build starts. Pre-create it for HOME=/root so +# mxbuild writes settings without tripping the create-time whitelist check (a +# harmless no-op on older Mono-based mxbuild). Proven in AIDE aide-mxtools. +RUN mkdir -p /root/.local/share/Mendix /root/.config/Mendix /root/.cache/Mendix + +# Crate entrypoint. +COPY build.sh /opt/mxtools/build.sh +RUN chmod +x /opt/mxtools/build.sh + +# Runs as root by design — an ephemeral, single-shot build container (not a +# long-running service). Root avoids the bind-mount uid-mismatch that would +# otherwise block writing the produced .mda back into the operator's mounted +# /workspace. On a Linux host where root-owned output is undesirable, pass +# `--user $(id -u)`; build.sh defensively re-creates the Mendix settings dir +# under whatever $HOME resolves to. +ENV JAVA_HOME=/opt/java/openjdk \ + MENDIX_VERSION=${MENDIX_VERSION} + +VOLUME ["/workspace"] +WORKDIR /workspace + +ENTRYPOINT ["/opt/mxtools/build.sh"] +CMD ["--help"] diff --git a/crates/mendix-7/build/README.md b/crates/mendix-7/build/README.md new file mode 100644 index 0000000..678b6fa --- /dev/null +++ b/crates/mendix-7/build/README.md @@ -0,0 +1,102 @@ +# Mendix 7 Build Crate + +> A model-agnostic, version-pinned Docker image that compiles **any** Mendix 7.x +> project (`.mpr`) into a deployable `.mda` — and runs `mx check` — with nothing +> on the host but Docker. The Mendix build toolchain (`mxbuild` + `mx`) is pulled +> from the official Mendix CDN at build time; no Studio Pro install, no committed +> binaries. + +This is the **build** companion to the [`mendix-7` runtime crate](../). The build +crate *produces* the `.mda`; the runtime crate *runs* it. + +## Why this exists + +The AIDE pipeline compiles models with `mxbuild` driven from a bind-mounted Studio +Pro install. That requires a licensed modeler on the host. This crate makes the +toolchain self-contained and version-pinned: one image per Mendix major, the +matching `mxbuild` fetched from `cdn.mendix.com` at `docker build` — so a CI runner +or a contributor with only Docker can compile a Mendix 7 app without Studio Pro. + +| | Studio Pro on host | aide-mxtools (bind-mount) | This build crate | +|---|---|---|---| +| Needs Studio Pro installed | yes | yes (modeler bind-mounted) | **no** | +| Toolchain source | local install | local install | **CDN at build** | +| Version pinning | manual | manual | **one image per version** | +| Runs in plain CI | no | partial | **yes** | + +## Image tags + +| Tag | Pins | Use | +|---|---|---| +| `ontologylabs/mendix-mxbuild:7.23.8.58888` | Exact version (immutable) | Reproducible CI | +| `ontologylabs/mendix-mxbuild:7` | Latest 7.x in this crate | Dev, demo | + +## Build the image + +```bash +cd crates/mendix-7/build +docker build \ + --platform linux/amd64 \ + --build-arg MENDIX_VERSION=7.23.8.58888 \ + -t ontologylabs/mendix-mxbuild:7.23.8.58888 . +# mxbuild + mx are pulled from cdn.mendix.com/runtime/mxbuild-7.23.8.58888.tar.gz at build. +``` + +## Compile an app + +```bash +# /path/to/project contains App.mpr +docker run --rm \ + --platform linux/amd64 \ + -v /path/to/project:/workspace \ + ontologylabs/mendix-mxbuild:7.23.8.58888 \ + build /workspace/App.mpr +# → writes /path/to/project/App.mda +``` + +Then run it with the runtime crate: + +```bash +unzip /path/to/project/App.mda -d /path/to/project/app +cd ../ # the mendix-7 runtime crate +docker compose -f tests/docker-compose.smoke.yml up # or your own compose +``` + +## Validate a model (`mx check`) + +```bash +docker run --rm -v /path/to/project:/workspace \ + ontologylabs/mendix-mxbuild:7.23.8.58888 \ + check /workspace/App.mpr +# exit 0 = clean · 1 = errors · 2 = warnings only +``` + +## What the entrypoint auto-injects + +`build.sh` supplies the toolchain paths `mxbuild` needs unless you pass them: + +* `--java-home` / `--java-exe-path` → the baked JDK (`$JAVA_HOME`) +* `--gradle-home` → the toolchain's bundled Gradle +* `--loose-version-check` → tolerate patch drift between toolchain and model +* `--output=/workspace/.mda` → default output beside the `.mpr` + +Pass any of these explicitly to override. + +## Notes & gotchas + +* **Runs as root by design.** A single-shot build container, not a service. Root + avoids the bind-mount uid mismatch that would block writing the `.mda` back into + your mounted `/workspace`. On Linux, pass `--user $(id -u)` if you want + host-uid-owned output; the entrypoint re-creates the Mendix settings dir under + the resulting `$HOME`. +* **`.NET` whitelist (MX 11 origin).** mxbuild creates settings under + `$HOME/.local/share/Mendix` and FailFast-rejects *creating* that path itself; the + image pre-creates it. (Same fix as the AIDE aide-mxtools image.) +* **amd64 emulation.** On Apple Silicon, `mxbuild` runs x86_64 under emulation. + Enable Colima Rosetta (`colima start --vm-type vz --vz-rosetta`) for speed; a + hung emulated build is the classic symptom of Rosetta being off. +* **No Mendix binary is committed.** The toolchain is fetched from the CDN at build, + on your licensed machine (D-DOCKER-LIB-002). See repo `guard.sh`. + +See [`provenance.yaml`](provenance.yaml) for the exact CDN source and +[`versions.yaml`](versions.yaml) for verified versions. diff --git a/crates/mendix-7/build/build.sh b/crates/mendix-7/build/build.sh new file mode 100644 index 0000000..775bcee --- /dev/null +++ b/crates/mendix-7/build/build.sh @@ -0,0 +1,134 @@ +#!/bin/bash +# Mendix Build Crate — entrypoint. Dispatches to mxbuild / mx from the toolchain +# baked at /opt/mxtools/modeler (pulled from the Mendix CDN at image build). +# +# Commands: +# build [mxbuild-options] Compile an .mpr → .mda +# check [mx-check-options] Run `mx check` (CI-normalised exit) +# version Print the mx toolchain version +# --help Usage +# +# Mount: -v :/workspace (the dir containing your App.mpr) +# Output: build writes .mda into /workspace next to the .mpr. +# +# Derived from the AIDE aide-mxtools entrypoint (production-proven mxbuild +# invocation) — same find-tool + auto-inject(--java-home/--gradle-home/ +# --loose-version-check) logic, retargeted at the baked /opt/mxtools/modeler. + +set -euo pipefail + +MXTOOLS_DIR="/opt/mxtools" + +# Defensive: re-create the Mendix settings dir under whatever HOME resolves to. +# The image bakes these for HOME=/root; this covers a runtime `--user`/`-e HOME=` +# override. MX 11's .NET mxbuild FailFast-rejects creating these; harmless elsewhere. Idempotent. +mkdir -p "${HOME:-/root}/.local/share/Mendix" \ + "${HOME:-/root}/.config/Mendix" \ + "${HOME:-/root}/.cache/Mendix" 2>/dev/null || true + +# find_tool — locate mxbuild/mx in the baked toolchain. Strict candidates +# first (the `modeler/` layout the CDN tarball extracts to), then a glob fallback +# so a future tarball-layout change degrades gracefully instead of failing hard. +find_tool() { + local tool_name="$1" + for candidate in \ + "${MXTOOLS_DIR}/modeler/${tool_name}" \ + "${MXTOOLS_DIR}/${tool_name}" \ + "${MXTOOLS_DIR}/runtime/${tool_name}" \ + "${MXTOOLS_DIR}/tools/${tool_name}"; do + if [ -x "$candidate" ]; then echo "$candidate"; return 0; fi + done + local found + found=$(find "${MXTOOLS_DIR}" -name "${tool_name}" -type f -executable 2>/dev/null | head -1) + [ -n "$found" ] && { echo "$found"; return 0; } + return 1 +} + +case "${1:-}" in + build) + shift + [ $# -ge 1 ] || { echo "Usage: build [mxbuild-options]" >&2; exit 1; } + MXBUILD_BIN=$(find_tool "mxbuild") || { echo "ERROR: mxbuild not found under ${MXTOOLS_DIR}" >&2; exit 1; } + echo "Using: ${MXBUILD_BIN}" >&2 + + # Auto-inject the toolchain paths mxbuild needs, unless the caller set them. + EXTRA_ARGS=() + HAS_JAVA_HOME=false; HAS_GRADLE_HOME=false; HAS_LOOSE=false; HAS_OUTPUT=false + for arg in "$@"; do + case "$arg" in + --java-home=*) HAS_JAVA_HOME=true ;; + --gradle-home=*) HAS_GRADLE_HOME=true ;; + --loose-version-check) HAS_LOOSE=true ;; + --output=*|--output) HAS_OUTPUT=true ;; + esac + done + if [ "$HAS_JAVA_HOME" = false ] && [ -n "${JAVA_HOME:-}" ]; then + EXTRA_ARGS+=(--java-home="${JAVA_HOME}" --java-exe-path="${JAVA_HOME}/bin/java") + fi + if [ "$HAS_GRADLE_HOME" = false ]; then + for g in "${MXTOOLS_DIR}/modeler/tools/gradle" "${MXTOOLS_DIR}/tools/gradle"; do + [ -d "$g" ] && { EXTRA_ARGS+=(--gradle-home="$g"); break; } + done + fi + # Tolerate patch-version drift between the toolchain and the model + # (SDK commits may bump the model's product version). + [ "$HAS_LOOSE" = false ] && EXTRA_ARGS+=(--loose-version-check) + + # Default the output beside the .mpr in /workspace if the caller didn't + # pass --output. mpr-path is the first positional after `build`. + if [ "$HAS_OUTPUT" = false ]; then + MPR_PATH="$1" + MPR_BASE="$(basename "${MPR_PATH%.mpr}")" + EXTRA_ARGS+=(--output="/workspace/${MPR_BASE}.mda") + fi + + exec "$MXBUILD_BIN" "${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}" "$@" + ;; + + check) + shift + [ $# -ge 1 ] || { echo "Usage: check [mx-check-options]" >&2; exit 1; } + MX_BIN=$(find_tool "mx") || { echo "ERROR: mx not found under ${MXTOOLS_DIR}" >&2; exit 1; } + echo "Using: ${MX_BIN}" >&2 + # mx check returns an OR'd bitmask: 1=errors, 2=warnings, 4=deprecations. + # Normalise to the CI convention: 0=clean, 1=errors, 2=warnings-only. + set +e + "$MX_BIN" check "$@" + MX_EXIT=$? + set -e + if [ $((MX_EXIT & 1)) -ne 0 ]; then exit 1 + elif [ $((MX_EXIT & 2)) -ne 0 ]; then exit 2 + else exit 0 + fi + ;; + + version) + # mx has no toolchain --version flag; the pinned version is the image's + # MENDIX_VERSION. (mx show-* verbs report an *app's* version, not the toolchain's.) + echo "Mendix build crate — toolchain version ${MENDIX_VERSION:-unknown} (mxbuild + mx)" + ;; + + --help|help|"") + cat <<'USAGE' +Mendix Build Crate — mxbuild + mx in a version-pinned container + +Commands: + build [options] Compile an .mpr → .mda (output → /workspace) + check [options] Run mx check (exit 0=clean, 1=errors, 2=warnings) + version Show the mx toolchain version + +Mount: + -v :/workspace Directory containing your App.mpr + +Examples: + docker run --rm -v "$PWD":/workspace ontologylabs/mendix-mxbuild:7 build /workspace/App.mpr + docker run --rm -v "$PWD":/workspace ontologylabs/mendix-mxbuild:7 check /workspace/App.mpr +USAGE + ;; + + *) + echo "Unknown command: ${1:-}" >&2 + echo "Run with --help for usage." >&2 + exit 1 + ;; +esac diff --git a/crates/mendix-7/build/provenance.yaml b/crates/mendix-7/build/provenance.yaml new file mode 100644 index 0000000..e2b58dc --- /dev/null +++ b/crates/mendix-7/build/provenance.yaml @@ -0,0 +1,31 @@ +# Per-image provenance for the mendix-7 BUILD crate (mxbuild + mx). +# Recipe-pull distribution model (D-DOCKER-LIB-001): no registry, no published +# Mendix binaries — the toolchain tarball is curl'd from the Mendix CDN at build. +# +# Stable provenance anchor: cdn_source_url (anyone can re-fetch + verify the same +# tarball). image_digest is the local image-config ID for a given build; it is +# build-environment-specific (apt package versions drift) and recorded for +# traceability, not byte-for-byte reproducibility. It is populated once the image +# has been built + smoke-verified on a release machine. + +crate: mendix-7/build +crate_version: "0.1.0" + +images: + - mendix_version: "7.23.8.58888" + cdn_source_url: "https://cdn.mendix.com/runtime/mxbuild-7.23.8.58888.tar.gz" + base_image: "eclipse-temurin:8-jdk-jammy" + java_version: 8 + platform: "linux/amd64" + image_tags: + - "ontologylabs/mendix-mxbuild:7.23.8.58888" + - "ontologylabs/mendix-mxbuild:7" + image_digest: null # filled after a verified release build + cdn_verified: "2026-06-23 — HTTP 200 (HEAD)" + smoke_verified: "pending" + notes: > + Recipe authored from the production AIDE aide-mxtools image (proven mxbuild + invocation: --java-home / --java-exe-path / --gradle-home / --loose-version-check) + and the verified runtime-crate recipe-pull pattern. The CDN source URL is + verified reachable; an image build + `mx version`/MDA smoke is the remaining + release gate. diff --git a/crates/mendix-7/build/tests/smoke-test.sh b/crates/mendix-7/build/tests/smoke-test.sh new file mode 100644 index 0000000..33b4db9 --- /dev/null +++ b/crates/mendix-7/build/tests/smoke-test.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Smoke test for the Mendix 7 build crate. +# +# Compiles a Mendix 7 project (.mpr) into an .mda using the build-crate image, +# then asserts the .mda was produced. Exits 0 on pass, non-zero on fail. +# +# Usage: ./smoke-test.sh /path/to/project/App.mpr +# +# You supply the licensed .mpr — none is committed to this repo (guard.sh blocks +# .mpr/.mda/.tar.gz patterns; D-DOCKER-LIB-002). The image's mxbuild toolchain is +# pulled from the Mendix CDN at `docker build`. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +BUILD_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +MENDIX_VERSION="${MENDIX_VERSION:-7.23.8.58888}" +IMAGE="${BUILD_IMAGE:-ontologylabs/mendix-mxbuild:${MENDIX_VERSION}}" + +MPR_PATH="${1:-}" +if [ -z "${MPR_PATH}" ] || [ ! -f "${MPR_PATH}" ]; then + echo "[FATAL] Pass the path to a Mendix 7 .mpr as the first argument." + echo " e.g. $0 /path/to/MyApp/MyApp.mpr" + exit 78 +fi + +# Colima mounts only \$HOME into the VM; a project outside \$HOME is invisible to +# the container. Warn rather than silently mount an empty dir. +case "${MPR_PATH}" in + "${HOME}"/*) : ;; + *) echo "[WARN] ${MPR_PATH} is not under \$HOME — on Colima it may be invisible to the container." ;; +esac + +PROJECT_DIR="$(cd "$(dirname "${MPR_PATH}")" && pwd)" +MPR_NAME="$(basename "${MPR_PATH}")" +MDA_NAME="$(basename "${MPR_NAME%.mpr}").mda" + +# Build the image if absent. +if ! docker image inspect "${IMAGE}" >/dev/null 2>&1; then + echo "=== Building image ${IMAGE} (mxbuild pulled from CDN) ===" + docker build --platform linux/amd64 \ + --build-arg MENDIX_VERSION="${MENDIX_VERSION}" \ + -t "${IMAGE}" "${BUILD_DIR}" +fi + +echo "=== Compiling ${MPR_NAME} → ${MDA_NAME} ===" +rm -f "${PROJECT_DIR}/${MDA_NAME}" +docker run --rm --platform linux/amd64 \ + -v "${PROJECT_DIR}:/workspace" \ + "${IMAGE}" \ + build "/workspace/${MPR_NAME}" + +if [ -f "${PROJECT_DIR}/${MDA_NAME}" ]; then + SIZE=$(du -h "${PROJECT_DIR}/${MDA_NAME}" | cut -f1) + echo "=== PASS — ${MDA_NAME} produced (${SIZE}) ===" + exit 0 +fi + +echo "=== FAIL — ${MDA_NAME} was not produced. Inspect mxbuild output above. ===" +exit 1 diff --git a/crates/mendix-7/build/versions.yaml b/crates/mendix-7/build/versions.yaml new file mode 100644 index 0000000..bd6a90a --- /dev/null +++ b/crates/mendix-7/build/versions.yaml @@ -0,0 +1,18 @@ +# Mendix v7 build-toolchain versions verified against this crate. +# When adding a new version: build the image, smoke-test (build a known .mpr + +# mx check), then add the row. The build toolchain (mxbuild + mx) ships on the +# same CDN as the runtime, as mxbuild-.tar.gz. + +crate_version: "0.1.0" +default_mendix_version: "7.23.8.58888" + +supported: + - mendix_version: "7.23.8.58888" + cdn_url: "https://cdn.mendix.com/runtime/mxbuild-7.23.8.58888.tar.gz" + java_version: 8 # JDK (mxbuild compiles javasource at release 8) + cdn_status: "200 (verified 2026-06-23)" + image_smoke: "pending" # image build + `mx version` not yet run in CI + notes: "Pairs with the mendix-7 runtime crate (same default version)." + +planned: + - mendix_version: "7.23.0" diff --git a/crates/mendix-8/CHANGELOG.md b/crates/mendix-8/CHANGELOG.md new file mode 100644 index 0000000..9e46664 --- /dev/null +++ b/crates/mendix-8/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog — Mendix 8 Runtime Crate + +## [0.1.0] — 2026-06-23 + +Initial release. Replicates the mendix-9 runtime crate pattern for Mendix 8.x. + +* `Dockerfile`: model-agnostic; downloads the Mendix 8 runtime from the official + CDN at build (`mendix-${MENDIX_VERSION}.tar.gz`); bind-mount target at + `/opt/mendix/app`; no Mendix binary committed (recipe-pull, D-DOCKER-LIB-001/002). +* **Java 11 base** (`eclipse-temurin:11-jre-jammy`): Mendix 8.18 LTS is certified + on Java 8 and Java 11; Java 11 is chosen (Mendix Cloud's late-MX8 default; runs + Java-8 bytecode). Rebuild on Temurin 8 via `--build-arg` for a Java-8-pinned app. +* `start.sh`: m2ee admin protocol entrypoint with DB-wait, DDL-sync retry, + DEVELOPMENT_MODE bypass; auto-builds `MicroflowConstants` from the deployed + model's `metadata.json` defaults (operator `MICROFLOW_CONSTANTS` env overrides). + Identical boot path to the mendix-9/10/11 crates. +* Default Mendix version: **8.18.35.97** (the final 8.18 LTS patch; override via + `--build-arg MENDIX_VERSION`). + +### Verification status (2026-06-23) +* CDN source `mendix-8.18.35.97.tar.gz`: **HTTP 200 verified**. +* Image build + MDA smoke: **pending** — no MX8 fixture app in the corpus yet; + recorded honestly in `provenance.yaml` (`smoke_verified: pending`). The boot + path is shared, verbatim, with the smoke-verified mendix-9 runtime crate. + +### Mendix 8 support +* MX8 is **out of standard Mendix support**. The default `8.18.35.97` is still + CDN-hosted, so it builds with no extra steps. Older MX8 patches are not all on + the CDN (e.g. `8.18.21.4259` → 404) — for those, see + [`PORTAL-DOWNLOAD.md`](PORTAL-DOWNLOAD.md). + +### Why +Replaces the dangling-image-per-reload anti-pattern of the CF-buildpack runtime +image for Mendix 8.x apps, and completes the 7–11 version coverage. See +`README.md` § "Why this exists". diff --git a/crates/mendix-8/Dockerfile b/crates/mendix-8/Dockerfile new file mode 100644 index 0000000..319a50f --- /dev/null +++ b/crates/mendix-8/Dockerfile @@ -0,0 +1,123 @@ +# Mendix 8 Runtime Crate — model-agnostic, app-agnostic, version-pinned. +# +# Image: ontologylabs/mendix-runtime:8 +# ontologylabs/mendix-runtime:8.18.35.97 (immutable) +# +# Contract: +# * Bake: JRE 11 + Mendix 8.x runtime + m2ee admin scripts +# * NEVER bake: any application's model / javasource / theme / userlib +# * Bind-mount the unzipped MDA at /opt/mendix/app +# * Bind-mount writable file storage at /opt/mendix/data +# * Configure via env (DATABASE_ENDPOINT, ADMIN_PASSWORD, MXRUNTIME_*) +# * One image per Mendix v8.x — reusable across every app at that version +# +# Why a "crate": +# The CF-buildpack image bakes the model, so every reload spawns a new +# image and orphans the previous one (785 MB dangling each time). This +# crate inverts that: model is bind-mounted, image is stable, reload = +# `docker compose restart` with zero rebuild and zero dangling images. +# +# Provenance: +# * Built from the official Mendix CDN runtime tarball: +# https://cdn.mendix.com/runtime/mendix-${MENDIX_VERSION}.tar.gz +# * Java: Eclipse Temurin 11 — Mendix 8.18 LTS is certified on Java 8 and +# Java 11. Java 11 is chosen here (Mendix Cloud's late-MX8 default; it runs +# Java-8 bytecode fine). For an app that pins Java 8, rebuild on +# eclipse-temurin:8-jre-jammy via --build-arg. +# * Boot mechanism: m2ee admin protocol over HTTP (port 8090) — the same +# protocol Mendix Cloud uses internally. + +FROM --platform=linux/amd64 eclipse-temurin:11-jre-jammy + +ARG MENDIX_VERSION=8.18.35.97 +ARG RUNTIME_CDN_BASE=https://cdn.mendix.com/runtime +ARG USER_UID=1001 + +LABEL ai.ayios.purpose="Mendix 8 runtime crate (model-agnostic)" +LABEL ai.ayios.crate="mendix-8" +LABEL ai.ayios.mendix-version="${MENDIX_VERSION}" +LABEL maintainer="hardy@agileworks.co.za" + +# Layer 1 — curl only. Stable across rebuilds. Lets the runtime tarball +# download (~341 MB) sit in its own layer that doesn't invalidate when +# unrelated apt deps change. +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Layer 2 — Mendix runtime tarball. ~341 MB download, ~700 MB extracted. +# Cached unless MENDIX_VERSION or RUNTIME_CDN_BASE changes. +# +# Tarball top-level is `${VERSION}/`, so extracting at /opt/mendix/runtimes/ +# produces the conventional CF-buildpack layout: +# /opt/mendix/runtimes/${MENDIX_VERSION}/runtime/launcher/runtimelauncher.jar +# /opt/mendix/runtimes/${MENDIX_VERSION}/runtime/mxclientsystem +# Identical to what the official cf-mendix-buildpack stages — m2ee + +# Mendix monitoring tools work without modification. +# +# Forward-compatible: future patch releases bake into the same image as +# additional subdirs under /opt/mendix/runtimes/, and start.sh resolves +# the right one per deployed MDA's metadata.json. +RUN set -eux; \ + mkdir -p /opt/mendix/runtimes /opt/mendix/app /opt/mendix/data; \ + echo "Downloading Mendix runtime ${MENDIX_VERSION} from ${RUNTIME_CDN_BASE}..."; \ + curl -fsSL "${RUNTIME_CDN_BASE}/mendix-${MENDIX_VERSION}.tar.gz" \ + -o /tmp/mxruntime.tar.gz; \ + echo "Extracting..."; \ + tar -xzf /tmp/mxruntime.tar.gz -C /opt/mendix/runtimes; \ + rm /tmp/mxruntime.tar.gz; \ + test -f "/opt/mendix/runtimes/${MENDIX_VERSION}/runtime/launcher/runtimelauncher.jar" || \ + (echo "FATAL: runtimelauncher.jar not found at expected path:" && \ + echo " /opt/mendix/runtimes/${MENDIX_VERSION}/runtime/launcher/runtimelauncher.jar" && \ + echo "Tarball top-level may have changed. Inspecting:" && \ + find /opt/mendix/runtimes -maxdepth 4 -type d && exit 1); \ + echo "Mendix ${MENDIX_VERSION} runtime ready at /opt/mendix/runtimes/${MENDIX_VERSION}/" + +# Layer 3 — secondary apt deps. Changes more often than the runtime tarball +# (e.g. flipping JSON parser between python3 and jq). Kept separate so +# tarball download cache survives ordinary churn. +# fontconfig + libfreetype6 — Mendix charting / PDF rendering +# libgomp1 — required by some native libs Mendix loads +# jq — JSON parsing in start.sh (metadata.json + m2ee result responses). +# Avoid python3 here: Ubuntu's --no-install-recommends + python3 +# resolves to python3-minimal which lacks the `json` stdlib module. +RUN apt-get update && apt-get install -y --no-install-recommends \ + fontconfig \ + libfreetype6 \ + libgomp1 \ + jq \ + && rm -rf /var/lib/apt/lists/* + +# Crate scripts +COPY start.sh /opt/mendix/start.sh +RUN chmod +x /opt/mendix/start.sh + +# Permissions for non-root user +RUN chown -R ${USER_UID}:0 /opt/mendix && chmod -R g=u /opt/mendix + +USER ${USER_UID} + +# Bind-mount targets. +# - /opt/mendix/app → operator bind-mounts the unzipped MDA contents +# (model/, web/, native/, theme/, userlib/, etc.) +# - /opt/mendix/data → writable file storage / DB-backed file documents +VOLUME ["/opt/mendix/app", "/opt/mendix/data"] + +WORKDIR /opt/mendix/app + +EXPOSE 8080 8090 + +# Default env values — operator overrides via docker-compose / docker run. +# An empty ADMIN_PASSWORD is rejected by start.sh; operator MUST set this. +ENV ADMIN_PASSWORD="" \ + DATABASE_ENDPOINT="" \ + MXRUNTIME_HttpHeaders="[]" \ + MX_LOG_LEVEL="i" \ + ADMIN_PORT=8090 \ + RUNTIME_PORT=8080 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=120s --retries=3 \ + CMD curl -fsS "http://localhost:8080/" >/dev/null || exit 1 + +ENTRYPOINT ["/opt/mendix/start.sh"] diff --git a/crates/mendix-8/PORTAL-DOWNLOAD.md b/crates/mendix-8/PORTAL-DOWNLOAD.md new file mode 100644 index 0000000..72b0826 --- /dev/null +++ b/crates/mendix-8/PORTAL-DOWNLOAD.md @@ -0,0 +1,103 @@ +# Obtaining a Mendix 8 toolchain that isn't on the public CDN + +This crate's **default** version (`8.18.35.97`, the final 8.18 LTS patch) is served +from the public Mendix CDN, so `docker build` Just Works — no portal needed. Use +this document only when you need an **older MX8 patch** (or any Mendix version) that +the CDN does **not** host. + +> **How to tell.** Probe the CDN first — no login required: +> ```bash +> curl -sI "https://cdn.mendix.com/runtime/mendix-8.18.21.4259.tar.gz" | head -1 +> curl -sI "https://cdn.mendix.com/runtime/mxbuild-8.18.21.4259.tar.gz" | head -1 +> ``` +> `HTTP/2 200` → it's on the CDN; just pass `--build-arg MENDIX_VERSION=` and +> skip this document. `HTTP/2 404` → it's portal-only; continue below. +> +> Observed (2026-06-23): `8.18.35.97` → **200**; `8.18.21.4259` → **404**. + +## Why some versions are portal-only + +Mendix prunes older runtime/mxbuild tarballs from the public CDN over time. Those +versions remain available to licensed users through the Mendix Platform (portal), +or are bundled inside the matching Studio Pro install. This repo commits **no +Mendix binary** (`guard.sh`, D-DOCKER-LIB-002) — you bring the licensed tarball; +the crate stays a recipe. + +## The recipe stays the same — only the source URL changes + +Both the runtime crate and the build crate already accept a CDN-base build-arg: + +| Crate | Build-arg | Default | +|---|---|---| +| `mendix-8` (runtime) | `RUNTIME_CDN_BASE` | `https://cdn.mendix.com/runtime` | +| `mendix-8/build` (mxbuild) | `MXBUILD_CDN_BASE` | `https://cdn.mendix.com/runtime` | + +So the portal flow is: **fetch the tarball → serve it over HTTP locally → point the +build-arg at your local host.** The tarball is never committed. + +### 1. Download from the portal (human or agent) + +The runtime tarball must be named `mendix-.tar.gz`; the build toolchain +tarball must be named `mxbuild-.tar.gz` (the crate's curl builds that name +from `MENDIX_VERSION`). + +### Agent download-instructions + +An AI agent with browser automation (e.g. Claude-in-Chrome / Playwright) can obtain +a portal-only tarball as follows. **Stop and ask the operator to log in** — never +script credential entry. + +1. **Authenticate.** Open `https://sprintr.home.mendix.com/` (or + `https://marketplace.mendix.com/`). If not already authenticated, hand control to + the operator to complete Mendix SSO. Do not enter credentials yourself. +2. **Locate the version.** Mendix Studio Pro / runtime archives live under the + Marketplace → "Studio Pro" downloads and the version archive + (`https://marketplace.mendix.com/link/studiopro/`). The runtime tarball + matches `mendix-.tar.gz`; the build toolchain is inside the Studio Pro + package as `modeler/mxbuild` (+ `modeler/mx`). +3. **If only a Studio Pro installer is available** (no standalone tarball): install + Studio Pro ``, then locate its `modeler/` directory and `tar -czf + mxbuild-.tar.gz modeler/` so the archive's top-level is `modeler/` + (the layout the crate's `tar -xzf … -C /opt/mxtools` expects). +4. **Verify the archive shape** before serving it: + ```bash + tar -tzf mxbuild-.tar.gz | grep -m1 'modeler/mxbuild' # build toolchain + tar -tzf mendix-.tar.gz | grep -m1 "/runtime/launcher/runtimelauncher.jar" # runtime + ``` +5. **Record provenance.** Note the portal URL + SHA-256 in the crate's + `provenance.yaml` (`cdn_source_url:` → the portal link, plus a `sha256:` of the + tarball you fetched). + +### 2. Serve the tarball locally + +```bash +mkdir -p /tmp/mxcdn && cp mendix-.tar.gz mxbuild-.tar.gz /tmp/mxcdn/ +( cd /tmp/mxcdn && python3 -m http.server 8000 ) # serves http://localhost:8000/ +``` + +On Docker-for-Mac / Colima the build reaches the host as `host.docker.internal`. + +### 3. Build pointing at your local source + +```bash +# runtime +docker build --platform linux/amd64 \ + --build-arg MENDIX_VERSION= \ + --build-arg RUNTIME_CDN_BASE=http://host.docker.internal:8000 \ + -t ontologylabs/mendix-runtime: crates/mendix-8 + +# build toolchain +docker build --platform linux/amd64 \ + --build-arg MENDIX_VERSION= \ + --build-arg MXBUILD_CDN_BASE=http://host.docker.internal:8000 \ + -t ontologylabs/mendix-mxbuild: crates/mendix-8/build +``` + +The resulting image is identical in shape to a CDN-built one; only the byte source +of the tarball differed. Tear down the local server when done. + +## Guard compliance + +Do **not** `cp` the tarball into `crates/mendix-8/` (the build context) and `git +add` it — `guard.sh` blocks `*.tar.gz` and any file over 256 KB. Keep the tarball +outside the repo (e.g. `/tmp/mxcdn`) and serve it; the repo stays binary-free. diff --git a/crates/mendix-8/README.md b/crates/mendix-8/README.md new file mode 100644 index 0000000..37f78fd --- /dev/null +++ b/crates/mendix-8/README.md @@ -0,0 +1,166 @@ +# Mendix 8 Runtime Crate + +> A model-agnostic, app-agnostic, version-pinned Docker image for running +> any Mendix 8.x application. Bind-mount your unzipped MDA, set a few env +> vars, get a working Mendix runtime. Zero rebuilds, zero dangling images. + +## Why this exists + +The official `cf-mendix-buildpack` Docker pattern bakes the application +model directly into the image. Every model change spawns a new image and +orphans the previous one — typically ~785 MB of dangling layers per +reload. For an iterative dev/test loop that runs reload many times per +day, this consumes tens of GB of disk in a single working session. + +This crate inverts the pattern: + +| | CF-buildpack image | This crate | +|---|---|---| +| Mendix runtime files | baked | baked | +| JRE | baked | baked | +| Application model | **baked** | **bind-mounted** | +| Reload mechanism | `docker build --no-cache` | `docker compose restart` | +| Disk cost per reload | ~785 MB (dangling) | 0 | +| Image-per-Mendix-version | many (one per app commit) | 1 | +| Reusable across apps | no | yes | + +Same Mendix version → same image. Multiple apps at the same version → one +image, multiple bind-mounts. + +## Image tags + +| Tag | What it pins | When to use | +|---|---|---| +| `ontologylabs/mendix-runtime:8.18.35.97` | Exact version (immutable) | Production, repeatable builds | +| `ontologylabs/mendix-runtime:8.18` | Latest 8.18 patch in this crate | Pinned LTS line | +| `ontologylabs/mendix-runtime:8` | Latest 8.x in this crate's release stream | Dev, demo, easy upgrades | + +Related crates: + +* `ontologylabs/mendix-runtime:11.x` — Mendix 11 (see `../mendix-11`) +* `ontologylabs/mendix-runtime:10.x` — Mendix 10 (see `../mendix-10`) + +## Build + +```bash +cd crates/mendix-8 +docker build \ + --platform linux/amd64 \ + --build-arg MENDIX_VERSION=8.18.35.97 \ + -t ontologylabs/mendix-runtime:8.18.35.97 \ + -t ontologylabs/mendix-runtime:8.18 \ + -t ontologylabs/mendix-runtime:8 \ + . +``` + +The base image is `eclipse-temurin:11-jre-jammy`. **Mendix 8.18 LTS is certified +on Java 8 and Java 11**; Java 11 is used here (Mendix Cloud's late-MX8 default; it +runs Java-8 bytecode). For an app that pins Java 8, rebuild on +`eclipse-temurin:8-jre-jammy` via `--build-arg`. Mendix 8 runtime files are +downloaded from `https://cdn.mendix.com/runtime/mendix-${VERSION}.tar.gz` during +build. + +> **Mendix 8 is out of standard support.** The final 8.18 LTS patch +> (`8.18.35.97`, this crate's default) is still served from the public CDN, so it +> builds with no extra steps. Older MX8 patches are **not** all CDN-hosted — for +> those, see [`PORTAL-DOWNLOAD.md`](PORTAL-DOWNLOAD.md). + +## Run + +### docker run (single-shot) + +```bash +docker run -d \ + --name myapp-runtime \ + -p 8080:8080 \ + -p 8090:8090 \ + -v /path/to/unzipped/mda:/opt/mendix/app:ro \ + -v myapp-data:/opt/mendix/data \ + -e DATABASE_ENDPOINT="postgres://mendix:mendix@db:5432/mendix" \ + -e ADMIN_PASSWORD="strong-password-here" \ + -e DEVELOPMENT_MODE="true" \ + --link postgres:db \ + ontologylabs/mendix-runtime:8.18.35.97 +``` + +### docker compose (recommended) + +See `tests/docker-compose.smoke.yml` for a runnable example. + +## Required environment + +| Variable | Purpose | Example | +|---|---|---| +| `ADMIN_PASSWORD` | m2ee admin protocol password (also rejects weak runtime passwords) | `MyStrongPass2026!` | +| `DATABASE_ENDPOINT` | PostgreSQL URL (parsed for host/port/user/pass/db) | `postgres://mendix:mendix@postgres:5432/mendix` | + +## Optional environment + +| Variable | Default | Purpose | +|---|---|---| +| `DEVELOPMENT_MODE` | `false` | If `true`, force DTAPMode=D (bypasses project-security checks like `CheckFormsAndMicroflows`) | +| `DTAPMode` | `D` | One of `D`/`A`/`P` | +| `ADMIN_PORT` | `8090` | m2ee admin protocol HTTP port | +| `RUNTIME_PORT` | `8080` | Mendix client HTTP port | +| `MX_LOG_LEVEL` | `i` | One of `t`/`d`/`i`/`w`/`e` | +| `MXRUNTIME_HttpHeaders` | `[]` | JSON array of `{"Name":"...","Value":"..."}` for CSP / custom headers | +| `MICROFLOW_CONSTANTS` | `{}` | JSON object of `{"Module.Constant":"value"}` | +| `JAVA_OPTS` | `-Xmx1g -Xms512m -Dfile.encoding=UTF-8` | JVM flags | +| `DB_HOST` `DB_PORT` `DB_NAME` `DB_USER` `DB_PASS` | parsed from `DATABASE_ENDPOINT` if set | DB connection (used only if `DATABASE_ENDPOINT` is unset) | + +## Bind-mount contract + +| Container path | Type | Required | Purpose | +|---|---|---|---| +| `/opt/mendix/app` | volume / bind | **yes** | Unzipped MDA contents (must contain `model/metadata.json`) | +| `/opt/mendix/data` | volume / bind | recommended | Writable file storage (file documents, model uploads) | + +The MDA is what `unzip your.mda -d /path/...` produces — a directory with +`model/`, `web/`, `userlib/`, `theme/`, `native/`, `sass/`, `tmp/` at top +level. + +## Ports + +| Port | Purpose | +|---|---| +| 8080 | Mendix client (HTTP) | +| 8090 | m2ee admin protocol — JSON-RPC over HTTP, password = `ADMIN_PASSWORD`, base64-encoded in `X-M2EE-Authentication` header | + +## Health + +```bash +curl -fsSI http://localhost:8080/ # 200 OK once runtime is up +``` + +The container's `HEALTHCHECK` polls `http://localhost:8080/` every 30 s. + +## Security notes + +* Runs as UID 1001 (non-root, OpenShift-compatible). +* `ADMIN_PASSWORD` must be set explicitly — the runtime rejects weak passwords + like `1` or `password` with a null-pointer-exception in `PasswordStrengthVerifier`. +* In production, mount the MDA `:ro` so the container cannot mutate it. + +## Testing + +`tests/smoke-test.sh` brings up postgres + this image with a known-good MDA, +hits `/`, and tears down. CI-able. + +## Provenance + +Built from the official Mendix CDN runtime tarball. No upstream Mendix code +is forked or modified. JRE is upstream Eclipse Temurin. Boot mechanism +(`runtimelauncher.jar` + m2ee admin protocol) is the same protocol Mendix +Cloud uses internally. Each built image records its CDN source URL, Mendix +version, crate version, and image digest in `provenance.yaml`. + +## Versioning + +This crate (the Dockerfile + start.sh + supporting files) is versioned +independently of the Mendix runtime. See `CHANGELOG.md`. + +## License + +Apache 2.0 for the crate scaffolding (Dockerfile, start.sh, README, tests). +The Mendix runtime files baked into the image are subject to Mendix's own +license — see https://www.mendix.com/terms-of-use/ . diff --git a/crates/mendix-8/build/CHANGELOG.md b/crates/mendix-8/build/CHANGELOG.md new file mode 100644 index 0000000..7c96af7 --- /dev/null +++ b/crates/mendix-8/build/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog — Mendix 8 Build Crate + +## [0.1.0] — 2026-06-23 + +Initial release. The build (mxbuild + mx) companion to the mendix-8 runtime crate. + +* `Dockerfile`: model-agnostic; JDK 11 base (`eclipse-temurin:11-jdk-jammy`); + downloads the Mendix 8 build toolchain from the official CDN at build + (`mxbuild-${MENDIX_VERSION}.tar.gz`); never commits a Mendix binary + (recipe-pull, D-DOCKER-LIB-001/002). apt deps for the .NET/Mono toolchain: + `libgdiplus libicu70 libssl3 sqlite3`. +* `build.sh`: `build` / `check` / `version` dispatch. Auto-injects + `--java-home` / `--java-exe-path` / `--gradle-home` / `--loose-version-check` + and defaults `--output=/workspace/.mda`. `mx check` exit codes normalised + to the CI convention (0 clean / 1 errors / 2 warnings). Derived from the + production AIDE aide-mxtools entrypoint. +* Pre-creates `$HOME/.local/share/Mendix` for the MX 11-origin `.NET` mxbuild whitelist + FailFast (same fix as aide-mxtools). +* Default Mendix version: 8.18.35.97 (override via `--build-arg MENDIX_VERSION`). + +### Verification status +* CDN source `mxbuild-8.18.35.97.tar.gz`: **HTTP 200 verified (2026-06-23)**. +* Image build + `mx version` / MDA smoke: **pending** — recorded honestly in + `provenance.yaml` (`smoke_verified: pending`). The mxbuild invocation itself is + the production-proven aide-mxtools path; the remaining gate is a release build. + +### Why +Lets a Docker-only environment (CI, a contributor without Studio Pro) compile and +validate a Mendix 8 app, producing the `.mda` the runtime crate runs. diff --git a/crates/mendix-8/build/Dockerfile b/crates/mendix-8/build/Dockerfile new file mode 100644 index 0000000..6c499ca --- /dev/null +++ b/crates/mendix-8/build/Dockerfile @@ -0,0 +1,113 @@ +# Mendix 8 Build Crate — model-agnostic mxbuild + mx toolchain (recipe-pull). +# +# Image: ontologylabs/mendix-mxbuild:8 +# ontologylabs/mendix-mxbuild:8.18.35.97 (immutable) +# +# Contract: +# * Bake: JDK 11 + the Mendix 8.x build toolchain (mxbuild + mx), pulled from +# the official Mendix CDN at `docker build` time. +# * NEVER commit a Mendix binary to the repo. The toolchain tarball is curl'd +# from cdn.mendix.com on the licensed user's own machine — exactly like the +# runtime crate downloads the runtime tarball (D-DOCKER-LIB-001 recipe-pull; +# D-DOCKER-LIB-002 no-binaries guard). You bring the licensed toolchain; we +# bring the build recipe. +# * Bind-mount the project directory (containing the `.mpr`) at /workspace. +# * Compile : `docker run -v :/workspace ... build /workspace/App.mpr` +# → writes App.mda next to the .mpr in /workspace. +# * Validate: `docker run -v :/workspace ... check /workspace/App.mpr` +# → `mx check` with CI-normalised exit codes (0 clean / 1 error / 2 warn). +# +# Why a "build crate" (companion to the runtime crate): +# The AIDE pipeline drives mxbuild via a bind-mounted Studio Pro modeler +# (the aide-mxtools image). This crate makes the toolchain self-contained and +# version-pinned: one image per Mendix major, the matching mxbuild fetched +# from the CDN at build, so a contributor with only Docker can compile any app +# at that version without a local Studio Pro install. Runtime crate runs the +# app; build crate produces the .mda the runtime crate runs. +# +# Provenance: +# * mxbuild + mx from https://cdn.mendix.com/runtime/mxbuild-${MENDIX_VERSION}.tar.gz +# * Tarball top-level is `modeler/` → modeler/mxbuild, modeler/mx, +# modeler/tools/gradle (verified against the AIDE build-server cache layout). +# * Java: Eclipse Temurin 11 JDK — Mendix 8 compiles Java actions at release 11. +# A full JDK (not JRE) is required: mxbuild invokes javac to compile the +# app's javasource and Java actions. + +FROM --platform=linux/amd64 eclipse-temurin:11-jdk-jammy + +ARG MENDIX_VERSION=8.18.35.97 +ARG MXBUILD_CDN_BASE=https://cdn.mendix.com/runtime + +LABEL ai.ayios.purpose="Mendix 8 build crate (mxbuild + mx, model-agnostic)" +LABEL ai.ayios.crate="mendix-8/build" +LABEL ai.ayios.mendix-version="${MENDIX_VERSION}" +LABEL maintainer="hardy@agileworks.co.za" + +# Layer 1 — apt deps for the mxbuild toolchain. mxbuild is a .NET/Mono program: +# libgdiplus — System.Drawing (image/PDF generation during build) +# libicu70 — globalization/ICU (jammy ships ICU 70) +# libssl3 — TLS for any build-time fetch +# sqlite3 — read the .mpr (modern Mendix projects are SQLite databases) +# curl — pull the toolchain tarball +# Stable layer; survives churn in the heavier toolchain layer below. +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + libgdiplus \ + libicu70 \ + libssl3 \ + sqlite3 \ + && rm -rf /var/lib/apt/lists/* + +# Layer 2 — Mendix build toolchain tarball (mxbuild + mx). ~300-500 MB download. +# Recipe-pull: curl'd from the CDN at build, never committed. Cached unless +# MENDIX_VERSION or MXBUILD_CDN_BASE changes. +# +# Tarball top-level is `modeler/`, so extracting at /opt/mxtools produces the +# same layout the AIDE build-server caches and mounts at /opt/mxtools: +# /opt/mxtools/modeler/mxbuild +# /opt/mxtools/modeler/mx +# /opt/mxtools/modeler/tools/gradle +RUN set -eux; \ + mkdir -p /opt/mxtools /workspace; \ + echo "Downloading Mendix build toolchain ${MENDIX_VERSION} from ${MXBUILD_CDN_BASE}..."; \ + curl -fsSL "${MXBUILD_CDN_BASE}/mxbuild-${MENDIX_VERSION}.tar.gz" -o /tmp/mxbuild.tar.gz; \ + echo "Extracting..."; \ + tar -xzf /tmp/mxbuild.tar.gz -C /opt/mxtools; \ + rm /tmp/mxbuild.tar.gz; \ + test -f /opt/mxtools/modeler/mxbuild || { \ + echo "FATAL: modeler/mxbuild not found at expected path /opt/mxtools/modeler/mxbuild."; \ + echo "Tarball top-level may have changed. Inspecting:"; \ + find /opt/mxtools -maxdepth 3 -type f -name 'mx*'; \ + exit 1; \ + }; \ + chmod +x /opt/mxtools/modeler/mxbuild /opt/mxtools/modeler/mx 2>/dev/null || true; \ + echo "Mendix ${MENDIX_VERSION} build toolchain ready at /opt/mxtools/modeler/" + +# Mendix mxbuild — MX 11's .NET toolchain in particular — creates settings under +# $HOME/.local/share/Mendix via GetFolderPath(LocalApplicationData). In a fresh +# container that directory does not exist, and mxbuild's WhitelistAware +# filesystem FailFast-rejects *creating* it ("UnauthorizedAccessException … +# EnsurePathSafety") before the build starts. Pre-create it for HOME=/root so +# mxbuild writes settings without tripping the create-time whitelist check (a +# harmless no-op on older Mono-based mxbuild). Proven in AIDE aide-mxtools. +RUN mkdir -p /root/.local/share/Mendix /root/.config/Mendix /root/.cache/Mendix + +# Crate entrypoint. +COPY build.sh /opt/mxtools/build.sh +RUN chmod +x /opt/mxtools/build.sh + +# Runs as root by design — an ephemeral, single-shot build container (not a +# long-running service). Root avoids the bind-mount uid-mismatch that would +# otherwise block writing the produced .mda back into the operator's mounted +# /workspace. On a Linux host where root-owned output is undesirable, pass +# `--user $(id -u)`; build.sh defensively re-creates the Mendix settings dir +# under whatever $HOME resolves to. +ENV JAVA_HOME=/opt/java/openjdk \ + MENDIX_VERSION=${MENDIX_VERSION} + +VOLUME ["/workspace"] +WORKDIR /workspace + +ENTRYPOINT ["/opt/mxtools/build.sh"] +CMD ["--help"] diff --git a/crates/mendix-8/build/README.md b/crates/mendix-8/build/README.md new file mode 100644 index 0000000..c064ef6 --- /dev/null +++ b/crates/mendix-8/build/README.md @@ -0,0 +1,106 @@ +# Mendix 8 Build Crate + +> A model-agnostic, version-pinned Docker image that compiles **any** Mendix 8.x +> project (`.mpr`) into a deployable `.mda` — and runs `mx check` — with nothing +> on the host but Docker. The Mendix build toolchain (`mxbuild` + `mx`) is pulled +> from the official Mendix CDN at build time; no Studio Pro install, no committed +> binaries. + +This is the **build** companion to the [`mendix-8` runtime crate](../). The build +crate *produces* the `.mda`; the runtime crate *runs* it. + +## Why this exists + +The AIDE pipeline compiles models with `mxbuild` driven from a bind-mounted Studio +Pro install. That requires a licensed modeler on the host. This crate makes the +toolchain self-contained and version-pinned: one image per Mendix major, the +matching `mxbuild` fetched from `cdn.mendix.com` at `docker build` — so a CI runner +or a contributor with only Docker can compile a Mendix 8 app without Studio Pro. + +| | Studio Pro on host | aide-mxtools (bind-mount) | This build crate | +|---|---|---|---| +| Needs Studio Pro installed | yes | yes (modeler bind-mounted) | **no** | +| Toolchain source | local install | local install | **CDN at build** | +| Version pinning | manual | manual | **one image per version** | +| Runs in plain CI | no | partial | **yes** | + +## Image tags + +| Tag | Pins | Use | +|---|---|---| +| `ontologylabs/mendix-mxbuild:8.18.35.97` | Exact version (immutable) | Reproducible CI | +| `ontologylabs/mendix-mxbuild:8` | Latest 8.x in this crate | Dev, demo | + +## Build the image + +```bash +cd crates/mendix-8/build +docker build \ + --platform linux/amd64 \ + --build-arg MENDIX_VERSION=8.18.35.97 \ + -t ontologylabs/mendix-mxbuild:8.18.35.97 . +# mxbuild + mx are pulled from cdn.mendix.com/runtime/mxbuild-8.18.35.97.tar.gz at build. +``` + +## Compile an app + +```bash +# /path/to/project contains App.mpr +docker run --rm \ + --platform linux/amd64 \ + -v /path/to/project:/workspace \ + ontologylabs/mendix-mxbuild:8.18.35.97 \ + build /workspace/App.mpr +# → writes /path/to/project/App.mda +``` + +Then run it with the runtime crate: + +```bash +unzip /path/to/project/App.mda -d /path/to/project/app +cd ../ # the mendix-8 runtime crate +docker compose -f tests/docker-compose.smoke.yml up # or your own compose +``` + +## Validate a model (`mx check`) + +```bash +docker run --rm -v /path/to/project:/workspace \ + ontologylabs/mendix-mxbuild:8.18.35.97 \ + check /workspace/App.mpr +# exit 0 = clean · 1 = errors · 2 = warnings only +``` + +## What the entrypoint auto-injects + +`build.sh` supplies the toolchain paths `mxbuild` needs unless you pass them: + +* `--java-home` / `--java-exe-path` → the baked JDK (`$JAVA_HOME`) +* `--gradle-home` → the toolchain's bundled Gradle +* `--loose-version-check` → tolerate patch drift between toolchain and model +* `--output=/workspace/.mda` → default output beside the `.mpr` + +Pass any of these explicitly to override. + +## Notes & gotchas + +* **Runs as root by design.** A single-shot build container, not a service. Root + avoids the bind-mount uid mismatch that would block writing the `.mda` back into + your mounted `/workspace`. On Linux, pass `--user $(id -u)` if you want + host-uid-owned output; the entrypoint re-creates the Mendix settings dir under + the resulting `$HOME`. +* **`.NET` whitelist (MX 11 origin).** mxbuild creates settings under + `$HOME/.local/share/Mendix` and FailFast-rejects *creating* that path itself; the + image pre-creates it. (Same fix as the AIDE aide-mxtools image.) +* **amd64 emulation.** On Apple Silicon, `mxbuild` runs x86_64 under emulation. + Enable Colima Rosetta (`colima start --vm-type vz --vz-rosetta`) for speed; a + hung emulated build is the classic symptom of Rosetta being off. +* **No Mendix binary is committed.** The toolchain is fetched from the CDN at build, + on your licensed machine (D-DOCKER-LIB-002). See repo `guard.sh`. +* **Mendix 8 is out of standard support.** The default `8.18.35.97` mxbuild is + CDN-hosted. For an older MX8 patch the CDN doesn't host, obtain the toolchain via + the Mendix portal and override `MXBUILD_CDN_BASE` — see + [`../PORTAL-DOWNLOAD.md`](../PORTAL-DOWNLOAD.md). + +See [`provenance.yaml`](provenance.yaml) for the exact CDN source and +[`versions.yaml`](versions.yaml) for verified versions. diff --git a/crates/mendix-8/build/build.sh b/crates/mendix-8/build/build.sh new file mode 100644 index 0000000..55beda6 --- /dev/null +++ b/crates/mendix-8/build/build.sh @@ -0,0 +1,134 @@ +#!/bin/bash +# Mendix Build Crate — entrypoint. Dispatches to mxbuild / mx from the toolchain +# baked at /opt/mxtools/modeler (pulled from the Mendix CDN at image build). +# +# Commands: +# build [mxbuild-options] Compile an .mpr → .mda +# check [mx-check-options] Run `mx check` (CI-normalised exit) +# version Print the mx toolchain version +# --help Usage +# +# Mount: -v :/workspace (the dir containing your App.mpr) +# Output: build writes .mda into /workspace next to the .mpr. +# +# Derived from the AIDE aide-mxtools entrypoint (production-proven mxbuild +# invocation) — same find-tool + auto-inject(--java-home/--gradle-home/ +# --loose-version-check) logic, retargeted at the baked /opt/mxtools/modeler. + +set -euo pipefail + +MXTOOLS_DIR="/opt/mxtools" + +# Defensive: re-create the Mendix settings dir under whatever HOME resolves to. +# The image bakes these for HOME=/root; this covers a runtime `--user`/`-e HOME=` +# override. MX 11's .NET mxbuild FailFast-rejects creating these; harmless elsewhere. Idempotent. +mkdir -p "${HOME:-/root}/.local/share/Mendix" \ + "${HOME:-/root}/.config/Mendix" \ + "${HOME:-/root}/.cache/Mendix" 2>/dev/null || true + +# find_tool — locate mxbuild/mx in the baked toolchain. Strict candidates +# first (the `modeler/` layout the CDN tarball extracts to), then a glob fallback +# so a future tarball-layout change degrades gracefully instead of failing hard. +find_tool() { + local tool_name="$1" + for candidate in \ + "${MXTOOLS_DIR}/modeler/${tool_name}" \ + "${MXTOOLS_DIR}/${tool_name}" \ + "${MXTOOLS_DIR}/runtime/${tool_name}" \ + "${MXTOOLS_DIR}/tools/${tool_name}"; do + if [ -x "$candidate" ]; then echo "$candidate"; return 0; fi + done + local found + found=$(find "${MXTOOLS_DIR}" -name "${tool_name}" -type f -executable 2>/dev/null | head -1) + [ -n "$found" ] && { echo "$found"; return 0; } + return 1 +} + +case "${1:-}" in + build) + shift + [ $# -ge 1 ] || { echo "Usage: build [mxbuild-options]" >&2; exit 1; } + MXBUILD_BIN=$(find_tool "mxbuild") || { echo "ERROR: mxbuild not found under ${MXTOOLS_DIR}" >&2; exit 1; } + echo "Using: ${MXBUILD_BIN}" >&2 + + # Auto-inject the toolchain paths mxbuild needs, unless the caller set them. + EXTRA_ARGS=() + HAS_JAVA_HOME=false; HAS_GRADLE_HOME=false; HAS_LOOSE=false; HAS_OUTPUT=false + for arg in "$@"; do + case "$arg" in + --java-home=*) HAS_JAVA_HOME=true ;; + --gradle-home=*) HAS_GRADLE_HOME=true ;; + --loose-version-check) HAS_LOOSE=true ;; + --output=*|--output) HAS_OUTPUT=true ;; + esac + done + if [ "$HAS_JAVA_HOME" = false ] && [ -n "${JAVA_HOME:-}" ]; then + EXTRA_ARGS+=(--java-home="${JAVA_HOME}" --java-exe-path="${JAVA_HOME}/bin/java") + fi + if [ "$HAS_GRADLE_HOME" = false ]; then + for g in "${MXTOOLS_DIR}/modeler/tools/gradle" "${MXTOOLS_DIR}/tools/gradle"; do + [ -d "$g" ] && { EXTRA_ARGS+=(--gradle-home="$g"); break; } + done + fi + # Tolerate patch-version drift between the toolchain and the model + # (SDK commits may bump the model's product version). + [ "$HAS_LOOSE" = false ] && EXTRA_ARGS+=(--loose-version-check) + + # Default the output beside the .mpr in /workspace if the caller didn't + # pass --output. mpr-path is the first positional after `build`. + if [ "$HAS_OUTPUT" = false ]; then + MPR_PATH="$1" + MPR_BASE="$(basename "${MPR_PATH%.mpr}")" + EXTRA_ARGS+=(--output="/workspace/${MPR_BASE}.mda") + fi + + exec "$MXBUILD_BIN" "${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}" "$@" + ;; + + check) + shift + [ $# -ge 1 ] || { echo "Usage: check [mx-check-options]" >&2; exit 1; } + MX_BIN=$(find_tool "mx") || { echo "ERROR: mx not found under ${MXTOOLS_DIR}" >&2; exit 1; } + echo "Using: ${MX_BIN}" >&2 + # mx check returns an OR'd bitmask: 1=errors, 2=warnings, 4=deprecations. + # Normalise to the CI convention: 0=clean, 1=errors, 2=warnings-only. + set +e + "$MX_BIN" check "$@" + MX_EXIT=$? + set -e + if [ $((MX_EXIT & 1)) -ne 0 ]; then exit 1 + elif [ $((MX_EXIT & 2)) -ne 0 ]; then exit 2 + else exit 0 + fi + ;; + + version) + # mx has no toolchain --version flag; the pinned version is the image's + # MENDIX_VERSION. (mx show-* verbs report an *app's* version, not the toolchain's.) + echo "Mendix build crate — toolchain version ${MENDIX_VERSION:-unknown} (mxbuild + mx)" + ;; + + --help|help|"") + cat <<'USAGE' +Mendix Build Crate — mxbuild + mx in a version-pinned container + +Commands: + build [options] Compile an .mpr → .mda (output → /workspace) + check [options] Run mx check (exit 0=clean, 1=errors, 2=warnings) + version Show the mx toolchain version + +Mount: + -v :/workspace Directory containing your App.mpr + +Examples: + docker run --rm -v "$PWD":/workspace ontologylabs/mendix-mxbuild:8 build /workspace/App.mpr + docker run --rm -v "$PWD":/workspace ontologylabs/mendix-mxbuild:8 check /workspace/App.mpr +USAGE + ;; + + *) + echo "Unknown command: ${1:-}" >&2 + echo "Run with --help for usage." >&2 + exit 1 + ;; +esac diff --git a/crates/mendix-8/build/provenance.yaml b/crates/mendix-8/build/provenance.yaml new file mode 100644 index 0000000..1551fb7 --- /dev/null +++ b/crates/mendix-8/build/provenance.yaml @@ -0,0 +1,31 @@ +# Per-image provenance for the mendix-8 BUILD crate (mxbuild + mx). +# Recipe-pull distribution model (D-DOCKER-LIB-001): no registry, no published +# Mendix binaries — the toolchain tarball is curl'd from the Mendix CDN at build. +# +# Stable provenance anchor: cdn_source_url (anyone can re-fetch + verify the same +# tarball). image_digest is the local image-config ID for a given build; it is +# build-environment-specific (apt package versions drift) and recorded for +# traceability, not byte-for-byte reproducibility. It is populated once the image +# has been built + smoke-verified on a release machine. + +crate: mendix-8/build +crate_version: "0.1.0" + +images: + - mendix_version: "8.18.35.97" + cdn_source_url: "https://cdn.mendix.com/runtime/mxbuild-8.18.35.97.tar.gz" + base_image: "eclipse-temurin:11-jdk-jammy" + java_version: 11 + platform: "linux/amd64" + image_tags: + - "ontologylabs/mendix-mxbuild:8.18.35.97" + - "ontologylabs/mendix-mxbuild:8" + image_digest: null # filled after a verified release build + cdn_verified: "2026-06-23 — HTTP 200 (HEAD)" + smoke_verified: "pending" + notes: > + Recipe authored from the production AIDE aide-mxtools image (proven mxbuild + invocation: --java-home / --java-exe-path / --gradle-home / --loose-version-check) + and the verified runtime-crate recipe-pull pattern. The CDN source URL is + verified reachable; an image build + `mx version`/MDA smoke is the remaining + release gate. diff --git a/crates/mendix-8/build/tests/smoke-test.sh b/crates/mendix-8/build/tests/smoke-test.sh new file mode 100644 index 0000000..0d3fe94 --- /dev/null +++ b/crates/mendix-8/build/tests/smoke-test.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Smoke test for the Mendix 8 build crate. +# +# Compiles a Mendix 8 project (.mpr) into an .mda using the build-crate image, +# then asserts the .mda was produced. Exits 0 on pass, non-zero on fail. +# +# Usage: ./smoke-test.sh /path/to/project/App.mpr +# +# You supply the licensed .mpr — none is committed to this repo (guard.sh blocks +# .mpr/.mda/.tar.gz patterns; D-DOCKER-LIB-002). The image's mxbuild toolchain is +# pulled from the Mendix CDN at `docker build`. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +BUILD_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +MENDIX_VERSION="${MENDIX_VERSION:-8.18.35.97}" +IMAGE="${BUILD_IMAGE:-ontologylabs/mendix-mxbuild:${MENDIX_VERSION}}" + +MPR_PATH="${1:-}" +if [ -z "${MPR_PATH}" ] || [ ! -f "${MPR_PATH}" ]; then + echo "[FATAL] Pass the path to a Mendix 8 .mpr as the first argument." + echo " e.g. $0 /path/to/MyApp/MyApp.mpr" + exit 78 +fi + +# Colima mounts only \$HOME into the VM; a project outside \$HOME is invisible to +# the container. Warn rather than silently mount an empty dir. +case "${MPR_PATH}" in + "${HOME}"/*) : ;; + *) echo "[WARN] ${MPR_PATH} is not under \$HOME — on Colima it may be invisible to the container." ;; +esac + +PROJECT_DIR="$(cd "$(dirname "${MPR_PATH}")" && pwd)" +MPR_NAME="$(basename "${MPR_PATH}")" +MDA_NAME="$(basename "${MPR_NAME%.mpr}").mda" + +# Build the image if absent. +if ! docker image inspect "${IMAGE}" >/dev/null 2>&1; then + echo "=== Building image ${IMAGE} (mxbuild pulled from CDN) ===" + docker build --platform linux/amd64 \ + --build-arg MENDIX_VERSION="${MENDIX_VERSION}" \ + -t "${IMAGE}" "${BUILD_DIR}" +fi + +echo "=== Compiling ${MPR_NAME} → ${MDA_NAME} ===" +rm -f "${PROJECT_DIR}/${MDA_NAME}" +docker run --rm --platform linux/amd64 \ + -v "${PROJECT_DIR}:/workspace" \ + "${IMAGE}" \ + build "/workspace/${MPR_NAME}" + +if [ -f "${PROJECT_DIR}/${MDA_NAME}" ]; then + SIZE=$(du -h "${PROJECT_DIR}/${MDA_NAME}" | cut -f1) + echo "=== PASS — ${MDA_NAME} produced (${SIZE}) ===" + exit 0 +fi + +echo "=== FAIL — ${MDA_NAME} was not produced. Inspect mxbuild output above. ===" +exit 1 diff --git a/crates/mendix-8/build/versions.yaml b/crates/mendix-8/build/versions.yaml new file mode 100644 index 0000000..7f9e312 --- /dev/null +++ b/crates/mendix-8/build/versions.yaml @@ -0,0 +1,18 @@ +# Mendix v8 build-toolchain versions verified against this crate. +# When adding a new version: build the image, smoke-test (build a known .mpr + +# mx check), then add the row. The build toolchain (mxbuild + mx) ships on the +# same CDN as the runtime, as mxbuild-.tar.gz. + +crate_version: "0.1.0" +default_mendix_version: "8.18.35.97" + +supported: + - mendix_version: "8.18.35.97" + cdn_url: "https://cdn.mendix.com/runtime/mxbuild-8.18.35.97.tar.gz" + java_version: 11 # JDK (mxbuild compiles javasource at release 11) + cdn_status: "200 (verified 2026-06-23)" + image_smoke: "pending" # image build + `mx version` not yet run in CI + notes: "Pairs with the mendix-8 runtime crate (same default version)." + +planned: + - mendix_version: "8.18.21.4259" # portal-only (not on CDN); see ../PORTAL-DOWNLOAD.md diff --git a/crates/mendix-8/provenance.yaml b/crates/mendix-8/provenance.yaml new file mode 100644 index 0000000..b63c446 --- /dev/null +++ b/crates/mendix-8/provenance.yaml @@ -0,0 +1,30 @@ +# Per-image provenance for the mendix-8 runtime crate. +# Recipe-pull distribution model (D-DOCKER-LIB-001): no registry, no published +# Mendix binaries — the runtime tarball is curled from the Mendix CDN at build. +# +# Stable provenance anchor: cdn_source_url (anyone can re-fetch + verify the same +# tarball). image_digest is the local image-config ID for a given build; it is +# build-environment-specific (apt package versions drift) and is populated once +# the image has been built + smoke-verified on a release machine. + +crate: mendix-8 +crate_version: "0.1.0" + +images: + - mendix_version: "8.18.35.97" + cdn_source_url: "https://cdn.mendix.com/runtime/mendix-8.18.35.97.tar.gz" + base_image: "eclipse-temurin:11-jre-jammy" + java_version: 11 + platform: "linux/amd64" + image_tags: + - "ontologylabs/mendix-runtime:8.18.35.97" + - "ontologylabs/mendix-runtime:8.18" + - "ontologylabs/mendix-runtime:8" + image_digest: null # filled after a verified release build + cdn_verified: "2026-06-23 — HTTP 200 (HEAD)" + smoke_verified: "pending" # no MX8 fixture app in the corpus yet + notes: > + Final Mendix 8.18 LTS patch. Recipe mirrors the mendix-9 runtime crate + (same Java 11 base, same start.sh m2ee boot path). The CDN source URL is + verified reachable; an image build + MDA smoke against a real MX8 app is + the remaining release gate. diff --git a/crates/mendix-8/start.sh b/crates/mendix-8/start.sh new file mode 100644 index 0000000..468cffa --- /dev/null +++ b/crates/mendix-8/start.sh @@ -0,0 +1,333 @@ +#!/bin/bash +# Mendix 8 Runtime Crate — entrypoint +# +# Boot sequence: +# 1. Validate inputs (DB endpoint reachable, ADMIN_PASSWORD set, MDA mounted). +# 2. Launch runtimelauncher.jar (the JVM) — it opens admin port 8090 first. +# 3. Wait for admin port to become healthy. +# 4. Drive m2ee admin protocol: configure logging → app container → DB + +# runtime config → start. +# 5. If start returns non-zero (e.g. DDL sync needed on first run), execute +# execute_ddl_commands and retry start. +# +# Trap SIGTERM/SIGINT to forward to the JVM for clean shutdown. + +set -euo pipefail + +APP_DIR="/opt/mendix/app" +RUNTIMES_DIR="/opt/mendix/runtimes" +DATA_DIR="/opt/mendix/data" +ADMIN_PORT="${ADMIN_PORT:-8090}" +RUNTIME_PORT="${RUNTIME_PORT:-8080}" +ADMIN_PASSWORD="${ADMIN_PASSWORD:-}" +MX_LOG_LEVEL="${MX_LOG_LEVEL:-i}" + +echo "============================================================" +echo " Mendix 8 Runtime Crate" +echo " ontologylabs/mendix-runtime" +echo " App: ${APP_DIR} Runtimes: ${RUNTIMES_DIR}" +echo "============================================================" + +# --- Input validation (fail loudly with actionable messages) ----------------- + +if [ -z "${ADMIN_PASSWORD}" ]; then + echo "[FATAL] ADMIN_PASSWORD env var is unset or empty." + echo " Set it via docker-compose env or `docker run -e ADMIN_PASSWORD=...`" + echo " (The Mendix runtime rejects weak passwords like '1' or 'password')" + exit 78 +fi + +if [ ! -f "${APP_DIR}/model/metadata.json" ]; then + echo "[FATAL] No MDA model found at ${APP_DIR}/model/metadata.json" + echo " The crate expects an unzipped MDA bind-mounted at ${APP_DIR}." + echo " On the host: unzip your.mda -d /path/to/project/ &&" + echo " docker run -v /path/to/project:/opt/mendix/app ..." + exit 78 +fi + +DEPLOYED_VERSION=$(jq -r '.RuntimeVersion // "unknown"' \ + "${APP_DIR}/model/metadata.json" 2>/dev/null || echo "unknown") + +# List the Mendix runtime versions baked into this crate. The expected layout is +# /opt/mendix/runtimes//runtime/launcher/runtimelauncher.jar — one +# subdirectory per supported version. The crate ships with at least one. +BAKED_VERSIONS=$(ls "${RUNTIMES_DIR}/" 2>/dev/null | sort -V) +echo "Deployed model RuntimeVersion: ${DEPLOYED_VERSION}" +echo "Crate baked versions: $(echo "${BAKED_VERSIONS}" | tr '\n' ' ')" + +# Resolve the runtime path. Strict match first; then fall back to a same-major +# match if the deployed patch isn't baked (Mendix runtime is patch-stable +# within a major across small ranges). +RUNTIME_DIR="" +if [ -d "${RUNTIMES_DIR}/${DEPLOYED_VERSION}/runtime" ]; then + RUNTIME_DIR="${RUNTIMES_DIR}/${DEPLOYED_VERSION}/runtime" + RESOLVED_VERSION="${DEPLOYED_VERSION}" +else + DEPLOYED_MAJOR="${DEPLOYED_VERSION%%.*}" + for v in ${BAKED_VERSIONS}; do + if [ "${v%%.*}" = "${DEPLOYED_MAJOR}" ]; then + RUNTIME_DIR="${RUNTIMES_DIR}/${v}/runtime" + RESOLVED_VERSION="${v}" + echo "[WARN] Exact runtime ${DEPLOYED_VERSION} not baked. Falling back to same-major ${v}." + break + fi + done +fi + +if [ -z "${RUNTIME_DIR}" ] || [ ! -f "${RUNTIME_DIR}/launcher/runtimelauncher.jar" ]; then + echo "[FATAL] No baked Mendix runtime matches deployed model version ${DEPLOYED_VERSION}." + echo " Baked versions: $(echo "${BAKED_VERSIONS}" | tr '\n' ' ')" + echo " Either rebuild the crate with --build-arg MENDIX_VERSION=${DEPLOYED_VERSION}" + echo " or bake additional versions into the crate image." + exit 78 +fi + +echo "Resolved runtime: ${RUNTIME_DIR}" + +# m2ee admin protocol expects MX_INSTALL_PATH to point at the runtime root +# (the dir containing `runtime/`). The launcher reads MX_INSTALL_PATH to +# locate runtime classes, mxclientsystem assets, and the launcher's own jar. +export MX_INSTALL_PATH="${RUNTIMES_DIR}/${RESOLVED_VERSION}" +export M2EE_ADMIN_PORT="${ADMIN_PORT}" +export M2EE_ADMIN_PASS="${ADMIN_PASSWORD}" + +# Writable dirs the runtime expects under APP_DIR. We ensure these exist on +# the bind-mounted volume, but writes here only persist if the operator +# bind-mounts a real writable host path or a docker volume. +mkdir -p "${APP_DIR}/data/database" \ + "${APP_DIR}/data/files" \ + "${APP_DIR}/data/tmp" \ + "${APP_DIR}/data/model-upload" \ + 2>/dev/null || true + +# --- Database wait ----------------------------------------------------------- + +DB_HOST_DEFAULT="postgres" +DB_PORT_DEFAULT="5432" + +if [ -n "${DATABASE_ENDPOINT:-}" ]; then + # Parse postgres://user:pass@host:port/db into vars we'll use below. + # Parse postgres://[user[:pass]@]host[:port][/db] in pure shell. The + # regex-based form below is sufficient for the canonical postgres URL + # shape; we fall back to defaults for any field that doesn't match. + # No external JSON/URL parser needed — keeps the crate dependency-free + # beyond curl + jq. + URL="${DATABASE_ENDPOINT#*://}" # strip scheme + URL_USERINFO="" + if [[ "${URL}" == *"@"* ]]; then + URL_USERINFO="${URL%%@*}" + URL="${URL#*@}" + fi + URL_HOSTPORT="${URL%%/*}" + URL_PATH="" + [[ "${URL}" == */* ]] && URL_PATH="${URL#*/}" + + DB_HOST="${URL_HOSTPORT%%:*}" + if [[ "${URL_HOSTPORT}" == *:* ]]; then + DB_PORT="${URL_HOSTPORT##*:}" + else + DB_PORT="${DB_PORT_DEFAULT}" + fi + DB_NAME="${URL_PATH%%[?&]*}" + [ -z "${DB_NAME}" ] && DB_NAME="mendix" + + if [ -n "${URL_USERINFO}" ]; then + DB_USER="${URL_USERINFO%%:*}" + if [[ "${URL_USERINFO}" == *:* ]]; then + DB_PASS="${URL_USERINFO#*:}" + else + DB_PASS="mendix" + fi + else + DB_USER="mendix" + DB_PASS="mendix" + fi + [ -z "${DB_HOST}" ] && DB_HOST="${DB_HOST_DEFAULT}" +else + DB_HOST="${DB_HOST:-${DB_HOST_DEFAULT}}" + DB_PORT="${DB_PORT:-${DB_PORT_DEFAULT}}" + DB_NAME="${DB_NAME:-mendix}" + DB_USER="${DB_USER:-mendix}" + DB_PASS="${DB_PASS:-mendix}" +fi + +echo "Waiting for PostgreSQL at ${DB_HOST}:${DB_PORT}..." +DB_READY=0 +for _ in $(seq 1 60); do + if timeout 2 bash -c "echo > /dev/tcp/${DB_HOST}/${DB_PORT}" 2>/dev/null; then + DB_READY=1 + break + fi + sleep 2 +done +if [ "${DB_READY}" != "1" ]; then + echo "[FATAL] PostgreSQL did not become reachable at ${DB_HOST}:${DB_PORT} after 120s." + exit 75 +fi +echo "PostgreSQL ready." + +# --- Launch runtimelauncher.jar --------------------------------------------- + +# Java options can be overridden via JAVA_OPTS; sane defaults for dev/demo. +JAVA_OPTS="${JAVA_OPTS:--Xmx1g -Xms512m -Dfile.encoding=UTF-8}" + +echo "Starting JVM (m2ee admin port ${ADMIN_PORT}, app port ${RUNTIME_PORT})..." +# shellcheck disable=SC2086 +java ${JAVA_OPTS} \ + -jar "${RUNTIME_DIR}/launcher/runtimelauncher.jar" \ + "${APP_DIR}" & +JVM_PID=$! + +trap "echo 'Shutting down...'; kill ${JVM_PID} 2>/dev/null; wait ${JVM_PID} 2>/dev/null; exit 0" SIGTERM SIGINT + +# --- Wait for admin port ----------------------------------------------------- + +AUTH_B64=$(echo -n "${ADMIN_PASSWORD}" | base64) + +m2ee() { + # JSON-RPC-ish over HTTP to the m2ee admin port. Body is + # {"action":"","params":}. Returns the raw response body. + curl -s "http://[::1]:${ADMIN_PORT}/" \ + -H "Content-Type: application/json" \ + -H "X-M2EE-Authentication: ${AUTH_B64}" \ + -d "{\"action\":\"$1\",\"params\":$2}" +} + +ADMIN_READY=0 +for _ in $(seq 1 60); do + if curl -sf "http://[::1]:${ADMIN_PORT}/" \ + -H "Content-Type: application/json" \ + -H "X-M2EE-Authentication: ${AUTH_B64}" \ + -d '{"action":"runtime_status","params":{}}' >/dev/null 2>&1; then + ADMIN_READY=1 + break + fi + if ! kill -0 ${JVM_PID} 2>/dev/null; then + echo "[FATAL] JVM exited before admin port came up. Check logs above." + exit 70 + fi + sleep 2 +done +if [ "${ADMIN_READY}" != "1" ]; then + echo "[FATAL] m2ee admin port ${ADMIN_PORT} did not respond after 120s." + kill ${JVM_PID} 2>/dev/null || true + exit 71 +fi +echo "Admin port ready." + +# --- Configure + start via m2ee -------------------------------------------- + +JDBC="jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}" +HTTP_HEADERS="${MXRUNTIME_HttpHeaders:-[]}" +DTAP_MODE="${DTAPMode:-D}" + +# MICROFLOW_CONSTANTS — JSON object of {"Module.Constant": "value"} that +# overrides defaults baked into the model. Empty (or unset) means we DO +# NOT send a MicroflowConstants update, so the runtime falls back to the +# model's defaultValue for each constant. Sending an empty object {} in +# update_configuration would clobber those defaults — verified empirically +# (Claudius `FeedbackModule.LocalStorageKey` returned "Could not find +# value for constant" with `MicroflowConstants:{}`). +# +# Resolution order: +# 1. Operator-supplied MICROFLOW_CONSTANTS env (validated JSON) — highest +# precedence; lets operators override any value. +# 2. Otherwise, build the constants map from the deployed model's OWN default +# values (metadata.json Constants[].DefaultValue). The MX runtime does NOT +# auto-apply model defaults when MicroflowConstants is absent — that is the +# cf-buildpack's job. Verified on MX9 (FigWarehouse): omitting the field +# yields "Could not find value for constant 'MoneyworksDatabase.Devkey'" +# even though the model declares a default. Building from metadata makes +# the crate self-sufficient (it boots any app whose constants have model +# defaults, with zero operator config). Values are the string forms Mendix +# stores; the runtime coerces each per its declared type. +CONSTANTS_OPTIONAL_FIELD="" +CONSTANTS_JSON="" +if [ -n "${MICROFLOW_CONSTANTS:-}" ] && [ "${MICROFLOW_CONSTANTS}" != "{}" ]; then + # Validate JSON shape before sending — bad JSON would fail + # update_configuration silently and produce confusing downstream errors. + if echo "${MICROFLOW_CONSTANTS}" | jq -e . >/dev/null 2>&1; then + CONSTANTS_JSON="${MICROFLOW_CONSTANTS}" + echo "Applying operator-supplied MICROFLOW_CONSTANTS overrides." + else + echo "[WARN] MICROFLOW_CONSTANTS is set but not valid JSON. Falling back to model defaults." + fi +fi +if [ -z "${CONSTANTS_JSON}" ] && [ -f "${APP_DIR}/model/metadata.json" ]; then + CONSTANTS_JSON=$(jq -c '[.Constants[]? | select(.DefaultValue != null) | {(.Name): (.DefaultValue|tostring)}] | add // {}' \ + "${APP_DIR}/model/metadata.json" 2>/dev/null || echo "{}") + NCONST=$(echo "${CONSTANTS_JSON}" | jq 'length' 2>/dev/null || echo 0) + [ "${NCONST:-0}" -gt 0 ] && echo "Applying ${NCONST} model-default constant value(s) from metadata.json." +fi +if [ -n "${CONSTANTS_JSON}" ] && [ "${CONSTANTS_JSON}" != "{}" ] && [ "${CONSTANTS_JSON}" != "null" ]; then + CONSTANTS_OPTIONAL_FIELD=",\"MicroflowConstants\":${CONSTANTS_JSON}" +fi + +# Optional development-mode bypass — set DEVELOPMENT_MODE=true to relax +# project-security checks (CheckFormsAndMicroflows / CheckEverything). +# Mendix 11 with strict project security refuses to load under DTAP=A/P +# unless DEVELOPMENT_MODE is explicitly set. +if [ "${DEVELOPMENT_MODE:-false}" = "true" ]; then + DTAP_MODE="D" +fi + +# 1. Logging — wire a console subscriber at INFO so JVM logs appear on stdout. +m2ee create_log_subscriber \ + '{"name":"ConsoleSubscriber","type":"console","autosubscribe":"INFO"}' >/dev/null +m2ee start_logging '{}' >/dev/null + +# 2. App container — bind to the runtime port on all addresses. +m2ee update_appcontainer_configuration \ + "{\"runtime_port\":${RUNTIME_PORT},\"runtime_listen_addresses\":\"*\"}" >/dev/null + +# 3. Full configuration — DB, paths, optional constants, custom HTTP headers. +# MicroflowConstants is omitted entirely if MICROFLOW_CONSTANTS is unset or +# empty — see CONSTANTS_OPTIONAL_FIELD construction above. +m2ee update_configuration "{\ +\"DatabaseType\":\"POSTGRESQL\",\ +\"DatabaseJdbcUrl\":\"${JDBC}\",\ +\"DatabaseHost\":\"${DB_HOST}:${DB_PORT}\",\ +\"DatabaseName\":\"${DB_NAME}\",\ +\"DatabaseUserName\":\"${DB_USER}\",\ +\"DatabasePassword\":\"${DB_PASS}\",\ +\"DTAPMode\":\"${DTAP_MODE}\",\ +\"ScheduledEventExecution\":\"NONE\",\ +\"EnableFileDocumentCaching\":false,\ +\"BasePath\":\"${APP_DIR}\",\ +\"MxClientSystemPath\":\"${RUNTIME_DIR}/mxclientsystem\",\ +\"RuntimePath\":\"${RUNTIME_DIR}\"${CONSTANTS_OPTIONAL_FIELD},\ +\"HttpHeaders\":${HTTP_HEADERS}\ +}" >/dev/null + +# 4. Start. autocreatedb=true creates schema on first run; subsequent starts +# return code=0 unless schema needs DDL (e.g. after model change adds an +# entity). On non-zero we run execute_ddl_commands and retry. +echo "Starting Mendix runtime..." +m2ee start '{"autocreatedb":true}' >/tmp/start_result.json 2>/dev/null +CODE=$(jq -r '.result // 99' /tmp/start_result.json 2>/dev/null || echo "99") + +if [ "${CODE}" != "0" ]; then + echo "Initial start returned code=${CODE}. Running DDL sync and retrying..." + m2ee execute_ddl_commands '{}' >/tmp/ddl_result.json 2>/dev/null || true + DDL_RESULT=$(cat /tmp/ddl_result.json 2>/dev/null || echo '{}') + echo "DDL: ${DDL_RESULT}" + m2ee start '{"autocreatedb":true}' >/tmp/start_result.json 2>/dev/null + CODE=$(jq -r '.result // 99' /tmp/start_result.json 2>/dev/null || echo "99") +fi + +if [ "${CODE}" = "0" ]; then + echo "============================================================" + echo " RUNTIME READY" + echo " App: http://0.0.0.0:${RUNTIME_PORT}/" + echo " Admin: http://0.0.0.0:${ADMIN_PORT}/ (m2ee protocol)" + echo "============================================================" +else + REASON=$(jq -r ' + (.feedback.startup_metrics.reason // .message // "unknown") + ' /tmp/start_result.json 2>/dev/null || echo "unknown") + echo "[ERROR] Mendix start failed (code=${CODE}): ${REASON}" + echo "[ERROR] Inspect /tmp/start_result.json inside the container for full payload." +fi + +# Block on the JVM. SIGTERM/SIGINT trap (above) forwards to the JVM. +wait ${JVM_PID} diff --git a/crates/mendix-8/tests/docker-compose.smoke.yml b/crates/mendix-8/tests/docker-compose.smoke.yml new file mode 100644 index 0000000..fe0f1ad --- /dev/null +++ b/crates/mendix-8/tests/docker-compose.smoke.yml @@ -0,0 +1,48 @@ +# Smoke-test compose file for the Mendix 8 runtime crate. +# +# Usage: +# 1. Build the crate image: +# docker build --platform linux/amd64 -t ontologylabs/mendix-runtime:8.18.35.97 .. +# 2. Place an unzipped MDA at ./mda/ (must contain model/metadata.json): +# unzip /path/to/your.mda -d ./mda/ +# 3. Bring up: +# docker compose -f docker-compose.smoke.yml up +# 4. Verify: +# curl -fsS http://localhost:8080/ +# 5. Tear down: +# docker compose -f docker-compose.smoke.yml down -v + +services: + postgres: + image: postgres:14-alpine + environment: + POSTGRES_USER: mendix + POSTGRES_PASSWORD: mendix + POSTGRES_DB: mendix + healthcheck: + test: ["CMD-SHELL", "pg_isready -U mendix"] + interval: 5s + timeout: 3s + retries: 10 + + mendix-runtime: + image: ontologylabs/mendix-runtime:8.18.35.97 + depends_on: + postgres: + condition: service_healthy + environment: + ADMIN_PASSWORD: "SmokeTest2026!" + DATABASE_ENDPOINT: "postgres://mendix:mendix@postgres:5432/mendix" + DEVELOPMENT_MODE: "true" + ports: + - "8080:8080" + - "8090:8090" + volumes: + # Bind-mount the unzipped MDA. Operator must place files here before + # `up`. Read-only is good practice but the runtime sometimes wants to + # write tmp files into BasePath/data — using rw to be safe. + - ./mda:/opt/mendix/app + - mendix-data:/opt/mendix/data + +volumes: + mendix-data: diff --git a/crates/mendix-8/tests/smoke-test.sh b/crates/mendix-8/tests/smoke-test.sh new file mode 100755 index 0000000..895ba6a --- /dev/null +++ b/crates/mendix-8/tests/smoke-test.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Smoke test for the Mendix 8 runtime crate. +# +# Brings up postgres + the crate against an MDA placed at $1 (or +# ./tests/mda/). Polls http://localhost:8080/ for HTTP 200, then tears +# down. Exits 0 on pass, non-zero on fail. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CRATE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.smoke.yml" +MDA_SOURCE="${1:-${SCRIPT_DIR}/mda}" + +if [ ! -f "${MDA_SOURCE}/model/metadata.json" ]; then + echo "[FATAL] No MDA at ${MDA_SOURCE}/model/metadata.json" + echo " Pass an unzipped MDA path as the first argument, e.g.:" + echo " $0 ${CRATE_DIR}/../../docker/mendix-buildpack/project" + exit 78 +fi + +# If the MDA isn't already at ./tests/mda/, symlink it so docker-compose +# can pick up the relative path. +if [ "${MDA_SOURCE}" != "${SCRIPT_DIR}/mda" ]; then + rm -rf "${SCRIPT_DIR}/mda" + ln -s "${MDA_SOURCE}" "${SCRIPT_DIR}/mda" +fi + +PROJECT_NAME="mendix-crate-smoke" + +trap 'echo "Tearing down..."; docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" down -v 2>/dev/null || true' EXIT + +echo "=== Smoke test: bringing up postgres + mendix-runtime ===" +docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" up -d + +# Large apps under amd64 emulation need well over 180 s for first-boot DDL +# sync. Override with SMOKE_TIMEOUT= if needed. +SMOKE_TIMEOUT="${SMOKE_TIMEOUT:-420}" +echo "=== Polling http://localhost:8080/ (timeout ${SMOKE_TIMEOUT} s) ===" +START=$(date +%s) +HEALTHY=0 +while [ $(($(date +%s) - START)) -lt "${SMOKE_TIMEOUT}" ]; do + if curl -fsS -o /dev/null -w "%{http_code}" "http://localhost:8080/" 2>/dev/null | grep -q "200"; then + HEALTHY=1 + break + fi + sleep 5 +done + +if [ "${HEALTHY}" = "1" ]; then + echo "=== PASS — runtime healthy at http://localhost:8080/ ===" + exit 0 +fi + +echo "=== FAIL — runtime never responded HTTP 200 within 180 s ===" +echo "=== Last 50 lines of runtime logs: ===" +docker compose -f "${COMPOSE_FILE}" -p "${PROJECT_NAME}" logs --tail 50 mendix-runtime +exit 1 diff --git a/crates/mendix-8/versions.yaml b/crates/mendix-8/versions.yaml new file mode 100644 index 0000000..e10dc99 --- /dev/null +++ b/crates/mendix-8/versions.yaml @@ -0,0 +1,27 @@ +# Mendix v8 versions verified against this crate. +# When adding a new version, build + smoke-test it, then add the row. +# +# MX8 is out of standard Mendix support, but the FINAL 8.18 LTS patch +# (8.18.35.97) is still served from the public CDN — so it is a normal +# recipe-pull crate. Older MX8 patches are NOT all CDN-hosted (e.g. +# 8.18.21.4259 → 404); for those see PORTAL-DOWNLOAD.md. + +crate_version: "0.1.0" +default_mendix_version: "8.18.35.97" + +supported: + - mendix_version: "8.18.35.97" + cdn_url: "https://cdn.mendix.com/runtime/mendix-8.18.35.97.tar.gz" + java_version: 11 # MX 8.18 is certified on Java 8 AND 11; 11 chosen here + cdn_status: "200 (verified 2026-06-23)" + image_smoke: "pending" # no MX8 fixture app in the corpus yet + notes: > + Final Mendix 8.18 LTS patch; CDN-available. Java 11 base (runs Java-8 + bytecode); rebuild on Temurin 8 if your app pins Java 8. + +# Older MX8 patches that are NOT on the public CDN — obtain via the Mendix +# portal (see PORTAL-DOWNLOAD.md) and override RUNTIME_CDN_BASE at build. +portal_only: + - mendix_version: "8.18.21.4259" + cdn_status: "404 (not hosted)" + obtain: "Mendix portal / Studio Pro 8 install — see PORTAL-DOWNLOAD.md" diff --git a/crates/mendix-9/build/CHANGELOG.md b/crates/mendix-9/build/CHANGELOG.md new file mode 100644 index 0000000..e0eb614 --- /dev/null +++ b/crates/mendix-9/build/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog — Mendix 9 Build Crate + +## [0.1.0] — 2026-06-23 + +Initial release. The build (mxbuild + mx) companion to the mendix-9 runtime crate. + +* `Dockerfile`: model-agnostic; JDK 11 base (`eclipse-temurin:11-jdk-jammy`); + downloads the Mendix 9 build toolchain from the official CDN at build + (`mxbuild-${MENDIX_VERSION}.tar.gz`); never commits a Mendix binary + (recipe-pull, D-DOCKER-LIB-001/002). apt deps for the .NET/Mono toolchain: + `libgdiplus libicu70 libssl3 sqlite3`. +* `build.sh`: `build` / `check` / `version` dispatch. Auto-injects + `--java-home` / `--java-exe-path` / `--gradle-home` / `--loose-version-check` + and defaults `--output=/workspace/.mda`. `mx check` exit codes normalised + to the CI convention (0 clean / 1 errors / 2 warnings). Derived from the + production AIDE aide-mxtools entrypoint. +* Pre-creates `$HOME/.local/share/Mendix` for the MX 9 `.NET` mxbuild whitelist + FailFast (same fix as aide-mxtools). +* Default Mendix version: 9.24.20.33307 (override via `--build-arg MENDIX_VERSION`). + +### Verification status +* CDN source `mxbuild-9.24.20.33307.tar.gz`: **HTTP 200 verified (2026-06-23)**. +* Image build + `mx version` / MDA smoke: **pending** — recorded honestly in + `provenance.yaml` (`smoke_verified: pending`). The mxbuild invocation itself is + the production-proven aide-mxtools path; the remaining gate is a release build. + +### Why +Lets a Docker-only environment (CI, a contributor without Studio Pro) compile and +validate a Mendix 9 app, producing the `.mda` the runtime crate runs. diff --git a/crates/mendix-9/build/Dockerfile b/crates/mendix-9/build/Dockerfile new file mode 100644 index 0000000..897f33c --- /dev/null +++ b/crates/mendix-9/build/Dockerfile @@ -0,0 +1,113 @@ +# Mendix 9 Build Crate — model-agnostic mxbuild + mx toolchain (recipe-pull). +# +# Image: ontologylabs/mendix-mxbuild:9 +# ontologylabs/mendix-mxbuild:9.24.20.33307 (immutable) +# +# Contract: +# * Bake: JDK 11 + the Mendix 9.x build toolchain (mxbuild + mx), pulled from +# the official Mendix CDN at `docker build` time. +# * NEVER commit a Mendix binary to the repo. The toolchain tarball is curl'd +# from cdn.mendix.com on the licensed user's own machine — exactly like the +# runtime crate downloads the runtime tarball (D-DOCKER-LIB-001 recipe-pull; +# D-DOCKER-LIB-002 no-binaries guard). You bring the licensed toolchain; we +# bring the build recipe. +# * Bind-mount the project directory (containing the `.mpr`) at /workspace. +# * Compile : `docker run -v :/workspace ... build /workspace/App.mpr` +# → writes App.mda next to the .mpr in /workspace. +# * Validate: `docker run -v :/workspace ... check /workspace/App.mpr` +# → `mx check` with CI-normalised exit codes (0 clean / 1 error / 2 warn). +# +# Why a "build crate" (companion to the runtime crate): +# The AIDE pipeline drives mxbuild via a bind-mounted Studio Pro modeler +# (the aide-mxtools image). This crate makes the toolchain self-contained and +# version-pinned: one image per Mendix major, the matching mxbuild fetched +# from the CDN at build, so a contributor with only Docker can compile any app +# at that version without a local Studio Pro install. Runtime crate runs the +# app; build crate produces the .mda the runtime crate runs. +# +# Provenance: +# * mxbuild + mx from https://cdn.mendix.com/runtime/mxbuild-${MENDIX_VERSION}.tar.gz +# * Tarball top-level is `modeler/` → modeler/mxbuild, modeler/mx, +# modeler/tools/gradle (verified against the AIDE build-server cache layout). +# * Java: Eclipse Temurin 11 JDK — Mendix 9 compiles Java actions at release 11. +# A full JDK (not JRE) is required: mxbuild invokes javac to compile the +# app's javasource and Java actions. + +FROM --platform=linux/amd64 eclipse-temurin:11-jdk-jammy + +ARG MENDIX_VERSION=9.24.20.33307 +ARG MXBUILD_CDN_BASE=https://cdn.mendix.com/runtime + +LABEL ai.ayios.purpose="Mendix 9 build crate (mxbuild + mx, model-agnostic)" +LABEL ai.ayios.crate="mendix-9/build" +LABEL ai.ayios.mendix-version="${MENDIX_VERSION}" +LABEL maintainer="hardy@agileworks.co.za" + +# Layer 1 — apt deps for the mxbuild toolchain. mxbuild is a .NET/Mono program: +# libgdiplus — System.Drawing (image/PDF generation during build) +# libicu70 — globalization/ICU (jammy ships ICU 70) +# libssl3 — TLS for any build-time fetch +# sqlite3 — read the .mpr (modern Mendix projects are SQLite databases) +# curl — pull the toolchain tarball +# Stable layer; survives churn in the heavier toolchain layer below. +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + libgdiplus \ + libicu70 \ + libssl3 \ + sqlite3 \ + && rm -rf /var/lib/apt/lists/* + +# Layer 2 — Mendix build toolchain tarball (mxbuild + mx). ~300-500 MB download. +# Recipe-pull: curl'd from the CDN at build, never committed. Cached unless +# MENDIX_VERSION or MXBUILD_CDN_BASE changes. +# +# Tarball top-level is `modeler/`, so extracting at /opt/mxtools produces the +# same layout the AIDE build-server caches and mounts at /opt/mxtools: +# /opt/mxtools/modeler/mxbuild +# /opt/mxtools/modeler/mx +# /opt/mxtools/modeler/tools/gradle +RUN set -eux; \ + mkdir -p /opt/mxtools /workspace; \ + echo "Downloading Mendix build toolchain ${MENDIX_VERSION} from ${MXBUILD_CDN_BASE}..."; \ + curl -fsSL "${MXBUILD_CDN_BASE}/mxbuild-${MENDIX_VERSION}.tar.gz" -o /tmp/mxbuild.tar.gz; \ + echo "Extracting..."; \ + tar -xzf /tmp/mxbuild.tar.gz -C /opt/mxtools; \ + rm /tmp/mxbuild.tar.gz; \ + test -f /opt/mxtools/modeler/mxbuild || { \ + echo "FATAL: modeler/mxbuild not found at expected path /opt/mxtools/modeler/mxbuild."; \ + echo "Tarball top-level may have changed. Inspecting:"; \ + find /opt/mxtools -maxdepth 3 -type f -name 'mx*'; \ + exit 1; \ + }; \ + chmod +x /opt/mxtools/modeler/mxbuild /opt/mxtools/modeler/mx 2>/dev/null || true; \ + echo "Mendix ${MENDIX_VERSION} build toolchain ready at /opt/mxtools/modeler/" + +# Mendix mxbuild — MX 11's .NET toolchain in particular — creates settings under +# $HOME/.local/share/Mendix via GetFolderPath(LocalApplicationData). In a fresh +# container that directory does not exist, and mxbuild's WhitelistAware +# filesystem FailFast-rejects *creating* it ("UnauthorizedAccessException … +# EnsurePathSafety") before the build starts. Pre-create it for HOME=/root so +# mxbuild writes settings without tripping the create-time whitelist check (a +# harmless no-op on older Mono-based mxbuild). Proven in AIDE aide-mxtools. +RUN mkdir -p /root/.local/share/Mendix /root/.config/Mendix /root/.cache/Mendix + +# Crate entrypoint. +COPY build.sh /opt/mxtools/build.sh +RUN chmod +x /opt/mxtools/build.sh + +# Runs as root by design — an ephemeral, single-shot build container (not a +# long-running service). Root avoids the bind-mount uid-mismatch that would +# otherwise block writing the produced .mda back into the operator's mounted +# /workspace. On a Linux host where root-owned output is undesirable, pass +# `--user $(id -u)`; build.sh defensively re-creates the Mendix settings dir +# under whatever $HOME resolves to. +ENV JAVA_HOME=/opt/java/openjdk \ + MENDIX_VERSION=${MENDIX_VERSION} + +VOLUME ["/workspace"] +WORKDIR /workspace + +ENTRYPOINT ["/opt/mxtools/build.sh"] +CMD ["--help"] diff --git a/crates/mendix-9/build/README.md b/crates/mendix-9/build/README.md new file mode 100644 index 0000000..fc3b6f3 --- /dev/null +++ b/crates/mendix-9/build/README.md @@ -0,0 +1,102 @@ +# Mendix 9 Build Crate + +> A model-agnostic, version-pinned Docker image that compiles **any** Mendix 9.x +> project (`.mpr`) into a deployable `.mda` — and runs `mx check` — with nothing +> on the host but Docker. The Mendix build toolchain (`mxbuild` + `mx`) is pulled +> from the official Mendix CDN at build time; no Studio Pro install, no committed +> binaries. + +This is the **build** companion to the [`mendix-9` runtime crate](../). The build +crate *produces* the `.mda`; the runtime crate *runs* it. + +## Why this exists + +The AIDE pipeline compiles models with `mxbuild` driven from a bind-mounted Studio +Pro install. That requires a licensed modeler on the host. This crate makes the +toolchain self-contained and version-pinned: one image per Mendix major, the +matching `mxbuild` fetched from `cdn.mendix.com` at `docker build` — so a CI runner +or a contributor with only Docker can compile a Mendix 9 app without Studio Pro. + +| | Studio Pro on host | aide-mxtools (bind-mount) | This build crate | +|---|---|---|---| +| Needs Studio Pro installed | yes | yes (modeler bind-mounted) | **no** | +| Toolchain source | local install | local install | **CDN at build** | +| Version pinning | manual | manual | **one image per version** | +| Runs in plain CI | no | partial | **yes** | + +## Image tags + +| Tag | Pins | Use | +|---|---|---| +| `ontologylabs/mendix-mxbuild:9.24.20.33307` | Exact version (immutable) | Reproducible CI | +| `ontologylabs/mendix-mxbuild:9` | Latest 9.x in this crate | Dev, demo | + +## Build the image + +```bash +cd crates/mendix-9/build +docker build \ + --platform linux/amd64 \ + --build-arg MENDIX_VERSION=9.24.20.33307 \ + -t ontologylabs/mendix-mxbuild:9.24.20.33307 . +# mxbuild + mx are pulled from cdn.mendix.com/runtime/mxbuild-9.24.20.33307.tar.gz at build. +``` + +## Compile an app + +```bash +# /path/to/project contains App.mpr +docker run --rm \ + --platform linux/amd64 \ + -v /path/to/project:/workspace \ + ontologylabs/mendix-mxbuild:9.24.20.33307 \ + build /workspace/App.mpr +# → writes /path/to/project/App.mda +``` + +Then run it with the runtime crate: + +```bash +unzip /path/to/project/App.mda -d /path/to/project/app +cd ../ # the mendix-9 runtime crate +docker compose -f tests/docker-compose.smoke.yml up # or your own compose +``` + +## Validate a model (`mx check`) + +```bash +docker run --rm -v /path/to/project:/workspace \ + ontologylabs/mendix-mxbuild:9.24.20.33307 \ + check /workspace/App.mpr +# exit 0 = clean · 1 = errors · 2 = warnings only +``` + +## What the entrypoint auto-injects + +`build.sh` supplies the toolchain paths `mxbuild` needs unless you pass them: + +* `--java-home` / `--java-exe-path` → the baked JDK (`$JAVA_HOME`) +* `--gradle-home` → the toolchain's bundled Gradle +* `--loose-version-check` → tolerate patch drift between toolchain and model +* `--output=/workspace/.mda` → default output beside the `.mpr` + +Pass any of these explicitly to override. + +## Notes & gotchas + +* **Runs as root by design.** A single-shot build container, not a service. Root + avoids the bind-mount uid mismatch that would block writing the `.mda` back into + your mounted `/workspace`. On Linux, pass `--user $(id -u)` if you want + host-uid-owned output; the entrypoint re-creates the Mendix settings dir under + the resulting `$HOME`. +* **`.NET` whitelist (MX 11 origin).** mxbuild creates settings under + `$HOME/.local/share/Mendix` and FailFast-rejects *creating* that path itself; the + image pre-creates it. (Same fix as the AIDE aide-mxtools image.) +* **amd64 emulation.** On Apple Silicon, `mxbuild` runs x86_64 under emulation. + Enable Colima Rosetta (`colima start --vm-type vz --vz-rosetta`) for speed; a + hung emulated build is the classic symptom of Rosetta being off. +* **No Mendix binary is committed.** The toolchain is fetched from the CDN at build, + on your licensed machine (D-DOCKER-LIB-002). See repo `guard.sh`. + +See [`provenance.yaml`](provenance.yaml) for the exact CDN source and +[`versions.yaml`](versions.yaml) for verified versions. diff --git a/crates/mendix-9/build/build.sh b/crates/mendix-9/build/build.sh new file mode 100644 index 0000000..01691e0 --- /dev/null +++ b/crates/mendix-9/build/build.sh @@ -0,0 +1,134 @@ +#!/bin/bash +# Mendix Build Crate — entrypoint. Dispatches to mxbuild / mx from the toolchain +# baked at /opt/mxtools/modeler (pulled from the Mendix CDN at image build). +# +# Commands: +# build [mxbuild-options] Compile an .mpr → .mda +# check [mx-check-options] Run `mx check` (CI-normalised exit) +# version Print the mx toolchain version +# --help Usage +# +# Mount: -v :/workspace (the dir containing your App.mpr) +# Output: build writes .mda into /workspace next to the .mpr. +# +# Derived from the AIDE aide-mxtools entrypoint (production-proven mxbuild +# invocation) — same find-tool + auto-inject(--java-home/--gradle-home/ +# --loose-version-check) logic, retargeted at the baked /opt/mxtools/modeler. + +set -euo pipefail + +MXTOOLS_DIR="/opt/mxtools" + +# Defensive: re-create the Mendix settings dir under whatever HOME resolves to. +# The image bakes these for HOME=/root; this covers a runtime `--user`/`-e HOME=` +# override. MX 11's .NET mxbuild FailFast-rejects creating these; harmless elsewhere. Idempotent. +mkdir -p "${HOME:-/root}/.local/share/Mendix" \ + "${HOME:-/root}/.config/Mendix" \ + "${HOME:-/root}/.cache/Mendix" 2>/dev/null || true + +# find_tool — locate mxbuild/mx in the baked toolchain. Strict candidates +# first (the `modeler/` layout the CDN tarball extracts to), then a glob fallback +# so a future tarball-layout change degrades gracefully instead of failing hard. +find_tool() { + local tool_name="$1" + for candidate in \ + "${MXTOOLS_DIR}/modeler/${tool_name}" \ + "${MXTOOLS_DIR}/${tool_name}" \ + "${MXTOOLS_DIR}/runtime/${tool_name}" \ + "${MXTOOLS_DIR}/tools/${tool_name}"; do + if [ -x "$candidate" ]; then echo "$candidate"; return 0; fi + done + local found + found=$(find "${MXTOOLS_DIR}" -name "${tool_name}" -type f -executable 2>/dev/null | head -1) + [ -n "$found" ] && { echo "$found"; return 0; } + return 1 +} + +case "${1:-}" in + build) + shift + [ $# -ge 1 ] || { echo "Usage: build [mxbuild-options]" >&2; exit 1; } + MXBUILD_BIN=$(find_tool "mxbuild") || { echo "ERROR: mxbuild not found under ${MXTOOLS_DIR}" >&2; exit 1; } + echo "Using: ${MXBUILD_BIN}" >&2 + + # Auto-inject the toolchain paths mxbuild needs, unless the caller set them. + EXTRA_ARGS=() + HAS_JAVA_HOME=false; HAS_GRADLE_HOME=false; HAS_LOOSE=false; HAS_OUTPUT=false + for arg in "$@"; do + case "$arg" in + --java-home=*) HAS_JAVA_HOME=true ;; + --gradle-home=*) HAS_GRADLE_HOME=true ;; + --loose-version-check) HAS_LOOSE=true ;; + --output=*|--output) HAS_OUTPUT=true ;; + esac + done + if [ "$HAS_JAVA_HOME" = false ] && [ -n "${JAVA_HOME:-}" ]; then + EXTRA_ARGS+=(--java-home="${JAVA_HOME}" --java-exe-path="${JAVA_HOME}/bin/java") + fi + if [ "$HAS_GRADLE_HOME" = false ]; then + for g in "${MXTOOLS_DIR}/modeler/tools/gradle" "${MXTOOLS_DIR}/tools/gradle"; do + [ -d "$g" ] && { EXTRA_ARGS+=(--gradle-home="$g"); break; } + done + fi + # Tolerate patch-version drift between the toolchain and the model + # (SDK commits may bump the model's product version). + [ "$HAS_LOOSE" = false ] && EXTRA_ARGS+=(--loose-version-check) + + # Default the output beside the .mpr in /workspace if the caller didn't + # pass --output. mpr-path is the first positional after `build`. + if [ "$HAS_OUTPUT" = false ]; then + MPR_PATH="$1" + MPR_BASE="$(basename "${MPR_PATH%.mpr}")" + EXTRA_ARGS+=(--output="/workspace/${MPR_BASE}.mda") + fi + + exec "$MXBUILD_BIN" "${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}" "$@" + ;; + + check) + shift + [ $# -ge 1 ] || { echo "Usage: check [mx-check-options]" >&2; exit 1; } + MX_BIN=$(find_tool "mx") || { echo "ERROR: mx not found under ${MXTOOLS_DIR}" >&2; exit 1; } + echo "Using: ${MX_BIN}" >&2 + # mx check returns an OR'd bitmask: 1=errors, 2=warnings, 4=deprecations. + # Normalise to the CI convention: 0=clean, 1=errors, 2=warnings-only. + set +e + "$MX_BIN" check "$@" + MX_EXIT=$? + set -e + if [ $((MX_EXIT & 1)) -ne 0 ]; then exit 1 + elif [ $((MX_EXIT & 2)) -ne 0 ]; then exit 2 + else exit 0 + fi + ;; + + version) + # mx has no toolchain --version flag; the pinned version is the image's + # MENDIX_VERSION. (mx show-* verbs report an *app's* version, not the toolchain's.) + echo "Mendix build crate — toolchain version ${MENDIX_VERSION:-unknown} (mxbuild + mx)" + ;; + + --help|help|"") + cat <<'USAGE' +Mendix Build Crate — mxbuild + mx in a version-pinned container + +Commands: + build [options] Compile an .mpr → .mda (output → /workspace) + check [options] Run mx check (exit 0=clean, 1=errors, 2=warnings) + version Show the mx toolchain version + +Mount: + -v :/workspace Directory containing your App.mpr + +Examples: + docker run --rm -v "$PWD":/workspace ontologylabs/mendix-mxbuild:9 build /workspace/App.mpr + docker run --rm -v "$PWD":/workspace ontologylabs/mendix-mxbuild:9 check /workspace/App.mpr +USAGE + ;; + + *) + echo "Unknown command: ${1:-}" >&2 + echo "Run with --help for usage." >&2 + exit 1 + ;; +esac diff --git a/crates/mendix-9/build/provenance.yaml b/crates/mendix-9/build/provenance.yaml new file mode 100644 index 0000000..7800970 --- /dev/null +++ b/crates/mendix-9/build/provenance.yaml @@ -0,0 +1,31 @@ +# Per-image provenance for the mendix-9 BUILD crate (mxbuild + mx). +# Recipe-pull distribution model (D-DOCKER-LIB-001): no registry, no published +# Mendix binaries — the toolchain tarball is curl'd from the Mendix CDN at build. +# +# Stable provenance anchor: cdn_source_url (anyone can re-fetch + verify the same +# tarball). image_digest is the local image-config ID for a given build; it is +# build-environment-specific (apt package versions drift) and recorded for +# traceability, not byte-for-byte reproducibility. It is populated once the image +# has been built + smoke-verified on a release machine. + +crate: mendix-9/build +crate_version: "0.1.0" + +images: + - mendix_version: "9.24.20.33307" + cdn_source_url: "https://cdn.mendix.com/runtime/mxbuild-9.24.20.33307.tar.gz" + base_image: "eclipse-temurin:11-jdk-jammy" + java_version: 11 + platform: "linux/amd64" + image_tags: + - "ontologylabs/mendix-mxbuild:9.24.20.33307" + - "ontologylabs/mendix-mxbuild:9" + image_digest: null # filled after a verified release build + cdn_verified: "2026-06-23 — HTTP 200 (HEAD)" + smoke_verified: "pending" + notes: > + Recipe authored from the production AIDE aide-mxtools image (proven mxbuild + invocation: --java-home / --java-exe-path / --gradle-home / --loose-version-check) + and the verified runtime-crate recipe-pull pattern. The CDN source URL is + verified reachable; an image build + `mx version`/MDA smoke is the remaining + release gate. diff --git a/crates/mendix-9/build/tests/smoke-test.sh b/crates/mendix-9/build/tests/smoke-test.sh new file mode 100644 index 0000000..88e64f9 --- /dev/null +++ b/crates/mendix-9/build/tests/smoke-test.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Smoke test for the Mendix 9 build crate. +# +# Compiles a Mendix 9 project (.mpr) into an .mda using the build-crate image, +# then asserts the .mda was produced. Exits 0 on pass, non-zero on fail. +# +# Usage: ./smoke-test.sh /path/to/project/App.mpr +# +# You supply the licensed .mpr — none is committed to this repo (guard.sh blocks +# .mpr/.mda/.tar.gz patterns; D-DOCKER-LIB-002). The image's mxbuild toolchain is +# pulled from the Mendix CDN at `docker build`. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +BUILD_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +MENDIX_VERSION="${MENDIX_VERSION:-9.24.20.33307}" +IMAGE="${BUILD_IMAGE:-ontologylabs/mendix-mxbuild:${MENDIX_VERSION}}" + +MPR_PATH="${1:-}" +if [ -z "${MPR_PATH}" ] || [ ! -f "${MPR_PATH}" ]; then + echo "[FATAL] Pass the path to a Mendix 9 .mpr as the first argument." + echo " e.g. $0 /path/to/MyApp/MyApp.mpr" + exit 78 +fi + +# Colima mounts only \$HOME into the VM; a project outside \$HOME is invisible to +# the container. Warn rather than silently mount an empty dir. +case "${MPR_PATH}" in + "${HOME}"/*) : ;; + *) echo "[WARN] ${MPR_PATH} is not under \$HOME — on Colima it may be invisible to the container." ;; +esac + +PROJECT_DIR="$(cd "$(dirname "${MPR_PATH}")" && pwd)" +MPR_NAME="$(basename "${MPR_PATH}")" +MDA_NAME="$(basename "${MPR_NAME%.mpr}").mda" + +# Build the image if absent. +if ! docker image inspect "${IMAGE}" >/dev/null 2>&1; then + echo "=== Building image ${IMAGE} (mxbuild pulled from CDN) ===" + docker build --platform linux/amd64 \ + --build-arg MENDIX_VERSION="${MENDIX_VERSION}" \ + -t "${IMAGE}" "${BUILD_DIR}" +fi + +echo "=== Compiling ${MPR_NAME} → ${MDA_NAME} ===" +rm -f "${PROJECT_DIR}/${MDA_NAME}" +docker run --rm --platform linux/amd64 \ + -v "${PROJECT_DIR}:/workspace" \ + "${IMAGE}" \ + build "/workspace/${MPR_NAME}" + +if [ -f "${PROJECT_DIR}/${MDA_NAME}" ]; then + SIZE=$(du -h "${PROJECT_DIR}/${MDA_NAME}" | cut -f1) + echo "=== PASS — ${MDA_NAME} produced (${SIZE}) ===" + exit 0 +fi + +echo "=== FAIL — ${MDA_NAME} was not produced. Inspect mxbuild output above. ===" +exit 1 diff --git a/crates/mendix-9/build/versions.yaml b/crates/mendix-9/build/versions.yaml new file mode 100644 index 0000000..49b6526 --- /dev/null +++ b/crates/mendix-9/build/versions.yaml @@ -0,0 +1,18 @@ +# Mendix v9 build-toolchain versions verified against this crate. +# When adding a new version: build the image, smoke-test (build a known .mpr + +# mx check), then add the row. The build toolchain (mxbuild + mx) ships on the +# same CDN as the runtime, as mxbuild-.tar.gz. + +crate_version: "0.1.0" +default_mendix_version: "9.24.20.33307" + +supported: + - mendix_version: "9.24.20.33307" + cdn_url: "https://cdn.mendix.com/runtime/mxbuild-9.24.20.33307.tar.gz" + java_version: 11 # JDK (mxbuild compiles javasource at release 11) + cdn_status: "200 (verified 2026-06-23)" + image_smoke: "pending" # image build + `mx version` not yet run in CI + notes: "Pairs with the mendix-9 runtime crate (same default version)." + +planned: + - mendix_version: "9.24.0" diff --git a/docs/building-mendix-apps-in-docker.md b/docs/building-mendix-apps-in-docker.md new file mode 100644 index 0000000..fb5307e --- /dev/null +++ b/docs/building-mendix-apps-in-docker.md @@ -0,0 +1,122 @@ +# Building Mendix apps in Docker — compile an `.mpr` to an `.mda` without Studio Pro + +You have a Mendix project (`.mpr`) and you want a deployable app package (`.mda`) — +in CI, on a build server, or on a laptop that doesn't have Studio Pro installed. +The **build crates** do exactly that: a version-pinned `mxbuild` + `mx` toolchain in +a Docker image, pulled from the Mendix CDN at build time. Bind-mount your project, +run `build`, get an `.mda`. Same pattern for Mendix 7, 8, 9, 10, and 11. + +> **TL;DR** +> ```bash +> cd crates/mendix-11/build +> docker build --platform linux/amd64 --build-arg MENDIX_VERSION=11.6.4 \ +> -t ontologylabs/mendix-mxbuild:11 . +> docker run --rm -v "$PWD/MyApp":/workspace ontologylabs/mendix-mxbuild:11 build /workspace/MyApp.mpr +> # → MyApp.mda appears next to MyApp.mpr +> ``` + +## What you get + +* **A JDK + the Mendix `mxbuild` + `mx` toolchain**, pulled from + `cdn.mendix.com/runtime/mxbuild-.tar.gz` at build time (never committed). +* **`build`** — compile an `.mpr` to an `.mda` (the package the [runtime + crates](../README.md) run). +* **`check`** — run `mx check` with CI-normalised exit codes (`0` clean, `1` + errors, `2` warnings only). +* One image per Mendix major version; reused across every project at that version. +* No Studio Pro, no licensed modeler install on the host — just Docker. + +## Pick the version + +The build crate must match the Mendix version your project was last edited with +(its `_ProductVersion`). Each major has its own crate and JDK: + +| Build crate | Default version | JDK | +|---|---|---| +| `crates/mendix-11/build` | `11.6.4` | Java 21 | +| `crates/mendix-10/build` | `10.24.13.86719` | Java 21 | +| `crates/mendix-9/build` | `9.24.20.33307` | Java 11 | +| `crates/mendix-8/build` | `8.18.35.97` | Java 11 | +| `crates/mendix-7/build` | `7.23.8.58888` | Java 8 | + +Override the exact patch with `--build-arg MENDIX_VERSION=`. The +toolchain CDN serves any patch that runtime serves; the build crate's `build.sh` +passes `--loose-version-check`, so a small patch drift between toolchain and model +is tolerated. + +## Build the image + +```bash +cd crates/mendix-10/build +docker build --platform linux/amd64 \ + --build-arg MENDIX_VERSION=10.24.13.86719 \ + -t ontologylabs/mendix-mxbuild:10.24.13.86719 \ + -t ontologylabs/mendix-mxbuild:10 . +``` + +## Compile a project + +```bash +# /path/to/MyApp contains MyApp.mpr +docker run --rm --platform linux/amd64 \ + -v /path/to/MyApp:/workspace \ + ontologylabs/mendix-mxbuild:10 \ + build /workspace/MyApp.mpr +# → writes /path/to/MyApp/MyApp.mda +``` + +Then run it with the matching runtime crate: + +```bash +unzip /path/to/MyApp/MyApp.mda -d /path/to/MyApp/app +# … docker run the mendix-10 runtime crate with /path/to/MyApp/app bind-mounted +``` + +## Validate a model (CI gate) + +```bash +docker run --rm -v /path/to/MyApp:/workspace \ + ontologylabs/mendix-mxbuild:10 \ + check /workspace/MyApp.mpr +echo $? # 0 = clean · 1 = errors · 2 = warnings only +``` + +`build.sh` auto-injects the toolchain paths `mxbuild` needs (`--java-home`, +`--java-exe-path`, `--gradle-home`, `--loose-version-check`) and defaults the +output to `/workspace/.mda`. Pass any of those explicitly to override. + +## Mendix 8 note + +Mendix 8 is out of standard support, but its final LTS patch (`8.18.35.97`, the +crate default) is still CDN-hosted, so it builds with no extra steps. Older MX8 +patches aren't all on the CDN — obtain the toolchain via the Mendix portal and +point `MXBUILD_CDN_BASE` at a local copy. See +[`crates/mendix-8/PORTAL-DOWNLOAD.md`](../crates/mendix-8/PORTAL-DOWNLOAD.md), +which includes step-by-step agent download-instructions. + +## Troubleshooting + +* **The build hangs / `mxbuild` crashes under emulation** — on Apple Silicon, + `mxbuild` (x86_64 .NET) needs Rosetta. Start Colima with + `colima start --vm-type vz --vz-rosetta`. A hung build is the classic + Rosetta-off symptom. +* **`modeler/mxbuild not found` at image build** — `MENDIX_VERSION` must be a + real CDN-hosted version; probe `curl -sI cdn.mendix.com/runtime/mxbuild-.tar.gz`. +* **`Project file '/workspace/App.mpr' does not exist`** — on Colima only `$HOME` + is mounted into the VM; keep your project under `$HOME` (or stage a copy there). +* **Root-owned `.mda` on Linux** — the build container runs as root to write the + output into your bind-mount; pass `--user $(id -u)` if you need host-uid output. + +## Provenance & licence + +The crate scaffolding (`Dockerfile`, `build.sh`, docs, tests) is **Apache-2.0**. +The Mendix `mxbuild`/`mx` toolchain is `curl`ed from the official CDN at build time +and is **never committed** — it remains Mendix's IP under +[Mendix's terms of use](https://www.mendix.com/terms-of-use/). The repo's +`no-mendix-binaries` CI guard enforces this on every push and PR. + +--- + +*Part of [mendix-runtime-crates](https://github.com/ontologylabs/mendix-runtime-crates) +— the build layer of the **mxto** Mendix toolchain. To run the `.mda` you produce, +see the [README](../README.md) and the per-version run guides.*