既存のWin32アプリをWindows Storeで公開したりWindows 10Sで実行するには、Desktop BridgeでUWPに見せかけるしかないんですが、ぱっとみすごくめんどくさそうに見えるんです。でもMSDNに裏技が書いてあったので試してみました。
※今回やったことはMSDNに全部書いてあります。Visual Studio による .NET デスクトップ アプリ用デスクトップ ブリッジ パッケージ ガイド - UWP app developer | Microsoft Docsのステップ1 変換の個所が該当します。
そもそも
そもそもミスリードしてる人が多いですが、Win32アプリをUWPに変換するためにDesktop App Converter (DAC)は必須要件では『ありません』。Desktop App Converterはインストーラが存在する既存のWin32アプリをある程度自動で変換するための仕組みです。
DACもやってることは単純で仮想マシン起動して、その中でインストーラ実行して、インストーラが終了した時点でのファイルシステムとレジストリの差分を回収するだけの仕組みです。Desktop Bridge自体はDAC非依存なので、UWPに含めるパッケージを自力で作れるのであれば、DACなんて使わなくてもいいんです。
結局はDesktop Bridgeで変換されたWin32アプリは、Appxの中にWin32の実行ファイルが入ってるだけのたんなるAppxパッケージです。
なので、たんにZIPしてるだけのアプリをUWPにしたいがためだけにインストーラ書くとか完全に間違いで、MSDN見たらもっと簡単な方法があったのでやってみました。今回はコードスニペットと、画像多めでお送りします。
準備
まず変換したいアプリを用意します。
MSDNで紹介されているのは、.NETアプリなのでWPFとかのが該当します。が、手元にそんなものはなかったのでAzureaを変換してみます。MSDNにあるのと、今回試したのの大きな違いは.NETかC++ Nativeかです。MSDNでは.NETアプリなので、Any CPUでおっけー!*1とか書いてますけど、これ残念ながらネイティブコードなのでx86とx64で実行コード違うし、そもそもAnyにしたらx86で詰むやろ…とかあるのでその辺も回避してきます。
この時点でソリューションはまだこうです。これからいろいろやっていきます。
JavaScript UWPプロジェクトの追加
何度も言いますけど、Desktop Bridgeで変換されたWin32アプリは、Appxの中にWin32の実行ファイルが入ってるだけのたんなるAppxパッケージです。なので、簡単にパッケージを作るには、UWPプロジェクトにファイル追加してマニフェスト編集するのが一番楽なんです。ストア用の署名とかも全部勝手にやってくれるし。
ということで、まずはJavaScript UWPプロジェクトを追加します。よーするにJavaScript UWPプロジェクトをいけにえにして、Desktop Bridgeで実行されるWin32アプリを含んだAppxを作ります。なんでJavaScriptなのかというと、最初のMSDNのページの一番最後にちらっと書いてあります。
- Debug でアプリをビルドすると、次のようなエラーが出力されます。Microsoft.Net.CoreRuntime.targets(235,5): エラー: カスタム エントリ ポイントを持つアプリケーションの実行可能ファイルはサポートされていません。 パッケージ マニフェストでの Application 要素の Executable 属性を確認してください。 この問題を回避するには、代わりに Release モードを使用します。
- UWP プロジェクトのルート フォルダーに格納されている Win32 バイナリは、Release では削除されます。 Win32 バイナリを格納するフォルダーを使用しない場合、.NET Native コンパイラは最終的なパッケージからそれらのバイナリを削除します。これにより、実行可能ファイルのエントリ ポイントが見つからないため、マニフェストの検証エラーが出力されます。
ということなので全く話になりません。JavaScript UWPプロジェクトは単なるHTMLを表示するだけのマニフェストでビルドもなにもしないから余計なこと何もしないので、試してみたらたまたまうまくいきました。とかそういう感じなんでしょう。きっと。
普段C#とかVBでUWP書いてる人はもしかするとインストールすらしてないかもしれないJavaScriptを選択して、Blank Appを作成します。名前は『アプリの名前.Package』とかにしとくと分かりやすいと思います。今回変換したいのはAzureaなのでAzurea.Packageという名前でプロジェクトを作りました。
プロジェクト作ったときに聞かれるターゲットバージョンですが、どうせ後で書き換えちゃうのでお好きな感じで結構です。
プロジェクトが出来上がると、ソリューションがこんな感じになります。
そしてVisual Studioでは自動的にmain.jsが開かれて
// Your code here!
とか言ってますが、このファイルが入ってるディレクトリを含め、赤丸の部分は不要なので削除します。
すっきりしました。
imagesには、アプリのアイコンとかが入ってたりするのでそのまま置いておきます。(あとで使います)
Win32実行ファイルの追加
いけにえにするJavaScript UWPプロジェクトの準備ができたので、Appxに含めるファイルを追加していきます。
MSDNの手順にそって、win32
というフォルダを作ります。
別にこれwin32である必要はまったくなくて、わかりやすそうだからwin32なだけです。appとか、applicationとかprogramとかなんでも大丈夫です。なんで何でもいいかというと、Win32の実行ファイルがAppxにパッケージングされていることが大事で、実行するWin32実行ファイルのパスはappxmanifestで指定するので本当になんでもいいです。なんでもいいんですけど、MSDNでwin32って書いてたので、とりあえずそのままwin32で進めます。
次に、アプリケーションが実行に必要なファイルをwin32の中にいい具合に追加します。とりあえず、Releaseビルドした結果をエクスプローラからドラッグ&ドロップで追加してみました。
この時pdbももれなく追加するのがポイントです*2。pdbを追加しておくと、Appxのパッケージングプロセスの中でpdbだけ切り出されていつものUWPみたいにappxsymにしてくれます。ぜひ追加しましょう。
ファイルを追加したら、win32配下のファイルをすべてコンテンツとしてマークします。コンテンツにしないとAppxのなかにうまく含めてくれません。
出力ディレクトリにコピーについても新しければコピーするもしくは、常にコピーするにします。
ここまで来たら基本的にはだいたい終わりです。あらまぁ簡単ですね。って感じです。
appxmanifestの編集
パッケージのコンテンツにWin32実行ファイルの実行に必要なファイルを追加したんですが、手動でコピーしました。これだと、毎回コピーしないといけなくて面倒なんですが、それの対応はちょっと後回しにしてappxmanifestを編集します。
残念ながらDesktop Bridge周りはUIで編集できないので手作業で編集します。まずはpackage.appxmanifestを右クリックして、コードを表示します。
するとこういうXMLが表示されます。
<?xml version="1.0" encoding="utf-8"?> <Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" IgnorableNamespaces="uap mp"> <Identity Name="0f891c8e-ba95-4518-b6ab-b73741a82090" Version="1.0.0.0" Publisher="CN=yutaka" /> <mp:PhoneIdentity PhoneProductId="0f891c8e-ba95-4518-b6ab-b73741a82090" PhonePublisherId="00000000-0000-0000-0000-000000000000" /> <Properties> <DisplayName>Azurea.Package</DisplayName> <PublisherDisplayName>yutaka</PublisherDisplayName> <Logo>images\storelogo.png</Logo> </Properties> <Dependencies> <TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.0.0" MaxVersionTested="10.0.0.0" /> </Dependencies> <Resources> <Resource Language="x-generate" /> </Resources> <Applications> <Application Id="App" StartPage="index.html"> <uap:VisualElements DisplayName="Azurea.Package" Description="Azurea.Package" BackgroundColor="transparent" Square150x150Logo="images\Square150x150Logo.png" Square44x44Logo="images\Square44x44Logo.png"> <uap:DefaultTile Wide310x150Logo="images\Wide310x150Logo.png" /> <uap:SplashScreen Image="images\splashscreen.png" /> </uap:VisualElements> </Application> </Applications> <Capabilities> <Capability Name="internetClient" /> </Capabilities> </Package>
MSDNに書いてある内容を順番に反映していきます。
まずPackage
タグにxmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
を追加します。さらに、Package
タグのIgnorableNamespaces
にrescap
も追加します。IgnorableNamespaces
はスペース区切りなので、uap mp rescap
になります。
追加したPackage
タグはこうなります。
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" IgnorableNamespaces="uap mp rescap">
次にDependencies
タグにあるTargetDeviceFamily
を<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.14393.351" MaxVersionTested="10.0.14393.351" />
で置き換えます*3。置き換えたDependenciesはこうなります。
<Dependencies> <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.14393.351" MaxVersionTested="10.0.14393.351" /> </Dependencies>
Capabilites
タグに<rescap:Capability Name="runFullTrust" />
を追加します。これでAppxが完全信頼で実行されるようになります*4。追加するとこうなります*5。
<Capabilities> <rescap:Capability Name="runFullTrust" /> <Capability Name="internetClient" /> </Capabilities>
最後にアプリのエントリポイントを設定します。Application
タグにExecutable
属性とEntryPoint
属性を指定します。この時、Executable
属性には、実行ファイルのAppx内の相対パスを*6。EntryPoint
属性にはWindows.FullTrustApplication
を指定します。今回変換するAzureaはAzurea.exeが実行ファイル名なので、これらを設定するとこうなりました。
<Application Id="Azurea" Executable="win32\Azurea.exe" EntryPoint="Windows.FullTrustApplication">
これで手書きで編集しないといけない部分はおしまいです。あとアイコンとかはいつも通りGUIから生成とかしちゃって大丈夫です。
ここまで全部変更したXMLはこうなります。
<?xml version="1.0" encoding="utf-8"?> <Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" IgnorableNamespaces="uap mp rescap"> <Identity Name="0f891c8e-ba95-4518-b6ab-b73741a82090" Version="1.0.0.0" Publisher="CN=yutaka" /> <mp:PhoneIdentity PhoneProductId="0f891c8e-ba95-4518-b6ab-b73741a82090" PhonePublisherId="00000000-0000-0000-0000-000000000000" /> <Properties> <DisplayName>Azurea.Package</DisplayName> <PublisherDisplayName>yutaka</PublisherDisplayName> <Logo>images\storelogo.png</Logo> </Properties> <Dependencies> <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.14393.351" MaxVersionTested="10.0.14393.351" /> </Dependencies> <Resources> <Resource Language="x-generate" /> </Resources> <Applications> <Application Id="Azurea" Executable="win32\Azurea.exe" EntryPoint="Windows.FullTrustApplication"> <uap:VisualElements DisplayName="Azurea.Package" Description="Azurea.Package" BackgroundColor="transparent" Square150x150Logo="images\Square150x150Logo.png" Square44x44Logo="images\Square44x44Logo.png"> <uap:DefaultTile Wide310x150Logo="images\Wide310x150Logo.png" /> <uap:SplashScreen Image="images\splashscreen.png" /> </uap:VisualElements> </Application> </Applications> <Capabilities> <rescap:Capability Name="runFullTrust" /> <Capability Name="internetClient" /> </Capabilities> </Package>
この状態でAppxmanifestのGUIエディタを開くとスタートページが設定されてない。とか言ってきます。
このパラメータは本来Application
タグに設定されているんですが、さっき消しちゃったのでこんなエラーがでます。出るんですが、Appx作るうえで何も起こられないのであまり気にせずこのまま放っておきます。
パッケージのビルド
あとはもうビルドしたらできあがりです。プロジェクトを右クリックして、ストア、アプリパッケージの作成でできます。
この先は普段のUWPのビルドと変わらないので割愛します。ひとつだけ注意する点があって、パッケージのアーキテクチャの指定です。
.NETでWPFみたいなILだけのアプリの場合はNeutralにAny CPUを指定します。これでOKです。それ以外の人は、x86とx64にチェックを入れて、それぞれビルドしましょう。しばらくするとパッケージが出力されます。インストールとかして試してみるといいと思います。
パッケージに含めるコンテンツを自動で更新する
ここまででできたAppxの中身を更新するには毎回手作業でコピーしてあげないといけなくてかったるいので自動でやります。そもそも、今回のAzureaはネイティブコードなのでx86/x64のパッケージを別々に生成しないといけなくて毎回コピーなんてめんどくさくてやってられません。
これらを実現するためにスクリプトを書きます。今時ならPowerShell使えよって感じなんですが、PowerShell力が低いのでバッチファイル書きます。
まず、win32の中身をクリーンするやつです。これをcleanup.bat
とします。終了コードが0じゃないとビルドが止まってしまうのでとりあえずexit 0
してます。
del /Q win32\Azurea.exe del /Q win32\Azurea.pdb del /Q win32\AzLang.dll del /Q win32\AzLang.pdb del /Q win32\ColorSchemes\* del /Q win32\Language\* exit 0
次に、プロジェクトの出力を集めてwin32ディレクトリにコピーするやつです。これはcopyfiles.bat
とします。Appxパッケージビルド時のプラットフォーム名を使ってコピーするディレクトリを振り分けています。
@echo off echo Packaging for %* set build=%1% set arch=%2% mkdir win32 if "%arch%"=="x64" goto copy_amd64 if "%arch%"=="x86" goto copy_x86 goto undefined_platform :copy_amd64 copy ..\x64\%build%\Azurea.exe win32\Azurea.exe copy ..\x64\%build%\Azurea.pdb win32\Azurea.pdb copy ..\x64\%build%\AzLang.dll win32\AzLang.dll copy ..\x64\%build%\AzLang.pdb win32\AzLang.pdb goto copy_noarch :copy_x86 copy ..\%build%\Azurea.exe win32\Azurea.exe copy ..\%build%\Azurea.pdb win32\Azurea.pdb copy ..\%build%\AzLang.dll win32\AzLang.dll copy ..\%build%\AzLang.pdb win32\AzLang.pdb goto copy_noarch :copy_noarch mkdir win32\ColorSchemes mkdir win32\Language xcopy ..\Azurea\ColorSchemes win32\ColorSchemes /-Y /E xcopy ..\Language win32\Language /-Y /E goto EOF :undefined_platform echo "Undefined platform " %arch% exit 1 :EOF
これらはプロジェクトの中のscriptsというディレクトリにまとめて保存しました。
次にこれらを呼び出してもらわないといけません。ということでまずJavaScript UWPプロジェクトをアンロードします。
次に、アンロードしたプロジェクトを右クリックして、編集を選択します。
表示されたXMLを下のほうまでスクロールして、最後にこんなのを追加します。
<Target Name="BeforeBuild"> <Exec Command="scripts\cleanup.bat" /> <Exec Command="scripts\copyfiles.bat $(Configuration) $(Platform)" /> </Target> <PropertyGroup> <DisableFastUpToDateCheck>true</DisableFastUpToDateCheck> </PropertyGroup>
追加したら保存して閉じます。それぞれの意味ですが、
BeforeBuild
ビルド前に実行するタスクです。$(Configuration)
ビルド設定で、Release
とかDebug
とか入っています。$(Platform)
アーキテクチャでx86
とかx64
とか入ってます。
これでAppxのビルド時に自動でコピーされるようになっていい感じになりました。
自動で追加できるようにすると、コピーするファイルをコンテンツでマークするのが面倒になります。その場合は、同じように、プロジェクトをアンロードして、編集して、Content
タグにワイルドカードを指定してあげるといい感じになります。
Azureaではこんな感じで指定してます。
<ItemGroup> <Content Include="win32\Azurea.exe"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> <Content Include="win32\AzLang.dll"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> <Content Include="win32\Azurea.pdb"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> <Content Include="win32\AzLang.pdb"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> <Content Include="win32\ColorSchemes\*.txt"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> <Content Include="win32\Language\*.*"> <CopyToOutputDirectory>Always</CopyToOutputDirectory> </Content> </ItemGroup>
おしまい
MSDNでは.NETアプリのみを対象としてJavaScript UWPプロジェクトをいけにえにしてDesktop BridgeなAppxを作る方法が紹介されてましたが、普通のNativeアプリケーションでも工夫すればどうってことなかったです。
今回のエントリはパッケージを作成するところまでですが、署名鍵をストア申請用に変更するためにパッケージと予約名を関連付けるとかは、通常のUWOとおんなじです。いい記事もたくさんあるので参考にしてみるといいと思います。
ちなみに、ですが、現状Desktop Bridgeを使ったFull TrustなAppxは通常の方法ではストアに提出できません。だそうです。Full TrustなAppxが提出できるフラグをアカウントにつけてもらわないといけないので、Desktop Bridge を活用して、既存のアプリやゲームを Windows ストアに移行しましょうのフォームを送信する必要があるそうです*7。
*3:MSDNに10.0.14393.0って書いてますが、どうもDesktop BridgeにバグがあってWindowsがBSODすることがあるらしく、この問題が修正された10.0.14393.351を指定するほうがよさそうです。
*4:Run Full Trust あきらかにパワーがある
*5:ちなみにrunFullTrustなアプリはAppContainerで実行されないのでinternetClient capabilityとか書いても書かなくてもあんまり意味はありません。
*6:なのでプロジェクトに作ったwin32というディレクトリ名はなんでもよかったのです。
*7:先日やったばかりなので実際どうなってるのかはよくわかりません