.nuspec は間違いの無いように書こう、もしくは MSBuild の詳細出力を確認する

実際にハマった事案。

NuGet パッケージを作成する際には、NuGet.exe に .nuspec というファイルを作成して渡すと、様々な情報をパッケージに埋め込むことができます。(プロジェクトファイルを利用しても一部なら可能)

そして、.nuspec にはそのパッケージが依存している対象を記述できる dependencies というセクションがあるのですが、もしもここに誤ったバージョン、開発時に参照したバージョンと異なるバージョンを記述したらどうなるでしょうか。例えばとあるパッケージの v2.0 アセンブリで開発したのに dependencies には「v1.0 以上」と書くとか。

まぁどうなるでしょうというか普通に誤った情報のままパッケージが出来るだけと言えばそうなんですが、これが場合によっては利用者側で思いがけない挙動をすることがあります。

何が起きる?

実際に踏んだ例は後述しますが、先に結論を書きます。

.nuspec の dependencies は、パッケージをインストールした際に依存アセンブリの有無判定およびインストールに利用しているのみで、ビルド時の参照解決には利用していません。ビルド時に参照するアセンブリは、.NET アセンブリ自身の中にメタデータとして保持している情報を利用しています

それを踏まえて、もしも参照しているパッケージが

メタデータ的参照バージョン > dependencies 的参照バージョン

という状態になっていた場合、利用側でビルド時の参照解決に失敗します。
そして、参照解決に失敗しているにも関わらずビルドは成功します。

これの何がまずいかというと、参照解決に失敗したことに気付かず、その失敗した(ただしビルドは成功している)プロジェクトをさらに別のプロジェクトで参照した場合、参照解決に失敗したアセンブリが出力フォルダにコピーされません

なので、実行時にアセンブリを見つけられず実行エラーが発生します。

分かりにくくて申し訳ないです。説明が難しいですね。。。

実際に踏んだ例

言葉だけだと訳がわからないので、実際に踏んだ例を踏まえてどうなるかを確認していきます。

利用するパッケージは Stripe.netJson.NET です。
利用していた Stripe.net の nuspec が間違っていました(ちょっと古いバージョンを使っていました。2015/01/27 現在最新の Ver では修正されています。念のため)。

Visual Studio を立ち上げて、適当にプロジェクトを 2 つ作りましょう。
ホント適当で良いんですが、今回は ConsoleApplication と ClassLibrary を作りました。

そして、

  • ClassLibrary は Stripe.net 2.3.3 を NuGet 参照
    • Stripe.net 2.3.3 は、.nuspec によると Json.NET 5.0.8 以上が必要です
    • Json.NET を事前にインストールしていない場合、NuGet の機能によって自動で Json.NET 5.0.8 が入ります
    • Json.NET 5.0.8 以上が必要 という事実を覚えておきましょう
  • ConsoleApplication は ClassLibrary を参照

します。

全て参照が終わると ↓ のようになります。

※分かり易くするため、不要な参照は削除しています。

f:id:kendik:20150127215757j:plain

それと、それぞれのプロジェクトに適当に参照アセンブリを利用する実装を書きましょう。本当に適当で良いです。↓ みたいに。

// ClassLibrary1#Class1.cs
namespace ClassLibrary1
{
    public class Class1
    {
        public void Method()
        {
            Stripe.Infrastructure.EpochTime.ConvertEpochToDateTime(1);
        }
    }
}

// ConsoleApllication#Program.cs
namespace ConsoleApplication113
{
    class Program
    {
        static void Main(string[] args)
        {
            new ClassLibrary1.Class1().Method();
        }
    }
}

この状態で ConsoleApplication をビルドして、bin フォルダを見てみましょう。
こうなります。

f:id:kendik:20150127215806j:plain

気付きますかね?Json.NET のアセンブリがありません。
ですので、当然 ConsoleApplication をどこかに発行しても Json.NET のアセンブリは付いてきません。
実行マシンのにインストールでもされていない限り、実行時に Json.NET が見つからずエラーになります。

なぜこうなってしまうのでしょうか。

答えは、ビルドの詳細なログを見ると分かります。

ビルドの詳細ログを出力するため、Visual Studio の [ツール] > [オプション] > [プロジェクトおよびソリューション] > [ビルド/実行] > [MSBuild プロジェクト ビルドの出力の詳細] を「詳細」に設定します。そして、再度ビルドをかけ出力ペインを見てみましょう。

全部出すと相当な量になるのでほんの一部だけ抜粋しますが

2> 依存関係 "Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed" です。
2> この参照を解決できませんでした。アセンブリ "Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed" が見つかりませんでした。アセンブリが間違いなくディスクに存在することを確認してください。 コードにこの参照が必要な場合、コンパイル エラーが発生する可能性があります。

と、このように Json.NET の参照解決に失敗していることがわかります。参照を解決できなければアセンブリが出力フォルダにないのもしょうがないですね。

て、ここでポイントなのが、なぜか Json.NET の Version=6.0.0.0 を参照していることです。
覚えていますか?.nuspec からの情報では Stripe.net 2.3.3 を利用するのに必要なのは Json.NET の 5.0.8 以上とのことなので、それに従ってアセンブリを検索してくれれば、 NuGet で Stripe.net をインストールした際に同時にインストールされた Json.NET を見つけられるはずなのです。

が、なぜか Version 6.0.0.0 を探しに行っています。

これは、Stripe.net 2.3.3 が Json.NET 6.0.0 を参照するよう[メタデータに情報を持っている]ためです。
試しに、Stripe.net.dll のアセンブリマニフェストを ildasm で見てみましょう。

f:id:kendik:20150127215814j:plain

Json.NET の 6.0.0.0 を参照していることがわかると思います。なんてことだ。。。それなら .nuspec にそう書いておいてくれよ。。。

まとめ

今回は単純な例でしたので問題の切り分けは簡単ですが、もっとたくさんの参照が存在して、依存関係が複雑になると解決も大変です。利用者に不要な苦労をさせないよう、.nuspec は正しく記述したいですね。

それと、MSBuild のログは実はかなり詳細に出力してくれるので、見てみると役に立ちます。
Visual Studio の規定の設定では「最小」のログしか出力されませんので、困ったときには設定を変えてみると何かわかるかもしれません。

でも参照解決できてないんだから、MSBuild もビルドエラーにしてほしいですよねぇ。。。