Astea sunt liniile pentru care există tot proiectul. Tot restul din docker-mql-compiler e pregătire pentru ele sau curățenie după ele:
# 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 || trueĂsta e MetaEditor-ul de la MetaQuotes, singura chestie de pe planetă care face dintr-un .mq5 un .ex5, un GUI de Windows care n-a auzit în viața lui de Linux, pornit de un script bash dintr-un container cu Wine, cu Xvfb pe post de ecran fals la care nu se uită nimeni. Uită-te la || true. Exit code-ul e aruncat la gunoi intenționat, pentru că minte. Editorul meu, agenții mei și CI-ul meu stau toți pe Linux, și m-am săturat să butoneze cineva o mașină cu Windows ca să dea click pe “Compile” și să copieze de mână erorile dintr-un panou de log. Așa că o să citesc entrypoint.sh în ordinea în care rulează: de la primul download până la linia de JSON pe care o citește un agent la final.
Puțin context mai întâi. mt5-httpapi are deja o rută POST /compile, intră sursă MQL5 ca JSON, iese un .ex5 în base64. Numai că stă într-un VM întreg de Windows sub KVM, care rulează terminale live și tranzacționează bani pe bune, știe doar MQL5, refuză include-urile cu căi absolute sau cu .., iar bibliotecile comune trebuie să le urci întâi în arborele lui de compilare. Ăsta compilează un director de cod MT4 și MT5 pe un laptop sau pe un runner GitHub fără să pornească Windows deloc.
verify_installer: semnat de MetaQuotes sau refuzat
Imaginea e Debian trixie cu Wine (64-bit plus jumătatea de 32-bit, pentru că MetaEditor-ul antic de la MT4 e cod Windows pe 32 de biți) și niciun byte de la MetaQuotes. MetaTrader nu e al meu ca să-l redistribui, așa că prima rulare descarcă installerele (pe cele de la RoboForex implicit, le schimbi cu MQL_COMPILER_MT4_URL și MQL_COMPILER_MT5_URL). Ia doar ce-i trebuie rulării: numai fișiere .mq5 înseamnă că se instalează doar MT5. Cam un minut și jumătate prima dată, câteva secunde după aia.
Înainte ca wine să atingă vreun installer, osslsigncode îi verifică semnătura Authenticode față de bundle-ul de CA din imagine, iar apoi asta verifică cine l-a semnat de fapt:
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 e MetaQuotes. Contează doar certificatul semnatarului, nu lanțul sau certificatele de timestamp care stau în același raport. O semnătură validă de la altcineva tot refuz înseamnă. Dacă serverul de revocare al CA-ului e picat, verify_installer încearcă de trei ori, apoi verifică digest-ul, lanțul și semnatarul fără interogarea de revocare și scrie un warning cu reason=crl_unreachable, ca să știi că s-a întâmplat.
unpack_stdlib: pornești terminalul o dată ca să existe Trade.mqh
Installerul nu pune biblioteca standard pe disc. O despachetează terminalul prima dată când pornește. Instalezi MetaEditor sub Wine, sari peste pasul ăsta și orice #include <Trade/Trade.mqh> crapă. Așa că setup-ul pornește fiecare terminal headless, în mod portabil, și așteaptă fișierul marcaj:
(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))
doneMarcajul e Include/Trade/Trade.mqh pentru MT5 și stdlib.mqh pentru MT4. Cum apare, wineserver -k omoară terminalul, care oricum nu se închide singur niciodată. Terminalele instalate sunt apoi mutate din Program Files în directoare portabile simple, drive_c/terminals/mt4 și drive_c/terminals/mt5, în volumul Docker mql-compiler-cache. Cam 2 GB, câte un subdirector pentru fiecare ID de utilizator. Containerul rulează cu UID-ul tău, deci fișierele compilate sunt ale tale, nu ale lui root.
build_merged_root: patru straturi de include, al tău deasupra
MetaEditor citește #include <...> dintr-un singur director Include, fix unul. Ție îți trebuie minim două, biblioteca standard de la MetaQuotes și header-ele tale. Asta e funcția care face tool-ul chiar utilizabil:
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"
doneCitește-o de sus în jos și ai ordinea straturilor:
Include-ul standard al terminalului (Trade/,Arrays/,stdlib.mqh, tot tacâmul).- Fiecare director
-I, în ordinea în care le-ai dat. Include-ul tău.--remove-destinationînseamnă că la conflict câștigă stratul de mai târziu, iar al tău e ultimul. Ai unTrade/Trade.mqhpeticit în arborele tău? Al tău se compilează.- Link-uri către restul directorului tău
MQL4/MQL5(Files,Imagesși gașca), ca să se rezolve#resource "\\Files\\data.txt".
Totul e symlink-uri (cp -rs) într-o zonă de staging, construită o dată per platformă și per director de utilizator, la fiecare trecere. Nu se scrie nimic în directoarele tale și dispare tot când se termină rularea. Care director MQL4 sau MQL5 e considerat al tău se decide simplu: cel mai apropiat deasupra fișierului sursă. Extensia singură alege compilatorul, .mq4 la metaeditor.exe de la MT4, .mq5 la MetaEditor64.exe de la MT5.
stage_source: un link pe lângă spații
Terminalul tău stă în MT5 - SomeBroker? Și /compile, și /inc de la MetaEditor se îneacă în spații. Așa că o sursă cu spații în cale se compilează printr-un link:
[[ "$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"}"Link-ul arată spre tot directorul MQL4/MQL5, nu spre fișier, așa că include-urile cu "../" din el tot se rezolvă. Din același motiv stă și rădăcina de include combinată în zona de staging: directorul unui terminal montat are des spații în nume.
compile_one: întâi șterge, apoi nu crede exit code-ul
Fix înainte de apelul wine din capul articolului e asta:
# 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"Orice .ex4/.ex5 găsești acolo după rulare a ieșit din rularea asta. Niciun binar vechi care se dă drept proaspăt după ce MetaEditor a crăpat. Fișierele se compilează pe rând, niciodată în paralel, pentru că două MetaEditor-uri sub Wine deodată e rețeta după care chemi dracii.
După apel, verdictul vine din log, nu din exit code:
result="$(grep -E '^Result: ' <<<"$lines" | tail -n 1)" || result=""Modul de linie de comandă al MetaEditor-ului întoarce de obicei 1 când totul a mers bine. Legi asta naiv în CI și ai un pipeline care pică la succes. Linia Result: conține numărul de erori și de warning-uri, două apeluri de sed le scot de acolo, iar --werror transformă warning-urile în eșec. Nicio linie Result: înseamnă că MetaEditor a crăpat sau n-a scris nimic, iar înregistrarea duce liniile brute de log în loc să se prefacă.
read_log: căile de Windows înapoi în ale tale
MetaEditor își scrie log-ul în UTF-16, cu căi de Windows care arată într-o zonă de staging despre care n-ai întrebat niciodată. read_log îl trece prin iconv, transformă Z:\ înapoi în / și rezolvă fiecare cale din staging la fișierul real din spatele ei cu readlink. Header-ul tău pentru un header de-al tău, al toolchain-ului pentru unul standard, iar directorul curent e tăiat din față. E înlocuire de string-uri în bash pur, nu sed, așa că niciun caracter dintr-o cale nu-l poate strica. Câștigul: o eroare dintr-un header arată spre header, nu spre EA-ul care l-a inclus.
parse_messages: un regex, o înregistrare JSON
Fiecare linie de eroare și de warning din log arată ca path(line,column) : error 256: text, iar un singur capture din jq le face obiecte:
| capture("^(?<file>.*)\\((?<line>[0-9]+),(?<column>[0-9]+)\\) : (?<severity>error|warning) (?<code>[0-9]+): (?<text>.*)$")?Fără --text, stdout e JSON Lines: câte o înregistrare file pentru fiecare sursă pe măsură ce termină, apoi un summary. Progresul merge pe stderr, tot ca linii JSON, deci 2>/dev/null lasă doar rezultatele.
{"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}Fiecare mesaj e {severity, file, line, column, code, text}, adică exact ce-i trebuie unui LLM care scrie EA-uri, în loc de un screenshot cu un panou de log. --format gnu afișează aceeași înregistrare ca linii path:line:column: error: text [code] la care VS Code, vim și emacs sar direct. Exit code-urile sunt plictisitoare intenționat: 0 s-a compilat tot, 1 a picat cel puțin unul, 2 argumente greșite sau un setup care n-a reușit să instaleze.
mql-compiler 2>/dev/null | jq -r 'select(.status == "failed") | .source'Ăsta e tot raportul de “ce s-a stricat”. Repo-ul vine și cu un skill pentru agenți, publicat pe ClawHub la fiecare release, care îi învață pe Claude Code, Codex, OpenClaw și peen să instaleze comanda, să compileze și să citească efectiv înregistrările astea, în loc să halucineze că EA-ul s-a compilat fără probleme.
Wrapper-ul care le cheamă pe toate
Nimic din ce e mai sus nu rulează până nu pornește ceva containerul. Îți trebuie Docker pe o mașină x86-64. Nu există imagine ARM, pentru că MetaEditor-ul de la MT4 are nevoie de jumătatea i386 a lui Wine, deci Apple Silicon e pe dinafară, doar dacă nu-ți emulează Docker amd64.
# 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 -- --systemInstallerul fixează cea mai nouă imagine de release și scrie un singur fișier, comanda mql-compiler, care e un docker run subțire. Îl rulezi din nou ca să faci upgrade, --rolling fixează în schimb :latest, --uninstall scoate comanda și imaginea ei. Volumul de cache rămâne, pentru că îl împart toți utilizatorii de pe mașină.
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 installersȚine sursele într-un director numit MQL4 sau MQL5, aceeași structură ca directorul de date al unui terminal, cu header-ele în Include-ul lui. Argumentele pot fi fișiere sau directoare, relative sau absolute, oriunde pe mașină, cu platforme amestecate. Wrapper-ul montează fiecare director necesar la aceeași cale în container și pornește un singur container pentru toată rularea. Rulează-l gol din / și refuză, pentru că să-ți scanezi tot discul după EA-uri e o idee de căcat.
–watch
--watch ține containerul deschis, compilează, apoi stă pe inotifywait și mai face o trecere completă de fiecare dată când se schimbă un .mq4, .mq5 sau .mqh în căile tale sau într-un director -I. Între treceri dă drumul lock-ului de pe cache, așa că o compilare izolată dintr-un alt terminal tot își primește rândul. Ctrl-C iese cu 130, curat.
–mt4-terminal and –mt5-terminal
Terminalul brokerului tău rulează un anumit build de MetaEditor și îl vrei fix pe ăla? Îndreaptă flag-urile astea spre un director de terminal portabil care e deja pe disc, iar compile_one rulează MetaEditor-ul lui pe biblioteca lui standard. Platforma aia nu se mai descarcă deloc, terminalul e montat read-only, iar dacă data viitoare nu mai pui flag-ul, ești înapoi pe build-ul din cache. Terminalul trebuie să fi fost pornit o dată, ca să-și fi despachetat header-ele, altfel e exit 2 înainte să se compileze ceva. Cum terminalele din cache sunt mutate în aceeași formă portabilă, nu e nicio diferență între ale mele și al tău.
Cache-ul păstrează pentru totdeauna build-ul de MetaEditor pe care l-a instalat primul. --version îți spune pe ce build-uri ești, --update reinstalează din cele mai noi installere, iar dacă asta pică, vechiul toolchain revine neatins.
Același entrypoint pe un runner GitHub
- uses: actions/checkout@v6
- uses: psyb0t/docker-mql-compiler@master
with:
paths: |
MQL5/Experts
MQL4/Indicators/Bands.mq4Înregistrările alea JSON sunt ce citește Action-ul. Erorile de compilare pică job-ul și apar ca adnotări fix pe liniile din sursă, în PR. Input-urile sunt paths, include, werror, image, cache și cache-dir. Cu cache pornit (implicit), toolchain-ul de 2 GB stă în cache-ul de Actions, deci doar prima rulare înghite instalarea de 90 de secunde. Output-urile sunt results (fișierul JSON Lines), total, compiled și failed, iar sumarul job-ului primește un tabel cu fiecare fișier. Un EA care nu se compilează nu mai ajunge niciodată în main, și n-a atins nimeni Windows ca să afle asta.
Regex-ul care și-a primit propriul commit de fix
Primul release a ieșit fără el. Următorul commit de fix din istoric există pentru linia asta:
# 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"Pe runnerele GitHub, descărcarea listei de revocare Sectigo făcută de osslsigncode venea trunchiată, și fiecare primă rulare pica verificarea semnăturii pe un installer MetaQuotes perfect în regulă, inclusiv job-ul de test din v0.1.0. Ca să reîncerci asta fără să reîncerci și o semnătură chiar proastă, trebuia să prinzi textul erorii de la osslsigncode, care pe drumul ăla sună “Faild to decode CRL”. (le)?-ul ăla e cea mai cinstită bucată de cod din repo, un regex care se îndoaie după greșeala de tipar a altuia ca verificarea să nu se îndoaie deloc. Și tot pipeline-ul pe care îl păzește îți compilează EA-ul. Nu ți-l face bun. Dacă EA-ul se compilează curat și apoi îți dă contul de pământ cu martingale, aia e pe tine.
Codul e aici: github.com/psyb0t/docker-mql-compiler. WTFPL, fă ce pula mea vrei.