Vivado, Git, Dockerを雑に組み合わせて雑に使う

# 前置き
手元の作業環境はWindowsがいいけどVivadoをWindowsにインストールして環境が汚れるのは嫌、どうせ動かすならLinux突っ込んだサーバ上で楽々コンパイルしたい等、諸々の欲求を満たすためにいろいろやったのでその結果のメモをまとめておく。

以下のような欲求がある人向け

* Vivadoのプロジェクトを雑にGitで管理したい
* 雑に開発環境構築したい
    * CentOSとか触りたくない、最悪Vivadoの動作環境としては許容するにしても作業環境はまともなOSにしたいし雑にアップデートもしたい
* VNCとかめんどいし、GUIをなるべく楽(というよりシームレス)に触りたい
* 触らずに済むならGUIは触りたくない

## 環境
* Vivado/Vitis 2020.1
* PetaLinux Tools v2020.1
* Ubuntu 20.04
    * Vivado 2020.1のサポートOS範囲外だがvivado-docker使えば無視できる(後述)
* docker-ce 20.10.7
    * インストール手順: [Install Docker Engine on Ubuntu](https://docs.docker.com/engine/install/ubuntu/)

## 例で使うディレクトリ構成
だいたいVivado 2020.1とかのデフォルト構造だが、それをpl_system, ps_system, embedded_systemといった具合で1リポジトリにまとめる感じを前提とする。具体的には以下の通り

* super_project_root
  * pl_system/
     * Vivadoのプロジェクトルート。PL向けHDLとかが入る
     * super_project_name.src/
     * super_project_name.hw/
     * etc...
  * ps_system/
     * PetaLinuxのプロジェクトルート。PS向けコード
  * embedded_system/
     * Vitisのプロジェクトルート。PL内のプロセッサ(MicroBlazeとか)向けコード

# Gitでのプロジェクト管理

## VivadoのプロジェクトをそのままGitで管理できない理由

Vivadoで生成したプロジェクトをGitで管理する際のノウハウは既にいろいろと公開されている。逆に言うと通常では考えられない謎の手順を踏まないとまともにバージョン管理ができない。

[Vivadoのプロジェクトをgitで管理するための最小限のファイル構成](https://qiita.com/nahitafu/items/b8bfee046b197c0fb833)
[XAPP1165: Vivado Design Suite におけるバージョン管理システムの使用](https://japan.xilinx.com/support/documentation/application_notes/j_xapp1165.pdf)

今回はこれらの事例を完全に無視して、 `Write Project to Tcl` 機能を使わずに雑にGitにファイルを突っ込む。

これまで`Write Project to Tcl`を用いたプロジェクトファイルのTclスクリプト化が必須とされてきた理由は概ね以下の通り。

* 何かする度にxprファイル等に無意味な差分が大量発生する
  * プロジェクトを置いたディレクトリのフルパスとか、各ステップの処理を行った日時などがxprファイルに記録される
  * 本質的には変更のないXMLファイルだが、要素順が毎回ランダムで出てくるとか
* コンパイル成果物とソースコードの類がディレクトリ単位で分けられておらず、ごちゃ混ぜで格納されている
  * コンパイル成果物はbuild/以下にしか出力されません、みたいなごく一般的な作りになっていない
  * サンプルデザインをビルドした程度でもプロジェクトが数GBクラスになるので、全てを無視してaddするのはあまりにも無理がある
* そもそも自動生成されたファイルのサイズがでかい
  * XMLファイル一発で数MBとか
  * Tclに落とし込むとこいつらが数百KBのTclファイルにまとまる！スマート！

とはいえTclにいちいちエクスポートする必要があるのはいくらなんでも面倒であり、これらの課題を解決する方法はTclに落とし込むこと以外にもあるはず。というわけで以下の条件については許容することとして、雑にVivadoが自動生成したファイルを丸々バージョン管理する方法を模索した。

* 他環境でclone後、Vivadoで開いて問題が出ない限りはクソ差分を許容する
  * 気兼ねなくcommitを切ることができる環境を優先した
  * commitさえ切れてればあとは気合いでどうにかなる
* push対象となるファイルサイズが合計で3桁前半MBぐらいなら許容する
  * これぐらいは別に珍しい話ではないし
  * 必要なら先人の教えに従ってTclエクスポートし、本当の最小限までサイズを落とせばいい

クソ差分を許容した場合、問題となるのはほぼ `コンパイル成果物とソースコード類がごちゃ混ぜになっている` という一点に絞ることができる。
これなら別に.gitignoreチューニングしていけば楽に達成できるんじゃない？ということで実践した結果のまとめが以下に続きます

## VivadoのプロジェクトをそのままGitで管理する

Vivadoのプロジェクトルートに後述の.gitignoreを突っ込みつつ、ignoreされないファイルはすべてgit addしていく。プロジェクトをtclにエクスポートとかはせず、xprファイルをそのままgitで管理してclone先でもxprファイルを使用する。
何も考えずxprファイルをバージョン管理すると死ぬほどどうでもいい差分(最後にビルドした日時がソースに埋め込まれる、XMLの要素順が毎回入れ替わる等)が山ほど出るが、そんなものを気にしているよりはとにかくcommitを切れという考えに基づき無視して全addしている。

注意点としては、プロジェクトを置いているディレクトリのフルパスがxprファイル内に記録されるため、Github等で何も考えずxprファイルを公開すると最後にプロジェクトを開いたユーザーの名前が丸見えになったりする。
この問題を含めたクソ差分問題の解決策として、プロジェクトを置いているディレクトリのフルパスをダミーに書き換えたりXMLの要素順をソートすることで無用な差分が出ないように整形するツールを試しに書いてみている。本記事の執筆時点でなんとなく動いてはいるが、厳密な検証はできておらずプロジェクトを破壊する可能性が高いのでこっそりリンクを張るだけにしておく。

[misodengaku/xpr_clean](https://github.com/misodengaku/xpr_clean)

以下は自分がVivadoプロジェクトを雑にgit addする際に使っている.gitignore。以下で挙げたファイルは概ねコンパイル時に自動で再生成されるため、git clone時には手元になくても問題ない。
またリポジトリを肥大化させているのも主にこいつらなので、このgitignoreを突っ込むだけでpush対象となるファイルサイズは大幅に削減できる。
数MBのXMLは依然としてpush対象だが、自動生成されたソースコードの類は圧縮してしまえばどうせ数百KBなので現代のストレージとネットワーク環境を考えれば無視してaddで問題ないと思われる。そのわずかなサイズですら惜しいという場合はその時にtclエクスポートを掛ければ良い。

配慮漏れとかがある可能性が高いため、流用する場合はとりあえずどっかにpush→新規にcloneしてコンパイルが通せることを確認してからgit cleanするなど、十分な動作確認をお願いします。

```
*.ip_user_files/
*.tmp/
*.runs/
*.cache/
*.hw/
*.sim/
  
**/*.srcs/*/bd/mref/
**/*.srcs/*/bd/*/ipshared/
**/*.srcs/*/bd/*/hw_handoff/
**/*.srcs/*/bd/*/ip/*/sim/
**/*.srcs/*/bd/*/ip/*/synth/
**/*.srcs/*/bd/*/ip/*/sysc/
**/*.srcs/*/bd/*/ip/*/src/
**/*.srcs/*/bd/*/sim/
**/*.srcs/*/bd/*/synth/
 
# ip_repoはIPコアを置いてるディレクトリ名
ip_repo/*/doc/
ip_repo/*/src/*/doc/
ip_repo/*/src/*/simulation/
ip_repo/*/src/*/hdl/
ip_repo/*/src/*/sim/
ip_repo/*/src/*/synth/
ip_repo/*/ipshared/
```


## Vitisの場合
VitisはVivadoと違ってEclipseベースなので単にsrcディレクトリをaddするだけ 嬉しい！（これが原因となって後で死にます）



# Vivado/Vitis/PetaLinuxをDocker内に押し込む
Vivado(と仲間達) on Dockerはいくつか実装例があるようだが、X11 forwardingでGUIが出せたりして便利なのでこれを使った。リンク先はディストリビューション等を弄った自前forkで、1コンテナでvivado, vivado_hls, vitis, petalinux-*系コマンドが全て実行できるようになっている。

[misodengaku/vivado-docker: Vivado on Docker](https://github.com/misodengaku/vivado-docker)

![リモートのLinuxサーバ上にあるVivadoをX11 forwardingで手元に召喚する例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102824/295ee939-a96e-c34b-f233-a942939eccf8.png "リモートのLinuxサーバ上にあるVivadoをX11 forwardingで手元に召喚する例")

## vivado-docker ビルド手順

物が物なのでDockerHubにpushする感じでもなく手元でのビルドが必要となる。時間は掛かるが./build.shを叩いたらあとは放置しておけば良いので環境を汚染されないメリットの方が大きいかなと思う。

```bash

$ git clone git@github.com:misodengaku/vivado-docker.git && cd vivado-docker
 
### どうにかして Xilinx_Unified_2020.1_0602_1208.tar.gz と petalinux-v2020.1-final-installer.run を入手し、ubuntu/18.04/vivado-petalinux/2020.1/files以下に置く
 
$ ./build.sh ubuntu 18.04 vivado-petalinux 2020.1
### ビルドに30分ぐらいかかる。またビルドするのに十分なディスク容量が必要（最終的に生成されるイメージが78GBとかになる）

$ docker images
REPOSITORY                                     TAG                   IMAGE ID       CREATED         SIZE
vivado-petalinux                               2020.1-ubuntu-18.04   XXXXXXXX       7 seconds ago   78.6GB
```

ビルドが終わればdocker runコマンド一発で起動するだけ。デスクトップ環境の入ったLinuxならそのままGUIが表示されるし、VcXsrvを入れて適切に設定を行ったWindows上でSSHコネクションを張っていればX11 forwardingで手元のWindowsでVivadoのウィンドウが開くはず。

…なんだけどdocker runがクソ長いので以下をコピペでどうぞ

```bash
$ docker run -it --net=host -v $HOME/.Xauthority:/home/vivado/.Xauthority:rw -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=$DISPLAY -e USER_ID=`id -u` -e GROUP_ID=`id -g` -v `pwd`:`pwd` vivado-petalinux:2020.1-ubuntu-18.04
```

引数の意味を噛み砕いてご説明

* -v \$HOME/.Xauthority:/home/vivado/.Xauthority:rw -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=$DISPLAY
  * X11 forwardingに必要なファイルをコンテナ内にマウントしたりDISPLAY環境変数を伝搬させる
* -e USER_ID=\`id -u\` -e GROUP_ID=\`id -g\`
  * USER_IDとGROUP_IDを渡すと起動時にusermod/groupmodでコンテナ内のユーザーIDを渡された値に書き換える
  * プロジェクトのディレクトリマウントしたときにパーミッション周りで問題が起きるのを防ぐためこうなっている
  * [dockerでvolumeをマウントしたときのファイルのowner問題](https://qiita.com/yohm/items/047b2e68d008ebb0f001#%E3%81%86%E3%81%BE%E3%81%8F%E3%81%84%E3%81%8F%E6%96%B9%E6%B3%953-dockerfile%E5%86%85%E3%81%A7%E3%81%99%E3%81%A7%E3%81%AB%E4%B8%80%E8%88%AC%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%8C%E4%BD%9C%E3%82%89%E3%82%8C%E3%81%A6%E3%81%84%E3%82%8B%E5%A0%B4%E5%90%88) の解決策をパクらせて頂きました
* -v \`pwd\`:\`pwd\`
  * カレントディレクトリをコンテナ内にマウントする
  * Vivadoプロジェクトのディレクトリ内でdocker runを叩けば当該プロジェクトがコンテナにマウントされる形となる
  * コンテナ内でmakeなど実行したい場合は -w \`pwd\` などとしておくとコンテナ起動時のカレントディレクトリが良い感じになって嬉しいですね


![ホストマシンの/home/miso/project_1をコンテナにマウントし、コンテナ内のVivadoでプロジェクトを開く図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102824/33f18047-e08e-07f7-51aa-60433a1c460d.png "ホストマシンの/home/miso/project_1をコンテナにマウントし、コンテナ内のVivadoでプロジェクトを開く図")


# CLIコンパイル
ナウなヤングとしてはGUIポチポチしてコンパイルとかやってらんねえ というわけでtclでどうにか一連の処理を記述して、cloneしたらmake叩くだけで成果物が得られる環境を目指す

## Vivado用ビルドスクリプト

```tcl
# set params
set JOBS_NUM 16
set PROJ_NAME super_project_name
set BD_NAME super_project_name
set BD_FILE "${PROJ_NAME}.srcs/sources_1/bd/${BD_NAME}/${BD_NAME}.bd"
 
# open project and bd file
open_project "./${PROJ_NAME}.xpr"
open_bd_design "./${BD_FILE}"
update_compile_order -fileset sources_1
 
# reset runs
reset_run impl_1
reset_run synth_1
 
# upgrade HLS IPs since minor revision will be changed in build process
upgrade_ip [get_ips {uz_petalinux_l8_pkt_buf_0_0 uz_petalinux_l8_pkt_buf_0_1}] -log ip_upgrade.log
export_ip_user_files -of_objects [get_ips {uz_petalinux_l8_pkt_buf_0_1 uz_petalinux_l8_pkt_buf_0_0}] -no_script -sync -force -quiet
 
# reset bd output product
export_ip_user_files -of_objects [get_files ${BD_FILE}] -sync -no_script -force -quiet
reset_target all [get_files ${BD_FILE}]
if { [get_fileset ${BD_NAME}] != "" } { delete_ip_run [get_files -of_objects [get_fileset ${BD_NAME}] ${BD_FILE}] }
 
# generate bd output
generate_target -force all [get_files ${BD_FILE}]
export_ip_user_files -of_objects [get_files ${BD_FILE}] -no_script -sync -force -quiet
create_ip_run [get_files -of_objects [get_fileset sources_1] ${BD_FILE}]
 
# implement and bitgen
launch_runs impl_1 -to_step write_bitstream -jobs ${JOBS_NUM}
wait_on_run impl_1
write_sysdef -force -hwdef ${PROJ_NAME}.runs/impl_1/${PROJ_NAME}_wrapper.hwdef -bitfile ${PROJ_NAME}_wrapper.bit -meminfo ${PROJ_NAME}_wrapper.mmi -file ${PROJ_NAME}_wrapper.sysdef
 
# export hardware
write_hw_platform -fixed -include_bit -force -file ${PROJ_NAME}_wrapper.xsa
```

Vivadoのbatch modeで上記tclを投げ込んでビルド実行。完走すれば良い感じにxsaファイルが生成されるはず

```bash
$ vivado -mode batch -source build.tcl
```




# Vitis CLIコンパイル

前述の通りVitisはEclipseがベースであり、Vitisの各種コマンドはEclipse本体に実装されている（？）ため、Vitisの各種コマンドを実行できるCLIフロントエンド [XSCT(Xilinx Software Command-line Tool)](https://japan.xilinx.com/html_docs/xilinx2020_2/vitis_doc/XSCT.html)を叩くと突然裏でEclipseが起動する。
そのせいでディスプレイのないサーバ上やSSHコンソールでxsct build.tclを叩くなどすると見事に爆死してくれるため、xsctをどうにかして騙す必要がある。

幸いXvfbという便利な物があるので、適当に↓のようなラッパースクリプトを作ると上手いこと動くようになる。readlinkでxsctの実体パスを取っているのはrlwrapを噛まさないとエラー吐くみたいなお節介機能を回避するため。

```bash
#!/bin/bash
XSCT_PATH=`readlink -f $(which xsct)`
XBIN_ROOT=`dirname ${XSCT_PATH}`
xvfb-run ${XBIN_ROOT}/loader -exec rdi_xsct vitis_build.tcl
```

上記スクリプト内で呼んでいるvitis_build.tclはこんな感じ。ここで注意点としてvitis_build.tclを実行した際にps_system/workspaceが作成され、ps_system/srcに存在するソースコードがworkspaceにコピーされる。
現状ではsrc内のソースコードを編集する度にps_systemをcleanしてvitis_build.tclでworkspace作り直しという挙動になっているが、Vivadoと違ってVitisではGUIで実行したコマンドが確認できず心が折れたため妥協として毎回作り直す造りになっている。
その都合上workspaceが既に存在している状態でvitis_build.tclを実行すると問題が起きるため、ビルドの度にworkspaceを削除する必要があり大変イケてない。がまあMakefile書いたらmake cleanするだけで済んでしまうので...みたいな気持ちで放置している。

```tcl
setws workspace
platform create -name super_project_name_wrapper_platform_0 -hw ../pl_system/super_project_name_wrapper.xsa -arch {32-bit}
domain create -name {domain_embedded_system_microblaze_0} -display-name {domain_embedded_system_microblaze_0} -os {standalone} -proc {embedded_system_microblaze_0} -runtime {cpp} -arch {32-bit} -support-app {empty_application}
app create -name embedded_mcu -platform super_project_name_wrapper_platform_0 -domain domain_embedded_system_microblaze_0 -proc embedded_system_microblaze_0 -template "Empty Application"
importsources -name embedded_mcu -path src
app build -name embedded_mcu
exec updatemem -force \
    -meminfo workspace/super_project_name_wrapper_platform_0/hw/super_project_name_wrapper.mmi \
    -bit workspace/super_project_name_wrapper_platform_0/hw/super_project_name_wrapper.bit \
    -data workspace/embedded_mcu/Debug/embedded_mcu.elf \
    -proc super_project_name_i/embedded_system/microblaze_0 \
    -out workspace/download.bit
exit
```