Je jedna vec, ktorú by som chcel poriadne vyriešiť už celkom dlho:

Vytvoriť spustiteľnú NodeJS aplikáciu na x64 stroji pre ARMv6

Teraz mám tento proces do určitej miery zvládnutý, s nasledovnými krokmi:

  1. Napísať aplikáciu na hostiteľskom x64 stroji
  2. scp zdrojové súbory na cieľový ARMv6 stroj (napr. počítač na báze Raspberry Pi)
  3. Vytvoriť spustiteľný súbor pomocou pkg odoslaním príkazov cez ssh
  4. Zmazať zdrojové súbory na cieľovom stroji
  5. VOLITEĽNE: Spustiť natívnu systemd alebo pm2 službu, ktorá udržuje aplikáciu bežiacu

Tento postup je v súčasnosti automatizovaný na jediný npm run príkaz. Má však niekoľko nevýhod, ktoré by som časom chcel odstrániť:

  1. Vyžaduje bežiaci stroj s cieľovou architektúrou, ku ktorému sa dá pripojiť cez ssh (to by sa dalo nahradiť QEMU a/alebo Dockerom, no zatiaľ som sa k tomu nedostal)
  2. Ladenie je menej priamočiare
  3. Zbytočne sa prenášajú zdrojové súbory, niektoré môžu byť omylom ponechané
  4. Vyžaduje node_modules/ na cieľovom stroji – môžu byť niekoľkonásobne väčšie ako samotná aplikácia (ktorá v sebe zahŕňa node spustiteľný súbor v rozmedzí 35 ~ 70 MB, v závislosti od verzie a architektúry)

Samozrejme, bolo by jednoduchšie zostaviť spustiteľný súbor a len ho preniesť na cieľový stroj. C, Go a Rust to zvládnu bez väčších komplikácií.

Príprava #

Viacerí ľudia by chceli cross-kompilovať pre inú architektúru, ako vidno z #136, #145, #363, #605, #784 a ďalších zdrojov. Jedno riešenie je získať binárny súbor pre cieľovú architektúru. Repozitár tiež umožňuje pripraviť binárne súbory na vlastnom stroji pomocou Docker image, hoci používatelia hlásili, že proces trvá 10 hodín a viac.

V minulosti som ho zostavoval len tak zo zvedavosti, no momentálne repozitár obsahuje množstvo predzostavených binárnych súborov. fetched-v14.4.0-linux-armv6 by mi mal dobre poslúžiť, ak ho uložím na správne miesto (momentálne ~/.pkg-cache/v2.6/). Skúsme:

pkg -t arm64 app.js
> [email protected]
> Warning Failed to make bytecode node14-arm64 for file /snapshot/test/app.js

Čo nefunguje #

Prvé navrhované riešenie v spomínaných issue vláknach je použiť --no-bytecode. Výsledky sú neuspokojivé:

pkg -t arm64 app.js --no-bytecode
> [email protected]
> Error! --no-bytecode and no source breaks final executable
  /home/peterbabic/app.js
  Please run with "-d" and without "--no-bytecode" first, and make
  sure that debug log does not contain "was included as bytecode".

Vypíše -d, čo znamená --debug, užitočné informácie? Dostaneme sa k tomu za chvíľu.

Pridanie architektúry #

Hľadajúc ďalej, distribúcie na báze Debianu majú zdanlivé riešenie v pridávaní knižníc architektúry z repozitára:

dpkg --add-architecture i386
apt-get
apt-get install -y libc6:i386 libstdc++6:i386

Bohužiaľ, na Arch Linuxe neexistuje ekvivalentná sada príkazov. Lákalo by ma to vyskúšať vo virtuálnom stroji. Ale zatiaľ vysvetlím kroky, ktorými som prešiel pri pokuse o funkčnú cross-kompiláciu.

Zostavenie ARM binárnych súborov na AMDx64 #

Kroky na skutočné skompilovanie spustiteľného ARMx86 (ARMv6/ARMv7) a ARMx64 (ARMv8) binárneho súboru na mojom laptope s 64-bitovým Intel procesorom začínajú nástrojovým reťazcom.

yay -S arm-linux-gnueabihf-gcc

V čase písania je balík označený ako zastaraný a nenainštaluje sa. Nie dobré. Je dostupný aj x64 nástrojový reťazec, podporovaný kompilátorom s názvom aarch64-linux-gnu-gcc. Nevedel som, do ktorého balíka patrí, tak som spustil tento príkaz:

pacman -Fq aarch64-linux-gnu-gcc | sudo pacman -S -

Áno, balík má rovnaký názov ako príkaz. Čo som si myslel. Nevadí, bolo to len pár zbytočných klávesových skratiek, ale teraz vieme zostaviť Hello World! pre ARM x64 na x86_64 stroji:

aarch64-linux-gnu-gcc hello.c -o hello

Spustenie ARM binárnych súborov na AMDx64 #

Spustenie priamo nebude fungovať:

$ file hello
hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=12eca1ab69cdf6c78169cb8a9c86cf21ea8c5873, for GNU/Linux 3.7.0, not stripped

$ ./hello
zsh: exec format error: ./cross

Na spustenie ARM aarch64 spustiteľného súboru potrebujeme qemu-aarch64:

pacman -Fq qemu-aarch64 | sudo pacman -S -

Spustíme náš cross-skompilovaný hello spustiteľný súbor:

qemu-aarch64 hello

Spustiteľný súbor by nás mal pozdraviť namiesto zobrazenia chyby.

Spustenie predskompilovaného NodeJS ARM x64 spustiteľného súboru #

S novonadobudnutými znalosťami môžeme skúsiť spustiť node spustiteľný súbor zo začiatku, ktorý bude zabalený pomocou pkg. Keďže náš nástrojový reťazec pre ARM x86 momentálne nefunguje, skúsime x64 variantu.

wget https://github.com/robertsLando/pkg-binaries/releases/download/v1.0.0/fetched-v14.4.0-linux-arm64 -P ~/.pkg-cache/v2.6

Ako poznámku na okraj, ARM x64 asi ešte nejaký čas trvá kým sa rozšíri. Raspberry Pi 4 sa už tohto problému dotýka, hoci som jeden zatiaľ v ruke nemal. Ale byť pripravený na budúcnosť sa niekedy oplatí.

Prejdeme do priečinka so stiahnutím a preskúmame súbor – dostaneme očakávaný ARM aarch64:

$ file fetched-v14.4.0-linux-arm64
fetched-v14.4.0-linux-arm64: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (GNU/Linux), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=c80da3252b3b6bc0dedfa29f77b38de5f55e771e, with debug_info, not stripped

Spustenie tohto binárneho súboru priamo fungovať nebude:

$ ./fetched-v14.4.0-linux-arm64
zsh: exec format error: ./fetched-v14.4.0-linux-arm64

Neočakávané je, že spustenie cez qemu-aarch64, ktoré predtým fungovalo, tiež zlyhá:

$ qemu-aarch64 fetched-v14.4.0-linux-arm64
/lib/ld-linux-aarch64.so.1: No such file or directory

Ktorý balík obsahuje tento súbor?

$ pacman -F ld-linux-aarch64.so.1
community/aarch64-linux-gnu-glibc 2.32-1 [installed]
    usr/aarch64-linux-gnu/lib/ld-linux-aarch64.so.1

Balík aarch64-linux-gnu-glibc bol nainštalovaný pred pár krokmi spolu s cross-kompilátorom aarch64-linux-gnu-gcc, ako vidno tu:

$ pacman -Si aarch64-linux-gnu-glibc | rg Required
Required By      : aarch64-linux-gnu-gcc

Rýchla kontrola – súbor naozaj existuje:

$ file /usr/aarch64-linux-gnu/lib/ld-linux-aarch64.so.1
/usr/aarch64-linux-gnu/lib/ld-linux-aarch64.so.1: symbolic link to ld-2.32.so

A ten, ktorý spustiteľný súbor hľadal, neexistuje:

