FsAdvent2014


F# Project Scaffoldを使ってプロジェクトを作成する

この記事はF# Advent Calendar 2014の4日目です。

前回はotfさんによるApiaryProviderで大マッシュアップ時代を生き抜くでした。

さてここではタイトルの通り、F# Project Scaffoldを使って プロジェクトをセットアップしてみましたという話をしたいと思います。

F# Project Scaffold を入手する

F# Project Scaffoldのページによると、 まずはプロジェクトを複製するか、ProjectScaffoldレポジトリを 各々のレポジトリにコピーしなさいと書かれています。 しかし今回は手っ取り早くGitHubのページから ZIPファイルをダウンロードして、 それを展開して使います。

ちなみに想定する環境はWindowsです。 Monoな環境はわからないので誰か他の人にお任せします。

展開先ですが、ディレクトリの浅い位置 に展開してください。 たとえばC:\ghMyProjectというフォルダ名で展開します。 C:\gh\MyProject\build.cmdといった具合にファイルが配置されることとします。

もしディレクトリの深い位置に展開してしまうとプロジェクトのビルドに失敗します。 具体的にはNuGetのパッケージを取得後に展開する段階で

パスの一部が見つかりません

という何を言っているのかわからないエラーに遭遇することになります。 最近のNuGetパッケージはそういう作りなのか、 何故かパッケージ(nupkgでしたっけ?実際は単なるzipファイル)の中に URLエンコードされたらしいやたらと長いフォルダ名があるのが原因に見えますが果たして。

プロジェクトの初期設定

展開したファイルを使ってプロジェクトの初期設定をします。 既にこの時点で展開先のフォルダをエクスプローラーで開いていると思うので、 アドレスバーにcmdと入力してエンターを押してコマンドプロンプトを立ち上げます。 エクスプローラーが既に居なくなっている場合には 普通にコマンドプロンプトを立ち上げて展開先のフォルダに移動します。

そしておもむろに以下のコマンドを実行します:

1: 
build.cmd

そうするとこれから作成するプロジェクトに関していくつか質問されるので、 それに答えて初期設定をします。 質問内容と対訳、入力例は以下の通りです:

質問(原文)

質問(対訳)

入力例

Project Name (used for solution/project files)

プロジェクト名(ソリューションとプロジェクトファイル名に使用されます)

MyProj

Summary (a short description)

要約(短い説明文)

My first project.

Description (longer description used by NuGet)

説明(NuGetで使われる長めの説明文)

My first project created with using F# Project Scaffold.

Author

作者

Anonymous

Tags (separated by spaces)

タグ(半角スペース区切り)

Sample F# Scaffold

Github User or Organization

GitHubのユーザ名または組織名

anonymous

Github Project Name (leave blank to use Project Name)

GitHubのプロジェクト名(空の場合はプロジェクト名)

(空のまま)

GitHubのプロジェクト名まで入力が終わるとそのままプロジェクトのビルドが走ります。 あとはこのままソリューションファイルを開いてコードを作成すればいいんですが、 今回はさらに英語と日本語のドキュメントを用意するところまで頑張ってみたいと思います。

日本語ドキュメントの追加

2015/04/22 追記

build.cmd AddLangDocs コマンドに不具合があり、各言語用のテンプレートファイルが 正しく適用されていないことがわかりました。 bleisさん報告ありがとうございます。

2015/4/22現在の最新版では修正されていますので、 これから使い始める場合には問題無いはずです。

既に使い始めている場合にはdocs/tools/generate.fsxを以下と同じように 修正すればいいはずです:

https://github.com/yukitos/ProjectScaffold/commit/5a2fdb4b72f5b2c704e8b85fabf5f0eb4fd89831

build.cmd AddLangDocs <言語名>コマンドでドキュメントを追加した場合、 残る作業としては docs\tools\templates\<言語名>\template.cshtmlのテンプレートファイルの編集と、 docs\content\<言語名>\ 以下のファイルを用意するだけです。

なお<言語名>としては2文字または3文字の言語名を指定します(例:「ja」「fr」「de」)。


2014/12/6 追記

bleisさんは言いました:

Chocolateyみたいに導入が簡単だと、もっといい感じになるような気がします。

Chocolateyがどのくらい簡単かはまだ知らないんですが、 言語別のドキュメントを簡単に用意できるコマンドを F# Project Scaffoldに追加しました。

build.cmd AddLangDocs ja

とするだけで、日本語(ja)用のテンプレートと初期ドキュメントを用意できるようになってます。

なにはともあれ、日本語ドキュメントの対象になるfsxファイルを用意します。

