NSIS で配布した .NET 8 デスクトップアプリを起動した瞬間、「You must install .NET Desktop Runtime to run this application」と表示されて立ち上がらない――。ランタイムを入れ直しても直らず、原因が見えづらいこの典型トラブルを、仕組みの解説から再発防止まで“運用に耐える粒度”で整理しました。現場でよく詰まるポイント(runtimeconfig.json/deps.json の扱い、環境変数、アーキテクチャ不一致、NSIS の打ち込み)を具体的に潰していきます。
症状と前提
インストーラ(NSIS)で導入したアプリを起動すると、次のダイアログが出て実行できません。
You must install .NET Desktop Runtime to run this application
- Windows Desktop Runtime 8.0.8 (x64) は導入済み
- すべての
.csprojは<TargetFramework>net8.0-windows10.0.20348.0</TargetFramework> dotnet --list-runtimesでも該当ランタイムを確認済み- ランタイムの再インストールやインストーラの再ビルドでも改善せず
なぜこのエラーが出るのか(仕組みの理解)
.NET のアプリ(Framework‑dependent 方式で発行)は、起動時に「アプリ本体」と同じフォルダーにある *.runtimeconfig.json を読み、必要なフレームワーク(例:Microsoft.WindowsDesktop.App 8.0.x)を決定します。ホスト(hostfxr)は次の順でランタイムを探索します。
DOTNET_ROOTなどの環境変数で明示された場所- ユーザー/マシンにインストール済みの標準パス(例:
C:\Program Files\dotnet\shared\...) - マルチレベル参照(
DOTNET_MULTILEVEL_LOOKUP=1)が許可されている場合、他レベルのインストール
この探索で「要求と合致する Microsoft.WindowsDesktop.App(アーキテクチャ・バージョン・エディション)」が見つからないと、質問のダイアログが出ます。ランタイムがあるのに見つからない状況は、多くが「環境変数の誤設定」「アーキテクチャ不一致」「runtimeconfig.json の欠落/不整合」「NSIS でのファイル同梱漏れ」に起因します。
よくある原因
| 区分 | 具体例 | 主な症状 |
|---|---|---|
| ランタイム競合 | 旧版(7.x など)や x86 ランタイムが残存。SDK のみ残っている。 | 特定マシンだけ起動不可。コマンドでは検出されるがアプリは失敗。 |
| 環境変数 | DOTNET_ROOT が C:\Program Files (x86)\dotnet を指す/DOTNET_MULTILEVEL_LOOKUP=0 固定。 | PowerShell での dotnet は動くのに GUI 起動が落ちる。 |
| メタデータ不足 | *.runtimeconfig.json/*.deps.json を EXE と同じフォルダーへ入れていない。 | 開発機では動くが、配布先でのみエラー。 |
| バージョン不整合 | runtimeconfig.json が 8.0.0 を要求、実機は 8.0.8 のみ等。 | Patch 異常時に発生。rollForward 指定なし。 |
| アーキテクチャ不一致 | アプリを win-x86 で発行、実機は x64 ランタイムのみ。 | 一部環境だけダイアログ。イベントログに Failed to load hostfxr。 |
| 発行方式 | Framework‑dependent のまま配布し、先方のランタイム依存が残る。 | テスト機依存の“動いたり動かなかったり”が発生。 |
最短で直す:実効性の高い対処(優先度順)
不要ランタイムの削除と環境変数の整理
まず衝突源を除去し、ホストの探索を正常化します。
- 「設定 > アプリ > インストールされているアプリ」から古い .NET Runtime / Windows Desktop Runtime / SDK を削除(対象:7.x 以前、x86)。
- 環境変数を確認・クリア(昇格シェルで実行)
$env:DOTNET_ROOT
$env:DOTNET_MULTILEVEL_LOOKUP
# もし値が出たら、システムの環境変数から削除して再起動
# (一時的検証なら現コンソールだけ無効化)
Remove-Item Env:DOTNET_ROOT -ErrorAction SilentlyContinue
Remove-Item Env:DOTNET_MULTILEVEL_LOOKUP -ErrorAction SilentlyContinue
再起動後、次を確認します。
dotnet --info
dotnet --list-runtimes
# 期待値:Microsoft.WindowsDesktop.App 8.0.8 (x64) が表示される
# 期待値:Microsoft.NETCore.App 8.0.8 (x64) も表示される
runtimeconfig.json / deps.json を必ず同梱
EXE と同一フォルダーに次の 2 ファイルがないと、ホストは正しく解決できません。
- アプリ名.runtimeconfig.json
- アプリ名.deps.json
NSIS への組み込み例:
!include "MUI2.nsh"
Name "MyApp"
OutFile "MyApp-Setup.exe"
InstallDir "$PROGRAMFILES64\MyCompany\MyApp"
RequestExecutionLevel admin
Section "Main" SEC01
SetOutPath "$INSTDIR"
File "bin\Release\net8.0-windows10.0.20348.0\publish\MyApp.exe"
File "bin\Release\net8.0-windows10.0.20348.0\publish\MyApp.runtimeconfig.json"
File "bin\Release\net8.0-windows10.0.20348.0\publish\MyApp.deps.json"
; 必要な衛星アセンブリ/Content も忘れずに
CreateShortcut "$SMPROGRAMS\MyApp.lnk" "$INSTDIR\MyApp.exe"
SectionEnd
注意:発行先が publish でない場合は自分のビルドディレクトリに合わせてパスを調整します。
runtimeconfig.json の整合性を確認
runtimeconfig.json の一例(Windows デスクトップ・フレームワーク依存):
{
"runtimeOptions": {
"tfm": "net8.0",
"framework": {
"name": "Microsoft.WindowsDesktop.App",
"version": "8.0.8",
"rollForward": "latestPatch"
}
}
}
ポイントは name が Microsoft.WindowsDesktop.App になっていること、version が導入済みランタイム(ここでは 8.0.8)と整合すること、そして運用では rollForward に latestPatch 以上を指定してパッチ更新を吸収することです。
自己完結型(Self‑contained)発行に切り替える(推奨)
配布先のランタイムに依存させないのが最も堅牢です。.csproj に以下を追加して発行(x64 の例)。
<PropertyGroup>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<PublishTrimmed>false</PublishTrimmed> <!-- まずはトリミング無効で動作確認 -->
</PropertyGroup>
dotnet publish -c Release
Self‑contained にすると配布パッケージは大きくなりますが、クライアント PC にランタイムがなくても起動します。初回はトリミング無効(PublishTrimmed=false)で確実に動かし、問題なければ後から最適化を検討します。
クリーン環境での検証
既存環境の残骸に引きずられない検証を行います。
- 新規ユーザープロファイルでインストール&起動(プロファイル汚染切り分け)
- Hyper‑V/VMware/VirtualBox の新品 VM でテスト(最終確認)
- PowerShell での簡易チェック:
Get-ChildItem "C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App\8.0.8"で存在確認
即効チェックリスト(コマンドと期待結果)
| コマンド | 確認ポイント | 期待結果 |
|---|---|---|
dotnet --list-runtimes | WindowsDesktop.App の有無 | Microsoft.WindowsDesktop.App 8.0.8 (x64) が列挙 |
$env:DOTNET_ROOT | 環境変数の残存 | 空(未設定)。設定するなら C:\Program Files\dotnet |
$env:DOTNET_MULTILEVEL_LOOKUP | 探索レベル抑制の有無 | 未設定(または 1) |
Get-Content .\MyApp.runtimeconfig.json | name/version/rollForward | Microsoft.WindowsDesktop.App / 8.0.8 / latestPatch |
dir .\MyApp.deps.json | deps の同梱 | サイズが 0 でないファイルが存在 |
NSIS スクリプト:配布漏れを防ぐテンプレ
ファイルの取りこぼしを無くすための、最低限のテンプレートを示します。
!include "MUI2.nsh"
Name "MyApp"
OutFile "MyApp-Setup.exe"
InstallDir "$PROGRAMFILES64\MyCompany\MyApp"
RequestExecutionLevel admin
BrandingText "MyApp Installer (.NET 8)"
!define VerifyFiles "!insertmacro VerifyFiles"
Section "Install" SEC01
SetOutPath "$INSTDIR"
; 発行ディレクトリを丸ごと突っ込むのが安全
File /r "publish*.*"
; 重要ファイルの検証(存在しなければダイアログを出して停止)
${IfNot} ${FileExists} "$INSTDIR\MyApp.exe"
MessageBox MB_ICONSTOP "MyApp.exe が見つかりません。ビルド設定を確認してください。"
Abort
${EndIf}
${IfNot} ${FileExists} "$INSTDIR\MyApp.runtimeconfig.json"
MessageBox MB_ICONEXCLAMATION "runtimeconfig.json が同梱されていません(FDD配布の場合必須)。"
${EndIf}
${IfNot} ${FileExists} "$INSTDIR\MyApp.deps.json"
MessageBox MB_ICONEXCLAMATION "deps.json が同梱されていません(FDD配布の場合必須)。"
${EndIf}
CreateShortcut "$SMPROGRAMS\MyApp\MyApp.lnk" "$INSTDIR\MyApp.exe"
WriteUninstaller "$INSTDIR\uninstall.exe"
SectionEnd
アーキテクチャ不一致の見分け方と対処
実機に x64 ランタイムしかないのに、アプリを win-x86(または AnyCPU で 32bit 優先)で発行すると不一致が起こります。
- 発行時の指定:FDD なら 指定しない(AnyCPU)か、Self‑contained なら
<RuntimeIdentifier>win-x64</RuntimeIdentifier>。 - イベントログ:「アプリケーションとサービス ログ > Microsoft > Windows > .NET Runtime」内のエラーで
Failed to load hostfxr等が出ていないかを確認。
Visual Studio で「32 ビットを優先」は .NET Framework 時代の設定です。.NET 6+ の FDD では OS のビット数に合わせて適切なランタイムが選ばれます。Self‑contained の場合だけ RuntimeIdentifier に注意しましょう。
バージョンとロールフォワードの戦略
運用中のパッチ更新で毎回配布し直すのは現実的ではありません。runtimeconfig.json の rollForward を設定し、パッチ/マイナー更新を受け入れる方針を決めておきます。
| rollForward | 許容範囲 | おすすめ用途 |
|---|---|---|
LatestPatch | 同一マイナー内の最新パッチ | まずはここから。互換性リスクが最小 |
Minor | 次のマイナーまで | 緊急性の高い修正を取り込みたい場合 |
Major | 次のメジャーまで | 長期運用・自己責任で |
発行方式の比較と選び方
| 方式 | 特徴 | 配布サイズ | 依存 | 現場の安定性 |
|---|---|---|---|---|
| Framework‑dependent | 最小サイズ。先方ランタイムに依存 | 小 | Microsoft.WindowsDesktop.App 等 | 環境差でブレやすい |
| Self‑contained | ランタイム同梱。起動が堅牢 | 大 | なし | 安定 |
| Self‑contained + Single‑file | 単一ファイルで配布容易 | 中〜大 | なし | 運用しやすい(ウイルス対策例外は要配慮) |
プロジェクト設定の見直しポイント
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.20348.0</TargetFramework>
<UseWPF>true</UseWPF> <!-- WinForms の場合は UseWindowsForms -->
<AssemblyName>MyApp</AssemblyName>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.Compatibility" Version="8.*" />
</ItemGroup>
</Project>
- UseWPF / UseWindowsForms を明示して WindowsDesktop SDK として発行される状態を確認。
- 参照パッケージのバージョンを 8.x 系で揃える(7.x と混在させない)。
インストーラ品質を上げる診断・ガード
NSIS の実行後に「起動自己テスト」を入れて、導入直後のコケを検知します。
Section "SmokeTest" SEC02
ExecWait '"$INSTDIR\MyApp.exe" --selfcheck' $0
${If} $0 != 0
MessageBox MB_ICONSTOP "起動自己テストに失敗しました。ランタイムや権限を確認してください。"
${EndIf}
SectionEnd
アプリ側には --selfcheck オプションで Environment.GetEnvironmentVariable("DOTNET_ROOT") などをログ出力するコードを用意しておくと、現場での切り分けが速くなります。
トラブル診断フローチャート(テキスト版)
起動時ダイアログ →
A) MyApp.runtimeconfig.json が EXE 隣にある? → NO: 同梱して再配布
YES →
B) dotnet --list-runtimes に WindowsDesktop.App 8.0.8 (x64) がある? → NO: 追加インストール
YES →
C) DOTNET_ROOT / DOTNET_MULTILEVEL_LOOKUP は未設定? → NO: 解除して再起動
YES →
D) runtimeconfig の name/version/rollForward は妥当? → NO: 修正して再発行
YES →
E) アーキテクチャ(win-x64/x86)は一致? → NO: そろえる
YES →
F) Self-contained で発行して切り分け → これで動けば環境依存、FDD の配布に戻すか検討
現場で踏みがちな落とし穴
- SDK だけ入っている:
dotnet --infoで SDK は見えるが、Microsoft.WindowsDesktop.Appが無い。 - x86 ランタイムしか無い:企業標準イメージで x86 のみ入っているケース。
- 圧縮・暗号化ツールの過剰適用:EXE パッカーや AV の介入で単一ファイル展開に失敗。
- NSIS の SetOutPath ミス:ショートカットの作業フォルダーが実体と異なり、相対パスの依存が崩れる。
CI/CD での再発防止チェック(サンプル)
# 1) 必須ファイルの存在
$required = @("MyApp.exe","MyApp.runtimeconfig.json","MyApp.deps.json")
$missing = $required | Where-Object { -not (Test-Path (Join-Path $env:BUILD_ARTIFACTSTAGINGDIRECTORY $_)) }
if ($missing) { throw "必須ファイル不足: $($missing -join ', ')" }
# 2) runtimeconfig のバージョン妥当性
$configPath = Join-Path $env:BUILD_ARTIFACTSTAGINGDIRECTORY "MyApp.runtimeconfig.json"
$json = Get-Content $configPath -Raw | ConvertFrom-Json
if ($json.runtimeOptions.framework.name -ne "Microsoft.WindowsDesktop.App") { throw "framework name 不一致" }
if ($json.runtimeOptions.framework.version -notmatch "^8.0.\d+$") { throw "version が 8.0.x ではありません" }
FAQ
Q. dotnet --list-runtimes では見えるのに、アプリはエラーになります。
A. DOTNET_ROOT が x86 を指している、DOTNET_MULTILEVEL_LOOKUP=0 が残っている、runtimeconfig.json の name が Microsoft.NETCore.App になっている、などが定番です。まず環境変数をクリアし、runtimeconfig の内容を確認してください。
Q. PublishSingleFile にすると runtimeconfig.json は不要ですか?
A. Self‑contained + Single‑file では実行時に展開を伴い、アプリ外に依存する runtimeconfig は不要です(FDD では必要)。ただし一部のネイティブ依存や AV 製品の干渉で展開失敗することがあるため、まずは通常の Self‑contained で動作検証してから Single‑file を有効化するのが安全です。
Q. 8.0.8 に固定すべきですか?
A. 検証が済んだパッチに固定しつつ、rollForward=latestPatch を併用するのが現実解です。FDD 配布なら、先方の更新に追従できる設計が望ましいです。
まとめ
- このエラーは「WindowsDesktop.App が見つからない/一致しない」ことが本質。
- まずはランタイム競合の除去と環境変数の整理、そしてruntimeconfig.json / deps.json の同梱を徹底。
- バージョン不整合には
rollForward、環境差の吸収には Self‑contained が効く。 - NSIS では 発行物を丸ごと同梱し、導入直後の起動自己テストで未然に検出。
- CI/CD にチェックを組み込めば、ヒューマンエラーを仕組みで潰せる。
最短解決の鍵は「①競合の排除 / ②同梱漏れの解消 / ③必要なら Self‑contained へ」。この 3 点を順に実施すれば、多くの現場でダイアログ地獄から解放されます。
付録:トラブル兆候と原因のマッピング
| 兆候 | 可能性が高い原因 | 初手 |
|---|---|---|
| 開発機だけ動く/配布先は失敗 | runtimeconfig/deps の同梱漏れ | NSIS の File 行を見直し、publish を丸ごと同梱 |
| 一部 PC だけ失敗 | アーキテクチャ不一致、環境変数の固定 | 環境変数をクリア、Self‑contained で切り分け |
| パッチ適用のたびに壊れる | version 固定で rollForward なし | rollForward を latestPatch に |
付録:ローカル再現用スクリプト(検証用)
あえて x86 のみを見せる状況を作り、問題の再現と回避を学習できます。
# ⚠ 実験用。管理者シェルで実行。
# x86 側にだけ DOTNET_ROOT を向ける(誤設定の再現)
[Environment]::SetEnvironmentVariable("DOTNET_ROOT","C:\Program Files (x86)\dotnet","Machine")
[Environment]::SetEnvironmentVariable("DOTNET_MULTILEVEL_LOOKUP","0","Machine")
# → アプリ起動が失敗するはず。正常化
[Environment]: :SetEnvironmentVariable\("DOTNET_ROOT",$null,"Machine"\)
[Environment]: :SetEnvironmentVariable\("DOTNET_MULTILEVEL_LOOKUP",$null,"Machine"\)
付録:最低限のログ出力コード(C#)
using System;
using System.IO;
internal static class BootstrapDiag
{
public static int SelfCheck()
{
try
{
var log = Path.Combine(AppContext.BaseDirectory, "bootstrap.log");
File.WriteAllLines(log, new []
{
$".NET Info: {System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}",
$"OS: {System.Runtime.InteropServices.RuntimeInformation.OSDescription}",
$"ProcessArch: {System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture}",
$"DOTNET_ROOT: {Environment.GetEnvironmentVariable("DOTNET_ROOT")}",
$"DOTNET_MULTILEVEL_LOOKUP: {Environment.GetEnvironmentVariable("DOTNET_MULTILEVEL_LOOKUP")}"
});
return 0;
}
catch { return 1; }
}
}
最後に
「動くマシンでは動くのに、別のマシンでは動かない」――このタイプの不具合は、情報の不足(runtimeconfig.json/deps.json)と探索の妨害(環境変数・競合)に集約されます。記事の手順どおりに配布物の完全性と探索経路の健全化をチェックすれば、ほぼ確実に脱出できます。配布対象が多い現場ほど、Self‑contained と CI の二段構えで「再発しない」仕組み化を目指してください。

コメント