Organizační pokyny

  • Odpovědník z organizačních pokynů v IS (Student -> Odpovědníky -> Organizační pokyny PB176)
  • 9-10 výukových přednášek
    • Organizovány do 5 tématických bloků
    • Ke každému bloku zpravidla elearningová laboratoř (4 laboratoře)
      • Nutno získat alespoň 75% bodů z laboratoří (tj. 1 lze vynechat)
  • 3 zvané přednášky (přednášející zatím neukotveni)
    • Korporátní prostředí
    • Zajetý "startup" či menší firma
    • Kritické aplikace
  • Kobayashi Maru
    • Název čistě záměrně náhodný, v jarním semestru 2021 neuskutečněna
    • Noční týmový projekt, účast povinná 1
    • Umístěn do noci z pátku na sobotu po odpřednášení výukového bloku
      • Bude vypsán v IS jako zkouškový termín, kam budete automaticky přihlášeni
  • Kolokvium
    • Zkoušení skupinovou rozpravou
      • Nadhodím téma k diskusi, diskutujete ale zejména vy
    • Reflexe zážitku z Kobayashi Maru a laboratoří
    • Termíny ve zkouškovém období

Přednášky

  • Účast na přednáškách není ze studijního a zkušebního řádu možno vymáhat
  • Záznamy přednášek zveřejňovány v ISu
  • Dotazy během přednášky: https://sli.do, událost #98176
  • Dotazy k laboratořím: diskusní fórum v IS, issues v Gitlabu
  • Přednáška: st 12:00-13:50, D1
  • Prostředí přednášky i interaktivní příklady lze spustit i lokálně (pokročilejší):
    [linux@localhost:~/pb176_prednasky] sed "s/UID=.*/UID=$(id -u)/g" < docker-compose.yml > compose-local.yml
    [linux@localhost:~/pb176_prednasky] __mezera__ env GITLAB_CREDENTIALS=xlogin:heslo docker-compose -f compose-local.yml up
    

Laboratoře

  • Zveřejnění zadání - nový issue v repozitáři https:/gitlab.fi.muni.cz/pb176/laboratore
    • Důrazně doporučeno nastavit si upozornění v gitlabu
  • Zadání první laboratoře bude zveřejněno i v diskusním fóru

Lze očekávat vydýchaný vzduch, deadline na krku a pokud to fakulta povolí, pizzu a kávu. Pokud budete nemocní (a řádně omluvení), najdeme nějaké jiné téma jak podobný zářez splnit. Cílem rozhodně není Vám udělit X, pokud byste v 19:05 usnuli vyčerpáním. Pokud v tomto plánu pozorujete nějaký problém, budeme jej řešit individuálně.


  1. Plán je obsadit D1 v noci z pátku na sobotu.

Blok 1 - Verzování

Drobná ilustrace chaosu, který je nejspíše důvěrně znám

Úkol pro bystrého posluchače: najít po 6 měsících od odevzdání správnou verzi.

Nalezení rozdílů

Základní otázkou, kterou je pro vyřešení předchozího příkladu zapotřebí odpovědět, je shoda - tedy spíše zobrazení rozdílů - mezi soubory. Jistě, pokud by byl autor předvídavý, mohl by si úkol usnadnit například tak, že si ke každému adresáři přidá do názvu datum. Ale ani to neřeší vše. V praxi nezřídkakdy máme situaci, kdy můžeme stejného výsledku dosáhnout různým zápisem. Nezbude tedy, než porovnat jednotlivé soubory.

Jedním ze základních nástrojů k porovnávání obsahu souborů je diff (který možná již znáte z Unixu, Linuxu, ...). Pro začátek tedy porovnejme soubory z adresářů "a" a "b".

In [1]:
test -d priklady || git clone https://gitlab.fi.muni.cz/pb176/private/2022-priklady-todo.git priklady
pushd priklady/blok1_diff > /dev/null
diff a b
popd > /dev/null
Cloning into 'priklady'...
remote: Enumerating objects: 160, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 160 (delta 0), reused 0 (delta 0), pack-reused 156
Receiving objects: 100% (160/160), 21.21 KiB | 10.61 MiB/s, done.
Resolving deltas: 100% (42/42), done.
diff a/postup_vyroby.txt b/postup_vyroby.txt
2a3
> Při jejich zakládání přijde vhod, pokud předem vybereme kousky, které se navzájem doplní.
4c5
< Do tohoto je potřeba přidat vejce.
---
> Do tohoto je potřeba přidat (čerstvá) vejce.
12,13d12
< Podobně se, pro lepší technologické vlastnosti, dá přidat i mléko.
< U této suroviny ale bude hrát stáří větší roli, než u výše uvedneých vajec - a tak musí být z lokálních chovů.

Znak < ve výpisu nám říká že dotyčný řádek pochází z argumentu více vlevo. Analogicky, znak > pak značí řádek ze souboru více vpravo. Ve výstupu jsou poznačena i čísla řádků, která se navzájem liší. Takové 4c5 skrývá myšlenku že se řádek 4 v 1. souboru změnil na řádek 5 ve druhém souboru. Podobně 2a3 znamená vložený řádek mezi řádky 2 a 3. 12,13d12 pak zase značí smazání řádků č. 12 a 13. Jinými slovy, výstup diffu říká, co se musí v 1. souboru změnit, aby se z něj stal 2. soubor.

A nyní, o postup výroby čeho tedy jde? Co když bude v souboru změn více? Co když někdo soubor přeformátuje či přejmenuje, budeme z takto poznačené změny schopni ještě jednoznačně novou verzi odvodit? Existuje ale pěknější zápis, ve kterém by bylo i vidět kontext takové změny?

Ano, jde o unified diff format. Klíčovým okamžikem bylo rozšíření diffu o možnosti odrážet změny v pojmenování souborů zavedené v BSD 2.8. To přineslo myšlenku poznačení změny ne před blokem se změnou, ale přímo na konkrétních řádcích. Do výstupu tak přibyl na každý řádek 0. znak - buď +, - a nebo ! (přidání, odebrání, změna). Odtud už byl jen krůček k formátu, který je dnes důvěrně znám každému vývojáři. Pojďme se na něj podívat podrobněji.

In [2]:
pushd priklady/blok1_diff > /dev/null
diff -rupN a b
popd > /dev/null
diff -rupN a/postup_vyroby.txt b/postup_vyroby.txt
--- a/postup_vyroby.txt 2022-01-30 18:31:52.660480610 +0000
+++ b/postup_vyroby.txt 2022-01-30 18:31:52.660480610 +0000
@@ -1,7 +1,8 @@
 Nejprve je potřeba připravit pískovcové bloky.
 Bloky následně skládáme tak, aby vytvářely vnější vyzdívku mostu.
+Při jejich zakládání přijde vhod, pokud předem vybereme kousky, které se navzájem doplní.
 Důležitá je ale i příprava vnitřní výplně mostu.
-Do tohoto je potřeba přidat vejce.
+Do tohoto je potřeba přidat (čerstvá) vejce.
 Ta ale nesmí být vařená, nejsou přece ku svačině.
 Je ale nutné jich přidat jen malé množství, aby nebyla malta spojující kameny příliš řídká.
 To by pak most moc nedržel.
@@ -9,8 +10,6 @@ Mimochodem, postup výroby středověký
 Až moderním výzkumem se podařilo potvrdit některé legendy a zkazky o přidávání - řekněme méně obvyklých - substancí do zdiva.
 Možná pamatujete i zkazku o krvi a tělech dělníků, kteří postavili velkou čínskou zeď.
 I na té je trocha pravdy, krev - typicky skotu - se opravdu jako složka malt používala.
-Podobně se, pro lepší technologické vlastnosti, dá přidat i mléko.
-U této suroviny ale bude hrát stáří větší roli, než u výše uvedneých vajec - a tak musí být z lokálních chovů.

 Mimochodem, to s těmi vejci je zdá se pravda:
 https://www.idnes.cz/zpravy/domaci/karluv-most-se-skutecne-stavel-z-vajec-zjistili-vedci.A081015_193239_domaci_zra

Oproti předchozímu příkladu máme na začátku výstupu souboru hlavičku s kontextem. Tato má zprvu relativně volný formát - dokud není nalezen řádek identifikující původní soubor, je obsah ignorován. Jinými slovy, výstup začíná neformátovaným komentářem, který může mít i více řádků. Jeho typickým obsahem je příkaz, který vedl k vygenerování tohoto rozdílu, případně servisní informace nástroje.

  • Řádek, začínající --- identifikuje původní soubor. Jinými slovy, výstup za cestou k němu následuje volitelné pole původně určené pro datum poslední modifikace původního souboru.
  • Řádek, začínající +++ identifikuje cílový soubor. Formát tohoto řádku je totožný s formátem pro původní soubor.
  • Řádek začínající @@ označuje oblast změn - hunk.

Pro každý hunk dále platí, že obsahuje změněné řádky a jejich okolí. Každý řádek dostane jako 0. znak identifikaci, zda jde o nezměněný řádek (` - mezera), odstraněný řádek (-) či řádek přidaný (+`). Změna v existujícím řádku je pak vyjádřena odstraněním původního a vložením nového řádku.

Formát unified diff format je použit i pro distribuci záplat (anglicky patch).

GUI

Práce s rozdíly v textovém formátu není vždy nejpřehlednější, existuje tedy i řada nástrojů pro vizualizaci změn. Najdete je často jako součást složitějších platforem (např. github, gitlab, IDE) - ale i jako samostatné nástroje určené jen pro porovnávání souborů a adresářů. V této části se budeme jednomu z nich letmo věnovat - nástroji Meld.

In [3]:
pushd priklady/blok1_diff > /dev/null
meld a b
popd > /dev/null

Editační okno meldu

Meld může operovat buď nad dvojicí souborů, nebo adresářů 1 , přičemž dvojici souborů můžete uvažovat za speciální případ rozdílu dvou adresářů. Pokud je Meld spuštěn nad dvojicí adresářů, rekurzivně je prohledá a barevně vyznačí odlišné soubory.

V editačním okně pak můžete jednotlivé změny mezi soubory přesouvat klepnutím na malou šipku při středovém panelu. Pokud naopak chcete nějaký řádek odebrat, stačí stisknout levý shift. Pro rozsáhlejší změny je pak možné přidávat synchronizační body, které - jsou-li správně nastaveny v obou editorech - usnadní orientaci porovnáváním skupin změn.

V nastavení je pak možno nastavit filtr ignorovaných změn pomocí regulárních výrazů a filtr ignorovaných souborů.

Takovým porovnáním říkáme třícestná a narazíte na ně obvykle u verzovacích systému s více uživateli.


  1. Toto je ale pouhou podmnožinou dovedností Meldu. V praxi totiž velice často potřebujete porovnat ne 2 soubory (či adresáře), ale hned 3.

Záplaty

Nyní tedy již víte, jak se ze dvou různých úprav téhož souboru stane záplata, umožňující nám přejít z jedné verze do druhé. Pojďme se podívat na opačný směr, na aplikaci změn.

V druhém příkladu jste si mohli všimnout, že se sice původní i upravený soubor jmenují stejně, ale v různých adresářích. Toto může být záměr při přesunu souboru do jiného adresáře, avšak soubory se mění častěji, než se přesouvají. Pokud tedy budeme vycházet z předpokladu, že jde o záznam vývoje nějakého souboru, chtěli bychom hlavičku tvaru odpovídajícímu přibližně (prosím o ignorování chyby v komentáři):

In [4]:
pushd priklady/blok1_diff > /dev/null
diff -rupN a b | filterdiff --strip 1
popd > /dev/null
diff -rupN postup_vyroby.txt postup_vyroby.txt
--- postup_vyroby.txt 2022-01-30 18:31:52.660480610 +0000
+++ postup_vyroby.txt 2022-01-30 18:31:52.660480610 +0000
@@ -1,7 +1,8 @@
 Nejprve je potřeba připravit pískovcové bloky.
 Bloky následně skládáme tak, aby vytvářely vnější vyzdívku mostu.
+Při jejich zakládání přijde vhod, pokud předem vybereme kousky, které se navzájem doplní.
 Důležitá je ale i příprava vnitřní výplně mostu.
-Do tohoto je potřeba přidat vejce.
+Do tohoto je potřeba přidat (čerstvá) vejce.
 Ta ale nesmí být vařená, nejsou přece ku svačině.
 Je ale nutné jich přidat jen malé množství, aby nebyla malta spojující kameny příliš řídká.
 To by pak most moc nedržel.
@@ -9,8 +10,6 @@ Mimochodem, postup výroby středověký
 Až moderním výzkumem se podařilo potvrdit některé legendy a zkazky o přidávání - řekněme méně obvyklých - substancí do zdiva.
 Možná pamatujete i zkazku o krvi a tělech dělníků, kteří postavili velkou čínskou zeď.
 I na té je trocha pravdy, krev - typicky skotu - se opravdu jako složka malt používala.
-Podobně se, pro lepší technologické vlastnosti, dá přidat i mléko.
-U této suroviny ale bude hrát stáří větší roli, než u výše uvedneých vajec - a tak musí být z lokálních chovů.

 Mimochodem, to s těmi vejci je zdá se pravda:
 https://www.idnes.cz/zpravy/domaci/karluv-most-se-skutecne-stavel-z-vajec-zjistili-vedci.A081015_193239_domaci_zra

Čímž sice zachováme adresář, ale komentář s příkazem teď nedává příliš smysl. Navíc, který ze souborů je novější verzí? Časové razítko poslední změny nám v orientaci pomáhá jen trochu, nemáme nikde garantováno, že jde poslední změnu logiky v něm obsažené. Přece jen, jména adresářů své výhody měla. Tento argument navíc podtrhne situace, kdy uvážíme rekurzivní diff nad původním motivačním mincomatem.

Naštěstí má program patch i přepínač -p N, který způsobí ignorování prvních N komponent cesty k souboru. Pro úplnost, patch předpokládá že záplatu obdrží na standardním vstupu.

In [5]:
pushd priklady/blok1_diff > /dev/null
diff -rupN a b > postup.patch
rm -rf c
cp -r a c
cd c
patch -p 1 < ../postup.patch
md5sum  ../b/postup_vyroby.txt ./postup_vyroby.txt
popd > /dev/null
patching file postup_vyroby.txt
3b24c42e290d5a554fbf95ea516d283f  ../b/postup_vyroby.txt
3b24c42e290d5a554fbf95ea516d283f  ./postup_vyroby.txt

Binární záplaty

Bystrého čtenáře jistě napadlo, že do tohoto okamžiku byla řeč vždy jen o textových souborech či zdrojových kódech. Nezřídka ale vyvstává nutnost zobrazit, resp. zaznačit (a distribuovat) změnu v binárním souboru. Ve srovnání s unified diff format ale nelze říci že existuje nějaký univerzální formát a tak záleží na sadě nástrojů, kterou použijete, přesto existuje de facto standard, který si popíšeme níže.

V systémech odvozených od BSD je k dispozici utilita bspatch, která na očekává cesty k původnímu a novému souboru a dále pak i záplatu v nativním formátu. Záplaty v tomto formátu pak získáte programem bsdiff. Binární záplaty se od záplat běžných tedy odlišují formátem a typicky nejsou člověku čitelné. Mnohdy se tak setkáte s označením delta pro binární záplaty a patch pro záplaty textové.

