CubePDF 正式版とカスタム仮想プリンタの公開

本日、CubePDF 1.0.0 を 最初の正式版 としてリリースしました。この記事では、1.0.0 までの道のりの回顧と、同時リリースした有償によるカスタム仮想プリンタ CubePDF Customize の宣伝、もとい、狙いについて記載します。

CubePDF 1.0.0 のリリースまでの道のり

CubePDF メイン画面

CubePDF 1.0.0 の修正点は下記の通りです。

  • Ghostscript 9.27 のバージョンアップに対応しました。
  • Ghostscript エラー (-100) が発生する現象を改善しました。
  • CubePDF インストール時、登録されているプリンタ数がゼロの時に発生する不都合を修正しました。

CubePDF は 2010 年 7 月 7 日に初版である 0.9.0 β をリリースして、その歴史が始まりました。そこから既に 9 年の月日が経過しており、1.0.0RC1 から数えても 6 年半位となっています。Web 上で「いつまで RC なんだよ」との評判をちらほら見かける事もあり、私としても「それな」とは思っていまいた。

CubePDF は最初期の頃、仮想プリンタを実現するためのシステムライブラリとして RedMon を利用していましたが、いくつかの課題が浮上した結果、最終的に自前のものに換装と言う選択をしました。この時にリリースされたバージョンが 1.0.0RC1 であり、当初の予定では、しばらく様子を見た後に 1.0.0 正式版とするつもりでした。しかし、様子を見ている内に様々な問題が発覚あるいは新たに出現し、それらの対策を検討している内に機を逸して現在に至っています。

参考までに、RC のバージョン毎の主な課題は下記のような感じでした。この内、フォント埋め込み問題に関しては、残念ながら現在においても根本的な解決方法は見つかっておらず、いったん機能自体を取り下げると言う形になっています。

  • PDF へのフォント非埋め込み設定時の文字化け (RC1 ~ RC5)
  • Microsoft ストアアプリ (UWP) への対応 (RC8 ~ RC11)
  • 内部コードの大幅なリファクタリング (RC12)
  • Ghostscript で発生した脆弱性への対応 (RC13 ~ RC15)
  • 仮想プリンタのインストール処理の改善および汎用化 (RC16 ~ RC19)
CubePDF の開発に対するモチベーション

また、別の理由として、私自身の CubePDF の開発に対するモチベーションが一時期低下していた事も挙げられます。 2015 年に Windows 10 がリリースされましたが、この時に「Microsoft Print To PDF」と言う機能が Windows 10 の標準機能として盛り込まれました。これは PDF に変換する機能を仮想プリンタとして実現しているものだったので、「あれ?これもう CubePDF 要らなくない?」と言うのが率直な感想でした。

モチベーションを取り戻したのは、CubePDF 1.0.0RC12 をリリースした頃となります。一つは、Microsoft Print To PDF を調査していると、どうもプリンタドライバの処理として埋め込み画像に対して JPEG 圧縮を実施しているようで、これを回避するのは困難ではないか?(標準機能に対する優位性があるのではないか?)と言う感触を得たのがきっかけとなりました。また、現実の現象として Windows 10 のリリース後も CubePDF のダウンロード数は減少しておらず、このまま終わらすべきではないと言う意識に変わった事も大きかったと思います。

そのような紆余曲折がありましたが、ここ 2 年位の試行錯誤により、個人的にも「不都合はもうない……なんて事を言えるはずはないが、内部コードも開発当初に比べてかなり整理され、ユニットテストの力を借りる事によってデグレードを引き起こす可能性もかなり低減された」と思える程度には改善されてきたため、今回「正式版」を名乗る事を決断しました。

CI サービス上でのテスト結果の様子

有償版カスタム仮想プリンタ CubePDF Customize の提供開始

CubePDF と CubePDF Customize の比較図

今回のもう一つの大きな発表は、CubePDF で使用している仮想プリンタ部分のみを有償で提供する CubePDF Customize です。概要に関しては、CubePDF Customize の Web ページ を参照して頂くとして、ここでは CubePDF Customize の提供開始に至った個人的な思いについて記載します。