$ file /lib/ld-linux-aarch64.so.1
/lib/ld-linux-aarch64.so.1: cannot open `/lib/ld-linux-aarch64.so.1' (No such file or directory)

Zjavné nečisté riešenie, ktoré by znečistilo /lib na tvojom stroji knižnicami pre rôzne architektúry a neskôr by pravdepodobne zlyhalo na ďalších závislostiach, by bolo kopírovanie (horšie) alebo symlink (lepšie):

sudo ln -s /usr/aarch64-linux-gnu/lib/ld-linux-aarch64.so.1 /lib

Predskompilovaný binárny súbor by teraz mal fungovať. Odstráň symlink, ak si to skúsil. Existuje lepšie riešenie:

qemu-aarch64 -L /usr/aarch64-linux-gnu/ fetched-v14.4.0-linux-arm64

Takto qemu vie, kde hľadať knižnice. Môžeš si z toho urobiť alias a považovať to za hotové, ak chceš len spúšťať binárne súbory príkazom. Ale toto predsa nie je náš cieľ, pamätáš? Potrebujeme globálnejší spôsob, ako emulátoru povedať, kde sú požadované knižnice. Jedným spôsobom je poskytnúť tieto informácie cez premennú prostredia QEMU_LD_PREFIX, ktorá je ekvivalentom parametra -L:

QEMU_LD_PREFIX=/usr/aarch64-linux-gnu/ qemu-aarch64 fetched-v14.4.0-linux-arm64

Ak používame qemu len pre jednu architektúru naraz, môžeme premennú exportovať:

export QEMU_LD_PREFIX=/usr/aarch64-linux-gnu/

S exportovanou premennou môžeme spustiť predzostavený binárny súbor:

$ qemu-aarch64 fetched-v14.4.0-linux-arm64
internal/validators.js:121
    throw new ERR_INVALID_ARG_TYPE(name, 'string', value);
    ^

TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received undefined
    at validateString (internal/validators.js:121:11)
    at Object.resolve (path.js:980:7)
    at resolveMainPath (internal/modules/run_main.js:12:40)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:65:24)
    at internal/main/run_main_module.js:17:47 {
  code: 'ERR_INVALID_ARG_TYPE'
}

To NodeJS používateľom znie povedomé, nie? V skutočnosti sme to spustili. Dôvod zlyhania je pravdepodobne to, že binárny súbor ešte nie je zabalený – čiže neobsahuje kód na spustenie, ktorý zatiaľ nemôže nájsť.

Bohužiaľ, aj keď teraz vieme spustiť tento binárny súbor na hostiteľskom stroji, príkaz pkg v priečinku s projektom stále zlyhá:

$ npx pkg -t arm64 app.js -d

... dlhý výstup vynechaný ...

/home/peterbabic/.pkg-cache/v2.6/fetched-v14.4.0-linux-arm64: /home/peterbabic/.pkg-cache/v2.6/fetched-v14.4.0-linux-arm64: cannot execute binary file
/home/peterbabic/.pkg-cache/v2.6/fetched-v14.4.0-linux-arm64: /home/peterbabic/.pkg-cache/v2.6/fetched-v14.4.0-linux-arm64: cannot execute binary file
> Warning Failed to make bytecode node14-arm64 for file /snapshot/app.js

Vidíme, že správne volá binárny súbor, ktorý sme pred chvíľou vedeli spustiť samostatne, ale teraz je problém v tom, že pkg nemá ako vedieť, že potrebuje volať qemu-aarch64, aby spustil ten binárny súbor transparentne. Na to potrebujeme nastaviť binfmt.

Transparentné spustenie cudzieho binárneho súboru #

Zistil som, že na natívne spúšťanie cudzích binárnych súborov môžem urobiť toto:

yay -S binfmt-qemu-static

Má aj voliteľný balík, ktorý stojí za zmienku a ktorý tiež inštalujem:

$ yay -Si binfmt-qemu-static | rg Optional
Optional Deps   : qemu-user-static

S QEMU_LD_PREFIX nastaveným môžeme spustiť predskompilovaný binárny súbor takto:

~/.pkg-cache/v2.6/fetched-v14.4.0-linux-arm64

Ale toto nám stále neumožňuje spustiť pkg.

$ npx pkg -t arm64 app.js -d

... dlhý výstup vynechaný ...

/lib/ld-linux-aarch64.so.1: No such file or directory
/lib/ld-linux-aarch64.so.1: No such file or directory
> Warning Failed to make bytecode node14-arm64 for file /snapshot/app.js

V tomto bode sa mi veci začínajú zamotávať, pretože QEMU_LD_PREFIX sa zdá byť ignorovaný, keď ho pkg potrebuje (je spúšťaný cez npm/npx/pkg, z ktorých žiadny neposkytuje žiadnu informáciu príkazom ldd).

Dokázal som postúpiť ďalej tým, že som sa uchýlil k symlinkovaniu knižnice do /lib, ako som spomínal skôr:

$ npx pkg -t arm64 app.js -d

... dlhý výstup vynechaný ...

/home/peterbabic/.pkg-cache/v2.6/fetched-v14.4.0-linux-arm64: error while loading shared libraries: libdl.so.2: cannot open shared object file: No such file or directory
/home/peterbabic/.pkg-cache/v2.6/fetched-v14.4.0-linux-arm64: error while loading shared libraries: libdl.so.2: cannot open shared object file: No such file or directory
> Warning Failed to make bytecode node14-arm64 for file /snapshot/v2.6/app.js

Tu je pre mňa slepá ulička. Nech som skúšal akékoľvek symlink triky, odmieta nájsť ten súbor, ktorý sa nachádza na /usr/aarch64-linux-gnu/lib/libdl.so.2. Môj stroj má aj súbor /lib/libdl.so.2, ktorý je samozrejme skompilovaný pre x86_64, takže symlinkovananie je definitívne riskantné a musí existovať iný spôsob. Ak vieš viac, daj mi vedieť.

Poznámka na okraj #

Občas to tiež uviazne na chýbajúcom libstdc++.so.6. Táto knižnica sa nachádza v /usr/aarch64-linux-gnu/lib64. Po rozsiahlom hľadaní po internete som našiel hacky riešenie:

export LD_LIBRARY_PATH=/usr/aarch64-linux-gnu/lib64

Ale tejto premennej prostredia by sa malo vyhnúť z dôvodov, ktoré som zatiaľ plne nepochopil.

Záver #

Postup na prípravu funkčného binárneho súboru NodeJS aplikácie na AMDx64 stroji pre ARMx86 architektúru by mi umožnil rýchlejší cyklus zostavovania. Žiaľ, nech som skúšal čokoľvek, riešenie mi stále uniká.

Cesta zdokumentovaná v tomto článku mi poslúžila ako bohatý vzdelávací kurz, takže nie je všetko stratené. Dúfam, že aj ty tu nájdeš niečo zaujímavé.

Zdroje #