Jiří Hýbek
Kompilujeme po síti

Kompilujeme po síti

Jak si zjednodušit kompilaci na vzdáleném stroji?

Začal jsem si hrát s paralelním výpočtem pomocí technologie CUDA, kterou podporují grafické karty nVidia. Pracuji ovšem na Macbooku Pro, který má grafickou kartu Intel Iris, a tudíž žádná CUDA. Naštěstí můj starý Acer má grafickou kartu nVidia a běží na něm Linux, takže jsem zachráněn.

Ovšem abych mohl s CUDA pracovat, musím veškeré zdrojáky dostat na starý Acer (který mám dostupný na síti), tam je zkompilovat a spustit. Což je proces, který je během vývoje dost otravný.

Díky tomu, že OS X a Linux jsou POSIXové systémy, napsal jsem si tedy bashový script, který tento proces kompletně automatizuje přes SSH a s pomocí pár utilit.

Výsledný script je uveden na konci příspěvku.

#Začínáme

Vytvoříme si bashový script, nejlépe do /usr/bin nebo jiného adresáře, který máme v systémové cestě.

1
2
touch /usr/bin/rbuild
chmod +x /usr/bin/rbuild

A do souboru zapíšeme hlavičku, která nám říka, aby se script spustil pod interpretem BASH:

1
#!/bin/bash

#Konfigurace

Konfiguraci jsem nechtěl řešit nijak složitě, a proto jen načteme další bashový soubor ./rbuild.conf, ve kterém zapíšeme konfigurační proměnné. V případě, že soubor neexistuje, vyhodíme chybu a zobrazíme usage. Náš bashový script budeme spuštět vždy z adresáře projektu, a proto konfigurační soubor vytvoříme v něm, a ve scriptu k němu budeme přistupovat pomocí relativní cesty.

rbuild

1
2
3
4
5
6
7
8
9
#Read config
if [ ! -f "./rbuild.conf" ]; then
echo -e "\x1B[91mConfiguration file rbuild.conf not found.\x1B[0m" >&2
echo ""
usage
fi
#Import config
source ./rbuild.conf

Všiměte si zvláštní sekvence znaků \x1B[91m, ty nám do výpisu dávají trochu barev :)

./rbuild.conf

1
2
3
USERNAME=user
HOSTNAME=192.168.1.2
REMOTE_DIR=/sandbox/cpp-test

#Parsování argumentů z příkazové řádky

Jelikož náš script je trochu sofistikovanější, je potřeba vyparsovat argumenty scriptu. To provedeme pomocí utility getopts.

rbuild

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#Parse args
RUN_ARG=""
while getopts "c:" OPT; do
case $OPT in
c)
RUN_ARG="echo -e '\x1B[93mStarting...\x1B[0m'; $OPTARG"
;;
\?)
usage
;;
\:)
usage
;;
esac
done
shift "$((OPTIND-1))"
MAKE_ARG=$1

Příkaz shift nám posune seznam argumentů, které se vyparsovaly, a díky tomu proměnná $1 bude obsahovat argumenty, které zůstaly (tedy náchází se za těmi vyparsovanými).

Dále si do proměnné MAKE_ARG uložíme další argument, jelikož funkce (kterou později napíšeme) k němu nebude mít přístup.

#Seznam ignorovaných souborů a adresářů

Zdrojové kódy potřebujeme synchronizovat na vzdálený stroj, ale zároveň bychom rádi některé soubory vynechali, např. ty, které jsme si zkompilovali lokálně. Proto si vytvoříme soubor ./rbuild.ignore, který bude na každém řádku obsahovat soubory, které chceme vynechat.

Jelikož utilita rsync, kterou budeme používat, vyhodí chybu pokud soubor neexistuje, scriptem jej projistotu vytvoříme.

1
2
3
4
#Create exclude file list
if [ ! -f "./rbuild.ignore" ]; then
touch ./rbuild.ignore
fi

#Nápověda

Jsme zvyklí na to, že pokud příkaz spustíme s neplatnými parametry, vypíše se nám nápověda (tzv. usage). Proto si na začátek scriptu (pod hlavičku) vytvoříme funkci usage, na kterou jsme se již odkazovali výše, a která nám vypíše nápovědu.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function usage {
echo "Usage: rbuild [make target] [-c command]"
echo ""
echo "Options:"
echo " -c Command to run after build"
echo ""
echo "Project files:"
echo " ./rbuild.conf Bash config file"
echo " USERNAME=<remote ssh username>"
echo " HOSTNAME=<remote ssh hostname>"
echo " REMOT_DIR=<remote project dir>"
echo ""
echo " ./rbuild.ignore File with exclude patterns (one per line)"
exit 2
}

#Synchronizace, kompilace a spuštění

Nyní si pod usage vytvoříme funkci remote_build.

1
2
3
function remote_build {
}

Do funkce zapíšeme volání utility rsync, která nám zařídí synchronizaci lokálních souborů projektu s těmi vzdálenými a to velmi efektivně (jen rozdíly + komprese).

Můžete si všimnout uvedených přepínačů –exclude–exclude-from, kterými říkáme, jaké soubory a adresáře chceme vynechat.