キューブ・ソフトでは、提供しているソフトウェアの多くのソースコードを cube-soft - GitHub にて公開しています。公開を決めた会社としてのきっかけは、CubePDF が Ghostscript 等のライブラリを利用している事によるものだったのですが、それとは別に 自分の書いたコードが公開できる と言う事が私自身にとって非常に強いモチベーションとして機能しています。しかし、(GPL ではない)ソースコードを公開する 会社として のモチベーションが弱い事は長年気になっていた部分であり、もう少し公開する事の意義を持たせたいとも感じていました。

この問題への打開策を模索していた時に、非常に参考となった記事がありました。

カスタマイズをするのはかなりしんどい。自社ではカスタマイズは受け付けていない。もし機能追加をする場合はオープンソースでの公開が前提となる。そのため「カスタマイズのサポート」を Sora を購入している顧客に対してのみ有償にて提供している。

(中略)

つまり、この製品のビジネスモデルこうだ。

  • カスタマイズを提供せずカスタマイズのテクニカルサポートを提供する
  • カスタマイズはせずオープンソースとして公開する
  • WebRTC SFU Sora を購入してもらっている顧客へのみ提供する

リアルタイムな音声や映像をバイナリ一つで配信できるのはとても便利だ。ただ、それを特定のハードに対応させる場合はカスタマイズが必要になる。さらにもともとリアルタイム配信の世界は難しい。

1から自分たちだけでカスタマイズするよりも、そもそも Momo を開発をしている会社と一緒にやっていったほうがいい。

クローズドソースとオープンソース – V – Medium

時雨堂の @voluntas さんの記事は、自分が開発を続ける、あるいは組織を継続させていく上で参考になるものが多く、よく読ませて頂いています。そして、自社ソフトウェアの クローズドソースとオープンソースの境界 を明確に認識できたきっかけとなったのが上記でした。

CubePDF は、以下の 3 種類の機能に大別されます。

  1. 仮想プリンタを構築するためのシステムライブラリ
  2. 仮想プリンタから渡されたデータを PDF 等に変換するためのアプリケーション
  3. これらの機能を連携させ、端末にインストールするためのアプリケーション

この内、2. の機能を完全に取り除き、1. と 3. のみを有償にて提供するのが CubePDF Customize となります。そして、2. の部分を ユーザ自身の手によって実装してもらう ために積極的に技術情報を公開していくのが、今回の大きな挑戦です。

CubePDF Customize に関する技術情報は、Cube.Vp.Docs を随時更新していく事により公開を続けます。また、この技術的サポートの一環として、CubePDF 1.0.0 では、CubePDF のメイン処理に当たる部分のみを抽出し NuGet パッケージとして公開する事も開始しました(Cube.Pdf.Converter)。例えば、CubePDF と同等の変換処理を行う最も簡単なサンプルコードは下記となります。

// using Cube.Pdf.Converter;
// using System.Reflection;

static void Main(string[] args)
{
    var settings = new SettingFolder(Assembly.GetExecutingAssembly());
    settings.Load();    // レジストリの設定をロード
    settings.Set(args); // 仮想プリンタからの引数を解析

    using (var facade = new Facade(settings)) facade.Invoke();
}

CubePDF Customize の概要を知るための最初のステップとしては Tutorial - Cube.Vp.Docs - GitHub を参照下さい。また、Cube.Pdf.Converter ライブラリに関しては、Cube.Pdf.Converter - Cube.Vp.Docs - GitHub に概要を記載しています。

仮想プリンタとして CubePDF が知名度を得ていくにつれて、ありがたい事に、これまで数多くのカスタマイズのご依頼を頂いてきました。しかし、それらの中には、金銭的、あるいは時間的な面で「本来であれば、アプリケーション部分は貴社の方で実装してもらった方が絶対良いはずなのだが……」と感じる事が多々あったのも事実です。今回の CubePDF Customize によって、私(弊社)とユーザの皆様、双方にとって良い形に向かえるようになる事を期待して、これからも開発を続けていきたいと思います。

