Dit zijn de regels waarvoor het hele project bestaat. Al het andere in docker-mql-compiler is voorbereiding voor deze regels, of opruimwerk erna:
# MetaEditor's exit code is backwards (1 usually means success); the log's
# Result line is the verdict, and its stderr is Wine noise.
wine "$editor" \
"/compile:$(winepath -w "$STAGED_SOURCE")" \
"/log:$(winepath -w "$log_file")" \
"/inc:$(winepath -w "$MERGED_ROOT")" >/dev/null 2>&1 || trueDat is MetaEditor van MetaQuotes, het enige ding op deze planeet dat van een .mq5 een .ex5 maakt, een Windows-GUI die nog nooit van Linux heeft gehoord, aangestuurd door een bash-script in een Wine-container met Xvfb als nepscherm waar niemand naar kijkt. Let op de || true. De exitcode gaat expres de prullenbak in, want die liegt. Mijn editor, mijn agents en mijn CI draaien allemaal op Linux, en ik was het spuugzat dat iemand aan een Windows-bak moest gaan zitten prutsen om op “Compile” te klikken en fouten met de hand uit een logvenster over te tikken. Dus lees ik entrypoint.sh in de volgorde waarin het draait: van de eerste download tot de JSON-regel die een agent aan het eind leest.
Eerst wat context. mt5-httpapi heeft al een POST /compile-route, MQL5-broncode erin als JSON, een base64 .ex5 eruit. Maar die route woont in een complete Windows-VM onder KVM die live terminals draait en met echt geld handelt, kan alleen MQL5, weigert includes met absolute paden of .., en gedeelde libraries moet je eerst naar de compile-boom ervan uploaden. Deze compileert een directory vol MT4- en MT5-code op een laptop of een GitHub-runner zonder dat Windows ooit opstart.
verify_installer: gesigneerd door MetaQuotes of geweigerd
De image is Debian trixie met Wine (64-bit plus de 32-bit helft, omdat de stokoude MetaEditor van MT4 32-bit Windows-code is) en geen enkele byte van MetaQuotes. MetaTrader is niet van mij om te herdistribueren, dus de eerste run downloadt de installers (standaard die van RoboForex, te overschrijven met MQL_COMPILER_MT4_URL en MQL_COMPILER_MT5_URL). Hij haalt alleen op wat de run nodig heeft: alleen .mq5-bestanden betekent dat alleen MT5 wordt geïnstalleerd. De eerste keer zo’n anderhalve minuut, daarna seconden.
Voordat wine een installer aanraakt, checkt osslsigncode de Authenticode-handtekening tegen de CA-bundle van de image, en daarna checkt dit wie hem echt heeft gesigneerd:
require_signer() {
local name="$1" report="$2" subject
subject="$(awk '/^Signer.s certificate:/ { found = 1 } found && /Subject:/ { print; exit }' <<<"$report")"
if [[ "$subject" != *"/CN=$SIGNER_PATTERN"* ]]; then
log ERROR "installer is validly signed but not by $SIGNER_PATTERN file=$name signer=${subject#*Subject: }"
return 1
fi
log INFO "installer signature ok signer=$SIGNER_PATTERN file=$name"
}SIGNER_PATTERN is MetaQuotes. Alleen het certificaat van de ondertekenaar telt, niet de keten of de timestamp-certificaten die in hetzelfde rapport rondslingeren. Een geldige handtekening van iemand anders is nog steeds een weigering. Als de revocatieserver van de CA plat ligt, probeert verify_installer het drie keer, checkt daarna digest, keten en ondertekenaar zonder de revocatie-lookup en logt een warning met reason=crl_unreachable, zodat je weet dat het gebeurd is.
unpack_stdlib: de terminal één keer opstarten zodat Trade.mqh bestaat
De installer zet de standaardbibliotheek niet op schijf. Die pakt de terminal uit de eerste keer dat hij start. Installeer MetaEditor onder Wine, sla dat over, en elke #include <Trade/Trade.mqh> gaat op zijn bek. Dus start de setup elke terminal headless in portable mode en wacht op het markerbestand:
(cd "$terminal" && wine "$(terminal_name "$platform")" /portable >/dev/null 2>&1 &)
while [[ ! -f "$marker" ]] && ((waited < STDLIB_UNPACK_TIMEOUT_SECONDS)); do
sleep "$POLL_SECONDS"
waited=$((waited + POLL_SECONDS))
doneDe marker is Include/Trade/Trade.mqh voor MT5 en stdlib.mqh voor MT4. Zodra die verschijnt, schiet wineserver -k de terminal af, want die sluit zichzelf nooit af. De geïnstalleerde terminals worden daarna uit Program Files verhuisd naar kale portable directory’s, drive_c/terminals/mt4 en drive_c/terminals/mt5, in het Docker-volume mql-compiler-cache. Ruwweg 2 GB, één subdirectory per user-ID. De container draait als jouw UID, dus de gecompileerde bestanden zijn van jou, niet van root.
build_merged_root: vier include-lagen, die van jou bovenop
MetaEditor leest #include <...> uit precies één Include-directory. Jij hebt er minstens twee nodig, de standaardbibliotheek van MetaQuotes en je eigen headers. Dit is de functie die de tool echt bruikbaar maakt:
cp -rs "$(stdlib_root "$platform")/Include" "$MERGED_ROOT/Include"
for include_dir in ${EXTRA_INCLUDES[@]+"${EXTRA_INCLUDES[@]}"}; do
cp -rs --remove-destination "$include_dir/." "$MERGED_ROOT/Include/"
done
MERGED_ROOTS[$key]="$MERGED_ROOT"
[[ -n "$user_root" ]] || return 0
for entry in "$user_root"/*; do
[[ -e "$entry" ]] || continue
name="$(basename "$entry")"
if [[ "${name,,}" == "$INCLUDE_FOLDER" && -d "$entry" ]]; then
cp -rs --remove-destination "$entry/." "$MERGED_ROOT/Include/"
continue
fi
ln -s "$entry" "$MERGED_ROOT/$name"
doneLees hem van boven naar beneden en je hebt de volgorde van de lagen:
- De standaard-
Includevan de terminal (Trade/,Arrays/,stdlib.mqh, de hele santenkraam). - Elke
-I-directory, in de volgorde waarin je ze opgaf. - Je eigen
Include.--remove-destinationbetekent dat bij een botsing de latere laag wint, en die van jou komt als laatste. Een gepatchteTrade/Trade.mqhin je boom? Die van jou wordt gecompileerd. - Links naar de rest van je
MQL4/MQL5-directory (Files,Imagesen consorten), zodat#resource "\\Files\\data.txt"resolvet.
Het zijn allemaal symlinks (cp -rs) in een staging-gebied, één keer per platform en user-directory per ronde gebouwd. Er wordt niks in jouw directory’s geschreven en het is weg als de run klaar is. Welke MQL4– of MQL5-directory als de jouwe telt, is gewoon de dichtstbijzijnde boven het bronbestand. De extensie alleen kiest de compiler, .mq4 naar metaeditor.exe van MT4, .mq5 naar MetaEditor64.exe van MT5.
stage_source: een link om de spaties heen
Woont je terminal in MT5 - SomeBroker? MetaEditor’s /compile en /inc verslikken zich allebei in spaties. Dus een bron met spaties in zijn pad wordt via een link gecompileerd:
[[ "$source" =~ [[:space:]] ]] || return 0
link="${SOURCE_LINKS[$root]:-}"
if [[ -z "$link" ]]; then
link="$STAGING_DIR/sources/${#SOURCE_LINKS[@]}"
mkdir -p "$STAGING_DIR/sources"
ln -s "$root" "$link"
SOURCE_LINKS[$root]="$link"
fi
STAGED_SOURCE="$link${source#"$root"}"De link wijst naar de hele MQL4/MQL5-directory, niet naar het bestand, dus "../"-includes daarbinnen resolven nog steeds. Om dezelfde reden woont de samengevoegde include-root in het staging-gebied: de directory van een gemounte terminal heeft vaak spaties in de naam.
compile_one: eerst weggooien, dan de exitcode niet geloven
Vlak voor de wine-aanroep van bovenaan deze post staat dit:
# An old .ex4/.ex5 must never outlive a compile that did not produce a new
# one. MetaEditor removes it on compile errors, but not when it crashes or
# writes no result.
rm -f "$output"Welke .ex4/.ex5 er na de run ook ligt, die komt uit deze run. Geen ouwe binary die zich voordoet als vers nadat MetaEditor op zijn plaat ging. Bestanden compileren één voor één, nooit parallel, want twee MetaEditors tegelijk onder Wine is hoe je demonen oproept.
Na de aanroep komt het oordeel uit de log, niet uit de exitcode:
result="$(grep -E '^Result: ' <<<"$lines" | tail -n 1)" || result=""De command-line-modus van MetaEditor geeft meestal 1 terug als alles goed ging. Hang dat naïef in je CI en je hebt een pipeline die faalt bij succes. De Result:-regel bevat het aantal errors en warnings, twee sed-aanroepen vissen ze eruit, en --werror maakt van warnings een mislukking. Helemaal geen Result:-regel betekent dat MetaEditor gecrasht is of niks heeft geschreven, en dan neemt het record de ruwe log-regels mee in plaats van te doen alsof.
read_log: Windows-paden terug naar die van jou
MetaEditor schrijft zijn log als UTF-16, met Windows-paden die wijzen naar een staging-gebied waar je nooit om gevraagd hebt. read_log haalt het door iconv, maakt van Z:\ weer / en resolvet elk staging-pad met readlink naar het echte bestand erachter. Jouw header voor een eigen header, die van de toolchain voor een standaardheader, en de huidige directory gaat er vooraan af. Het is pure string-vervanging in bash, geen sed, dus geen enkel teken in een pad kan het slopen. De winst: een error in een header wijst naar de header, niet naar de EA die hem binnenhaalde.
parse_messages: één regex, één JSON-record
Elke error- en warningregel in de log ziet eruit als path(line,column) : error 256: text, en één jq-capture maakt er objecten van:
| capture("^(?<file>.*)\\((?<line>[0-9]+),(?<column>[0-9]+)\\) : (?<severity>error|warning) (?<code>[0-9]+): (?<text>.*)$")?Zonder --text is stdout JSON Lines: een file-record per bron zodra die klaar is, daarna een summary. Voortgang gaat naar stderr, ook als JSON-regels, dus 2>/dev/null laat alleen de resultaten over.
{"type":"file","source":"MQL5/Experts/Scalper.mq5","platform":"mt5","status":"failed","output":null,"errors":1,"warnings":0,"messages":[{"severity":"error","file":"MQL5/Experts/Scalper.mq5","line":41,"column":7,"code":256,"text":"undeclared identifier 'lots'"}]}
{"type":"summary","total":1,"compiled":0,"failed":1,"werror":false}Elke melding is {severity, file, line, column, code, text}, en dat is wat een LLM dat EA’s schrijft echt nodig heeft in plaats van een screenshot van een logvenster. --format gnu rendert hetzelfde record als path:line:column: error: text [code]-regels waar VS Code, vim en emacs direct naartoe springen. De exitcodes zijn expres saai: 0 alles gecompileerd, 1 minstens één mislukt, 2 foute argumenten of een setup die niet kon installeren.
mql-compiler 2>/dev/null | jq -r 'select(.status == "failed") | .source'Dat is het hele “wat is er stuk”-rapport. De repo levert ook een agent-skill mee, bij elke release gepubliceerd op ClawHub, die Claude Code, Codex, OpenClaw en peen leert om het commando te installeren, te compileren en die records echt te lezen, in plaats van te hallucineren dat de EA prima gebouwd is.
De wrapper die het allemaal aanroept
Niks van het bovenstaande draait voordat iets de container start. Je hebt Docker nodig op een x86-64-bak. Er is geen ARM-image, want de MetaEditor van MT4 heeft de i386-helft van Wine nodig, dus Apple Silicon valt af, tenzij Docker amd64 voor je emuleert.
# per user, lands in ~/.local/bin
curl -fsSL https://raw.githubusercontent.com/psyb0t/docker-mql-compiler/master/install.sh | bash
# system wide, /usr/local/bin
curl -fsSL https://raw.githubusercontent.com/psyb0t/docker-mql-compiler/master/install.sh | sudo bash -s -- --systemDe installer pint de nieuwste release-image en schrijft één bestand, het commando mql-compiler, dat een dun laagje docker run is. Draai hem opnieuw om te upgraden, --rolling pint in plaats daarvan :latest, --uninstall haalt het commando en zijn image weg. Het cachevolume blijft staan, omdat alle users op de bak het delen.
mql-compiler # every .mq4 and .mq5 under the current directory
mql-compiler MQL5/Experts/Scalper.mq5 # one source
mql-compiler --text MQL5 MQL4 # both platforms, human output
mql-compiler --watch MQL5/Experts # recompile on save until Ctrl-C
mql-compiler --werror MQL5 # warnings fail the run
mql-compiler -I ~/libs/mql5 MQL5 # shared headers outside your tree
mql-compiler --mt5-terminal ~/mt5 MQL5 # use a portable terminal already on disk
mql-compiler --setup # install the toolchain, compile nothing
mql-compiler --version # image version and MetaEditor builds
mql-compiler --update # reinstall from the newest installersZet je bronnen onder een directory die MQL4 of MQL5 heet, dezelfde indeling als de datadirectory van een terminal, met de headers in zijn Include. Argumenten mogen bestanden of directory’s zijn, relatief of absoluut, waar dan ook op de bak, platforms door elkaar. De wrapper mount elke benodigde directory op hetzelfde pad in de container en start één container voor de hele run. Draai hem kaal vanuit / en hij weigert, want je hele schijf afzoeken naar EA’s is een kutidee.
–watch
--watch houdt de container open, compileert, gaat dan op inotifywait zitten en doet een nieuwe volledige ronde zodra een .mq4, .mq5 of .mqh verandert onder je paden of in een -I-directory. Tussen de rondes laat hij de cachelock los, dus een losse compile vanuit een andere terminal komt alsnog aan de beurt. Ctrl-C stopt met 130, netjes.
–mt4-terminal and –mt5-terminal
De terminal van je broker draait een specifieke MetaEditor-build en je wilt precies die? Richt deze flags op een portable terminal-directory die al op schijf staat en compile_one draait de MetaEditor van die terminal tegen zijn eigen standaardbibliotheek. Dat platform wordt nooit gedownload, de terminal wordt read-only gemount, en laat je de flag de volgende keer weg, dan zit je weer op de gecachete build. De terminal moet één keer gestart zijn zodat hij zijn headers heeft uitgepakt, anders is het exit 2 voordat er iets compileert. Omdat de gecachete terminals naar dezelfde portable vorm verhuisd worden, is er geen verschil tussen die van mij en die van jou.
De cache houdt voor altijd de MetaEditor-build die hij als eerste installeerde. --version vertelt je op welke builds je zit, --update herinstalleert vanaf de nieuwste installers, en als dat mislukt, komt de oude toolchain onaangeroerd terug.
Hetzelfde entrypoint in een GitHub-runner
- uses: actions/checkout@v6
- uses: psyb0t/docker-mql-compiler@master
with:
paths: |
MQL5/Experts
MQL4/Indicators/Bands.mq4Die JSON-records zijn wat de Action leest. Compile-errors laten de job falen en komen als annotaties op de exacte bronregels in de PR terecht. De inputs zijn paths, include, werror, image, cache en cache-dir. Met cache aan (de standaard) staat de toolchain van 2 GB in de Actions-cache, dus alleen de eerste run vreet de installatie van 90 seconden. De outputs zijn results (het JSON Lines-bestand), total, compiled en failed, en de job-samenvatting krijgt een tabel met elk bestand. Een EA die niet compileert, haalt main nooit meer, en niemand heeft Windows aangeraakt om daarachter te komen.
De regex die een eigen fix-commit kreeg
De eerste release ging de deur uit zonder. De allereerstvolgende fix-commit in de history bestaat voor deze ene regel:
# osslsigncode's words when it could not get a CRL (its own typo included).
readonly CRL_FETCH_FAILURE="Fai(le)?d to (decode|download|get|fetch) CRL"Op de runners van GitHub kwam de Sectigo-revocatielijst die osslsigncode ophaalde afgekapt binnen, en elke eerste run zakte voor de handtekeningcheck op een kerngezonde MetaQuotes-installer, de testjob van v0.1.0 incluis. Dat opnieuw proberen zonder ook een echt foute handtekening opnieuw te proberen betekende matchen op de fouttekst van osslsigncode, en die luidt op dat pad “Faild to decode CRL”. Die (le)? is het eerlijkste stukje code in de repo, een regex die meebuigt met de tikfout van een ander zodat de check zelf geen millimeter hoeft te buigen. En de hele pipeline die hij bewaakt, compileert je EA. Een goede EA maakt hij er niet van. Als die EA schoon bouwt en daarna je account met martingale de grond in boort, dan is dat jouw eigen schuld.
De code staat hier: github.com/psyb0t/docker-mql-compiler. WTFPL, doe wat je verdomme wilt.