1
2
3
#Start sync
echo -e "\x1B[93mSyncing project...\x1B[0m"
rsync -az --exclude=rbuild.conf --exclude rbuild.ignore --exclude-from ./rbuild.ignore ./ ${USERNAME}@${HOSTNAME}:${REMOTE_DIR}

Nyní se přes SSH přesuneme do vzdáleného adresáře projektu, zavoláme make a případně spustíme příkaz, který jsme nastavili přepínačem -c.

1
2
3
#Build and run
echo -e "\x1B[93mBuilding...\x1B[0m"
ssh ${USERNAME}@${HOSTNAME} "cd ${REMOTE_DIR}; make ${MAKE_ARG}; ${RUN_ARG}"

Nakonec celého scriptu přidáme následující řádku, která nám zavolá funkci remote_build, pokud konfigurace šla hladce.

1
2
#Build
remote_build

POZOR: Script počítá s použitím utility make a souboru Makefile

#Použití

Mějme testovací projekt s následující souborovou strukturou:

1
2
3
4
5
6
7
8
./projekt
./build
./obj
./src
./main.cpp
./rbuild.conf
./rbuild.ignore
./Makefile

Soubor rbuild.conf vytvoříme dle výše uvedeného příkladu.

Do souboru rbuild.ignore zapíšeme následující řádky, abychom vynechali lokálně zkompilované soubory:

1
2
build/*
obj/*

#Makefile

Vytvoříme si jednoduchý Makefile s targety buildclean:

POZOR: Makefile strikně využívá indentaci tabelátory, takže pokud následující kód zkopírujete, musíte jej přeformátovat.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.PHONY: build
.PHONY: clean
BUILD = build/main
OBJ_DIR = obj
SRC_DIR = src
OBJECTS = main.o
OBJS = $(patsubst %,$(OBJ_DIR)/%,$(OBJECTS))
build: ${OBJS}
gcc ${OBJS} -o ${BUILD}
clean:
rm -f ${BUILD}
rm -f ${OBJ_DIR}/*.o
${OBJ_DIR}/%.o: ${SRC_DIR}/%.cpp
gcc -c -o $@ $<

#main.cpp

Vytvoříme jednoduchý C++ hello world soubor ./src/main.cpp:

1
2
3
4
5
6
#include <stdio.h>
int main(){
printf("Hello world!\n");
return 0;
}

#Kompilace

Zkusíme si projekt zkompilovat lokálně a následně spustit:

1
2
3
4
5
6
projekt user$ make
gcc -c -o obj/main.o src/main.cpp
gcc obj/main.o -o build/main
projekt user$ ./build/main
Hello world!

A nyní projekt zkusíme zkompilovat vzdáleně a výsledný program spustit:

1
2
3
4
5
6
7
projekt user$ rbuild -c ./build/main
Syncing project...
Building...
gcc -c -o obj/main.o src/main.cpp
gcc obj/main.o -o build/main
Starting...
Hello world!

Hotovo, projekt zkompilován a aplikace úspěšně spuštěna!

Nakonec si ještě ukážeme, jak utilitě make předat target clean:

1
2
3
4
5
projekt user$ rbuild clean
Syncing project...
Building...
rm -f build/main
rm -f obj/*.o

#Závěrem

Doufám, že vám tento příspěvek byl k něčemu užitečný. Mě script ušetřil spoustu času a navíc jsem se naučil zajímavé věci:

#Výsledný script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#!/bin/bash
function remote_build {
#Start sync
echo -e "\x1B[93mSyncing project...\x1B[0m"
rsync -az --exclude=rbuild.conf --exclude rbuild.ignore --exclude-from ./rbuild.ignore ./ ${USERNAME}@${HOSTNAME}:${REMOTE_DIR}
#Build and run
echo -e "\x1B[93mBuilding...\x1B[0m"
ssh ${USERNAME}@${HOSTNAME} "cd ${REMOTE_DIR}; make ${MAKE_ARG}; ${RUN_ARG}"
}
function usage {
echo "Usage: rbuild [make target] [-c command]"
echo ""
echo "Options:"
echo " -c Command to run after build"
echo ""
echo "Project files:"
echo " ./rbuild.conf Bash config file"
echo " USERNAME=<remote ssh username>"
echo " HOSTNAME=<remote ssh hostname>"
echo " REMOT_DIR=<remote project dir>"
echo ""
echo " ./rbuild.ignore File with exclude patterns (one per line)"
exit 2
}
#Read config
if [ ! -f "./rbuild.conf" ]; then
echo -e "\x1B[91mConfiguration file rbuild.conf not found.\x1B[0m" >&2
echo ""
usage
fi
#Import config
source ./rbuild.conf
#Parse args
RUN_ARG=""
while getopts "c:" OPT; do
case $OPT in
c)
RUN_ARG="echo -e '\x1B[93mStarting...\x1B[0m'; $OPTARG"
;;
\?)
usage
;;
\:)
usage
;;
esac
done
shift "$((OPTIND-1))"
MAKE_ARG=$1
#Create exclude file list
if [ ! -f "./rbuild.ignore" ]; then
touch ./rbuild.ignore
fi
#Build
remote_build