CubeICE 0.9.1β

CubeICE 0.9.1β をリリースしました。0.9.1βでの修正点は下記の通りです。

未対応形式のファイルが指定された時のエラーメッセージを修正

CubeICE エラーメッセージ

CubeICE で未対応形式、または圧縮形式ではないファイルを指定した際のエラーメッセージを分かりやすい文言に修正しました。尚、上記のエラーメッセージが表示された場合、指定したファイルが圧縮ファイルではない可能性が高いと予想されますので、今一度、指定したファイル内容をご確認頂くようお願いいたします。

ファイル一覧の表示機能を完全に無効化できるように修正

ファイル一覧の表示機能

CubeICE には、圧縮ファイルにマウスカーソルを当てた時にファイル一覧を表示する機能があります。この機能は、CubeICE 設定の「詳細」タブ、「圧縮ファイルのツールチップにファイル一覧を表示する」の項目より有効・無効の設定が可能です。ただ、これまでのバージョンでは、無効時には該当のライブラリ(シェル拡張)をロードした上で無効時の、すなわち標準と同じ挙動を示すようにしていました。

0.9.1βでは、無効設定時にはシェル拡張用ライブラリの読み込み自体を無効化するように修正しました。尚、これに伴い、CubeICE 設定にて、該当設定を変更する際にも管理者権限が要求されるようになります。

その他の修正

その他の修正は下記の通りです。

  • 特定の条件でファイルの関連付け設定が機能しない不都合を修正しました。
  • タスクバーの進捗表示機能に関する不都合を修正しました。
  • パスワード入力画面で、特定の条件で OK ボタンが押せなくなる不都合を修正しました。
  • 7-Zip 19.00 の修正を適用しました。

Visual Studio 2019 と新 csproj への移行

2019 年 4 月 2 日、Visual Studio 2019 が正式版としてリリース (GA: General Available) されました。この記事では、 Visual Studio 2019 に関連する内容として、C# の新しいプロジェクト形式 (csproj) への移行について記載します。新 csproj 自体は Visual Studio 2017 の頃から利用可能でしたが、WinForms や WPF などのプロジェクトに対する Visual Studio 側の対応も 2019 で進んだようなので、移行するには良いタイミングではないでしょうか。

新 csproj の基本的な構成

新しい csproj 形式では、システム側が既定値を設ける事により、多くの項目が省略可能となりました。これは、Visual Studio による自動生成・更新を前提にした複雑なものから、手動による更新も想定した簡素なものへの転換と言う感じで、特にバージョン管理などの観点から見ると非常に楽になったように思います。

以下に、新 csproj において省略しない方が良い(多くの場合、初期設定とは異なる)ものを記述します。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net45</TargetFramework>
    <LangVersion>latest</LangVersion>
    <Platforms>AnyCPU;x86;x64</Platforms>
    <Optimize>true</Optimize>
    <EnableDefaultNoneItems>false</EnableDefaultNoneItems>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <DefineConstants Condition=" '$(Configuration)' == 'Debug' ">DEBUG;TRACE</DefineConstants>
  </PropertyGroup>
</Project>

TargetFramework には net45, netcoreapp3.0, netstandard2.0 などの文字列を指定します(参考:Target Frameworks Reference for NuGet)。この項目は、旧 csproj の TargetFrameworkVersion から置き換わったものです。また、新 csproj 形式では TargetFrameworks(複数形)を代わりに用いる事で、複数の TargetFramework を ";" (semicolon) で区切って列挙する事もできるようです。

LangVersion には C# のバージョンを指定します(参考:Select the C# language version)。具体的なバージョン番号を指定する事もできますが、多くの場合においては、latest または latestMajor のどちらかだと思います。Visual Studio 2019 においては LangVersion の初期設定は latest なので、この項目は省略しても問題ありません。ただ、AppVeyor 等で継続的インテグレーション (CI: Continuous Integration) を実行する際に意図しない挙動をいくつか確認したため、明記する事にしています。