In [6]:
pushd priklady/blok1_diff > /dev/null
bsdiff a/postup_vyroby.txt b/postup_vyroby.txt postup.bspatch
hexdump -C postup.bspatch
popd > /dev/null
00000000  42 53 44 49 46 46 34 30  45 00 00 00 00 00 00 00  |BSDIFF40E.......|
00000010  2b 00 00 00 00 00 00 00  7a 04 00 00 00 00 00 00  |+.......z.......|
00000020  42 5a 68 39 31 41 59 26  53 59 ac 6e 6b ed 00 00  |BZh91AY&SY.nk...|
00000030  1c 43 d4 78 18 00 10 10  00 00 00 c0 01 05 00 00  |.C.x............|
00000040  10 20 00 31 00 00 06 4d  20 64 7a 8b b3 ae 72 9e  |. .1...M dz...r.|
00000050  dd 02 4d 86 88 0a 1a 8c  98 b9 7f 17 72 45 38 50  |..M.........rE8P|
00000060  90 ac 6e 6b ed 42 5a 68  39 31 41 59 26 53 59 c9  |..nk.BZh91AY&SY.|
00000070  75 dd 25 00 00 02 40 00  c0 02 00 08 20 00 20 aa  |u.%...@..... . .|
00000080  6d 41 98 8a 39 e2 ee 48  a7 0a 12 19 2e bb a4 a0  |mA..9..H........|
00000090  42 5a 68 39 31 41 59 26  53 59 6f 1c 8b 19 00 00  |BZh91AY&SYo.....|
000000a0  08 13 f4 40 64 40 00 3e  7f df 30 00 02 00 20 20  |...@d@.>..0...  |
000000b0  22 0e 00 20 00 6a 2a 7a  8d 31 1b 24 d0 7a 9a 36  |".. .j*z.1.$.z.6|
000000c0  a3 d4 04 14 6c 50 34 d0  d0 06 9a 6d 4c 86 2e 00  |....lP4....mL...|
000000d0  8a ab 5d 26 aa 3c 7a 36  8a 50 ca 71 95 49 5e cb  |..]&.<z6.P.q.I^.|
000000e0  52 8b dd 56 10 32 29 25  cc 9c 27 ea 09 ac fb 48  |R..V.2)%..'....H|
000000f0  ad aa 36 09 6d a9 de 48  61 86 8e cd c5 46 16 00  |..6.m..Ha....F..|
00000100  0a f2 94 1f 20 de 91 60  fc 5d c9 14 e1 42 41 bc  |.... ..`.]...BA.|
00000110  72 2c 64                                          |r,d|
00000113
In [7]:
pushd priklady/blok1_diff > /dev/null
mkdir d
bspatch a/postup_vyroby.txt d/postup_vyroby.txt postup.bspatch
md5sum  b/postup_vyroby.txt d/postup_vyroby.txt
popd > /dev/null
3b24c42e290d5a554fbf95ea516d283f  b/postup_vyroby.txt
3b24c42e290d5a554fbf95ea516d283f  d/postup_vyroby.txt

Commit a historie

Zcela přirozeně, nic nám nebrání záplaty řetězit a aplikovat postupně. Vývoj projektu tak lze pomocí záplat stopovat od počátečního, prázdného, projektu až k aktuální verzi. Každá záplata tak obnáší minimální příspěvek do projektu proti předešlé záplatě -- commit. Takové posloupnosti záplat říkáme historie, posloupnost commitů (pro jednoduchost zatím uvažujme jen lineární historie, tedy takové kde má každý commit právě jednoho předka, s výjimkou commitu počátečního).

Co vlastně takový commit obsahuje?

  1. Záplatu jako takovou. Nemusí jít nutně o jediný změněný soubor, spíše nějakou jednu elementární logickou změnu.
  2. Informaci o autorovi (author), případně schvalovateli změny (sign-of).
  3. Odůvodnění změny (commit message).
  4. Referenci na historii, o kterou se tento commit opírá (parent).

Jaká je ale velikost nějaké logické změny (sady změn - changesetu), kdy ještě má smysl hovořit o jediném commitu? Absolutní měřítko pro něco takového není, dá se ale řídit podle následujících pravidel:

  1. Takřka vždy se dá změna rozdělit na zřetězení nějakých dílčích změn a mezikroků.
  2. Jednotlivé dílčí změny musí být srozumitelné, čitelné a snadno hodnotitelné.
  3. Po každé dílčí změně je žádoucí, aby byl projekt v konzistentním stavu. I za cenu zavedení věcí, které budou některým následujícím commitem v changesetu opět odstraněny (komentovat jako takové).

Zůstává nám ale jedna nezodpovězená otázka - kdo vlastně udržuje přehled o dostupných commitech.

Repozitáře a verzovací systém GIT

Historicky existuje řada verzovacích systémů, s více či méně podobnými prvky organizace commitů a spolupráce autorů. Mezi (historicky) nejrozšířenější lze nejspíše s přehledem zařadit:

  • Concurrent Versions System (příkaz cvs), který dnes spíše již nepotkáte. Princip klienta + serveru. Nejnovější stabilní release 2008.
  • Subversion (příkaz svn), potkáte typicky tam, kde ještě nemigrovali na něco jiného. Princip klienta + serveru. Nejnovější stabilní release 2021.
  • Mercurial (příkaz hg), předmět aktivního vývoje. Nabízí nástroje pro migraci, například z SVN. Decentralizovaná architektura. Nejnovější release 2021.
  • Git (příkaz git), předmět aktivního vývoje. Nabízí nástroje pro migraci, například z Mercurialu. Decentralizovaná architektura. Nejnovější release 2021.

Jako mnoho jiných otázek mezi programátory, i zde existují svaté války (tabs vs. spaces, vim vs. emacs) - v tomto případě Git vs. Mercurial. 1 V tomto předmětu se budeme z praktických důvodů opírat o Git, zprvu za pomoci příkazové řádky.

V klasickém modelu klient-server je udržována centralizovaná informace o stavu projektu a jeho kódu (repozitáře) na straně serveru, v platformně závislé podobě - může tedy jít jak o databázi, tak o plnohodnotnou kopii projektu. Git je ale strukturován jako decentralizovaný systém, tedy každá kopie repozitáře může fungovat jako klient i server. Přesto jsou zde jistá praktická omezení.

Pokud se detailněji podíváme na kopii (klon, downstream) existujícího projektu (upstream), najdeme v něm skrytý adresář s názvem .git. Tento adresář obsahuje režijní data gitu, která (zatím) nebudeme příliš rozepisovat, sic není dobrou praxí tyto upravovat ručně. Vězme, že pokud bychom tento adresář odstranili, ztratíme tím i párování mezi repozitářem a zbytkem adresáře.

Právě uložení režijních informací gitu v pomocném adresáři (a jeho odstranění) nám může přijít velmi vhod, například pokud chceme koncovému zákazníkovi předat zdrojové kódy, aniž bychom mu prozradili způsob, jakým způsobem (a kdy) vznikaly, kdo všechno se na nich podílel, že o některých problémech můžeme vědět již řadu let... Zkrátka a jednoduše, vše co má zůstat za zavřenými dveřmi.


  1. Správné odpovědi vyznačeny kurzívou.

In [8]:
ls -aihl
ls -aihl .git
total 580K
271163774 drwxrwxrwx.  9 root root  266 Jan 30 18:31 .
271163773 drwxrwxrwx.  4 root root   44 Jan 30 18:30 ..
135761741 drwxrwxrwx.  2 root root   34 Jan 30 18:30 .ci
405316875 drwxrwxrwx.  6 root root  113 Jan 30 18:30 .git
271187378 -rw-rw-rw-.  1 root root   17 Jan 30 18:30 .gitignore
271187386 -rw-rw-rw-.  1 root root 3.3K Jan 30 18:30 .gitlab-ci.yml
271187387 -rw-rw-rw-.  1 root root    0 Jan 30 18:30 .keep
271189045 -rw-rw-rw-.  1 root root 346K Jan 30 18:30 PB176_prednasky.ipynb
271189046 -rw-rw-rw-.  1 root root 2.4K Jan 30 18:30 Readme.md
279219316 -rw-r--r--.  1 root root 208K Jan 30 18:31 __generated.ipynb
271189057 drwxrwxrwx.  3 root root   40 Jan 30 18:30 ci-overrides
271189062 -rw-rw-rw-.  1 root root  732 Jan 30 18:30 docker-compose.yml
  2178224 drwxrwxrwx.  2 root root   52 Jan 30 18:30 docker_notebook
136044763 drwxrwxrwx.  2 root root   58 Jan 30 18:30 extern
271189064 drwxrwxrwx.  2 root root 4.0K Jan 30 18:30 figure
167409625 drwxr-xr-x. 28 root root 4.0K Jan 30 18:31 priklady
total 16K
405316875 drwxrwxrwx. 6 root root  113 Jan 30 18:30 .
271163774 drwxrwxrwx. 9 root root  266 Jan 30 18:31 ..
405316879 -rw-rw-rw-. 1 root root  394 Jan 30 18:30 FETCH_HEAD
405316945 -rw-rw-rw-. 1 root root   41 Jan 30 18:30 HEAD
405316876 -rw-rw-rw-. 1 root root  317 Jan 30 18:30 config
405316883 -rw-rw-rw-. 1 root root 2.8K Jan 30 18:30 index
405316877 drwxrwxrwx. 3 root root   17 Jan 30 18:30 lfs
135761737 drwxrwxrwx. 3 root root   30 Jan 30 18:30 logs
405316878 drwxrwxrwx. 4 root root   30 Jan 30 18:30 objects
  2178216 drwxrwxrwx. 6 root root   63 Jan 30 18:30 refs

Zkusme si tedy založit klon existujícího repozitáře, například toho s doprovodnými příklady:

**VAROVÁNÍ**: Toto může poškodit lokální kopii přednášek, buďte opatrní pokud jste si dělali nějaké poznámky do adresáře s příklady. Ostatně, toto platí pro všechny příklady v tomto dokumentu.

In [9]:
test -d priklady/.git || git clone https://gitlab.fi.muni.cz/pb176/private/2022-priklady-todo.git priklady

Protože je sám klon repozitáře současně také plnohodnotným repozitářem a "serverem", můžeme jej dále klonovat. A to zcela nezávisle na tom, co se mezitím děje v původním zdroji. Nový klon se totiž bude odkazovat jen na místo, odkud byl sám naklonován.

In [10]:
pushd priklady > /dev/null
git remote show origin
git checkout master
popd > /dev/null
test -d priklady-lokalni-klon && rm -rf priklady-lokalni-klon
git clone ./priklady ./priklady-lokalni-klon
pushd priklady-lokalni-klon > /dev/null
git remote show origin
ls -aihl
popd > /dev/null
* remote origin
  Fetch URL: https://gitlab.fi.muni.cz/pb176/private/2022-priklady-todo.git
  Push  URL: https://gitlab.fi.muni.cz/pb176/private/2022-priklady-todo.git
  HEAD branch: master
  Remote branches:
    blok1_blame          tracked
    blok1_mkonflikt_a    tracked
    blok1_mkonflikt_b    tracked
    blok1_mkonflikt_work tracked
    master               tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)
Already on 'master'
Your branch is up to date with 'origin/master'.
Cloning into './priklady-lokalni-klon'...
done.
* remote origin
  Fetch URL: /builds/pb176/private/prednasky/./priklady
  Push  URL: /builds/pb176/private/prednasky/./priklady
  HEAD branch: master
  Remote branch:
    master tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)
total 12K
301858573 drwxr-xr-x. 28 root root 4.0K Jan 30 18:32 .
271163774 drwxrwxrwx. 10 root root 4.0K Jan 30 18:32 ..
452515593 drwxr-xr-x.  8 root root  163 Jan 30 18:32 .git
301858640 -rw-r--r--.  1 root root    0 Jan 30 18:32 .keep
 39817125 drwxr-xr-x.  4 root root   42 Jan 30 18:32 blok1_diff
452515611 drwxr-xr-x. 16 root root 4.0K Jan 30 18:32 blok1_motivace
301858652 drwxr-xr-x.  2 root root   83 Jan 30 18:32 blok2_autoconf
452515629 drwxr-xr-x.  2 root root  159 Jan 30 18:32 blok2_automake
 39817137 drwxr-xr-x.  2 root root   36 Jan 30 18:32 blok2_buildsh1
168701450 drwxr-xr-x.  2 root root   36 Jan 30 18:32 blok2_buildsh2
301858658 drwxr-xr-x.  2 root root   36 Jan 30 18:32 blok2_buildsh3
452515643 drwxr-xr-x.  2 root root   66 Jan 30 18:32 blok2_cmake
 39817140 drwxr-xr-x.  2 root root   66 Jan 30 18:32 blok2_cmake_cond
168701453 drwxr-xr-x.  2 root root   22 Jan 30 18:32 blok2_make1
301858661 drwxr-xr-x.  2 root root   22 Jan 30 18:32 blok2_make2
452515650 drwxr-xr-x.  2 root root   22 Jan 30 18:32 blok2_make3
 39817144 drwxr-xr-x.  3 root root   50 Jan 30 18:32 blok2_make4
301858664 drwxr-xr-x.  2 root root   22 Jan 30 18:32 blok2_makefile_syntax
452515658 drwxr-xr-x.  2 root root   41 Jan 30 18:32 blok2_pkgconfig
 39817147 drwxr-xr-x.  2 root root   56 Jan 30 18:32 blok3_code_pb071_sem4
168701457 drwxr-xr-x.  2 root root  103 Jan 30 18:32 blok4_ci_demo
301858668 drwxr-xr-x.  2 root root   32 Jan 30 18:32 blok4_compose_01_simple
452515668 drwxr-xr-x.  2 root root   57 Jan 30 18:32 blok4_compose_02_multi
 39817151 drwxr-xr-x.  2 root root   24 Jan 30 18:32 blok4_docker_01_simple
168701463 drwxr-xr-x.  3 root root   69 Jan 30 18:32 blok4_docker_02_user
452515673 drwxr-xr-x.  3 root root  101 Jan 30 18:32 blok4_docker_03_buildargs
168701466 drwxr-xr-x.  3 root root   43 Jan 30 18:32 blok4_docker_04_multistage
452515682 drwxr-xr-x.  2 root root   83 Jan 30 18:32 blok4_nightly
 39817157 drwxr-xr-x.  2 root root   73 Jan 30 18:32 blok5_badcode

Nyní zkusme do takto vytvořené kopie repozitáře zanést vlastní příspěvek - vytvoříme nový souboru Readme.txt s oblíbenou větou "Ahoj světe", který zkusíme propagovat směrem k původnímu repozitáři:

In [11]:
pushd priklady-lokalni-klon > /dev/null
echo "Ahoj světe" > Readme.txt
git add Readme.txt
git commit -m "Add Readme holding a first hello world example, that will fail soon." Readme.txt
git push
popd > /dev/null
[master 3832f4a] Add Readme holding a first hello world example, that will fail soon.
 1 file changed, 1 insertion(+)
 create mode 100644 Readme.txt
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 329 bytes | 329.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
remote: error: refusing to update checked out branch: refs/heads/master
remote: error: By default, updating the current branch in a non-bare repository
remote: is denied, because it will make the index and work tree inconsistent
remote: with what you pushed, and will require 'git reset --hard' to match
remote: the work tree to HEAD.
remote:
remote: You can set the 'receive.denyCurrentBranch' configuration variable
remote: to 'ignore' or 'warn' in the remote repository to allow pushing into
remote: its current branch; however, this is not recommended unless you
remote: arranged to update its work tree to match what you pushed in some
remote: other way.
remote:
remote: To squelch this message and still keep the default behaviour, set
remote: 'receive.denyCurrentBranch' configuration variable to 'refuse'.
To /builds/pb176/private/prednasky/./priklady
 ! [remote rejected] master -> master (branch is currently checked out)
error: failed to push some refs to '/builds/pb176/private/prednasky/./priklady'

Pojďme si projít příklad příkaz po příkazu a vysvětlit si, k čemu zde vlastně došlo:

  1. echo "Ahoj světe" > Readme.txt - Tento příkaz v sobě žádné tajemství neskrývá, jde o obyčejné vytvoření souboru.
  2. git add Readme.txt - Z principu je potřeba rozlišovat soubory, které jsou sledovány verzovacím systémem (verzovány) a těmi, které jsou vedlejšími produkty a které verzovány být nemají. Příkaz git add tak dělá dvě věci: a. Doposud nesledované soubory jsou nově označeny jako sledované. b. Soubor je poznačen jako součást obsahu nadcházejícího commitu.
  3. git commit -m "Zdůvodnění" [případná explicitní soupiska souborů, které mají být do commitu zahrnuty] - Tento příkaz vytvoří samotný commit a upraví referenci na vrchol historie. Zdůvodnění commitu může být u triviálních změn jednořádkové, složitější zprávy je ale žádoucí komentovat obšírněji. Pokud přepínač -m nezadáte, otevře se textový editor daný proměnnou prostředí EDITOR s šablonou zdůvodnění, čítající zakomentovanou soupisku souborů v commitu zahrnutých. Zda na konci příkazu uvedeme soubor Readme.txt či nikoliv nemá v tomto konkrétním případě vliv, sic tento soubor již byl ke commitu poznačen příkazem git add.
  4. Synchronizujeme změny v lokálním repozitáři s repozitářem, ze kterého jsme klonovali. Tento příkaz ale selhal, podívejme se proč.

Bare repository vs. working dir

Doposud jsme viděli repozitáře se soubory, které chceme verzovat a režijními informacemi gitu uloženými v podadresáři .git. Takovým repozitářům říkáme repozitáře s pracovním adresářem. Tyto jsou samozřejmě repozitáři a lze je klonovat. Ale - a je to velmi důležité ale - obsahují soubory k nějaké verzi kódu, v nějakém (blíže nespecifikovaném) stavu rozpracování. Pokud bychom nahrávali své změny do takového repozitáře, mohli bychom vytvořit nekonzistenci mezi aktuálním stavem a vrcholem historie. A právě takové nekonzistenci se git snaží zabránit.

Jak ale z této problémové situace ven? Řešením jsou holé (bare) repozitáře, které obsahují jen a pouze režijní data a historii, nikoliv však pracovní soubory. Právě v podobě bare repozitářů jsou typicky projekty uloženy na synchronizačních serverech.

Takové repozitáře mezi sebou pak můžeme synchronizovat z druhé strany, stažením změn. 1 Toto si ale ukážeme později.


  1. Ano, push lze i vynutit, ale to není dobrý nápad.

Vytvoření nesynchronizovaného repozitáře

Na chvíli teď opusťme představu toho, že máme nějaký server a více různých kopií, které potřebujeme synchronizovat. Git lze totiž používat i lokálně, například pro udržování konfigurace serveru a zdůvodňování (a logování) změn. Pro takové případy je možné vytvářet repozitáře i lokálně, bez asistence nějaké 3. strany či kopie.

In [12]:
rm -rf priklady-lokalni-init
mkdir priklady-lokalni-init
pushd priklady-lokalni-init > /dev/null
git init .
echo "Ahoj světe" > Readme.txt
git add Readme.txt
git commit -m "Add Readme holding a first hello world example." Readme.txt
popd > /dev/null
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint:   git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint:   git branch -m <name>
Initialized empty Git repository in /builds/pb176/private/prednasky/priklady-lokalni-init/.git/
[master (root-commit) 155c65e] Add Readme holding a first hello world example.
 1 file changed, 1 insertion(+)
 create mode 100644 Readme.txt

Synchronizace repozitářů

Jak jsme již naznačili v případě výše, existence více kopií téhož repozitáře, spolu s tím že commit je vždy jen do aktuální kopie vede nevyhnutelně na problém synchronizace jednotlivých kopií (slovo kopie je kopírováno zcela záměrně). Zatím ale nebudeme uvažovat projekty s více vývojáři a konflikty mezi jejich změnami, což nám situaci přece jen zjednoduší. Vraťme se tedy k příkladu, kde jsme napoprvé narazili na chybu. Tentokrát ale zkusíme párování repozitářů doplnit o zpětný směr. Operace, kterou budeme chtít provést se jmenuje fetch, přesněji tedy git fetch.

In [13]:
pushd priklady > /dev/null
git remote add kopie2 ../priklady-lokalni-klon
git fetch kopie2
popd > /dev/null
From ../priklady-lokalni-klon
 * [new branch]      master     -> kopie2/master

To nevypadá vůbec zle, že? Kam ale zmizel problém synchronizace pracovního adresáře a indexu? Tajemství tkví v tom, že jsme k sobě pouze pouze přenesli informace o existenci změn v jiné kopii spolu se změnami - ty jsme ale nepromítli do svého pracovního adresáře. Pokud by v tomto repozitáři nečekaly žádné nedořešené změny, mohli bychom se vskutku na "nejnovější" verzi přepnout. Nuž, je na čase provést kontrolu:

In [14]:
pushd priklady > /dev/null
git status
popd > /dev/null
On branch master
Your branch is up to date with 'origin/master'.

nothing to commit, working tree clean

Odpověď verzovacího systému je vcelku jednoznačná. Pokud bychom byli zvláště paranoidní, mohli bychom ještě zkusit ověřit co vše git sleduje:

In [15]:
pushd priklady > /dev/null
git ls-files
popd > /dev/null
.keep
blok1_diff/.gitignore
blok1_diff/a/postup_vyroby.txt
blok1_diff/b/postup_vyroby.txt
blok1_motivace/.keep
"blok1_motivace/Nov\303\241 slo\305\276ka (odevzdan\303\241).zip"
"blok1_motivace/Nov\303\241 slo\305\276ka/.keep"
"blok1_motivace/Z\303\241v\304\233re\304\215n\303\241 pr\303\241ce/.keep"
blok1_motivace/__mincomat__tenhle/.keep
blok1_motivace/__mincomat__tenhle/keep.c
blok1_motivace/build-amd64-DEBUG/.keep
blok1_motivace/build-x86_64-DEBUG/.keep
blok1_motivace/build/.keep
blok1_motivace/mincomat (kopie)/.keep
blok1_motivace/mincomat (kopie)/keep.c
"blok1_motivace/mincomat (z\303\241loha).zip"
"blok1_motivace/mincomat (z\303\241loha)/.keep"
"blok1_motivace/mincomat (z\303\241loha)/keep.c"
"blok1_motivace/mincomat - funk\304\215n\303\255 (bez vracen\303\255)/.keep"
"blok1_motivace/mincomat - funk\304\215n\303\255 (bez vracen\303\255)/keep.c"
"blok1_motivace/mincomat fin\303\241ln\303\255/.keep"
"blok1_motivace/mincomat fin\303\241ln\303\255/keep.c"
"blok1_motivace/mincomat fin\303\241ln\303\2552 oprava/.keep"
"blok1_motivace/mincomat fin\303\241ln\303\2552 oprava/keep.c"
"blok1_motivace/mincomat fin\303\241ln\303\2552/.keep"
"blok1_motivace/mincomat fin\303\241ln\303\2552/keep.c"
blok1_motivace/mincomat-nefunguje/.keep
blok1_motivace/mincomat-nefunguje/keep.c
blok1_motivace/mincomat/.keep
blok1_motivace/mincomat/CMakeLists.txt
blok1_motivace/mincomat/keep.c
blok1_motivace/mincomat/mincomat.c
blok2_autoconf/Makefile.in
blok2_autoconf/config.h.in
blok2_autoconf/configure.ac
blok2_autoconf/uuid_demo.c
blok2_automake/AUTHORS
blok2_automake/ChangeLog
blok2_automake/Makefile.am
blok2_automake/NEWS
blok2_automake/README
blok2_automake/autogen.sh
blok2_automake/config.h.in
blok2_automake/configure.ac
blok2_automake/uuid_demo.c
blok2_buildsh1/build.sh
blok2_buildsh1/run.sh
blok2_buildsh2/build.sh
blok2_buildsh2/run.sh
blok2_buildsh3/build.sh
blok2_buildsh3/run.sh
blok2_cmake/CMakeLists.txt
blok2_cmake/config.h.in
blok2_cmake/uuid_demo.c
blok2_cmake_cond/CMakeLists.txt
blok2_cmake_cond/config.h.in
blok2_cmake_cond/uuid_demo.c
blok2_make1/Makefile
blok2_make2/Makefile
blok2_make3/Makefile
blok2_make4/Makefile
blok2_make4/demo.sh
blok2_make4/index/Makefile
blok2_makefile_syntax/Makefile
blok2_pkgconfig/Makefile
blok2_pkgconfig/uuid_demo.c
blok3_code_pb071_sem4/.gitignore
blok3_code_pb071_sem4/Makefile
blok3_code_pb071_sem4/task.url
blok4_ci_demo/CMakeLists.txt
blok4_ci_demo/Readme.md
blok4_ci_demo/_gitlab-ci.yml
blok4_ci_demo/config.h.in
blok4_ci_demo/vardemo.c
blok4_compose_01_simple/docker-compose.yml
blok4_compose_02_multi/docker-compose.yml
blok4_compose_02_multi/mtbmap-cz.service
blok4_docker_01_simple/Dockerfile
blok4_docker_02_user/Dockerfile
blok4_docker_02_user/Dockerfile-noclone
blok4_docker_02_user/ssh-private/.gitignore
blok4_docker_02_user/ssh-private/known_hosts
blok4_docker_03_buildargs/Dockerfile.1
blok4_docker_03_buildargs/Dockerfile.2
blok4_docker_03_buildargs/Dockerfile.3
blok4_docker_03_buildargs/build.sh
blok4_docker_03_buildargs/ssh-private/.gitignore
blok4_docker_03_buildargs/ssh-private/known_hosts
blok4_docker_04_multistage/Dockerfile
blok4_docker_04_multistage/ssh-private/.gitignore
blok4_docker_04_multistage/ssh-private/known_hosts
blok4_nightly/Makefile.in
blok4_nightly/config.h.in
blok4_nightly/configure.ac
blok4_nightly/uuid_demo.c
blok5_badcode/ex1.c
blok5_badcode/ex2.c
blok5_badcode/ex3.cxx
blok5_badcode/ex4.c
blok5_badcode/log.h

Nyní tedy k samotnému přepnutí na importované změny:

In [16]:
pushd priklady > /dev/null
git reset --hard kopie2/master
popd > /dev/null
HEAD is now at 3832f4a Add Readme holding a first hello world example, that will fail soon.

Tento postup je nicméně poněkud komplikovaný a navíc se opírá o některé předpoklady, které nebudou splněny takřka nikdy. Praxe bývá o malinko komplikovanější. Existuje nicméně příkaz, který toto udělá za vás a dokonce se i pokusí odhadnout, do jakého stavu byste kód rádi dostali. Před samotným příkazem bude ještě rychlý reset repozitáře do jednoho z předešlých stavů:

In [17]:
pushd priklady-lokalni-klon > /dev/null
git log -n 2
git reset --hard origin/master
git log -n 2
popd > /dev/null
commit 3832f4a5ed31cae6321a6053f20dfe4274ff8efc (HEAD -> master)
Author: Lukas Rucka <xrucka@fi.muni.cz>
Date:   Sun Jan 30 18:32:03 2022 +0000

    Add Readme holding a first hello world example, that will fail soon.

commit 86afc3524130b5204f9eb267221077bf28a985fc (origin/master, origin/HEAD)
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Tue May 4 07:58:35 2021 +0200

    Add SSome code examples (really bad code)
HEAD is now at 86afc35 Add SSome code examples (really bad code)
commit 86afc3524130b5204f9eb267221077bf28a985fc (HEAD -> master, origin/master, origin/HEAD)
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Tue May 4 07:58:35 2021 +0200

    Add SSome code examples (really bad code)

commit 82754511945b8d3230410313fa547172b6cc9905
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Sat May 15 16:08:40 2021 +0200

    Blok4 - Add CI example

Pamatujete? Origin odkazuje na původní repozitář, ze kterého jsme klonovali. V tomto repozitáři jsme ale žádné změny nestahovali, nemá tedy jak vědět, že mezitím co jsme si vysvětlovali látku někdo odtud změny vykopíroval. Máme tak zastaralou představu o tom, v jakém stavu projekt je. Tu ale můžeme rychle napravit.

In [18]:
pushd priklady-lokalni-klon > /dev/null
git pull
popd > /dev/null
hint: Pulling without specifying how to reconcile divergent branches is
hint: discouraged. You can squelch this message by running one of the following
hint: commands sometime before your next pull:
hint:
hint:   git config pull.rebase false  # merge (the default strategy)
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
From /builds/pb176/private/prednasky/./priklady
   86afc35..3832f4a  master     -> origin/master
Updating 86afc35..3832f4a
Fast-forward
 Readme.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 Readme.txt

Třetím příkazem pro synchronizaci, který jste již potkali je git push. Zde již zbývá jen doplnit, že pro synchronizaci jiné než výchozí kopie repozitáře stačí uvést jméno tohoto repozitáře jako další argument příkazu git. Kupříkladu git push kopie2.

Prohlédnutí historie

Z nejdůležitějších základních příkazů pro práci s gitem nám chybí poslední - sice k prohlížení historie. Ten jste ale již také mohli vidět v akci - je jím git log. Pro úplnost tedy:

In [19]:
git log -n 5
commit df872272c25964f6efeb36962cd23b3c33a7c29a (HEAD, origin/debug-papermill, refs/pipelines/107528)
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Sun Jan 30 19:30:22 2022 +0100

    Add forgotten pipeline configuration

commit 0417615ff11f04026a5c38bdf18e9255570b118c
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Sun Jan 30 19:20:09 2022 +0100

    Generate usefull notebook diff

commit a710db0e98fba01892c3c2146e4954bfc8757eb6
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Sun Jan 30 09:49:31 2022 +0100

    Fix typo in jq output name

commit 5ceb217fabcbbedbc3ed019eb1c976f152f90b1e
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Sun Jan 30 09:38:17 2022 +0100

    jq to erase errornous outputs

commit ebf2870f1494266aa2d79eba0e31ec2dc297dc4b
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Sun Jan 30 08:20:50 2022 +0100

    Attempt to fix jq2

Podobně jako u řady příkazů které možná znáte z Linuxu, i zde máme přepínač -n, sice s obvyklým omezením počtu záznamů které se mají vypsat. Co tedy ve výpisu vidíme?

  • Autora commitu, určeného jménem a emailem. Z pohledu gitu jsou dva autoři tímtéž člověkem, pokud mají identický email.
  • Datum, kdy byl commit vytvořen. Tedy spíše, datum ke kterému byly vytvořeny změny v něm obsažené. Je ale naprosto v pořádku, pokud máme commity v pořadí 22. ledna, 2. února, 15. ledna. Jde jen o orientační informaci, sama jako taková nemá pro verzovací systém význam.
  • Zdůvodnění změn, odpovídající argumentu -m příkazu git commit.
  • SHA1 hash commitu. Ten jsme si doposud nepředstavovali, o co tedy jde? Velmi zjednodušeně řečeno, jde o unikátní identifikátor commitu v závislosti na změnách v něm a historii, ke které se vztahuje. Pomocí těchto hashů lze odkazovat jednotlivé commity (či celé historie, pokud uvážíme i rodiče tohoto commitu). Pro běžné použití není nutno uvažovat celých 40 hexadecimálních znaků. Proto git užívá zkrácený hash - z počátku 7 číslic - a v případě kolize postupně uvažovanou délku navyšuje. 1


  1. Pro případné zájemce o doporučující čtení si dovolím odkázat krásný článek na githubu, rozepisující jak je to v gitu interně s hashi, co vše se hashuje, co jsou to objekty, stromy - a spousta dalšího.

Větve vývoje a začleňování změn

Nyní tedy umíme - alespoň hypoteticky - verzovat svůj kód. Kdyby to bylo ovšem takto jednoduché, mohli bychom zůstat v prehistorii s CVS. Ale není. Zásadní otázka totiž zní: Jak nerozbít to co už funguje při práci na tom, co fungovat teprve možná bude? Jinými slovy, občas potřebujeme aby vedle sebe mohly fungovat dvě kopie téhož projektu, v různých stádiích rozpracovanosti. A právě k tomu slouží větve.

Vcelku jednoduchý model, častý na nekomplikovaných projektech, je model dvou větví - stabilní a vývojové. Ty se sice mohou jmenovat různě, často se ale jmenují master a devel, případně stable a master, či ještě jinak. Pro jednoduchost budeme uvažovat master a devel. Vycházet můžeme z předchozího případu, nejprve ale zameteme a seznámíme se s existujícími větvemi v repozitáři s příklady.

  • rebase větví bez konfliktů
  • git rebase nad stažené změny, git reset soft
In [20]:
rm -rf priklady-lokalni-kopie
pushd priklady > /dev/null
git branch
git checkout master
git reset --hard origin/master
git branch -D devel 2> /dev/null
git branch -D devel2 2> /dev/null
popd > /dev/null
* master
Already on 'master'
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)
HEAD is now at 86afc35 Add SSome code examples (really bad code)
In [21]:
pushd priklady > /dev/null
git branch
git log -n 1
git checkout -b devel
git branch
git branch devel2
git log -n 1
popd > /dev/null
* master
commit 86afc3524130b5204f9eb267221077bf28a985fc (HEAD -> master, origin/master, origin/HEAD)
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Tue May 4 07:58:35 2021 +0200

    Add SSome code examples (really bad code)
Switched to a new branch 'devel'
* devel
  master
commit 86afc3524130b5204f9eb267221077bf28a985fc (HEAD -> devel, origin/master, origin/HEAD, master, devel2)
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Tue May 4 07:58:35 2021 +0200

    Add SSome code examples (really bad code)

Příkaz git log již znáte, nováčci jsou zde ale git checkout a git branch.

  1. Příkaz git checkout má dva operační režimy, oba v zásadě do pracovního adresáře nahrají soubory, které jsou gitem sledovány k nějaké verzi. Jeho detailní chování se ale liší podle přítomnosti přepínače -b a toho, zda argument (pathspec) vypadá jako cesta k souboru:
    • Bez -b s argumentem neobsahujícím znak /, dojde k přepnutí na danou větev pokud tato existuje. Jinak je argument považován za jméno souboru.
    • Bez -b s argumentem obsahujícím znak /, dojde k nasazení nejnovější commitnuté verze souboru, který je argumentem příkazu. Řečeno jinak, git tímto příkazem zahodí naši rozpracovanou verzi. Cestu k souboru je nutno uvažovat buď jako relativní jméno, nebo za pomoci relativního adresáře . (tedy git checkout ./Readme.txt obnoví stav Readme.txt v poslední verzi).
    • S argumentem -b není v následujícím argumentu jméno souboru, který se má obnovit, ale název cílové větve, která má být nově vytvořena. Její HEAD (vrchol) je nastaven dle vrcholu větve, ve které jsme se nacházeli původně.
  2. Příkaz git branch má opět dva režimy. Bez argumentu jen vypíše lokálně obsluhované větve. S argumentem naopak zakládá novou větev, dle stejných pravidel jako git checkout -b. Je zde nicméně rozdíl - nová větev je sice založena a její vrchol nastaven, nedojde ale ke změně obsahu pracovního adresáře.
In [22]:
pushd priklady > /dev/null
git checkout devel
echo "Ahoj světe" > Readme.txt
git add Readme.txt
git commit -m "Add Readme holding a first hello world example to the development branch." Readme.txt
git log -n 2
popd > /dev/null
Already on 'devel'
[devel fdab1ae] Add Readme holding a first hello world example to the development branch.
 1 file changed, 1 insertion(+)
 create mode 100644 Readme.txt
commit fdab1aea32a7d98f97be72ead75c09c7528ffd23 (HEAD -> devel)
Author: Lukas Rucka <xrucka@fi.muni.cz>
Date:   Sun Jan 30 18:32:19 2022 +0000

    Add Readme holding a first hello world example to the development branch.

commit 86afc3524130b5204f9eb267221077bf28a985fc (origin/master, origin/HEAD, master, devel2)
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Tue May 4 07:58:35 2021 +0200

    Add SSome code examples (really bad code)

Máme tedy dvě různé historie, kde jedna z druhé vychází, sice master a devel (devel2 odteď pomiňme). V okamžiku, kdy je soubor změn pro začlenění kompletní můžeme podniknout další krok - začlenit změny z develu do masteru. To už jsme ale jednou viděli, kde to jen bylo? Jistě, slučování masteru s kopie2/master.

In [23]:
pushd priklady > /dev/null
git log -n 2
git checkout master
git reset --hard origin/master
git branch -D old-master
git branch old-master
git reset --hard devel
git log -n 2
popd > /dev/null
commit fdab1aea32a7d98f97be72ead75c09c7528ffd23 (HEAD -> devel)
Author: Lukas Rucka <xrucka@fi.muni.cz>
Date:   Sun Jan 30 18:32:19 2022 +0000

    Add Readme holding a first hello world example to the development branch.

commit 86afc3524130b5204f9eb267221077bf28a985fc (origin/master, origin/HEAD, master, devel2)
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Tue May 4 07:58:35 2021 +0200

    Add SSome code examples (really bad code)
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
HEAD is now at 86afc35 Add SSome code examples (really bad code)
error: branch 'old-master' not found.
HEAD is now at fdab1ae Add Readme holding a first hello world example to the development branch.
commit fdab1aea32a7d98f97be72ead75c09c7528ffd23 (HEAD -> master, devel)
Author: Lukas Rucka <xrucka@fi.muni.cz>
Date:   Sun Jan 30 18:32:19 2022 +0000

    Add Readme holding a first hello world example to the development branch.

commit 86afc3524130b5204f9eb267221077bf28a985fc (origin/master, origin/HEAD, old-master, devel2)
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Tue May 4 07:58:35 2021 +0200

    Add SSome code examples (really bad code)

Zde ale dojde k promazání obsahu adresáře, což není jiné cesty? Nezoufejte, máme tady git merge. Pojďme se nejprve podívat, co se stane pokud využijeme výchozí chování:

In [24]:
pushd priklady > /dev/null
# nejprve vraťme repozitář do stavu bez předešlého přepnutí
git checkout master
git reset --hard old-master
# výše jen reset příkladu
git log -n 2
git merge devel
git log -n 3
popd > /dev/null
Already on 'master'
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)
HEAD is now at 86afc35 Add SSome code examples (really bad code)
commit 86afc3524130b5204f9eb267221077bf28a985fc (HEAD -> master, origin/master, origin/HEAD, old-master, devel2)
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Tue May 4 07:58:35 2021 +0200

    Add SSome code examples (really bad code)

commit 82754511945b8d3230410313fa547172b6cc9905
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Sat May 15 16:08:40 2021 +0200

    Blok4 - Add CI example
Updating 86afc35..fdab1ae
Fast-forward
 Readme.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 Readme.txt
commit fdab1aea32a7d98f97be72ead75c09c7528ffd23 (HEAD -> master, devel)
Author: Lukas Rucka <xrucka@fi.muni.cz>
Date:   Sun Jan 30 18:32:19 2022 +0000

    Add Readme holding a first hello world example to the development branch.

commit 86afc3524130b5204f9eb267221077bf28a985fc (origin/master, origin/HEAD, old-master, devel2)
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Tue May 4 07:58:35 2021 +0200

    Add SSome code examples (really bad code)

commit 82754511945b8d3230410313fa547172b6cc9905
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Sat May 15 16:08:40 2021 +0200

    Blok4 - Add CI example

Vida, obešli jsme se tedy i bez resetu celého pracovního adresáře. Jaké jsou tedy možnosti příkazu git merge, oproti git reset?

To první a nejdůležitější je porozumění tomu, k čemu doopravdy příkaz git reset slouží - a to je návrat aktuální větve ke stavu odpovídajícímu nějaké konkrétní revizi. Má dva operační režimy, které se svým fungováním významně liší, byť jednu část mají společnou. Sice úpravu představy gitu o tom, na kterém commitu se právě nacházíte.

  • git reset --soft neupravuje žádné soubory v pracovním adresáři, manipuluje jen index - tedy představu o tom, jaká verze byla vybalena do pracovního adresáře. Je to tedy přesně ten příkaz, který použijete, pokud jste udělali v commitu na vrcholu nějakou chybu. V takovém případě můžete místo hashe uvést speciální řetězec HEAD^, který znamená rodiče aktuálního vrcholu. To vám umožní udělat commit znovu a lépe, například jej rozdělit do dvou menších změn. Jen je potřeba si uvědomit, že pokud toto uděláte někde hlouběji v historii, přestanou vám správně fungovat návaznosti potomků. O tom ale více za chvíli.
  • git reset --hard se postará o to, aby gitem sledované soubory odpovídaly obsahu k danému commitu a upraví HEAD aktuální větve. Efektivně to znamená, že pokud vám na původní HEAD neukazuje žádná větev, zahodíte tím celý vývoj mezi cílovým commitem a původní HEAD.

Oproti uvedenému, sloužícímu k přepisování a nahrazování historie, je git merge určen k začleňování změn v (cizí) historii do mé aktuální. Pokud začleňujeme historii, která je naším přímým potomkem - tedy první commit který je v začleňované historii navíc má za předka aktuální HEAD, je začlenění vcelku přímočaré - aplikujeme změny a posuneme vrchol. Svět ale nebývá tak jednoduchý.

Vývojář (klasickým pojmenováním Alice) může pracovat na jednom souboru, Bob na druhém. Oba provedou commit a následně potřebují své změny v projektu sloučit. Doposud jsme se ale bavili, že commit má jednoznačného předka. Toto ale neplatí pro merge commity - ty spojují divergující historie. Některé z nich mohou být automatizovány, zejména pokud se týkají různých souborů, v různých místech. Platí, že pokud vzniká merge commit, musí mít zdůvodnění.

In [25]:
pushd priklady > /dev/null
git checkout devel
#
printf '\n\nHARD\n\n'
#
git reset --hard old-master
echo "Ahoj světte s překlepem" > Readme.md
git add Readme.md
git commit -m "Add Readme holding a first hello world example to the development branch." Readme.md
git log -n 2
git show HEAD
#
printf '\n\nSOFT\n\n'
#
cat Readme.md
git reset --soft HEAD^
echo "Ahoj světe bez překlepu" > Readme.md
git commit -m "Add Readme holding a first hello world example to the development branch." Readme.md
git log -n 2
#
printf '\n\nMERGE\n\n'
#
git log -n 3 master old-master devel
git checkout master
git merge devel -m "Alice and Bob agreed that everybody will maintain his/her"\
" own Readme, as a sign of excellent teamwork."
git log -n 4
popd > /dev/null
Switched to branch 'devel'


HARD

HEAD is now at 86afc35 Add SSome code examples (really bad code)
[devel 7823ecf] Add Readme holding a first hello world example to the development branch.
 1 file changed, 1 insertion(+)
 create mode 100644 Readme.md
commit 7823ecf9faead22168aa4b100c56c270bf9f7bd5 (HEAD -> devel)
Author: Lukas Rucka <xrucka@fi.muni.cz>
Date:   Sun Jan 30 18:32:24 2022 +0000

    Add Readme holding a first hello world example to the development branch.

commit 86afc3524130b5204f9eb267221077bf28a985fc (origin/master, origin/HEAD, old-master, devel2)
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Tue May 4 07:58:35 2021 +0200

    Add SSome code examples (really bad code)
commit 7823ecf9faead22168aa4b100c56c270bf9f7bd5 (HEAD -> devel)
Author: Lukas Rucka <xrucka@fi.muni.cz>
Date:   Sun Jan 30 18:32:24 2022 +0000

    Add Readme holding a first hello world example to the development branch.

diff --git a/Readme.md b/Readme.md
new file mode 100644
index 0000000..9a94ca9
--- /dev/null
+++ b/Readme.md
@@ -0,0 +1 @@
+Ahoj světte s překlepem


SOFT

Ahoj světte s překlepem
[devel 49ee898] Add Readme holding a first hello world example to the development branch.
 1 file changed, 1 insertion(+)
 create mode 100644 Readme.md
commit 49ee898b98026dac2395903c2f0a138219a3c956 (HEAD -> devel)
Author: Lukas Rucka <xrucka@fi.muni.cz>
Date:   Sun Jan 30 18:32:25 2022 +0000

    Add Readme holding a first hello world example to the development branch.

commit 86afc3524130b5204f9eb267221077bf28a985fc (origin/master, origin/HEAD, old-master, devel2)
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Tue May 4 07:58:35 2021 +0200

    Add SSome code examples (really bad code)


MERGE

commit 49ee898b98026dac2395903c2f0a138219a3c956 (HEAD -> devel)
Author: Lukas Rucka <xrucka@fi.muni.cz>
Date:   Sun Jan 30 18:32:25 2022 +0000

    Add Readme holding a first hello world example to the development branch.

commit fdab1aea32a7d98f97be72ead75c09c7528ffd23 (master)
Author: Lukas Rucka <xrucka@fi.muni.cz>
Date:   Sun Jan 30 18:32:19 2022 +0000

    Add Readme holding a first hello world example to the development branch.

commit 86afc3524130b5204f9eb267221077bf28a985fc (origin/master, origin/HEAD, old-master, devel2)
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Tue May 4 07:58:35 2021 +0200

    Add SSome code examples (really bad code)
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)
Merge made by the 'recursive' strategy.
 Readme.md | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 Readme.md
commit 9888d3d3a5824c2da6f92cd540f0e03739967a11 (HEAD -> master)
Merge: fdab1ae 49ee898
Author: Lukas Rucka <xrucka@fi.muni.cz>
Date:   Sun Jan 30 18:32:26 2022 +0000

    Alice and Bob agreed that everybody will maintain his/her own Readme, as a sign of excellent teamwork.

commit 49ee898b98026dac2395903c2f0a138219a3c956 (devel)
Author: Lukas Rucka <xrucka@fi.muni.cz>
Date:   Sun Jan 30 18:32:25 2022 +0000

    Add Readme holding a first hello world example to the development branch.

commit fdab1aea32a7d98f97be72ead75c09c7528ffd23
Author: Lukas Rucka <xrucka@fi.muni.cz>
Date:   Sun Jan 30 18:32:19 2022 +0000

    Add Readme holding a first hello world example to the development branch.

commit 86afc3524130b5204f9eb267221077bf28a985fc (origin/master, origin/HEAD, old-master, devel2)
Author: Lukas Rucka <359687@mail.muni.cz>
Date:   Tue May 4 07:58:35 2021 +0200

    Add SSome code examples (really bad code)

Mnohem častěji ale dojde na situaci, které říkáme konflikt. Situaci, kdy dva commity upravují tentýž řádek. Z principu věci nemá git jak vědět, která z obou úprav je lepší a kterou má zachovat. Vyřešení takové situace pak nechává na programátorovi, sice výzvou k manuálnímu sloučení rozbitého souboru. Pokud k takové úpravě dojde někde hluboko v historii, obvykle si git dokáže s následnými konflikty dobře poradit, může se ale i stát, že se objeví znovu. V dřívějších verzích se mohlo stát i to, že k vyřešení konfliktu budou prezentovány totožné soubory, toto chování ale přednášející osobně nepozoroval již pár let.

Pojďme se podívat na příkazy, které budeme pro řešení konfliktu potřebovat:

  • git merge --abort Tento příkaz nám umožňuje v kterémkoliv bodě merge zrušit celou operaci. Ať už proto, že dospějeme k názoru, že to bude lepší celé udělat znovu a pořádně, nebo narazíme na něco, nad čím bude potřeba déle přemýšlet.
  • git mergetool -t <nástroj> Ve výchozím stavu git rozpracuje automatické slučování a vytvoří několik pomocných souborů s pomocnými jmény BASE, LOCAL, REMOTE (odpovídající rozslučovanému kódu, verzi aktuální větve po zapracování konfliktů z předešlých commitů, verzi z pohlcované větve). Do souboru BASE git vyznačí konfliktní oblasti pomocí <<<< HEAD, ==== a >>>> pohlcovana. To ale není nejpohodlnější způsob a tak nám git umožňuje použít i některé předkonfigurované nástroje k třícestnému sloučení.
  • git commit Tento příkaz sice již znáte, avšak: po manuálním vyřešení konfliktu je potřeba udělat commit a následně zavolat další příkaz - viz níže. Jinak by git nevěděl, zda konflikt chcete řešit posloupností více commitů (pamatujte na poučku o granularitě commitů).
  • git merge --continue Signál gitu že konflikt byl vyřešen a má se pokusit pokračovat ve slučování.

Příklad tentokrát nebudeme vytvářet po jednotlivých commitech, naopak jej najdeme v připravených větvích blok1_mkonflikt, blok1_mkonflikt_a a blok1_mkonflikt_b

In [26]:
pushd priklady > /dev/null
git log -n 5 --graph --decorate --oneline origin/blok1_mkonflikt_work origin/blok1_mkonflikt_a origin/blok1_mkonflikt_b
#
#
git checkout blok1_mkonflikt_work
git reset --hard origin/blok1_mkonflikt_work
git merge origin/blok1_mkonflikt_a
git merge origin/blok1_mkonflikt_b
#
cat blok1_mkonflikt/Readme.txt
# následující řádek v tichosti předpokládá GNU sed a nevedou ke správnému výsledku, jde jen o hack
sed -i '/^<<</,/^>>>/cmeld\rToto je nové a krásné ale poněkud stručné Readme, vzniknuvší pro demonstraci problémů s merge.' blok1_mkonflikt/Readme.txt
cat blok1_mkonflikt/Readme.txt
git add blok1_mkonflikt/Readme.txt
git commit -m "Merged the example (A)"
git log -n 6 --graph --decorate --oneline blok1_mkonflikt_work origin/blok1_mkonflikt_a origin/blok1_mkonflikt_b
popd > /dev/null
* bd34c34 (origin/blok1_mkonflikt_a) Conflict part A2 injected
* 7650d39 Conflict part A injected
| * 983f0ad (origin/blok1_mkonflikt_b) Conflict part B2 injected
| * f306095 Conflict part B injected
|/
* 913f11e (origin/blok1_mkonflikt_work) Adding base readme for the branch divergence example
Branch 'blok1_mkonflikt_work' set up to track remote branch 'blok1_mkonflikt_work' from 'origin'.
Switched to a new branch 'blok1_mkonflikt_work'
HEAD is now at 913f11e Adding base readme for the branch divergence example
Updating 913f11e..bd34c34
Fast-forward
 blok1_mkonflikt/.Readme.txt.merged.00 | 4 ++++
 blok1_mkonflikt/Readme.txt            | 3 ++-
 2 files changed, 6 insertions(+), 1 deletion(-)
 create mode 100644 blok1_mkonflikt/.Readme.txt.merged.00
Auto-merging blok1_mkonflikt/Readme.txt
CONFLICT (content): Merge conflict in blok1_mkonflikt/Readme.txt
Automatic merge failed; fix conflicts and then commit the result.
Pro začátek zajímavost - věděli jste, že existuje i další git-tool příkaz?
git difftool -t \
Jeho jméno se dovíte po merge.
<<<<<<< HEAD
meld
Toto je nové krásné Readme, vzniknuvší pro demonstraci problémů s merge.
=======
Toto je nové ale poněkud stručné Readme, vzniknuvší pro demonstraci problémů s merge.
>>>>>>> origin/blok1_mkonflikt_b
Aby bylo lépe vidět, jak se problémové merge chová, neváhejte použít vhodný mergetool.
Pro začátek zajímavost - věděli jste, že existuje i další git-tool příkaz?
git difftool -t \
Jeho jméno se dovíte po merge.
Toto je nové a krásné ale poněkud stručné Readme, vzniknuvší pro demonstraci problémů s merge.
Aby bylo lépe vidět, jak se problémové merge chová, neváhejte použít vhodný mergetool.
[blok1_mkonflikt_work ee38dda] Merged the example (A)
*   ee38dda (HEAD -> blok1_mkonflikt_work) Merged the example (A)
|\
| * 983f0ad (origin/blok1_mkonflikt_b) Conflict part B2 injected
| * f306095 Conflict part B injected
* | bd34c34 (origin/blok1_mkonflikt_a) Conflict part A2 injected
* | 7650d39 Conflict part A injected
|/
* 913f11e (origin/blok1_mkonflikt_work) Adding base readme for the branch divergence example

Takto pak vypadá slučování změn a jejich výsledek, pokud ve výše uvedeném příkazu použijeme pro vyřešení konfliktu git mergetool -t meld. U rozsáhlejších změn ale přijde vhod i to, že meld umí zobrazovat rozdíly v řádcích explicitním zvýrazněním sytější barvou. Bloky můžete kopírovat mezi soubory stiskem tlačítka šipky. Pokud naopak chcete nějaký blok změn odstranit, stiskněte klávesu shift a šipka se změní v křížek.

Takto vypadá sloučení v Meldu

In [27]:
pushd priklady > /dev/null
git log -n 5 --graph --decorate --oneline origin/blok1_mkonflikt_work origin/blok1_mkonflikt_a origin/blok1_mkonflikt_b
#
git checkout blok1_mkonflikt_work
git reset --hard origin/blok1_mkonflikt_work
git merge origin/blok1_mkonflikt_a
git merge origin/blok1_mkonflikt_b
#
git mergetool -t meld
# následující řádek v tichosti předpokládá GNU sed a nevedou ke správnému výsledku, jde jen o hack
git add blok1_mkonflikt/Readme.txt
git commit -m "Merged the example (meld)"
cat blok1_mkonflikt/Readme.txt
git log -n 6 --graph --decorate --oneline blok1_mkonflikt_work origin/blok1_mkonflikt_a origin/blok1_mkonflikt_b
popd > /dev/null
* bd34c34 (origin/blok1_mkonflikt_a) Conflict part A2 injected
* 7650d39 Conflict part A injected
| * 983f0ad (origin/blok1_mkonflikt_b) Conflict part B2 injected
| * f306095 Conflict part B injected
|/
* 913f11e (origin/blok1_mkonflikt_work) Adding base readme for the branch divergence example
Already on 'blok1_mkonflikt_work'
Your branch is ahead of 'origin/blok1_mkonflikt_work' by 5 commits.
  (use "git push" to publish your local commits)
HEAD is now at 913f11e Adding base readme for the branch divergence example
Updating 913f11e..bd34c34
Fast-forward
 blok1_mkonflikt/.Readme.txt.merged.00 | 4 ++++
 blok1_mkonflikt/Readme.txt            | 3 ++-
 2 files changed, 6 insertions(+), 1 deletion(-)
 create mode 100644 blok1_mkonflikt/.Readme.txt.merged.00
Auto-merging blok1_mkonflikt/Readme.txt
CONFLICT (content): Merge conflict in blok1_mkonflikt/Readme.txt
Automatic merge failed; fix conflicts and then commit the result.
Merging:
blok1_mkonflikt/Readme.txt

Normal merge conflict for 'blok1_mkonflikt/Readme.txt':
  {local}: modified file
  {remote}: modified file
[blok1_mkonflikt_work 2356406] Merged the example (meld)
Pro začátek zajímavost - věděli jste, že existuje i další git-tool příkaz?
git difftool -t meld
Toto je nové a krásné, ale poněkud stručné, Readme, vzniknuvší pro demonstraci problémů s merge.
Aby bylo lépe vidět, jak se problémové merge chová, neváhejte použít vhodný mergetool.
*   2356406 (HEAD -> blok1_mkonflikt_work) Merged the example (meld)
|\
| * 983f0ad (origin/blok1_mkonflikt_b) Conflict part B2 injected
| * f306095 Conflict part B injected
* | bd34c34 (origin/blok1_mkonflikt_a) Conflict part A2 injected
* | 7650d39 Conflict part A injected
|/
* 913f11e (origin/blok1_mkonflikt_work) Adding base readme for the branch divergence example

Jak jste si možná všimli, merge začíná na společném předkovi větví. Ač se git interně snaží (vcelku důmyslnou strategií) minimalizovat počet zásahů, často přijde vhod mít o společném předkovi a změnách ve stabilnější větvi přehled. Kupříkladu proto, že sami nemusíme být na větvi devel, ale na nějaké další, odvozené. A v té může probíhat vývoj jen jediného vylepšení. Do develu se pak v tomto modelu sbírají jednotlivá vylepšení, která jsou pak dále propagována k masteru.

Význam lineární historie

Lineární, nedivergující historie má jednu krásnou vlastnost - velmi (relativně) rychle můžeme binárním hledáním zjistit, který commit nám vnesl chybu, který vývojář kdy upravil (a s jakým zdůvodněním) který řádek. Jako rozcvičení si tedy nejprve ukážeme prstem na viníka:

In [28]:
pushd priklady > /dev/null
git checkout blok1_blame # větev s příkladem
git blame blok1_mkonflikt/Readme.txt
popd > /dev/null
Branch 'blok1_blame' set up to track remote branch 'blok1_blame' from 'origin'.
Switched to a new branch 'blok1_blame'
913f11ef (Lukas Rucka 2021-02-26 10:21:34 +0100 1) Pro začátek zajímavost - věděli jste, že existuje i další git-tool příkaz?
913f11ef (Lukas Rucka 2021-02-26 10:21:34 +0100 2) Jeho jméno se dovíte po merge.
a0d1d8c7 (Lukas Rucka 2021-03-01 13:05:12 +0100 3) git difftool -t meld
a0d1d8c7 (Lukas Rucka 2021-03-01 13:05:12 +0100 4) Toto je nové ale poněkud zlé Readme, vzniknuvší pro demonstraci problémů s merge.
913f11ef (Lukas Rucka 2021-02-26 10:21:34 +0100 5) Aby bylo lépe vidět, jak se problémové merge chová, neváhejte použít vhodný mergetool.

V řadě případů jsme ale postaveni před zcela opačný problém. Nevíme dopředu, co vlastně hledáme. Víme jen, že v některých případech poznáme co hledáme, když to najdeme.

Zní to nepředstavitelně? A co tento popis:

  • Přijali jste hlášení o chybě, řekněme snadno otestovatelné.
  • Víme, že současná verze je rozbitá, ale nevíme jak to.
  • Víme, že 6 let stará verze rozbitá nebyla, protože v ní celý subsystém ještě nebyl implementován.

Pokud bychom měli historii lineární, tedy bez merge commitů, bylo by hledání relativně snadné - stačilo by použít důvěrně známý algoritmus binárního hledání a nalézt první verzi, která je rozbitá. Seznamte se, princip algoritmu za příkazem git bisect. Tento má hned několik podpříkazů, jejichž detailní soupis získáte příkazem man git-bisect. Pro začátek tedy:

  • git bisect start bad-commit good-commit - Tímto příkazem zahájíte hledání v rozsahu uvedených commitů. Po zadání tohoto příkazu git zvolí commit poblíž pomyslného středu, který rozbalí do pracovního adresáře. Následně vám předá řízení, abyste mohli stav projektu zhodnotit. Následně provedete jeden ze dvou příkazů níže.
  • git bisect good či git bisect bad - Tato dvojice příkazů slouží k poznačení aktuálního commitu za novou mez binárního hledání. Těmito příkazy cyklicky pokračujete, dokud se hledání nezúží na jediný commit.
  • git bisect reset - Pokud máte hotovo, bývá užitečné pracovní adresář do stavu před bisectem.

Zkrátka a jednoduše, lineární historie má výhody. Jak jí ale dosáhnout (i) při souběžném vývoji mnoha lidmi? Vzpomeňte si na komentář k tomu, co pro git znamená datum. Není totiž důležité datum, ale pořadí commitů. Seznamme se s přepisováním historie.

Změny hlubokých historií

První z příkazů, se kterými se v tomto oddílu seznámíme je git rebase. Cílem tohoto příkazu je odvodit změny v aktuální větvi od jiného commitu - tedy například aktuálního vrcholu develu.

In [29]:
pushd priklady > /dev/null
git rebase --abort 2> /dev/null # reset prikladu
git log -n 5 --graph --decorate --oneline origin/blok1_mkonflikt_a origin/blok1_mkonflikt_b
#
# provedeme lokalni checkout obou vetvi
git checkout blok1_mkonflikt_work 2> /dev/null # reset prikladu
git branch -D blok1_mkonflikt_rebased_b 2> /dev/null # reset prikladu
git checkout -b blok1_mkonflikt_rebased_b $(git rev-parse origin/blok1_mkonflikt_b)
git rebase origin/blok1_mkonflikt_a
#
# nyní postupujme analogicky k merge
# 
cat blok1_mkonflikt/Readme.txt
# následující řádek v tichosti předpokládá GNU sed a nevedou ke správnému výsledku, jde jen o hack
sed -i '/^<<</,/^>>>/cmeld\rToto je nové a krásné ale poněkud stručné Readme, vzniknuvší pro demonstraci problémů s merge.' blok1_mkonflikt/Readme.txt
cat blok1_mkonflikt/Readme.txt
git add blok1_mkonflikt/Readme.txt
env EDITOR=cat git commit #-m "Rebase the example (sed)"
git rebase --continue
cat blok1_mkonflikt/Readme.txt
git log -n 7 --graph --decorate --oneline blok1_mkonflikt_rebased_b origin/blok1_mkonflikt_a origin/blok1_mkonflikt_b
popd > /dev/null
* bd34c34 (origin/blok1_mkonflikt_a) Conflict part A2 injected
* 7650d39 Conflict part A injected
| * 983f0ad (origin/blok1_mkonflikt_b) Conflict part B2 injected
| * f306095 Conflict part B injected
|/
* 913f11e (origin/blok1_mkonflikt_work) Adding base readme for the branch divergence example
Your branch is ahead of 'origin/blok1_mkonflikt_work' by 5 commits.
  (use "git push" to publish your local commits)
Switched to a new branch 'blok1_mkonflikt_rebased_b'
Auto-merging blok1_mkonflikt/Readme.txt
CONFLICT (content): Merge conflict in blok1_mkonflikt/Readme.txt
error: could not apply f306095... Conflict part B injected
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply f306095... Conflict part B injected
Pro začátek zajímavost - věděli jste, že existuje i další git-tool příkaz?
Jeho jméno se dovíte po merge.
<<<<<<< HEAD
meld
Toto je nové krásné Readme, vzniknuvší pro demonstraci problémů s merge.
=======
Toto je nové ale poněkud stručné Readme, vzniknuvší pro demonstraci problémů s merge.
>>>>>>> f306095 (Conflict part B injected)
Aby bylo lépe vidět, jak se problémové merge chová, neváhejte použít vhodný mergetool.
Pro začátek zajímavost - věděli jste, že existuje i další git-tool příkaz?
Jeho jméno se dovíte po merge.
Toto je nové a krásné ale poněkud stručné Readme, vzniknuvší pro demonstraci problémů s merge.
Aby bylo lépe vidět, jak se problémové merge chová, neváhejte použít vhodný mergetool.
hint: Waiting for your editor to close the file...
Conflict part B injected

# Conflicts:
# blok1_mkonflikt/Readme.txt

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# interactive rebase in progress; onto bd34c34
# Last command done (1 command done):
#    pick f306095 Conflict part B injected
# Next command to do (1 remaining command):
#    pick 983f0ad Conflict part B2 injected
# You are currently rebasing branch 'blok1_mkonflikt_rebased_b' on 'bd34c34'.
#
# Changes to be committed:
# modified:   blok1_mkonflikt/Readme.txt
#
# Untracked files:
# blok1_mkonflikt/Readme.txt.orig
#
[detached HEAD 0de4c93] Conflict part B injected
 1 file changed, 1 insertion(+), 2 deletions(-)
Successfully rebased and updated refs/heads/blok1_mkonflikt_rebased_b.
Pro začátek zajímavost - věděli jste, že existuje i další git-tool příkaz?
git difftool -t \
Jeho jméno se dovíte po merge.
Toto je nové a krásné ale poněkud stručné Readme, vzniknuvší pro demonstraci problémů s merge.
Aby bylo lépe vidět, jak se problémové merge chová, neváhejte použít vhodný mergetool.
* 0a92a2a (HEAD -> blok1_mkonflikt_rebased_b) Conflict part B2 injected
* 0de4c93 Conflict part B injected
* bd34c34 (origin/blok1_mkonflikt_a) Conflict part A2 injected
* 7650d39 Conflict part A injected
| * 983f0ad (origin/blok1_mkonflikt_b) Conflict part B2 injected
| * f306095 Conflict part B injected
|/
* 913f11e (origin/blok1_mkonflikt_work) Adding base readme for the branch divergence example

Porovnejte si výpis historie s tím, co vidíte po aplikaci merge. Tentokrát sice nemáme žádný merge commit, ale za to máme commity původně z větve blok1_mkonflikt_b duplikovány (což je ale jen logický důsledek nutnosti pokrýt novou historii). Získali jsme ale významnou výhodu - lineární historii. Příkaz git rebase ale získá zcela novou sílu s přepínačem -i, tedy interaktivní rebase.

Podstatou interaktivního rebase je to, že historie kterou začleňujeme není jen pasivně přejata, ale můžeme do ní zasáhnout. Co to znamená?

  • Vybrat které commity se mají aplikovat tak, jak jsou.
  • Vybrat pořadí aplikace commitů.
  • Rozdělit existující commit, či naopak sloučit commity do jednoho.
  • Zcela zahodit některé commity.

Tato kombinace možností má jeden nečekaný důsledek - v pracovním repozitáři můžete mít soukromou větev s velkými desítkami commitů, které pak při přípravě na začlenění do vývojové či stabilní větve setřepete na hromádku, vyčistíte od svých poznámek a vyrobíte z nich sadu změn o (např.) 10 commitech, které jsou věcné a všeobecně působí profesionálním dojmem.

Velkou nevýhodou rebase je, že ztrácíme vazbu na původní historii. Každý commit získává totiž nový hash, nesouvisející s původním. Zde se skrývá velké nebezpečí, protože zatím co v případě merge je nový vrchol aktuální větve potomkem původního, zde bude jejich vztah odpovídat spíše sourozencům.

Představme si, co by se stalo, pokud bychom provedli rebase větve, která je již vykopírována jinam.

In [30]:
test -d priklady-rebase-misuse && rm -rf priklady-rebase-misuse
pushd priklady > /dev/null
git checkout blok1_mkonflikt_a
git branch -D blok1_rebase_misuse 2> /dev/null
git branch blok1_rebase_misuse $(git rev-parse origin/blok1_mkonflikt_b)
popd > /dev/null
#
# priklady ted obsahuje vetev odvozenou z blok1_mkonflikt_b
# repozitar naklonujeme a v klonu provedeme rebase nad zmeny v blok1_mkonflikt_a
# a nasledne zkusime repozitare synchronizovat
#
git clone ./priklady ./priklady-rebase-misuse
pushd priklady-rebase-misuse > /dev/null
git checkout blok1_rebase_misuse
git rebase origin/blok1_mkonflikt_a
sed -i '/^<<</,/^>>>/cmeld\rToto je nové a krásné ale poněkud stručné Readme, vzniknuvší pro demonstraci problémů s merge.' blok1_mkonflikt/Readme.txt
git add blok1_mkonflikt/Readme.txt
env EDITOR=cat git commit > /dev/null
git rebase --continue
# rebase hotovo, muzeme synchronizovat
git push
# push neprosel, v nadrazenem repozitari jsou zmeny
git pull
sed -i '/^<<</,/^>>>/cmeld\rToto je nové a krásné ale poněkud stručné Readme, vzniknuvší pro demonstraci problémů s merge.' blok1_mkonflikt/Readme.txt
git add blok1_mkonflikt/Readme.txt
git commit -m "Yet another?"
git log -n 10 --graph --decorate --oneline
popd > /dev/null
Branch 'blok1_mkonflikt_a' set up to track remote branch 'blok1_mkonflikt_a' from 'origin'.
Switched to a new branch 'blok1_mkonflikt_a'
Cloning into './priklady-rebase-misuse'...
done.
Branch 'blok1_rebase_misuse' set up to track remote branch 'blok1_rebase_misuse' from 'origin'.
Switched to a new branch 'blok1_rebase_misuse'
Auto-merging blok1_mkonflikt/Readme.txt
CONFLICT (content): Merge conflict in blok1_mkonflikt/Readme.txt
error: could not apply f306095... Conflict part B injected
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply f306095... Conflict part B injected
hint: Waiting for your editor to close the file...
Successfully rebased and updated refs/heads/blok1_rebase_misuse.
To /builds/pb176/private/prednasky/./priklady
 ! [rejected]        blok1_rebase_misuse -> blok1_rebase_misuse (non-fast-forward)
error: failed to push some refs to '/builds/pb176/private/prednasky/./priklady'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
hint: Pulling without specifying how to reconcile divergent branches is
hint: discouraged. You can squelch this message by running one of the following
hint: commands sometime before your next pull:
hint:
hint:   git config pull.rebase false  # merge (the default strategy)
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
Auto-merging blok1_mkonflikt/Readme.txt
CONFLICT (content): Merge conflict in blok1_mkonflikt/Readme.txt
Automatic merge failed; fix conflicts and then commit the result.
[blok1_rebase_misuse cf454a5] Yet another?
*   cf454a5 (HEAD -> blok1_rebase_misuse) Yet another?
|\
| * 983f0ad (origin/blok1_rebase_misuse) Conflict part B2 injected
| * f306095 Conflict part B injected
* | 81d54d2 Conflict part B2 injected
* | 12f8f8d Conflict part B injected
* | bd34c34 (origin/blok1_mkonflikt_a, origin/HEAD, blok1_mkonflikt_a) Conflict part A2 injected
* | 7650d39 Conflict part A injected
|/
* 913f11e Adding base readme for the branch divergence example
* 0bd9880 Blok1: diff, patch, copy hell
* e5d6f01 Initial commit

GUI

Na internetu najdete různá GUI pro git, mnohdy i jako součást některých IDE. Explicitně si dovolím některá zmínit:

  • Ungit - Node.js aplikace, přístup přes webový prohlížeč.
  • GitHub Desktop - Aplikace vyvíjená GitHubem, historicky nepodporovala alternativní hostingy.
  • GitKraken - Komerční aplikace s neplacenou verzí pro soukromé užití (s limitovanou funkcionalitou).
  • TortoiseGit - Relativně malý, integrace do průzkumníka Windows.
  • gitk - TCL gui pro git, v některých distribucích součást instalace gitu.

Best practices

  • Nejprv pull (fetch + log + diff + merge/rebase), poté teprve push.
  • Vývoj dělejte ve svých (privátních) větvích.
  • Privátní větve synchronizujte jen proti privátním repozitářům.
  • Udělat dva commity zavčasu je snazší a levnější, než commit zpětně rozdělovat.
  • Dělejte hodně dílčích commitů, čas od času udělejte interaktivní rebase s fixupy, squashi, ...
  • Mějte správně nastavený .gitignore, raději explicitní ignorování než wildcard. Verzujte .gitignore.
  • Zaveďte si pre-push háček zamítající commity s názvy WIP či TMP jinam, než do privátního repozitáře.

Co bude teprve pokryto

  • git cherry-pick (Blok 3)
  • GitLab (Blok 3)
  • Tagy vs. větve (Blok 4)
  • Háčky (hooks), avšak pouze okrajově

Co nebylo (záměrně) pokryto

  • git config --global, git config --local - uživatelská nastavení gitu, globální a per-repo
  • git remote - správa vazeb mezi repozitáři
  • git merge -s ours strategie merge, merge s neaplikováním 2. větve
  • git am accept merge - začleňování patchů odjinud
  • Migrace z jiných SCM včetně přenosu historie (cvs, svn přímo, ostatní pomocí exportů)
  • Git shell
  • Submoduly (vnořené repozitáře)
  • Hashe v gitu - objekty, stavy repozitářů.

Samostudium

  • git diff - diff proti verzi známé gitu
  • .gitignore, .gitattributes - maska souborů, kterých si git nemá všímat + atributy (např. vypnutí ošetření \r\n)
  • git reflog - log změn/checkoutů větví a commitů
  • git rev-parse - z reference získá hash
  • git commit --amend - úprava nejnovějšího commitu
  • git commit --sign-off - schvalování commitů konkrétním vývojářem
  • git stash - privátní dočasné commity (stashe) pro odložení rozpracovaného kódu
  • git merge-base - společný předek pro merge
  • git tag - aliasování commitů (zafixování verze)
  • git gc a git fsck - velmi důležité pro obnovu ztracených dat
  • ls -aihl .git/hooks - události v repozitáři

Doplňující literatura

  • Příručka.
  • Slovník - ukotvuje názvosloví a pojmy, důležité i pro manuál (tree-ish, commit-ish).
  • Manuálové stránky v Linuxu man git-commit pro nápovědu k příkazu git commit.

Pro pobavení a zamyšlení

  • Příkaz pro ujasnění priorit - git fire, příkaz který vznikl jako reakce na anekdotu z 1. apríla. Užitečný.

Blok 2 - Buildsystémy

Sestavení projektu

Proč lidé nakupují i dnes více zásob, než bezprostředně spotřebují? Protože jsme v zásadě tvorové líní. Protože je občas snazší koupit polotovar s unifikovanou chutí, než uvařit něco z čerstvých surovin dle neurčitého návodu plného frází jako "dosolíme dle chuti". Proč by programování a vývoj mělo být jiné?

Historické okénko - dokumentace a návody

Richard Feynman se ve své vzpomínkové (a snad i poněkud humoristické) knize vrací (mimo jiné) k popisu fungování výpočetního centra laboratoří Los Alamos, kde popisuje i systém kterým tehdy řešili paralelizaci úloh i korekce výpočtů. Takový popis rozhodně stojí za to, ale - přiznejme si - postup fungování výpočetního centra z něj reprodukovatelný není. Přesto ale byl někde zdokumentován, aby byl nasaditelný i v dalších pobočných výzkumných laboratořích v USA.

Přesto, stejně jako dnešní dokumentace - trpí na jeden zásadní problém - a to je aktuálnost dokumentace. Typicky totiž takovou dokumentaci udržujeme stranou od dokumentovaného díla, což sice zvyšuje přehlednost, ale přináší problémy se synchronizací.

Vezměme v úvahu už jen instrukce pro zprovoznění jupyter notebooku na aise, které jste měli možnost vidět naživo na první přednášce. Ruku na srdce, replikovatelnost takovéhoto postupu, zejména pokud s ním nemáte předchozí zkušenosti - bude mizivá. Pokud by se ale povedlo celé instrukce zjednodušit do jediného příkazu, byla by úspěšnost obratem vyšší.

Než se ale pustíme do něčeho obsáhlejšího, přijde vhod krátká rekapitulace příkazové řádky na *NIXových systémech.

Interaktivní shell a jeho příkazy

Pozn.: Tato část je jen velmi rychlým crashkurzem, pro komplexní vysvětlení látky uvažte předměty PB152, PV004, PV065.

Příkazová řádka typicky nabývá podoby:

prompt > příkaz --přepínač argument

Pojďme popořadě:

  • shell - prostředí, pomocí kterého komunikuje uživatel s počítačem. Může být konzolové (terminálové), nebo grafické (např. Windows, gnome-shell, ...).
  • prompt - česky také výzva shellu - je řetězec, kterým (textový) shell informuje uživatele že očekává vstup. Výzva může mít více úrovní, podle toho co od nás shell očekává, či její součástí může být i informace o aktuálním čase, adresáři, návratovém kódu předchozího případu - variant je mnoho. Výzva bývá zakončena nějakým nepísmenným znakem - typicky např. #, >, $. Za výzvu píšeme příkaz.
  • příkaz - program, který chceme spustit. V praxi jsou 3 druhy příkazů: Příkazy vestavěné ve shellu. Ovlivňují to, jak shell pracuje, mění jeho vnitřní proměnné. Externí programy umístěné v některém z adresářů uvedených ve výčtu v proměnné prostředí PATH. ** Absolutní či relativní cesty ke spouštěným programům, i těm v PATH.
  • přepínače a argumenty - jsou jedno a totéž, argumenty začínající -- či - je nicméně zvykem označovat jako přepínače. Navzájem jsou odděleny znakem mezery, tuto lze escapovat - typicky \, nebo ji uvést uvnitř uvozovek či apostrofů.

Aby mohly jednotlivé příkazy na sebe navazovat, předávat si data, užívá se dále pomocných konstrukcí:

  • | (pipe, svislítko, roura) a její kombinovaná varianta |& - pokyn pro shell, že má standardní výstup jednoho procesu pro standardní vstup druhého.
  • >, 2>, 2>&1, >>, 2>>, < - přesměrování standardního výstupu (1-5) procesu do souboru (přepsat, přepsat soubor standardním chybovým výstupem, sloučit chybový výstup do standardního výstupu, přidávat k souboru a přidávat chybový výstup`; potažmo přesměrování souboru na standardní vstup (6).

