M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

辻村深月『盲目的な恋と友情』

盲目的な恋と友情

盲目的な恋と友情

「恋」に溺れる主人公と、彼女のそばで「友情」に溺れる友人のお話。あるいは、恋や友情による社会的結びつきの実践が提供してくれる「地位」(○○の恋人、○○の夫・妻、○○の親友など)と、それがもたらしてくれるであろう「利潤」の追求に、意識的にも無意識的にも囚われた主人公たちのお話です。自分の文化資本に照らしてふさわしい相手、自分のコンプレックスを解消してくれる相手、自分の破綻・没落しつつある出自の代替となり、自身の社会的上昇を実現してくれる相手などなど。それらを探し求めるゲームは、経済的安定の追求とくらべて、本質からして終わりのない、より疲労困憊させられるゲームです。柚木麻子の『けむたい後輩』などもそうですが、こういう差異化・卓越化の実践のお話は重たいですがきらいではありません。

空のAssemblyKeyFile・AssemblyKeyName属性の罠

最近仕事でひとさまがC#で作成したライブラリをリファクタリングする機会がありました。既存コードに対してもそうですが、とくにリファクタリングで追加したコードに対してはきっちりUTを実施する方針です。

そこで既存のプロジェクトに加えて同一ソリューション内にNUnitを使用したテスト用プロジェクトを加えました:

─Sample.Solution
  ├Sample.Solution.sln
  ├...
  ├Sample.Project
  │  ├Sample.Project.csproj
  │  ├...
  │  └Properties
  │    └AssemblyInfo.cs
  └Test.Sample.Project
      ├Test.Sample.Project.csproj
      ├...
      └...

そして既存プロジェクトのAssembyInfo.csに、アセンブリに対するInternalsVisibleTo属性のコードを追加しました:

[assembly: InternalsVisibleTo("Test.Sample.Project")]

APIの公開メンバーでなくとも、UTを実施したいということは当然ありえます。私見ですが、公開メンバーが限定されており、内部実装がきっちり隠蔽されたAPIほど、そうした需要は大きくなる傾向すらあるのではないかと考えています。

このようなときテスト対象のアセンブリに対して上記の属性指定を行い、テストコードが含まれるアセンブリ名を指定しておくと、テスト対象アセンブリinternalアクセス修飾子が指定されたメンバーがテストコードアセンブリから見てpublicなものという扱いになります。これを「フレンドアセンブリ」といいます。

このソリューションはVisual Studio 2017では正常にビルドすることができました。ところが同じソリューションをVisual Studio 2013でオープンしてビルドしようとすると次のエラーが発生してしまいます:

エラー 47 フレンド アセンブリ参照 'Test.Sample.Project' は無効です。厳密な名前の署名つきアセンブリはその InternalsVisibleTo 宣言内で公開キーを指定しなければなりません。 X:\path\to\AssemblyInfo.cs 60 31 Sample.Project

たしかに厳密な名前の署名付きアセンブリであれば、InternalsVisibleTo属性によってメンバーを公開するのに、しかるべき手間暇が必要になります。

しかし今回作業対象としていたプロジェクトはいずれも厳密な名前の署名付きアセンブリではありません。それはVisual Studioのプロジェクトのプロパティ画面を見ても明らかでした。

あれこれ調べて見た後でわかったのはInternalsVisibleTo属性を追加する以前からAssemblyInfo.csに存在していた次の2つの属性でした:

[assembly: AssemblyKeyFile("")]
[assembly: AssemblyKeyName("")]

この属性たちを削除するとコンパイルは成功するようになりました。

この属性があるとVisual Studio 2013が内蔵するコンパイラをして、「このプロジェクトの成果物であるアセンブリは厳密な名前の署名付きアセンブリだから、メンバ公開対象のアセンブリ名だけをパラメータとしてとるInternalsVisibleTo属性は無効である」と判断させてしまうらしいのです。

つまりAssemblyKeyFile属性やAssemblyKeyName属性の実際の値を見てそれが厳密な名前の署名付きアセンブリであるかどうかを判断する前に、InternalsVisibleTo属性は無効と判断してしまうらしいのです。

Visual Studio 2013でも2017でも、プロジェクトを作成したときこれらの属性がAssemblyInfo.csに自動でコードされるなどということはありませんでした。おそらくリファクタリング対象のこのプロジェクトを過去に作成した開発者が、一度厳密な名前の署名付きアセンブリを作成しようとして結局やめたのでしょう。あとに残されたのは空文字列が指定された2つの属性だったわけです。

その後、例によってGoogle先生に質問してみると、ほんの僅かですがこの問題について触れたWebページが見つかりました。例えばこちらの2008年9月2日付の記事".NET 2.0 InternalsVisibleTo Attribute and Unsigned Assemblies"の記述:

It turns out that the compiler considers your assembly as "signed" if there is either an AssemblyKeyFile or AssemblyKeyName defined on the assembly, even though both of them are empty.
So, to be able to use AssemblyKeyName InternalsVisibleTo with unsigned assemblies, just remove AssemblyKeyFile or AssemblyKeyName attributes if you don't use them.

(AssemblyKeyFile属性もしくはAssemblyKeyName属性が当該アセンブリ上に定義されているとき、たとえその値が空文字列であっても、コンパイラはそのアセンブリを「署名付き」とみなしてしまう。そういうわけで、署名付きでないアセンブリでInternalsVisibleTo属性を使用する場合、AssemblyKeyFile属性とAssemblyKeyName属性を削除してやる必要がある。)