今更の説明ですが、F# Project Scaffold は 標準でドキュメントの作成機能が備わっていて、 docs/content フォルダ以下に.fsxまたは.mdファイルを置いておくと F# Formatting の力を借りてこれらのファイルを htmlファイルに変換してくれます。 変換後のファイルはdocs/outputフォルダ以下に出力されます。 さらに、プロジェクト内のコードにXMLドキュメントコメントを追加しておけば、 作成したプロジェクト内のAPIリファレンスドキュメントも自動的に生成してくれます。

そういうわけなので、docs/content/index.fsxdocs/content/ja/index.fsx にコピーして、 中身もそれとわかるように少し書き換えておきます。

日本語ファイル用テンプレートの追加

次に日本語ファイル用のテンプレートが必要です。 既にあるものをそのまま使っても問題ないのですが、 昨今は多言語対応が求められる時代ですので、 元のテンプレートは英語用にして、 日本語用には別にきちんと用意しておくにこしたことはないでしょう。

docs/tools/templates/template.cshtmldocs/tools/templates/ja/template.cshtml にコピーします。

ここでもやはりそれとわかるようにファイルを編集しておくとよいです。

ドキュメント生成スクリプトの編集

テンプレートファイルを用意しただけでは誰もそれを使ってくれないので、 ドキュメント生成スクリプトを編集して、 日本語ファイルには日本語テンプレートが使われるようにします。

docs/tools/generate.fsx を以下のように書き換えます:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
38: 
39: 
40: 
41: 
42: 
43: 
44: 
45: 
46: 
47: 
48: 
49: 
50: 
51: 
(前略)

let docTemplate = formatting @@ "templates/docpage.cshtml"

// Where to look for *.csproj templates (in this order)
// Note that the key will be the directory name of language specific contents
// except "en" which is used as the default language.

// TODO: Add new entries below when you create more language specific documents.

let layoutRootsAll =
  dict [ ("en", [templates; formatting @@ "templates"
                 formatting @@ "templates/reference"])
         ("ja", [templates @@ "ja"; formatting @@ "templates"
                 formatting @@ "templates/reference"]) ]

// Copy static files and CSS + JS from F# Formatting
let copyFiles () =

(中略)

// Build API reference from XML comments
let buildReference () =
  CleanDir (output @@ "reference")
  let binaries =
    referenceBinaries
    |> List.map (fun lib-> bin @@ lib)
  // NOTE: Currently the API reference is available in English only.
  MetadataFormat.Generate
    ( binaries, output @@ "reference", layoutRootsAll.["en"], 
      parameters = ("root", root)::info,
      sourceRepo = githubLink @@ "tree/master",
      sourceFolder = __SOURCE_DIRECTORY__ @@ ".." @@ "..",
      publicOnly = true, libDirs = [bin] )

// Build documentation from `fsx` and `md` files in `docs/content`
let buildDocumentation () =
  let subdirs = Directory.EnumerateDirectories(content, "*", SearchOption.AllDirectories)
  for dir in Seq.append [content] subdirs do
    let sub = if dir.Length > content.Length then dir.Substring(content.Length + 1) else "."
    let langSpecificPath(lang, path:string) = path.Split('/', '\\') |> Array.exists(fun i -> i = lang)
    let layoutRoots =
        let key = layoutRootsAll.Keys |> Seq.tryFind (fun i -> langSpecificPath(i, dir))
        match key with
        | Some x -> layoutRootsAll.[x]
        | None -> layoutRootsAll.["en"] // "en" is the default language
    Literate.ProcessDirectory
      ( dir, docTemplate, output @@ sub, replacements = ("root", root)::info,
        layoutRoots = layoutRoots )

(後略)

元のファイルとの差分は主にL11-L15で定義されているlayoutRootsAllの追加です。 これはキーに言語名、値にテンプレートファイルを探す複数の場所を保持しています。 特にjaの方では1つめの値がtemplates @@ "ja"となっている点に注意してください。 このようにすることで、先ほど追加した日本語用のテンプレートファイルが 使われるようになります。

また、APIリファレンスは今のところ多言語出力出来ないので英語決め打ちにします(L30)。

L41のヘルパー関数はパスを区切り文字で分割した後、 その中に言語名が含まれている場合にはSomeを返します。 L43でlayoutRootsAll.Keysのいずれかが 現在処理中のファイルパスdirの一部になっているかどうか調べて、 一部になっている場合はその言語用のテンプレートを、 そうでなければ英語用のテンプレートを適用されるようにしています(L45-46)。

ちなみにF# Dataのgenerate.fsxではdir.Contains("ja")ならば 日本語テンプレートを使うようになっているのですが、 これだとたまたまフォルダ名にjaが入っているだけで日本語テンプレートが 当たってしまうので後々まずいことになりそうな気がします :)

言語ページ用のリンクをメニューに追加する

さてここまでで英語と日本語のページが作成できるようになったわけですが、 せっかく用意した日本語ページへのリンクも用意したいところです。