Buildskripty - build.sh

Skripty se začínají objevovat ruku v ruce s prvními operačními systémy a jejich shelly. Nejjednodušší skripty jsou jen sérií příkazů shellu, zapsaných za sebe, ať již předem, či jen zkopírováním toho co kdo kam zadával. Ty složitější již mohou vytvářet velmi komplikované a nečitelné programy.

Z ukázek kódu v tomto dokumentu můžeme snadno sestavit skript instalující na aisu tyto přednášky a Jupyter, prostým vložením příkazů do souboru (viz priklady/blok2_buildsh1/build.sh).

In [31]:
#!/bin/bash

module load python3-3.7.4-aisa
pip3.7 install --user notebook
pip3.7 install --user bash_kernel
python3 -m bash_kernel.install

rm -rf ~/pb176_workdir
mkdir -p ~/pb176_workdir
cd ~/pb176_workdir

git clone https://gitlab.fi.muni.cz/pb176/prednasky.git
cd prednasky
git clone https://gitlab.fi.muni.cz/pb176/private/2022-priklady-todo priklady
Cloning into 'prednasky'...
remote: Enumerating objects: 47, done.
remote: Counting objects: 100% (18/18), done.
remote: Compressing objects: 100% (18/18), done.
remote: Total 47 (delta 6), reused 1 (delta 0), pack-reused 29
Receiving objects: 100% (47/47), 5.83 MiB | 20.94 MiB/s, done.
Resolving deltas: 100% (17/17), done.
Cloning into 'priklady'...
warning: redirecting to https://gitlab.fi.muni.cz/pb176/private/2022-priklady-todo.git/
remote: Enumerating objects: 160, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 160 (delta 0), reused 0 (delta 0), pack-reused 156
Receiving objects: 100% (160/160), 21.21 KiB | 5.30 MiB/s, done.
Resolving deltas: 100% (42/42), done.

