Jenkinsの公式Dockerイメージ使ってみた
以前のエントリ( http://knjname.hateblo.jp/entry/2014/05/03/190842 )で自分でJenkinsのDockerイメージを作成したりしてみましたが、 Jenkins公式でDockerイメージを配布するようになったので、それを使用したほうがいいと思います。
- Github https://github.com/cloudbees/jenkins-ci.org-docker
- DockerHub https://registry.hub.docker.com/_/jenkins/
普通に使うだけなら、下記のようにすればいいだけですが、
docker run -p 8080:8080 jenkins
これだと何も細かいことを設定できていないので、いくつか補足。
Dockerイメージ内のJenkinsのバージョンについて
基本的にJenkinsのサイトのトップ( http://jenkins-ci.org/ )に載っているようなマジもんの最新版は不安定です。
ただ、Dockerイメージのlatest
はLTS版(安定版)の最新がデフォルトで指定されているようです。
ちゃんと運用するのであれば明示的なバージョン指定をオススメしますが、特に気にしなくても問題なさそうです。
# 今(2015-02-10)のところ下記3つは同じ docker run -p 8080:8080 jenkins docker run -p 8080:8080 jenkins:latest docker run -p 8080:8080 jenkins:1.580.2 # (実はLTS版は1.580.3がリリースされていたりするがDockerHubにはない模様…)
今のところ、使用されるJavaはOpenJDK7のようです。気に入らなければ自分でイメージを作って別のJDKを使わせるようにしたほうがいいでしょう。
永続化ディレクトリ(JENKINS_HOME)について
永続化が必要なデータはコンテナ内の /var/jenkins_home
に作成されるので、ホストディレクトリをその位置にマウントしましょう。
docker run -p 8080:8080 -v /hostdir/jenkins_home:/var/jenkins_home jenkins
ただし、root権限でそのまんま作成したディレクトリ(普通にdocker run
が勝手に作ったものがこれに該当)はコンテナ側のJenkinsユーザ(jenkins(UID=1000)
)で書くことができません。
以下のいずれかで対処しましょう。
- オススメ:
chown 1000 /hostdir/jenkins_home
する。(※UIDはDockerfile内で明示的に指定されているので変わる可能性は低いでしょう) chmod 777 /hostdir/jenkins_home
する。- 独自のJenkinsイメージを作成してJenkinsの実行ユーザをrootにする。
- 一度マウントしないで起動させたコンテナ内から
/var/jenkins_home
をコピーしたものをあらためてマウントする。
JenkinsのJVMのオプションについて
JavaのメモリetcのためのJVMオプションはJAVA_OPTS
という名前で環境変数に設定すればいいです。
docker run -p 8080:8080 -e JAVA_OPTS="-Xmx1g" jenkins
Jenkinsのオプションについて
たとえば、 http://hostname/jenkins のようにURLプレフィックスを設定するには、docker run jenkins
の後ろに引数追加すればいいです。あるいはJENKINS_OPTS
に定義すればいいです。
docker run -p 8080:8080 jenkins --prefix=/jenkins docker run -p 8080:8080 -e JENKINS_OPTS="--prefix=/jenkins" jenkins
Jenkins用の引数一覧は https://wiki.jenkins-ci.org/display/JENKINS/Starting+and+Accessing+Jenkins で一部リスト化されています。
ログファイルの出力先などもオプションでの指定できるはずなのですが、記載がないですね。https://github.com/jenkinsci/jenkins/blob/master/rpm/SOURCES/jenkins.init.in とかを参考に追加してみるといいかもしれません。
docker run -p 8080:8080 jenkins --logfile=/var/jenkins_home/jenkins.log
Jenkins初期化Groovyスクリプト
ほか、設定ファイルに永続化されておりパラメータでは初期指定できない情報や、 初期プラグインを勝手にインストールさせるなどを行わせるGroovyスクリプトをロードさせることができます。
(Groovyでの自動化の詳細は https://wiki.jenkins-ci.org/display/JENKINS/Jenkins+Script+Console を見て下さい。)
ためしに、下記のスクリプト(my-jenkins-init.groovy
)が入ったディレクトリを /var/jenkins_home/init.groovy.d/
にマウントしてみましょう。
// my-jenkins-init.groovy import hudson.model.*; import jenkins.model.*; def inst = Jenkins.instance def uCenter = inst.updateCenter // Set the proxy for retrieving plugins. (Comment out and modify it if you need.) // inst.proxy = new hudson.ProxyConfiguration("192.168.10.10", 3128, "proxyUser", "proxyPassword", "localhost, 192.*") // Ensure the Update Center is up-to-date. uCenter.updateAllSites() def checkIfPluginInstalled = { pluginid -> for(p in inst.pluginManager.plugins) { if(p.shortName == pluginid){ return true } } return false } // Installs the following plugins if not installed. ["checkstyle", "copyartifact", "build-timeout", "emotional-jenkins-plugin"].findAll({! checkIfPluginInstalled(it)}).each { uCenter.getPlugin(it).deploy() } // Set the number of executors (in master-node) to 0. inst.setNumExecutors(0) inst.reload()
自動でいくつかプラグインがインストールされており、マスターノードのエグゼキュータ数が0の設定が行われているはずです。
初期のイメージでは、スレーブ用のJNLPポートの自動設定スクリプトが入っているようです。
// tcp-slave-angent-port.groovy import jenkins.model.*; Thread.start { sleep 10000 println "--> setting agent port for jnlp" Jenkins.instance.setSlaveAgentPort(50000) }
Feb 09, 2015 5:08:41 PM jenkins.util.groovy.GroovyHookScript execute INFO: Executing /var/jenkins_home/init.groovy.d/tcp-slave-angent-port.groovy Feb 09, 2015 5:08:43 PM org.jenkinsci.main.modules.sshd.SSHD start
(2015/09/09) 今のJenkinsのイメージだとコンテナ起動時に指定された環境変数 JENKINS_SLAVE_AGENT_PORT
の番号にJNLPのポートを変更するようになっています。良い改善だと思います。
ということで、
だいたい上記でやりたいことはできます。
yum
やapt-get
で入れたパッケージ版のJenkinsも結局は、
上記で説明しているようなパラメータの設定を設定ファイルにバラしているだけなので、
パッケージ版に機能で遅れを取るといったことは一切心配不要です。
Jenkinsのマスターはマスター!
以前のエントリ( http://knjname.hateblo.jp/entry/2014/05/03/190842 )でも書いたとおり、Jenkinsのマスターノードにビルドを直接させる必要は一切ありません。
大量にいろんな種類のジョブをこなすようになると、スレーブが増えていき、 そんな中でマスターがビルドしているのが不自然かつ、マスター依存のジョブがあった日にはスケールが面倒になってきます。
特に貧弱な環境じゃない限り私はマスターノードにビルドをさせていません。マスターのジョブキューの太さは0にしてます。
今回のエントリでふれているのは、Jenkinsマスターノードのセットアップであり、スレーブノードの話ではありません。 Jenkinsのマスターノードは、WebUIを提供して、設定を保存して、スレーブノードとつながって、 スレーブが収めてくるジョブの履歴などをディレクトリに貯めていればそれでいいんです。(というのが私の考え。)
Jenkinsが活躍している開発現場でマスターノードが死ぬと開発者への影響が大ですが、 スレーブに関していえばホストOSごとおっ死んでも大して影響はないでしょうし、代替のスレーブがいればスレーブの1台2台ダウンしようが 平気なところも多いはずです。
実際ビルドを行わさせるスレーブについては、Jenkinsのビルド用のDockerコンテナを作成してSSHでマスタから繋くとかの平易な構成でいいと思います。 (もちろんDockerコンテナ使い捨てとか頑張るのもアリ)
ちなみにスレーブ内にビルド用のJDKやAntを含めてもいいですが、実際には含めず、Jenkinsの設定画面で JDKやAntのtar.gzやzipをビルド実行時にネットからインストール or どっかのURLから解凍できるように構成したほうが楽です。 (とはいえ、スレーブプロセスはJVMで動くので、スレーブ自身が動くためのJREは必要です。)
現状の公式Dockerイメージへの個人的な不満点
- OpenJDK7を使っている。OpenJDK8じゃ駄目なのかな。個人的にはヒープはともかく、いろいろやると広がるパーマネント領域を意識したくない。 → 最新のイメージはOpenJDK8使っているようです (2015/08/05)
/var/jenkins_home
にroot権限で作成した外部ディレクトリをマウントさせると起動に失敗する件。…まあこれはDocker自体の問題に近いですが。 →docker run
時に-u root
オプション付ければうまくいくことも多いでしょう(2015/09/09)- Jenkinsのログファイルの出力場所やログレベルの指定など細かい起動オプションが分かりづらい。
VBAで長いパスが扱えないと思ったら
Windowsでは長いパス(260文字前後)を使うと呪いに遭います
Windowsでは長いパスを使うと呪いに遭います。
(個人的には英語の長ったらしいディレクトリ名とファイルパスをついたものをJenkinsでこれまた長ったらしいワークスペースフォルダにチェックアウトした時に遭遇している)
こんなディレクトリとファイルがあったとしましょう。
C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\ 01234567.xlsx a.xlsx b.xlsx
上記中の 01234567.xlsx
はWindowsで許されている最長のファイルパスを持つエントリです。
ただ、ファイルパスが長過ぎるためか、、、
- ExcelVBAの
Workbooks.Open()
で開くことができません。(アプリケーション側から開くのも不可能です。) - FSOが妙な挙動を示すようになります。
このFSOの妙な挙動というのは、詳しくいうと、下記のようにファイルの列挙に失敗します。
Sub listFiles() Dim fso As New FileSystemObject Dim i& ' 正しいファイル数 3 が表示される Debug.Print fso.GetFolder("C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789").Files.Count ' 長いファイルパスのFileオブジェクトの作成にFor Each文で失敗し、2が表示される For Each f In fso.GetFolder("C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789").Files i& = i& + 1 Next Debug.Print i End Sub
ロングパス対策
では、どうやってこれを克服すればいいかというと、解決方法は簡単で、長いパス部分についてショートパスを使ってあげればOKです。
Sub listFiles2() Dim fso As New FileSystemObject Dim i& Debug.Print fso.GetFolder("C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789").Files.Count For Each f In fso.GetFolder("C:\LONGPA~1\012345~2").Files Debug.Print f i& = i& + 1 Next Debug.Print i End Sub
同様に、長過ぎるパスのブックをExcelは開けないので、かわりにショートパスを与えてあげれば問題なく開くことができます。
Dim wb As Workbook ' 失敗する Set wb = Workbooks.Open("C:\LongPathTest\01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\01234567.xlsx") ' これならOK Set wb = Workbooks.Open("C:\LONGPA~1\012345~2\012345~1.XLS")
ちなみに上記のようなショートパスでファイルを開いた場合、ショートパスのファイルとしてVBA内では扱われます。
Debug.Print wb.Name ' 01234567.xlsx Debug.Print wb.Path ' C:\LONGPA~1\012345~2 Debug.Print wb.FullName ' C:\LONGPA~1\012345~2\01234567.xlsx
ショートパス⇔ロングパス変換
たとえばファイルパスの一覧を出すといったマクロの場合、長過ぎるパスを処理する場合に、ショートパスが欲しかったり、逆にショートパスで処理しているが故にレポート出力時ではちゃんとロングパスで出力したい場合があると思います。
ロングパス → ショートパス変換はFSOを使って行うこともできますが、
Dim fso As New FileSystemObject Dim shortPath As String shortPath = fso.GetFile("えらい長いパス").ShortPath
そもそも長すぎてGetFileする時点で失敗するかもしれないし、相互の変換については、専用のWindows APIがあるので、それを使いましょう。
以下がWindows API(の宣言とその応用のためのVBAプロシージャ)です。64bit Officeでしか試していませんし素人なので、なんか間違ってるかも。(いつか調べます)
' LongPath -> ShortPath #If VBA7 Then Private Declare PtrSafe Function Win32APIGetShortPathName Lib "kernel32" Alias "GetShortPathNameA" _ (ByVal lpszLongPath As String, ByVal lpszShortPath As String, ByVal cchBuffer As LongLong) As LongLong #Else Private Declare Function Win32APIGetShortPathName Lib "kernel32" Alias "GetShortPathNameA" _ (ByVal lpszLongPath As String, ByVal lpszShortPath As String, ByVal cchBuffer As Long) As Long #End If Function GetShortPathName(ByVal longPath As String) As String Const PATH_LENGTH = 260 Dim pathBuffer As String pathBuffer = String$(PATH_LENGTH + 1, vbNull) #If VBA7 Then Dim pathLength As LongLong #Else Dim pathLength As Long #End If pathLength = Win32APIGetShortPathName(longPath, pathBuffer, PATH_LENGTH) GetShortPathName = Left(pathBuffer, CLng(pathLength)) End Function ' ShortPath -> LongPath #If VBA7 Then Private Declare PtrSafe Function Win32APIGetLongPathName Lib "kernel32" Alias "GetLongPathNameA" _ (ByVal lpszShortPath As String, ByVal lpszLongPath As String, ByVal cchBuffer As LongLong) As LongLong #Else Private Declare Function Win32APIGetLongPathName Lib "kernel32" Alias "GetLongPathNameA" _ (ByVal lpszShortPath As String, ByVal lpszLongPath As String, ByVal cchBuffer As Long) As Long #End If Function GetLongPathName(ByVal shortPath As String) As String Const PATH_LENGTH = 260 Dim pathBuffer As String pathBuffer = String$(PATH_LENGTH + 1, vbNull) #If VBA7 Then Dim pathLength As LongLong #Else Dim pathLength As Long #End If pathLength = Win32APIGetLongPathName(shortPath, pathBuffer, PATH_LENGTH) GetLongPathName = Left(pathBuffer, CLng(pathLength)) End Function
上記のソースで使っているWinAPIはこちら。
https://msdn.microsoft.com/en-us/library/windows/desktop/aa364989(v=vs.85).aspx https://msdn.microsoft.com/en-us/library/windows/desktop/aa364980(v=vs.85).aspx
このVBAソースはGistにもあげてあります。
PowerShellのダイナミックスコープについて
PowerShellのスコープが好きになれないし、罠にわりとハマっているので、個人的に動きを確認してみた。
ある程度以上の規模のスクリプトを書くにあたって、PowerShellは特に、スコープの意識が絶対必要です。
基本はダイナミックスコープ
PowerShellのスコープはダイナミックスコープ(スタック上、最後の変数宣言を見る)。
普通の言語みたいにレキシカルスコープ(上に辿って一番近くの変数宣言を見る)ではない。
$foo = "FOO" "foo is $foo" # foo is FOO Function Update-foo(){ $foo = "BAR" "foo is $foo" # foo is BAR } Update-foo "foo is $foo" # foo is FOO
まあこれが基本。
ただ、ダイナミックスコープって一言でいうけど、どういうシチュエーションでどう変わるか、確信持てますか?
ダイナミックスコープ七変化
とりあえずどういうスコープで変数が束縛されているのか知るためにいろいろコードのパターン並べてみました。
ちゃんと脳内動作と一致しているか確認してみよう!たぶん一致してないと思うよ!
(僕もいくつかの動作はよく理解できませんでした。いつか学習したい…)
思ったこと
PowerShellがシンプルに見える人は病院いったほうがいいと思う。
パクり元のPerlみたいにmy $lexicalVar; local $dynamicVar;
させてほしい…。
Jenkinsのジョブの進化
Jenkinsのジョブの進化をみていくよ〜。
はじまりはシンプルだった
プロジェクトの成果物をチェックするために、チェッカを動かすジョブ check-project-artifacts
を作る。
check_a
というチェッカを動かす必要があり、そのチェッカを動かした後のワークスペースの成果物はこんな感じ:
check-project-artifacts: ./target/check_a-result
一般の開発者は check-project-artifacts
のJenkins成果物をブラウザで開いて利用する。
チェッカが増えた
チェッカの種類が増え、もう1つのチェック check_b
を実施することになった。
check-project-artifacts
のジョブの最後にチェッカの追加をそのまま追加することにした。
check-project-artifacts: ./target/check_a-result ./target/check_b-result
さらにチェッカが増えた
更にチェッカの種類が増えた。(check_c
)
このままチェッカを一つのジョブに追加しつづけると埒があかず、しかもチェッカを直列に実行していると遅いので、チェッカごとにジョブを分割することにした。
check-project-artifacts
(もともとあったやつ)check-project-artifacts_a
(分割後)check-project-artifacts_b
(分割後)check-project-artifacts_c
(分割後)
それぞれ子供側のチェッカのジョブが終了したら、自分のチェック結果を成果物として保存し、 check-project-artifacts
を後続ジョブとして呼ぶ。
check-project-artifacts
ではそれぞれ分割したジョブから成果物をコピーし、そのまま自分のジョブ成果物として保存する。
check-project-artifacts: ./target/check_a-result ./target/check_b-result ./target/check_c-result check-project-artifacts_a: ./target/check_a-result check-project-artifacts_b: ./target/check_b-result check-project-artifacts_c: ./target/check_c-result
一部のチェッカについて、実行時間が長いので並列化を検討する
check_b
が長時間かかっているようなので、チェック対象となるプロジェクト成果物をある程度の単位(ここでは dirA
,dirB
,dicC
)に切り分け、ジョブをさらに細かく分割する。
check-project-artifacts
check-project-artifacts_check_a
check-project-artifacts_check_b
check-project-artifacts_check_b_dirA
(分割後)check-project-artifacts_check_b_dirB
(分割後)check-project-artifacts_check_b_dirC
(分割後)
check-project-artifacts_check_c
check-project-artifacts_check_b
は check-project-artifacts
を分割した時と同様、分割したジョブが完了したら分割前の親ジョブを呼び、親ジョブで子ジョブの成果物を回収する。
ただし、分割された子ジョブの成果物はそのままでは分割されていて見づらいため、親ジョブで分割された成果物を結合する処理を実施する。
check-project-artifacts_check_b_dirA: ./target/dirA_check_b-result check-project-artifacts_check_b_dirB: ./target/dirB_check_b-result check-project-artifacts_check_b_dirC: ./target/dirC_check_b-result check-project-artifacts_check_b: ./target/dirA_check_b-result (結合用) ./target/dirB_check_b-result (結合用) ./target/dirC_check_b-result (結合用) ./target/check_b-result (上記3ファイルを結合したもの) (ほかのプロジェクトは省略)
で、何が言いたいの?
Jenkinsのジョブがこうやって成長と複雑化を遂げることを言いたかった。
また、上記のようなジョブの分割といった場合は前のエントリで紹介しているように、Job DSL Pluginを使うと簡単に好きなだけコントロール可能な方法で分割できます。
もしJenkinsでちょっとずつ違う100のジョブを管理しなくてはいけなくなったら
(2015/05/14 追記) このエントリで言及しているSubversionリポジトリを複数指定できない問題ですが、今は解決されています。
個人用メモも兼ねて。
ちょっとこんがらがったプロジェクトではありがちですが、ある特定の環境へのリリースのためにいくつかのジョブで構成されたビルドパイプラインがあり、それを環境ごとに微妙な差分をつけて、複数種類のビルドパイプラインを構成する必要があったりします。
環境の数やアプリの数によっては、これらの差分を含めてJenkins上にジョブを構成しようとすると、下手をすると100個近いジョブに到達することもあります。
(いうまでもないですが、こういうジョブ過多な状態を通常は目指すべきではありません。)
こういう場合に手作業でJenkinsのジョブを構成しようとすると恐ろしいクリックゲームになるので、Jenkins公式が用意しているJob DSL Plugin を使ってプログラミングで大量のジョブを効率よく構成しましょう。
Job DSL Pluginの概要
https://github.com/jenkinsci/job-dsl-plugin/wiki
- 普通にプラグイン設定画面からインストール可能です。入れてない人はぜひ入れましょう。
- GroovyのDSLで、Jenkinsのジョブやビューをサクッと構成可能。
- 意外なほど多種のプラグインに対応している。
- (ちなみにSCM Sync Pluginとこれを一緒に使うと、内部のジョブ定義1つごとにSCMにコミットされます。ジョブ定義をするたびにコミットログがウザいことになりますが、気にしないことにしましょう:-p )
さっそくJob DSLを書いてみる
ローカル開発環境のインストール (Windows可)
JenkinsのJob DSL PluginのGroovyソースのテキストエリアにDSLを貼り付けてもジョブの生成はできるのですが、
- Groovyの文法が正しいのか、
- セマンティックなエラーは起きないのか、
- どういうジョブ定義が出力されるのか、
などなどJenkins上だけで実行ボタンをおしたりしてチマチマ確認するのはとっても面倒なので、ローカルでDSLの実行結果を確認できる環境を作ると非常に捗ります。
詳しい手順は https://github.com/jenkinsci/job-dsl-plugin/wiki/User-Power-Moves に書いてあるのですが、とりあえずここでも紹介しておきます。
まずは、Groovyを扱うため、JDKを入れて使えるようにしておきましょう。
あとはJob DSL Pluginをgit clone
したりして
$> git clone https://github.com/jenkinsci/job-dsl-plugin.git $> cd job-dsl-plugin
ちょっと適当なJob DSLを所定のフォルダにおいてあげましょう。下記のJob DSLをjob-dsl-core/sample.groovy
として保存してください。
def jobNames = ['Alpha', 'Bravo', 'Charlie'] for (jobName in jobNames) { job { name jobName } }
ここまでやったら下記コマンドを実行すれば、
./gradlew run -Pargs=sample.groovy
job-dsl-core/
以下に下記3つのそれぞれジョブの設定が入ったファイルができているはずです。
-rw-r--r-- 1 knjname staff 588 Jan 6 01:29 Alpha.xml -rw-r--r-- 1 knjname staff 588 Jan 6 01:29 Bravo.xml -rw-r--r-- 1 knjname staff 588 Jan 6 01:29 Charlie.xml
(Alpha.xmlの中身) <project> <actions></actions> <description></description> <keepDependencies>false</keepDependencies> <properties></properties> <scm class='hudson.scm.NullSCM'></scm> <canRoam>true</canRoam> <disabled>false</disabled> <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding> <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding> <triggers class='vector'></triggers> <concurrentBuild>false</concurrentBuild> <builders></builders> <publishers></publishers> <buildWrappers></buildWrappers> </project>
基本的にはDSLを編集→gradlew run ...
を実行→ローカルに出力されたXMLの中身を確認→DSLを編集、というループで開発していき、納得できる形になってから最後にJenkins上でDSLを実行という形になると思います。
Jenkins上でJob DSLを動かしてジョブを定義する
特に難しいことはなく、Tutorialにあるように、ジョブ生成用の種ジョブを作成するだけですね。
種ジョブは通常のジョブとほとんどかわらないので、たとえばJob DSLや特定の定義ファイルが更新されたら起動するように種ジョブのトリガをしかけておいて、状況に応じてジョブが勝手に増えるようにすることもできます。
ほか
各プラグインに対応したビュー、ジョブ、トリガー、前処理、後処理などなどの指定方法はWikiを見ましょう。リファレンスが充実しています。意外に対応範囲が広いことに驚くと思います。
Wiki見てピンとこなければGithub上でソースで該当の関数を検索するのがてっとり早いです。
自分の使っているプラグインに該当するDSLがなければ configure
ブロックを駆使して設定ファイルほぼそのまんまで記述しましょう。
ちょっとしたTipsなど
Gradleでの*.groovyファイルの指定方法いくつか
入力となるDSLのgroovyファイルを絶対パスで特定の場所で指定したい人は-Pargs=/root/hoge.groovy
(Windowsなら -Pargs=\C:\Users\root\hoge.groovy
みたいに指定)、複数指定したい人は"-Pargs=hoge.groovy fuga.groovy"
とすれば大丈夫です。
ちなみにXMLの出力場所の指定方法は知りません!
Gradleの起動が遅くてイラつく
gradlew
の起動が遅くてイライラする人はgradlew
のユーザごとプロファイル設定ファイル(~/.gradle/gradle.properties
)に下記の値を指定してGradle用のJVMをデーモン起動させるといいらしいよ!
org.gradle.daemon=true
DSL上で使えるコンテキスト依存の関数の作り方(+Subversionの場合の複数箇所チェックアウト)
たとえば、Subversionを使っている場合、普通に1ジョブ内で複数箇所からチェックアウトすると思うのですが、DSLではこの手のケースは想定されていません!
だから公式ではこうやれってよ!
scm { svn('http://hoge/svn/trunk', 'hoge') { it / locations << 'hudson.scm.SubversionSCM_-ModuleLocation' { remote 'http://fuga/svn/trunk' local 'fuga' } it / locations << 'hudson.scm.SubversionSCM_-ModuleLocation' { remote 'http://baz/svn/trunk' local 'baz' } } }
ロケーションの1個目だけを特別扱いして書いて、あとは無理やり設定XMLまんま書けとかイケてないですね。
せっかくプログラミング言語を使っているので、関数に出してすっきりさせてあげましょう。
def svnRepositories = { repositories -> svn(repositories[0].remote, repositories[0].local) { if(repositories.size == 1) return for(repos in repositories[1..-1]) { it / locations << 'hudson.scm.SubversionSCM_-ModuleLocation' { remote repos.remote local repos.local } } } } // ... scm { svnRepositories([ [remote: 'http://hoge/svn/trunk', local: 'hoge'], [remote: 'http://fuga/svn/trunk', local: 'fuga'], [remote: 'http://baz/svn/trunk', local: 'baz'], ]) }
Groovy的にいいコードなのかどうかわかりませんが、適当に上記のようにしてみました。
しかし、これを実行すると下記のように例外が発生してしまいます。
Exception in thread "main" groovy.lang.MissingMethodException: No signature of method: sample.svn() is applicable for argument types: (java.lang.String, java.lang.String, sample$_run_closure1_closure3) values: [http://hoge/svn/trunk, hoge, sample$_run_closure1_closure3@279fedbd] Possible solutions: run(), run(), any(), find(), wait(), dump() at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:55)
まあ当然といえば当然なのですが、GroovyのDSLと呼ばれているものは、DSLを書く場所に実行時、あらかじめコンテキストオブジェクトがはりついており、コンテキストオブジェクト上のメソッド―上記だとsvn(...)
を呼ぶことによって実装しているわけですね。
ところが外部関数にいくと関数呼び出し元のコンテキストもへったくれもない。見えないというわけです。さあて困った。
なので、下記の苦しい呪文をsvnRepositories([...])
の前に挟みます。
scm { svnRepositories.delegate = delegate // 苦しい呪文 svnRepositories([ [remote: 'http://hoge/svn/trunk', local: 'hoge'], [remote: 'http://fuga/svn/trunk', local: 'fuga'], [remote: 'http://baz/svn/trunk', local: 'baz'], ]) }
この苦しい呪文より、うまくいくようになります。同様にDSL内で関数くくりだしをした場合は上記呪文を挟めばいけます。
(delegate
でコンテキストオブジェクトを参照し、さらに関数の同名のプロパティにあらかじめ設定することで、その関数内のコンテキストオブジェクトを書き換えることができるので、関数呼び出し元のコンテキストオブジェクトを伝播させてやっているわけですね。正直Groovyはまったく詳しくないので、厳密にどうなっているのか知りませんが。)
WindowsでEmacsのClojure開発環境を立ち上げる
Windowsで最低限のEmacs+Clojureの開発環境を立ち上げる手順を書いてみました。
対象読者は、あやふやです。なんとなくEmacsもClojureもやったことがある、ぐらいな感じ。
ちなみに全般的にWindowsでこの手のUnix世界に塗れたものを開発するのは地獄なので、LinuxやOS Xという選択肢を選べる人は素直にそちらを選んだほうがいいでしょう。
まずはChocolateyを入れる
今や常識、Windowsのパッケージ管理ツール、Chocolatey(https://chocolatey.org/)を入れましょう。
そこまでこだわりのないパッケージであればChocolatey経由でインストールしたものでまかなえます。
基本的なツールを入れる
choco install curl wget
MsysGit(https://msysgit.github.io/)もChocolatey経由じゃなくてもいいですが、入れておきましょう。Chocolateyでは以下。
choco install git
Clojureの実行環境を作る
http://leiningen-win-installer.djpowell.net/ というのもあるみたいですが、使いません。
まずはJDK8をインストール。
choco install jdk8
次にLeiningen(Clojureのビルド兼実行ツール)をインストール。leiningenのインストール時、お呼びでないJDK7も入れようとするので -ignoreDependencies
を指定しておきます。
choco install lein -ignoreDependencies
このままだと多分Leiningenが古いので、仕上げに下記コマンドを打っておきましょう。
lein upgrade lein self-install
JDKもLeiningen自分で入れるんじゃーい!と思った方は、OracleからJDKダウンロードしてインストールして、Leiningenも公式サイト(http://leiningen.org/)からlein.batをダウンロードしてパスを通せばOKです。
最後に下のコマンドでREPLに入れればOKです。
lein repl
Emacsを入れる
EmacsはEmacs24以上を使います。色々Windows用にビルドはあるのですが、それぞれ長所短所があり、決定打というものがありません。
- 公式 - https://ftp.gnu.org/gnu/emacs/windows/
- Chocolatey -
choco install emacs
- Gnupack - http://sourceforge.jp/projects/gnupack/
- NTEmacs - http://cha.la.coocan.jp/doc/NTEmacs.html
- emacs-w64 - http://emacsbinw64.sourceforge.net/
今回はemacs-w64(Windows x64用のEmacs25)を使いたいと思います。おそらく、日本語入力は辛いと思うので日本語は使わない想定で… (改行コードの扱いもあやしい気がするが…)
普通に解凍して、 bin
フォルダにパスを通します。起動時は runemacs.exe
を使うといいでしょう。
EmacsでClojureを開発できる環境を作る(Caskを入れる)
Emacsといえばその拡張性の高さが有名ですが、拡張性が高いだけで最近までいい拡張の管理方法がありませんでした。
しかし今どきはCask(https://github.com/cask/cask)というツールがあり、これにパッケージ依存性を書いたファイルを食べさせると自動でEmacsにパッケージをインストールしてくれます。
基本的にCaskはUnix上じゃないとどうも怪しい動きをしているのですが、Windowsで使えないことはありません。インストールしてみましょう。
まず、Caskの動作のためにChocolateyでPython2を入れます。残念ながらPython3は不可です。
choco install python2
(Python3と同居させたい人には、pyenvというものがWindows以外ならありますが、 https://github.com/yyuu/pyenv/issues/62、Windowsだとどうすればいいんでしょうね…)
次に公式サイトに載っているコマンド(http://cask.readthedocs.org/en/latest/guide/installation.html#manual-installation)でCaskをインストールします。
curl -fsSL https://raw.githubusercontent.com/cask/cask/master/go | python
上記コマンドで %userprofile%\.cask\bin
に cask
および cask.bat
が用意されます。( %HOME%
変数を定義している人は %userprofile%
のかわりに、そこを探しましょう) この bin
フォルダにパスを通しておきましょう。
その状態で、 %userprofile%\.emacs.d
フォルダ(なければ作成)上で下記コマンドを実行し、Caskの初期ファイルを作成します。
cask init
上記コマンドで出来た Cask
ファイルに下記内容を追記しましょう。Emacsでcider(Clojureの開発用)とparedit(Lispのカッコ編集用)という拡張に依存するという内容です。
(depends-on "cider") (depends-on "paredit")
そのまま同じフォルダでCaskに依存している拡張を .emacs.d
フォルダ内にインストールさせます。
cask install
なんかこのコマンド、Windows上だと一瞬で終了しちゃいますが、非同期で結果がバカバカ吐かれるので、ちょっと待ちましょう。
(この際、Emacs24から使えるGnuTLSのサポートが無いEmacs(公式配布とか)を使っていると、Caskに文句言われます。)
上記インストールが終わったら、今度は %userprofile%\.emacs.d\init.el
(Emacsが起動時に読むユーザーカスタマイズ用のファイル) に下記内容を追記します。
(add-to-list 'load-path "~/.cask") (require 'cask) (cask-initialize) (show-paren-mode 1) (require 'paredit) (add-hook 'emacs-lisp-mode-hook #'enable-paredit-mode) (require 'cider) (add-hook 'clojure-mode-hook #'enable-paredit-mode) (add-hook 'cider-repl-mode-hook #'enable-paredit-mode)
追記が終わったら、 runemacs.exe
を実行してみましょう。エラーなく起動すればOKです。
試しに、Clojureのプロジェクトを作ってみる
簡単なClojureのプロジェクトを作成してREPLを開いてみましょう。
どっか作業用のフォルダ上で、下記コマンドを打ち、Clojure開発用の空のプロジェクト(my-test
)を作成します。
lein new my-test
できた my-test
フォルダ内の project.clj
ファイルを開いて、 C-c M-j
キーを押せばプロジェクトに対応したClojureのREPLを立ち上げることができます。
src
フォルダの中にひな形のソースファイルが入っているので C-x C-e
(カーソルの前の式を評価) などを使ってREPLに投げて遊んだりしましょう。
EmacsのCIDERとLeiningenのCIDERのバージョンが合わないエラー
EmacsのCaskで入れたEmacs側のCIDERのバージョンと、LeiningenがダウンロードしたJava側のCIDER(cider-nrepl)のJARのバージョンがあってねえよと文句言われることが多々あると思います。
CIDER's version (0.8.2-snapshot) does not match cider-nrepl's version (0.8.1)
ちゃんとLeiningenが持ってくるJava側のcider-nreplのバージョンをEmacsのCIDERにあわせてあげましょう。
LeiningenのユーザごとのプロファイルにCIDERのバージョンを指定してあげます。下記のいずれかのファイルを編集します。
%LEIN_HOME%
を定義済みならば%LEIN_HOME%\profiles.clj
- それ以外の場合は、
%userprofile%\.lein\profiles.clj
( 多分%HOME%
は見てないかも )
ファイルに下記内容を書きます。
{:user {:plugins [[cider/cider-nrepl "0.8.2-SNAPSHOT"]]}} ; ←ここをEmacs側のバージョンにあうように変更しましょう!
ちょっと管理上よろしくないですが、上記のプラグインの設定を各プロジェクトの project.clj
の :plugins
に追記してもいいです。
試しに Clojure+ClojureScript (+Facebook ReactのラッパーのOm) のプロジェクトを作ってみる
ちょっとだけ背伸びをしてみましょう。プロジェクトテンプレートとして、plexus/chestnut(https://github.com/plexus/chestnut)を使います。
どっか作業用のフォルダ上で、下記コマンドを打ち、ClojureScriptを開発する雛形のプロジェクト(my-test
)を作成します。
lein new chestnut my-test
その中の project.clj
ファイルをEmacsで開いて C-c M-j
キーを押してプロジェクトに対応したClojureのREPLを立ち上げます。
REPLはデフォルトの名前空間( my-test.server
)で起動していると思いますが、そこで (run)
関数を起動してWebサーバを起動します。
; CIDER 0.8.2snapshot (package: 20141130.803) (Java 1.8.0_25, Clojure 1.6.0, nREPL 0.2.6) my-test.server> (run) Starting figwheel. Starting web server on port 10555 . #<Server org.eclipse.jetty.server.Server@e7a73f8> my-test.server> Compiling ClojureScript. Figwheel: Starting server at http://localhost:3449 Figwheel: Serving files from '(dev-resources|resources)/public' Compiling "resources/public/js/app.js" from ("src/cljs" "env/dev/cljs")... WARNING: Use of undeclared Var cljs.core.async/do-alts at line 62 file:/C:/Users/owner/.m2/repository/org/clojure/core.async/0.1.278.0-76b25b-alpha/core.async-0.1.278.0-76b25b-alpha.jar!/cljs/core/async/impl/ioc_helpers.cljs WARNING: Bad method signature in protocol implementation, impl/Handler does not declare method called lock-id at line 214 file:/C:/Users/owner/.m2/repository/org/clojure/core.async/0.1.278.0-76b25b-alpha/core.async-0.1.278.0-76b25b-alpha.jar!/cljs/core/async.cljs WARNING: Use of undeclared Var cljs.core.async.impl.protocols/lock-id at line 217 file:/C:/Users/owner/.m2/repository/org/clojure/core.async/0.1.278.0-76b25b-alpha/core.async-0.1.278.0-76b25b-alpha.jar!/cljs/core/async.cljs WARNING: Bad method signature in protocol implementation, impl/Handler does not declare method called lock-id at line 214 resources\public\js\out\cljs\core\async.cljs WARNING: Use of undeclared Var cljs.core.async.impl.protocols/lock-id at line 217 resources\public\js\out\cljs\core\async.cljs Successfully compiled "resources/public/js/app.js" in 13.856 seconds. notifying browser that file changed: /js/app.js notifying browser that file changed: /js/out/goog/deps.js notifying browser that file changed: /js/out/my_test/dev.js notifying browser that file changed: /js/out/my_test/core.js
上記出力が終わったら http://localhost:10555 を見ればClojureで立ち上がったWebサーバ上が見えると思います。この時のサーバのページの表示はClojureScriptで行われています。
今度はClojureScriptをいじってみましょう。REPL上で
(browser-repl)
でClojureScriptのREPLを起動してみましょう。これにより cljs.user
名前空間に切り替わるので、REPL上で
(js/alert "Hello!")
とか打ってみましょう。ちゃんとブラウザ上でalertが出たのが見えるでしょうか。
つづいて、Om(React)らしく、JS上のモデルを書き換えてページのDOMが書き換わるのを観察してみましょう。
src/cljs/my_test/core.cljs
を開いて参考にしつつ、ファイルに下記式を書いて評価( C-x C-e
)してみましょう。
(reset! app-state {:text "Goodbye Chestnut!"})
シームレスにブラウザ上のDOMがリレンダされたのがわかるでしょうか。
このまま Omのチュートリアル(https://github.com/swannodette/om/wiki/Basic-Tutorial)にすすんでみましょう。
Jenkinsのビルドエグゼキュータ(ビルド状況)をREST APIで取得したい
個人的なメモ。
Jenkinsのビルドエグゼキュータは、たとえば https://ci.jenkins-ci.org/ajaxExecutors のように /ajaxExecutors
経由でJenkinsのウェブサイトに定期的に描画されているものですが、これはHTMLのスニペットを定期的に配信していることで実現していて、たとえばTwitter BootstrapのカッコイイUIでこのビルドエグゼキュータを描画したいといった場合、HTMLのスニペットの定期取得では難しいこともあるでしょう。
(以下はそのスニペット)
<div id="executors" class="container-fluid pane-frame"><div class="row"><div class="col-xs-24 pane-header"><a title="collapse" class="collapse" href="/toggleCollapse?paneId=executors"><img style="width: 16px; height: 16px; " alt="collapse" class="icon-collapse icon-sm" src="/static/8e43c810/images/16x16/collapse.png" /></a><a href='/computer/'>ビルド実行状態</a></div></div><div class="row pane-content"><table class="pane "><colgroup><col width="30" /><col width="200*" /> <!-- 以下略 -->
ということで、Jenkinsに備え付けられているREST APIから、ビルドエグゼキュータの情報を取ろうと考えつくわけですが、どこを取ればいいかわからない。
まあ正解からいうと、 https://ci.jenkins-ci.org/ (公式Jenkins)を例にとったとして、 https://ci.jenkins-ci.org/computer/api/json?pretty=true を取ればいいんですが、エグゼキュータ描画に必要な情報が入っていない。( ?pretty=true
は実装時には要らないです)
なので、より深い情報を取るために https://ci.jenkins-ci.org/computer/api/json?pretty=true&depth=2 などと depth=2
を指定すりゃあいいんですが、
- 余計な情報つきすぎ
- ノードをON/OFFしていると例外をBeanからシリアライズできないとかで500出る(バグ?)
といった苦難にあうので、きちんと取るべき情報を指定してあげましょう。
https://ci.jenkins-ci.org/api/ に書いてあるんですが、RESTで取る際のデータはある程度選べます。
今回のビルドエグゼキュータを自分で描画したいといった場合、JSON上の下記の構造の部分だけデータが取れれば、まあだいたい僕の欲望は満たせました。
- computer (複数) スレーブごとに存在
- displayName スレーブの名前
- executors (複数) スレーブのエグゼキュータ(キュー)ごとに存在
- progress ジョブの進捗%
- idle 空いているか?
- currentExecutable 実行中ビルドの詳細
- estimatedDuration ジョブの完了見積もりミリ秒
- fullDisplayName ビルドのフル名
- url ビルドのURL
上記のデータだけを取る場合は tree
パラメータを駆使して、 https://ci.jenkins-ci.org/computer/api/json?pretty=true&tree=computer[displayName,executors[progress,idle,currentExecutable[estimatedDuration,fullDisplayName,url]]] とすればよいです。前までつけていた depth
は不要になります。
下記に(Lisp並みに)わかりやすく分解したURLを置いておきます。
https://ci.jenkins-ci.org/computer/api/json?pretty=true&tree= computer[ displayName, executors[ progress, idle, currentExecutable[ estimatedDuration, fullDisplayName, url]]]
上記のURLで下記のようなJSONがとれるので、後は定期的に再取得しなおしたり、再取得するまでの間は estimatedDuration
と progress
からビルドの予定時間を算出してスムーズにプログレスバーでビルド進捗率を描画するなりすればいいと思います。
{ "computer" : [ { "displayName" : "master", "executors" : [ { "currentExecutable" : { "estimatedDuration" : 8807897, "fullDisplayName" : "infra_update_center_v3 #57", "url" : "http://ci.jenkins-ci.org/job/infra_update_center_v3/57/" }, "idle" : false, "progress" : 81 }, { "currentExecutable" : null, "idle" : true, "progress" : -1 } ] }, { "displayName" : "celery", "executors" : [ { "currentExecutable" : null, "idle" : true, "progress" : -1 }, { "currentExecutable" : null, "idle" : true, "progress" : -1 } ] // 以下略
ちなみになんでこんなことを僕が調べているかというと、開発サーバ上で動くJenkins数台などを巻き込んだSPAをAngularJSとか使って作っているからです。