そこで言語毎のページへのリンクとして表示する国旗画像を追加して、 メニューバーにもリンクを追加します。

国旗画像はとりあえずF# Dataのものをぱちってコピーして

  • docs/files/img/en.png
  • docs/files/img/ja.png

に置きます。

そして各テンプレートファイルを変更します。

docs/tools/templates/template.cshtmldocs/tools/templates/ja/template.cshtml にあるul#menu以下にリンクを追加します:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
<li class="nav-header">
  <a href="@Root/ja/index.html" class="nflag"><img src="@Root/img/ja.png" /></a>
  <a href="@Root/index.html" class="nflag nflag2"><img src="@Root/img/en.png" /></a>
  @Properties["project-name"]
</li>

<li class="divider"></li>

これだけだと縦並びになってしまったりと都合が悪いので、 プロジェクト固有のCSSを追加して体裁を整えます。

プロジェクト固有のCSSファイルの追加

docs/files/content以下にプロジェクト固有のCSSファイルを追加します。

たとえばproject.cssファイルを以下の内容で追加して、 言語毎のページリンクを体裁よく表示されるようにします。

1: 
2: 
3: 
4: 
5: 
6: 
7: 
.nav-list > li > a.nflag {
  float:right;
  padding:0px;
}
.nav-list > li > a.nflag2 {
  margin-right:18px;
}

あわせてテンプレートファイルdocs/tools/templates/template.cshtmldocs/tools/templates/ja/template.cshtmlheadタグ内に linkタグを追加します。

1: 
<link type="text/css" rel="stylesheet" href="@Root/content/project.css" />

ビルドスクリプトの変更

以上で日英ドキュメントを作成する環境が整備できましたが、 このままビルドしてみて、docs/output 以下に出力されるドキュメントを開いてみても GitHub Pages用のURLで各リンクが生成されるため、CSSなども読み込まれずひどいことになります。

そこでローカル環境でドキュメントをチェックできるように プロジェクトのフォルダ直下にあるbuild.fsxを以下の通り編集します。

2014/12/6 追記

下記のGenerateHelpDebugターゲットはF# Project Scaffoldに取り込んでもらえたので build.fsxを編集する必要は無くなっています。

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
38: 
39: 
40: 
let generateHelp' fail debug =
    let args = if debug then ["--define:HELP"]
               else ["--define:RELEASE"; "--define:HELP"]
    if executeFSIWithArgs "docs/tools" "generate.fsx" args [] then
        traceImportant "Help generated"
    else
        if fail then
            failwith "generating help documentation failed"
        else
            traceImportant "generating help documentation failed"

let generateHelp fail =
    generateHelp' fail false

Target "GenerateHelp" (fun _ ->
    DeleteFile "docs/content/release-notes.md"    
    CopyFile "docs/content/" "RELEASE_NOTES.md"
    Rename "docs/content/release-notes.md" "docs/content/RELEASE_NOTES.md"

    DeleteFile "docs/content/license.md"
    CopyFile "docs/content/" "LICENSE.txt"
    Rename "docs/content/license.md" "docs/content/LICENSE.txt"

    generateHelp true
)

Target "GenerateHelpDebug" (fun _ ->
    DeleteFile "docs/content/release-notes.md"    
    CopyFile "docs/content/" "RELEASE_NOTES.md"
    Rename "docs/content/release-notes.md" "docs/content/RELEASE_NOTES.md"

    DeleteFile "docs/content/license.md"
    CopyFile "docs/content/" "LICENSE.txt"
    Rename "docs/content/license.md" "docs/content/LICENSE.txt"

    generateHelp' true true
)

"CleanDocs"
  ==> "GenerateHelpDebug"

変更箇所は以下の通りです:

  • 元のgenerateHelp関数の名前をgenerateHelp'に変更。 あわせてデバッグビルドかどうかを引数に渡せるようにした。
  • ビルドターゲットGenerateHelpDebugを追加。
  • GenerateHelpDebugを実行する場合には必ずCleanDocsを実行して、 一旦ドキュメントを全消去するようビルドチェインを作成

見ての通り、デバッグ時にはgenerate.fsx--define:RELEASEを指定せずに実行するようになりました。

そしてローカル用にドキュメントをビルドする場合、以下のようにターゲットを指定してbuild.cmdを実行します:

1: 
build.cmd GenerateHelpDebug

新しい言語用のドキュメントを追加する

英語と日本語以外のドキュメントを追加するために必要な手順は以下の通りです:

  1. docs/content/<lang> フォルダにファイルを用意する
  2. docs/tools/templates/<lang> フォルダに言語固有のテンプレートファイルを用意する
  3. docs/tools/generate.fsx にある layoutRootsAll の値を編集する

layoutRootsAll の値は