Souběžně s build.sh můžeme vytvořit i skript odpovědný za spuštění notebooku - typicky run.sh.

In [32]:
#!/bin/bash

cd ~/pb176_workdir/prednasky
git pull
jupyter-notebook --port 12348 --no-browser
hint: Pulling without specifying how to reconcile divergent branches is
hint: discouraged. You can squelch this message by running one of the following
hint: commands sometime before your next pull:
hint:
hint:   git config pull.rebase false  # merge (the default strategy)
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
Already up to date.
launichng tainted notebook
[I 18:32:51.758 NotebookApp] Writing notebook server cookie secret to /root/.local/share/jupyter/runtime/notebook_cookie_secret
[I 18:32:51.982 NotebookApp] Serving notebooks from local directory: /root/pb176_workdir/prednasky
[I 18:32:51.982 NotebookApp] Jupyter Notebook 6.2.0 is running at:
[I 18:32:51.982 NotebookApp] http://localhost:12348/?token=25c2efc749929a722ba4f8b6240f92c5d65c8388ebff706b
[I 18:32:51.982 NotebookApp]  or http://127.0.0.1:12348/?token=25c2efc749929a722ba4f8b6240f92c5d65c8388ebff706b
[I 18:32:51.982 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[C 18:32:51.986 NotebookApp]

    To access the notebook, open this file in a browser:
        file:///root/.local/share/jupyter/runtime/nbserver-2199-open.html
    Or copy and paste one of these URLs:
        http://localhost:12348/?token=25c2efc749929a722ba4f8b6240f92c5d65c8388ebff706b
     or http://127.0.0.1:12348/?token=25c2efc749929a722ba4f8b6240f92c5d65c8388ebff706b
[C 18:33:11.457 NotebookApp] received signal 15, stopping
[I 18:33:11.457 NotebookApp] Shutting down 0 kernels
[I 18:33:11.457 NotebookApp] Shutting down 0 terminals
notebook should be dead by now

Co se ale stane, pokud takový skript pustíme vícekrát po sobě?

Jednotlivé řádky jsou více či méně nezávislé, přesto předpokládají úspěšné dokončení těch předchozích. Mohou proto selhat, tím že jsme část práce "automatizovali" jsme ale přišli právě o možnost reagovat na to, jak se skripty chovají. Přitom na vině nemusí být nic složitějšího než změna pracovního adresáře. Naštěstí můžeme závislost mezi jednotlivými řádky vyjádřit již ve skriptu - jednoduše podmíníme vykonání nového příkazu úspěchem předešlého. Nejjednodušší způsob, jak toho docílit v interaktivním shellu je vložit mezi oba příkazy operátor logického součinu - &&. Náš skript to poněkud... poznamená.

In [33]:
#!/bin/bash

module load python3-3.7.4-aisa && \
pip3.7 install --user notebook && \
pip3.7 install --user bash_kernel && \
python3 -m bash_kernel.install && \
\
echo "zkuste si nasledujici radek zakomentovat a pustit tento skript 2x po sobě" && \
rm -rf ~/pb176_workdir && \
mkdir -p ~/pb176_workdir && \
cd ~/pb176_workdir && \
\
git clone https://gitlab.fi.muni.cz/pb176/prednasky && \
cd prednasky && \
git clone https://gitlab.fi.muni.cz/pb176/private/2022-priklady-todo priklady && \
cd ..
zkuste si nasledujici radek zakomentovat a pustit tento skrip 2x po sobě
Cloning into 'prednasky'...
warning: redirecting to https://gitlab.fi.muni.cz/pb176/prednasky.git/
remote: Enumerating objects: 47, done.
remote: Counting objects: 100% (18/18), done.
remote: Compressing objects: 100% (18/18), done.
remote: Total 47 (delta 6), reused 1 (delta 0), pack-reused 29
Receiving objects: 100% (47/47), 5.83 MiB | 21.62 MiB/s, done.
Resolving deltas: 100% (17/17), done.
Cloning into 'priklady'...
warning: redirecting to https://gitlab.fi.muni.cz/pb176/private/2022-priklady-todo.git/
remote: Enumerating objects: 160, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 160 (delta 0), reused 0 (delta 0), pack-reused 156
Receiving objects: 100% (160/160), 21.21 KiB | 7.07 MiB/s, done.
Resolving deltas: 100% (42/42), done.

Účel to sice splnilo, ale není to řešení, ba naopak, celou věc silně komplikujeme [^sete:]. Navíc závisíme na konkrétní verzi pythonu3 - python3-3.7.4-aisa. Místo jednoduché soupisky instrukcí končíme se skriptem, ve kterém jen narůstá podíl režijního kódu. A právě z tohoto důvodu (a řady jiných) vzešla idea "sepisovat primárně závislosti". O zbytek, ať se postará někdo jiný - buildsystém.

Jako drobné cvičení se zamyslete, zda jde o vestavěný příkaz či externí program.


    Make

    Z výše uvedené myšlenky vychází i nástroj make a jeho konfigurační soubor Makefile. Původní make vychází z PWB/UNIX 1.0 a datuje se do roku 1976, takže můžeme směle prohlásit že by dnešním dialektům a systémům příliš nerozuměl. Zpravidla se tak setkáte s GNU make, který má pár triků v rukávu navíc.

    Soubor Makefile se skládá z několika prvků:

    • Proměnné - v souboru můžeme mít zavedené proměnné, kterými upravujeme jednak interpretaci samotného Makefile, druhak i chování spouštěných programů. Proměnné mohou být nicméně nastavovány jen ve scope (oboru platnosti) souboru. Zevnitř pravidla proměnnou nastavit nelze.
    • Cíle a pravidla. Cíle jsou de facto soubory. Pravidla pro vytvoření cíle jsou vůči cíli odsazena tabulátorem. make se stará o to, aby byly cíle novější než jejich závislosti. Závislosti lze v souboru uvádět průběžně a stejně tak je lze i opakovat (jsou uvažovány jako množina). Každý řádek pravidla je spuštěn v separátním shellu. Pravidla mohou běžet paralelně, o jejich souběh se stará make, který pravidla spouští tak aby byly uspokojeny závislosti mezi cíli.
    #!/usr/bin/make -f
    
    PROMENNA=hodnota
    PROMENNA2:=hodnota pokud neni nastavena z venci
    
    # komentář, první pravidlo je výchozím pro provedení při spuštění make bez explicitního cíle
    all: soupiska cilu
    
    soupiska: soupiska.c
    
    cilu: cilu.c
    
    clean:
        rm -rf cilu soupiska
    #<tabulátor>rm -rf cilu soupiska
    
    In [34]:
    pushd prednasky > /dev/null
    cat priklady/blok2_makefile_syntax/Makefile
    popd > /dev/null
    
    #!/usr/bin/make -f
    
    CC=gcc
    
    cil: zavislosti
      echo prikaz1
      echo $(CC) -o $@ -std=c99 $^
      echo prikaz co selze
      touch /neexistujici/adresarova/struktura
    
    zavislosti:
      echo cil se zavislostmi
    

    Pojďme se podívat, jak by - zapsány formou receptu (recipe) vypadal původní skript build.sh

    #!/usr/bin/make -f
    
    run: pb176_prednasky
        cd ~/pb176_workdir/prednasky
        git pull
        jupyter-notebook --port 12348 --no-browser
    
    pb176_prednasky: jupyter
        mkdir -p ~/pb176_workdir
        cd ~/pb176_workdir
        git clone https://gitlab.fi.muni.cz/pb176/prednasky
        cd prednasky
        git clone https://gitlab.fi.muni.cz/pb176/private/2022-priklady-todo priklady
    
    jupyter:
        module load python3-3.7.4-aisa
        pip3.7 install --user notebook
        pip3.7 install --user bash_kernel
        python3 -m bash_kernel.install
    

    Tento recept nicméně nefungoval (ne tak jak jsme očekávali), proč? Důvodů je několik:

    • Každý řádek receptu je spouštěn v odděleném subshellu.
    • Makefile poměřuje jen a pouze stáří/přítomnost cíle (souboru).
    • Selhání libovolného cíle povede k selhání celého make

    To první co můžeme udělat je převést samostatné řádky pravidel do dlouhého příkazu. Při té příležitosti můžeme Makefile i zobecnit, kupříkladu když by se na aise potkalo více studentů na jednou - každý bude pro svůj notebook potřebovat unikátní port.

    In [35]:
    pushd prednasky > /dev/null
    cat priklady/blok2_make2/Makefile
    popd > /dev/null
    
    #!/usr/bin/make -f
    
    NBPORT=12348
    NOTEBOOK=jupyter-notebook
    PIP=pip3.7
    PYTHONMODULE=python3-3.7.4-aisa
    
    run: pb176/prednasky prednasky_sync
      cd ~/pb176/prednasky && \
      git pull && \
      jupyter-notebook --port $(NBPORT) --no-browser
    
    pb176/prednasky: jupyter
      mkdir -p ~/pb176 && \
      cd ~/pb176 && \
      git clone git@gitlab.fi.muni.cz:pb176/2021-prednasky && \
      cd 2021-prednasky && \
      git clone git@gitlab.fi.muni.cz:pb176/2021-priklady priklady && \
    
    prednasky_sync: pb176/prednasky
      cd pb176/prednasky && \
      git fetch origin && \
      git checkout master && \
      git reset --hard origin/master && \
      cd priklady && \
      git fetch origin && \
      git checkout master && \
      git reset --hard origin/master
    
    
    jupyter: $(HOME)/.local/bin/jupyter-notebook
      module load $(PYTHONMODULE) && \
      $(PIP) install --user notebook && \
      $(PIP) install --user bash_kernel && \
      python3 -m bash_kernel.install
    
    

    Toto samo o sobě ale ještě nestačí, protože máme nadále příkazy, které budou selhávat. Nezávisle na tomto nyní musí každý pro unikátní číslo portu editovat Makefile, čímž jsme nic navíc nezískali. Další úprava substitucí je tedy nasnadě - nemít číslo portu zadrátováno v Makefile, ale převzít jej zvenčí. To nám zajistí nahrazení = v definici proměnných za ?=.

    In [36]:
    pushd prednasky > /dev/null
    cat priklady/blok2_make3/Makefile
    popd > /dev/null
    
    #!/usr/bin/make -f
    
    NBPORT ?= 12348
    NOTEBOOK ?= jupyter-notebook
    PIP ?= pip3.7
    PYTHONMODULE ?= python3-3.7.4-aisa
    
    run: pb176/prednasky prednasky_sync
      cd ~/pb176/prednasky && \
      git pull && \
      jupyter-notebook --port $(NBPORT) --no-browser
    
    pb176/prednasky: jupyter
      mkdir -p ~/pb176 && \
      cd ~/pb176 && \
      git clone git@gitlab.fi.muni.cz:pb176/2021-prednasky && \
      cd 2021-prednasky && \
      git clone git@gitlab.fi.muni.cz:pb176/2021-priklady priklady && \
    
    .PHONY: prednasky_sync
    
    prednasky_sync: pb176/prednasky
      cd pb176/prednasky && \
      git fetch origin && \
      git checkout master && \
      git reset --hard origin/master && \
      cd priklady && \
      git fetch origin && \
      git checkout master && \
      git reset --hard origin/master
    
    
    jupyter: $(HOME)/.local/bin/jupyter-notebook
      ifeq ($(HOST),aisa.fi.muni.cz)
      module load $(PYTHONMODULE) && \
      endif
      $(PIP) install --user notebook && \
      $(PIP) install --user bash_kernel && \
      python3 -m bash_kernel.install
    
    

    Make pak zavoláme příkazem:

    In [37]:
    pushd prednasky > /dev/null
    cd priklady/blok2_make3
    make PORT=9988 # listen on 9988/tcp
    popd > /dev/null
    
    ifeq (,aisa.fi.muni.cz)
    /bin/sh: 1: Syntax error: word unexpected (expecting ")")
    make: *** [Makefile:34: jupyter] Error 2
    

    Jsou nicméně situace, kdy stávající struktura pravidel neodpovídá našim představám. Typicky tomu bývá v případech:

    • Některé cíle chceme provést vždy, bez ohledu na existenci a stáří záležitostí.
    • Chceme sestavit podadresáře jejich vlastním Makefile.
    • Chceme vytvářet cíle a zdroje i v podadresářích.

    První z položek je v Makefile dosažen speciálními cíli a zdroji, atributy. Příkladem takového speciálního cíle, který je sestaven vždy, je cíl .PHONY . Pokud tedy chceme nějaký cíl znovu vytvořit s každým jeho zařazením mezi závislosti, uvedeme jej v závislostech cíle .PHONY.

    .PHONY: clean
    
    clean:
        rm -rf index/index
    

    Cíl clean je tedy make znovu sestaven pokaždé, když o něj někdo zažádá.

    Druhý bod je o něco pragmatičtější, bez speciálních triků. Nikdo nám totiž nebrání - v rámci pravidla zavolat... make. Volání se zde malinko liší dle platformy, zatím co GNU make má přepínač -C, make na jiných platformách jej mít nemusí. Vzpomeňme ale, že každý řádek je vykonán v samostatném shellu. A tedy:

    .PHONY: index
    
    index:
        cd index ; make
    

    Poslední z uvedených položek je triviální. Jednoduše stačí cíl zapsat jako relativní cestu.

    index/demo: demo
        ln -s ../demo index/demo
    

    O prostředí

    Prostředím zpravidla rozumíme nějaký soubor softwarových nástrojů a hardware, jejich propojení, nastavení a zdroje jimi zpřístupněné. V přeneseném smyslu slova lze tedy do této definice hravě skrýt ledasco. Zkusme tedy, pro usnadnění orientace, problém obrátit - místo otázky "co všechno je ještě prostředí?" uvažujme test "je toto součást prostředí?"

    Pro začátek shell. Spouštíme pomocí něj programy. Pokud budeme mít starou verzi bashe, která neumí přesměrování výstupu pomocí |&, sotva nám bude fungovat skript tento operátor využívající. Jinými slovy, náš skript závisí na nějaké minimální verzi bashe. Tento typ závislosti je vcelku přímočarý, existují ale i nějaké jiné typy závislostí než "mít nainstalovaný program AB ve verzi XY?"

    Vzpomeňme na podobu skriptu. Udělali jsme tam jednu (možná?) chybu - nikde jsme nenapsali, kde se mají programy které v něm voláme hledat. Náš postup docela vystihuje filozofii "nějak to zařiď". Jednoduše se spoléháme na to, že shell nějak vybere ten správný program. A shell se spoléhá na to, že program který po něm chceme, bude umístěn v některém z adresářů uvedených v proměnné PATH. Přesněji řečeno, PATH je proměnnou prostředí.

    In [38]:
    echo $PATH
    
    /builds/pb176/private/prednasky/ci-overrides/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    

    Proměnné prostředí

    Proměnné prostředí v posixovém shellu nastavujeme příkazem export (pak se vztahují i na shell samotný), nebo příkazem env (pak se vztahují jen na dceřiný proces).

    In [39]:
    #!/bin/bash
    env PATH=/usr/bin:/bin bash -c 'echo $PATH'
    echo $PATH
    #vs.
    export MINION_DEP=banana
    bash -c 'echo $MINION_DEP'
    echo $MINION_DEP
    
    /usr/bin:/bin
    /builds/pb176/private/prednasky/ci-overrides/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    banana
    banana
    

    Podívejme se tedy na některé proměnné prostředí, které mohou ovlivnit naše fungování:

    • PATH - Proměnná prostředí, obsahující výčet adresářů kde se hledají programy implementující příkazy zadané bez cesty ke spustitelnému souboru. Na Linuxu a příbuzných platformách jsou jednotlivé položky odděleny znakem dvojtečka (:). Co myslíte, jak se oddělují záznamy ve Windows?
    • LD_LIBRARY_PATH - Proměnná prostředí, obsahující výčet adresářů kde se hledají v okamžiku spuštění programu dynamicky linkované knihovny (obdoba .dll známých z Windows, případně .dl z Mac OS). Záznamy jsou opět odděleny dvojtečkou.
    • LIBRARY_PATH - Totéž co LD_LIBRARY_PATH, avšak prohledávána v čase kompilace.
    • C_INCLUDE_PATH - Tato proměnná obsahuje výčet adresářů, které mají být považovány za adresáře se systémovými include pro jazyk C. Záznamy odděleny... dvojtečkou.
    • CPLUS_INCLUDE_PATH - Totéž, ale pro C++.
    • CMAKE_PREFIX_PATH - Adresáře, kde má CMake hledat podpůrné moduly a balíčky (systémové).
    • PKG_CONFIG_PATH - Adresáře, kde má pkg-config hledat rozšiřující knihovny.
    • PYTHONPATH - Adresáře s lokálními instalacemi pythonovských knihoven.

    Obsahují ale proměnné prostředí i něco jiného než výčty adresářů?

    • EDITOR - Příkaz pro spuštění výchozího textového editoru.
    • PAGER - Příkaz pro spuštění stránkovacího programu (vyzkoušejte less vs. more).
    • PWD - Cestu k pracovnímu adresáři.
    • HOME - Cestu k domovskému adresáři.

    A konečně,

    • PS1 - Primární prompt shellu, tedy ten který vás vyzývá k zadání příkazu. Můj vypadá takto (s barevnými substitucemi):
    \[\033[01;32m\]\u@\h\[\033[01;34m\] \w \$\[\033[00m\]
    

    Proč ale máme tolik proměnných s cestami, které zdánlivě jen kopírují standardní cesty k programům? Protože nám to nabízí alternativu ke standardním cestám, kterou můžeme ovlivnit z prostředí uživatele bez nutnosti kontaminace prostředí celého systému a jiných uživatelů. Ba co více, i v rámci vlastního prostředí můžeme měnit to co se kde použije pouhou změnou proměnné prostředí v potomkovi našeho shellu. Můžeme mít nainstalováno mnoho různých verzí téhož programu, jen v různých cestách. Konec konců, právě proměnné prostředí jsou to co umožňuje fungovat příkazu module, který znáte z aisy.

    Platforma

    Pod pojmem prostředí jsme ale doteď diskutovali pouze softwarové prvky. Závislosti mezi instalovanými programy, jejich verze, proměnné ovlivňující chování. Pokud nám ale přijde od zákazníka oznámení, že "ten náš nový systém je příliš pomalý", "trvá to dlouho" apod., nebude nám povědomí o softwarové konfiguraci nejspíše stačit. Je tedy nutné uvažovat i hardware, jeho výkon, časování, zapojení. A to bez ohledu na to, zda jde o hardware virtualizovaný (VirtualBox, KVM, OpenNebula - stratus, ...), či fyzický. Jako platformu tedy můžeme uvažovat zobecnění prostředí i na prvky fyzického světa. Tedy donedávna jsme mohli, masivní rozvoj architektur typu platform as a service a virtualizace, zefektivňování návrhu hardware - to vše vede ke stírání rozdílů mezi software a hardware. Pod pojmem platforma tedy dnes najdete i ryze softwarová řešení. Proto tento pojem ponechme spíše synonymem.

    Toolchain

    Od historického modelu, kdy docházelo k aktualizacím kompletní výměnou (nejlépe sálového počítače) se svět software vydal se vznikem a rozvojem OS v 80. letech cestou modularizace. Vydefinování rozhraní mezi moduly - ve světě UNIXu často formou souboru s více či méně rigidní strukturou - umožnilo vylepšovat systém jako celek posupně, po částech.

    Pojmem toolchain tedy rozumíme kolekci nástrojů, které mezi sebou navzájem interagují a kde se výsledky jednoho nástroje stávají vstupy nástroje jiného. To je často i díky tomu, že jednotlivé nástroje toolchainu mají často stejného výrobce. Příklady toolchainu mohou být:

    • GNU - GNU make, GNU Compiler Collection (GCC), GNU C Library (glibc), GNU M4 + GNU Autotools, GNU Debugger (gdb), ...
    • The LLVM Compiler Infrastructure - llvm, clang, clang++, clang-tidy, clang-format, ...
    • Java SE Tools - Annotation Processing Tool (apt), javac, jvisualvm, jconsole, jinfo, jps, ...

    V některých případech lze navíc jednotlivé části toolchainů kombinovat - nikdo vám nebrání použít GNU Make a javac, gdb a clang, či jiné kombinace. Toto v praxi umožňuje poměrně velkou variabilitu.

    Závislosti I

    Doposud naše úvahy neobnášely externí knihovny, přesto je jich svět pln. Ve světě unixových systémů dlouho platil za základní nástroj balíček pkg-config, který funguje jako nápověda pro programátora, jak má upravit přepínače kompilátoru pro užití externí knihovny. To díky tomu, že nutnost uvádět konkrétní argumenty pro danou platformu přenáší z koncového programátora na autora knihovny. Ta musí poskytnout odpovídající soubor s příponou .pc, který pkg-config hledá ve svých standardních cestách.

    Jeho použití je pak prosté, pro získání přepínačů pro kompilaci lze užít přepínače pkg-configu --cflags, pro argumenty linkeru pak --libs. V obou případech je nutno přepínač doplnit o jméno hledané knihovny.

    In [40]:
    cd prednasky
    pushd priklady/blok2_pkgconfig > /dev/null
    cat Makefile
    make
    echo
    ./uuid_demo
    popd > /dev/null
    
    #!/usr/bin/make -f
    
    CC = gcc
    CFLAGS = -g -O0 $(shell pkg-config --cflags uuid)
    LDLIBS = $(shell pkg-config --libs uuid)
    
    all: uuid_demo
    
    uuid_demo: uuid_demo.o
    
    .PHONY: clean
    
    clean:
      rm -rf uuid_demo uuid_demo.o
    
    gcc -g -O0 -I/usr/include/uuid   -c -o uuid_demo.o uuid_demo.c
    gcc   uuid_demo.o  -luuid -o uuid_demo
    
    A random universally-unique identifier: 36328c16-349c-42f5-83f5-000b6825714a
    See https://en.wikipedia.org/wiki/Universally_unique_identifier for details
    

    Konfigurátory - Autotools

    Ne vždy ale chceme na nějaké knihovně záviset povinně. Krásným příkladem takové volitelné, platformně závislé vazby jsou multimediální API. Zatím co na Windows je DirectShow, na Linuxu je video4linux. Ač make jako takový umožňuje do jisté míry větvení dle hodnot proměnných, zdaleka se flexibilitou nevyrovná shellovému skriptu build.sh. Nabízí se tedy řešení, skriptem nesestavovat přímo kýžený produkt, ale vygenerovat Makefile na míru.

    Takový skript se zpravidla jmenuje configure.

    Vracíme se s ním ale zpět do situace, kdy jsme museli řešit co na dané platformě máme k dispozici. A právě z tohoto důvodu se objevily snahy nějakým způsobem tyto skripty unifikovat, psát univerzálně. Zástupcem těchto snah mohou být GNU Autotools. Samotný configure se tak ale stává... předmětem výstupu dalšího generátoru. Představuji vám tedy configure.ac a autoconf.

    Soubor configure.ac je psán v makrojazyce M4 a platformně závislý kód tak generují makra. Díky tomu je podivnou směsicí skriptů shellu a fragmentů maker, což umožňuje snadno a rychle implementovat i kontroly, které ještě nejsou součástí nástroje jako takového. Soubor se sestává z logických sekcí:

    • Inicializace autoconfu - makro AC_INIT, vyžaduje jméno projektu ve strojově zpracovatelné formě, verzi, volitelně pak emailovou adresu pro reportování bugů a konečně plné jméno projektu.
    • Sekci testů - tyto nastavují různá makra (C) a proměnné ve skriptu, které mohou být propagovány do Makefile a jiných vygenerovaných souborů.
    • Substituce proměnných - určují, které proměnné mají být uloženy do vygenerovaného Makefile.
    • Výstupy - soupisku konfiguračních hlaviček a vygenerovaných souborů.

    Configure, tak jak jej pojímá autoconf, je nicméně založen na substitucích. Než tedy vygeneruje Makefile, případně i další soubory, jako demo.pc (konfigurační soubor pro pkg-config), musí dostat šablonu ve které má substituce provést. V případě Autotools jsou jí soubory stejného názvu jako cílové, jen s dodatečnou příponou .in. Než se tedy pustíme do ilustrace configure.ac, pojďme se podívat na takový Makefile.in, odpovídající předchozí úloze.

    In [41]:
    cat priklady/blok2_autoconf/Makefile.in
    
    #!/usr/bin/make -f
    CC = @CC@
    CFLAGS = @CFLAGS@
    LDLIBS = @LDLIBS@
    
    # promenna OBJS je z hlediska dema nezajimava
    OBJS=uuid_demo uuid_demo.o
    
    all: $(OBJS)
    
    $(OBJS): config.h
    
    uuid_demo: uuid_demo.o
    
    .PHONY: clean
    
    clean:
      rm $(OBJS)
    

    V souboru se něco málo přece jen změnilo - kompilátor, jeho přepínače a přepínače linkeru teď závisí na konstrukci @PROMENNA@, kterou nahradí autoconfem vytvořený configure za cílový obsah. Vedle této změny se objevila ale ještě jedna - všechny sestavované soubory začaly záviset na souboru config.h, jehož generování má autoconf rovněž v gesci. Tuto šablonu ale nemusí dodat autor projektu, autoconf ji vytvoří sám v momentě, kdy generuje configure z configure.ac.

    In [42]:
    cat priklady/blok2_autoconf/config.h.in
    
    /* config.h.in.  Generated from configure.ac by autoheader.  */
    
    /* Compile with uuid support */
    #undef HAVE_UUID_H
    
    /* Define to the address where bug reports for this package should be sent. */
    #undef PACKAGE_BUGREPORT
    
    /* Define to the full name of this package. */
    #undef PACKAGE_NAME
    
    /* Define to the full name and version of this package. */
    #undef PACKAGE_STRING
    
    /* Define to the one symbol short name of this package. */
    #undef PACKAGE_TARNAME
    
    /* Define to the home page for this package. */
    #undef PACKAGE_URL
    
    /* Define to the version of this package. */
    #undef PACKAGE_VERSION
    
    In [43]:
    cat priklady/blok2_autoconf/configure.ac
    
    AC_INIT([blok2-autoconf], [0.1], [xrucka@fi.muni.cz])
    
    AC_PROG_CC
    PKG_PROG_PKG_CONFIG
    
    uuid=auto
    
    AC_ARG_ENABLE([uuid],
      [  --enable-uuid    Enable support for libuuid],
      uuid=${enableval}
    )
    
    if test "no" != "$uuid" ; then
      PKG_CHECK_MODULES([UUID], [uuid], [
        CFLAGS+=" ${UUID_CFLAGS}"
        LDFLAGS+=" ${UUID_LIBS}"
        AC_DEFINE(HAVE_UUID_H, 1, [Compile with uuid support])
      ], [
        AC_MSG_ERROR("uuid was not found")
      ])
    fi
    
    AC_CONFIG_HEADER(config.h)
    AC_CONFIG_FILES([Makefile])
    AC_OUTPUT
    

    Projděme si tedy ukázku, i když poněkud na přeskáčku:

    • AC_INIT - aneb inicializace. Ta již byla popsána výše.
    • AC_CONFIG_HEADER - M4 makro značící výstupní jméno pro soubor s definicemi
    • AC_CONFIG_FILES - M4 makro pro soubory s obecnými substitucemi - například Makefile.
    • AC_OUTPUT - Koncový příkaz celého configure, způsobí nahrazení proměnných.

    Co ale dělá takové AC_ARG_ENABLE? Jde o volitelné závislosti. Typicky máme 3 stavy - explicitní on, explicitní off - a výchozí. Toto makro má za úkol provést v případě přítomnosti argumentu --enable- provést kód ve svém 3. argumentu. Ten v našem případě obnáší nastavení proměnné uuid. Na toto pak reaguje shellový if o pár řádků níže, který v případě, že nebyla feature uuid zakázána zkusí najít její závislosti pomocí pkg-configu, resp. jeho zastřešujícího M4 makra.

    Podobně jako v předešlém příkladu s pkg-config v Makefile, i zde vyvstanou přepínače pro překladač a linker. Tyto jsou přidány k existujícím a následně substituovány do Makefile. Pokud by závislost na uuid uspokojena nebyla, provede se 4. větev makra PKG_CHECK_MODULES (její důsledek je ponechán k odpozorování čtenáři).

    Soubor configure ale ještě musíme vygenerovat. O to se stará kombinace příkazů autoheader (generuje config.h.in) a autoreconf (generuje configure).

    In [44]:
    pushd priklady/blok2_autoconf > /dev/null
    autoheader
    autoreconf
    ./configure
    popd
    
    checking for gcc... gcc
    checking whether the C compiler works... yes
    checking for C compiler default output file name... a.out
    checking for suffix of executables...
    checking whether we are cross compiling... no
    checking for suffix of object files... o
    checking whether we are using the GNU C compiler... yes
    checking whether gcc accepts -g... yes
    checking for gcc option to accept ISO C89... none needed
    checking for pkg-config... /usr/bin/pkg-config
    checking pkg-config is at least version 0.9.0... yes
    checking for uuid... yes
    configure: creating ./config.status
    config.status: creating Makefile
    config.status: creating config.h
    ~/pb176_workdir/prednasky
    

    Krátká rekapitulace:

    • Začali jsme s ručně psaným build skriptem.
    • Následně jsme jej nahradili ručně psaným Makefile.
    • Nyní máme ručně psaný Makefile.in a ručně psaný configure.ac, ze kterého se vygeneruje configure, který teprve vygeneruje Makefile.
    • To nezní jako zjednodušení, protože místo toho aby se věci děly samy, nadále musíme psát spoustu režijního kódu, navíc v různých jazycích.

    V tento okamžik nastupuje automake, který celý Makefile.in redukuje na Makefile.am. Cílem automake je vygenerovat Makefile, se správnými závislostmi mezi cíli tak, abychom nejlépe jen napsali které programy chceme sestavit ze kterých zdrojových souborů. Syntaxe i možnosti automake jsou výrazně větší než pokrytí v tomto předmětu, nebývá tak než pro zvědavost odkázat předmět PV065.

    Automake nicméně vyžaduje zavést i do configure.ac, naštěstí jde ale o jediný řádek bezprostředně po AC_INIT.

    In [45]:
    cat priklady/blok2_automake/Makefile.am
    echo
    diff -rupN priklady/blok2_auto{conf,make}/configure.ac
    echo
    
    bin_PROGRAMS=uuid_demo
    uuid_demo_SOURCES=uuid_demo.c
    
    
    --- priklady/blok2_autoconf/configure.ac  2022-01-30 18:33:16.837614036 +0000
    +++ priklady/blok2_automake/configure.ac  2022-01-30 18:33:16.838614037 +0000
    @@ -1,4 +1,6 @@
    -AC_INIT([blok2-autoconf], [0.1], [xrucka@fi.muni.cz])
    +AC_INIT([blok2-automake], [0.1], [xrucka@fi.muni.cz])
    +
    +AM_INIT_AUTOMAKE([-Wall -Werror])
    
     AC_PROG_CC
     PKG_PROG_PKG_CONFIG
    
    

    Automake nicméně vyžaduje ještě několik dodatečných souborů s dokumentací projektu. Jde o:

    • AUTHORS - soubor shrnující autory projektu.
    • README - typicky popis projektu, následovaný instalačními instrukcemi. Tyto nicméně mohou být i v pomocném souboru INSTALL.
    • NEWS a ChangeLog - News jsou shrnující doprovodný popis nového vydání projektu (nové release). ChangeLog oproti tomu bývá podrobnější a techničtější popis, typicky může obnášet soupisku opravených chyb.

    Protože ale automake vyžaduje dodatečné příkazy, typicky narazíte i na skript autogen.sh, jehož jediným smyslem je zavolat příkazy automake a autoreconf (s doprovodnými příkazy) ve správném pořadí a delegovat své argumenty na nově vzniklý configure.

    In [46]:
    cat priklady/blok2_automake/autogen.sh
    
    #!/bin/sh
    set -e
    
    aclocal
    automake --add-missing -c
    autoreconf -i
    
    ./configure "$@"
    
    set +e
    

    Makefile vygenerovaný automake přidal do našich pravidel ještě několik dalších cílů. Jedním z nich je i cíl install, který je odpovědný za instalaci do cílového umístění v systému. Ne vždy ale chceme nutně instalovat přímo do prostředí ve kterém proběhlo sestavení. Proto je s tímto spojena i proměnná DESTDIR, která udává cílový adresář do kterého instalace proběhne.

    Nadále ale bude projekt odkazovat (potenciálně absolutní) cesty dle původního prostředí sestavení. K úpravě cest slouží přepínač configure --prefix=cesta, který se stará o nastavení pozic do podadresářů v cílové instalaci. Pokud tedy bude sestavený program pojmenovaný uuid_demo, bude jeho lokace v cílovém systému /usr/bin/uuid_demo (přičemž cesta /usr je prefix). Pokud bychom jej tedy chtěli instalovat přímo v lokálním systému do systémových cest, můžeme provést make install DESTDIR=/.

    Konfigurátory - CMake

    Autotools jsou nicméně velmi silně spjaty s komunitou okolo GNU. Toto je konec konců patrné i na přístupu ke shellu, skriptům a dokumentaci vyžadované automake. Jednou z alternativ, dále rozvíjejících myšlenku "popis, nikoliv instrukce", je CMake.

    CMake zakládá popis závislostí a součástí do jediného textového souboru, CMakeLists.txt, přičemž (sic to není vyžadováno), odděluje adresář se zdrojovými kódy od adresáře ve kterém probíhá sestavení (build, obecně builddir). V tomto adresáři si CMake vygeneruje podobnou adresářovou strukturu jako má původní projekt, přičemž jejímu kořenu vládne textová cache proměnných. Do této si CMake poznačuje jednak volby zadané při prvním průchodu konfigurací, druhak i automaticky zjištěné hodnoty. Z této cache pak CMake vychází když generuje Makefile v odpovídajících podadresářích. Jinými slovy, CMake explicitně odděluje průzkum systému a prostředí pro které se bude sestavovat od generování Makefile. Tento přístup nabízí některé výhody, mimo jiné i rychlejší reakci na změny v projektu.

    Po stránce vnitřní architektury mají jednotlivé výstupní artefakty vlastní cíle, ke kterým se váží přepínače, nastavení a (typované) proměnné. Tyto je možno buď explicitně poznačit k uložení v cache, nebo je použít jen jako neveřejné. Krom proměnných nastavených přímo v CMakeLists.txt jsou jimi proměnné samotného CMake (např. CMAKE_BUILD_TYPE), nebo také proměnné nastavené jednotlivými balíčky a pomocnými moduly.

    Moduly v CMake

    V řadě situací si bohatě vystačíte s předinstalovanými moduly, případně moduly které CMake vygeneruje ke knihovnám jím samotným sestaveným. Přesto není občas zbytí (zejména pokud křížíte buildsystémy) a nezbývá než si vlastní modul napsat.

    Zatím co rozšíření funkcionality configure stálo na shellových skriptech, resp. dodatečných makrech pro autoconf, je použití CMake v některých ohledech přímočařejší. Doplňkové moduly pro CMake totiž bývají uloženy v souborech tvaru Find<Module>.cmake, přičemž tyto CMake zpracuje v okamžiku, kdy se v CMakeLists.txt objeví řádek tvaru find_package(Module [options]). Samotný CMake je ale svou interní strukturou relativně omezený, přesto však dovoluje spouštět i externí programy. V tomto směru budiž čtenář odkázán na dvě nápovědy na konci této kapitoly a soubor FindPkgConfig.cmake (na Linuxu typicky instalován v /usr/share/cmake/Modules/UsePkgConfig.cmake. Po nestandardních modulech se pak CMake typicky poohlíží v podadresáři projektu cmake, tedy za předpokladu že na tento nasměruje CMakeLists.txt proměnnou CMAKE_MODULE_PATH.

    Úprava v cache

    Proměnné v cache lze upravovat a nastavovat 3 způsoby:

    1. Textovým editorem, tedy pokud jeden ví co dělá
    2. Spuštěním cmake s přepínačem -D: cmake -DCMAKE_C_COMPILER=clang
    3. Gui - pro konzoli je zde ccmake, pro grafické prostředí například cmake-gui

    Toto typicky znamená nutnost přegenerovat (i kdyby jen částečně) cache a v případě ccmake i explicitní pokyn pro generování Makefile. V případě cmake pak může být potřeba spustit dvakrát tentýž příkaz.

    Pozor, změna překladače typicky vede k zahození celé cache, včetně dříve ručně nastavených proměnných.

    Konečně, příklad výše sepsán pomocí CMake vypadal by takto:

    In [47]:
    cat priklady/blok2_cmake/CMakeLists.txt
    
    project(blok2-cmake)
    cmake_minimum_required(VERSION 3.0)
    
    find_package(PkgConfig REQUIRED)
    pkg_check_modules(UUID REQUIRED uuid)
    
    include_directories(${UUID_INCLUDE_DIRS})
    link_directories(${UUID_LIBRARY_DIRS})
    add_executable(uuid_demo uuid_demo.c)
    
    add_definitions(${UUID_CFLAGS_OTHER})
    
    set(HAVE_UUID_H 1)
    
    target_link_libraries(uuid_demo ${UUID_LIBRARIES})
    
    configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.in ${CMAKE_CURRENT_SOURCE_DIR}/config.h)
    
    In [48]:
    pushd priklady/blok2_cmake > /dev/null
    mkdir -p build
    cd build
    cmake ..
    make
    ./uuid_demo
    popd > /dev/null
    
    -- The C compiler identification is GNU 10.2.1
    -- The CXX compiler identification is GNU 10.2.1
    -- Detecting C compiler ABI info
    -- Detecting C compiler ABI info - done
    -- Check for working C compiler: /usr/bin/cc - skipped
    -- Detecting C compile features
    -- Detecting C compile features - done
    -- Detecting CXX compiler ABI info
    -- Detecting CXX compiler ABI info - done
    -- Check for working CXX compiler: /usr/bin/c++ - skipped
    -- Detecting CXX compile features
    -- Detecting CXX compile features - done
    -- Found PkgConfig: /usr/bin/pkg-config (found version "0.29.2")
    -- Checking for module 'uuid'
    --   Found uuid, version 2.36.1
    -- Configuring done
    -- Generating done
    -- Build files have been written to: /root/pb176_workdir/prednasky/priklady/blok2_cmake/build
    Scanning dependencies of target uuid_demo
    [ 50%] Building C object CMakeFiles/uuid_demo.dir/uuid_demo.c.o
    [100%] Linking C executable uuid_demo
    [100%] Built target uuid_demo
    A random universally-unique identifier: d915fede-dc1d-4ec9-9a75-88a1bde9789b
    See https://en.wikipedia.org/wiki/Universally_unique_identifier for details
    

    Podmíněný kód pak můžeme do CMakeLists.txt vložit podobně pohodlným způsobem, jako v případě configure.ac. Varianta příkladu s volitelnou podporou libuuid je přibalena v repozitáři také.

    In [49]:
    cat priklady/blok2_cmake_cond/CMakeLists.txt
    
    project(blok2-cmake-conditional)
    cmake_minimum_required(VERSION 3.0)
    
    add_executable(uuid_demo uuid_demo.c)
    
    set(UUID_ENABLED ON CACHE BOOL "Build with uuid support")
    if (UUID_ENABLED)
      find_package(PkgConfig REQUIRED)
      pkg_check_modules(UUID REQUIRED uuid)
    
      include_directories(${UUID_INCLUDE_DIRS})
      link_directories(${UUID_LIBRARY_DIRS})
    
      add_definitions(${UUID_CFLAGS_OTHER})
    
      set(HAVE_UUID_H 1)
    
      target_link_libraries(uuid_demo ${UUID_LIBRARIES})
    endif()
    
    configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.in ${CMAKE_CURRENT_SOURCE_DIR}/config.h)
    
    In [50]:
    pushd priklady/blok2_cmake_cond > /dev/null
    mkdir -p build
    cd build
    cmake -DUUID_ENABLED=NO ..
    make
    ./uuid_demo
    popd > /dev/null
    
    -- The C compiler identification is GNU 10.2.1
    -- The CXX compiler identification is GNU 10.2.1
    -- Detecting C compiler ABI info
    -- Detecting C compiler ABI info - done
    -- Check for working C compiler: /usr/bin/cc - skipped
    -- Detecting C compile features
    -- Detecting C compile features - done
    -- Detecting CXX compiler ABI info
    -- Detecting CXX compiler ABI info - done
    -- Check for working CXX compiler: /usr/bin/c++ - skipped
    -- Detecting CXX compile features
    -- Detecting CXX compile features - done
    -- Configuring done
    -- Generating done
    -- Build files have been written to: /root/pb176_workdir/prednasky/priklady/blok2_cmake_cond/build
    Scanning dependencies of target uuid_demo
    [ 50%] Building C object CMakeFiles/uuid_demo.dir/uuid_demo.c.o
    [100%] Linking C executable uuid_demo
    [100%] Built target uuid_demo
    Example compiled without libuuid support. Not much to show.
    

    Závislosti II - Repozitáře

    To poslední, stran závislostí, co zatím nezaznělo zní: "Kde se berou?" Doposud jsme totiž vždy měli knihovny instalovány lokálně a buildsystém pouze konfiguroval již přítomné. Současně známe i nástroje, které umí instalovat software z online katalogů (vzpomeňte už jen obyčejný PIP). Takový software je známý jako manažer balíčků, katalogu balíčků pak repozitář. V tradičním modelu, který můžete znát z Linuxových distribucí, je balíčkovací systém oddělený od buildsystému.

    Třetí typ buildsystému tak přebírá nejen roli samotného sestavování, ale i mapování závislostí. Příklady takových buildsystémů jsou npm nebo maven (s tím se můžete blíže seznámit v PB162).

    Blok 3 - Testování a chyby

    Zdroje chyb

    Zamysleme se na chvíli - jak se vlastně do programů dostanou chyby? Zdrojů je více, ale můžeme sestavit orientační soupisku:

    • Přímým vnesením při programování
      • Přepsání se při implementaci
      • Opomenutím nějaké větve kódu
      • Nevhodným návrhem
      • Neznalostí prostředí, portováním
    • Údržbou a refaktoringem
      • Při dělení / slučování funkcí
      • Přejmenováváním proměnných a funkcí (nejlépe doplněno drobnými změnami v API - např. z příležitostného parametru se stane parametr povinným)
    • (Ne)Synchronizací a sdíleným kontextem

    Podobně, jako u leteckých nehod jde ale často o souběh více okolností. Což ovšem nic nemění na tom, že chyby je potřeba řešit. Nebo ještě lépe, jim předcházet. A právě o tom bude tento blok.

    Deterministické vs. nedeterministické chování

    Počítače, tak jak je dnes známe a používáme jsou v principu deterministické stroje. Vykonávají jednotlivé instrukce, operující nad daty v paměti. Přesto se ale setkáváme s nahodile se vyskytujícími chybami. Odkud se tedy může v (jinak) deterministickém systému vzít náhoda?

    Odpovědí je samotná fyzikální podstata, tepelný šum a částice s vysokou energií. Díky tomu může dojít k chybě v jediném bitu, který ale může zcela změnit obsah paměti - uvažte pouhé přehození znaménkového bitu celého čísla.

    Většina (nedeterministických) chyb se kterými se ale v praxi setkáte má daleko prozaičtější zdroj:

    • Chybný přístup do paměti, následkem kterého jsou čteny neinicializované bloky paměti. Tento typ chyb je posílen o to, že konkrétní obsah paměti závisí na dosavadním běhu programu, ale i na technice ochrany proti některým vektorům útoku - randomizaci adresního prostoru. Při této technice jsou části programu (kód, zásobník, halda, statické oddíly, soubory) mapovány v každém běhu programu různě.
    • Synchronizační chyby a nesoulady v časování. Tyto v principu mohou být dvou původů - vnějšího (plánování procesů stran OS, diskové / uživatelské IO, síť, senzory, ...) nebo vnitřního - neošetřené kritické sekce (části programu, které nemohou běžet paralelně), či zápolení o zdroje (race-condition).

    Deterministické chyby jsou oproti tomu relativně nudné, k jejich replikování v laboratorních podmínkách typicky stačí jen znát správné vstupy a prostředí. Samy o sobě nicméně nestačí, pokud by byly překladače jen pouhými přepisovači kódu z vyšších jazyků do nižších, byly by výsledné programy velmi pomalé, s velkou fluktuací koncového výkonu. Jinými slovy, je nutno brát v úvahu i stupeň optimalizace vykonávaného kódu překladačem. Spuštění v debuggeru tak, abychom mohli prozkoumat stavy jednotlivých proměnných může vést k nutnosti optimalizace vypnout a tak nemusí dojít k replikaci chyby. Překladače by nicméně měly i při kompilaci s optimalizacemi zachovávat ekvivalenci operací v optimalizovaném i neoptimalizovaném kódu.

    Nedeterministické chyby zahrnují to vše a k tomu navíc náhodu. A tak se v praxi můžete setkat i s několika neformálními označeními, typicky pojmenovaných po velikánech fyziky.

    • Heisenbug - typ bugu, pojmenovaný po Werneru Heisenbergovi. Je charakterizován tím, že v okamžiku kdy se jej snažíme vystopovat, odladit - ať již instrumentací kódu nebo během ve sledovaném prostředí (debuggeru), zmizí. Bug, jehož existence je ovlivněna samotným pozorováním.
      • Tento zpravidla zmizí buď díky změně rozložení paměti při kompilaci bez optimalizací (více proměnných umístěných do RAM paměti, místo předávání registry); nebo díky zpomalení programu kvůli využití pomalejší kombinace instrukcí.
    • Mandelbug - pojmenovaný ku cti Benoîta Mandelbrota, resp. jeho fraktálu. Je to bug, jehož příčiny jsou komplexní (nejen ve smyslu složité, ale i smyslu reálné a imaginární složky). Díky tomu se chová na pohled chaoticky. Při analýze se objevují další a další bugy. Jeho vyřešení má za následek kaskádový nárůst bugů jinde v projektu.
    • Schrödinbug - bug, jehož vlnová funkce zkolabuje okamžikem jeho objevení při analýze kódu, který kvůli přítomnosti bugu neměl vůbec nikdy fungovat. Přesto fungoval až do okamžiku objevení.
      • Tento je typicky spojen s paměťovou chybou.
    • Bohrbug - typ bugu, pojmenovaný po Nielsi Bohrovi. Je to dobře podmíněný bug, lze pro něj najít (deterministický) postup vyvolání a lze jej ladit. Ve smyslu výše uvedeném je "nudný" až deterministický.

    Díky svému významu tyto dva typy chyb mívají paměťové a synchronizační chyby v řadě případů i specializované debuggery -- memory debugger a concurrency debugger. Jako příklad memory-debuggeru můžete uvažovat (snad již dostatečně) známý Valgrind či cuda-memcheck. Jako příklad concurrency debuggeru pak lze uvažovat např. Valgrind s nástrojem Hellgrind, či již dnes nedostupný Jinx.

    Principiálně jsme nicméně nadále v situaci, kdy se chyba může, ale nemusí projevit. Právě technikám, které se snaží detekovat výskyt chyby za běhu, říkáme dynamická analýza.

    Její protějšek, statická analýza program nespouští. Buduje si postupně síť vzájemných ovlivnění operací a proměnných, kterou porovnává s modelem fungování programovacího jazyka. Samozřejmě, technické možnosti - zejména u systémů navržených k potenciálně nekonečným běhům - jsou poněkud omezené. Přesto však nabízí statická analýza možnosti, jak hledat chyby i v kódu, který se nemusí vykonat nikdy.

    Replikovatelnost

    Pro úspěšné vyřešení bugů je ale klíčová i jiný myšlenka. Tou je replikovatelnost. Řada bugů totiž závisí na konkrétním prostředí, časování, paměťovém rozložení a použitých datech. Pokud by nezávisela, nejspíše by již byla objevena nástroji statické kontroly před testováním a dynamické kontroly při testování.

    Co tedy můžeme udělat (jako uživatelé) proto, aby námi zpozorovanou chybu mohli vývojáři replikovat?

    1. Uvědomit si, jaké vstupy jsme (programu) zadali a co je potřeba udělat za kroky, aby se projevila.
      • Vše sepsat, nejlépe pořídit videonahrávku událostí předcházejících chybě.
      • Zjistit, zda se chyba projeví i na menším vstupu - ať již objemem zpracovávaných dat, či časovém rozestupu.
      • Poskytnout (ekvivalentní) vzorek vstupu (Minimal Complete Verifiable Example).
      • Zaznamenat přesný čas v okamžiku selhání.
      • Popsat očekávaný výsledek, popsat stávající výsledek.
    2. Zdokumentovat prostředí.
      • Jde o standardní instalaci daného OS a software?
      • Komunikuje software po síti? Je čistý log antivirového programu?
      • Vytvořil program před svým selháním nějaké soubory?
    3. Spustit nástroje pro reportování chyb, je-li jimi software vybaven.
      • Vývojáři typicky ví co potřebují z havarovaného procesu.
      • Coredump - soubor obnášející registry CPU spolu s zásobníkem funkčních volání; generovaný při pádu programu.
      • Data může být nutné z legislativních důvodů sebrat zvlášť, nutné vyslovit doplňkový souhlas se zpracováním (legislativa EU).

    Ze strany vývojáře či managementu kvality je pak potřeba mít připraven systém pro reportování chyb. Toto dnes typicky obnáší nějaký bug tracker (např. Bugzilla, mantis, GitHub/GitLab/BitBucket), ve kterém mohou uživatelé zakládat tickety k chybám. Nenechte se ovšem zmýlit, takový systém nemusí mít jen podobu webového formuláře, používány jsou i maillistové fronty (rt), či privátní systémy. Co by tedy měl takový sytém umět?

    1. Udržovat soupisku hlášení chyb (issues, tickety); včetně historie a odkazů na regrese..
    2. Umožňovat propojení jednotlivých hlášení, pokud je podezření na tutéž příčinu.
    3. K jednotlivým issues držet historii.
    4. Spravovat atributy jednotlivých hlášení - štítky, milníky, přiřazení vývojářům; držet informaci o stavu otevřený/nový (new), potvrzený (confirmed), kandidátní záplata (fix candidate), vyřešený (resolved); také ale nereplikovatelný (cannot-reproduce), not-a-bug či wontfix. Nezřídkakdy se používá i feature-request.
    5. Notifikace zúčastněným - typicky formou emailové konference.

    Dále lze učinit opatření, která jdou chybovým hlášením naproti - kupříkladu skript, který v prostředí posbírá relevantní informace a připraví bundle k odeslání a analýze. Nebo šablonu k vyplnění v ticketovacím systému.

    Příklad integrovaného správce repozitářů kódu - GitLab

    GitLab - přehled issues

    GitLab - detail issue

    GitLab - detail merge requestu

    Testy

    Ač je mnohým jistě radostí jen tak něco programovat, spoléhaje se na instinkt a doplňovat program podle toho co by se zrovna hodilo, praxe bývá zpravidla jiná. Někdo (říkejme mu třeba zákazník) má nějakou potřebu, kterou se rozhodne realizovat naším prostřednictvím. Pak ale musíme vědět, co máme dělat; někdo systém navrhne, rozepíše jednotlivé požadavky a nakonec vytvoří funkční specifikaci pro jednotlivé dílčí funkcionality. Testování samo o sobě pak dělíme dle granularity funkcionality a toho, zda máme či nemáme přístup ke zdrojovým kódům:

    Blackbox testování

    Opíráme se (takřka pouze) o specifikaci, do implementace nevidíme. Můžeme ale analyzovat specifikaci, najít okrajové podmínky chování a na základě této analýzy definovat konkrétní testovací vstupy a výstupy, v souladu se specifikací. Pokud tedy specifikace říká, že vstupem z jejího pohledu jsou argumenty funkce, budou jednotlivé dílčí testy (test case) definovány jako hodnoty argumentů funkcí. Pokud specifikace hovoří o souboru na vstupu, bude testovacím vstupem konkrétní obsah souboru. Analogicky pak můžeme hovořit o výstupech.

    Pozor, vstupy nemusíme fixovat jen na konkrétní obsah - pokud má vstup či výstup splňovat konkrétní invarianty, můžeme testovat i tyto. Tohoto se zejména užívá při randomizovaném testování, kde se vstupy generují dle (předpokládaného) pravděpodobnostního rozložení. Takové testování nicméně klade značné nároky na logování, tedy zaznamenání konkrétních (fixních) vstupů a výstupů. Jinak nelze výstup interpretovat jinak, než "možná je to rozbité".

    Velkou výhodou této formy testování je to, že zjišťujeme zda kód opravdu dělá to co má. Nevýhodou pak to, že může dojít k emisi příliš mnoha jinak zbytečně komplikovaných vstupů, testujících tytéž bloky implementace.

    Whitebox testování

    Oproti blackboxovému přístupu zde máme k dispozici zdrojový kód, tedy lze zamířit testy tak, aby došlo k spuštění každého bloku programu. U tohoto typu testů se nám automaticky nabízí vcelku přirozený pojem pokrytí větví (coverage), který udává procento testy vyzkoušených větví. Je ale nutné si uvědomit, že pokud se do zdrojového kódu podíváme, nevyhnutelně jsme jím ovlivněni a snadno se stane, že začneme vstupy uzpůsobovat kódu, vzdalující se specifikaci.

    Druhým významným pojmem spojeným s whitebox testy jsou unit (jednotkové) testy. Tyto se opírají o konkrétní funkce (v zásadě každou, i pomocné, avšak nikoliv nutně) a operují s uměle sestaveným stavem paměti, nad které má být podprogram vykonán, přičemž vzájemné volání funkcí je spíše minimální. Jejich pojmenování pak odkazuje k překladovým jednotkám, které můžete znát jako jednotlivé soubory se zdrojovým kódem. Nad jednotkovými testy jsou pak testy integrační, které testují propojení a spolupráci jednotek (komponenty vnitřně). Integrační testy předpokládají, že jednotlivé dílčí funkce již byly otestovány jednotkovými testy. Výše se pak nachází systémové testy, které - jak již název napovídá - testuje celý systém. A konečně, end-to-end testy, které testují jednotlivé interakční scénáře, včetně propojení s jinými systémy. V praxi se nicméně (nejspíše) potkáte nejčastěji s unit testy.

    Protože oba přístupy mají své výhody i nevýhody, setkáte se v praxi s graybox testováním, které spojuje výhody i nevýhody obou systémů. Máte tak například k dispozici implementaci a využíváte i informaci o pokrytí, ale vstupy definujete hrubozrnné (tedy ne pro každou funkci a proti specifikaci).

    Ilustrace získání pokrytí v C

    Zatím co u interpretovaných jazyků lze získat informaci o pokrytí pouhým provedením testů, pouze přepnutím režimu interpretu, vyžadují kompilované jazyky (nejen tyto) instrumentaci, byť automatizovanou překladačem. Pokud uvážíme toolchain překladače gcc, je dvojicí nástrojů které nám podají smysluplnou informaci dvojice gcov a lcov. Tato ukázka staví nad kódem 4. cvičení předmětu PB071, obohaceným o Makefile pro získání informace o pokrytí.

    GCC nejprve přidá do binárního kódu informace o blocích, větveních, ze kterých vykonávaná část pochází. Tyto jsou doplněny o čítače četností průchodů (tedy proměnná pro každou hranu mezi větveními). Při spuštění takto upraveného programu je pak zapsán soubor, obsahující tyto pomocné proměnné (resp. aktualizován o aktuální běh). Tuto informaci je nutno dále zpracovat.

    In [51]:
    pushd priklady/blok3_code_pb071_sem4 > /dev/null
    cat Makefile
    
    printf "\n\ncommence make\n"
    make
    
    popd > /dev/null
    
    #!/usr/bin/make -f
    
    CFLAGS = -g -O0  -fprofile-arcs -ftest-coverage
    CC = gcc
    
    
    all: matrix tictac
    
    .PHONY: coverage-gcov coverage-lcov
    
    coverage-gcov: matrix
      ./matrix
      gcov -a -b matrix.c
    
    coverage-lcov: coverage-gcov
      lcov -c --directory . --output-file matrix.info
      genhtml matrix.info --output-directory out
    
    .PHONY: clean
    
    clean:
      rm -rf matrix tictac *.gcno *.gcna *.gcda *.html *.gcov *.info out
    
    
    commence make
    make: *** No rule to make target 'matrix', needed by 'all'.  Stop.
    

    Samotný gcov vypisuje pouze souhrn informace o navštívených větvích, který nemusí být přehledný. Proto je za něj zařazen lcov, který načte data gcovu a vizualizuje je formou statické webové stránky. Pro usnadnění je tedy vhodné utility kombinovat.

    Regresní testování

    Vraťme se na chvíli k hlášení chyb. Uvažme situaci, kdy byl ve vašem produktu objeven bug. Tento se vám povede zreplikovat, z bugu se tak stane potvrzený bug. Vaším cílem - zcela přirozeně - je tento opravit. Avšak pokud půjde o nějaký komplexnější problém, může se snadno stát že se časem objeví znovu, postupně navrácen na první pohled nesouvisejícími změnami. Takový stav je znám jako regrese (bugu); stav, který jen velmi neradi vidíte.

    Regresní testování je tedy technika, kdy se proaktivně bráníte regresi tím, že pro každou (velkou) změnu vytvoříte test, jehož cílem je detekovat situaci kdy je rozbito něco, co (dříve) fungovalo. U velmi striktního výkladu tak jde o testy proti každému v minulosti učiněnému commitu.

    V praxi je nicméně regresní testování vyskytuje jako součást řízení chyb, tedy regrese bugů.

    Z historie jsou známy případy, kdy jinak vhodná praxe byla zničena, ať už technickým či manažerským rozhodnutím. Není pro ně nutno chodit daleko. Kupříkladu v roce 2012 se firma Oracle rozhodla zrušit framework mysql-test, který byl de facto databází regresních testů k databázi MySQL (tuto Oracle získal jako vedlejší projekt při akvizici společnosti Sun).

    Názvosloví

    Pokud se nicméně budeme držet slovníku odborné společnosti, resp. její starší verze můžeme se opřít o pojmy:

    • Test suite - množina testovacích případů, kde výstupní podmínky funkcionality jednoho testovacího případu jsou současně předpoklady dalšího, navazujícího, testovacího případu.
    • Test case (testovací případ) - zafixovaný vstup, zafixovaný výstup, jejich invarianty, prostředí a počáteční stav (specifikace až na úroveň jednotlivých proměnných), invarianty stavů.
    • Master test - testovací plán, jehož úkolem je aktivovat jiné plány, typicky různých úrovní nebo typů.
    • Test log - záznam o běhu testů, typicky chronologicky řazený.
    • Assert, check - kontrolní pravidla, pokud jsou porušena - test selhal. Ve slovníku je najdete jako kritéria.

    Testovací frameworky

    Testovacích frameworků je pochopitelně celá řada, dají se ale rozdělit do několika rovin, uvažme dvě základní:

    1. Jazyky s makry - makra jsou zde využita typicky pro vygenerování anotace jednotlivých test case, případně zaobalení kódu testu do pomocných funkcí. U objektově orientovaných jazyků jde typicky o zabalení testu do nějakého objektu, u jazyků procedurálních se vygeneruje odpovídající procedura. Příkladem tohoto přístupu budiž například GoogleTest, nebo CUT.
    2. Jazyky s anotacemi - testy jsou buď přímo vyznačeny anotacemi, nebo je anotacemi upraveno jejich vykonávání. Framework pak čte anotace a spouští odpovídající definice testů. Příkladem tohoto přístupu je Python - unittest, nebo JUnit.

    Často frameworky zavádí i vlastní pojmy a doplňující názvosloví, i dle zvyklostí konkrétních jazyků - např. fixture - obslužné rutiny vykonávající přípravu počátečního stavu (setup) a úklid po testu (teardown). Mimo samotnou organizaci spouštění jednotlivých testů je u dobrého testovacího programu žádoucí ještě jedna vlastnost - izolace jednotlivých testů. U jazyků s přímou možností ovlivnit obsah celé paměti (C/C++ a obecně jazyky s ukazatelovou aritmetikou) jde zejména o to, aby jeden test nerozbil ostatní, i když sám projde (pokud program obsahuje paměťovou chybu, je výsledek nedefinovaný - včetně předstírání že vše funguje). Obecně pak jde o kompenzaci efektů hystereze (jev, kdy k návratu do původního stavu nestačí pouze udělat všechny kroky v obráceném pořadí - například kvůli vnitřním stavům a testům skrytým proměnným). Toto typicky znamená spouštění testů v samostatných procesech.

    Odborná tělesa a profesní komory

    Testování software vzniklo jako snaha minimalizovat škody, způsobené nekvalitním dílem. A podobně jako v jiných oborech, i zde se časem objevila snaha garantovat profesní kvality testerů - tedy vzniku různých profesních komor.

    V případě testerů se můžeme bavit o International Software Testing Quality Board - mezinárodní těleso, které se stará mimo jiné o certifikaci testerů. Právě požadavky na certifikáty této organizace tvoří často část popisků pracovních míst pro QA (quality assurance) pozice.

    Dále (doplněno po přednášce M. Drengubiaka) se můžete v Brně setkat s:

    • Testcrunch - konference cílená na odbornou veřejnost i začátečníky, zařazená mezi konference ISTQB.
    • CaSTB - Česko-slovenská asociace testerů, člen ISTQB.
    • yes4q - Konzultační a poradenský servis, mentoring testerů.

    Rady k testování závěrem

    U funkcí (i těch testovaných) se můžeme bavit o více různých vlastnostech. Za poznámku stojí zejména:

    • To, zda jde o čisté funkce - tedy funkce, jejichž výsledek je závislý jen a pouze na explicitně předaných parametrech, a které nemají žádné vedlejší efekty (tedy ani nic nevypisují, nečtou ze souboru, aj.). Čisté funkce jsou totiž za všech okolností striktně deterministické. A tedy se i dobře a snadno testují.
    • To, zda je funkce reentrantní - tedy zda je možné tutéž funkci v zavolat na více místech souběžně Což, mimo jiné, znamená, že funkce nemůže přepisovat globální proměnné a stavy. Tyto je nutné realizovat externím stavem, kontextem (v C typicky předán ukazatelem, v Javě kontextový objekt).
    • Držet se unixové filozofie - dělat jen jednu věc, ale zato zatraceně pořádně (tedy vhodná dekompozice, již od začátku navržená pro snadné testování).
    • Pravidla, že neexistuje bezchybný kód, nýbrž jen kód nedostatečně otestovaný.
    • Pořizujte s chybovými hlášeními i vzorky dat, která mohou být použita k replikaci. Jinak nepochodíte, a to i přes snahu vývojářů a kvalitářů váš problém vyřešit.
    • Poslouchejte svého kvalitáře.
    • Programujte defenzivně, držte se pravidla "Vždycky pište kód, jako by další osoba co ho po vás bude číst byla násilnický psychopat, co ví kde bydlíte."

    Blok 4 - Distribuce, kontejnery, průběžné začleňování a nasazování

    Tento blok, ač se možná nezná, bude hodně o Dockeru. Protože věci zjednodušuje. Tedy, pokud s ním umíte.

    Killswitch

    Začneme ovšem poněkud z jiného soudku - místo toho abychom diskutovali jak zařídit aby aplikace fungovala se budeme bavit o tom, jak zařídit aby nefungovala. Řeč je o tom, co má ve slangové češtině označení k**vítko, odborněji killswitch.

    Killswitch je prostředek, jak zajistit že od určitého okamžiku nebude daná věc fungovat. Řada těchto prostředků byla implementována s cílem aplikaci či zařízení vyřadit z provozu, ať už z důvodu generační obměny, nebo jako pojistka proti neoprávněnému užití. Nenechte se nicméně zmást, killswitch není nutně jen software, má své hardwarové ekvivalenty, i v podobě součástek, které časem odejdou.

    Krásným příkladem z oblasti hardware je elektrolytický kondenzátor. Časem (a ano, nevhodným umístěním - např. poblíž zdroje tepla - lze toto urychlit) dochází k vysychání elektrolytu v kondenzátoru, až tento jednoho dne přestane fungovat. Funkce zařízení je pak buď významně (kvalitativně) omezena, nebo selže zcela. Nemusí přitom jít vůbec o úmysl, některé součástky mají jednoduše řečeno životnost danou svým principem a na místě potřeba jsou.

    Jindy jde o důsledek dopadu protipirátské ochrany. Pokud byste dnes chtěli aktivovat čerstvou instalaci Windows XP v prostředí bez sítě, budete mít problém. Telefonní čísla, uvedená v pokynech k aktivaci, již nejsou provozována. I "neomezená" licence na software nemusí stačit, pokud jsou aktivační servery výrobce vypnuty či jejich DNS záznamy zrušeny.

    Pro úplnost, využití killswitche může být i pro obecně prospěšnou věc, nikoliv jen vynucování ekonomických záměrů výrobce. Takřka každý větší obchod s aplikacemi obsahuje kód pro vzdálené vyřazení aplikace, která byla auditem označena jako škodlivá. I toto je forma killswitche.

    Killswitch ale nemusí být spojen ani s ochranou práv dodavatele. Může jím být i jinak neškodný mechanismus, který má naopak chránit uživatele. Součástí předinstalovaných distribucí Androidu bývá framework Google Play Services (balík gms), jehož součásti jsou klíčové pro běh aplikací Google na telefonu. Tento balíček je z výkonnostních důvodů kompilován nativně - běží tedy přímo na CPU, nikoliv v rámci Dalvik VM (de facto Java Virtual Machine). Součástí balíčku Play Store je pak služba, která na pozadí tento balíček aktualizuje, s možností vynucené aktualizace kvůli bezpečnostním záplatám. Jinými slovy, Play Store se stará o to, abyste měli ozáplatovaný runtime od Google.

    Přesto se může tento subsystém stát killswitchem. Když se Google rozhodl vyřadit podporu pro Armv6 z Androidu (2016), došlo u některých balíků (v závislosti na nastavení buildsystému) ke změně verze v manifestu. Místo přechodu armeabi (Armv6) a armeabi-v7l (Armv7) došlo k produkci armeabi (Armv7) a armeabi-v7l (Armv7). Jedním z ovlivněných balíků byly i právě Google Play Services. A ty začaly aktualizovat z gms-9.2.56 na gms-9.4.52. Pokud by bylo možno aktualizaci zakázat, měl by problém snadné řešení. Takto byl ale důsledek (v kombinaci s automatickým restartem spadnuvších služeb) jiný - telefony přestaly přecházet do režimu spánku a naopak se začaly přehřívat a vybíjet baterie. Snad nezamýšlený, přesto killswitch.

    To s jakou motivací kdo killswitch vytvoří - nechám na něm. K vám mám ale jeden apel - těm nezáměrným se vyhýbejte, a u těch záměrných se postarejte o to, aby - až přijde čas - nebyly překážkou. Uložte si obrazy aktivačních serverů, místo IP adres se opírejte o doménová jména. Používejte tlačítka živosti, která - v případě že už tak nebudete sto sami učinit - alespoň uvolní vaše dílo světu.

    Historický vývoj hardware a software

    Pokud se podíváme na historii výpočetní techniky optikou 20. století, jsme hodně vázáni jednotlivými platformami. Zejména zúčastněným hardware. Vývoj programovacích jazyků, od strojového kódu přes symbolické adresy, knihovny a extrakci funkcionality do operačních systémů a dnešních vysokoúrovňových jazyků - to vše šlo cestou pryč od hardware. Cestou zvyšování abstrakce, oddělení hardware a software. Toto umožnilo zkrátit vývojový cyklus a obecně jej zlevnit - přece jen, snáze se upravuje software, než hardware.

    Tohoto faktu si nicméně byli vědomi i výrobci hardware, a tak se začaly objevovat abstrakce nejen v software, ale i v doméně ryze fyzické - hardware. Vyčlenění operací s plovoucí řadovou čárkou do koprocesoru byl jen první krok. Brzy přišly další. Konec konců, díky tomu že se CISC systémy inspirovaly růstem RISC se objevil mikrokód (o kterém jste s trochou štěstí již slyšeli od doc. Brandejse). Tedy, instrukční sada procesoru se stala do značné míry virtualizovanou.

    Proč se ale zastavovat? Proč nevyužít například myšlenku FPGA k tomu, aby si procesor nakonfiguroval architekturu vhodnou pro konkrétní úlohu? Ač toto může znít nezkušeným jako hudba budoucnosti, faktem je že tyto technologie jsou přítomny již dnes, byť stále limitovány cenou. A kdo tomu nevěří, ten ať raději nehledá pojmy jako "Management Engine" či "Active Management Technology".

    Zkrátka a jednoduše, dnes již nelze tak snadno nakreslit čáru mezi hardware a software.

    Kontejnery

    Pokud svolíme z požadavku, aby naše CPU bylo fyzické, dostáváme se do oblasti virtualizace či emulace. Inu, pro pořádek:

    • Emulace CPU je vytvoření softwarové varianty existujícího (ne nutně) fyzického CPU. Typicky jiné architektury či časování, než je CPU skutečné. Vykonání emulovaného programu je pak reprezentováno změnami ve stavovém automatu emulovaného CPU. Velkou nevýhodou je významné zpomalení.
    • Virtualizace CPU je vytvoření softwarové varianty částí existujícího CPU tak, aby bylo možno v maximální možné míře využívat fyzické CPU přímo pro koncovou aplikaci. To na jednu stranu přináší výkonnostní zlepšení, na stranu druhou stále ještě netriviální výkon okupovaný virtualizovanými částmi.
    • Kontejnerizace je technika, která se obejde bez virtualizace, avšak nadále izoluje koncovou aplikaci od zbytku systému hostitele. A to za vydatné pomoci jádra operačního systému.

    Jmenné prostory

    V případě jádra OS Linux to znamená, že jsou klíčové prvky, klíčové subsystémy, rozděleny a (za běhu) duplikovány. Aby se zabránilo konfliktům mezi duplikáty, jsou tyto odděleny. Tomuto konceptu říkáme jmenné prostory. Jejich účelem není nic jiného, než izolovat instance abstrakcí zdrojů.

    Mezi základní jmenné prostory Linuxového jádra patří:

    • Identifikátory procesů
    • Síťový subsystém
    • IPC (a v důsledku RAM)
    • Uživatelé
    • Přípojné body a svazky

    Spuštěný proces z těchto prostorů získává zdroje, které se k němu váží. Ač lze (relativně) snadno spravovat mapování ručně pomocí virtuálního souborového systému sysfs, je přece jen pohodlnější spravovat kontejnery nějakým nástrojem, kterému stačí říci co chceme, nikoliv nutně jak to má zařídit.

    Docker

    V současnosti nejspíše nejrozšířenější technologií pro správu kontejnerů je Docker. Tento se dočkal rozšíření i na platformy OSX a Windows. Zatím co na OSX je nutný mezilehlý virtuální stroj, a tak nelze připojovat svazky z hostitele bez komplikovaných hacků, ve Windows je nyní v aktuálních verzích již nativní podpora. Nás ale bude zajímat primárně domovský systém Dockeru - Linux, sice pro dostupnost plného portfolia toho, co Docker umí.

    Architektonicky se Docker sestává z následujících částí:

    1. Démon (služba) dockerd, jehož rolí je vytvářet samotné kontejnery a umisťovat je do relevantních prostorů, přidělovat jim počáteční zdroje.
      • S démonem se pojí i proces docker-proxy, jehož úlohou je vystavit konkrétní síťové porty (tcp/udp) na hostiteli dovnitř kontejneru.
      • Z pohledu kontejneru je příchozí provoz NATován.
    2. REST API, kterým s dockerd komunikují nástroje (příkazové řádky). Tyto jsou dostupné pod příkazem docker nástroj [argumenty], podobně jako to již znáte z gitu.
    3. Registry - služby, vystavující přes HTTP(S) (diskové) obrazy ke spuštění.

    Kontejnery vznikají tak, že se vybalí do podadresáře v cestách dockeru postupně jednotlivé vrstvy obrazu kontejneru. Tyto jsou tvořeny archivy se změnami v souborovém systému oproti předchozí vrstvě, doplněné popisným manifestem. Obraz je pak právě kolekcí vrstev doplněnou o metadata. Vytvoření obrazu je řízeno konfiguračním souborem - Dockerfile. Z konfigurace se pak obraz sestaví příkazem docker build -t smysluplne_jmeno .

    In [52]:
    pushd priklady/blok4_docker_01_simple > /dev/null
    cat Dockerfile
    printf "\n\nA nyní build\n"
    docker build -t pb176-blok4-docker-01-simple .
    popd > /dev/null
    
    FROM debian:buster
    
    RUN printf "%s\n%s\n" \
      "deb http://ftp.linux.cz/pub/linux/debian buster main contrib non-free" \
      "deb http://ftp.linux.cz/pub/linux/debian buster-updates main contrib non-free" \
      > /etc/apt/sources.list
    
    RUN apt-get update \
      && apt-get install -y openssh-client git jupyter-notebook \
      && apt-get clean
    
    RUN pip3 install bash_kernel && python3 -m bash_kernel.install
    
    EXPOSE 8888/tcp
    
    ENTRYPOINT jupyter-notebook --no-browser --ip=0.0.0.0 --allow-root
    
    
    A nyní build
    bash: docker: command not found
    

    Přepínač -t udává tag, označení které nám napomáhá v orientaci mezi jednotlivými obrazy. Jako identifikátory totiž docker běžně používá hashe právě obrazů, resp. konfigurací kontejnerů. Tag je ve formátu název[:verze], přičemž pokud není uvedena konkrétní verze, je automaticky uvažována verze latest. Jeho konkrétní popis získáte příkazem man docker-image-tag.

    Užitá konfigurace obnáší tyto direktivy:

    • FROM - bázový obraz, na který navazujeme. Tento může být dán jak hashem, tak - a to je obvyklejší - právě tagem (např. debian:buster, nebo také debian:latest). Pokud nenavazujeme na žádný obraz, je nutné uvést speciální hodnotu scratch.
    • RUN - uvádí soupisku příkazů potřebných pro sestavení kontejneru (například balíčky k instalaci, kompilace aj.). Direktiva se může opakovat, podobně jako u direktiv ADD a COPY platí, že co direktiva, to pomocná vrstva.
    • EXPOSE - obnáší porty, které má kontejner publikovat (tedy vystavit vně). Nejedná se ale o něco k čemu by došlo automaticky, jde spíše jen pro nápovědu které porty jsou určeny k vystavení.
    • ENTRYPOINT - Přímo spustitelný obraz má pak dán program ČI skript který má být spuštěn jako vstupní bod kontejneru. Tomuto je současně přiděleno PID 1, v běžném OS vyhrazené pro init (tedy kořenový proces OS). Pokud kontejner spustíme - například příkazem docker container run --rm -i -t tag argument1 argument2, jsou argument1 a argument2 předány entrypointu.

    Zatím co docker build obrazy vytváří, docker run je spouští (pro úplnost, totéž - jen trochu jinak - dělá i příkaz docker container start). Podobně jako u gitu, i zde získáte nápovědu příkazem man docker-příkaz-podpříkaz.

    Co ale znamenají jednotlivé argumenty runu?

    • -i - kontejner bude mít připojen stdin, stdout a stderr.
    • -t - kontejneru bude přidělena tty (virtuální terminál).
    • --rm - ukončením entrypointu dojde ke smazání kontejneru a jeho dat. Opětovné zavolání příkazu docker run povede k vytvoření nového kontejneru, nezávislého na historii právě zaniknuvšího kontejneru.

    Ve specifikaci nám ale schází jakákoliv zmínka o uživateli, znamená to snad že proces v kontejneru běží pod identitou uživatele, který run zavolal? Nikoliv, pamatujte - jde o jiný jmenný prostor. Běží pod superuživatelem (byť s některými omezeními). To se ale dá snadno změnit.

    In [53]:
    pushd priklady/blok4_docker_02_user > /dev/null
    cat Dockerfile
    printf "\n\nA nyní build\n"
    docker build -t pb176-blok4-docker-02-user .
    popd > /dev/null
    
    FROM debian:buster
    
    RUN printf "%s\n%s\n" \
      "deb http://ftp.linux.cz/pub/linux/debian buster main contrib non-free" \
      "deb http://ftp.linux.cz/pub/linux/debian buster-updates main contrib non-free" \
      > /etc/apt/sources.list
    
    RUN apt-get update \
      && apt-get install -y openssh-client git \
         jupyter-notebook \
      && apt-get clean
    
    RUN useradd -u 1001 -d /repos -s /bin/bash pb176
    
    ### DANGEROUS SECTION - push-in your ssh keys to the ssh-private directory, build
    ### the image, save it with docker image save -o image.tar and inspect the contents
    ### of the layers.
    COPY ssh-private /repos/.ssh
    RUN chown -R 1001 /repos
    
    USER pb176
    WORKDIR /repos
    
    RUN    git clone git@gitlab.fi.muni.cz:pb176/2021-prednasky.git prednasky \
        && git clone git@gitlab.fi.muni.cz:pb176/2021-priklady.git prednasky/priklady
    
    RUN rm -rf /repos/.ssh
    RUN pip3 install bash_kernel && python3 -m bash_kernel.install
    
    EXPOSE 8888/tcp
    ENTRYPOINT jupyter-notebook --no-browser --ip=0.0.0.0
    
    
    A nyní build
    bash: docker: command not found
    

    Oproti předchozímu příkladu je zde zavedeno hned několik direktiv navíc. Tentokrát je okomentujme od konce:

    • WORKDIR - prosté nastavení pracovního adresáře, ve kterém bude entrypoint spuštěn. Tento musí v obrazu existovat.
    • USER - nastavuje uživatele (a potažmo skupinu), identitu pod kterou entrypoint a příkazy poběží. Jak WORKDIR, tak USER mohou být v Dockerfile uvedeny opakovaně, s platností počínaje místem uvedení dále.
    • RUN chown, RUN useradd - správa uživatelů a práv uvnitř kontejneru. Tímto způsobem je i vytvořen uživatel, který bude později uveden v USER.
    • COPY - touto direktivou kopírujeme dovnitř kontejneru soubory, které se tak stávají součástí aktuální vrstvy.

    Tento příkaz ale není bezpečný, jakmile se totiž něco jednou stane součástí vrstvy, už jí navždy zůstane - a tedy i nakopírovaný privátní klíč. Tomu se nelze vyhnout, ale lze to moderovat. Pro začátek můžeme uvážit další z vlastností dockeru - buildargs. Argumenty se použijí v době sestavení obrazu, na rozdíl od proměnných prostředí se ale nutně nemusí stát jeho součástí. Prostudujte si prosím příklad níže, včetně dalších obrazů které sestaví skript build.sh v odpovídajícím adresáři. Ke studiu budete potřebovat ještě příkazy docker image inspect a docker image save, se kterými vás blíže seznámí jejich manuálová stránka. Přečtěte si i samotný skript build.sh.

    In [54]:
    pushd priklady/blok4_docker_03_buildargs > /dev/null
    cat Dockerfile.2
    printf "\n\n\nA věru nebezpečná varianta\n"
    cat Dockerfile.3
    popd > /dev/null
    
    FROM debian:buster
    
    RUN printf "%s\n%s\n" \
      "deb http://ftp.linux.cz/pub/linux/debian buster main contrib non-free" \
      "deb http://ftp.linux.cz/pub/linux/debian buster-updates main contrib non-free" \
      > /etc/apt/sources.list
    
    RUN apt-get update \
      && apt-get install -y openssh-client git \
         jupyter-notebook \
      && apt-get clean
    
    ### DANGEROUS SECTION - push-in your ssh keys to the ssh-private directory, build
    ### the image, save it with docker image save -o image.tar and inspect the contents
    ARG UID
    ARG USER
    ARG SSH_PRIVATE_KEY_CONTENTS
    
    RUN useradd -u $UID -d /repos -s /bin/bash ${USER}
    
    COPY ssh-private /repos/.ssh
    RUN chown -R ${USER} /repos
    
    USER ${USER}
    WORKDIR /repos
    
    RUN test -n "${SSH_PRIVATE_KEY_CONTENTS}" \
        && echo "${SSH_PRIVATE_KEY_CONTENTS}" > .ssh/id_rsa \
        && chmod 600 .ssh/id_rsa \
        && git clone git@gitlab.fi.muni.cz:pb176/2021-prednasky.git prednasky \
        && git clone git@gitlab.fi.muni.cz:pb176/2021-priklady.git prednasky/priklady  \
        && rm -rf /repos/.ssh/id_rsa
    
    RUN pip3 install bash_kernel && python3 -m bash_kernel.install
    
    EXPOSE 8888/tcp
    ENTRYPOINT jupyter-notebook --no-browser --ip=0.0.0.0
    
    
    
    A věru nebezpečná varianta
    FROM debian:buster
    
    RUN printf "%s\n%s\n" \
      "deb http://ftp.linux.cz/pub/linux/debian buster main contrib non-free" \
      "deb http://ftp.linux.cz/pub/linux/debian buster-updates main contrib non-free" \
      > /etc/apt/sources.list
    
    RUN apt-get update \
      && apt-get install -y openssh-client git \
         jupyter-notebook \
      && apt-get clean
    
    ### DANGEROUS SECTION - push-in your ssh keys to the ssh-private directory, build
    ### the image, save it with docker image save -o image.tar and inspect the contents
    ARG UID
    ARG USER
    ARG SSH_PRIVATE_KEY_CONTENTS
    ENV SSH_PRIVATE_KEY=${SSH_PRIVATE_KEY_CONTENTS}
    
    RUN useradd -u $UID -d /repos -s /bin/bash ${USER}
    
    COPY ssh-private /repos/.ssh
    RUN chown -R ${USER} /repos
    
    USER ${USER}
    WORKDIR /repos
    
    RUN test -n "${SSH_PRIVATE_KEY}" \
        && echo "${SSH_PRIVATE_KEY}" > .ssh/id_rsa \
        && chmod 600 .ssh/id_rsa \
        && git clone git@gitlab.fi.muni.cz:pb176/2021-prednasky.git prednasky \
        && git clone git@gitlab.fi.muni.cz:pb176/2021-priklady.git prednasky/priklady
    
    RUN rm -rf /repos/.ssh/id_rsa
    
    RUN pip3 install bash_kernel && python3 -m bash_kernel.install
    
    EXPOSE 8888/tcp
    ENTRYPOINT jupyter-notebook --no-browser --ip=0.0.0.0
    

    V těchto souborech jsou 2 nové direktivy:

    • ARG - tyto proměnné jsou použity v okamžiku sestavování obrazu a jsou implicitně zahazovány. Příklad s Dockerfile.3 ale ukazuje, že mohou snadno uniknout.
    • ENV - proměnné prostředí, jsou nastavitelné i při docker run. Jejich hodnoty platné v době vytváření obrazu jsou zařazeny do metadat obrazu. Neměly by tedy obsahovat citlivé údaje. Při příkazu docker run se nastavují přepínačem -e.

    Jako nejlepší řešení se nicméně jeví použít vlastnost novějších verzí dockeru - multistage Dockerfile. Ten oproti klasickému Dockerfile obnáší více direktiv FROM, přičemž do koncového obrazu jsou zahrnuty pouze ty vrstvy, které jsou přímo dosažitelné z poslední direktivy FROM a vytvořené jí následujícími příkazy. To, co potřebujete skrýt tak provedete v první části, pak pomocí nového FROM založíte nový obraz a z předchozího stage jen vykopírujete konkrétní soubory pomocí upravené direktivy COPY.

    In [55]:
    pushd priklady/blok4_docker_04_multistage > /dev/null
    cat Dockerfile
    printf "\n\nA nyní build\n"
    docker build -t pb176-blok4-docker-04-multistage .
    popd > /dev/null
    
    FROM debian:buster
    
    RUN printf "%s\n%s\n" \
      "deb http://ftp.linux.cz/pub/linux/debian buster main contrib non-free" \
      "deb http://ftp.linux.cz/pub/linux/debian buster-updates main contrib non-free" \
      > /etc/apt/sources.list
    
    RUN apt-get update \
      && apt-get install -y openssh-client git \
      && apt-get clean
    
    RUN useradd -u 1001 -m -s /bin/bash temporary
    
    COPY ssh-private /home/temporary/.ssh
    RUN chown -R 1001 /home/temporary
    
    USER temporary
    WORKDIR /home/temporary
    
    RUN mkdir -p /home/temporary/repos \
      && git clone git@gitlab.fi.muni.cz:pb176/2021-prednasky.git repos/prednasky \
      && git clone git@gitlab.fi.muni.cz:pb176/2021-priklady.git repos/prednasky/priklady
    
    FROM debian:buster
    
    COPY --from=0 /etc/apt/sources.list /etc/apt/sources.list
    COPY --from=0 /home/temporary/repos /repos
    
    RUN apt-get update \
      && apt-get install -y openssh-client git jupyter-notebook \
      && apt-get clean
    
    RUN pip3 install bash_kernel && python3 -m bash_kernel.install
    
    RUN useradd -u 1001 -d /repos -s /bin/bash pb176
    
    USER pb176
    WORKDIR /repos
    
    EXPOSE 8888/tcp
    
    ENTRYPOINT jupyter-notebook --no-browser --ip=0.0.0.0
    
    
    A nyní build
    bash: docker: command not found
    

    Zatím jsme pracovali s intuitivním významem slova kontejner. Pojďme jej ale ukotvit. Kontejner je proces a prostředí ve kterém běží, izolovaný od zbytku OS (a jeho prostředí). Jinými slovy, kontejner existuje jen tak dlouho, dokud běží jeho entrypoint. Jeho doběhnutím je kontejner zastaven, opětovným spuštěním vzkříšen.

    Docker nabízí nástroj, jak se do prostředí kontejneru dostat i ze strany - pokud běží entrypoint, můžeme použít příkaz docker exec, který nám umožňuje v prostředí běžícího kontejneru pustit pomocné procesy, prohlédnout data aj.

    Docker(d) jako takový se stará nicméně jen o spuštění kontejneru, nijak jej dále nekoordinuje1. Umožňuje ale zakonzervovat prostředí ve kterém služba běží. V důsledku toho je tak trochu jedno, kde konkrétně běží. A to nás přivádí k orchestrátorům. Zjednodušeně řečeno, orchestrátory jsou služby hlídající kontejnery, distribuující je mezi zúčastněné uzly a starající se, aby byla služba dostupná v nějakém předvoleném režimu. Orchestrátory nicméně nejsou předmětem tohoto kurzu, setkáte se s nimi (docker swarm) ale například v PB138.


    1. Toto není pravda, ale patří mezi lži-dospělým

    Compose

    Tedy až na docker-compose. Docker-compose je orchestračním nástrojem, který vám umožní mezi sebou koordinovaně provázat skupinu kontejnerů tak, aby vytvářely jednu logickou službu (např webový server + PHP engine + MariaDB/MySQL server).

    Naneštěstí, verzování compose a Dockeru není navzájem zrcadleno, byť koná podobné inkrementální kroky. Jeden z hlavních důvodů proč se docker-compose používá i v malém je že Vám umožní místo opakovaného ručního pouštění kontejneru s mapováním portů, svazků a sítě ručně spouštět jen příkaz docker-compose up. Toho je dosaženo díky konfiguračnímu souboru docker-compose.yml, který podrobněji popisuje vazby mezi kontejnery a jejich nastavení. Po syntaktické stránce jde o formát YAML, který sice vyžaduje trochu cviku, ale strukturálně se podobá známějšímu formátu JSON (je nicméně člověku čitelnější).

    Vzpomeňme příklad s kontejnerem z minulé přednášky (notebook v kontejneru). Pokud jsme chtěli namapovat adresář hypervizora dovnitř kontejneru, bylo nutno zadávat ručně větší množství přepínačů. Takto pro uvedený příklad vypadá jeho compose (# je zakomentovaný řádek):

    In [56]:
    cat priklady/blok4_compose_01_simple/docker-compose.yml
    
    version: "3.3"
    services:
      notebook:
        build:
          context: ../blok4_docker_02_user
          dockerfile: Dockerfile-noclone
        volumes:
    #      - ./prednasky/:/prednasky/
          - ../../:/prednasky
        working_dir: /prednasky
        ports:
          - 8080:8888
    
    

    Záměrně nepokryto

    • Sítě v dockeru vs. sítě v compose
    • Sdílení síťových socketů mezi kontejnery
    • Jak v kontejneru spouštět grafické aplikace
    • Princip jedna služba / jeden kontejner

    Úkrok stranou - data a overlay

    Ve výchozí konfiguraci mívají kontejnery vyhrazen diskový prostor v rámci hostitele (typicky /var/lib/docker). Aby nebyl zbytečně zabírán cenný diskový prostor, je využit princip copy-on-write - data obrazu jsou uvažována jen pro čtení a nad obrazem je pro každý kontejner vytvořen overlay, do kterého probíhají zápisy.

    Overlay ale můžete využít i pro vlastní potřeby a vlastní data. Kupříkladu PostgreSQL server má onu nepříjemnou vlastnost, kdy potřebuje možnost zapisovat i do databáze, kde jinak probíhá jen čtení. Obyčejné vykopírování tak nepřipadá v úvahu, neboť by se nám kopírované soubory měnily pod rukami. Nezoufejte, můžete totiž:

    1. Databázový server na malou chvíli zastavit.
    2. Využít atomicity přejmenování adresáře s daty, vytvořit náhradní adresář se stejnými právy.
    3. Vytvořit pomocné adresáře (work, upper).
    4. Do náhradního adresáře připojit overlay (pro overlay2 zní příkaz takto: mount -o lowerdir=data_ro,upperdir=upper,workdir=work none data).
    5. Znovu spustit databázový server.
    6. Vykopírovat adresář data_ro.

    Díky tomuto lehkému triku pak nemusíte neustále přesouvat velké objemy dat, ale pouze propagovat overlay do původní kopie.

    Jde nicméně o špinavý hack, věřím že zálohování databází nebudete muset nikdy1 řešit.


    1. Takže se vám to bude zatraceně hodit. Viz:

    In [57]:
    pushd priklady/blok4_compose_02_multi > /dev/null
    printf "Služba pro systemd\n"
    cat mtbmap-cz.service
    printf "\n\nA odpovídající compose\n"
    cat docker-compose.yml
    popd > /dev/null
    
    Služba pro systemd
    [Unit]
    Description=MTBmap.cz service containers
    Requires=autofs.service
    After=syslog.target
    After=network.target
    
    [Service]
    Type=simple
    ExecStartPre=/bin/bash -c 'while [ `mount | grep ceph-plzen.xfs | wc -l` -eq  0 ] ; do sleep 1s ; done ; grep -q ceph/postgresql /proc/mounts || mount -t overlay -o rw,noatime,lowerdir=/mnt/ceph/postgresql-lower,upperdir=/mnt/ceph/postgresql-upper,workdir=/mnt/ceph/postgresql-workdir none /mnt/ceph/postgresql'
    WorkingDirectory=/mnt/ceph/mtbmap-czechrep/mtbmap-docker
    
    User=root
    Group=root
    
    
    ExecStart=/usr/bin/docker-compose up
    ExecStop=/usr/bin/docker-compose down
    
    TimeoutSec=50
    
    [Install]
    WantedBy=multi-user.target
    
    
    A odpovídající compose
    version: "3.3"
    services:
      postgres:
        build: postgres-mtbmap
        volumes:
          - /mnt/ceph/postgresql/mtbmap-docker-9.6/:/var/lib/postgresql/data
        networks:
          mtbmap-internal:
    
      web:
        build: mtbmap
        volumes:
          - /mnt/ceph/mtbmap-czechrep/docroot/:/var/www/html/
          # temporary for upgrade
          - /mnt/ceph/mtbmap-czechrep/Data/:/var/www/Data/
          - /mnt/ceph/mtbmap-tiles/mod_tile:/var/lib/tirex:rw
          - ./mtbmap/website.conf:/etc/apache2/sites-enabled/000-default.conf
          - ./mtbmap/tileserver.conf:/etc/apache2/sites-enabled/001-tileserver.conf
        links:
          - postgres:postgres
          - renderer:tirex
          #- tileserver:tileserver
        depends_on:
          - postgres
          - renderer
          #- tileserver
        networks:
          mtbmap-internal:
            aliases:
              - intraweb
          public:
            ipv4_address: 147.251.54.117
    
      renderer:
        build: mtbmap
        volumes:
          - /mnt/ceph/mtbmap-czechrep/docroot/:/var/www/html/
          - /mnt/ceph/mtbmap-czechrep/Data/:/var/www/Data/
          - /mnt/ceph/mtbmap-tiles/mod_tile:/var/lib/tirex:rw
          - ./mtbmap/tirex.conf:/etc/tirex/tirex.conf
          - ./mtbmap/mapnik.conf:/etc/tirex/renderer/mapnik.conf
        links:
          - postgres:postgres
        entrypoint: /docker-entrypoint.sh renderer
        networks:
          mtbmap-internal:
    
    networks:
      mtbmap-internal:
        driver: bridge
        internal: true
      public:
        external: true
    
    

    Kódová jména, verze, čísla sestavení a nightly buildy

    Přemýšleli jste někdy, odkud se vzala jména jako "Bionic Beaver", "Spherical Cow", či "Ice Cream Sandwich"? Takové kódové označení má několik základních rolí:

    • Utajení. Z kódového označení nemusí být nutně obratem jasné, že jde o záplatovací vydání například platebního terminálu. Podpořit utajení lze i nahodilými změnami.
    • Unikátní identifikace projektu uvnitř společnosti. Toto je typicky podpořeno volbou kódového jména mimo obvyklý slovník.
    • Reklama a PR (Windows eXPerience), označení "rodové linie", nikoliv konkrétní verze (Windows 10 vs. Windows 10 Pro). Restart projektu pod novým názvem, oddělení od ryze vývojové a experimentální fáze (Windows Longhorn).
    • Rozlišení pre-releases od nového vydání.
    • Daleko snáze se pamatují.

    Zatím co kódové označení je tedy spíše nějakým souborným označením, verzovací čísla mají za úkol zcela jednoznačnou identifikaci verze (podobně jako hash commitu). Na rozdíl od hashů commitů ale slouží i pro orientaci člověka - kýženým prvkem je tedy i pozvolný nárůst a snadná rychlá lidská i strojová orientace.

    Existuje mnoho různých, více či méně logických schémat. Podívejme se ale na pár nejčastějších.

    1. Verze zapisovaná jednotlivými číslicemi (zpravidla dekadickými), oddělenými zpravidla znakem tečky. Tato nabývá podoby MAJOR.MINOR.REVISION ; přičemž:
      • Změna v MAJOR rozbíjí kompatibilitu (zpětnou), či je jiným významným milníkem (např. přidání nové platformy, změna šifrovacího schématu, aj.).
      • Změna v MINOR typicky značí přidání funkcionality, bez rozbití kompatibility.
      • Změna v REVISION typicky řeší nějaký bug. Je také známa jako PATCH
    2. S číslovanými alfa verzemi
      • Typicky MAJOR.MINOR.REVISION-a.1 - a pro alfa, b pro beta...
      • Revize .90+ - např 5.4.98.
      • Případně zkráceně release-candidate (5.5.0-rc4).
    3. Verze dána CURRENT:REVISION:AGE, typicky používaná pro verzování API dle Libtool ; přičemž:
      • Opravdovou verzí je CURRENT.
      • REVISION značí revizi, rozšíření existujícího API bez nutnosti úpravy vazeb a kódu.
      • AGE - říká s jak starými implementacemi je API kompatibilní. Jinými slovy, s aktuální verzí budou fungovat klienti předpokládající verzi CURRENT-AGE, CURRENT-AGE+1 .. CURRENT.
    4. Datum ve tvaru (YY)YYMMDD, případně doplněno o číselnou revizi či hash commitu: 2021-05-16.01-deadbeef.
      • Speciální formou datového kódování je třeba systém používaný firmou Canonical pro Ubuntu Linux. Tato nabývá tvaru YY.MM[.REVISION] ostré vydání je číslováno 2 čísly, navazující LTS (long-term support) pak třemi; tedy Ubuntu 18.04 (Bionic Beaver) se mění v Ubuntu 18.04.1 LTS (Bionic Beaver). Tento systém je pak dvojnásob verzovaný i v tom smyslu, že přídavné jméno kódového označení starší verze lexikograficky předchází přídavné jméno kódového označení verze novější.
    5. Simultánní značení vývojové verze lichou číslicí a ostré verze sudou. Historicky užito například v Linuxu, kdy stabilní větev byla 2.4, vývojová 2.5 (Linux od této praxe nicméně upustil).
    6. Označení pouze kódovým jménem. Pokud jste někdy přemýšleli nad logikou číslování windows, je to proto že Windows 95, 98, 7, 8, 10 - všechny měly verzovací řetězec, nikoliv číslo. S nástupem Windows ME se testoval jen začátek verzovacího řetězce, kde když byla 9, bylo to pre-NT jádro.

    Jako speciální případ pak platí číslování nightly buildů. Nightly build - jak už název napovídá - je každonoční sestavování projektu z jeho aktuálních zdrojových kódů. Toto má dvě základní výhody.

    1. Uživatel nightly buildu má k dispozici nejnovější verzi, včetně kandidátů na opravy chyb. Pravda, cenou může být horší stabilita a chyby nové. V některých případech i nekompatibilita souborů vygenerovaných touto verzí se soubory verze předchozí.
    2. Vývojářům poskytují základní povědomí o tom, zda nedošlo k rozbití vnitřních API stran kompilace. Pokud navíc dochází k sestavením proti různým platformám, je informace o případných problémech s novým kódem dostupná obratem.

    Představují nicméně drobný oříšek z hlediska číslování verze. Nadále bychom totiž rádi udrželi žádoucí vlastnosti, jako možnost jednoznačně najít párování na zdrojový kód či obratem říci, která verze je novější. Můžeme ale zkombinovat přístupy výše. Není tak výjimkou, že se setkáme s verzí tvaru 1.5-nightly-20210511-deadbeef. Pravda, je malinko delší, ale identifikuje jednoznačně (1.5 je v tomto případě označení typicky větve ze které se vychází, nikoliv příští). Takové číslo můžeme snadno získat i v configure.

    In [58]:
    pushd priklady/blok4_nightly > /dev/null
    autoheader && autoreconf
    ./configure --prefix=/usr && make
    printf "\n\nVýsledek:\n"
    ./uuid_demo
    popd > /dev/null
    
    checking for gcc... gcc
    checking whether the C compiler works... yes
    checking for C compiler default output file name... a.out
    checking for suffix of executables...
    checking whether we are cross compiling... no
    checking for suffix of object files... o
    checking whether we are using the GNU C compiler... yes
    checking whether gcc accepts -g... yes
    checking for gcc option to accept ISO C89... none needed
    checking for pkg-config... /usr/bin/pkg-config
    checking pkg-config is at least version 0.9.0... yes
    checking for uuid... yes
    configure: creating ./config.status
    config.status: creating Makefile
    config.status: creating config.h
    gcc -g -O2 -I/usr/include/uuid   -c -o uuid_demo.o uuid_demo.c
    gcc   uuid_demo.o config.h  -luuid -o uuid_demo
    
    
    Výsledek:
    Built from sources as nightly-86afc35
    with configure ./configre --prefix=/usr
    A random universally-unique identifier: dd79dc9b-8543-430a-aaf8-00e15df71610
    See https://en.wikipedia.org/wiki/Universally_unique_identifier for details
    

    Reprodukovatelná sestavení

    Možná už jste si někdy kladli otázku "Kdo hlídá hlídače?" Na úrovni zdrojového kódu, ze kterého lokálně zkompilujete systém (nemusíme hned propagovat Gentoo Linux je to snadné, řada lidí ale nemá ani zdroje, ani čas, ani odbornou kapacitu se o vlastní stroj takto starat. A tak sáhne po binárním systému. Jak ale garantovat že dotyčné binární soubory opravdu pochází z proklamovaných zdrojových kódů, například při bezpečnostním auditu? Že vývojáři nikdo nezaviroval stroj a že při zveřejňování binárních souborů v tichosti nešíří vir?

    Řešením je reprodukovatelné sestavení, tedy takové podchycení událostí a proměnných reálného světa, které mohou ovlivnit výsledek. To na první pohled vypadá jednoduše, je tomu ale opravdu tak? Stačí se podívat, co vše mohlo být jinak v příkladu výše.

    • Kompilace mohla proběhnout bez přibaleného adresáře .git - například kvůli exportu repozitáře do zipu.
    • Na systému nemuselo být nainstalováno GCC, ale clang.
    • Nikde není uvedena konkrétní verze knihovny libuuid.
    • Co kdyby knihovna vůbec nebyla nainstalována?

    Je tedy nezbytné naprosto přesně zmapovat co do sestavení vchází a v jakých verzích. Jaké byly přepínače konfiguračního skriptu, neponechávat věci autodetekci jako explicitnímu povolení/zakázání. V tomto směru může pomoci použití zafixované sady nástrojů a závislostí, například kontejnerem někoho důvěryhodného.

    P.S. Pokud by snad byly uváženy náhodnostní testy či otázky časových razítek, mějte je konfigurovatelné zvenčí a deterministické. Reprodukce postupu a dosažení identického výsledku za to stojí.

    In [ ]:
    
    
    • konzervace prostředí buildu
    • časová razítka (timestamp)
    • náhodná čísla

    CI jako součást zajišťování kvality

    • pipeline, výchozí obraz

    Blok 6 - Zvané přednášky

    • [11. května 2021 - Martin Drengubiak, Comprimato](#2021-05-11)