どうやらこの記事が執筆された2008年ころ(=Visual Studio 2008?)から、Visual Studio 2013のころまで、Visual Studio 2013に内臓されたC#コンパイラはそのような動作をしていたようです。そして少なくとも直近Visual Studio 2017の時点ではこの動作は変化しており、AssemblyKeyFile属性とAssemblyKeyName属性が空文字列とともに使用されている場合、当該アセンブリを厳密な名前の署名付きアセンブリとはみなさないようになっていたようです。

思いつく限りいろいろなキーワードでGoogle検索してみましたがこれ以上の情報は見つかりませんでした。

macOS x NuGet v4.1で*.csprojに基づくパッケージ作成

およそ半年前の記事「maxOS環境でNuGetパッケージをつくる」を投稿した段階では、macOS上でNuGetによるパッケージ作成をしようとしたとき、nuget pack *.csproj -Build ...コマンドを実行した時点でエラーとなる状態でした。*.nuspecファイルによるパッケージングはOKですが、*.csprojファイルに基づくそれはNGだったのです(CLRはMono。Xamarin Studioとともにインストールされたもの)。

しかし数日前に公式サイトから最新版であるv4.1をダウンロードしてパッケージングを試行してみたところこの問題がすでに解消していました。

続く説明は、ターミナルでmonoコマンドが実行でき、かつ、v4.1のnuget.exeをダウンロード済みで、mono (nuget.exeのパス) (NuGetのコマンドライン)という形式で実行できる前提で進めます。

*.nuspectファイルに基づくパッケージング

例えば次のようなディレクトリ構成になっているとき、nuget pack Sample.Project.nuspecコマンドを実行すると、パッケージ(*.nupkgファイル)に封入されるNuGetパッケージ情報は純粋に*.nuspecファイルに記述された情報に基づいたものになります。*.nuspecファイルの内容にはNuGetコマンドを実行する前にプロジェクトの最新の情報を手作業で反映させておく必要があります:

─Sample.Solution
  ├Sample.Solution.sln
  ├...
  └Sample.Project
    ├Sample.Project.csproj
    ├Sample.Project.nuspec
    ├Properties
    │├...
    │└AssemblyInfo.cs
    ├...
    └bin
      ├Debug
      └Release
        ├...
        ├Sample.Project.dll
        └Sample.Project.xml

*.csprojファイルに基づくパッケージング

一方、*.nuspecファイルの内容が次のようなプレースホルダを含むものである場合、nuget pack Sample.Project.csproj -Build ...コマンドを実行すると、まずは*.csprojファイルの情報に基づきデフォルトのビルドが実行されて、成果物が所定のディレクトリに生成されます(コマンドライン...の部分には適宜オプションの引数を指定):

<?xml version="1.0"?>
<package >
  <metadata>
    <id>$id$</id>
    <version>$version$</version>
    <authors>$author$</authors>
    <description>$description$</description>
  </metadata>
</package>

NuGetはこの成果物の中のアセンブリの情報を読み取って、*.nuspecファイルの内容をプレースホルダ部分を補完した上でパッケージに封入します。「アセンブリの情報」は一般にはAssemblyInfo.csファイルに記述する[AssemblyName("Sample.Project")]などの各種の属性(Attribute)により指定されるものです。

これらの属性とプレースホルダの対応関係はMicrosoft社のドキュメント.nuspec referenceに示されています。

*.csprojのビルド設定と*.nuspecのビルド設定の関係

ところでこの*.csprojファイルによるパッケージング──正確に言えば「コマンドラインに*.csprojファイルを指定することで、*.csprojファイルと*.nupkgファイルとアセンブリ情報の定義に基づき行うパッケージング」──では、おおよそ次のような順序で処理が進められているようです:

  1. *.csprojファイルの情報に基づきデフォルトのビルドを実行
  2. *.nuspecファイルをメモリ上にロード
  3. 最前のビルド成果物に含まれるアセンブリからメタ情報を抽出
  4. メモリ上の*.nuspecファイル内容にメタ情報をマージ→パッケージZIPに追加
  5. *.nuspecファイルの<files/>タグの内容に基づきファイル特定→パッケージZIPに追加
  6. 最前のビルド成果物をコピーしパッケージZIPに追加

この最後のビルド成果物のパッケージ封入の際、*.csprojファイルで指定されたビルド・ターゲットの情報に基づき、ZIP内のパスも自動で調整されます。例えば.NET Framework v4.5.2をターゲットとしたビルド成果物の場合、ZIP内のパスはlib/net452/Sample.Project.dllのようになります。

そして*.nuspecファイルの<files/>タグの内容に基づきすでに同名のパスにファイルが存在している場合は、「警告: パッケージには既にファイル 'lib/net452/Sample.Project.dll' が含まれているため、ファイル 'Sample.Solution/Sample.Project/bin/Release/Sample.Project.dll' は追加されません」というような警告ログを出力します。

また*.nuspecファイルに<files/>タグの記述がない場合は「最前のビルド成果物をコピーしパッケージZIPに追加」しか行われないので、その結果生成されるパッケージも*.csprojファイルでターゲット指定したフレームワーク・バージョンにしか対応しないものになります。