1: 
2: 
("<lang>", [templates @@ "<lang>"; formatting @@ "templates"
            formatting @@ "templates/reference"])

という形式になります。

ドキュメントをGitHub Pagesとして公開する

GitHub Pagesとしてドキュメントを公開するには、単にgh-pagesというブランチを作成して、 そのブランチへファイルをコミット&プッシュするだけです。

既にgitがインストールしてあって、git.exeがパスの通った場所に見つかる場合には

1: 
build.cmd ReleaseDocs

とするだけでgh-pagesブランチにdocs/output以下のファイルをコミットしてくれます。 ひょっとするとgh-pagesブランチも作成してくれるかもしれませんが試していません。

手元の環境ではgitがインストールされていなくて、 SourceTree しかなかったので、 この場合はターミナルを起動して

1: 
./build.sh ReleaseDocs

とすればGitHub Pagesにドキュメントをアップロードできます。お手軽です。

終わりに

お気づきかとは思いますが、以上のようにして作成したのがまさにこのページです。

F# Project Scaffoldを利用すれば プロジェクトのビルドからドキュメントの生成まで一揃いの環境が手軽に用意できます。 F#のプロジェクトを作成する場合には、GitHub上で プロジェクトを管理するかどうかに関わらず利用できますので 是非試してみてはいかがでしょうか。

あと上記で変更した多言語対応generate.fsxはたぶん便利なのではないかなあと思うので、 後ほどPRを投げてみようかなと目論んでいたりします。

次回はbleis-tiftさんよろしくお願いします。

val docTemplate : obj

Full name: index.docTemplate
val layoutRootsAll : System.Collections.Generic.IDictionary<string,obj list>

Full name: index.layoutRootsAll
val dict : keyValuePairs:seq<'Key * 'Value> -> System.Collections.Generic.IDictionary<'Key,'Value> (requires equality)

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.dict
val copyFiles : unit -> bool

Full name: index.copyFiles
val buildReference : (unit -> 'a)
val binaries : '_arg3 list
Multiple items
module List

from Microsoft.FSharp.Collections

--------------------
type List<'T> =
  | ( [] )
  | ( :: ) of Head: 'T * Tail: 'T list
  interface IEnumerable
  interface IEnumerable<'T>
  member Head : 'T
  member IsEmpty : bool
  member Item : index:int -> 'T with get
  member Length : int
  member Tail : 'T list
  static member Cons : head:'T * tail:'T list -> 'T list
  static member Empty : 'T list

Full name: Microsoft.FSharp.Collections.List<_>
val map : mapping:('T -> 'U) -> list:'T list -> 'U list

Full name: Microsoft.FSharp.Collections.List.map
val lib : 'a (requires member ( @@ ))
val buildDocumentation : (unit -> unit)
val subdirs : 'a (requires 'a :> seq<string>)
val dir : string
module Seq

from Microsoft.FSharp.Collections
val append : source1:seq<'T> -> source2:seq<'T> -> seq<'T>

Full name: Microsoft.FSharp.Collections.Seq.append
val sub : string
property System.String.Length: int
System.String.Substring(startIndex: int) : string
System.String.Substring(startIndex: int, length: int) : string
val langSpecificPath : (string * string -> bool)
val lang : string
val path : string
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = System.String

Full name: Microsoft.FSharp.Core.string
System.String.Split(params separator: char []) : string []
System.String.Split(separator: string [], options: System.StringSplitOptions) : string []
System.String.Split(separator: char [], options: System.StringSplitOptions) : string []
System.String.Split(separator: char [], count: int) : string []
System.String.Split(separator: string [], count: int, options: System.StringSplitOptions) : string []
System.String.Split(separator: char [], count: int, options: System.StringSplitOptions) : string []
module Array

from Microsoft.FSharp.Collections
val exists : predicate:('T -> bool) -> array:'T [] -> bool

Full name: Microsoft.FSharp.Collections.Array.exists
val i : string
val layoutRoots : obj list
val key : string option
property System.Collections.Generic.IDictionary.Keys: System.Collections.Generic.ICollection<string>
val tryFind : predicate:('T -> bool) -> source:seq<'T> -> 'T option

Full name: Microsoft.FSharp.Collections.Seq.tryFind
union case Option.Some: Value: 'T -> Option<'T>
val x : string
union case Option.None: Option<'T>
type 'T list = List<'T>

Full name: Microsoft.FSharp.Collections.list<_>
Multiple items
val float : value:'T -> float (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.float

--------------------
type float = System.Double

Full name: Microsoft.FSharp.Core.float

--------------------
type float<'Measure> = float

Full name: Microsoft.FSharp.Core.float<_>
val failwith : message:string -> 'T

Full name: Microsoft.FSharp.Core.Operators.failwith
F# Project
Fork me on GitHub