tmytのらくがき

個人の日記レベルです

Desktop Bridgeで既存のWin32アプリを手軽にパッケージングしたい

既存の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で詰むやろ…とかあるのでその辺も回避してきます。

この時点でソリューションはまだこうです。これからいろいろやっていきます。

f:id:tmyt:20170509004204p:plain

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を表示するだけのマニフェストでビルドもなにもしないから余計なこと何もしないので、試してみたらたまたまうまくいきました。とかそういう感じなんでしょう。きっと。

f:id:tmyt:20170509004751p:plain

普段C#とかVBでUWP書いてる人はもしかするとインストールすらしてないかもしれないJavaScriptを選択して、Blank Appを作成します。名前は『アプリの名前.Package』とかにしとくと分かりやすいと思います。今回変換したいのはAzureaなのでAzurea.Packageという名前でプロジェクトを作りました。

プロジェクト作ったときに聞かれるターゲットバージョンですが、どうせ後で書き換えちゃうのでお好きな感じで結構です。

f:id:tmyt:20170509005623p:plain

プロジェクトが出来上がると、ソリューションがこんな感じになります。

f:id:tmyt:20170509005704p:plain

そしてVisual Studioでは自動的にmain.jsが開かれて

// Your code here!

f:id:tmyt:20170509005742p:plain

とか言ってますが、このファイルが入ってるディレクトリを含め、赤丸の部分は不要なので削除します。

f:id:tmyt:20170509005907p:plain

すっきりしました。

f:id:tmyt:20170509010009p:plain

imagesには、アプリのアイコンとかが入ってたりするのでそのまま置いておきます。(あとで使います)

Win32実行ファイルの追加

いけにえにするJavaScript UWPプロジェクトの準備ができたので、Appxに含めるファイルを追加していきます。

MSDNの手順にそって、win32というフォルダを作ります。

f:id:tmyt:20170509010254p:plain

別にこれwin32である必要はまったくなくて、わかりやすそうだからwin32なだけです。appとか、applicationとかprogramとかなんでも大丈夫です。なんで何でもいいかというと、Win32の実行ファイルがAppxにパッケージングされていることが大事で、実行するWin32実行ファイルのパスはappxmanifestで指定するので本当になんでもいいです。なんでもいいんですけど、MSDNでwin32って書いてたので、とりあえずそのままwin32で進めます。

次に、アプリケーションが実行に必要なファイルをwin32の中にいい具合に追加します。とりあえず、Releaseビルドした結果をエクスプローラからドラッグ&ドロップで追加してみました。

f:id:tmyt:20170509010751p:plain

この時pdbももれなく追加するのがポイントです*2pdbを追加しておくと、Appxのパッケージングプロセスの中でpdbだけ切り出されていつものUWPみたいにappxsymにしてくれます。ぜひ追加しましょう。

ファイルを追加したら、win32配下のファイルをすべてコンテンツとしてマークします。コンテンツにしないとAppxのなかにうまく含めてくれません。

f:id:tmyt:20170509011159p:plain

出力ディレクトリにコピーについても新しければコピーするもしくは、常にコピーするにします。

f:id:tmyt:20170509011243p:plain

ここまで来たら基本的にはだいたい終わりです。あらまぁ簡単ですね。って感じです。

appxmanifestの編集

パッケージのコンテンツにWin32実行ファイルの実行に必要なファイルを追加したんですが、手動でコピーしました。これだと、毎回コピーしないといけなくて面倒なんですが、それの対応はちょっと後回しにしてappxmanifestを編集します。

残念ながらDesktop Bridge周りはUIで編集できないので手作業で編集します。まずはpackage.appxmanifestを右クリックして、コードを表示します。

f:id:tmyt:20170509011536p:plain

するとこういう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タグのIgnorableNamespacesrescapも追加します。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内の相対パス*6EntryPoint属性には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エディタを開くとスタートページが設定されてない。とか言ってきます。

f:id:tmyt:20170509013058p:plain

このパラメータは本来Applicationタグに設定されているんですが、さっき消しちゃったのでこんなエラーがでます。出るんですが、Appx作るうえで何も起こられないのであまり気にせずこのまま放っておきます。

パッケージのビルド

あとはもうビルドしたらできあがりです。プロジェクトを右クリックして、ストア、アプリパッケージの作成でできます。

f:id:tmyt:20170509013305p:plain

この先は普段のUWPのビルドと変わらないので割愛します。ひとつだけ注意する点があって、パッケージのアーキテクチャの指定です。

f:id:tmyt:20170509013420p:plain

.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というディレクトリにまとめて保存しました。

f:id:tmyt:20170509014256p:plain

次にこれらを呼び出してもらわないといけません。ということでまずJavaScript UWPプロジェクトをアンロードします。

f:id:tmyt:20170509014350p:plain

次に、アンロードしたプロジェクトを右クリックして、編集を選択します。

f:id:tmyt:20170509014428p:plain

表示された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

*1:実行コードはILで書いてあってCLRで実行されるので

*2:MSDNに書いてないけど

*3:MSDNに10.0.14393.0って書いてますが、どうもDesktop BridgeにバグがあってWindowsBSODすることがあるらしく、この問題が修正された10.0.14393.351を指定するほうがよさそうです。

*4:Run Full Trust あきらかにパワーがある

*5:ちなみにrunFullTrustなアプリはAppContainerで実行されないのでinternetClient capabilityとか書いても書かなくてもあんまり意味はありません。

*6:なのでプロジェクトに作ったwin32というディレクトリ名はなんでもよかったのです。

*7:先日やったばかりなので実際どうなってるのかはよくわかりません