Platforms は Visual Studio 上で x86 や x64 を選択可能にしたい時に明記します。C#/.NET では多くの場合において AnyCPU で良いと思いますが、Unmanaged なライブラリとの兼ね合いでプラットフォームを固定したい場合などに設定します。

GenerateDocumentationFile は、XML Documentation Comments に従って記述されたコメントに基づいて XML ファイルを生成するかどうかを決定します。尚、XML ファイル自体は後述する DocumentationFile に出力パスを記述すれば、この項目がなくても生成されます。しかし、その場合には生成された XML ファイルが NuGet パッケージ (*.nupkg) に含まれないようです。NuGet パッケージにも含める場合、この項目を true で明記する必要があります。

DefineConstants は必要なシンボルを記述するための項目ですが、省略時には TRACE のみが設定されるようです。多くの場合、Debug モード時には TRACE に加えて DEBUG シンボルの存在も想定されているので、それを明記しています。

既定の Include に関する設定

新 csproj では、ソースファイル等に関する記述をできるだけ省略可能にするために、既定では以下の規則で自動的に Include される事になっています(参考:Default compilation includes in .NET Core projects)。

Element Include Remove
Compile **/*.cs N/A
EmbeddedResource **/*.resx N/A
None **/* **/*.cs; **/*.resx

しかし、これらの Include 規則は邪魔になる事もあるため、無効にする方法もいくつか提供されています。具体的には、上記の全ての規則を無効にするには EnableDefaultItems を、Compile および None の項目のみを無効にするにはそれぞれ EnableDefaultCompileItems, EnableDefaultNoneItems を false で記述します(EnableDefaultEmbeddedResourceItems が存在するかどうかは不明)。

個人的には None の Include 規則には不安に感じる所もあるため、EnableDefaultNoneItems を false に設定した上で自力で明記する事にしています。

NuGet パッケージに関する設定

新 csproj では、従来 AssemblyInfo.cs および *.nuspec に記述されていた内容が全て csproj に統合されました。生成されるアセンブリのメタ情報、および NuGet パッケージに関する項目は下記の通りです。

<PropertyGroup>
  <Version>1.0.0</Version>
  <Authors>clown;cube-soft</Authors>
  <Company>CubeSoft</Company>
  <Description>Some description.</Description>
  <Copyright>Copyright © 2010 CubeSoft, Inc.</Copyright>
  <PackageTags>Ta1;Tag2;Tag3</PackageTags>
  <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
  <PackageProjectUrl>https://github.com/cube-soft/$(AssemblyName)</PackageProjectUrl>
  <PackageIconUrl>https://github.com/cube-soft/$(AssemblyName)/blob/master/Icon.png?raw=true</PackageIconUrl>

  <!-- プロジェクト名と異なる場合 -->
  <Product>Cube.Custom.Product</Product>
  <AssemblyName>Cube.Custom.Product</AssemblyName>
  <AssemblyTitle>Custom Title</AssemblyTitle>
  <RootNamespace>CustomNamespace</RootNamespace>

  <!-- NuGet パッケージの生成可否を明記する場合 -->
  <IsPackable>true</IsPackable>

  <!-- 厳密な名前付けに関する設定 -->
  <SignAssembly>true</SignAssembly>
  <AssemblyOriginatorKeyFile>Cube.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

PackageLicenseExpression には、成果物(NuGet パッケージ)に適用するライセンスを指定します。指定可能な文字列に関しては SPDX License List を参照下さい。これまで、ライセンスは URL で指定していましたが、NuGet の仕様変更により deprecated となったようです。

Product, AssemblyName, AssemblyTitle, RootNamespace の 4 種類は、省略時にはプロジェクト名(csproj 拡張子を除く名前)が設定されます。

IsPackable は、プロジェクトが NuGet パッケージを生成可能かどうかを指定します。既定値は true なので明記する必要性は薄いと思いますが、AppVeyor 等の CI サービスにおいて、自動的に NuGet パッケージを生成して欲しくない時などに false を設定する事があります。

SignAssembly および AssemblyOriginatorKeyFile は、厳密な名前付けに関する項目です。これらの項目は旧 csproj と同じなので、詳細は省略します。

AnyCPU に関する設定

新 csproj は Platform が AnyCPU の場合、初期設定では(例えば)"bin/Release/net45" に出力されます(ちなみに、旧 csproj は "bin/Release")。

<PropertyGroup Condition=" '$(Platform)' == 'AnyCPU' ">
  <OutputPath>bin\Any CPU\$(Configuration)\</OutputPath>
  <DocumentationFile>bin\Any CPU\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>

多くの場合においては初期設定のままで問題ないと思いますが、AppVeyor 等の CI サービスにおいて意図しない挙動をいくつか確認したため、AnyCPU の OutputPath を上記のように設定しています。尚、新 csproj では OutputPath で設定された文字列に $(TargetFramework) を追加したものが実際の出力パスとなるようです。

Properties.Resources に関する設定

前述した通り、新 csproj において *.resx ファイルは EmbeddedResource として自動的に Include されます。しかし、この状態では Visual Studio 上でのリソース編集が Resources.Designer.cs に反映されません。これを反映させるには、旧 csproj で指定されていた内容を明記する必要があります。

<ItemGroup>
  <Compile Update="Properties\Resources.Designer.cs" DependentUpon="Resources.resx" AutoGen="True" DesignTime="True" />
  <EmbeddedResource Update="Properties\Resources.resx" Generator="ResXFileCodeGenerator" LastGenOutput="Resources.Designer.cs" />
</ItemGroup>

ここで重要なのは、Update 属性を使用している点です。*.resx は全て追加済みとなっているため、Include 属性で明記すると重複エラーとなります。これを回避するには、一つは前述した EnableDefaultItems に false を設定する事、そしてもう一つが Update 属性を使用する事です。

尚、パスの区切り文字を "/" (slash) にした場合、Visual Studio 上でリソースを編集したタイミングで csproj が自動的に編集されてしまいました("Properties/Resources" と "Properties\Resources" が両方とも記述される)。少なくとも .NET Framework をターゲットにしている場合、もうしばらくは "\" (backslash) を使用するほうが無難なようです。

参照に関する設定

新 csproj では、参照に関する記述方法も簡素化されています。特に、これまで packages.config と旧 csproj の 2 種類のファイルに記述されていた NuGet パッケージの参照内容が、新 csproj では PackageReference と言う項目に統合されました。

<ItemGroup>
  <ProjectReference Include="..\Another\Another.csproj" />
  <PackageReference Include="SomePackage" Version="1.0.0" />
  <Reference Include="System.For.Bar" />
</ItemGroup>

ProjectReference や PackageReference は推移的な参照をサポートしており、ProjectReference の ProjectReference や PackageReference の PackageReference などは自動的に解決してくれます。このため、新 csproj には直近のプロジェクトまたは NuGet パッケージの参照のみを追加すれば OK です。ただし、推移的なプロジェクト参照に関しては、全てのプロジェクトが新 csproj 形式でなければならないそうです(参考:新しい csproj 形式)。

削除するファイル一覧

前述したように、新しい csproj 形式では、これまでいくつかのファイルに分割して記述されていた情報が一つのファイルに統合されました。そのため、以下のファイルは必要な情報を csproj に記述した後に削除する必要があります。

  • **/Properties/AssemblyInfo.cs
  • **/packages.config
  • **/*.nuspec

WinForms

ここからは、WinForms および WPF アプリケーションの新 csproj について説明します。

基本的な構成

WinForms の基本的な設定は下記の通りです。尚、これまで記述した内容の多くを省略していますが、それらの設定も必要に応じて追加して下さい。

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net45</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <EnableDefaultNoneItems>false</EnableDefaultNoneItems>
    <ApplicationIcon>App.ico</ApplicationIcon>
    <ApplicationManifest>App.manifest</ApplicationManifest>
  </PropertyGroup>
</Project>

重要な点は、Project の Sdk 属性を Microsoft.NET.Sdk.WindowsDesktop にする事と、UseWindowsForms を true で明記する事です(参考:Windows Desktop)。これらの項目の意味については、後述します。

OutputType の規定値は Library のようです。そのため、WinForms や WPF アプリケーションの場合、この項目を WinExe に設定します。

ApplicationIcon および ApplicationManifest には、アイコン等のファイルを指定します。これらの項目は旧 csproj と同じなので、詳細は省略します。

Microsoft.NET.Sdk.WindowsDesktop に関して

Visual Studio 2019 かつ .NET Core 3.0 の環境において新たに指定可能となった Microsoft.NET.Sdk.WindowsDesktop および UseWindowsForms ですが、手元の環境で確認した限り、.NET Framework では既定の参照内容に影響を及ぼすようです*1。以下の図は、UseWindowsForms の設定値と Visual Studio 2019 上で確認できる参照内容の対応関係を表したものになります。

UseWindowsForms の設定による違い

この図を見ると、UseWindowsForms が true の場合、WinForms ライブラリである System.Windows.Forms が参照に追加されている事が分かります。他に UseWPF と言う項目も存在しますが、true にした場合には同様に PresentationCore 等の WPF に必要なライブラリが参照に追加されます。ただし、これらの項目は Sdk が Microsoft.NET.Sdk.WindowsDesktop の場合のみ有効で、Microsoft.NET.Sdk の場合は無視されます。

注意点として、Visual Studio 2017 では、.NET Core 3.0 SDK の有効・無効に関わらず Microsoft.NET.Sdk.WindowsDesktop を指定したプロジェクトの読み込みに失敗します。また、Visual Studio 2019 であっても .NET Core 3.0 が無効の場合、ビルドに失敗します。したがって、これらの環境との互換性を考慮する場合、Sdk に Microsoft.NET.Sdk を指定した上で各種ライブラリの参照を明記する必要があります。

Include および Update 規則

WinForms におけるソースファイル等の Include および Update 規則に関する記述内容は下記になります。

<ItemGroup>
  <Compile Update="Views\**\*.cs" SubType="Form" />
  <Compile Update="Views\**\*.Designer.cs" SubType="Code" />
  <EmbeddedResource Update="Views\**\*.resx" DependentUpon="%(Filename).cs" />
  <None Include="Assets\**\*" />
  <None Include="App.config" />
</ItemGroup>

この中で最低限必要な項目は EmbeddedResource です。前述したように *.resx は自動的に Include されますが、これだけだと Form にアイコンを設定している場合などで実行時エラーが発生します。これを防止するために WinForms に関連する *.resx に対して、DependentUpon 属性を指定する形で Update します。尚、Include や Update にワイルドカードで指定した場合、"%(Filename)" で各ファイルの拡張子を除いた名前を取得できるようです。

2 種類の Compile 設定は、各種 Form を Visual Studio のデザイナ上で編集可能にするための設定です。ただし、これらの設定は Visual Studio 2017 では効果がありませんでした。

WPF

次は、WPF アプリケーションの新 csproj について説明します。

基本的な構成

WPF の基本的な設定は下記の通りです。UseWindowsForms が UseWPF に置き換わったのみで、それ以外は全て WinForms と同様です。

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net45</TargetFramework>
    <LangVersion>latest</LangVersion>
    <UseWPF>true</UseWPF>
    <EnableDefaultNoneItems>false</EnableDefaultNoneItems>
    <ApplicationIcon>App.ico</ApplicationIcon>
    <ApplicationManifest>App.manifest</ApplicationManifest>
  </PropertyGroup>
</Project>

尚、前述したように UseWindowsForms および UseWPF は各ライブラリの参照設定に関するもののようなので、併記する事も可能です。WPF の場合、GUI コンポーネントが足りない等の理由で WinForms を併用する事もあるので、その場合には UseWindowsForms も true で設定します。

Include および Update 規則

WPF におけるソースファイル等の Include および Update 規則に関する記述内容は下記になります。

<ItemGroup>
  <ApplicationDefinition Include="App.xaml" SubType="Designer" Generator="MSBuild:Compile" />
  <Page Include="Views\**\*.xaml" SubType="Designer" Generator="MSBuild:Compile" />
  <Compile Update="Views\**\*.xaml.cs" SubType="Code" DependentUpon="%(Filename)" />
  <Resource Include="Assets\**\*" />
  <Resource Include="App.ico" />
  <None Include="App.config" />
</ItemGroup>

まず、App.xaml を ApplicationDefinition で明記する必要があります。次に、Page および Compile の項目で、それ以外の *.xaml ファイル(およびコードビハインドに当たる *.cs ファイル)を追加します。この時、App.xaml が同じディレクトリに存在すると重複エラーとなります。その場合は、Page の項目に対して Exclude="App.xaml" と言う記述を追加して下さい。個人的には、ディレクトリ階層で区分する事で解決するようにしています。

新 csproj 具体例へのリンク一覧

最後に、これまでに説明した内容を実際のプロジェクトで記述したものをサンプルとしていくつか紹介します。

*1:尚、TargetFramework を netcoreapp3.0 に設定した場合、現時点では Microsoft.NET.Sdk.WindowsDesktop と記述した時点で UseWindowsForms, UseWPF の設定に関わらず全ての WinForms/WPF ライブラリが参照に追加されるようです。

Azure Pipelines で OpenCover/NUnit を実行し Codecov に送信

CubeSoft の各種プロジェクトでは、継続的インテグレーション (CI: Continuous Integration) 用サービスとして AppVeyor を利用していますが、諸々の事情を考慮して Azure Pipelines でも同等の CI を実行できるように環境の整備を進めています。この記事では、Azure Pipelines での CI、特に OpenCover/NUnit を用いてユニットテストを実行し、結果を Codecov に送信するまでの手順について記述します。

概要

前提として、何らかの csproj (多くの場合、ユニットテスト用のプロジェクト)に対して下記の PackageReference が記述されている事とします。

<ItemGroup>
    <PackageReference Include="NUnit" Version="3.11.0" />
    <PackageReference Include="NUnit.ConsoleRunner" Version="3.10.0" />
    <PackageReference Include="OpenCover" Version="4.7.922" />
</ItemGroup>

この状態で、下記のタスクを実行するように YAML ファイルを編集していきます。

  1. NuGet Resotre
  2. Build
  3. Run OpenCover with NUnit
  4. Send to Codecov
  5. Publish test results
  6. NuGet Pack
  7. Publish pipline artifacts

ここでは、ユニットテストに関係のある 3. ~ 5. の記述を抜粋します。下記を含む全ての記述内容は AzurePipelines.yml を参照下さい。*1

- script: >
    "$(TEST_TOOL)"
    -log:Error
    -register:user
    -target:"$(TEST_CORETOOL)"
    -targetargs:"$(PROJECT_NAME).Tests.dll"
    -targetdir:"Tests\$(PROJECT_BIN)"
    -returntargetcode
    -hideskipped:All
    -output:"$(TEST_COVERAGE)"
    -filter:"$(TEST_FILTERS)"
  displayName: 'Run tests via OpenCover and NUnit'

- script: |
    pip install codecov
    codecov -f "$(TEST_COVERAGE)" -t $(CODECOV_TOKEN)
  displayName: 'Send coverage results to Codecov'

- task: PublishTestResults@2
  inputs:
    testResultsFormat: 'NUnit'
    testResultsFiles: '**\$(TEST_RESULT)'
  displayName: 'Publish test results'

最初の task (script) で OpenCover を実行し、次の task で Codecov に結果を送信します。尚、この記事の執筆時点では、Azure Pipelines から Codecov に結果を送信するためにはトークンを指定する必要があります。そのため、How to integrate codecov.io in an Azure Build Pipeline を参考にして、あらかじめ CODECOV_TOKEN と言う環境変数を作成し、必要な値を Secret 設定で追加しておいて下さい。

Azure Pipelines の環境変数

最後の task で Azure Pipelines 上にテスト結果を送信します。PublishTestResults タスクを実行すると、各ビルドの Tests タブに結果が表示されるようになります。

Azure Pipelines 上でのテスト結果

備考

これ以降は、今回 Azure Pipelines 上で CI 環境を整えるにおいて、嵌まったポイント等いくつかの関連事項を記述します。

NuGet task を利用した Restore

NuGet に関連するコマンドは NuGet task が用意されており、Restore に関しても通常はこの task を利用します。

- task: NuGetCommand@2
  inputs:
    command: 'restore'
    restoreSolution: '$(PROJECT_NAME).sln'
    #feedsToUse: 'config'
    #nugetConfigPath: 'NuGet.config'
  displayName: 'Restore NuGet packages'

しかし、NuGetCommand@2 経由で実行した場合、初期設定ではカレントディレクトリに存在する NuGet.config は無視されます。config ファイルを反映させるには feedsToUse および nugetConfigPath 引数を利用するようなのですが、実際に試した所、今度は api.nuget.org が Source として認識しないような挙動を示しました。恐らく、明示的に config を指定した場合、この辺りも含めて設定する必要があるものと予想されます。

- script: |
    nuget restore "$(PROJECT_NAME).sln"
  displayName: 'Restore NuGet packages'

以上を考慮すると、NuGet.config を用意している場合、現時点では上記のように script で実行する方が楽なようです。

GitHub Releases からダウンロード

依存するライブラリが全て NuGet パッケージとして取得できれば良いのですが、必ずしもそうではない場合もあります。例えば、CubeICE は 7z.dll と言うライブラリに依存しています。ここでは、このライブラリを Releases - cube-soft/7z から取得する事を試みます。

- task: DownloadGitHubRelease@0
  inputs:
    connection: 'cube-soft-ci'
    userRepository: 'cube-soft/7z'
    itemPattern: '7z-*-x64.zip'
    downloadPath: '$(Build.SourcesDirectory)'
  displayName: 'Download 7-Zip modules'

GitHub Releases からのダウンロードには Download GitHub Release task を利用します。userRepository に対象となるリポジトリの名前、itemPattern にダウンロードするファイルを表す文字列、downloadPath に保存ディレクトリのパスを指定します。

connection には、Service connections と呼ばれる機能で作成した文字列を記述します。作成手順は、まず Creating a personal access token for the command line を参考に、GitHub 上でトークンを生成します(生成時に指定するスコープに関しては、repo, user, admin:repo_hook が推奨されているようです)。次に、Azure Pipelines の左下にある Project settings から Service connections を選択し、New service connection で GitHub を選択します。

Service connection の新規作成

新規作成画面で Personal access token を選択し、Connection Name には適当な名前、Token には GitHub で取得したトークンを入力します。最後に、ここで設定した名前を DownloadGitHubRelease@0 の connection 引数に記述すると完了です。

Pipeline Artifacts の設定

Azure Pipelines には Artifacts と言う項目が存在します。これは、Azure DevOps の Artifacts(左側にメニューとして表示されている項目)とは別物で、各ビルド結果の右上に表示されるリンクから辿る事ができます。

Pipeline Artifacts

この Artifacts に成果物を表示するには、Publish Pipeline Artifact task を利用します。

- task: PublishPipelineArtifact@0
  inputs:
    artifactName: '$(PROJECT_NAME)'
    targetPath: '$(Build.ArtifactStagingDirectory)'
  displayName: 'Publish pipline artifacts'

NuGet Pack など多くの task において、実行結果は $(Build.ArtifactStagingDirectory) に保存されます。そのため、targetPath 引数にはこの変数を指定しておくと良いようです。

*1:Cube プロジェクトでは、取得した各種 NuGet パッケージを "../packages" に配置するように設定しています(参考:Cube のプロジェクト構成およびビルド&テスト方法)。初期設定では、これらの NuGet パッケージは "$(UserProfile)/.nuget/packages" に配置されるようなので、もしリンク先の YAML ファイルを利用する場合には、適当に置き換えて下さい。