Moai+Easter Advanced Manual

Moai Manual Advanced Manual Annoucement FAQ
ご案内 Moaiエンジン CustomBoyエンジン HowToコンパイル Moai CGI Developers Raraku言語
HowToコンパイル VC2017を最小限でインストールする方法

はじめに

この記事ではVisual Studio 2017のインストール方法についてやさしく述べます。 Visual Studio 2017は(MSの公式のアナウンスによれば)Windows7以降でインストール可能ということになっています。

ここではコマンドラインオンリーな方向けと言われているVisual Studio Build Tools 2017(無料)を使います (以下これをVS2017と表記することがあります)。 これによりファイルサイズを最小限に絞り、最小限のインストールをめざします。 そして最終的には、まだVCがインストールされていない新しいPCに1分でVCがインストールできるように、全体を一つのポータブルなzipファイルにする所までめざします。

ただしVS2017に同梱されている基本コマンド群はすべて SubsystemVersion 6.00 形式(Vista以降で起動できる形式)の exe となっており、 これを仮にWindows XP上のマシンへコピーしてダブルクリックしても起動させることはできません。 SubsystemVersionとは何かについてもこの記事では詳しく解説します。

おそらく一般的なインストール方法とは大分異なるやり方をしております。 この記事執筆時点でのVCの最新バージョンは2022であることにご留意ください (ただし最新のモノが必ずしも最適な選択ともいえません)。 それをふまえ、興味がある方はお読みください。

この記事ではフォルダのことをディレクトリと呼ぶ場合がありますが、両者は同じものです。 おおよそエクスプローラ上から操作する場合はフォルダと呼び、 一方コマンドプロンプトやバッチファイル、パスなどを意識したコンテキストではディレクトリと呼ぶことが多いですが、 使い分けは割と適当でごちゃ混ぜにして使うこともあります。

MoaiにおけるVS2017の対応
VS2017ではVCの環境変数系とディレクトリ構成が大きく変わっており、MoaiではこれまでVS2017以降のコンパイル対応が完全ではありませんでした。 ソースコードの中身自体はコンパイル不可能というわけではないのですが、Moaiで使用しているauto_triggerと呼ばれるツールではVS2017以降のインストール構成の自動認識ができないため、 コンパイラを起動させる所までいくのに手動で環境変数等を設定する必要がありました。 MoaiのソースコードビルドシステムのVS2017への対応はMoaiにおいても一つの節目と言えます。


目次



最小限のインストールを目指す

インストールするファイル群を最小限に絞れば、インストールの総サイズはそれほど大きくはならないはずです。

我々が欲しいのは最低限のC/C++コンパイラ(cl.exe)、リンカ(link.exe)、ライブラリアン(lib.exe)、make(nmake.exe)などのコマンド、そして最低限のヘッダファイルとライブラリ群です。 当然、32bitバイナリ(x86)と64bitバイナリ(x64)の両方が作れるように必要なコマンドとライブラリをそろえることとします。 理論上はこれらだけで、必要に応じて後から外部ライブラリを追加することでいかなるアプリ(exe)やライブラリ(lib, dll)も作成できることになります。

将来的にOpenGLやDirectXなどもも使いたいですか?大丈夫です。最近のWindows SDKではこれらはどちらも標準で入っています。 今回のインストールでは、Windows SDK(古くはPlatform SDKとも呼ばれていました)のインストールについても述べます。

MinGWにおいては最小限のインストールでは総サイズが150MB程度で済むことを確認しております。 これを比較の一つの目安としましょう。 仮にそのサイズが10倍、20倍となってくると「一体どこでそんなにファイル容量食うのよ?フリーザ戦の界王拳かい?」とさすがに気になります。

VS2017の場合、最小限インストールとは果たしてどのくらいの総サイズとなるのでしょうか? そして中の構成は一体いかなるものとなっているのでしょうか?

尚、今回はVisual Studio Community(こちらも基本的に無料ですが)は使いません。 とはいえコマンドツールの部分に関して基本的なディレクトリ構成は、Visual Studio Build Toolsとそう差はありません。 またコマンドオンリーがIDEに劣る唯一のポイントとして、デバッガによるスタックとレースなどの表示がビジュアルにできないというのはあります (代替となるフリーウェアがあるにはありますが JIT-Debugまでできるものとなると、おそらくVSに限られるのでしょう)。 気が向いたらこちらも検証するかもしれません。

目次に戻る

7-Zipの入手方法

「7-Zip?イキナリ何を言ってるんだ?」と思われるかもしれませんが、少々お待ちください。

Visual Studio 関連のインストーラはよくisoファイルで配布されることがあります。 しかも実際にインストールしてみるとなんらかの不都合に気づき、やはりアンインストールしたり他のバージョンを試すなどといったことも結構おきます。 (特に今回のような最小限インストールなどを目指しているとおきます)。

つまり割と試行錯誤の連続になりがちなわけですが、それを毎回わざわざ CD-ROM や DVD-ROM に一旦焼いてから試すなどやってられませんので7-Zipを使います。

7-Zipは、Igor Pavlov 氏によって開発されたオープンソースのツールです。 基本的には 7z 形式と呼ばれる圧縮ファイルや zip ファイルを解凍/圧縮するためのツールですが、 それに加え、ダウンロードした iso ファイルの中身を閲覧したり、解凍することもできます。 つまりMSのサイトからダウンロードした iso ファイルを一旦ハードディスク上に解凍して、その中にあるセットアップ用の exe などを実行するわけです。 そうすればわざわざ CD-ROM や DVD-ROM に焼く必要がなくなります。

もっとも今回の VS2017 のインストーラは iso ファイルではないのですが、Windows SDK などを単独でダウンロードした場合、 それが iso ファイルで提供されることがあります。 またこの記事の最終セクション「はじめてのダイエット」で zip 化する作業がありますが、その場合に必要になります。 まだ入れていない方は是非インストールしてお使い下さい(コマンドラインから使うことすらできます)。 https://www.7-zip.org/download.html よりダウンロードできます。 この記事執筆時点での最新安定版は 7-Zip 23.01 (7z2301.exe)ですのでそちらをダウンロードすればいいでしょう (1.5MBと非常に小サイズであり、かつ動作は軽快です)。

インストールはダウンロードしたexeをダブルクリックして実行するだけで、1分もあれば終わります。 このツールはWindowsの場合はWindows 2000以降で動作します。

Windows 2000をお使いの場合
困ったことに最近の多くのウェブサイトは新しいバージョンのHTTPS(TLS1.2以上)に対応したブラウザでないと閲覧やファイルをダウンロードできません。 githubや7-Zipの公式サイトも例外ではなく、これらのサイトからファイルをダウンロードするには、まずTLS1.2以上に対応したブラウザをインストールする必要があります。 (従ってこの記事を github 上から直接ご覧できている方は、お使いのブラウザは既にTLS1.2に対応していることになります)。

ここでは幸運にして何らかの形でこの記事を閲覧できている方(たとえばやスマホなどの別の機器経由)で、 お使いのPC(Windows 2000)にまだTLS1.2以上のブラウザがインストールされていない方を対象に、 ゼロからそれを導入する手順を説明します。
もっともスマホなどの別の機器が存在するなら、そこから必要なツールをダウンロードしてそれをPC(Windows 2000)に転送してもよいのですが、 ここではせっかくですのでPC(Windows 2000)単独で必要なツールを自力でダウンロードして最短で整備する方法を検討してみたいと思います。
今時ダイアルアップ接続などで直接WANに繋ぐ方はそんなにはいないとは思いますが、 特に古いWindowsでネットに繋げる場合は、必ずルータを介して繋ぎましょう。 WAN側へのポートをすべて閉じておくことが重要です。

IEやFirefoxやChromeではダメなのか?
ダメです。

Windows 2000 をインストールした初期の状態では(たとえSP4を当てても) IE5 しか入っておりませんが、 このブラウザはTLS1.0 にしか対応しておりません。 7-Zipの公式サイト(https://sevenzip.osdn.jp)からインストーラをダウンロードしようとしてもHTTPSによる通信に失敗してしまいます (IE6も同様です。IE7以降はWindows2000ではインストール不可です)。

替わりに Firefox を入れるとしても、Windows 2000 で動作するFirefoxの最後のバージョンは 12.0 (あるいは10.0.12esr)となりますが、 これもまた TLS1.0 にしか対応しておりません(規定で無効とかいうのではなく、そもそも根本的に対応してないため about:config等で変更することもできません)。

Firefox のこのバージョンでは、7-Zipの公式サイト(https://sevenzip.osdn.jp)を閲覧することだけならできます。 このことから https://sevenzip.osdn.jp 自体は TLS1.0 で通信可能なサイトであると考えられます(それでもIE5ではおそらく他の要因も絡んで閲覧できませんが)。 ところが肝心のインストーラをダウンロードすることができないのです。 一見不思議な現象に見えますが、これはダウンロード時に d3.7-zip.org という別のダウンロード用サーバにリダイレクトされ、 そのサーバとの通信ではおそらく TLS1.2 以上が必要となっているためでしょう。

因みにChromeは一番最初のバージョンからWindows2000には対応しておりませんのでこれも選択肢から除外されます。

これで三大ブラウザは全滅しました。

え?Opera?
結論から言えばこやつはオープンソースではないので今回は除外です。うまくいきません。

Windows 2000でインストールできるOperaの最終バージョンは12.02です。 またこれについては Firefox からであれば https://ftp.opera.com/pub/opera/win/1202/int よりダウンロードできます。 (IE5ではうまくいきません)。

Opera Ver12.02では、TLS1.2についてはデフォルトでは無効となっています。 「設定」⇒「詳細設定」⇒「セキュリティ」⇒「セキュリティプロトコル」から一応 TLS1.2を有効にできるようにはなっています。 しかしこれを有効にしても github の閲覧や 7-Zip をダウンロードしようとするとエラーが表示され残念ながらうまくいきません。 デフォルトで無効となっていることから、おそらくTLS1.2の実装はまだ完全ではなく実験的なものなのでしょう。


その他、もっと単純に wget for Windows や lynx for Windows なども思いつくかもしれませんが、これもダメです。 これらのサイトからダウンロードする場合、osdn や sourceforge などへリダイレクトされますが、こちらがTLS1.2となっており、 結局ダウンロードできません。

最後にダメ押ししましょう。 IEやFirefoxではもはやGoogleすらまともに動作しないのです。 ググることすらままなりません。 Windows 2000 の初期の状態は想像以上に過酷な環境ですね。

Close


私はプログラマだ!手に入らないなら必要なツールは自分で作る!
ダメです。 そもそもビルドしようにもまだコンパイラがありません。

Q. ではVCやMinGWをインストールしたらどうか?

A. ダメです。 どちらもダウンロードできません。 ダウンロードサイト(microsoft や sourceforge)がTLS1.2を使っているためです。 (つーかそもそもVCインストールのための準備作業だ!今はっ!)

Q. Cはやめて go とか python 使って何とかならんかなー?

A. ならんです。 確かに Firefox からなら go の公式サイトはなんとか閲覧可能で、それをダウンロードすることもできます。 しかし肝心の go.exe(Ver1.3以降)は Windows 2000 では動作しません(Windows 2000ではサポートされていないWindows APIを使っています)。 python についてはダウンロードできません。 ダウンロードサイト(www.python.org)がTLS1.2を使っているためです。

Q. それこそ Moai 使ってなんとかならんか?

A. ならんです。 ダウンロードサイト(github)がTLS1.2を使っているためです。

Close


現状、Windows 2000 で使う最適なブラウザは、この種の話題ではよく挙げられるように roytam1 氏がWindows 2000用に修正ビルドした Pale Moon です。 これならばTLS1.2に対応しています。

本家のPale Moonじゃダメなのか?
ダメです。

本家のサイトからでも古いバージョンのPale Moonをダウンロードできるコーナーがありますが、 残念ながら Windows 2000 で動作可能なバイナリは既に消失しています。


以下からダウンロードできます。

http://o.rthost.win/gpc/files1.rt/home.html

Windows 2000に最初から入っているIE5からでもダウンロード可能な所がポイントです。 見てのとおり、初めが「http://」で始まっております(つまり通信にHTTPSを使っておりませんので今回の問題は発生しません)。 またダウンロード可能なファイルだけが並んだシンプルなサイトですので、 IE5のような超古いブラウザからでも特に問題なく表示されます。

このサイトには多くのファイルが置かれておりますが、上からアルファベット順に並んでおりますので必要なファイルは探しやすいと思います。 今回ダウンロードするファイルは palemoon-26.*.*-*.win2000.7z のような名前のファイルでよろしいかと思います。 因みに私が確認した時点で最新のものは palemoon-26.5.0-20210814.win2000.7z となっていました。

roytam1氏のサイト
roytam1 氏はジャッキーチェーンと同じく香港のプログラマで、氏のサイトは以下の3つがあります。
  1. https://github.com/roytam1
  2. http://rtfreesoft.blogspot.com
  3. http://o.rthost.win

1番目がgithubで現時点では氏の一番代表的なサイトなのでしょう。 2番目がおそらく開発ブログのようなもの、3番目が今回取上げた単なるファイル置き場です。 https://github.com/roytam1/UXP には rtfreesoft.blogspot.com へのリンクがありますし、 rtfreesoft.blogspot.com からは o.rthost.win のリンクがありますので、 github 以外の残り2つも確かに同氏のサイトであることが確認できます。

「rt」というのがおそらくroytam1を表すのでしょう。 ちなみに「http://o.rthost.win/gpc/」には日本語とAA(アスキーアート)でメッセージが書かれてあります。

Close


次にダウンロードしたこの 7z ファイルを解凍する必要があるわけですが…

「あれ?これどうやって解凍するんだ?」

7z 形式のファイルを解凍するには普通は 7-Zip が必要ですね。 しかし今回、これをダウンロードすることができません(まだPale Moonを入れてませんからね)。 そこで NYSL で有名な k.inaba 氏が開発した Noah を使います。 Noah でも 7z 形式が解凍できるからです(ただし現状のNoahでは iso は取り扱えないようなのでやはり最終的には 7-Zip は必要です)。 以下からダウンロードできます。

https://www.kmonos.net/lib/noah.ja.html

このサイトもroytam1氏のサイトと同じく、Windows 2000に最初から入っているIE5からでもダウンロード可能な所がポイントです。 上の方にある「3.199+ Installer」というリンクから noahinst.exe というファイルをダウンロードします。 ダウンロードしたらこれをダブルクリックしてインストールを行います。 このとき素晴らしいことに必要なDLLはすべてNoahが自力でダウンロードします。 他の大御所のソフトが軒並み不甲斐ないのを見た後ですと、Noahのこの対応は感動的ですね!

これで先にダウンロードしておいた palemoon の 7z ファイルを解凍できます。 解凍後、中にある palemoon.exe を起動します。 これで様々なサイトを正常に閲覧することができます(当然 github や google にも正常にアクセスできます)。 念のため、about:config から security.tls.version.max の値を見てみますと、確かにTLS1.2を示す「3」となっていることが確認できます。

ようやくゴールにたどり着けました。 7-Zip のサイトからそのインストーラをダウンロードしてインストールしましょう。

Close


Windows 98をお使いの場合
Windows 98をお使いの場合は、7-Zip 9.20 ( https://ja.osdn.net/projects/sevenzip/downloads/64455/7z920.exe ) が動作可能な最終バージョンになるようです。

未確認ですがおそらくWindows 98では roytam1 氏の Pale Moon でさえも起動は無理でしょう。 さすがにおとなしく、他の機器等を経由して必要なファイルを転送しましょう…。


目次に戻る

インストーラ(vs_BuildTools.exe)の入手方法

とりあえず一般的とされている方法から述べます。
まず以下のサイトへアクセスします。

https://visualstudio.microsoft.com/ja/vs/older-downloads/

表示されたページを下の方へスクロールすると「2017」と書かれた部分があると思いますのでそこをクリックします。 「Visual Studio 2017 およびその他の製品」と書かれたパネルが表示され、横の方に「ダウンロード」とあると思いますのでこれを押します。

このときMicrosoftアカウントのサインインを求められると思います。 Microsoftアカウントをまだ作っていない方は作っておき、サインインしましょう(無料かつ匿名で作れます)。 サインインするとおそらく次のようなサイトへ自動的に転送されるはずです。

https://my.visualstudio.com/Downloads?q=visual%20studio%202017

URLのドメインがmicrosoft.comとなっておりませんが、visualstudio.comもMSの公式サイトです。 qオプションで検索キーワードを指定できるらしく、visual studio 2017 がキーワードに含まれる項目を表示するようになっていると思われます。 それでも結構多くの項目が出て来て大変ですが、「Build Tools for Visual Studio 2017」とあるものがあるはずなので探しましょう。 そのようなものが二つ以上存在する場合は、横にバージョン番号が書かれていると思いますので、バージョンの新しい方(番号が大きい方)を選びましょう。 不具合等が修正されているため、このように複数のバージョンが存在している可能性が高いです。

私が確認した2021年12月時点では最も新しいものはBuild Tools for Visual Studio 2017 (version 15.9)でした。 Downloadボタンを押し、vs_BuildTools.exe (1.4MB)なるファイルをダウンロードします。 ファイルサイズからみてこれは単なるトリガーでしょう。

ところで、このqオプションを使えば、もっと高速に色々なものを探せそうです。
少し脱線しますが、実験してみます。

https://my.visualstudio.com/Downloads?q=visual%20studio%202017%20build%20tools

このURLでアクセスすると、「Build Tools for Visual Studio 2017」だけが表示され、実にスッキリします。
面倒なら最初からこのURLにアクセスすればよさそうですね。

目次に戻る

vs_BuildTools.exe 実行にあたっての注意点

早速ダウンロードしたvs_BuildTools.exeを実行してみたい所ですが、その前にいくつかの注意点があります.
  1. 環境によってはインストーラ自体が起動しない場合がある。

  2. これは大抵の場合、.NET Frameworkが古いことが原因です。 .NET Framework 4.5.1以上を要求されますので、それをダウンロードして入れておけば大丈夫です。

  3. 一つのフォルダを作ってその中で作業を行う。

  4. これは好みの問題ですが、おそらく追加で何かをダウンロードしたり、試しにtest_hello.cファイルを書いて色々コンパイルをテストしたいと思うことがあると思います。 vs_BuildTools.exe以外にもいくつかのファイルが必要になる可能性に備え、例えばE:\MyVStudioInstallerという一つのフォルダを作っておき、 その配下にインストール関係のファイルをすべてまとめて置けば、見通しをよくできるでしょう。

  5. このインストーラのインストール項目は、Visual Studioの知識がある程度ないとかなり難解である。

  6. 前バージョンと比べても、このインストーラはかなりの項目を選択インストールできるようになっています。 わからなければデフォルトの設定でもいいのですが、その場合余計なものも含まれるので最小限インストールを目指すならそれらをカットする必要があります。 どの項目が何を示すのか、そしてハードディスクのどの位置に、どのくらいのサイズインストールされるのか、把握しておくことが重要です。

  7. Cドライブの空き容量として2GB以上は余裕をみておく。

  8. 仮にインストール先をDドライブやEドライブのパスに設定したとしても、vs_BuildTools.exeは一時的な作業領域としてCドライブを使います。 また、こちらの指定に関わらず問答無用でCドライブへインストールされるファイル群(Windows SDK、Windows Universal CRT SDKなど)もあります。 今回のミッションでは、最低でもCドライブに2GB(できれば3GB)の余裕はあった方がよいと思います。 ディスクを占有しているエロ動画等は別の場所へ退避させましょう。

  9. vs_BuildTools.exeは、必要ファイル一式インストール後、最後にWindowsの「再起動」を要求してくる。

  10. これが一番驚きます。 たかがC/C++コンパイラのインストールごときでOSの再起動を要求するとは…。 インストール中にエロ動画等を鑑賞される方は インストール中に何か別の作業をされる方は要注意です。

以上の点に気をつけながらインストール作業に入ることにいたしましょう。

目次に戻る

インストールする項目を検討する

それではvs_BuildTools.exeを実行してみます。

今回筆者がテストで使ったマシンはWindows7 64bit版となります。 Windows7ですので、今となってはかなり古い部類のOSとなります。 新しいプログラムを実行する場合、一般には新しいWindowsの方が問題が起こりにくいでしょうから、私の場合若干厳しめの「縛り」を入れている感じではあります。

このマシンではvs_BuildTools.exeを実行すると「.NET Framework 4.6以上が必要」というエラーが表示されました。 新しいWindowsを使っている方は多分このエラーは表示されないでしょう。

この問題の解決方法
.NET Framework 4.6を以下から入手してインストールしましょう。

https://www.microsoft.com/en-us/download/confirmation.aspx?id=48137

64bi版 Windows を使っている場合、NDP46-KB3045557-x86-x64-AllOS-ENU.exe というファイルがダウンロードできると思いますので、これを実行します。


無事vs_BuildTools.exeが起動したら次は、インストール項目を最小限に絞ります。 今回のミッションで我々が欲しいものは3つです。 すなわち、C/C++コンパイラ本体、C/C++標準ライブラリ、そしてWindows依存の処理を実現するためのWindows SDKです (以下、C/C++コンパイラ本体を単に「Cコンパイラ本体」、C/C++標準ライブラリを単に「C標準ライブラリ」と表記することがあります)。

残念ながら、Visual Studio Build Tools 2017では、これら3つが綺麗に分断されていない部分があるため、少々話が込み入っています。 「個別のコンポーネント」ペインでは、数多くのインストール項目をカスタムに選択できますが、どの項目をインストールしなければならないかを見極めるのはなかなかに骨が折れます。 ここでは私が色々検証した結果を踏まえ、インストールすべき項目をご紹介する形になります。

では早速始めます。 「個別のコンポーネント」ペインで以下に列挙した4つの項目についてチェックを入れます(3番目と4番目の項目についてはオプションです)。
  1. VC++ 2017 version 15.9 v14.16 latest v141 tools

  2. 必須項目です。

    C/C++コンパイラ(cl.exe)、リンカ(link.exe)、ライブラリアン(lib.exe)、nmake.exeなどの基本コマンドが含まれています。 これらのコマンドで x86/x64実行バイナリ(exe)、x86/x64用ライブラリ(lib,dll)のすべてを作成できます。

    補足
    他にもいくつか似たような名前の項目があり、それらもC/C++コンパイラ等を含んでいそうな名目ではありますが、 私がこれを選んだのは単にこれが一番ファイルサイズが小さくなるからです。

    stdio.hなどの標準ライブラリヘッダはここには含まれていないことに注意してください。 またその他にもkernel32.libなど最低限必要なライブラリは含まれていません。 よってこれらを補うため、実際には以下に述べるWindows Universal CRT SDK (MSがVS2015以降に導入した新しい標準Cライブラリの一部で、わかりにくいことにCコンパイラ本体と分離されて配布されています) とWindows SDKも必須になります。 これらが欠けるとcl.exeにおいてヘッダ(例えばstdio.h)がインクルードできないというエラーになったり、 link.exeにおいて libucrt.lib や kernel32.lib のリンクに失敗するなどのエラーになるはずです。

    尚、この項目にチェックを入れた場合、「スタティック分析ツール」という項目(おそらく20MB程度のもの)も自動的に有効になります。 絶対に必要というツールではありませんが、これのチェックを外すと「VC++ 2017 version 15.9 v14.16 latest v141 tools」のチェックも外されてしまうので 道連れでインストールするしかありません。 まあ大したサイズではないので許容しましょう。


  3. Windows Universal CRT SDK

  4. 必須項目です。

    似たような項目として「Windows ユニバーサル CRT」というのがありますが、そちらではなく 「Windows Universal CRT SDK」という方を選択します。 ここは要注意です! 「Windows ユニバーサル CRT」とある方を選択してはいけません

    参考: Universal CRTとは何か?
    自然な疑問として、そもそもUniversal CRTって何よ?ってなりますね。

    CRTとはC-RunTimeの略と思われますが、ここでランタイムなどという言葉を出すとむしろ混乱するのでこの名前の意味はこの際無視して構いません。 これは一言で言えば、VS2015以降にMSが新しく作ったC標準ライブラリを構成する実体の一部です。

    そもそも旧来までのVCではC標準ライブラリとして(多くの場合)libcmt.lib というスタティックライブラリをリンクしていました。 言い換えればこれがC標準ライブラリの実体であり、これ一つで完結しておりました。 ところがVS2015以降(すなわちVC14)では、libcmt.libに加え、新しいライブラリ libucrt.lib もデフォルトでリンクされます. つまりVC14では、C標準ライブラリの実体は libcmt.lib と libucrt.lib の二本立てになっているというイメージでよろしいでしょう。

    ファイル名にcrtという文字があるので紛らわしいですが、libucrt.libはスタティックライブラリです。 スタティックライブラリをリンクした場合、ユーザ側のPCに特別なランタイムをインストールしておく必要はありません。

    わざわざこんなものを導入したということはC標準ライブラリの機能自体が大幅にパワーアップしたのかというと、そういうわけでもありません。 じゃ、なんでわざわざ2つのlibファイルから構成されるようになったのかと不満も起こりそうなもんですが、 これはMSが謳う文句を見る限り、C標準ライブラリを(スタティックライブラリではなく)インポートライブラリとしてリンクする一部の方にのみメリットがありそうなものですね。 バージョンメンテナンスが楽になるとかそういった関係のことです。

    しかし繰り返しになりますがデフォルトではスタティックライブラリの方がリンクされますし、私も通常はそれが望ましいと思います (その理由は作成したアプリを古いWindows下でも何の前提もなく動作させるためですね。Linuxなどになるとまた事情が変わりますが)。 よって実際のところ我々が直接受ける恩恵は何もありません(つまり単に環境変数LIBの設定が面倒になっただけということであります^_^)。

    またstdio.hなどのC標準ライブラリヘッダも「Windows Universal CRT SDK」に含まれています。 (つまり単に環境変数INCLUDEの設定もまた面倒になっただけということであります^_^;)。

    「Windows Universal CRT SDK」というコンポーネントはstdio.h等のヘッダとlibucrt.lib を補うためだけのものという認識で結構です。

    Close


    補足: インストール項目について
    「Windows ユニバーサル CRT」というのは、今回は選んではいけない項目です (そもそも「ユニバーサル」の所だけ何故かカタカナで書かれてる時点で妙なパチモン感はありますが)。 繰り返しますが「Windows Universal CRT SDK」という方を選択してください。

    しかし、ならばこのカタカナなお方は一体何のためのものなのでしょうか? 「Windows ユニバーサル CRT」でポップアップされる説明を見るとこうあります。

    Windows ユニバーサル Cランタイム用のヘッダ、ライブラリ、ツール、再配布パッケージ

    「Windows ユニバーサル Cランタイム」の部分は「Windows Universal CRT」に脳内変換してよいのでしょうか? そう考えるといかにもC標準ライブラリ用のヘッダ、インポート/スタティックライブラリが入ってそうに見えるところがまた罠ですね。 実際にインストールしてみるとわかりますが、そういう我々が期待するようなものは含まれていません。 含まれているのはdll、すなわちランタイムライブラリです。 これは開発時にインポートライブラリをリンクするようなことがない限りは必要ないものです。

    さらに紛らわしいのは、この「Windows ユニバーサル CRT」という項目の方が「Windows Universal CRT SDK」より上に配置されているということです。 「Windows Universal CRT SDK」はかなり下へスクロールした場所にあります。 ちなみに「Windows Universal CRT SDK」の説明を見るとこうあります。

    以前のWindows SDK用 Universal CRT SDK サポート

    こちらはこちらで一見するとよくわからない説明ではありますが、しかしこちらが本物です。 つまりこちらに我々が所望するいわゆるstdio.hヘッダや、スタティックライブラリ(libucrt.lib等)が一式含まれています。

    Close


    参考: スタティックライブラリとインポートライブラリ(基本編)
    この辺がよくわからない方のためにここで解説しておきます。 ご存知の方は読み飛ばしてください。
    • スタティックライブラリ:

    • すべての処理がライブラリ内に記述されており、このライブラリファイルだけで自己完結します。 「おまえの助けはいらんっ!」みたいなやつですね(ここでは多少誇張して表現しております)。 コンパイル時に処理の実体まですべてがくっついてくるということで、スタティック(静的)リンクとも呼ばれます。

    • インポートライブラリ:

    • スタティックライブラリとは逆にすべての処理がライブラリ内に全く記述されておりません。 「他力本願、おまえがやれっ!」七武海のモリア様みたいなやつですね。 受付だけはしっかりしているので、アプリ側から見るとあたかも普通のスタティックライブラリのような感じで使えます。 しかしアプリ起動時になると、裏でこやつはdllに助けを呼び、そのdll内の処理の実体をメモリ上に召喚します (正確にはWindowsがOSとしてこの仕事を行います)。 すべての仕事をdllへたらい回しにし、dllが実際の処理を行うということです。

      一般的にはこのような処理を総称してロードと呼びます。 ただ今回の場合、起動時に初めて処理の実体がくっついてくる(リンクする)ということで、 これを特に(起動時)ダイナミックリンク(動的リンク)と呼びます (いわゆるプラグインなどを実現するときに使われるダイナミックロードと呼ばれる機構もありますが、この記事ではあれとは明確に区別するためダイナミックリンクという用語を使います)。

      このdllは通常Windowsのシステムディレクトリか、アプリが独自に用意するdllの場合はそのインストールディレクトリに存在します。 この二つに存在しない場合、さらに環境変数PATHで指定された場所から検索を行います。

      ただし厳密に言えば、インポートライブラリそれ自体はスタティック(静的)リンクされます。 もっと言えば、アプリで実際に使われている関数に相当する受付関数だけをインポートライブラリからひっこぬいて、それだけがアプリへリンクされます。 この辺がややこしいですね。 実際にたらい回しを行う受付部分だけが、コンパイル時にくっつくということです。

      じゃあダイナミックライブラリってないんけ?と思われた方、dll(Dynamic Link Libraryの略)がそれです。 動的に「呼ぶ側」がインポートライブラリ、動的に「呼ばれる側」がdllですね (dllが他のdllを呼ぶこともありますけれども、混乱するかもしれないのでこの辺でやめます)。

      ちなみにこのインポートライブラリはWindows環境にのみ存在します。 Linuxではsoファイル(Windowsでのdllファイルに相当します)をコンパイル時に「形式的に」リンクすると、同様のことが実現できるようになっています。

      参考: インポートライブラリのメリット
      上記の説明だけでは、何だかインポートライブラリがアホの子のように見えてメリットがよくわからないかもしれません。 インポートライブラリの名誉のためにここで少し補足しておきます。

      インポートライブラリはほぼ中身がないため、ファイルサイズが極小さくなります。 よってアプリに静的にリンクしたとしてもファイルサイズとしては誤差の範囲となるわけです。

      例えば二つのアプリAとBがあり、それらはスタティックライブラリDをともに使うとします。 このとき、最終的にできあがる二つのファイルの中身は、それぞれ概念的にはAD, BDといった構成になります。 Dのファイルサイズが大きい場合、この二つのファイル内にはそれぞれDの部分を持つわけですから、 その重複分ファイルサイズが無駄とも言えるわけです。

      ただし実際はDがまるごとくっつくわけではなく、Dの一族のうちAとBから実際に使われる関数のみがひっこぬかれて、 それぞれA, Bにくっつきます。 このあたりは無駄なリンクが起きないようリンカが頑張って依存関係まで見てくれているわけです。 つまり実際には上記で言うほどサイズの無駄は起きないわけですが、ここでは説明のため少々誇張して書いておきます。

      そこで今度はDをdllとし、AとBにファイルサイズが小さなインポートライブラリCをリンクさせます。 今度は二つのファイル AC, BC が出来上がり、Dはdllファイルとして独立して一つだけ存在する形になります。 ここでCのサイズは極小さいということがポイントです。 つまり実質Cのサイズ分は無視できます。 Dの一族はCを介して使うことができますので、仮想的にAD, BDという状況が、ファイルサイズを節約して実現できているわけです。

      ファイルサイズ節約という観点から説明しましたが、それ以外にもインポートライブラリを使うメリットはあります。 例えば、Dの中身を修正したときにコンパイルの手間が省けるなどいったことですが、さすがに脱線しすぎなのでこの辺でやめます。

    Close


    補足: 標準Cライブラリについて
    VCで用意されているC標準ライブラリは、スタティックライブラリとインポートライブラリのどちらなのでしょうか?

    結論から言うと、実は標準Cライブラリの場合、どちらも用意されています。 つまりスタティックライブラリ版C標準ライブラリインポートライブラリ版C標準ライブラリの二つが用意されているということです。 前者はlibcmt.lib、後者はmsvcrt.libのようなファイル名になっています。 「ような」という表現を使ったのはデバッグ版かどうか等でさらなる亜種に分かれ、名前がわずかに異なるからです。 名前がまるで暗号のようで覚えにくいですが、こういうときは語源から辿ると記憶に残りやすいです。

    libcmt.libのmtの部分はMulti Thread の略です。 さらに頭についているlibというプレフィックスは、UNIXから来た名残でしょう。 こちらがスタティックライブラリになります。 大昔はSingle Thread版しかなく、元々はlibc.libという素直な名前でした。 現代では、Multi Thread版以外を使う理由はほぼないため、libcmt.libがデフォルトとなっている感じですね。

    clのオプション引数で/MTを指定することで明示的にこれへのリンクを指示することもできます。 古いバージョンのVCの場合、これを指定しておいた方がよいかもしれません。

    msvcrt.libのmsはMicroSoft、crtはC RunTimeです。 こちらがインポートライブラリになります。

    こちらを使わない理由は、アプリが実行されるWindows側にmsvcrt.dllを別途用意しておく必要があるからです。 このようなdllをランタイムライブラリと呼びます。 つまりアプリを使うユーザ側がこれを自分で用意しなければならないわけで、ユーザに負担を強いることになります。 あるいはMicrosoftの定める規定に従ってこれを再配布する必要があります。 Windowsに最初から入っている場合もありますが、新しいバージョンのmsvcrt.dllの場合、入ってないこともありえます。 このような懸念から、こちらは(何か特別な理由がない限り)使わないわけです。

    Close


  5. C++ に関する Windows XP サポート

  6. 必要に応じて選択してください。

    かなり意味不明な項目名ですが、翻訳しますとこの項目でインストールされるものはWindows SDK 7.1Aです。 従って既にWindows SDK(どのバージョンでもよい. 6.0以前でもよい)を持っている方はインストール不要です。 そちらに環境変数INCLUDEとLIBを設定すれば済むからです(その手順は「環境変数の設定法概論」のセクションで述べます)。

    下記の項目4を選んだ方はこの項目3は不要です。 つまり下記の項目Windows SDK 8.1を選んだ方はこちらは不要ですし、逆にこちら(C++ に関する Windows XP サポート) を選んだ方はWindows SDK 8.1は不要ということです。

    VCを全くインストールしたことがない方はこのC++ に関する Windows XP サポートを選択しておけばよろしいでしょう。

    補足: Windows SDKとは?
    Windows SDKとは、Windows APIに対応するヘッダとライブラリが含まれているもの一式のことです(最近ではDirectXも含まれています)。 通常、MSが用意するVC用のヘッダとライブラリ群になります。

    ちなみにMinGWであれば、Windows SDKの基本的なライブラリ群はコンパイラ本体側のlibにすべてインストールされる形になります。 非常にスッキリしています。 またVCのインポートライブラリとMinGWのインポートライブラリは基本的に互換性がありません。 よってMinGW用のWindows SDKが、MinGW側で提供されるわけです。

    注意点として kernel32.lib もこのWindows SDKに含まれることに注意してください. (上記1や2の項目に、これは含まれていません)。 kernel32.lib は、Windowsにおけるすべてのプログラムにとって必須となる基盤処理を行います。 (ちなみにx64環境でも同じくkernel32.libという名前となります)。 このkernel32.libはリンカで明示的に指定しなくても自動的にリンクされるライブラリとなりますので、 コマンドライン上に登場しはしませんが、実は常にリンクされています。 従って、結局はWindows APIを使わないプログラムでも、コンパイルするにはWindows SDKが必要になってしまいます。

    ただしVCのエディションによっては、このkernel32.libをコンパイラ本体側のlibディレクトリに含んでいるものもあるかもしれません (あるいは将来、どんなエディションでもそうなるかもしれません)。

    C標準ライブラリはWindowsにおいては通常はスタティックライブラリとリンクする方が望ましく、それがデフォルトでもあります。 一方、Windows SDKでのライブラリに関してはスタティックライブラリとではなく、つまりインポートライブラリとリンクします (というよりインポートライブラリしか提供されていないのでそうするしかありません)。 作成したアプリは実行時にWindows APIのdllをダイナミックリンクします。 これらのdllは通常、C:\Windows\System32 または C:\Windows\SysWOW64 直下に置かれてあります。 (言い換えれば、これらがdllファイルのデフォルトの検索パスになっているということです)。

    Close


    補足: どこまで古い/新しいSDKなら許容範囲か?
    あまりに古いSDK(古くはPlatform SDKと呼ばれていましたが)を指定すると、コンパイル時にWarningやErrorが表示される確率が高くなる可能性は否めません。 たとえば私は「Windows Server 2003 SP1 Platform SDK」というのを持っており、今でもたまに使うことがあります。 (正確にはWindows SDKという名前ではなくその前身であるPlatform SDKですが)Windows SDKのバージョンとして言えばこれは 5.2.3790 となります。

    このバージョンは https://en.wikipedia.org/wiki/Microsoft_Windows_SDK によれば Last version with VC6 support and latest version with Windows 95 and Windows 98 support. とあります。 ただ、これより新しいSDKのインポートライブラリを使うと、その瞬間ただちにWindows 98では動作しない exe になるかといえばそんなことはなく、 MSが公式としてのサポートするものとしてはこれが最終であるということだと思います。 Windows 98でしかサポートされていないWindows APIのみを使ってコードを書いた上でコンパイルさえ通せば原理的には大丈夫というわけです。

    このSDKでたとえば winsock2.h などをインクルードしますと新しいCコンパイラ(cl.exe)では、特にマクロが再定義されているといったWarningが大量に表示されます。 これはおそらく古いコンパイラでは警告とみなされなかった事項が新しいコンパイラではそうなった、 もしくは新しいCライブラリに含まれるC標準ライブラリ内のヘッダの実装とSDK内のヘッダの実装の相性の問題でしょう。

    ではこれより新しい、例えば「Microsoft Windows SDK for Windows 7 and .NET Framework 4」はどうでしょうか? これはWindows SDKのバージョンとして言えば、いわゆる7.1、もっと正確に言えば 7.1.7600 となります。 因みに「C++ に関する Windows XP サポート」でインストールされるものは Windows SDK 7.1A ですが、これはより正確には 7.1.51106、つまり 7.1より少しだけ古いものとなります。

    このバージョンは https://en.wikipedia.org/wiki/Microsoft_Windows_SDK によれば It is the latest version that officially supports Windows XP target. とあります。 では、これより新しいSDKのインポートライブラリを使うと、その瞬間Windows XPでは動作しない exe になるかといえばやはりそんなことはなく、MSが公式としてのサポートするものとしてはこれが最終であるということですね。 実際、Visual Studio 2008においてWindows SDK 8.1を使ってコンパイルしたバイナリが、Windows 2000(SP4)上で動作することを私は確認しております。

    とはいえ、あまりに新しいWindows SDKの場合、MSは古いWindowsをターゲットOSとして公式にはサポートしないと言っていることは確かですので、 このあたり、どうバランスをとるかだと思います。

    Close


    参考: 新しいWindows SDKのインポートライブラリをリンクしたEXEは、古いWindowsのDLL上で動作するか?
    結論から言えば、プログラマが気をつけて使えば原理的には大丈夫です。

    以下この根拠を述べますが、この内容はかなり発展的な内容となります。今回のミッションで特段必須な知識ではありませんが興味ある方はお読み下さい。 またここで述べているのは(起動時)ダイナミックリンクの説明となります。 いわゆるプラグインなどを実現するダイナミックロードと呼ばれる機構もありますがそれはまた少し違うものです。

    参考: ダイナミックロード
    ダイナミックロードは極簡単に言えば、プログラマが明示的にdllをファイルとして開き、これをロードする手法です。 そもそもdllとはダイナミックリンクライブラリの略ですが、これはダイナミックロードされる対象でもあるわけです。

    dll ファイルを開くといってもfopenなど使ってやるわけではなく、例えばWindowsの場合ですとLoadLibraryというWindows API関数を使ってこれを開き、 これが返すポインタをさらにGetProcAddressというWindows API関数に与え、実際に呼び出すべき所望の関数へのポインタを返してもらいます。 この手順はLinuxにおいても全く同じ手順になります (ただしダイナミックロードされるライブラリは so ファイルであり、使うべきシステムコール名はdlopenやdlsymとなります)。
    znk_project/libZnk/Znk_dload.c などをご覧いただくと、Windows/LinuxにおいてどのようなAPIが使われるかが一目瞭然かと思います。

    ダイナミックロードは非常に強力な柔軟性を持ったロード機構です。 ダイナミックリンクは exe 起動時に必ず行われますが、ダイナミックロードの場合そのような制限はありません。 それどころかダイナミックロードではプログラマが(アプリの実装によってはエンドユーザが)選択的にdllをロードしたりしなかったりといったことができます。 そのためこの機構はアプリを拡張するためのプラグインなどでよく利用されます。 例えばMoaiのプラグインや、EasterのHTTPSサポートでもこの機構を利用しています。


    インポートライブラリ内には(受付としての)登録された関数の表を持っています。 また同時にdll内にも対応する(処理の実体としての)関数の表を持っています。 するとこれらの関数は1:1に対応しているはずと考えるのが自然でしょう。 あるいは少なくともインポートライブラリ内に登録された関数は、dll内にはすべて完備されていなければならないはずです (インポートライブラリ内の関数の集合はdll内の関数の集合の部分集合であるはずです)。

    確かにそれが本来あるべき理想的な姿ですが、実際には必ずしもそうでなくても構いません。 インポートライブラリ内に(受付として)登録された関数に対する、dll内の(処理の実体としての)関数が、仮にdll内に存在しないとしても、場合によってはこれが許容されるケースがあります。

    ちなみに、インポートライブラリやdll内にどんな関数が登録されているかをコンパイラ付属のコマンド等で実際に調べることができます。 VCの場合ですとコマンドプロンプト上で dumpbin.exe を以下のように実行しますと、それぞれのファイルについて登録されている関数の一覧が表示されます。

    dumpbin /exports Znk-2.3.imp
    dumpbin /exports Znk-2.3.dll
    

    なぜこれが許されるケースがあるのでしょうか?

    それは exe にインポートライブラリがリンクされるフェーズにおいて、インポートライブラリ内にある(受付としての)関数すべてが exe にリンクされるわけではないからです。 このフェーズにおいてリンカ(VCの場合link.exe)は、まず(前フェーズにおいてコンパイラcl.exeによって作られた)オブジェクトファイル(obj)全体を解析します。 そしてその中でオブジェクトファイル内で実際にコールしている関数を調べ、インポートライブラリからそれだけをひっこぬき、それをくっつけて最終的な exe ファイルとします(exeファイルにそれ以外の関数はくっつきません)。

    結果的には、ある関数がインポートライブラリに一応受付として用意されていても、アプリ側からそれを実際にコールしていなければ(起動時ダイナミックリンクにおいて)その関数が問題を起こすことはないということになります。 逆にアプリ側からその関数を実際にコールしており、かつdll内には存在しなければ(起動時ダイナミックリンクにおいて)問題となります(このときWindowsがその旨を報告するエラーを出します)。

    もう少し具体化して説明します。

    簡略化のため、インポートライブラリを単に imp、それによってロードされるdllを単に dll と表記しましょう。
    imp 内に登録されている関数を func1, func2, func3 とします。
    dll 内に登録されている関数を func1, func2 とします( dll 内には func3 のみ欠けております)。
    作成するアプリ exe においては imp をスタティックリンクし、実際の起動時には dll をダイナミックリンクすることにします。

    1. アプリ exe が func2, func3 を実際にコールして使っていた場合

    2. このとき exe 起動時に「dll 内に func3 がない」ことを報告するエラーが(Windowsによって)表示されます。 そしてWindowsは exe の起動を中止します。 dll 内に func3 はないのだからこれは当然と言えます。

    3. アプリ exe が func3 をコールしていない(func2だけを使っている)場合

    4. このとき exe 起動時にエラーは表示されず、つまり無事ダイナミックリンクに成功します (これはDLLを勉強したばかりの方にとっては予想外の結果かもしれません)。

    上記1, 2のいずれのケースでも、exe は imp(今回問題となっているfunc3を含むimp)ファイルをスタティックリンクして作られたものです。 ところがこのとき imp ファイル内のすべての関数が exe へスタティックリンクされるわけではありません。 上記2のケースにおいて重要なことは、アプリ は func3 を実際には使っていないということです。 link.exe はオブジェクトファイルの情報からこれを自動的に検出し、実際には(imp内の)func3 を exe へスタティックリンクしません。 このときlink.exeはアプリ内で実際に使っている func2 のみを自動的に選んでひっこぬき、exe へスタティックリンクします。 このようにして、最終的にできあがる exe ファイルは、元々のソースコード内で実際に使っていた関数のみをdllへ要求するようなバイナリに仕上がったわけです。

    ここまで長々と説明してきましたが、実はこのことは新しいWindows SDK(のインポートライブラリ)をリンクして作ったアプリが古いWindowsでも(原理的には)動作可能な理論的な根拠となります。 新しいWindows SDKでは古いWindowsでは存在しない新しいWindows API関数がインポートライブラリに含まれます。 そんなものをlink.exeでリンクした瞬間、そのアプリは古いWindowsでは使えなくなってしまうんじゃないかと思ってしまうかもしれませんが、実は単にリンクしただけではただちにそうはならないというのがこれまで述べてきたことです。

    極端な話、Windows SDK 8.1内の(新しいWindows API関数の登録を含む)インポートライブラリを使ったとしても、それだけでただちにWindows 2000で動作しない exe になるとはいえないということになります。 Windows 2000でサポートされているWindows APIだけをうまく選んでプログラミングすれば原理的には大丈夫というわけです。

    ただし新しい開発環境を使って出来上がった exe ファイルがWindows 2000で動作しない原因は、新しいAPI由来のものだけとは限りません。 VS2010以降(すなわちVC10以降)のリンカ(link.exe)はSUBSYSTEMのバージョンとして5.01(Windows XP相当)以上しか指定できませんので、 これらのVC(というかリンカですが)を使っている場合、どのみちWindows 2000で動作する exe を作ることはできません。 とはいえ、これはインポートライブラリとはまた別の話です。

    Close


  7. Windows SDK 8.1

  8. 必要に応じて選択してください。

    こちらはわかりやすいですね。 文字通りWindows SDK 8.1です。

    従って既にWindows SDK(どのバージョンでもよい. 6.0以前でもよい)を持っている方はインストール不要です。 そちらに環境変数INCLUDEとLIBを設定すれば済むからです(その手順は「環境変数の設定法概論」で述べます)。

    上記の項目3を選んだ方はこの項目4は不要です。 つまり上記の項目(C++ に関する Windows XP サポート)を選んだ方はこちらは不要ですし、 逆にこちら(Windows SDK 8.1)を選んだ方はC++ に関する Windows XP サポートは不要ということです。
インストールするコンポーネントは以上です。 尚、Windows SDK 10は無駄にサイズが大きいため(つまりさらに余計なものが増えているということでしょうが)、今回のミッションでは選択肢から外しますのでご了承ください。

たったこれだけで本当に十分なのか?と思われるかもしれませんが、十分過ぎます。 下の方に「コア機能」とか書いてある項目があなたを誘惑してくるかもしれませんが、実際インストールしてみるとわかりますが中身は全然「コア」じゃありませんので要りません(因みにポップアップされる説明には「サポートするコア機能」とかいう、補助なのかコアなのかよくわからんことが書いてあります)。 最小限インストールをめざすならこれらにチェックはしないようにしましょう。

vs_BuildTools.exeの「個別のコンポーネント」ペインでチェックを入れると、右下に必要な総ファイルサイズが表示されるようになっています。 私の場合、以下のようになりました。 ここまでかなり厳選いたしましたが、それでも尚、結構な容量食ってますね。

システムドライブ(C) 1.47GB
その他のドライブ 1.69GB
必要な領域の合計 3.16GB

これらの内訳についての推察
これらの内訳について、出来る限り推察してみます。

まずシステムドライブ(C) へは 1.47GB 要求されています。

Windows SDKとWindows Universal CRT SDKについては問答無用でCドライブへインストールされますのでこの分の容量が含まれることになりますが、 私はWindows SDK 6.0を既に持っているため、今回は「C++ に関する Windows XP サポート」と「Windows SDK 8.1」には両方ともチェックをつけておりません。 これらの総サイズは前者は150MB程度、後者は450MB程度になるはずですから、例えば仮に後者にチェックした場合、およそ2GBに達するということです。 ずっと上の方の注意事項でCドライブに2GB以上の空きが必要と書きましたが、その数字の根拠がこれになります。 Windows Universal CRT SDKのサイズがおよそ500MBですので、残りの1GB程度はインストール作業の途中で一時領域として必要となるサイズ容量であると考えられます。 これらの多くはおそらくシステムのTEMPフォルダに入れられるものと思われます。 この部分はインストール作業が終了すればおそらく自動的に解放されるサイズであろうことが期待できます。 また仮にTEMPフォルダ内にゴミが残っていてもそれらは手動で削除可能でありましょう。

次にその他のドライブ へは 1.69GB 要求されています。

今回は私は「インストール場所」ペインにおいて「Visual Studio IDE」の欄を「E:\MyVStudio\2017\BuildTools」、「ダウンロードキャッシュ」の欄を「E:\MyVStudio_DownloadCache\Packages」としました。 今回は敢えてCドライブ以外、デフォルトのパス以外に変えて試しております。 そのため、その他のドライブへの空き容量も要求されているわけです。 ちなみにこれらの値のデフォルトはそれぞれ、C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools、C:\Program Files (x86)\Microsoft Visual Studio\Packages となります。

今回の私の設定では、C/C++コンパイラ本体はE:\MyVStudio\2017\BuildTools へとインストールされるはずでありますが、このサイズがおよそ1GBとなります。 残りの約0.69GBはダウンロードキャッシュ(E:\MyVStudio_DownloadCache\Packages)で使われるものと思われます。 実際、今回Microsoftのサイトから受信した総バイト量を計測してみると確かに650MB程度でありましたので計算が合います。 そしてこのDownloadCacheの分のサイズもまたインストール作業が終了すれば解放できるサイズということになります。

以上まとめますと、最終的には実質1.5GB程度にまとまるということになります。 ただしWindows SDK 7.1A(150MB程度)を含めた場合ですと1.6GB弱、Windows SDK 8.1(450MB程度)を含めた場合ですと2GB程度といった感じになります。 VCでは例えば単純にlibディレクトリだけをみてもPDBファイルや最近のものですとDirectX SDK、そしてx86/x64以外の環境向けのlibファイルなどが含まれており、これらに関してはやむを得ない部分もありますが、 MinGWの150MBと比べるとさすがに気になるデカさですね(界王拳並の倍率です)。

Close


今回のミッションでは、最終的に余計な贅肉を手動で削ぎ落とす必要がありそうです。

目次に戻る

全部ダウンロードしてからインストールする

インストール項目を選択したら、いよいよ実際にダウンロードおよびインストールを始めますが、その前に一旦モードを変更します。 右下の方に「ダウンロードしながらインストールする」とありますが、これをクリックして「全部ダウンロードしてからインストールする」に変更します。 このモードにすると、まずは全部のパッケージファイルを一旦ダウンロードキャッシュにダウンロードし終わってから、インストール処理に入ります。

なぜこのモードを選ぶかと言うと、その方が万一トラブルが起きた場合、原因を識別しやすいと思うからです。 「Visual Studioのインストーラあるある」ですが、インストールのプログレスバーが止まる現象が起きます。 このとき「ダウンロードしながらインストールする」モードですと、内部でなんらかの異常が発生して無限ループに陥っているのか、それとも単に通信環境がたまたま悪くダウンロードが激遅なためによるものなのか判断できません。

一方「全部ダウンロードしてからインストールする」モードですと、すくなくともダウンロードが済んでいなければネット通信系の問題と疑えますし、逆にダウンロードが済んでいればその可能性は除外できます。

最後にすぐ右にある「変更(M)」ボタンを押すとダウンロードおよびインストールが始まります。 ダウンロードを示すプログレスバーとインストールを示すプログレスバーの二つが表示されます。 まず前者が100%になった後、後者が100%になるといった流れになるはずです。

参考: インストールのプログレスバーが止まるその他の原因
インストールのプログレスバーが止まる現象が起きる要因は他にもあります。

例えばインストールが進んでいきCドライブの空き容量が本当にギリギリに近くなった場合、おそらく残り容量が200MBを切ったあたり(0MBではありません)でインストーラのプログレスバーが全く動かなくなる現象が起こります。 このときvs_BuildTools。exeがその原因をメッセージ表示してくれればいいのですが、そういった表示も一切なされません(後でログを見ると、そのとき止まっていた原因がわかったりします)。

待ってる側は何らかのエラーが起きているため止まっているのか、それとも単にインストールに時間がかかっているだけなのかの判断が全くつきません。 いつまで立ってもプログレスバーが全く動かない(目安として15分を超える)ようであれば、「一時停止」してまた「再開」して見るとか ディスクの残り容量、ダウンロード速度なども気にしてみる必要があるかもしれません。


幸いにしてインストールのプログレスバーが100%になればインストール完了です。

しかし最後まで油断はできません。ここで驚くべきことにWindowsの再起動を要求されます。 うっかり「再開」ボタンを押すと自動的にWindowsが再起動されますので、何か同時に作業されていた場合は一旦中断しましょう。

無視して再起動しなかった場合どうなるか?
そもそもWindowsの再起動までも要求してきたということは、このインストーラは裏で我々が感知しない何かをコピーし、 システムに変更を加えていたということになります。 ただこれは現象の性質からして「おま環」である可能性もあります。

念のため、最近システムにインストールされたプログラムをコントロールパネルの「プログラムと機能」から見てみました。 インストーラ自体(vs_BuildTools。exe)、VS2017版の再配布ランタイム、そして最初にインストールした.NET Frameworkがある程度で、再起動が必要そうなインストールは見当たりませんでした。 それ以外の何かが実は裏でインストールされたということになりますかね?

再起動をする前の段階でも、今回我々が欲しかった全ファイルはすべて所定の位置にコピーされてはおりました。 従って今回の我々の目的に即して考えるなら、この再起動は全く必要はないと思います。

とはいえ、とりあえずここはMS様の指示通り、素直に再起動しておきましょう。


目次に戻る

インストールされたものの確認

では、要求したものがこちらが指定した位置に確かにインストールされているか、さもなくばそれはどこにインストールされているかの確認作業に入ります。

C/C++コンパイラ本体については「インストール場所」で指定した(今回の私の場合ですとE:\MyVStudio\2017\BuildTools)にインストールされます。 しかし一方、Windows Universal CRT SDK と Windows SDKについてはその指定を無視して固定位置にインストールされることに注意してください。
  1. VC++ 2017 version 15.9 v14.16 latest v141 tools(C/C++コンパイラ本体)
  2. ユーザが指定した場所。 デフォルト値は C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools
  3. Windows Universal CRT SDK
  4. C:\Program Files (x86)\Windows Kits\10
  5. C++ に関する Windows XP サポート(Windows SDK 7.1A)
  6. C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A
  7. Windows SDK 8.1
  8. C:\Program Files (x86)\Windows Kits\8.1
これらは「そこへインストールされた」というメッセージ表示すらインストーラからは一切なされません。 よって上記の位置を知っておかないとインストールが終わった後、どこにインストールされたのか悩むことになります。

インストール位置が固定であるにも関わらずWindows SDK 7.1以前と8.0以降でのインストール位置が統一されておらず紛らわしいですが、 上記を参考にbin, include, libディレクトリなどが内部に含まれているか否かを確認しましょう。 Windows Universal CRT SDKに至っては、Cコンパイラ本体と不可分な必須コンポーネントであるにも関わらず、知らないと全く想像できないような位置にあります。 これはおそらくWindows SDK 10においては、Windows Universal CRT SDKをデフォルトで含むような構成になっていることに由来するのでしょう。

Visual Studio 2017以降、コンパイラ本体(cl.exeなど)が配置されている位置が奥深くに移動し、非常にわかりにくくなったのでここで補足しておきます。 BuildToolsあるディレクトリから、さらに以下のようにディレクトリを潜っていきます。

BuildTools\VC\Tools\MSVC\14.16.27023

最後の14.16.27023という部分はバージョンによって名前が異なるのでしょう。 ただしMSVCディレクトリの中にはこの名前のディレクトリしか存在しないため、迷うことはないと思います。 ここまで潜るとようやく bin, include, lib ディレクトリなどが現れます。

Visual Studio 2015以前では「VC」という名前のディレクトリの直下にこれらがありました。

binディレクトリ配下にはcl.exeなどのコンパイラコマンド群の本体が配置されています。

includeディレクトリ配下には STLのヘッダ(いわゆるC++の標準ライブラリですね)があります。 しかし C標準ライブラリ(例えばstdio.h)はここにはありません。 上述したようにこれらは Windows Universal CRT SDK の一部となっていますので、そちらのインストールディレクトリ内にあります。

libディレクトリ配下には libcmt.lib などがあるはずです(いわゆる古から伝わるC標準ライブラリの本体です)。

最終的に環境変数PATHにはbinディレクトリへのフルパス、環境変数INCLUDEにはincludeディレクトリへのフルパス、環境変数LIBにはlibディレクトリへのフルパスを「おおよそ」追加指定する方針になります。 上記2, 3, 4 についても基本的には同じ方針で同じ環境変数を設定します(従って一つの環境変数につき複数のパスを指定することになりますが、これは ; 文字で区切って指定します)。

上で「おおよそ」と曖昧に書きましたのは、bin配下にさらにx86版やx64版といった細かなサブディレクトリが途中に存在したりと、若干の注意点があるためです。 これらの環境変数の設定の仕方についての詳細は、「環境変数の設定法概論」のセクションで詳しく述べます。

目次に戻る

環境変数の設定法概論


VCを動作させる3つの環境変数


ここではVCコンパイラとリンカ、ならびにその他の基本コマンドを最低限動作させるために必要な環境変数とその設定方法について詳しく述べます。

VCを動作させるためには、3つの環境変数 PATH、INCLUDE、LIB の設定が必要です。
  1. PATH

  2. これはVCに限った話ではありませんが、一般にWindowsのコマンドプロンプトからコマンドを打って実行する場合、 そのコマンドの実体(exeファイル)のあるディレクトリのパスを、環境変数PATHに「;」で区切って設定しておかなければなりません。

    また、ダイナミックリンクするdllの置かれたディレクトリのパスもまた、環境変数PATHに「;」で区切って追加的に設定しておかなければなりません。 exeが起動するとき、Windowsは環境変数PATHでリストされたパスを最初から順に検索し、ダイナミックリンクに必要なdllを検索します。 このパス指定に不備がある場合、exeファイルがとりあえずみつかって起動まで辿り着けたとしても、起動時ダイナミックリンクに失敗して起動が中断することになります。 cl.exe や link.exe 等はVisual Studio内のいくつか別のディレクトリ内にあるdllにも依存していることが往々にしてあります (しかもその位置はVisual Studioのバージョンによっても変わり、割とバラバラです)。 それらのディレクトリのパスもすべて漏れなく指定しておかなければなりません。

    それらを見つけ出す作業を完全に解説するのはさすがにこの記事の想定レベルを超えますので、最終的には私が作成したdetect_vc.batを使って これをある程度自動的に検出することにします(detect_vc.batについては後述します)。

  3. INCLUDE

  4. VCの場合、インクルードするヘッダの存在するディレクトリの検索パスを、環境変数INCLUDEに「;」で区切って設定しておかなければなりません。

    必要なものは基本的にはC標準ライブラリのヘッダ(stdio.hなど)が格納されているディレクトリのパス、 Windows SDKのヘッダ(windows.hなど)が格納されているディレクトリのパスです。 ただし、VS2015以降(VC14以降)では Windows Universal CRT SDKにより、C標準ライブラリのヘッダが格納されているディレクトリがもう一つ存在しますので そちらも指定することになります(実はこちらにstdio.hが存在します)。

    つまり今回、環境変数INCLUDEに指定すべきパスは少なくとも3つあります。
    自力で設定するとなると中々大変です。

  5. LIB

  6. VCの場合、リンクするインポートライブラリまたはスタティックライブラリの存在するディレクトリの検索パスを、環境変数LIBに 「;」で区切って設定しておかなければなりません。

    必要なものは基本的にはC標準ライブラリのライブラリ(libcmt.libなど)が格納されているディレクトリのパス、 Windows SDKのインポートライブラリ(kernel32.libやuser32.libなど)が格納されているディレクトリのパスです。 ただし、VS2015以降(VC14以降)では Windows Universal CRT SDKにより、libucrt.libと呼ばれるライブラリが格納されているディレクトリも指定しなければなりません。 libcmt.lib、libucrt.lib、kernel32.libはlink.exeコマンドにおける引数に登場しませんが、これらは裏で必ずリンクされるものになります。

    つまり今回、環境変数LIBに指定すべきパスは少なくとも3つあります。
    こちらも自力で設定するとなると中々大変です。

仮にIDEを使っている場合でも、まず内部でこれら3つの環境変数を(一時的にでも)定義して cl.exeコマンドやlink.exeを呼び出しているはずです。

参考: setvars32.batとVsDevCmd.bat
setvars32.batやVsDevCmd.bat と呼ばれる環境変数設定用のバッチスクリプトがCommon7/Toolsなどに用意されていることがあります。 ただこれは(将来のバージョンではどうなるかわかりませんが)内部でレジストリの情報を参照しています。 つまり一旦(IDEつきの)Visual Studioを普通にインストールした状態でのみ使えるものです。

またレジストリの情報を参照しているということはポータブルではないということです。 Windows SDKやVCコンパイラ本体の位置を変更したり、他のマシンへ移動したりした場合にこれらのバッチファイルでは(少なくとも環境変数を微調整するレベルでは)対応できないということになります。 従って今回はこれらを使いません。

実はMoaiの旧バージョンではそのソースコードのビルドシステムの内部でこれらを使っておりましたが、 以上の理由によりMoaiの新しいバージョンのソースコードでは使わないことになりました。


とはいえVisual Studioのディレクトリ構成を知っていないと、この3つの環境変数を自力で設定するのは中々に骨が折れます(特にPATHが)。 では具体的にどうするかといいますと、次のような選択肢があります。
  • この記事を隅々まで読んでコマンドプロンプトから自力でセットする

  • 一番面倒です。

  • この記事を隅々まで読んで設定用のバッチファイルを自力で書く

  • 一番敷居が高いです (一応その開発の取り掛かりとなりそうなサンプルを「3分(かそこら)では速習できないバッチファイル講座: 上級編」で解説してはおりますが)。

  • 私が作成したdetect_vc.batである程度自動的にセットさせる

  • このバッチファイルを使いますと、最低限の環境変数を与えるだけで上で説明した3つの環境変数が自動的に設定されます。 最初の取り掛かりとしては割とよい選択肢ではないかと思います。

  • Moaiで用意されているauto_trigger.bat(mkfsys)、あるいはRaraku Studioを使う

  • これを使うと、最低限の環境変数を与えるだけで自動的に設定され、コンパイル、リンクまで行えます。 またMakefileの自動生成にも対応します。 私も普段はこれでやっておりますが、ただしMoaiを別途用意する必要がありますね。

  • おとなしくIDEをインスコ

  • おとなしくMinGWをインスコ

3番目が一番簡単ですぐに試せそうです。 というわけでここでは detect_vc.bat を使い、この設定を自動的に行うことにします。

準備


環境変数の設定はWindowsの環境変数設定ダイアログを使用することでも可能ですが、あれは色々テストして試行錯誤する分には使いにくいため、 この記事ではまずコマンドプロンプトおよびバッチファイルを用いてこれを設定する方法について解説します。

コマンドプロンプトやバッチファイルの文法がよくわからない方のために、とりあえずこの記事で使用する分の知識が3分で速習できる非常にやさしい講座を以下にご用意しました(Windows2000以降のコマンドプロンプト、バッチファイルを前提といたします)。 このあたりの知識に不安がある方はご活用ください。

鬼軍曹による3分で速習できるコマンドプロンプト講座: その1
鬼軍曹「何ッ?コマンドプロンプトを開く方法がわからないのけ?
よし、ちょっとこっちゃ来い!」

鬼軍曹「まずコマンドプロンプトにて作業を行いたいフォルダをエクスプローラから開け!
そのフォルダのアイコンにマウスカーソルを合わせShiftキーを押しながら右クリックするッ!
ブルダウンメニューが表示されるので、「コマンドウィンドウをここで開く」を選択するっちゃ…するのだっ!」

Shiftキーを押しながらというのがここでのポイントです。 そうしないと「コマンドウィンドウをここで開く」という項目が出現しません。

鬼軍曹「すると以下のような真っ黒なウィンドウが表示されるはずだっちゃ! これがコマンドプロンプトのウィンドウだっち……ウインドウだッ!。」

C:\Lum>

鬼軍曹「『C:\Lum>』と表示されているが、これは今『C:\Lum』フォルダにいるということ表しているっ!」

鬼軍曹「Windowsの種類によっては上記の方法がデフォでは使えないことがあるッ!
その場合は、スタートメニューから開く方法もある。
スタートメニュー?なんだっちゃそれは?』な方はいるけ?
画面一番左下にあるWindowsのロゴが入ったボタンがあるだろう?
ソイツだ!」

鬼軍曹「なにィっ!?『そんなものは見当たらない…』だとォ?
どうしよう…テンちゃん…。
とりあえず『コマンドプロンプト Windows8』をキーワードにしてググって欲しい(責任放棄)」

鬼軍曹「スタートメニューはあると仮定するぞ!?
これを開いたら以下の手順で項目を選ぶっ!
Windowsの種類によっては手順が多少異なるので、それぞれについて記述しておくっちゃよ!」

  • WindowsXP/Windows2000の場合

  • 「プログラム」⇒「アクセサリ」⇒「コマンドプロンプト」の順となる。

  • Windows7の場合

  • 「すべてのプログラム」⇒「アクセサリ」⇒「コマンドプロンプト」の順となる。

  • Windows8/8.1の場合

  • スタートメニューに該当するボタンないので別の経路で起動する。 「スタート」と表示されている画面内で右クリックすると、画面一番右下に「すべてのアプリ」というものが現れる。 これを押すと画面上に「コマンドプロンプト」のアイコンが現れる。 これを押す。

  • Windows10の場合

  • 「Windows システムツール」⇒「コマンドプロンプト」の順となる。


補足: cdコマンドによるフォルダ(=ディレクトリ)の移動
フォルダのことをディレクトリとも呼びます。 特にコマンドプロンプト上ではディレクトリという用語を使うことが多いです。 またその位置は E:\mathematics\ero_gazo のように表記されます。

スタートメニューからコマンドプロンプトを開いた場合、実際に作業を行うディレクトリまでコマンドプロンプト上で移動する必要があります。 移動には cd コマンドを使い、引数には移動先を指定します。

例えば今現在C:ドライブ内におり、その中のZenkakuというディレクトリに移動したいならば、cd Zenkaku と入力し、最後にEnterキーを押します。 これにより、コマンドプロンプト上で「cd Zenkaku」というコマンドを実行することになります。 「C:\Zenkaku>」と表示されたなら成功です。現在の位置がC:\Zenkakuに変わっています。 その様子を以下に示します。

C:>
C:>cd Zenkaku
C:\Zenkaku>

今いる位置から他のドライブ、例えばEドライブ内のmathematicsディレクトリ内のero_gazoディレクトリに移動したい場合には、 まず現在のドライブ(カレントドライブ)を変更しなければなりません。 このためには単に E: と入力してEnterキーを押します。 「E:>」と表示されたなら成功です。現在の位置がEドライブのトップに変わっています。 その様子を以下に示します。

C:\Zenkaku>
C:>E:
E:>

その上で cd コマンドで目的のディレクトリ(この例の場合E:\mathematics\ero_gazoへ移動します)。 あるいは次のように/d オプションをつけて一気に移動することもできます。

C:\Zenkaku>cd /d E:\mathematics\ero_gazo
E:\mathematics\ero_gazo>

/d オプションをつけずに単にcd E:\mathematics\ero_gazoとやった場合、異なるドライブを跨る場合は移動できないので注意してください。 しかもこのとき特に何のエラーも表示されないため、バッチファイルなどでこれをやらかすとハマることがあります。

同一ドライブ内を移動する場合であれば /d オプションがなくても大丈夫ですが、常に /d をつけておけば間違いはありません。 特にバッチファイルで移動する処理を書く場合は常に cd /d として記述すべきです。

ディレクトリ名が長い場合などはキーボードから正確に打つのが大変ですが、最近のコマンドプロンプトではこのようなディレクトリのパスを入力する時に tabキーを使えば、自動的な補完ができます。 たとえば m を打ってtabキーを押すだけで、配下に名前が m から始まるディレクトリ(mathematicsなどのディレクトリ)が存在するなら それを自動的に補完します。 そのようなディレクトリが存在しないならばtabキーを押しても何もおきません。 また名前が m から始まるディレクトリがmathematics以外にも複数存在するようなケースでは、 tabキーを続けて連打すると補完の候補を次々と変えていくこともできます。 大変便利なので是非活用してください。

今回は一つずつ順に移動しましょう。 すなわちまず cd mathematics を実行し、次に cd ero_gazo を実行します。 その様子を以下に示します。

E:>
E:>cd mathematics
E:\mathematics>
E:\mathematics>cd ero_gazo
E:\mathematics\ero_gazo>

今いる位置から一つ上のディレクトリへ上がりたいとします。 このときは一つ上を意味する「..」という特別な記号列を用いて、cd ..のように実行します。 その様子を以下に示します。

E:\mathematics\ero_gazo>
E:>cd ..
E:\mathematics>

この「..」を「\」記号で区切って連続させ、今いる位置から2つ上のディレクトリへ一気に上がることもできます。 cd ..\.. のように実行すればよいです。 その様子を以下に示します。

E:\mathematics\ero_gazo>
E:>cd ..\..
E:\>
Close


鬼軍曹「コマンドプロンプトを閉じるにはキーボードから exit と入力してEnterキーを押すか普通にウィンドウ右上にある x ボタンを押す!」

鬼軍曹「以上がコマンドプロンプトの鬼本の鬼の字だっちゃ!
全くの初心者の方は10回くらいコマンドプロンプトを開けて閉じてのスクワットを繰り返し、確実にマスターしておくことォ!」

Close


鬼軍曹による3分で速習できるコマンドプロンプト講座: その2
鬼軍曹「またおまいけーっ!」

鬼軍曹「まあいいっちゃ。 今回は環境変数を設定する方法だっちゃ…方法だっ!!
それにはsetコマンドを使うッ!たとえばこんな感じだッ!」

set MY_DARLING=Ataru

鬼軍曹「今設定した環境変数MY_DARLINGの値を確認するにはsetコマンドを使うッ!
単にsetと打って実行するだけでいい!
簡単だな?」

set

鬼軍曹「これだけで今設定されているすべての環境変数の一覧が見れる!」

鬼軍曹「何ィ!?他にも設定されている環境変数が余計にズラズラと表示されて邪魔ってか!?
うるせえやつらがいっぱい表示されたってか!?
どうしよう…テンちゃん…」

鬼軍曹「と…とりあえず、アルファベット順に並んで表示されているはずなので、適当にウィンドウをスクロールさせろ!
MY_DARLINGは、Lで始まる人のそばにいるはずっ!
MY_DARLINGの値がAtaruになっていることを確認するっちゃ!」

…
LOGONSERVER=\\UFO
MY_DARLING=Ataru
NUMBER_OF_PROCESSORS=4294967295
OS=Windows_NT
…

補足: echoコマンド
設定した環境変数は、echoコマンドを使って確認することもできます。 echoコマンドはC言語におけるprintf関数もどきのようなものと思ってもらって結構です。

例えば環境変数windirの値が見たいならば、echo %windir% を実行します。 環境変数の名前を%で挟む所がポイントです。 その様子を以下に示します。

C:\Zenkaku>
C:\Zenkaku>echo %windir%
C:\Windows

C:\Zenkaku>

因みに環境変数windirはシステムで最初から定義されている環境変数で、Windowsディレクトリのフルパスが値として格納されています。


鬼軍曹「ここで一つ注意点だ!
今うちが設定した環境変数MY_DARLINGは、このコマンドプロンプトのウィンドウの中でのみ有効だ。
このウィンドウの外にはMY_DARLINGの値は反映されない!
よそのウィンドウでMY_DARLINGの値を参照してもAtaruは入っていないっちゃ!」

補足: 環境変数の2通りの寿命
一方、Windowsの環境変数設定ダイアログで設定された環境変数については、すべてのコマンドプロンプトウィンドウ、ならびにすべてのアプリから参照可能です。 またこのような環境変数の値を仮にコマンドプロンプト内の set コマンドで上書きしても、コマンドプロンプトのウィンドウの中で一時的にその値になりはしますが、 Windowsの環境変数設定ダイアログにおける値がそれで上書きされるわけではありません。 この二つはC言語におけるローカル変数とグローバル変数の関係にも似ています。

set コマンドにより設定した環境変数は所詮一時的なものであり、一方、Windowsの環境変数設定ダイアログで設定された環境変数は PCの電源をOFFにしてまたONにしても記憶されている永続的なものとなります。


鬼軍曹「コマンドプロンプトのウィンドウを閉じれば、setコマンドにより設定した環境変数もすべて自動的に消える。
だからもし何か間違った設定をしてよくわからん状態になったら、一旦コマンドプロンプトウィンドウを閉じる!
そしてまた新しく開きなおせばすべてがリセットされた状態から始められるっちゃよー!」

鬼軍曹「それでも setコマンドで設定した環境変数MY_DARLINGを明示的に削除したければ、以下のように set MY_DARLING= を実行する!
つまりsetコマンドを使って削除したい環境変数の値を空に設定する!」

C:\Lum>set MY_DARLING=

C:\Lum>
C:\Lum>set
…MY_DARLINGが存在しないことを確認…
C:\Lum>

鬼軍曹「これで環境変数MY_DARLINGそのものが set コマンドによるリストアップから消える! Ataruが削除されただけでなく、MY_DARLING自体も綺麗サッパリ消えたっちゃ…」

Close


3分(かそこら)で速習できるバッチファイル講座: その1

ここではバッチファイルを書いたことも作ったこともない方向けに、その方法を学びます。 また環境変数PATHとその役割についても例を踏まえて説明します。

batファイルの作成と起動
batファイルの作成と起動

バッチファイルとは拡張子が bat となっているファイルであり、エクスプローラからダブルクリックして実行することができます。 このときコマンドプロンプトウィンドウが自動的に開き(このプログラムの実体はC:\Windows\system32\cmd.exeであり、これが起動しています)、 中に記述されているスクリプトが実行され、その結果がそのウィンドウ内に出力されます。

何はともあれ、まずもっとも簡単な Hello World バッチファイルから始めましょう。 まずtest1.batという空のファイルを作り、あなたのテキストエディタでそれを編集し、次のような内容にして保存しましょう。

@echo off
echo Hello World.
pause

これをダブルクリックして実行してみます。 無事コマンドプロンプトウィンドウが開き、その中で Hello World. が表示されたでしょうか?

では各行の解説をします。
  1. @echo off
  2. すべてのバッチファイルの最初の行に書くおまじないと考えてもらってもいいです。 これの値は on がデフォルトですが、これが on の状態ですと以降の行で実行されるすべてのコマンドにおいて、 それを呼び出した(引数等を含めた)コマンドの記述自体もそのままコマンドプロンプトウィンドウ内に出力されてしまいます。 batファイル実行においては多くの場合、これでは出力結果が非常に醜くなり困りますので、通常は off にしておくわけです。

  3. echo Hello World.
  4. C言語におけるprintf( "Hello World.\n" );と同じと考えていただいて結構です。 コマンドウィンドウプロンプトの画面に、指定した文字列(今回の場合 Hello World)を出力します。 ただしHello World.の部分をダブルクォートで囲う必要はありません(というかダブルクォートそのものが出力されてしまいます)。 echoコマンドの後ろにスペースを一つ置きますがこのスペースだけは出力の対象となりません。 それ以降のすべての文字が%記号を除きそのまま出力されます(%記号については後述します)。

  5. pause
  6. ユーザからの何らかのキーボード入力(なんのキーでもよい)を待つコマンドです。 バッチファイル終了時、通常コマンドプロンプトウィンドウは即座に閉じてしまいます。 これでは画面に何か文字列を表示させるようなスクリプトの場合、それを読み切る前にウィンドウが閉じることになります。 この pause を入れておけば、ユーザからの何らかのキーボード入力があるまではコマンドプロンプトウィンドウは閉じずに保留され、 画面に何が表示されているかをゆっくりと読むことができるわけです。 そのコマンドプロンプトウィンドウを閉じたければキーボードから何か入力するか、ウィンドウの右上にある x ボタンを押せばよいです。
Close


コマンドプロンプト上からの起動と環境変数PATH
コマンドプロンプト上からの起動と環境変数PATH

batファイルはコマンドプロンプト上からもこれを実行させることができます。 たとえば現在Zenkakuディレクトリにいるとして、その直下にあるtest1.batという名前のバッチファイルをコマンドプロンプト上から実行させるには以下のように 単に test1.bat と入力してEnterを押すだけで結構です(Press and key to continue ...の部分は日本語で表示されることもあります)。

C:\Zenkaku>
C:\Zenkaku>test1.bat
Hello World.
Press any key to continue ...
C:\Zenkaku>

現在Zenkakuディレクトリにいない場合、例えばC:\Yakisobaディレクトリにいる場合、C:\Zenkaku\test1.bat を実行させるには 以下のように ..を駆使したパス(これを相対パスと呼びますが)を前に付加し、..\Zenkaku\test1.bat を入力して実行します。

C:\Yakisoba>
C:\Yakisoba>..\Zenkaku\test1.bat
Hello World.
Press any key to continue ...
C:\Yakisoba>

あるいは以下のように フルパス(これは絶対パスとも呼びますが)で、C:\Zenkaku\test1.bat を入力して実行します。

C:\Yakisoba>
C:\Yakisoba>C:\Zenkaku\test1.bat
Hello World.
Press any key to continue ...
C:\Yakisoba>

あるいはまず環境変数PATHをセッティングします。 環境変数PATHとは、実行するファイルを単に名前としてだけ入力してEnterキーを押した場合に、 そのファイルが実際に置かれたディレクトリを検索するためのパスの候補が「;」文字を区切りとして複数リストされたものです。

その値はデフォルトで既に指定されており、環境によっても異なりますが echo %PATH% を実行すると見ることができます。 例えば C:\Windows\system32;C:\Windows のようになっていた場合、(すべてが1行にまとまっていて見づらいですが) C:\Windows\system32 と C:\Windows の二つのディレクトリのパスが指定されているという意味になります。

この状態でコマンドプロンプトから単に cl と入力してEnterを押しますと、C:\Windows\system32 と C:\Windows の二つのディレクトリから 実行ファイル cl.exe や cl.bat 等が存在するか否かを、この順番で検索します。 そしてそれが一番最初に見つかった位置に存在する cl.exe や cl.bat が実際に実行されるという仕組みです (このとき拡張子まで明示的に指定して「cl.exe」と入力しても構いません)。 一方、これが環境変数PATHで指定された範囲においてはどこにも見つからなかった場合、以下のようなメッセージが表示されて実行に失敗します。

'cl' is not recognized as an internal or external command,
operable program or batch file.

ではtest1.batを実行させる話に戻りましょう。 環境変数PATHでデフォルトで設定されている値は通常消去したくはないし、多くの場合消去するべきではありません。 そこでこれの値を完全に上書きするのではなく、その後ろに「;」区切りで新しい検索パスを追加するのが普通です。 そのためには set PATH=%PATH%;C:\Zenkaku といった形で set コマンドを実行するのが定石的な記述となります。 %記号で囲まれた %PATH% は既に設定されている環境変数PATHの値を参照するための記法です。 この後ろに ;C:\Zenkaku と続いたものが結局、環境変数PATHに再設定される構文となっています。

C:\Yakisoba>
C:\Yakisoba>set PATH=%PATH%;C:\Zenkaku
C:\Yakisoba>

環境変数PATHの設定が整ったら、いよいよtest1.batを実行します。 単に test1.bat と入力して Hello World. と表示されれば成功です。

C:\Yakisoba>
C:\Yakisoba>test1.bat
Hello World.
Press any key to continue ...
C:\Yakisoba>
Close


3分(かそこら)で速習できるバッチファイル講座: その2

ここではバッチファイルの便利で基本的な文法について学びます。 バッチファイルに関してはここまでの知識があれば、この記事の内容を理解するには概ね十分かと思います。

callコマンドの基本
callコマンドの基本

バッチファイル内から別のバッチファイルを呼び出すこともできます。 例えばtest2.batを実行中、test1.batを呼び出して実行するにはcallコマンドを使います

まずtest1.batとtest2.batをあなたのテキストエディタで編集し、次のような内容にして保存しましょう。 (test1.batについてはその1で使ったものと全く同じ内容です)

test1.bat
@echo off
echo Hello World.
pause

test2.bat
@echo off
echo Test2 Start.
call test1.bat
echo Test2 End.
pause

今回はtest2.batをダブルクリックして実行することにします。 コマンドプロンプトウィンドウが開き、以下のような表示になれば成功です。 まずtest2.batが実行され、途中test1.batが実行された後、再びtest2.batに戻ってきています。

Test2 Start.
Hello World.
Press any key to continue ...
Test2 End.
Press any key to continue ...

参考: 頭に call を付けなかった場合どうなるか?
バッチファイル内から他のバッチファイルを呼び出して実行するとき、 call をつけないと多くの場合期待した動作とはなりません。

今回の例では、test2.bat 内に単に「test1.bat」と記述するだけでは問題があります。 このようにしてしまうと、test1.bat に入ったとき、元々 test2.bat にいたという情報が消え、 test1.bat の中身に関してはきちんと実行されはしますが、それが終了した瞬間、test2.batに戻ることなく全体が終了してしまいます。 多くの場合、test1.batをサブルーチン的に呼び出し、再びtest2.batの呼び出し元へ制御を戻して欲しいはずですから、call コマンドを使うわけです。

上記では「全体が終了する」とすこし雑に書きましたが、もっと正確に言えば、call test1.bat と実行したとき、 元々 test2.bat にいたという情報がcmd.exeの内部に保持されたスタックへプッシュされます。 (逆に言えば、callコマンドを付けなかった場合、この情報がスタックにプッシュされません)。 そして test1.bat の処理が全部終わった瞬間に今度はそのスタックからポップし、その情報から戻るべき場所を決めるわけです。

従って仮に call を付けずに test1.bat を呼び出した場合、「test2.batへ戻るべきである」という情報がスタックにプッシュされていないわけですから、 test1.bat の処理が全部終わった後に test2.bat へ戻ることはできません。 スタックのトップに格納されている情報は test2.bat のものではなくそれとは別のバッチファイルのものです。 例えば test2.bat をcallコマンドで呼び出した第3のバッチファイル test3.bat があるならそちらに戻るでしょう。 そうではなく test2.bat が最初に実行されたバッチファイルである場合は、スタックはまだ空ですが、 この場合バッチファイルのプロセス全体が終了しコマンドプロンプトウィンドウが閉じられることになります。

ただしコマンドプロンプトウィンドウを単独で開いて、手入力でtest1.batを実行する場合に限っては、 単に「test1.bat」と入力して実行させることができます。 このときに限り特例的に「call test1.bat」と頭にcallを付けて実行させたのと同じ効果になり(つまりスタックに呼び出し元の情報がプッシュされ)、 test1.bat が終了してもコマンドプロンプトウィンドウが閉じられることはないわけです。


Close


callコマンドで環境変数を設定したbatファイルを呼び出す
callコマンドで環境変数を設定したbatファイルを呼び出す

次に環境変数PATH、INCLUDE、LIBを設定するようなbatファイルを作成します。 hello_config.batというファイルを作り、内容を次のようにしましょう。 今回は最後のpauseは除去しておきます。

列の位置を合わせて見やすくするためsetとPATHの間、ならびにsetとLIBの間にスペースを何個か入れておりますが、 この部分に追加でスペースを入れるのは構いません。 ただし「=」文字の直前にスペースを入れてはいけません。 また「=」文字の直後にスペースを入れた場合は、そのスペースが値として認識されて格納されてしまうので、 今回のケースではできません。

hello_config.bat
@echo off
echo Now setting INCLUDE and LIB...

set    PATH=%PATH%;C:\MyVC\bin
set INCLUDE=C:\MyWindowsSDK\include;C:\MyVC\include
set     LIB=C:\MyWindowsSDK\lib;C:\MyVC\include

次にhello_show.bat内で環境変数INCLUDEとLIBの値を表示するよう改造します。 hello_show.batの内容を次のように変更しましょう。 こちらは最後のpauseを残しておきます。

ちなみにechoコマンドにおいて、echoの直後以外のスペース、「=」文字、カギカッコ文字に意味はありません。 単なる表示すべき文字です。この例では「%」のみ特殊な文字で、これは環境変数を展開する意味で使われます。 %変数名% という書式となりますが、これを表示すべき文字列の中に埋め込むこともできます。

hello_show.bat
@echo off
call hello_config.bat

echo PATH   =[%PATH%]
echo INCLUDE=[%INCLUDE%]
echo LIB    =[%LIB%]
pause

hello_show.batをダブルクリックして実行します。 コマンドプロンプトウィンドウが開き、以下のような表示になれば成功です。 hello_config.bat内で設定されたPATH、INCLUDE、LIBの値がhello_show.batにおいても参照できています。 環境変数PATHの値だけは元々設定されている値がお使いのシステムによって異なることがあります。

Now setting INCLUDE and LIB...
PATH   =[C:Windows\system32;C:\Windows;C:\MyVC\bin]
INCLUDE=[C:\MyWindowsSDK\include;C:\MyVC\include]
LIB    =[C:\MyWindowsSDK\lib;C:\MyVC\include]
Press any key to continue ...
Close


コメント行、ifコマンド、gotoコマンド、ラベル
コメント行、ifコマンド、gotoコマンド、ラベル

行頭にREMで始まる行はコメント行となり無視されます。

ifコマンドでは、環境変数に設定されている文字列を比較したり、指定したファイルのパスが存在するかを判定(if existという書式)し、 それがtrueなら直後のコマンドを実行させるといったことができます。 ただしifコマンドは直後のコマンドも含め、必ず1行で書く必要がありまず。 さらに直後のコマンドは一つしか置けません。 複数のコマンドを実行したい場合は以下で述べるgotoコマンドで一旦別の場所へ飛ばすなど、なんらかの工夫をする必要があります。

ifコマンドでは、環境変数に設定されている文字列の比較処理として「==」演算子が使えます。 しかしC言語のような「!=」演算子なるものは用意されていません。その場合、替わりに if notという書式を使います。 以下の例では%INCLUDE%等の前後をダブルクォートで挟んでおりますが、これは環境変数に設定されている値が空である場合に備えたものです。 (環境変数の値が空であった場合は、"" == "" という形に展開されます。 仮にダブルクォートがないと空のとき左辺も右辺も完全に消滅し「==」だけが残った不正な構文となってしまいます)。

if existという書式においてもダブルクォートでファイル名を囲っておりますが、この場合は例外的にダブルクォートが一般に期待されているような形で認識されます。 つまりダブルクォートで囲まれた中身の値のみが指定されたものとみなされます。

gotoコマンドでは、指定したラベルへジャンプすることができます。 目的地となるラベルの方は「:」で始まる文字列であり、以下の例ですと「:Error_Include」、「:Error_Lib」、「:End」などが相当します。 バッチファイルは制御の構造化機構が貧弱なため、想像以上にgotoを使うことになると思います。

ラベルを呼び出すgoto文の引数の方には「:」は必要ありません。 (goto文の目的地である)ラベルの方にのみ「:」が必要になります。

見やすいさのためタブでインデントしてechoを記述している行がありますが、 このようにコマンドの前にtab文字やスペースを置くのは問題ありません。

@echo off
REM ----
REM This is a sample of if command and goto command.
REM ----
if not "%INCLUDE%" == "" echo Environment variable INCLUDE is already defined.
if not "%LIB%"     == "" echo Environment variable LIB     is already defined.

REM Setting VC environment variables if hello_config.bat exists.
if exist "hello_config.bat" call hello_config.bat

if "%INCLUDE%" == "" goto Error_Include
if "%LIB%"     == "" goto Error_Lib
goto End

REM Error Handlings.
:Error_Include
	echo Error: INCLUDE is not set.
goto End

:Error_Include
	echo Error: LIB     is not set.
goto End

:End
pause

Close


setとechoにおける特殊文字
よくあるパターンが、単に空行だけを表示したい場合です(文と文の間を空けたい場合とかですね)。 このとき「echo:」と記述します。 echoの後ろにホワイトスペースを並べるだけも行けそうに思えますが、そのようにすると「ECHOはなんちゃらです」というエラーが表示されて期待通りになりません。

echo Welcome to This Program!
echo:
echo:
echo I am... your faaather!

あと場合によっては便利なのが「&」や「&&」でこれも特殊文字です。 これらはコマンドとコマンドを連結し、それらを1行で一気に書くためのものです。

厳密に言えば「&&」については、直前のコマンドが成功したときのみ、その直後のコマンドが実行されます。 一方、「&」については、直前のコマンドが成功失敗に関わらず、その直後のコマンドが実行されます。

どちらを使ってもコマンドを1行で連結できますが、特に必要がなければ「&」を使うとよろしいかと思います。

Windowsのバージョンによっては、直前のコマンドが思わぬ終了コードを返す場合があり、 一見成功しているように見えて実は失敗と扱われるようなケースもあります。 例えば「set VAR=」と環境変数VARに空値を設定する場合、Windows 7以降だとこれは成功と扱われますが、 Windows XP以前だとこれは失敗として扱われます(Windows Vistaについては未確認です)。

つまり、以下のようなコードの実行結果はWindowsのバージョン依存になってしまい望ましくありません。

set VAR=&&echo Hello.
set VAR=%NAME%&&goto End

Windows 7以降だと直後の echo Hello は実行されます。 しかしこれを Windows XPへ持ってきた瞬間、set VAR= が失敗とみなされ、echo Hello は実行されなくなります。 また、2行目の例については環境変数NAMEが空値ではない場合はどちらのWindowsでも goto Endが実行されますが、 環境変数NAMEが空値になった瞬間、Windows 7以降では goto End が実行され、Windows XP 以前ではこれが実行されないといった 処理系依存の挙動をしてしまいます。 「&&」で連結する場合はこの点に注意が必要です。

この例の場合「&&」ではなく以下のように「&」で連結させる方が望ましいことになります。

set VAR=&echo Hello.
set VAR=%NAME%&goto End


echoの場合「&」の手前で表示すべき文字列が終了し、その後ろからは次の別のコマンドを書くことができます。 例えば次のような感じです。

echo Hello World.& echo Hello Work.

あるいはifコマンドとgotoコマンドを組み合わせると便利になるケースがあります。 以下の例では環境変数MACHINEに設定された値に応じて表示させるメッセージを変えています。

if "%MACHINE%" == "x86" echo This is x86( Target OS is 32bit/64bit Windows).& goto Report_End
if "%MACHINE%" == "x64" echo This is x64( Target OS is 64bit Windows only).&  goto Report_End
…
:Report_End

setも同様で、「&」の手前でsetが格納すべき文字列が終了し、その後ろからは次の別のコマンドを書くことができます。 例えば次の例はバッチファイルの文法的にはあっています。

set TEST=Hello World& echo TEST is [%TEST%]& goto Set_End

「World」と「&」の間にスペースを入れるとそのスペースも値として格納されますから、それが不要な場合はスペースを入れないように注意します。 しかしこの例にはそれよりもっと重要な注意事項があります。 このように「&」で複数のコマンドを連結した場合、その一塊における個々の環境変数の展開は、個々のコマンドの実行に先立ってまずすべて事前に行われます。 つまり上の例はまず一旦次のように展開されます。

set TEST=Hello World& echo TEST is [環境変数TESTの古い値]& goto Set_End

この状態になって初めて set コマンドと echo コマンドが左から順に実行されます。 結果的にこのとき echo で表示される値は「Hello World」ではなく、環境変数TESTに元々設定されていた古い方の値ということになります。

この性質を逆手にとったテクニックが、規模が大きなバッチファイルの作成では重要です。 これについて興味がある方は、「3分(かそこら)では速習できないバッチファイル講座: 上級編」の 「setlocal、endlocal コマンドと環境変数のエクスポート」もご覧ください。

Close


その他
その他

いわゆる if-else を一見便利に書ける「else コマンドと (...) の組み合わせ」というのもありますが、 これらは特に環境変数の展開に関して紛らわしい注意事項もあるのであまりオススメはしません。 これらはgotoコマンドで代用することができ、そちらの方が安全です。

その注意事項とは「&」で複数のコマンドを連結した場合と同じく、環境変数の展開に関するものです。

ただ「&」であからさまに繋げるときと比べ、 (...) を使うと複数行に分けても記述できるため、 一見すると普通のコードに見えてこのことに関する注意事項がより意識に上がりにくくなるとでも申しましょうか? つまり、コーディングに没頭してるうちに (...) の中であるということをつい忘れ、いつもの調子で書いてしまいがちになります。 このあたりはハマると大ハマりします。

一応「遅延環境変数」とよばれる特別な記法を用いることでこの問題を回避できますが、 そもそもその記法を使うのを忘れていつもの調子で書いてしまうことが心配だと私は思うわけです。

gotoを使うとジャンプ先のラベルと正しく綴りが合致しているのか目視で確認するのが大変だという方は、 お使いのテキストエディタの検索キーワードのハイライト機能を使いましょう。

例えばVimの場合について少し紹介しますと、ソースコード中の検索したいキーワードまでカーソルをもっていき、 そこで「*」を入力する(日本語キーボードだとShift+「:」キーを入力する形になりますが)と そのキーワードが自動的に検索対象になって(つまりハイライトされて)便利です。 nやNを入力することでハイライトされた他の位置へ移動することもできます。

ちなみに、Googleのプログラマも同じように考えているのか、Android SDKなどに含まれているバッチファイルも ifコマンドは基本的にgotoとともに使用しています。 参考までに少しだけ抜粋いたしますとこんな感じですね。

if exist "%proguard%" goto proguardOk
REM set proguard location for the Android tree case
    set "PROGUARD_HOME=%~dp0\..\..\..\..\external\proguard"
    set "proguard=%PROGUARD_HOME%\bin\%proguardExec%"

:proguardOk

唯一、明らかに環境変数に関係せずかつ1行で済む用途のみ、「else コマンドと ( ) の組み合わせ」をgotoコマンドとコンボで使ってる感じですね。

if defined params ( goto makeTmpJar ) else ( goto usage )
Close




目次に戻る

detect_vc.bat の導入と使い方


最低限の動作確認


このバッチファイルは、VCを実行するのに必要なPATH, INCLUDE, LIBを自動的に設定するというものです (VC6.0、VS2003、VS2005、VS2008、VS2010、VS2012、VS2013、VS2015、VS2017で動作することを確認しています)。 ライセンスはNYSL(煮るなり焼くなり好きにしろライセンス)とします。

以下からダウンロードできます。

detect_vc.batのダウンロード

これの全ソースコードとも貼っときます。 中身に興味ある方のみご覧ください(今回はさすがに長いので、一旦ダウンロードして慣れたテキストエディタなどでご覧になった方がいいかもしれません)。

detect_vc.batの全ソースコード (1108 lines)

@echo off
REM
REM DetectVC Ver1.0 (introduced in 2021.12)
REM
REM @brief
REM  This batch file will automatically set up the PATH, INCLUDE, and LIB required to run VC.
REM  It has been confirmed to work with VC6.0, VS2003, VS2005, VS2008, VS2010, VS2012, VS2013, VS2015, and VS2017. 
REM
REM  DetectVC will work to some extent with the default specification, but usually you will need to set the basic path
REM  in the three environment variables ZNK_VC_DIR, ZNK_VC_WINSDK_DIR, and ZNK_VC_UCRT_DIR before running this.
REM  DetectVC will use them to set up the necessary environment variables for VC.
REM
REM @auther
REM  Zenkaku
REM
REM @licence
REM  Copyright (c) Zen-nippon Network Kenkyujo(ZNK)
REM  Licensed under the NYSL( see http://www.kmonos.net/nysl/index.en.html for detail ).
REM
REM @disclaimer
REM  This software is provided 'as-is', without any express or implied warranty.
REM  In no event will the authors be held liable for any damages arising
REM  from the use of this software.
REM


REM Check if the execution environment is 2000 or later.
REM NOTE: The "&" "&&" operator is not available in command.com before Windows 98.
if exist "%windir%\System32\findstr.exe" goto Windows2000OrLaterCheck_End
	echo [NG]: This script requires Windows 2000 or later to work.
	pause
	goto End
:Windows2000OrLaterCheck_End


set DET_VC_PATH=
set DET_VC_INCLUDE=
set DET_VC_LIB=
set DET_VC_SUBSYS_VER=


if "%__DET_VC_ORIG_PATH_SAVE__%" == "" set __DET_VC_ORIG_PATH_SAVE__=%PATH%


setlocal


REM Default
	if not "%ProgramFiles(x86)%" == "" set PROGRAM_FILE_X86_DIR=%ProgramFiles(x86)%& goto ProgramFilesAutoSet_End
	if not "%ProgramFiles%"      == "" set PROGRAM_FILE_X86_DIR=%ProgramFiles%&      goto ProgramFilesAutoSet_End
                                       set PROGRAM_FILE_X86_DIR=C:\Program Files&    goto ProgramFilesAutoSet_End
	:ProgramFilesAutoSet_End

	if "%ZNK_VC_WINSDK_DIR%" == "" set ZNK_VC_WINSDK_DIR=%PROGRAM_FILE_X86_DIR%
	if "%ZNK_VC_UCRT_DIR%"   == "" set   ZNK_VC_UCRT_DIR=%PROGRAM_FILE_X86_DIR%
	if not "%ZNK_VC_DIR%" == "" goto ZnkVCDirAutoSet_End
		REM VS2017
		set TRY_DIR=%PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio\2017\BuildTools\VC
		if exist "%TRY_DIR%" set ZNK_VC_DIR=%TRY_DIR%& goto ZnkVCDirAutoSet_End

		REM VS2015(VC14)
		set TRY_DIR=%PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio 14.0\VC
		if exist "%TRY_DIR%" set ZNK_VC_DIR=%TRY_DIR%& goto ZnkVCDirAutoSet_End

		REM VS2013(VC12)
		set TRY_DIR=%PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio 12.0\VC
		if exist "%TRY_DIR%" set ZNK_VC_DIR=%TRY_DIR%& goto ZnkVCDirAutoSet_End

		REM VS2012(VC11)
		set TRY_DIR=%PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio 11.0\VC
		if exist "%TRY_DIR%" set ZNK_VC_DIR=%TRY_DIR%& goto ZnkVCDirAutoSet_End

		REM VS2010(VC10)
		set TRY_DIR=%PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio 10.0\VC
		if exist "%TRY_DIR%" set ZNK_VC_DIR=%TRY_DIR%& goto ZnkVCDirAutoSet_End

		REM VS2008(VC9)
		set TRY_DIR=%PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio 9.0\VC
		if exist "%TRY_DIR%" set ZNK_VC_DIR=%TRY_DIR%& goto ZnkVCDirAutoSet_End

		REM VS2005(VC8)
		set TRY_DIR=%PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio 8\VC
		if exist "%TRY_DIR%" set ZNK_VC_DIR=%TRY_DIR%& goto ZnkVCDirAutoSet_End

		REM VS2003(VC7.1)
		set TRY_DIR=%PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio .NET 2003\Vc7
		if exist "%TRY_DIR%" set ZNK_VC_DIR=%TRY_DIR%& goto ZnkVCDirAutoSet_End

		REM VS6.0
		set TRY_DIR=%PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio\VC98
		if exist "%TRY_DIR%" set ZNK_VC_DIR=%TRY_DIR%& goto ZnkVCDirAutoSet_End

		echo DetectVC: Warning: ZNK_VC_DIR is not set. Also, the default could not be detected.
	:ZnkVCDirAutoSet_End


set     VC_DIR=%ZNK_VC_DIR%
set WINSDK_DIR=%ZNK_VC_WINSDK_DIR%
set   UCRT_DIR=%ZNK_VC_UCRT_DIR%


REM CmdArg1:
	REM Since we are inside setlocal-endlocal, this overwrite is acceptable.
	if "%1" == "x64" set MACHINE=x64& goto CmdArg1_End
	if "%1" == "x86" set MACHINE=x86& goto CmdArg1_End
	if "%1" == "_"   goto MachineAutoSet
	if "%1" == ""    goto MachineAutoSet
	echo DetectVC: Warning: Invalid argument(1nd) "%1" : We regard it as "_".
	REM default: MachineAutoSet
	:MachineAutoSet
		if not "%MACHINE%" == "x64" set MACHINE=x86
		REM if "%PROCESSOR_ARCHITECTURE%" == "AMD64" set MACHINE=x64
	:CmdArg1_End

REM CmdArg2:
	REM This is a way to add to the environment variables of the VC system.
	REM For environment variables of the DET_VC_* system, the specification here is not relevant.
	set HOW_TO_SET=overwrite
	if "%2" == "overwrite"     set HOW_TO_SET=overwrite& goto CmdArg2_End
	if "%2" == "add_back"      set HOW_TO_SET=add_back&  goto CmdArg2_End
	if "%2" == "add_front"     set HOW_TO_SET=add_front& goto CmdArg2_End
	if "%2" == "_"             goto CmdArg2_End
	if "%2" == ""              goto CmdArg2_End
	echo DetectVC: Warning: Invalid argument(2nd) "%2" : We regard it as "%HOW_TO_SET%".
	:CmdArg2_End

REM CmdArg3:
	REM Specifies the type of environment variables that are actually exported.
	REM There are two main types of environment variables: environment variables starting with DET_VC_*
	REM or VC environment variables( PATH, INCLUDE, LIB ).
	REM In the case of "recommanded" mode, DET_VC_SUBSYS_VER, PATH, LIB, and INCLUDE are exported.
	set EXPORT_MODE=recommanded
	if "%3" == "all"           set EXPORT_MODE=all&           goto CmdArg3_End
	if "%3" == "det_vars_only" set EXPORT_MODE=det_vars_only& goto CmdArg3_End
	if "%3" == "vc_vars_only"  set EXPORT_MODE=vc_vars_only&  goto CmdArg3_End
	if "%3" == "recommanded"   goto CmdArg3_End
	if "%3" == "_"             goto CmdArg3_End
	if "%3" == ""              goto CmdArg3_End
	echo DetectVC: Warning: Invalid argument(3nd) "%3" : We regard it as "%EXPORT_MODE%".
	:CmdArg3_End

REM CmdArg4:
	REM Specify verbosity of reporting.
	set VERBOSE_DETAIL=no
	set VERBOSE_REPORT=yes
	if "%4" == "detail" set VERBOSE_REPORT=yes& set VERBOSE_DETAIL=yes& goto CmdArg4_End
	if "%4" == "report" set VERBOSE_REPORT=yes&                         goto CmdArg4_End
	if "%4" == "silent" set VERBOSE_REPORT=no&  set VERBOSE_DETAIL=no&  goto CmdArg4_End
	if "%4" == "_"      goto CmdArg4_End
	if "%4" == ""       goto CmdArg4_End
	echo DetectVC: Warning: Invalid argument(4nd) "%4" : We regard it as "_".
	:CmdArg4_End


set FINDSTR=findstr
if exist "%windir%\System32\findstr.exe" set FINDSTR=%windir%\System32\findstr.exe
set SAFE_CD=cd /d

REM echo VCTop:
	set TRY_DIR=%VC_DIR%
	call :Func_FindVCTop
	if not "%ANS_DIR%" == "" set VC_DIR=%ANS_DIR%

REM echo VCWinSdkDir:
	set TRY_DIR=%WINSDK_DIR%
	call :Func_FindVCWinSdkDir
	if not "%ANS_DIR%" == "" set WINSDK_DIR=%ANS_DIR%

REM echo VCUcrtDir:
	set TRY_DIR=%UCRT_DIR%
	call :Func_FindVCUcrtDir
	if not "%ANS_DIR%" == "" set UCRT_DIR=%ANS_DIR%

REM echo VCBin:
	set TRY_DIR=%VC_DIR%
	set SUB_DIR=Hostx86
	set LANDMARK_EXE=cl.exe
	set TRY_MACHINE=%MACHINE%
	call :Func_FindVCBin
	if not "%ANS_DIR%" == "" set VC_BIN=%ANS_DIR%
	if "%VC_BIN%" == "" echo DetectVC: Warning: VC_BIN is not detected.

REM echo VCBin2: ( for the path where the dll that cl.exe depends on is located )
	set TRY_DIR=%VC_DIR%
	set SUB_DIR=Hostx86
	set LANDMARK_EXE=cl.exe
	set TRY_MACHINE=x86
	call :Func_FindVCBin
	if not "%ANS_DIR%" == "" if not "%ANS_DIR%" == "%VC_BIN%" set VC_BIN2=%ANS_DIR%

REM echo VCBin3: ( for old VC )
	if exist "%VC_DIR%\..\Common7\IDE" set VC_BIN3=%VC_DIR%\..\Common7\IDE

	REM The following scan is intended to find for VC6.0 dll path.
		set ANS_PATH=%VC_BIN3%
		if exist "%VC_DIR%\..\Common\MSDev98\Bin\mspdb*.dll" call :Func_PathAddBack "%VC_DIR%\..\Common\MSDev98\Bin"
		set VC_BIN3=%ANS_PATH%

	REM The following scan is intended to find, for example, VC_DIR\..\Visual Studio .NET Professional 2003 - Japanese\msvcp71.dll.
		set TRY_DIR=%VC_DIR%\..
		set ANS_PATH=
		if exist "%TRY_DIR%" for /f "delims=" %%a in ( 'dir /b "%TRY_DIR%" ^| %FINDSTR% /i ".*"' ) do if exist "%TRY_DIR%\%%a\*.dll" call :Func_PathAddBack "%TRY_DIR%\%%a"
		if not "%ANS_PATH%" == "" set VC_BIN3=%VC_BIN3%;%ANS_PATH%

	REM The following scan is intended to find, for example, VC_DIR\redist\x86\Microsoft.VC*.CRT (for VS2013)
		set TRY_DIR=%VC_DIR%\redist\x86
		set ANS_PATH=
		if exist "%TRY_DIR%" for /f "delims=" %%a in ( 'dir /b "%TRY_DIR%" ^| %FINDSTR% /i "Microsoft\.VC.*\.CRT"' ) do if exist "%TRY_DIR%\%%a\*.dll" call :Func_PathAddBack "%TRY_DIR%\%%a"
		if not "%ANS_PATH%" == "" set VC_BIN3=%VC_BIN3%;%ANS_PATH%

REM echo VCInclude:
	set TRY_DIR=%VC_DIR%
	set SUB_DIR=um
	set LANDMARK_HDR=vector
	call :Func_FindVCSdkInclude
	if not "%ANS_DIR%" == "" set VC_INCLUDE=%ANS_DIR%

REM echo VCLib:
	set TRY_DIR=%VC_DIR%
	set SUB_DIR=um
	set LANDMARK_LIB=libcmt.lib
	call :Func_FindVCSdkLib
	if not "%ANS_DIR%" == "" set VC_LIB=%ANS_DIR%


REM echo VCWinSdkBin: ( for the path of rc.exe )
	set TRY_DIR=%WINSDK_DIR%
	set SUB_DIR=Hostx86
	set LANDMARK_EXE=rc.exe
	set TRY_MACHINE=x86
	call :Func_FindVCSdkBin
	if not "%ANS_DIR%" == "" set WINSDK_BIN=%ANS_DIR%

REM echo VCWinSdkInclude:
	set TRY_DIR=%WINSDK_DIR%
	set SUB_DIR=um
	set LANDMARK_HDR=windows.h
	call :Func_FindVCSdkInclude
	if not "%ANS_DIR%" == "" set WINSDK_INCLUDE=%ANS_DIR%

	set TRY_DIR=%WINSDK_DIR%
	set SUB_DIR=shared
	set LANDMARK_HDR=winapifamily.h
	call :Func_FindVCSdkInclude
	if not "%ANS_DIR%" == "" set WINSDK_INCLUDE=%WINSDK_INCLUDE%;%ANS_DIR%

REM echo VCWinSdkLib:
	set TRY_DIR=%WINSDK_DIR%
	set SUB_DIR=um
	set LANDMARK_LIB=user32.lib
	call :Func_FindVCSdkLib
	if not "%ANS_DIR%" == "" set WINSDK_LIB=%ANS_DIR%

REM echo VCUcrtInclude:
	set TRY_DIR=%UCRT_DIR%
	set SUB_DIR=ucrt
	set LANDMARK_HDR=stdio.h
	call :Func_FindVCSdkInclude
	if not "%ANS_DIR%" == "" set UCRT_INCLUDE=%ANS_DIR%

REM echo VCUcrtLib:
	set TRY_DIR=%UCRT_DIR%
	set SUB_DIR=ucrt
	set LANDMARK_LIB=libucrt.lib
	call :Func_FindVCSdkLib
	if not "%ANS_DIR%" == "" set UCRT_LIB=%ANS_DIR%


REM echo VCInclude(ATLMFC):
	set TRY_DIR=%VC_DIR%\atlmfc
	set SUB_DIR=um
	set LANDMARK_HDR=afxres.h
	call :Func_FindVCSdkInclude
	if not "%ANS_DIR%" == "" set VC_ATLMFC_INCLUDE=%ANS_DIR%

REM echo VCLib(ATLMFC):
	set TRY_DIR=%VC_DIR%\atlmfc
	set SUB_DIR=um
	set LANDMARK_LIB=atl.lib
	call :Func_FindVCSdkLib
	if not "%ANS_DIR%" == "" set VC_ATLMFC_LIB=%ANS_DIR%


if not "%VERBOSE_DETAIL%" == "yes" goto ReportForDetail_End
	echo:
	echo DetectForDetail:
	call :Func_PrintPathStr "VC_DIR"         "%VC_DIR%"
	call :Func_PrintPathStr "VC_BIN"         "%VC_BIN%"
	call :Func_PrintPathStr "VC_BIN2"        "%VC_BIN2%"
	call :Func_PrintPathStr "VC_BIN3"        "%VC_BIN3%"
	call :Func_PrintPathStr "VC_INCLUDE"     "%VC_INCLUDE%"
	call :Func_PrintPathStr "VC_LIB"         "%VC_LIB%"
	call :Func_PrintPathStr "WINSDK_DIR"     "%WINSDK_DIR%"
	call :Func_PrintPathStr "WINSDK_INCLUDE" "%WINSDK_INCLUDE%"
	call :Func_PrintPathStr "WINSDK_LIB"     "%WINSDK_LIB%"
	call :Func_PrintPathStr "WINSDK_BIN"     "%WINSDK_BIN%"
	call :Func_PrintPathStr "UCRT_DIR"       "%UCRT_DIR%"
	call :Func_PrintPathStr "UCRT_INCLUDE"   "%UCRT_INCLUDE%"
	call :Func_PrintPathStr "UCRT_LIB"       "%UCRT_LIB%"
	call :Func_PrintPathStr "VC_ATLMFC_INCLUDE" "%VC_ATLMFC_INCLUDE%"
	call :Func_PrintPathStr "VC_ATLMFC_LIB"     "%VC_ATLMFC_LIB%"
	echo:
:ReportForDetail_End


REM echo IntegrationOfDetection:
	set ANS_PATH=%ZNK_VC_PATH%
	call :Func_PathAddBack "%VC_BIN%"
	call :Func_PathAddBack "%VC_BIN2%"
	call :Func_PathAddBack "%VC_BIN3%"
	call :Func_PathAddBack "%WINSDK_BIN%"
	set DET_VC_PATH=%ANS_PATH%
	
	REM NOTE:
	REM WINSDK_INCLUDE should be given higher priority than VC_INCLUDE.
	REM Otherwise, when you compile WinSDK8.1 with VS2010 or earlier VC, you will get a compilation error.
	REM ( https://devblogs.microsoft.com/cppblog/using-the-windows-8-sdk-with-visual-studio-2010-configuring-multiple-projects )
	set ANS_PATH=%ZNK_VC_INCLUDE%
	call :Func_PathAddBack "%VC_ATLMFC_INCLUDE%"
	call :Func_PathAddBack "%WINSDK_INCLUDE%"
	call :Func_PathAddBack "%VC_INCLUDE%"
	call :Func_PathAddBack "%UCRT_INCLUDE%"
	set DET_VC_INCLUDE=%ANS_PATH%
	
	set ANS_PATH=%ZNK_VC_LIB%
	call :Func_PathAddBack "%VC_ATLMFC_LIB%"
	call :Func_PathAddBack "%WINSDK_LIB%"
	call :Func_PathAddBack "%VC_LIB%"
	call :Func_PathAddBack "%UCRT_LIB%"
	set DET_VC_LIB=%ANS_PATH%


REM echo DetectLinkSubsysVer:
REM See WINVER macro for details.
setlocal
	REM for dll path
	set PATH_SAVE=%PATH%
	set PATH=%__DET_VC_ORIG_PATH_SAVE__%;%DET_VC_PATH%

	set TRY_LINK_EXE=%VC_BIN%\link.exe
	REM set CURRENT_DIR_SAVE=%CD%
	REM %SAFE_CD% "%VC_BIN%"

	REM if "%VERBOSE_DETAIL%" == "yes" echo [FYI]: DetectVC: Report: Check link.exe in [%CD%]
	REM if not exist "link.exe" echo DetectVC: Warning: link.exe not found in [%VC_BIN%].
	REM if not exist "link.exe" goto DetectLinkVer_End
	if not exist "%TRY_LINK_EXE%" goto DetectLinkVer_End
		REM Target OS : Windows 95/98
			call :Func_CheckVCLinkSubsysVer "4.00"
			if not "%DET_VC_SUBSYS_VER%" == "" goto DetectLinkVer_End
	
		REM Target OS : Windows 2000/ME
			call :Func_CheckVCLinkSubsysVer "5.00"
			if not "%DET_VC_SUBSYS_VER%" == "" goto DetectLinkVer_End
	
		REM Target OS : Windows XP etc.
			if "%MACHINE%" == "x64" goto DetectLinkVer_x64
				REM Target OS : Windows XP (x86)
				call :Func_CheckVCLinkSubsysVer "5.01"
				if not "%DET_VC_SUBSYS_VER%" == "" goto DetectLinkVer_End

				goto DetectLinkVer_x64_End
			:DetectLinkVer_x64
				REM Target OS : Windows XP (x64)/ Windows Server 2003
				call :Func_CheckVCLinkSubsysVer "5.02"
				if not "%DET_VC_SUBSYS_VER%" == "" goto DetectLinkVer_End

				goto DetectLinkVer_x64_End
			:DetectLinkVer_x64_End

		REM Target OS : Vista
			call :Func_CheckVCLinkSubsysVer "6.00"
			if not "%DET_VC_SUBSYS_VER%" == "" goto DetectLinkVer_End

		REM Target OS : Windows 7
			call :Func_CheckVCLinkSubsysVer "6.01"
			if not "%DET_VC_SUBSYS_VER%" == "" goto DetectLinkVer_End

		REM Target OS : Windows 8
			call :Func_CheckVCLinkSubsysVer "6.02"
			if not "%DET_VC_SUBSYS_VER%" == "" goto DetectLinkVer_End

		REM Target OS : Windows 10
			call :Func_CheckVCLinkSubsysVer "10.00"
			if not "%DET_VC_SUBSYS_VER%" == "" goto DetectLinkVer_End

		goto DetectLinkVer_End

	:DetectLinkVer_End
	REM %SAFE_CD% %CURRENT_DIR_SAVE%
	set PATH=%PATH_SAVE%
REM export variables:
REM   DET_VC_SUBSYS_VER
endlocal& set DET_VC_SUBSYS_VER=%DET_VC_SUBSYS_VER%


REM echo HowtoSet VCVars:
REM If a ZNK_VC_* series environment variable is defined,
REM it will be inserted before the DET_VC_* series one in priority.
if not "%HOW_TO_SET%" == "overwrite" goto HowtoSet_Overwrite_End

	set ANS_PATH=%__DET_VC_ORIG_PATH_SAVE__%
	call :Func_PathAddBack "%DET_VC_PATH%"
	set PATH=%ANS_PATH%

	set ANS_PATH=
	call :Func_PathAddBack "%DET_VC_INCLUDE%"
	set INCLUDE=%ANS_PATH%

	set ANS_PATH=
	call :Func_PathAddBack "%DET_VC_LIB%"
	set LIB=%ANS_PATH%

:HowtoSet_Overwrite_End
if not "%HOW_TO_SET%" == "add_back" goto HowtoSet_AddBack_End

	set ANS_PATH=%PATH%
	call :Func_PathAddBack "%DET_VC_PATH%"
	set PATH=%ANS_PATH%

	set ANS_PATH=%INCLUDE%
	call :Func_PathAddBack "%DET_VC_INCLUDE%"
	set INCLUDE=%ANS_PATH%

	set ANS_PATH=%LIB%
	call :Func_PathAddBack "%DET_VC_LIB%"
	set LIB=%ANS_PATH%

:HowtoSet_AddBack_End
if not "%HOW_TO_SET%" == "add_front" goto HowtoSet_AddFront_End

	set ANS_PATH=%PATH%
	call :Func_PathAddFront "%DET_VC_PATH%"
	set PATH=%ANS_PATH%

	set ANS_PATH=%INCLUDE%
	call :Func_PathAddFront "%DET_VC_INCLUDE%"
	set INCLUDE=%ANS_PATH%

	set ANS_PATH=%LIB%
	call :Func_PathAddFront "%DET_VC_LIB%"
	set LIB=%ANS_PATH%

:HowtoSet_AddFront_End


REM export variables:
REM   DET_VC_PATH
REM   DET_VC_INCLUDE
REM   DET_VC_LIB
REM   DET_VC_SUBSYS_VER
REM   PATH
REM   INCLUDE
REM   LIB
if "%EXPORT_MODE%" == "recommanded"   goto Export_recommanded
if "%EXPORT_MODE%" == "all"           goto Export_all
if "%EXPORT_MODE%" == "det_vars_only" goto Export_det_vars_only
if "%EXPORT_MODE%" == "vc_vars_only"  goto Export_vc_vars_only
REM default : Export_recommanded
	:Export_recommanded
		if not "%VERBOSE_REPORT%" == "yes" goto ExportReport_recommanded_End
			echo ExportMode: %EXPORT_MODE%
			echo:
			echo VCVars:
			call :Func_PrintPathStr "PATH"    "%PATH%"
			call :Func_PrintPathStr "INCLUDE" "%INCLUDE%"
			call :Func_PrintPathStr "LIB"     "%LIB%"
			echo:
			echo DetectSubsystemVersion:
			echo   DET_VC_SUBSYS_VER=[%DET_VC_SUBSYS_VER%]
			echo:
		:ExportReport_recommanded_End
		endlocal ^
			& call :Func_Set "DET_VC_SUBSYS_VER" "%DET_VC_SUBSYS_VER%" ^
			& call :Func_Set "PATH"              "%PATH%"              ^
			& call :Func_Set "INCLUDE"           "%INCLUDE%"           ^
			& call :Func_Set "LIB"               "%LIB%"

	goto Export_End
	
	:Export_all
		if not "%VERBOSE_REPORT%" == "yes" goto ExportReport_all_End
			echo ExportMode: %EXPORT_MODE%
			echo:
			echo DetectVars Detail:
			call :Func_PrintPathStr "DET_VC_PATH"    "%DET_VC_PATH%"
			call :Func_PrintPathStr "DET_VC_INCLUDE" "%DET_VC_INCLUDE%"
			call :Func_PrintPathStr "DET_VC_LIB"     "%DET_VC_LIB%"
			echo:
			echo VCVars:
			call :Func_PrintPathStr "PATH"    "%PATH%"
			call :Func_PrintPathStr "INCLUDE" "%INCLUDE%"
			call :Func_PrintPathStr "LIB"     "%LIB%"
			echo:
			echo DetectSubsystemVersion:
			echo   DET_VC_SUBSYS_VER=[%DET_VC_SUBSYS_VER%]
			echo:
		:ExportReport_all_End
		endlocal ^
			& call :Func_Set "DET_VC_PATH"       "%DET_VC_PATH%"       ^
			& call :Func_Set "DET_VC_INCLUDE"    "%DET_VC_INCLUDE%"    ^
			& call :Func_Set "DET_VC_LIB"        "%DET_VC_LIB%"        ^
			& call :Func_Set "DET_VC_SUBSYS_VER" "%DET_VC_SUBSYS_VER%" ^
			& call :Func_Set "PATH"              "%PATH%"              ^
			& call :Func_Set "INCLUDE"           "%INCLUDE%"           ^
			& call :Func_Set "LIB"               "%LIB%"

	goto Export_End
	
	:Export_det_vars_only
		if not "%VERBOSE_REPORT%" == "yes" goto ExportReport_det_vars_End
			echo ExportMode: %EXPORT_MODE%
			echo:
			echo DetectVars Detail:
			call :Func_PrintPathStr "DET_VC_PATH"    "%DET_VC_PATH%"
			call :Func_PrintPathStr "DET_VC_INCLUDE" "%DET_VC_INCLUDE%"
			call :Func_PrintPathStr "DET_VC_LIB"     "%DET_VC_LIB%"
			echo:
			echo DetectSubsystemVersion:
			echo   DET_VC_SUBSYS_VER=[%DET_VC_SUBSYS_VER%]
			echo:
		:ExportReport_det_vars_End
		endlocal ^
			& call :Func_Set "DET_VC_PATH"       "%DET_VC_PATH%"       ^
			& call :Func_Set "DET_VC_INCLUDE"    "%DET_VC_INCLUDE%"    ^
			& call :Func_Set "DET_VC_LIB"        "%DET_VC_LIB%"        ^
			& call :Func_Set "DET_VC_SUBSYS_VER" "%DET_VC_SUBSYS_VER%"

	goto Export_End
	
	:Export_vc_vars_only
		if not "%VERBOSE_REPORT%" == "yes" goto ExportReport_vc_vars_End
			echo ExportMode: %EXPORT_MODE%
			echo:
			echo VCVars:
			call :Func_PrintPathStr "PATH"    "%PATH%"
			call :Func_PrintPathStr "INCLUDE" "%INCLUDE%"
			call :Func_PrintPathStr "LIB"     "%LIB%"
			echo:
		:ExportReport_vc_vars_End
		endlocal ^
			& call :Func_Set "PATH"              "%PATH%"              ^
			& call :Func_Set "INCLUDE"           "%INCLUDE%"           ^
			& call :Func_Set "LIB"               "%LIB%"

	goto Export_End

:Export_End


goto End


REM ----
REM Sub-routine Definitions
REM ----


REM
REM Note:
REM   If you specify empty in the 'for' command delim (i.e. /f "delims="), only the newline code will be used as the delimiter.
REM


REM Sub-routine Func_PathAddBack
REM Add the directory given in the argument to the back of ANS_PATH.
REM
REM Arg    : "%1"
REM Return : ANS_PATH
:Func_PathAddBack
	call :Func_ConnectStr "%ANS_PATH%" %1 ";"
	set ANS_PATH=%ANS_STR%
	exit /b
REM endof Sub-routine


REM Sub-routine Func_PathAddFront
REM Add the directory given in the argument to the front of ANS_PATH.
REM
REM Arg    : "%1"
REM Return : ANS_PATH
:Func_PathAddFront
	call :Func_ConnectStr %1 "%ANS_PATH%" ";"
	set ANS_PATH=%ANS_STR%
	exit /b
REM endof Sub-routine


REM Sub-routine Func_ConnectStr
REM Concatenate strings in the order lhs-strings delimiter rhs-strings.
REM However, if either lhs-strings or rhs-strings is an empty string, delimiter is not added.
REM
REM Arg    : const "%1" lhs-string
REM Arg    : const "%2" rhs-string
REM Arg    : const "%3" delimitor
REM Return : ANS_STR
:Func_ConnectStr
setlocal
	set LHS=%1
	set RHS=%2
	set DLM=%3
	REM Strip sides quotes.
	set LHS=%LHS:~1,-1%
	set RHS=%RHS:~1,-1%
	set DLM=%DLM:~1,-1%

	if "%LHS%" == "" goto Func_ConnectStr_EmptyLHS
	if "%RHS%" == "" goto Func_ConnectStr_EmptyRHS
	set ANS_STR=%LHS%%DLM%%RHS%
	goto Func_ConnectStr_End

	:Func_ConnectStr_EmptyLHS
		set ANS_STR=%RHS%
		goto Func_ConnectStr_End

	:Func_ConnectStr_EmptyRHS
		set ANS_STR=%LHS%
		goto Func_ConnectStr_End

:Func_ConnectStr_End
endlocal& set ANS_STR=%ANS_STR%
	exit /b
REM endof Sub-routine


REM Sub-routine Func_FindVCTop
REM Arg    : TRY_DIR
REM Return : ANS_DIR
:Func_FindVCTop
set ANS_DIR=

	REM for VC8.0 or later
	if exist "%TRY_DIR%\VC"         set TRY_DIR=%TRY_DIR%\VC
	REM for VC6.0
	if exist "%TRY_DIR%\VC98"       set TRY_DIR=%TRY_DIR%\VC98
	REM for VC7.1
	if exist "%TRY_DIR%\Vc7"        set TRY_DIR=%TRY_DIR%\Vc7
	REM for VC2017
	if exist "%TRY_DIR%\Tools\MSVC" set TRY_DIR=%TRY_DIR%\Tools\MSVC
	
	if exist "%TRY_DIR%\BIN"        set ANS_DIR=%TRY_DIR%
	if not "%ANS_DIR%" == "" goto Func_FindVCTop_End
	
	if exist "%TRY_DIR%" for /f "delims=" %%a in ( 'dir /b "%TRY_DIR%" ^| %FINDSTR% /i ".*"' ) do if exist "%TRY_DIR%\%%a\BIN" set ANS_DIR=%TRY_DIR%\%%a
	if not "%ANS_DIR%" == "" goto Func_FindVCTop_End

:Func_FindVCTop_End
	exit /b
REM endof Sub-routine


REM Sub-routine Func_FindVCWinSdkDir
REM Arg    : TRY_DIR
REM Return : ANS_DIR
:Func_FindVCWinSdkDir
setlocal
	set ANS_DIR=
	set TRY_DIR_SAVE=%TRY_DIR%

	REM Microsoft SDKs type : Windows SDK 7.1 or 6.0 etc.
	set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\Microsoft SDKs"                                    set TRY_DIR=%TRY_DIR%\Microsoft SDKs
		if exist "%TRY_DIR%\Windows" if not exist "%TRY_DIR%\Windows\system32" set TRY_DIR=%TRY_DIR%\Windows
		if exist "%TRY_DIR%\include" set ANS_DIR=%TRY_DIR%
		if not "%ANS_DIR%" == "" goto Func_FindVCWinSdkDir_End

		call :Func_FindVCWinSdkVerDir
		if not "%ANS_DIR%" == "" goto Func_FindVCWinSdkDir_End

	REM Windows Kits type : Windows SDK 8.0 or 10 etc.
	set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\Windows Kits" set TRY_DIR=%TRY_DIR%\Windows Kits
		if exist "%TRY_DIR%\include" set ANS_DIR=%TRY_DIR%
		if not "%ANS_DIR%" == "" goto Func_FindVCWinSdkDir_End

		call :Func_FindVCWinSdkVerDir
		if not "%ANS_DIR%" == "" goto Func_FindVCWinSdkDir_End

REM for Future version ?
REM	set TRY_DIR=%TRY_DIR_SAVE%
REM	if exist "%TRY_DIR%" for /f "delims=" %%a in ( 'dir /b "%TRY_DIR%" ^| %FINDSTR% /i ".*"' ) do if exist "%TRY_DIR%\%%a\LIB" set ANS_DIR=%TRY_DIR%\%%a
REM	if not "%ANS_DIR%" == "" goto Func_FindVCWinSdkDir_End

	REM Old VC : VC\PlatformSDK (VS2003,VS2005)
	if exist "%ZNK_VC_DIR%\PlatformSDK" set ANS_DIR=%ZNK_VC_DIR%\PlatformSDK
	if not "%ANS_DIR%" == "" goto Func_FindVCWinSdkDir_End

	REM Old VC : VC\Include\windows.h (VC6.0) : VC_DIR itself is a WINSDK_DIR.
	if exist "%ZNK_VC_DIR%"\Include\windows.h set ANS_DIR=%ZNK_VC_DIR%
	if not "%ANS_DIR%" == "" goto Func_FindVCWinSdkDir_End

	echo DetectVC: Warning: WINSDK_DIR is not detected in [%TRY_DIR_SAVE%].
:Func_FindVCWinSdkDir_End
endlocal& set ANS_DIR=%ANS_DIR%
	exit /b
REM endof Sub-routine


REM Sub-routine Func_FindVCUcrtDir
REM Arg    : TRY_DIR
REM Return : ANS_DIR
:Func_FindVCUcrtDir
set ANS_DIR=
set TRY_DIR_SAVE=%TRY_DIR%

	REM for Windows SDK 10
	set TRY_DIR=%TRY_DIR_SAVE%
	if exist "%TRY_DIR%\Windows Kits\10\LIB" set ANS_DIR=%TRY_DIR%\Windows Kits\10
	if not "%ANS_DIR%" == "" goto Func_FindVCUcrtDir_End

	REM for Windows SDK 10 (for modified by users)
	set TRY_DIR=%TRY_DIR_SAVE%
	if exist "%TRY_DIR%\Windows Kits" set TRY_DIR=%TRY_DIR%\Windows Kits
	if exist "%TRY_DIR%\10"           set TRY_DIR=%TRY_DIR%\10
	if exist "%TRY_DIR%\LIB" set ANS_DIR=%TRY_DIR%
	if not "%ANS_DIR%" == "" goto Func_FindVCUcrtDir_End
	
	REM for Future version ?
	if exist "%TRY_DIR%" for /f "delims=" %%a in ( 'dir /b "%TRY_DIR%" ^| %FINDSTR% /i ".*"' ) do if exist "%TRY_DIR%\%%a\LIB" set ANS_DIR=%TRY_DIR%\%%a
	if not "%ANS_DIR%" == "" goto Func_FindVCUcrtDir_End

:Func_FindVCUcrtDir_End
	exit /b
REM endof Sub-routine


REM Sub-routine Func_FindVCSdkInclude
REM Arg    :       TRY_DIR
REM Arg    : const SUB_DIR      ( e.g. um, ucrt )
REM Arg    : const LANDMARK_HDR ( e.g. windows.h, stdio.h )
REM Return : ANS_DIR
:Func_FindVCSdkInclude
set ANS_DIR=
set TRY_DIR_SAVE=%TRY_DIR%

	REM for Windows SDK 8.1
	set TRY_DIR=%TRY_DIR_SAVE%
	if exist "%TRY_DIR%\include\%SUB_DIR%\%LANDMARK_HDR%" set ANS_DIR=%TRY_DIR%\include\%SUB_DIR%
	if not "%ANS_DIR%" == "" goto Func_FindVCSdkInclude_End
	
	REM for Windows SDK 7.1 and 6.0
	set TRY_DIR=%TRY_DIR_SAVE%
	if exist "%TRY_DIR%\include\%LANDMARK_HDR%"    set ANS_DIR=%TRY_DIR%\include
	if not "%ANS_DIR%" == "" goto Func_FindVCSdkInclude_End
	
	REM for Windows SDK 10
	set TRY_DIR=%TRY_DIR_SAVE%
	if exist "%TRY_DIR%\include" set TRY_DIR=%TRY_DIR%\include
	if exist "%TRY_DIR%" for /f "delims=" %%a in ( 'dir /b "%TRY_DIR%" ^| %FINDSTR% /i ".*"' ) do if exist "%TRY_DIR%\%%a\%SUB_DIR%\%LANDMARK_HDR%" set ANS_DIR=%TRY_DIR%\%%a\%SUB_DIR%
	if not "%ANS_DIR%" == "" goto Func_FindVCSdkInclude_End

:Func_FindVCSdkInclude_End
	exit /b
REM endof Sub-routine


REM Sub-routine Func_FindVCSdkLib
REM Arg    :       TRY_DIR
REM Arg    : const SUB_DIR      ( e.g. um, ucrt )
REM Arg    : const LANDMARK_LIB ( e.g. user32.lib, libucrt.lib )
REM Return : ANS_DIR
:Func_FindVCSdkLib
set ANS_DIR=
set TRY_DIR_SAVE=%TRY_DIR%

	if "%MACHINE%" == "x64" goto Func_FindVCSdkLib_x64
		REM x86
	
		REM for VC2017
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\lib\x86\%LANDMARK_LIB%"   set ANS_DIR=%TRY_DIR%\lib\x86
		if not "%ANS_DIR%" == "" goto Func_FindVCSdkLib_End
	
		REM for Windows SDK 7.1 and 6.0
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\lib\%LANDMARK_LIB%"       set ANS_DIR=%TRY_DIR%\lib
		if not "%ANS_DIR%" == "" goto Func_FindVCSdkLib_End
	
		REM for Windows SDK 8.1 or later
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\lib" set TRY_DIR=%TRY_DIR%\lib
		if exist "%TRY_DIR%" for /f "delims=" %%a in ( 'dir /b "%TRY_DIR%" ^| %FINDSTR% /i ".*"' ) do if exist "%TRY_DIR%\%%a\%SUB_DIR%\x86\%LANDMARK_LIB%" set ANS_DIR=%TRY_DIR%\%%a\%SUB_DIR%\x86
		if not "%ANS_DIR%" == "" goto Func_FindVCSdkLib_End
	
	goto Func_FindVCSdkLib_x64_End
	:Func_FindVCSdkLib_x64
		REM x64
	
		REM for VC2017, Windows SDK 7.1 and 6.0
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\lib\x64\%LANDMARK_LIB%"   set ANS_DIR=%TRY_DIR%\lib\x64
		if not "%ANS_DIR%" == "" goto Func_FindVCSdkLib_End
	
		REM for PlatformSDK 5.1
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\lib\amd64\%LANDMARK_LIB%" set ANS_DIR=%TRY_DIR%\lib\amd64
		if not "%ANS_DIR%" == "" goto Func_FindVCSdkLib_End
	
		REM for Windows SDK 8.1 or later
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\lib" set TRY_DIR=%TRY_DIR%\lib
		if exist "%TRY_DIR%" for /f "delims=" %%a in ( 'dir /b "%TRY_DIR%" ^| %FINDSTR% /i ".*"' ) do if exist "%TRY_DIR%\%%a\%SUB_DIR%\x64\%LANDMARK_LIB%" set ANS_DIR=%TRY_DIR%\%%a\%SUB_DIR%\x64
		if not "%ANS_DIR%" == "" goto Func_FindVCSdkLib_End
	
	:Func_FindVCSdkLib_x64_End

:Func_FindVCSdkLib_End
	exit /b
REM endof Sub-routine


REM Sub-routine Func_FindVCSdkBin
REM Arg    :       TRY_DIR
REM Arg    : const SUB_DIR      ( e.g. Hostx86 )
REM Arg    : const LANDMARK_EXE ( e.g. rc.exe )
REM Arg    : const TRY_MACHINE  ( x86 or x64 )
REM Return : ANS_DIR
:Func_FindVCSdkBin
set ANS_DIR=
set TRY_DIR_SAVE=%TRY_DIR%

	if "%TRY_MACHINE%" == "x64" goto Func_FindVCSdkBin_x64
		REM x86
	
		REM for PlatformSDK 5.2 WinSDK 6.0
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin\%LANDMARK_EXE%"     set ANS_DIR=%TRY_DIR%\bin
		if not "%ANS_DIR%" == "" goto Func_FindVCSdkBin_End

		REM for WinSDK 8.0 WinSDK 10
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin\x86\%LANDMARK_EXE%" set ANS_DIR=%TRY_DIR%\bin\x86
		if not "%ANS_DIR%" == "" goto Func_FindVCSdkBin_End
	
		REM for Future version ?
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin" set TRY_DIR=%TRY_DIR%\bin
		if exist "%TRY_DIR%" for /f "delims=" %%a in ( 'dir /b "%TRY_DIR%" ^| %FINDSTR% /i ".*"' ) do if exist "%TRY_DIR%\%%a\%SUB_DIR%\x86\%LANDMARK_EXE%" set ANS_DIR=%TRY_DIR%\%%a\%SUB_DIR%\x86
		if not "%ANS_DIR%" == "" goto Func_FindVCSdkBin_End
	
	goto Func_FindVCSdkBin_x64_End
	:Func_FindVCSdkBin_x64
		REM x64

		REM for PlatformSDK 5.2 WinSDK 6.0
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin\%LANDMARK_EXE%"           set ANS_DIR=%TRY_DIR%\bin
		if not "%ANS_DIR%" == "" goto Func_FindVCSdkBin_End

		REM for VC(PlatformSDK is marged) ?
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin\x86_amd64\%LANDMARK_EXE%" set ANS_DIR=%TRY_DIR%\bin\x86_amd64
		if not "%ANS_DIR%" == "" goto Func_FindVCSdkBin_End

		REM for WinSDK 8.0 WinSDK 10
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin\x64\%LANDMARK_EXE%"       set ANS_DIR=%TRY_DIR%\bin\x64
		if not "%ANS_DIR%" == "" goto Func_FindVCSdkBin_End

		REM for Future version(WinSDK is marged) ?
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin" set TRY_DIR=%TRY_DIR%\bin
		if exist "%TRY_DIR%" for /f "delims=" %%a in ( 'dir /b "%TRY_DIR%" ^| %FINDSTR% /i ".*"' ) do if exist "%TRY_DIR%\%%a\%SUB_DIR%\x64\%LANDMARK_EXE%" set ANS_DIR=%TRY_DIR%\%%a\%SUB_DIR%\x64
		if not "%ANS_DIR%" == "" goto Func_FindVCSdkBin_End
	
		REM for VC(PlatformSDK is marged) ?
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin\amd64\%LANDMARK_EXE%"     set ANS_DIR=%TRY_DIR%\bin\amd64
		if not "%ANS_DIR%" == "" goto Func_FindVCSdkBin_End
	:Func_FindVCSdkBin_x64_End
:Func_FindVCSdkBin_End
	exit /b
REM endof Sub-routine


REM Sub-routine Func_FindVCBin
REM Arg    :       TRY_DIR
REM Arg    : const SUB_DIR      ( e.g. Hostx86 )
REM Arg    : const LANDMARK_EXE ( e.g. cl.exe )
REM Arg    : const TRY_MACHINE  ( x86 or x64 )
REM Return : ANS_DIR
:Func_FindVCBin
set ANS_DIR=
set TRY_DIR_SAVE=%TRY_DIR%

	if "%TRY_MACHINE%" == "x64" goto Func_FindVCBin_x64
		REM x86
	
		REM for VC2017
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin\Hostx86\x86\%LANDMARK_EXE%" set ANS_DIR=%TRY_DIR%\bin\Hostx86\x86
		if not "%ANS_DIR%" == "" goto Func_FindVCBin_End
	
		REM for VC10, VC11
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin\%LANDMARK_EXE%"             set ANS_DIR=%TRY_DIR%\bin
		if not "%ANS_DIR%" == "" goto Func_FindVCBin_End
	
		REM for Future version ?
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin" set TRY_DIR=%TRY_DIR%\bin
		if exist "%TRY_DIR%" for /f "delims=" %%a in ( 'dir /b "%TRY_DIR%" ^| %FINDSTR% /i ".*"' ) do if exist "%TRY_DIR%\%%a\%SUB_DIR%\x86\%LANDMARK_EXE%" set ANS_DIR=%TRY_DIR%\%%a\%SUB_DIR%\x86
		if not "%ANS_DIR%" == "" goto Func_FindVCBin_End
	
	goto Func_FindVCBin_x64_End
	:Func_FindVCBin_x64
		REM x64
	
		REM for VC2017
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin\Hostx86\x64\%LANDMARK_EXE%" set ANS_DIR=%TRY_DIR%\bin\Hostx86\x64
		if not "%ANS_DIR%" == "" goto Func_FindVCBin_End
	
		REM for VC10, VC11
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin\x86_amd64\%LANDMARK_EXE%"   set ANS_DIR=%TRY_DIR%\bin\x86_amd64
		if not "%ANS_DIR%" == "" goto Func_FindVCBin_End
	
		REM for Future version ?
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin" set TRY_DIR=%TRY_DIR%\bin
		if exist "%TRY_DIR%" for /f "delims=" %%a in ( 'dir /b "%TRY_DIR%" ^| %FINDSTR% /i ".*"' ) do if exist "%TRY_DIR%\%%a\%SUB_DIR%\x64\%LANDMARK_EXE%" set ANS_DIR=%TRY_DIR%\%%a\%SUB_DIR%\x64
		if not "%ANS_DIR%" == "" goto Func_FindVCBin_End
	
		REM Alternatively, we use Native compiler (not Cross-compiler) (in old VC)
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin\amd64\%LANDMARK_EXE%" set ANS_DIR=%TRY_DIR%\bin\amd64
		if not "%ANS_DIR%" == "" goto Func_FindVCBin_End
	:Func_FindVCBin_x64_End
:Func_FindVCBin_End
	exit /b
REM endof Sub-routine


REM Sub-routine Func_FindVCWinSdkVerDir
REM Arg    : TRY_DIR
REM Return : ANS_DIR
:Func_FindVCWinSdkVerDir
setlocal
	set ANS_DIR=
	set TRY_DIR_SAVE=%TRY_DIR%
	set VER=

	set VER=v6.0
	if exist "%TRY_DIR%\%VER%\include\windows.h" set ANS_DIR=%TRY_DIR%\%VER%
	if not "%ANS_DIR%" == "" goto Func_FindVCWinSdkVerDir_End
	set VER=v6.0a
	if exist "%TRY_DIR%\%VER%\include\windows.h" set ANS_DIR=%TRY_DIR%\%VER%
	if not "%ANS_DIR%" == "" goto Func_FindVCWinSdkVerDir_End
	set VER=v6.1
	if exist "%TRY_DIR%\%VER%\include\windows.h" set ANS_DIR=%TRY_DIR%\%VER%
	if not "%ANS_DIR%" == "" goto Func_FindVCWinSdkVerDir_End
	set VER=v7.0a
	if exist "%TRY_DIR%\%VER%\include\windows.h" set ANS_DIR=%TRY_DIR%\%VER%
	if not "%ANS_DIR%" == "" goto Func_FindVCWinSdkVerDir_End
	set VER=v7.1
	if exist "%TRY_DIR%\%VER%\include\windows.h" set ANS_DIR=%TRY_DIR%\%VER%
	if not "%ANS_DIR%" == "" goto Func_FindVCWinSdkVerDir_End
	set VER=v7.1A
	if exist "%TRY_DIR%\%VER%\include\windows.h" set ANS_DIR=%TRY_DIR%\%VER%
	if not "%ANS_DIR%" == "" goto Func_FindVCWinSdkVerDir_End

	set VER=8.0
	if exist "%TRY_DIR%\%VER%\include\um\windows.h" set ANS_DIR=%TRY_DIR%\%VER%
	if not "%ANS_DIR%" == "" goto Func_FindVCWinSdkVerDir_End
	set VER=8.0A
	if exist "%TRY_DIR%\%VER%\include\um\windows.h" set ANS_DIR=%TRY_DIR%\%VER%
	if not "%ANS_DIR%" == "" goto Func_FindVCWinSdkVerDir_End
	set VER=8.1
	if exist "%TRY_DIR%\%VER%\include\um\windows.h" set ANS_DIR=%TRY_DIR%\%VER%
	if not "%ANS_DIR%" == "" goto Func_FindVCWinSdkVerDir_End
	set VER=8.1A
	if exist "%TRY_DIR%\%VER%\include\um\windows.h" set ANS_DIR=%TRY_DIR%\%VER%
	if not "%ANS_DIR%" == "" goto Func_FindVCWinSdkVerDir_End

	set VER=10
	if exist "%TRY_DIR%\%VER%\include" set ANS_DIR=%TRY_DIR%\%VER%
	if not "%ANS_DIR%" == "" goto Func_FindVCWinSdkVerDir_End

:Func_FindVCWinSdkVerDir_End
endlocal& set ANS_DIR=%ANS_DIR%
	exit /b
REM endof Sub-routine


REM Sub-routine Func_CheckVCLinkSubsysVer
REM Arg    : const "%1" Try Version
REM Arg    : const TRY_LINK_EXE
REM Return : DET_VC_SUBSYS_VER
:Func_CheckVCLinkSubsysVer
	REM Strip sides quotes.
	set TRY_VER=%1
	set TRY_VER=%TRY_VER:~1,-1%

	REM
	REM NOTE:
	REM   The check of SubsystemVersion also requires the option /MACHINE to be specified.
	REM   Otherwise, Warnig LNK4010: will not be displayed correctly.
	REM   (Supported SubsystemVersion may differ depending on whether X86 or AMD64 is specified.)
	REM
	if "%MACHINE%" == "x64" ( set VC_MACHINE=AMD64 ) else ( set VC_MACHINE=X86 )

	REM
	REM NOTE:
	REM   The original command is "LINK /MACHINE:%VC_MACHINE% /SUBSYSTEM,WINDOWS,%TRY_VER%",
	REM   but in a for-in statement, "," is a special character, so it must be escaped by writing "^" before ",".
	REM
	REM NOTE:
	REM   If you want to dynamically execute an arbitrary command in the IN ( '...' ) of a FOR statement,
	REM   you need to enclose the path in double quotes.
	REM   In this case, you should avoid using double quotes (") at the beginning and end of the IN ( '...' ),
	REM   because then those double quotes will be forced to be stripped.
	REM
	REM   To do so, insert a dummy command, 'if x==x', at the very beginning of the IN ( '...' ).
	REM   Note that "set IN_BEGIN=if x^^=^^=x" by itself works with the "^" as an escape character,
	REM   so to store "if x^=^=x", you must use "if x^^=^^=x". 
	REM
	set IN_BEGIN=if x^^=^^=x
	set IN_REDIRECT_2TO1=2^^^>^^^&1
	REM set CMD=C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\link.exe
	REM for /f "delims=" %%a in ( '%IN_BEGIN% "%CMD%" ^| findstr /i "Version"' ) do echo [%%a]

	set FOUND_LNK4010=
	for /f "delims=" %%a in ( '%IN_BEGIN% "%TRY_LINK_EXE%" /MACHINE:%VC_MACHINE% /SUBSYSTEM:WINDOWS^,%TRY_VER% ^| %FINDSTR% /i "LNK4010:"' ) do set FOUND_LNK4010=%%a
	if "%FOUND_LNK4010%" == "" set DET_VC_SUBSYS_VER=%TRY_VER%

	exit /b
REM endof Sub-routine


REM Sub-routine Func_PrintPathStr
REM Arg    : const "%1" KEY
REM Arg    : const "%2" VAL
:Func_PrintPathStr
setlocal
	set KEY=%1
	set VAL=%2
	REM Strip sides quotes.
	set KEY=%KEY:~1,-1%
	set VAL=%VAL:~1,-1%

REM	echo   %KEY%=[%VAL%]

	if "%VAL%" == "" echo   %KEY% : {}
	if "%VAL%" == "" goto Func_PrintPathStr_End
	echo   %KEY% : {
	call :Func_PrintDelimitedStr "%VAL%" ";" "    [" "]"
	echo   }

:Func_PrintPathStr_End
endlocal
	exit /b
REM endof Sub-routine


REM Sub-routine Func_PrintDelimitedStr
REM Arg    : const "%1" VAL
REM Arg    : const "%2" DLM
REM Arg    : const "%3" PFX
REM Arg    : const "%4" SFX
:Func_PrintDelimitedStr
setlocal
	set VAL=%1
	set DLM=%2
	set PFX=%3
	set SFX=%4
	REM Strip sides quotes.
	set VAL=%VAL:~1,-1%
	set DLM=%DLM:~1,-1%
	set PFX=%PFX:~1,-1%
	set SFX=%SFX:~1,-1%

	REM
	REM NOTE:
	REM   The variable expansions in IN(...) and DO(...) are performed separately,
	REM   so the value of LEFT is expanded to the latest one each time after the execution of DO(...), as expected.
	REM
	set LEFT=%VAL%
	:Func_PrintDelimitedStr_Next
		for /f "tokens=1,* delims=%DLM%" %%a in ( "%LEFT%" ) do (
			echo %PFX%%%a%SFX%
			set LEFT=%%b
		)
		if not "%LEFT%" == "" goto Func_PrintDelimitedStr_Next

endlocal
	exit /b
REM endof Sub-routine


REM Sub-routine Func_Set
REM Arg    : const "%1" KEY
REM Arg    : const "%2" VAL
:Func_Set
setlocal
	set KEY=%1
	set VAL=%2
	REM Strip sides quotes.
	set KEY=%KEY:~1,-1%
	set VAL=%VAL:~1,-1%

:Func_Set_End
endlocal&& set %KEY%=%VAL%
	exit /b
REM endof Sub-routine


:End
Close


初期設定などは特になくソースファイルのある近くの適当な位置に置くだけです。 またどこから実行しても構いません(detect_vc.batが置かれているディレクトリ以外から実行しても大丈夫です)。

いわゆるインストール、アンインストールといった作業は不要で、しかもこの1ファイルだけで完結した存在です。 一時ファイル等は作成されません。 またレジストリの読み書き等はしておりません。

このdetect_vc.batを特に何も指定せず、コマンドプロンプト上で以下のように実行した場合、Visual Studio(VC本体)、Windows SDK、Windows Universal CRT SDKのすべてが デフォルトの位置にインストールされている場合に限ってはうまくいくと思います。 例えば以下のように、自動設定されたPATHの値、INCLUDEの値、LIBの値が表示されます。

C:\Zenkaku>detect_vc.bat
ExportMode: recommanded

VCVars:
  PATH : {
    [C:\Windows\system32]
    [C:\Windows]
    [C:\Program Files\TortoiseSVN\bin]
    [C:\Program Files (x86)\Git\cmd]
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin]
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\..\Common7\IDE]
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\..\Crystal Reports]
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\..\EnterpriseFrameworks]
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\..\ReportViewer]
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\..\sqlserver]
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\redist\x86\Microsoft.VC80.CRT]
  }
  INCLUDE : {
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\PlatformSDK\include]
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\include]
  }
  LIB : {
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\PlatformSDK\lib]
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\lib]
  }

DetectSubsystemVersion:
  DET_VC_SUBSYS_VER=[4.00]
C:\Zenkaku>


ここで注目して欲しいのは、最後の「DET_VC_SUBSYS_VER=[4.00]」と表示されている部分です。 この部分になんらかの数字(この例ですと4.00)が表示されているならdetect_vc.batは成功しています。 逆にこれが空、つまり「DET_VC_SUBSYS_VER=[]」のように表示された場合、detect_vc.batは失敗しています。 まずはこの値が空になっていないかどうかご確認ください(空であった場合の対策については後述します)。

detect_vc.batの成功/失敗の判定をバッチファイル上で行うこともできます。 この環境変数DET_VC_SUBSYS_VERが空か否かをif文で調べればよいです。 例えば以下のような感じです。

call detect_vc.bat
if "%DET_VC_SUBSYS_VER%" == "" goto Error_DetectVC

echo [OK]: DetectVC done successfully.
goto End

:Error_DetectVC
echo [NG]: Failed to execute DetectVC.
goto End

:End
pause


これがうまくいかない場合、すなわちDET_VC_SUBSYS_VERの値が空になっている場合、Visual Studio(VC本体)がインストールされたトップディレクトリが 標準の場所ではない可能性があります。 例えば(Cドライブではなく)Dドライブの D:\Program Files (x86) 配下や、シブい方だと C:\MyVC 配下なんてこともあるかもしれませんが、 この場合は、環境変数ZNK_VC_DIR、ZNK_VC_WINSDK_DIR、ZNK_VC_UCRT_DIRの3つをあらかじめ明示的に指定しておく必要があります。 それぞれの環境変数が示す意味についてはすぐ後で詳しく述べますが、一言で言うとZNK_VC_DIRがVC本体、ZNK_VC_WINSDK_DIRがWindows SDKのトップディレクトリの位置になります。 また ZNK_VC_UCRT_DIRがWindows Universal CRT SDKのトップディレクトリの位置になりますが、これに関してはVS2015以降をお使いの方が対象になります。

あるいはうまくいく場合であっても、複数のVSやWindows SDKをインストールしていて、どちらか一方を明示的に指定したい場合などでも これらの環境変数を明示的に指定することで対応可能です。

これらの環境変数をWindowsの環境変数設定ダイアログへ永続的に設定しておいてもよいですが、 我々はコマンドプロンプトやバッチファイルという便利な道具を知っていますので、次のようなバッチファイルを用意しておく手もあります (その方が慣れたテキストエディタで編集しやすいです)。 このバッチファイルをdetect_vc_config.batとしましょう。

detect_vc_config.bat

@echo off
REM VC Directory
REM set        ZNK_VC_DIR=C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\VC
REM set        ZNK_VC_DIR=D:\Program Files (x86)\Microsoft Visual Studio 14.0\VC
REM set        ZNK_VC_DIR=C:\MyVC\VC
REM set        ZNK_VC_DIR=E:\share_e\trunk_id1\mt_redundant_dev\mt_compiler_ms\minvs\vs2017\BuildTools\VC


REM Windows SDK
REM set ZNK_VC_WINSDK_DIR=C:\Program Files (x86)
REM set ZNK_VC_WINSDK_DIR=C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A
REM set ZNK_VC_WINSDK_DIR=D:\Program Files (x86)
REM set ZNK_VC_WINSDK_DIR=D:\Program Files (x86)\Windows Kits\8.1
REM set ZNK_VC_WINSDK_DIR=E:\share_e\trunk_id1\mt_redundant_dev\ms_devs\WinSDK_v7


REM Windows Universal CRT SDK
REM set   ZNK_VC_UCRT_DIR=C:\Program Files (x86)
REM set   ZNK_VC_UCRT_DIR=D:\Program Files (x86)
REM set   ZNK_VC_UCRT_DIR=D:\Program Files (x86)\Windows Kits\10
REM set   ZNK_VC_UCRT_DIR=E:\share_e\trunk_id1\mt_redundant_dev\ms_devs\WindowsUniversalCRTSDK


detect_vc_config.batのダウンロード

このdetect_vc_config.batの中身は、setコマンドによって、環境変数ZNK_VC_DIR、ZNK_VC_WINSDK_DIR、ZNK_VC_UCRT_DIRの3つをセットするテンプレートとなっています。 一応いくつかの例がコメントアウトされた状態で記述してあります。

行頭にREMとあるのがバッチファイルでのコメントアウトです。 この3文字を削除するとその部分が有効になります。

また「set 環境変数名=値」という書式で環境変数に値を設定できます。 ただし「=」の前後にスペースを入れてはいけません。 set コマンドで同じ環境変数名を2回以上設定した場合、最後の設定が有効になります。

この例を参考に必要な変数のみ、お使いのPCでのVCならびにWindows SDKのインストール位置に応じて明示的に指定してください。

それぞれの環境変数について説明します。 これらはいわばdetect_vc.batの入力となる環境変数ですが、この指定値は割とアバウトでも構いません。

  • ZNK_VC_DIR

  • VC本体のトップディレクトリの位置になります。
    指定する値は、「VC」という名前のディレクトリが現れるまでのパスです(「VC」までは含めて指定します)。

    ここまで与えてもらえばその配下にあるはずの bin, include, lib まではdetect_vc.batが自動的に潜って探します
    (あるいは bin, include, lib が直下に現れるまでのパスを明示的に指定していただいても構いません)。

    「VC」という名前のディレクトリは、VS2003では「Vc7」、VC6.0では「VC98」という名前になっていると思います。 (あまりいないとは思いますが)これらを使っている方は参考にしてください。

    またVS2017以降は「VC」ディレクトリの直下にbin, include, libは配置されておらず、 「VC\Tools\MSVC\バージョン番号」といったディレクトリの直下に位置が変更されていますが、 この場合でも「VC」ディレクトリが現れるまでのパスで構いません(「\Tools\MSVC\バージョン番号」の部分はあってもなくてもどちらでもよいです)。

  • ZNK_VC_WINSDK_DIR

  • Windows SDKのトップディレクトリの位置になります。
    指定する値は、「Program Files (x86)」に相当するディレクトリが現れるまでのパスです。

    ここまで与えてもらえばその配下にあるはずのWindows SDKのうち、最も古いものをdetect_vc.batが自動的に認識します (Windows SDK 6.0、7.1A などは Microsoft SDKs\Windows、Windows SDK 8.0以降は Windows Kitsというディレクトリ配下に格納されておりますが、 それらをチェックしているということですね)。

    さらにそれより奥にあるはずの bin, include, lib まではdetect_vc.batが自動的に潜って探します
    (あるいは bin, include, lib が直下に現れるまでのパスを明示的に指定していただいても構いません)。

  • ZNK_VC_UCRT_DIR

  • Windows Universal CRT SDKのトップディレクトリの位置になります。
    これに関してはVS2015以降をお使いの方が対象になります。 また、Windows SDK 10を使っている場合は、通常そのトップディレクトリと同じになるはずです。
    指定する値は、「Program Files (x86)」に相当するディレクトリが現れるまでのパスです。

    ここまで与えてもらえばその配下にあるはずのWindows Universal CRT SDKの位置(これは要するにWindows SDK 10ですが)をdetect_vc.batが自動的に認識します (Windows SDK 10は Windows Kitsというディレクトリ配下に格納されておりますが、それをチェックしているということですね)。

    さらにそれより奥にあるはずの bin, include, lib まではdetect_vc.batが自動的に潜って探します
    (あるいは bin, include, lib が直下に現れるまでのパスを明示的に指定していただいても構いません)。

detect_vc.bat の実行に成功しますと、VCの環境変数PATH, INCLUDE, LIBが自動的にセッティングされ、cl.exeやlink.exeなどが使える状態になります。 少なくともPATHの設定に関しては以下のような非常に簡単なテストでこれを確認できます。 「Microsoft(R) 32-bit C/C++ Optimizing Compiler」などと表示されればclコマンドが起動できています。

C:\Zenkaku>detect_vc.bat
C:\Zenkaku>cl


またdetect_vc.batが成功した場合、デフォルトでは環境変数DET_VC_SUBSYS_VERがセットされます。 これは現在お使いのlinkコマンドで指定できるSubsystemVersion の最小値が格納されています。 以下のようにlinkコマンドのオプションで指定して使います。

C:\Zenkaku>detect_vc.bat
C:\Zenkaku>cl -c test_hello.c
C:\Zenkaku>link /SUBSYSTEM:CONSOLE,%DET_VC_SUBSYS_VER% /OUT:test_hello.exe test_hello.obj

SubsystemVersionについてご存知ない方は何がなんだかわからないと思いますが、 この指定は特にVS2012以降でWindows XPでも動作するexeを作りたい場合などで重要です。 とりあえずlinkコマンドで指定するお守りのようなものくらいに思って頂いても結構です。 (WinMain関数を使ってコーディングしている方は、以下のCONSOLEの部分がWINDOWSとなります)。

これに関して詳細が気になる方は以下の参考をご覧ください。

参考: なぜDET_VC_SUBSYS_VERをlink.exeに指定する必要があるのか?
VS2012以降でWindows XPでも動作するような実行バイナリを作りたい場合、面倒ですがlink.exeの/SUBSYSTEMオプションに、 5.01を指定する必要があります。

もっと正確に言えば、x86バイナリ作成用のlink.exe には 5.01(Windows XP相当)の指定に加え 次のように /MACHINE:X86 オプションも指定しなければなりません。

C:\Zenkaku>link /MACHINE:X86   /SUBSYSTEM:CONSOLE,5.01 /OUT:test.exe test.obj

また x64バイナリ作成用のlink.exe には 5.02 (Windows XP Professional x64 Edition相当)の指定に加え 次のように /MACHINE:AMD64 オプションも指定しなければなりません。

C:\Zenkaku>link /MACHINE:AMD64 /SUBSYSTEM:CONSOLE,5.02 /OUT:test.exe test.obj

「/MACHINE:」オプションの指定と「/SUBSYSTEM:」オプションの指定の整合性がとれてないと link.exe はエラーを出します。

ただ「Windows XPでも動作するような実行バイナリ」という条件ならば、普通は32bit版バイナリさえ作れればいいわけですから (仮にそれをWindows XP Professional x64 Edition上に持っていっても動作しますから)、「とにかく5.01を指定する」と考えても そう差し支えないとは思います。

これを指定しなかった場合(つまりデフォルトでは)6.00が指定されたものとみなされてしまいます。 これは Windows Vista 以降で動作するバイナリになります。

6.00 以上の SubsystemVersion の値は x86、x64ともに共通の値になっています。 例えば Windows Vista 以降で動作するバイナリを作りたい場合、x86版、x64版いずれのバイナリを作る場合でも6.00を指定すればよいです。

つまりVS2012以降で生成されるバイナリは、デフォルトですとVista以降でしか動作しないものとなるわけですが、 それはイヤですので、(デフォルト値を変えて)5.01(あるいはx64版なら5.02)を指定するわけです。

ここで指定する値が小さいほど昔のWindowsでも動作するバイナリを作ることができます。 例えば5.00を指定しますと Windows 2000以降で動作するバイナリとなりますし、4.00を指定しますとWindows 95以降なら動作するバイナリとなります (尚、Windows 2000以前に対応したx64版バイナリは存在しません)。 ならば常に4.00固定でいいじゃんと思ってしまいますが、4.00や5.00を指定することができないVCが(特に新しいバージョンのVCで)存在します。

ではそれぞれのVCのバージョンの違いで、SUBSYSTEM Versionに指定可能な値の範囲は具体的にどう変わるのか? ここでその全体像を見ておく方がわかりやすいと思いますので、以下に列挙します。

  • VS2005以前のVC(正確にはVC付属のlink.exeですが)

  • 4.00(Windows 95相当)が指定可能な最小値であり、またそれがデフォルトです。

    3.00(Windows 3.1あたり?)などは指定できません。 仮に最小値である4.00より小さい値を指定した場合、link.exe は Warning LNK4010: を表示し、 ユーザの指定を無視してデフォルト値(4.00)が指定されたものとみなします。

    んじゃVS2005が一番いいんじゃね?
    まあぶっちゃげそうなんですが、ただしVS2005の場合、Expressエディション(無料で手に入るエディション)では、x64向けのバイナリを作るためのコンパイラが含まれていません。 Windows SDKに含まれている場合があるので、それと組み合わせるなどといった工夫が必要になります (ただし x64 向けのバイナリに限って言えば、いずれにせよSubsystemVersionの最小値は5.02(Windows XP Professional x64 Edition相当)にせざるを得ませんので、 VS2005である必要はないのですけども)。

    また、最近のC言語の参考書等でC言語の新しい文法を使っているものもあるっちゃあありますので、 そういった本のサンプルをとりあえず動かすために、新しいVCが欲しくなるケースもあるかもしれませんね…。

    まあ、MinGWなら全部動くんですがねッ!


  • VS2008のVC(正確にはVC付属のlink.exeですが)

  • 5.00(Windows 2000相当)が指定可能な最小値であり、またそれがデフォルトです。

    4.00などは指定できません。 仮に最小値である5.00より小さい値を指定した場合、link.exe は Warning LNK4010: を表示し、 ユーザの指定を無視してデフォルト値(5.00)が指定されたものとみなします。

  • VS2010のVC(正確にはVC付属のlink.exeですが)

  • 5.01(Windows XP相当)が指定可能な最小値であり、またそれがデフォルトです。 このバージョンからはExpressエディションでもx64向けバイナリを生成できるlink.exeが付属します。 そちらについては5.02(Windows XP Professional x64 Edition相当)が指定可能な最小値であり、またそれがデフォルトです。

    4.00や5.00などは指定できません。 仮に最小値である5.01(x64の場合は5.02)より小さい値を指定した場合、link.exe は Warning LNK4010: を表示し、 ユーザの指定を無視してデフォルト値(5.01(x64の場合は5.02))が指定されたものとみなします。

  • VS2012以降のVC(正確にはVC付属のlink.exeですが)

  • x86版のlink.exeの場合 5.01(Windows XP相当)、x64版のそれの場合5.02(Windows XP Professional x64 Edition相当)が指定可能な最小値ですが、 なんとそれがデフォルトではありません!。 6.00(Windows Vista相当)がデフォルトとなります。 従って、これらのVCではlink.exe のオプション引数に最小値を明示的に指定しなければならないというわけです。

    4.00や5.00などは指定できません。 仮に最小値である5.01(x64の場合は5.02)より小さい値を指定した場合、link.exe は Warning LNK4010: を表示し、 ユーザの指定を無視してデフォルト値(6.00)が指定されたものとみなします。

すべてのバージョンの積集合をとると、x86では5.01以上の範囲、x64では5.02になりますね。 x64版の場合は、将来のVCでどうなるか等考えなければ、5.02固定で構わないように思います(それより前の64bit版Windowsがそもそも存在しないからです)。 しかしながら x86版で5.01を固定で指定すると、今度はWindows XP以降でしか動作しないバイナリになり、 Windows 2000以前など古いWindowsをターゲットとしたバイナリを生成できなくなってしまいます。 つまりVS2008以前などで、Windows 2000向けのバイナリを作ろうといった場合にこの値では困ります。

もうWindows 2000向けのバイナリなんて作らないし…
まあぶっちゃげそうなんですが、たとえそうだとしても実は油断できません。 それでも万全を期すなら、やはりlink.exeのオプション指定に 5.01 (Windows XP相当)あるいは5.02(Windows XP Professional x64 Edition相当)という値をハードコーディングすべきではありません。

というのも、現時点ではこの値の最小値はどのコンパイラでも5.01ですが、これが将来リリースされるであろう未来のVCでは どうなるかわからないからです。 最小値が6.00に底上げされ、さらにデフォルト値が10.00になるなんてシナリオもありえます。

そうなるとこれに対処するため、5.01(または5.02)の値を修正して回る必要が生じます。 さもなければlink.exeは、この5.01(または5.02)という指定を無視して、おそらくデフォルト値(10.00)が指定されたものとみなすでしょう。 (これがまだ自動的に最小値が指定されたものとみなしてくれるなら助かるのですが、現在の挙動を見るにおそらくデフォルトが設定されるでしょう…)。

つまり、この種の値は極力ハードコーディングせず、最低限変数で設定すべきです。 さらにその値が自動的に取得できるならそれにこしたことはないわけです。


要するにどのバージョンのVCのlinkであろうとも、そのlinkで指定可能なSubsystem Versionの最小値を柔軟かつ自動的に指定したいわけです。 その最小値はVCのバージョンによって(さらに言えばx86かx64かによっても)変わってくるわけですが、そこの所を抽象化して書きたいがために、DET_VC_SUBSYS_VERという変数を用意しているわけです。

ではどうやってこの最小値を自動取得しましょう?

VCの(正確にはVC付属のlink.exeですが)のバージョンが自動取得できればよさそうですが、 それよりも「Warning LNK4010:」の「LNK4010:」という文字列がカギになるとは思えませんでしょうか? 私にはこれが救世主に見えます。 MSのlink.exeを開発したプログラマはこの種のError/Warningコードをこれまでずっと変えていませんし、おそらくこれから先も変えないと期待できます (マーケティングに全く関係しない部分だからです)。 というわけで未来のバージョンのVCにおいても利用できそうな指標です。

この「LNK4010:」は最小値より小さい値をlink.exeに指定した場合のみ表示されるわけですから、つまり 4.00、5.00、5.01、5.02、6.00、6.01、10.00と順に指定して試していき、 これが表示されなくなった瞬間の番号がそのバージョンのVCでの指定可能な最小値であるはずです。 DET_VC_SUBSYS_VERはそのような方法で取得しています。

Close

実際のコンパイルおよびリンク


それでは detect_vc.bat を使ってセッティングをした上で、実際に test_hello.c をコンパイル、リンクしてみます。

test_hello.c の中身はいわゆる Hello World を表示する程度のサンプルコードならなんでもいいですが、 作るのが面倒な方はこちらからダウンロードしてください。

x86版バイナリ(32bit/64bit Windows双方で動作するバイナリ)を作る場合、コマンドプロンプト上で次のように実行します (ただしdetect_vc_config.batの実行についてはこれを作成した方のみで結構です。detect_vc.batの前にこれを実行します)。

C:\Zenkaku>set MACHINE=x86
C:\Zenkaku>detect_vc_config.bat
C:\Zenkaku>detect_vc.bat
C:\Zenkaku>cl -c test_hello.c
C:\Zenkaku>link /MACHINE:X86   /SUBSYSTEM:CONSOLE,%DET_VC_SUBSYS_VER% /OUT:test.exe test.obj

x64版バイナリ(64bit Windows専用で動作するバイナリ)を作る場合、コマンドプロンプト上で次のように実行します (ただしdetect_vc_config.batの実行についてはこれを作成した方のみで結構です。detect_vc.batの前にこれを実行します)。

C:\Zenkaku>set MACHINE=x64
C:\Zenkaku>detect_vc_config.bat
C:\Zenkaku>detect_vc.bat
C:\Zenkaku>cl -c test_hello.c
C:\Zenkaku>link /MACHINE:AMD64 /SUBSYSTEM:CONSOLE,%DET_VC_SUBSYS_VER% /OUT:test.exe test.obj

まず最初に、環境変数MACHINEにx86またはx64の値を指定しておきます。 detect_vc.batは内部でこの環境変数を参照していますので、あらかじめこの環境変数を明示的に設定しておく必要があります。 ただし設定されていない場合はデフォルトでは x86 が設定されているものとみなします。 detect_vc.batの第1引数にこの値を指定することもできます。

次に detect_vc_config.bat において、ZNK_VC_* 系の環境変数の指定を明示的に行います。 detect_vc.batだけの実行ではうまくいかない場合にこれで調整してください。

次に detect_vc.bat 本体を実行します。 デフォルトでは、VC用の環境変数PATH、INCLUDE、LIB、そしてdetect_vc.bat独自の環境変数であるDET_VC_SUBSYS_VERが設定されます。

Cソースファイル一個しかないなら本当は、cl test_hello.c でも作成できてしまいますが、 コンパイルとリンカを分けて実行した方が教育上、そして応用上も望ましいのでここでもそうしています。 とはいえ、これを毎回コマンドプロンプトから打ち込むのも面倒でしょうから、例えば次のようなバッチファイルmy_build.batを作っておき、 ダブルクリックでコンパイル、リンクできるようにしておくのも最初の取り掛かりとしてはいいでしょう。

my_build.bat

@echo off

REM x86 or x64.
REM x86 : Build x86 binary (for 32bit/64bit Windows)
REM x64 : Build x64 binary (for 64bit Windows only)
set MACHINE=x86

REM Define ZNK_VC_DIR, ZNK_VC_WINSDK_DIR, ZNK_VC_UCRT_DIR
if exist "detect_vc_config.bat" call detect_vc_config.bat

REM Define PATH, INCLUDE, LIB and DET_VC_SUBSYS_VER for VC, target binary %MACHINE%.
call detect_vc.bat

REM Making output directory automatically.
set OUT_DIR=.\out_dir\vc%MACHINE%
if not exist %OUT_DIR% mkdir %OUT_DIR%


REM Compile.
@echo on
	cl -Fo%OUT_DIR%\test_hello.obj -c test_hello.c
@echo off

REM Link.
set VC_MACHINE=X86
if "%MACHINE%" == "x86" set VC_MACHINE=X86
if "%MACHINE%" == "x64" set VC_MACHINE=AMD64
set LINKER_OPT=/MACHINE:%VC_MACHINE% /SUBSYSTEM:CONSOLE,%DET_VC_SUBSYS_VER%
@echo on
	link %LINKER_OPT% /OUT:%OUT_DIR%\test_hello.exe %OUT_DIR%\test_hello.obj
@echo off

pause


my_build.batのダウンロード

環境変数OUT_DIRまわりの処理について解説しておきます。

作成するobjファイルとexeファイルは、test_hello.c と同列のディレクトリに置くと散らかるので、 out_dir\vcx86やout_dir\x64というディレクトリを作ってその中に生成されるようにします。 そうすればout_dirというディレクトリを消すだけで、バイナリがまだ全く作られていない元の状態にリセット、クリーンできます。

またこのようにすることでx86版とx64版を両方作るとき、それらを完全に分離できます。 「set OUT_DIR=.\out_dir\vc%MACHINE%」として環境変数OUT_DIRの値を構築しておりますが、 これにより環境変数MACHINEに与える値に応じて、出力先のディレクトリを変えることができる仕組みです。

ディレクトリの作成には、コマンドプロンプトの組込みコマンドであるmkdirを用いています。 このコマンドに作成したいディレクトリのパスを指定すると、そのディレクトリが作られます。

clコマンドにおいては、cファイルをobjファイルにコンパイルします。 -Foオプションで実際に出力されるobjファイルのパスを明示的に指定できます。 このとき、-Foとファイル名の間にスペースを空けてはいけません。 この例ですと、%OUT_DIR%\test_hello.obj が出力されるobjファイルのパスになります。

この前後に @echo on と @echo off とあるのは、実際どんな cl コマンドが実行されたのかをバッチファイル実行時に一時的に表示して欲しいからです (逆にそのようにしないと、もしもコンパイルエラーやリンクエラーが出たとき、 かつそれが何らかのコンパイル/リンカオプションの指定の不備に由来するものであったとき、 エラーの原因に気づきにくくなります)。 通常バッチファイルは@echo offとしておりますので、clコマンドが終わったら、再び@echo offとします(linkコマンドについても同様のことをしています)。

最後にlinkコマンドでリンクします。 MACHINEの値によって、/MACHINE:オプションに与えるべき値はX86やAMD64といった値に変化しますので注意してください。 /SUBSYSTEM:オプションには上述しました DET_VC_SUBSYS_VERを指定します。 /OUT:オプションで実際に出力するexeのパスを明示的に指定できます。

VC本体、Windows SDK、Windows Universal CRT SDKのトップディレクトリを変更する


VC本体、Windows SDK、Windows Universal CRT SDKのトップディレクトリを、デフォルトの位置から変更することは、この記事の大きな目標の一つです。 他のCコンパイラ、たとえばMinGWなどでは当たり前のようにできることが、VCでは中々面倒なのです。 detect_vc.bat を使えば、上のdetect_vc_config.batで書かれているパスで変更先を指定してやれば、 VC、Windows SDK、Windows Universal CRT SDKのトップディレクトリがいかなる場所にあっても対応できます。

ただしコマンドプロンプトやバッチファイルは「\\」で始まるパス(他のマシンの共有ディレクトリを使っている場合ですね)には対応できないので、 そのような位置にある場合はあらかじめ「ネットワークドライブの割り当て」をしておく必要があると思います。

修正例:
REM VC Directory
REM set        ZNK_VC_DIR=C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\VC
REM set        ZNK_VC_DIR=D:\Program Files (x86)\Microsoft Visual Studio 14.0\VC
REM set        ZNK_VC_DIR=C:\MyVC\VC
set        ZNK_VC_DIR=E:\share_e\trunk_id1\mt_redundant_dev\mt_compiler_ms\minvs\vs2017\BuildTools\VC


REM Windows SDK
REM set ZNK_VC_WINSDK_DIR=C:\Program Files (x86)
REM set ZNK_VC_WINSDK_DIR=C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A
REM set ZNK_VC_WINSDK_DIR=D:\Program Files (x86)
REM set ZNK_VC_WINSDK_DIR=D:\Program Files (x86)\Windows Kits\8.1
set ZNK_VC_WINSDK_DIR=E:\share_e\trunk_id1\mt_redundant_dev\ms_devs\WinSDK_v7


REM Windows Universal CRT SDK
REM set   ZNK_VC_UCRT_DIR=C:\Program Files (x86)
REM set   ZNK_VC_UCRT_DIR=D:\Program Files (x86)
REM set   ZNK_VC_UCRT_DIR=D:\Program Files (x86)\Windows Kits\10
set   ZNK_VC_UCRT_DIR=E:\share_e\trunk_id1\mt_redundant_dev\ms_devs\WindowsUniversalCRTSDK

例えば上記は私のPCでのVC本体、Windows SDK、Windows Universal CRT SDKのトップディレクトリを指定しています。 かなり突拍子もない位置へ移動していますが、この3つの環境変数で対応できます。 このような移動は、単にエクスプローラ上で普通にフォルダをそのまままるごと他の場所へ移動すればOKです。

私がどう移動させているか、参考までに少し詳しく状況を書くと、それぞれ以下のような感じになっています。

  • デフォルトの C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildToolsフォルダ(とその配下全体)

  • E:\share_e\trunk_id1\mt_redundant_dev\mt_compiler_ms\minvs\vs2017\ 直下へ移動しています。

  • デフォルトの C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A フォルダ(とその配下全体)

  • E:\share_e\trunk_id1\mt_redundant_dev\ms_devs\WinSDK_v7 配下へ移動しています。 ただし(C:\Program Files (x86)よりも深部である) Microsoft SDKs\Windows\v7.1Aという部分の構造は維持して移動しております。 つまり移動先は、(WinSDK_v7以下だけを書きますと)WinSDK_v7\Microsoft SDKs\Windows\v7.1A となっており、 かつ(WinSDK_v7配下には)それしか含まれていない状態にしています。

    私の場合、v7.1Aの他にもv6.0やv5.2など色々なWindows SDKをこれと同列のディレクトリに格納しており、 それらを適宜切り替えて開発/実験するといったようなことをしております。 そのため、PlatformSDK_v5.2、WinSDK_v6、WinSDK_v7、WinSDK_v8 などという名前をつけたフォルダをすべて 同列のディレクトリに置いて、その名前からその配下に含まれるバージョンが即座にわかるようにし、 かつ元々あったProgram Files (x86)より深い部分の構造はできるだけ維持するといった形をとっております。

  • デフォルトの C:\Program Files (x86)\Windows Kits\10 フォルダ(とその配下全体)

  • E:\share_e\trunk_id1\mt_redundant_dev\ms_devs\WindowsUniversalCRTSDK 配下へ移動しています。 Windows SDKのときと同様に (C:\Program Files (x86)よりも深部である)Windows Kits\10という部分の構造は維持して移動しております。 つまり移動先は、(WindowsUniversalCRTSDK以下だけを書きますと)WindowsUniversalCRTSDK\Windows Kits\10 となっており、 かつ(WindowsUniversalCRTSDK配下には)それしか含まれていない状態にしています。

移動後は、キチンと動作するか否か(要するにコンパイルとリンクができるかどうか)のテストを一通りしておくべきでしょう。 テストについては「やさしいテスト」のセクションで説明します。

補足: Visual Studioのアンインストーラを起動すべきか?
デフォルトの位置を移動させるのはいいが、レジストリにデフォルトのインストール場所の情報が残って気持ち悪いという潔癖症な方は、 まず移動先へ移動ではなく一旦コピーしておき、そのあとVisual Studioのアンインストーラでアンインストールすればよろしいでしょう。 ただし、アンインストーラでも削除されず残るファイルやディレクトリもあります(デフォルトのインストール位置に残ってるはずです)ので、 このような残ったゴミはやはり手動で削除する必要があります。


目次に戻る

複数のディレクトリからdetect_vc.batを共有して使う


コマンドプロンプトから全部やった場合


ここまでは test_hello.c が最低限コンパイルできるかテストするような使い方でしたが、 ここでは複数のディレクトリがあり、さらにそれぞれに複数のCファイルがある場合に どのように detect_vc.bat を使えばよいか、そのシミュレーションしてみます。

例として C:\Zenkaku\my_proj ディレクトリ配下に次のような階層でディレクトリがあり、その中にいくつかのCソースファイルが含まれているような構成を考えます。
  • ディレクトリmy_proj
    • detect_vc.bat
    • detect_vc_config.bat
    • make_world.bat
    • make_world_clean.bat
    • ディレクトリapp1
      • main.c
      • app1.c
      • build.bat
      • Makefile_vc.mak
    • ディレクトリapp2
      • main.c
      • app2.c
      • build.bat
      • Makefile_vc.mak
    • ディレクトリutil
      • util1.c
      • util2.c
      • build.bat
      • Makefile_vc.mak

この階層をまるごとzip化したmy_proj.zipも用意しておきましたので、実際に試したい方はダウンロードしてください。

my_proj.zipのダウンロード

detect_vc_config.bat および detect_vc.bat は異なるディレクトリ下からでも実行できます。 また一時ファイル等も作られませんので異なるプロセスから同時に実行されたとしても衝突は起きません。 色んなディレクトリからこれらのbatを共用して構いません。 従ってこの例では、ディレクトリmy_proj直下にdetect_vc_config.bat と detect_vc.bat をそれぞれ一つだけ置いてあります。

今回は説明のため、コマンドプロンプトから全部やるとしたらどうなるかをシミュレーションしますが、 実際にはこんなのを全部打ち込むのは面倒ですので、Makefileを使うか、せめてbuild.batのようなバッチファイルを作っておき、 ダブルクリックだけで一連の処理ができるようにしておくべきでしょう (これについては後で詳しく述べます)。

ディレクトリapp1内のCソースのコンパイルとリンクを行う手順は以下のようになります。
  1. 「cl -c」ですべてのCソースをコンパイルし、objファイルだけを作る
  2. 生成されたすべてのobjファイルを linkコマンドによってリンクし、app1.exeという一つの実行バイナリを作る

以下に実行例を示します。

ここではx86版のみ示しますが、x64版の場合は「MACHINE=x86」の部分を「MACHINE=x64」に、 「/MACHINE:X86」の部分を「/MACHINE:AMD64」と読み替えてください。 また「..\」は一つ上のディレクトリを示します(親ディレクトリとも呼ばれます)。 よって「..\detect_vc_config.bat」は一つ上のディレクトリにあるdetect_vc_config.batを実行するという意味になります。

C:\Zenkaku>cd my_proj
C:\Zenkaku\my_proj>cd app1
C:\Zenkaku\my_proj\app1>
C:\Zenkaku\my_proj\app1>set MACHINE=x86
C:\Zenkaku\my_proj\app1>..\detect_vc_config.bat
C:\Zenkaku\my_proj\app1>..\detect_vc.bat
C:\Zenkaku\my_proj\app1>
C:\Zenkaku\my_proj\app1>cl -c main.c
C:\Zenkaku\my_proj\app1>cl -c app1.c
C:\Zenkaku\my_proj\app1>link /MACHINE:X86   /SUBSYSTEM:CONSOLE,%DET_VC_SUBSYS_VER% /OUT:app1.exe main.obj app1.obj

これで app1.exe が出来上がりました。 app1については終わりとします。

次にapp2を作りますが、これは util.lib というライブラリに依存します。 よって先にディレクトリutilへ移動し、util.lib を作ります。 次の手順となります。
  1. 「cl -c」ですべてのCソースをコンパイルし、objファイルだけを作る
  2. 生成されたすべてのobjファイルを libコマンド(これはライブラリアンと呼ばれます)によってutil.libという一つのファイルに纏める

libコマンドは今回初めて使うと思いますが簡単です。 linkコマンドのときのように/MACHINE:オプションで何かを指定する必要もありません。 使うオプションは「/OUT:」で出力ファイル名を指定するくらいです。

detect_vc.batは既に実行済みなので今回は省略できます。 以下に実行例を示します。

C:\Zenkaku\my_proj\app1> cd ..\util
C:\Zenkaku\my_proj\util1>
C:\Zenkaku\my_proj\util1>cl -c util1.c
C:\Zenkaku\my_proj\util1>cl -c util2.c
C:\Zenkaku\my_proj\util1>lib /OUT:util.lib util1.obj util2.obj

これで util.lib が出来上がりました。

次にディレクトリapp2へ移動し、app1のときと同様にコンパイルとリンクを行います。 ただし今回一点だけ違うのは、先ほど作った util.lib もリンクする点です。 app2ディレクトリから見るとこれは ..\util\util.lib にあります。

以下に実行例を示します。

C:\Zenkaku\my_proj\util> cd ..\app2
C:\Zenkaku\my_proj\app2>
C:\Zenkaku\my_proj\app2>cl -c app2.c
C:\Zenkaku\my_proj\app2>cl -c main.c
C:\Zenkaku\my_proj\app1>link /MACHINE:X86   /SUBSYSTEM:CONSOLE,%DET_VC_SUBSYS_VER% /OUT:app2.exe main.obj app2.obj ..\util\util.lib

これで app2.exe が出来上がりました。 app2については終わりとします。

最終的に出来上がった生成物は app1.exe と app2.exe になります。 また util.lib には汎用的で再利用可能な処理を纏めることによって、他のアプリを開発するときにも使えるものとなるでしょう。

バッチファイルから全部やった場合


my_proj配下には、app1、app2、utilそれぞれにコンパイルとリンク用のバッチファイルbuild.batを作っており、 それをダブルクリックするだけでdetect_vc.batによるセッティング、コンパイル、リンクまですべて行えるようになっています (detect_vc_config.batの設定が適切ならばですが)。

まずはmy_proj\app1\build.batをご覧ください (あるいはmy_projをダウンロードした方はこれを実行してみてください)。

my_proj\app1\build.bat

@echo off

REM x86 or x64.
REM x86 : Build x86 binary (for 32bit/64bit Windows)
REM x64 : Build x64 binary (for 64bit Windows only)
set MACHINE=x86

REM Define ZNK_VC_DIR, ZNK_VC_WINSDK_DIR, ZNK_VC_UCRT_DIR
call ..\detect_vc_config.bat
if exist "..\my_config.bat" call ..\my_config.bat

REM Define PATH, INCLUDE, LIB and DET_VC_SUBSYS_VER for VC, target binary %MACHINE%.
call ..\detect_vc.bat
if "%DET_VC_SUBSYS_VER%" == "" goto Error_DetectVC

REM Making output directory automatically.
set OUT_DIR=.\out_dir\vc%MACHINE%
if not exist %OUT_DIR% mkdir %OUT_DIR%


REM Compile.
@echo on
	cl -Fo%OUT_DIR%\main.obj /c main.c
	cl -Fo%OUT_DIR%\app1.obj /c app1.c
@echo off

REM Link.
set VC_MACHINE=X86
if "%MACHINE%" == "x86" set VC_MACHINE=X86
if "%MACHINE%" == "x64" set VC_MACHINE=AMD64
set LINKER_OPT=/MACHINE:%VC_MACHINE% /SUBSYSTEM:CONSOLE,%DET_VC_SUBSYS_VER%
@echo on
	link %LINKER_OPT% /OUT:%OUT_DIR%\app1.exe %OUT_DIR%\main.obj %OUT_DIR%\app1.obj
@echo off

goto End


:Error_DetectVC
echo [NG]: Failed to execute DetectVC.
goto End

:End
pause



my_build.batを少し修正したような形になっており、比較的平易なバッチファイルです。 my_build.batと比べて今回新しいのは以下の点になります。
  • 一つ上のディレクトリのdetect_vc_config.batやdetect_vc.batを実行している
  • detect_vc.batの実行に失敗した場合は、最後のError_DetectVCにジャンプしてエラーメッセージを表示するようにしている
  • 複数のCソースファイルをコンパイルし、複数のオブジェクトファイルを生成している
  • 複数のオブジェクトファイルをlinkに指定している

次にmy_proj\util\build.batをご覧ください。 (あるいはmy_projをダウンロードした方はこれを実行してみてください)。
my_proj\app1\build.bat と比べて大差ないため、際立って異なる部分だけを以下に抜粋します (唯一違うのはlinkコマンドを使ってexeを作っているのではなく、libコマンドを使って(スタティック)ライブラリを作っている所です)。
全体を見たい方は「全ソースコード」をクリックしてください。

REM Make static-library.
@echo on
	lib /OUT:%OUT_DIR%\util.lib %OUT_DIR%\util1.obj %OUT_DIR%\util2.obj
@echo off
util\build.batの全ソースコード (40 lines)

@echo off

REM x86 or x64.
REM x86 : Build x86 binary (for 32bit/64bit Windows)
REM x64 : Build x64 binary (for 64bit Windows only)
set MACHINE=x86

REM Define ZNK_VC_DIR, ZNK_VC_WINSDK_DIR, ZNK_VC_UCRT_DIR
call ..\detect_vc_config.bat
if exist "..\my_config.bat" call ..\my_config.bat

REM Define PATH, INCLUDE, LIB and DET_VC_SUBSYS_VER for VC, target binary %MACHINE%.
call ..\detect_vc.bat
if "%DET_VC_SUBSYS_VER%" == "" goto Error_DetectVC

REM Making output directory automatically.
set OUT_DIR=.\out_dir\vc%MACHINE%
if not exist %OUT_DIR% mkdir %OUT_DIR%


REM Compile.
@echo on
	cl -Fo%OUT_DIR%\util1.obj /c util1.c
	cl -Fo%OUT_DIR%\util2.obj /c util2.c
@echo off

REM Make static-library.
@echo on
	lib /OUT:%OUT_DIR%\util.lib %OUT_DIR%\util1.obj %OUT_DIR%\util2.obj
@echo off

goto End


:Error_DetectVC
echo [NG]: Failed to execute DetectVC.
goto End

:End
pause
Close


最後にmy_proj\app2\build.batをご覧ください (あるいはmy_projをダウンロードした方はこれを実行してみてください)。
my_proj\app1\build.bat と比べて大差ないため、際立って異なる部分だけを以下に抜粋します (唯一違うのはlinkコマンドの部分で、一番最後の引数として「..\util\%OUT_DIR%\util.lib」という util内で作成したライブラリが指定されているところです)。
全体を見たい方は「全ソースコード」をクリックしてください。

@echo on
	link %LINKER_OPT% /OUT:%OUT_DIR%\app2.exe %OUT_DIR%\main.obj %OUT_DIR%\app2.obj ..\util\%OUT_DIR%\util.lib
@echo off
app2\build.batの全ソースコード (44 lines)

@echo off

REM x86 or x64.
REM x86 : Build x86 binary (for 32bit/64bit Windows)
REM x64 : Build x64 binary (for 64bit Windows only)
set MACHINE=x86

REM Define ZNK_VC_DIR, ZNK_VC_WINSDK_DIR, ZNK_VC_UCRT_DIR
call ..\detect_vc_config.bat
if exist "..\my_config.bat" call ..\my_config.bat

REM Define PATH, INCLUDE, LIB and DET_VC_SUBSYS_VER for VC, target binary %MACHINE%.
call ..\detect_vc.bat
if "%DET_VC_SUBSYS_VER%" == "" goto Error_DetectVC

REM Making output directory automatically.
set OUT_DIR=.\out_dir\vc%MACHINE%
if not exist %OUT_DIR% mkdir %OUT_DIR%


REM Compile.
@echo on
	cl -Fo%OUT_DIR%\main.obj /c main.c
	cl -Fo%OUT_DIR%\app2.obj /c app2.c
@echo off

REM Link.
set VC_MACHINE=X86
if "%MACHINE%" == "x86" set VC_MACHINE=X86
if "%MACHINE%" == "x64" set VC_MACHINE=AMD64
set LINKER_OPT=/MACHINE:%VC_MACHINE% /SUBSYSTEM:CONSOLE,%DET_VC_SUBSYS_VER%
@echo on
	link %LINKER_OPT% /OUT:%OUT_DIR%\app2.exe %OUT_DIR%\main.obj %OUT_DIR%\app2.obj ..\util\%OUT_DIR%\util.lib
@echo off

goto End


:Error_DetectVC
echo [NG]: Failed to execute DetectVC.
goto End

:End
pause
Close

Makefileの使用


ソースコードの数が増えてきたら、バッチファイルだけですべてを行う方法ではいつか限界がきます。 そのときはMakefileの作成なども検討してみてください。 VCではnmakeコマンドが使えるはずです。

my_projには簡単なMakefile(ファイル名はMakefile_vc.mak)も付属させております。 これを使ってコマンドプロンプト上からコンパイルとリンクをするには「nmake -f Makefile_vc.mak」と実行します。

まずは my_proj\app1内のMakefile_vc.makを実行させてみます。 その様子を以下に示します。

C:\Zenkaku> cd my_proj\app1
C:\Zenkaku\my_proj\app1>
C:\Zenkaku\my_proj\app1>set MACHINE=x86
C:\Zenkaku\my_proj\app1>..\detect_vc_config.bat
C:\Zenkaku\my_proj\app1>..\detect_vc.bat
C:\Zenkaku\my_proj\app1>
C:\Zenkaku\my_proj\app1>nmake -f Makefile_vc.mak

この場合でもまず最初にdetect_vc.batは実行させておきます。

ここではMakefileの文法については詳しくは説明しませんが(それをやるとそれだけで何本もの記事になってしまいます)、 重要な部分だけを抜粋して、簡単には説明しておきます。 全体を見たい方は「全ソースコード」をクリックしてください。

INCLUDE_FLAG =  \

EXE_FILE0=$O\app1.exe
OBJS0=\
	$O\app1.obj \
	$O\main.obj \

LIBS0=\

# Product files rule.
$(EXE_FILE0): $(OBJS0) 
	$(LINKER) /OUT:$(EXE_FILE0)  $(OBJS0) $(LIBS0)
app1\Makefile_vc.makの全ソースコード (77 lines)

# Source directory
S = .

!IF "$(MACHINE)" == "x64"
VC_MACHINE=AMD64
PLATFORM=win64
!ELSE
MACHINE=x86
VC_MACHINE=X86
PLATFORM=win32
!ENDIF
# Output directory
ABINAME=vc$(MACHINE)$(DEBUG)
O = .\out_dir\$(ABINAME)

#
# In VS2012(VC11) or later, if you want to generate XP-compatible EXE/DLL, you need to specify the subsystem-version (5.01/5.02)
# explicitly in LINK /SUBSYSTEM: option. For example, LINK /SUBSYSTEM:WINDOWS,5.01 etc.
# This is because the subsystem-version is set to 6.00 (target OS is Vista or later) by default in VC11 or later.
#
# Note that the version specified in the /SUBSYSTEM option of LINK has a different minimum value for each compiler.
# For VC2005 and earlier, the minimum value is 4.00 (target OS is Windows95 or later).
# For VS2008 (VC9) and VS2010 (VC10), the minimum value is 5.00 (target OS is Windows2000 or later).
# For VC2012 or later, the minimum value is 5.01/5.02 (target OS is WindowsXP x86 / Windows XP x64, Windows Server 2003 or later).
#
# If you specify a value that is less than the minimum, LINK will output an error LNK4010,
# so you can automatically get the minimum version value that should be specified by detecting that string.
# The detect_vc.bat (by Zenkaku@znk_project) will automatically retrieve it and set the result to the environment variable DET_VC_SUBSYS_VER.
# 
LINKER_OPT_SUBSYSTEM=/SUBSYSTEM:CONSOLE,$(DET_VC_SUBSYS_VER)

!IF "$(DEBUG)" == "d"
COMPILER=cl -nologo -EHsc /GR /W4 /MTd /Z7 /D_DEBUG /RTC1 
LINKER=link /MACHINE:$(VC_MACHINE) $(LINKER_OPT_SUBSYSTEM) /DEBUG /INCREMENTAL:NO 
!ELSE
COMPILER=cl -nologo -EHsc /GR /W4 /MT /O2 /DNDEBUG 
LINKER=link /MACHINE:$(VC_MACHINE) $(LINKER_OPT_SUBSYSTEM)  
!ENDIF


INCLUDE_FLAG =  \

EXE_FILE0=$O\app1.exe
OBJS0=\
	$O\app1.obj \
	$O\main.obj \

LIBS0=\


# Entry rule.
all: all_at_first $O $(EXE_FILE0)

# Init.
all_at_first:

# Mkdir rule.
$O:
	if not exist $O mkdir $O


# Product files rule.
$(EXE_FILE0): $(OBJS0) 
	$(LINKER) /OUT:$(EXE_FILE0)  $(OBJS0) $(LIBS0) 


# Suffix rule.
{$S}.c{$O}.obj:
	$(COMPILER) -I$S $(INCLUDE_FLAG) -Fo$@ -c $<

{$S}.cpp{$O}.obj:
	$(COMPILER) -I$S $(INCLUDE_FLAG) -Fo$@ -c $<


# Clean rule.
clean:
	rmdir /S /Q $O\ 
Close


なんだかバッチファイルのときより長くなっていますが、実際にはそれよりも楽に修正等ができます。 殆どの部分は修正する必要のない固定的で決まりきった処理だからです。

Makefile初心者にとって最初に抑えておくポイントは、Cのソースファイルが増えたとき、どこを修正するかです。 中ほどに「INCLUDE_FLAGS=」と書かれた行があります。 弄くる箇所はこのあたりになると思います。 上ではこの部分だけを抜粋しています。

Cのソースファイルが増えたときに、修正しなければならない箇所は、「OBJS0=」の部分です。 拡張子「c」のファイルではなく、拡張子「obj」ファイル(オブジェクトファイル)を指定しておりますがこの部分です。 Makefileでは、Suffix rule というものを用いて、拡張子「obj」のファイルの指定から自動的に拡張子「c」のファイル名を導出します。

Suffix ruleの記述を工夫することで、拡張子「c」のファイルと拡張子「obj」ファイルが異なるディレクトリである場合にも対応させることができます。 今回がまさにそのケースで、Makefile_vc.mak内の「Suffix rule」と書かれた部分でその処理を実装してあります。

従って、拡張子「c」のファイルについては明示的に指定する必要がありません(ここがバッチファイルと比べて楽なポイントの一つです)。

この例では「タブ文字$O\app1.obj \」、「タブ文字$O\main.obj \」のようにオブジェクトファイルが列挙されてあります。 $O の部分は、Oという名前の変数に入った値を参照しています。 Makefileの一番上の方(13行目あたり)で「O = .\out_dir\$(ABINAME)」としておりますが、例えば変数ABINAMEの値がvcx86であった場合、 変数Oの値として「.\out_dir\vcx86」がセットされることになります。

「O」のようにその変数名が1文字ならば、その値の参照は直前に「$」をつけて「$O」とします。 「ABINAME」のようにその変数名が2文字以上ならば、まず「(」と「)」でその名前を囲った上でその直前に「$」をつけて「$(ABINAME)」とします。

また「O = .\out_dir\$(ABINAME)」という代入文において、「=」の前後にスペースを含めても構いません。 このときそれらのスペースは無かったものとして処理されます。
Makefileにおける変数は、環境変数とも関係しており、たとえば $(DET_VC_SUBSYS_VER) と記述すると、環境変数DET_VC_SUBSYS_VERに設定された値をMakefile内からも参照できます。 しかしこれはあくまで環境変数DET_VC_SUBSYS_VERそのものではなく、そのクローンが(nmake.exeによって自動的に)定義されたものです。 従って、仮にMakefile内でDET_VC_SUBSYS_VERの値を上書きしたとしても、そのクローンの値が書き変わるだけで、 実際の環境変数DET_VC_SUBSYS_VERの値には何の影響もありません。

これはWindowsの環境変数ダイアログで永続的に設定された環境変数と、コマンドプロンプト内でsetコマンドにより一時的に設定された環境変数の関係性とよく似ています。

行末の「\」はエスケープ文字であり、改行をエスケープしています。 これにより、本来1行に収めなければならない代入文を複数に渡って記述できます。 ただし以下の点に注意してください。 以下で「この箇所」とは「OBJS0=\」とある行の次以降の行です。
  1. この箇所においては、オブジェクトファイルを1行に一つづつ書きます。

  2. これは主に見易さと修正のしやすさのためです。

  3. この箇所においては、各行はタブ文字から始めることとします。

  4. これも単に見易さのためです。 ただしMakefileの後半、ルールと呼ばれる行についてはMakefileの文法上必ずタブ文字で始めなければならないなどの決まりがあります。 まあそれは置いておくとしてもタブを入れた方が見やすいので、ここでもこうしています。

  5. この箇所においては、最後は「 \」(スペースとバックスラッシュ)が来て改行としなければなりません。

  6. これは仮に「DOSのディレクトリ区切り子の意味」での「\」が最後に来るような値が指定されていた場合(ディレクトリなどの指定ではあり得ます)、 その直後に来る「記述が複数行に跨るために使用するエスケープの意味」での「\」がその意味を失わないために必要です。

  7. 最終行(この例では「タブ文字$O\main.obj \」ですが)の次の行は空行を一つ以上入れます。

  8. この例では、「タブ文字$O\main.obj \」の行と「LIBS0=\」の行の間に空行が一つ入っています。 これは最終行も他の行に揃えてバックスラッシュで終わらせているためです(修正時に誤りが起き難くするよう敢えてそうしています)。 本当の改行はその次の行(空行)の行末に来ます。
この注意を守りつつ修正を行います。 このとき例えば main.c が不要になったなら、以下のように単にその行を削除すればよいことになります。

INCLUDE_FLAG =  \

EXE_FILE0=$O\app1.exe
OBJS0=\
	$O\app1.obj \

LIBS0=\


あるいは新しいCソースファイル new1.c を追加したい場合なら、以下のように単に他の行と同様の1行を追加すればよいことになります。

INCLUDE_FLAG =  \

EXE_FILE0=$O\app1.exe
OBJS0=\
	$O\app1.obj \
	$O\main.obj \
	$O\new1.obj \

LIBS0=\


Makefile_vc.makの下の方に次のような記述があります。 この意味ついても解説しておきます。

# Product files rule.
$(EXE_FILE0): $(OBJS0) 
	$(LINKER) /OUT:$(EXE_FILE0)  $(OBJS0) $(LIBS0)


まず「$(EXE_FILE0): $(OBJS0)」の部分は $(EXE_FILE0) を作る材料は $(OBJS0) であると述べています。 つまり $(OBJS0) がすべて揃っていないならば(材料が不足しているわけですから)先に材料の方を作らなければなりません。 これらがすべて揃って初めてこの部分が実行され、$(EXE_FILE0) が作られるということです。 このような文で、「:」より左をターゲットと呼び、特に作られるものを強調して言及したい場合ターゲット$(EXE_FILE0)などと言います。

ちなみに今、$(EXE_FILE0) の値は「$O\app1.exe」となっています。 また、$(OBJS0) とはつまり上で指定したオブジェクトファイルのリストでした。 このMakefile_vc.makでは $(OBJS0) で指定したオブジェクトファイルはすべて Suffix rule で作られるようになっています。 よってターゲット$(OBJS0)は、このMakefile_vc.makでは明示的には指定されていません。

もしも材料がすべて揃っているなら、その時初めて次の行が参照されます。 このとき nmake は次のアルゴリズムで処理を行います。
  • 次の行がタブ文字で始っている行である場合

  • nmakeはその行に書かれているものを(行頭のタブ文字を除き)コマンドプロンプト上でのコマンド記述(バッチファイルの記述)とみなし、 これを実行します(シェル(コマンド)が起動/実行されるなどといった言い方をする場合もあります)。

    ただし厳密にはバッチファイルの記述と一致しない部分もあります。

    その実行が終わったならnmakeはさらに次の行を参照し、同様の判定を行い、以下これを繰り返していきます。

  • 次の行がタブ文字で始っている行でない場合

  • nmakeはその行が来た時点でシェルの起動/実行をやめ、Makefileに制御を戻します。
よって $(EXE_FILE0) を作成するために複数行のコマンドを実行させたい場合は、行頭がタブ文字となるようにコマンドを書き、それを連続させていけばよいことになります。 そしてタブで始まらない行(典型的には空行など)が来た時点でそれは終了となります。 今回のケースでは、「タブ文字$(LINKER) /OUT:$(EXE_FILE0) $(OBJS0) $(LIBS0)」という1行が来て、その後に空行が来ていますから、 この1行がシェル(cmd.exe)によって解釈され実行されるわけです。

ターゲットを含む行、およびこの行頭がタブ文字である一連の行の連続を合わせてルールと呼びます。

ちなみにここでは $(LINKER) の値は、「LINK」(つまりlinkコマンドですが)とさらにそれに関するいくつかのオプション文字列が付いた値になっています。 すなわち、入力ファイルを $(OBJS0) と $(LIBS0)、出力ファイルを$(EXE_FILE0)として、linkコマンドを実行しています。

ターゲットは実存するファイルでなくても構わない場合もあります。 そのようなターゲットはMakefile内におけるいわばサブルーチンのように働きます(そのようなファイルが存在しないため、常に実行されると考えてもよいです)。 例えば今回のMakefile_vc.makですと、all、all_at_first、clean はいずれもこの種のターゲットになります。

ここでターゲットの依存関係を木構造とみなした場合、そのルートに位置するものは何かという疑問が生じます。 換言すれば依存関係のもっとも起点となるターゲットですが、それはどれになるのでしょうか?

単に「nmake -f Makefile_vc.mak」と実行させた場合は、Makefile内で一番最初に記述されているターゲットがそのような起点とみなされます。 今回のMakefile_vc.makですと ターゲットallが一番最初に記述されていますので、これがデフォルトの起点となります。

起点となるターゲットはコマンドラインから明示的に指定することもできます。 例えばターゲットcleanを明示的にルートとしたい場合は「nmake -f Makefile_vc.mak clean」といった形になります。 これによりcleanとその材料となるすべての子ターゲットが実行されることになります。 またこの場合はデフォルトの起点(今回の場合ですとall)の方は実行されません。

次に my_proj\util 内のMakefile_vc.makを実行させてみます。 その様子を以下に示します。

C:\Zenkaku\my_proj\app1>cd ..\util
C:\Zenkaku\my_proj\util>
C:\Zenkaku\my_proj\util>nmake -f Makefile_vc.mak

detect_vc.batは既に実行済みのため、省略できます。

my_proj\app1\Makefile_vc.mak と比べて大差ないため、際立って異なる部分だけを以下に抜粋します。 全体を見たい方は「全ソースコード」をクリックしてください。

INCLUDE_FLAG =  \

SLIB_FILE0=$O\util.lib
OBJS0=\
	$O\util1.obj \
	$O\util2.obj \

LIBS0=\

# Product files rule.
$(SLIB_FILE0): $(OBJS0)
	LIB /NOLOGO /OUT:$(SLIB_FILE0) $(OBJS0) $(SUB_LIBS)
util\Makefile_vc.makの全ソースコード (77 lines)

# Source directory
S = .

!IF "$(MACHINE)" == "x64"
VC_MACHINE=AMD64
PLATFORM=win64
!ELSE
MACHINE=x86
VC_MACHINE=X86
PLATFORM=win32
!ENDIF
# Output directory
ABINAME=vc$(MACHINE)$(DEBUG)
O = .\out_dir\$(ABINAME)

#
# In VS2012(VC11) or later, if you want to generate XP-compatible EXE/DLL, you need to specify the subsystem-version (5.01/5.02)
# explicitly in LINK /SUBSYSTEM: option. For example, LINK /SUBSYSTEM:WINDOWS,5.01 etc.
# This is because the subsystem-version is set to 6.00 (target OS is Vista or later) by default in VC11 or later.
#
# Note that the version specified in the /SUBSYSTEM option of LINK has a different minimum value for each compiler.
# For VC2005 and earlier, the minimum value is 4.00 (target OS is Windows95 or later).
# For VS2008 (VC9) and VS2010 (VC10), the minimum value is 5.00 (target OS is Windows2000 or later).
# For VC2012 or later, the minimum value is 5.01/5.02 (target OS is WindowsXP x86 / Windows XP x64, Windows Server 2003 or later).
#
# If you specify a value that is less than the minimum, LINK will output an error LNK4010,
# so you can automatically get the minimum version value that should be specified by detecting that string.
# The detect_vc.bat (by Zenkaku@znk_project) will automatically retrieve it and set the result to the environment variable DET_VC_SUBSYS_VER.
# 
LINKER_OPT_SUBSYSTEM=/SUBSYSTEM:CONSOLE,$(DET_VC_SUBSYS_VER)

!IF "$(DEBUG)" == "d"
COMPILER=cl -nologo -EHsc /GR /W4 /MTd /Z7 /D_DEBUG /RTC1 
LINKER=link /MACHINE:$(VC_MACHINE) $(LINKER_OPT_SUBSYSTEM) /DEBUG /INCREMENTAL:NO 
!ELSE
COMPILER=cl -nologo -EHsc /GR /W4 /MT /O2 /DNDEBUG 
LINKER=link /MACHINE:$(VC_MACHINE) $(LINKER_OPT_SUBSYSTEM)  
!ENDIF


INCLUDE_FLAG =  \

SLIB_FILE0=$O\util.lib
OBJS0=\
	$O\util1.obj \
	$O\util2.obj \

LIBS0=\


# Entry rule.
all: all_at_first $O $(SLIB_FILE0)

# Init.
all_at_first:

# Mkdir rule.
$O:
	if not exist $O mkdir $O


# Product files rule.
$(SLIB_FILE0): $(OBJS0)
	LIB /NOLOGO /OUT:$(SLIB_FILE0) $(OBJS0) $(SUB_LIBS)


# Suffix rule.
{$S}.c{$O}.obj:
	$(COMPILER) -I$S $(INCLUDE_FLAG) -Fo$@ -c $<

{$S}.cpp{$O}.obj:
	$(COMPILER) -I$S $(INCLUDE_FLAG) -Fo$@ -c $<


# Clean rule.
clean:
	rmdir /S /Q $O\ 
Close


OBJS0 で指定しているオブジェクトファイルも勿論違いますが、今回一番大きな違いは EXE_FILE0 ではなく SLIB_FILE0 の値が指定されているということです。 その値は「$O\util.lib」となり、今回作りたいスタティックライブラリの出力パス名となっています。

今回はターゲット$(EXE_FILE0) はなく、替わりにターゲット$(SLIB_FILE0)に関するルールが記述されています。 このルールにより実際に起動するシェルコマンドは「LIB /NOLOGO /OUT:$(SLIB_FILE0) $(OBJS0) $(SUB_LIBS)」となります。 即ち、libコマンド(ライブラリアン)を実行して、$(OBJS0)を元にライブラリ$(SLIB_FILE0)を作る処理を行います。 (一番最後に $(SUB_LIBS) とありますが、今回はこの値は空になっています)。

次に my_proj\app2 内のMakefile_vc.makを実行させてみます。 その様子を以下に示します。

C:\Zenkaku\my_proj\util>cd ..\app2
C:\Zenkaku\my_proj\app2>
C:\Zenkaku\my_proj\app2>nmake -f Makefile_vc.mak

detect_vc.batは既に実行済みのため、省略できます。

my_proj\app1\Makefile_vc.mak と比べて大差ないため、際立って異なる部分だけを以下に抜粋します。 全体を見たい方は「全ソースコード」をクリックしてください。

INCLUDE_FLAG =  \
	-I..\util \

SLIB_FILE0=$O\util.lib
OBJS0=\
	$O\app2.obj \
	$O\main.obj \

LIBS0=\
	..\util\$O\util.lib \

# Product files rule.
$(EXE_FILE0): $(OBJS0) 
	$(LINKER) /OUT:$(EXE_FILE0)  $(OBJS0) $(LIBS0) 

app2\Makefile_vc.makの全ソースコード (79 lines)

# Source directory
S = .

!IF "$(MACHINE)" == "x64"
VC_MACHINE=AMD64
PLATFORM=win64
!ELSE
MACHINE=x86
VC_MACHINE=X86
PLATFORM=win32
!ENDIF
# Output directory
ABINAME=vc$(MACHINE)$(DEBUG)
O = .\out_dir\$(ABINAME)

#
# In VS2012(VC11) or later, if you want to generate XP-compatible EXE/DLL, you need to specify the subsystem-version (5.01/5.02)
# explicitly in LINK /SUBSYSTEM: option. For example, LINK /SUBSYSTEM:WINDOWS,5.01 etc.
# This is because the subsystem-version is set to 6.00 (target OS is Vista or later) by default in VC11 or later.
#
# Note that the version specified in the /SUBSYSTEM option of LINK has a different minimum value for each compiler.
# For VC2005 and earlier, the minimum value is 4.00 (target OS is Windows95 or later).
# For VS2008 (VC9) and VS2010 (VC10), the minimum value is 5.00 (target OS is Windows2000 or later).
# For VC2012 or later, the minimum value is 5.01/5.02 (target OS is WindowsXP x86 / Windows XP x64, Windows Server 2003 or later).
#
# If you specify a value that is less than the minimum, LINK will output an error LNK4010,
# so you can automatically get the minimum version value that should be specified by detecting that string.
# The detect_vc.bat (by Zenkaku@znk_project) will automatically retrieve it and set the result to the environment variable DET_VC_SUBSYS_VER.
# 
LINKER_OPT_SUBSYSTEM=/SUBSYSTEM:CONSOLE,$(DET_VC_SUBSYS_VER)

!IF "$(DEBUG)" == "d"
COMPILER=cl -nologo -EHsc /GR /W4 /MTd /Z7 /D_DEBUG /RTC1 
LINKER=link /MACHINE:$(VC_MACHINE) $(LINKER_OPT_SUBSYSTEM) /DEBUG /INCREMENTAL:NO 
!ELSE
COMPILER=cl -nologo -EHsc /GR /W4 /MT /O2 /DNDEBUG 
LINKER=link /MACHINE:$(VC_MACHINE) $(LINKER_OPT_SUBSYSTEM)  
!ENDIF


INCLUDE_FLAG =  \
	-I..\util \

EXE_FILE0=$O\app2.exe
OBJS0=\
	$O\app2.obj \
	$O\main.obj \

LIBS0=\
	..\util\$O\util.lib \


# Entry rule.
all: all_at_first $O $(EXE_FILE0)

# Init.
all_at_first:

# Mkdir rule.
$O:
	if not exist $O mkdir $O


# Product files rule.
$(EXE_FILE0): $(OBJS0) 
	$(LINKER) /OUT:$(EXE_FILE0)  $(OBJS0) $(LIBS0) 


# Suffix rule.
{$S}.c{$O}.obj:
	$(COMPILER) -I$S $(INCLUDE_FLAG) -Fo$@ -c $<

{$S}.cpp{$O}.obj:
	$(COMPILER) -I$S $(INCLUDE_FLAG) -Fo$@ -c $<


# Clean rule.
clean:
	rmdir /S /Q $O\ 
Close


今回一番大きな違いは app2 は util.lib を使っているため、そのためのインクルードパスとutil.lib本体が置かれた場所を指定しているということです。

INCLUDE_FLAG においては「-I..\util」という値を指定し、コンパイラ(cl.exe)のオプションとしてこれを与えるようにしています。 これにより「..\util」内もインクルードの検索対象ディレクトリとみなれるようになり、main.c 内で単に「#include <util.h>」と記述するだけで、 これ(実体は ..\util\util.h に存在)がインクルードできるようになるわけです。

LIBS0 においては「..\util\$O\util.lib」という値を指定し、リンカ(link.exe)の最終引数としてこれを与えるようにしています。 今回は main.obj 内で util.lib 内に定義された関数を呼び出して使用しておりますが、link.exe は util.lib を参照しつつ、 最終的にそのutil.lib内の関数の実体を app2.exe にくっつけなければいけません。 この作業をスタティックリンクと呼びます。

以上で app1.exe、util.lib、app2.exe を Makefileにより生成することができました。

ところで nmake -f Makefile.mak をもう一度実行しても何も起こりません。 これは大元となるCソースファイルがまだ何も変更されていないからです。 何も変更されていないCソースファイルを再度コンパイル、リンクしても同じものしか生成されず無駄であるため、 nmake はその処理をスキップします。

nmake は Cソースファイルの最終更新時間を参照して、その時間がそれに対応するオブジェクトファイルよりも新しいものに限り、 再度オブジェクトファイルを作成しなおします。

ただし生成対象のオブジェクトファイルやexeファイルが削除されていた場合はそれを生成します。

このように修正したCソースファイルだけをコンパイルしてくれるため無駄がないわけで、特にCソースファイルの数が多い場合に、 コンパイル時間の短縮となります(ここが全部バッチファイルでコンパイルする場合と比べてMakefileの有利なポイントの一つです)。

ただ時に、一旦生成されたものを全部削除して一から全部作り直したいという場合もあります。 生成されたものをMakefileを使って削除(クリーン)する方法についてここで補足説明しておきましょう。 別にMakefileを使わなくても、今回の場合は out_dir ディレクトリを丸ごと削除すればそれで終わりなわけですが、 次のように実行しても実質同じことができます(今 app2 ディレクトリにおり、その直下にある out_dir の中身を削除したいとします)。

C:\Zenkaku\my_proj\app2>nmake -f Makefile_vc.mak clean


今回は nmake のコマンドライン引数として -f オプションに加え、clean も与えています。 このとき、Makefile_vc.mak 内のターゲットcleanに関するルールが適用され、そこに記述されたシェルコマンド「rmdir /S /Q $O\」が実行されます。

# Clean rule.
clean:
	rmdir /S /Q $O\ 



シェルコマンド「rmdir /S /Q」は、引数で指定したディレクトリを確認メッセージなしで削除するというものです。 今回は引数として $O を与えていますが、この値は out_dir\vcx86 のような値ですからそのディレクトリを(中身も含め)削除しています (その一つ上、すなわちout_dirディレクトリそのものは残ります)。

make world


Makefile_vc.mak は各ソースディレクトリ毎に用意されてあります。 すなわち、my_proj\app1\Makefile_vc.mak、my_proj\util\Makefile_vc.mak、my_proj\app2\Makefile_vc.mak の3つです。 これらについて、それぞれ nmake -f Makefile_vc.mak などと指定して実行していくわけですが (3つくらいならそれぞれ手動で実行していけばいいのですが)、これも数が増えると面倒になってきます。

そこでこれらをさらに一括で実行する方法について考えましょう。 my_projディレクトリ直下にある make_world.bat というバッチファイルを使います。 このバッチファイルは、配下にあるすべてのMakefile_vc.makに対し「nmake -f Makefile_vc.mak」を実行するものです。 単に make_world.bat をダブルクリックするだけですべてが完了するので楽です。

今は昔(いや今もかもしれまんが)、X-Window System(ゴメンなさい、UNIXの話です)に含まれていたMakefileでは、 make world というコマンドですべてがコンパイル、リンクできました。 make_world.bat という名前はそこから拝借しております。

それでは make_world.bat の中身を見てみます。


@echo off

REM x86 or x64.
REM x86 : Build x86 binary (for 32bit/64bit Windows)
REM x64 : Build x64 binary (for 64bit Windows only)
set MACHINE=x86

REM Define ZNK_VC_DIR, ZNK_VC_WINSDK_DIR, ZNK_VC_UCRT_DIR
if exist "detect_vc_config.bat" call detect_vc_config.bat

REM Define PATH, INCLUDE, LIB and DET_VC_SUBSYS_VER for VC, target binary %MACHINE%.
call detect_vc.bat
if "%DET_VC_SUBSYS_VER%" == "" goto Error_DetectVC


set MAK_TGT=
call make_one.bat app1 %MAK_TGT%& if "%IS_ERROR%" == "yes" goto Error_Make
call make_one.bat util %MAK_TGT%& if "%IS_ERROR%" == "yes" goto Error_Make
call make_one.bat app2 %MAK_TGT%& if "%IS_ERROR%" == "yes" goto Error_Make


echo [OK]: The make_world is done successfully.
goto End

:Error_DetectVC
echo [NG]: Failed to execute DetectVC.
goto End

:Error_Make
echo [NG]: Failed to execute nmake.
goto End

:End
pause




前半はいつも通りdetect_vc.batでセッティングする部分です。 中ほどにある以下の記述は call コマンドで make_one.bat を呼び出しています。

set MAK_TGT=
call make_one.bat app1 %MAK_TGT%& if "%IS_ERROR%" == "yes" goto Error_Make
call make_one.bat util %MAK_TGT%& if "%IS_ERROR%" == "yes" goto Error_Make
call make_one.bat app2 %MAK_TGT%& if "%IS_ERROR%" == "yes" goto Error_Make

今回はこのmake_one.batがサブルーチンの役割をはたしています。 以下では少し厳密に、呼ぶ側の引数を「引数(arguments)」、呼ばれる側の引数を「引数(parameters)」と記述します。

例えば最初の call ですと、引数(arguments)として app1、%MAK_TGT% の2つが指定されています。 ただし今回はMAK_TGTの値は空ですので、実質指定されている引数(arguments)はapp1のみとなります。 2番目のutil、3番目のapp2も同様です。 callの後ろは「&」で連結して、if文とgoto文のペアを記述しています。 make_one.batが実行された結果、IS_ERRORにyesがセットされていた場合は、多くの場合コンパイルかリンクに失敗した場合ですから、 その時点でError_Makeへ飛び、すべての処理を中断するようにします。

ここで中断せず残りの実行をさせてしまうと、コンパイルまたはリンカエラーの表示が上へスクロールして流れ、 これらを見つけるのが大変になってしまいます。

次に call で呼ばれた make_one.bat の中身を見てみます。


@echo off
set DST_DIR=%1
set MAK_TGT=%2

set SAFE_CD=cd /d

echo ======
echo === Entering [%DST_DIR%] ===
	set BAK_DIR=%CD%
	%SAFE_CD% %DST_DIR%

	REM REM Only when the one before && succeeds, the one after && will be executed.
	set IS_ERROR=yes
	nmake -f Makefile_vc.mak %MAK_TGT%&& set IS_ERROR=

	%SAFE_CD% %BAK_DIR%
echo === Leaving [%DST_DIR%] ===
echo ======
echo:



まず呼び出し元が与えた引数(arguments)を取得します。 バッチファイルでは、第1引数(parameters)は%1、第2引数(parameters)は%2と記述します(もっと一般化してNで書くと、第N引数(parameters)は%Nです)。 これらの記述で呼び出し元が与えた引数(arguments)が取得できます。

%1や%2などといった記述をコード内であまり使い散らすべきではありませんので、これらを一旦わかりやすい名前に格納しておきます。 今回の場合、%1は移動先のディレクトリDST_DIR、%2は nmake コマンドに引数として与えるターゲットMAK_TGT(これについては後述します)になります。 いずれにしても今回はMAK_TGTは空です。

このバッチファイルではディレクトリを移動するため、cd /d コマンドを使います(バッチファイルでは cd ではなく常に cd /d を使うべきです)。 そのことを目立たせるため、ここでは一旦これをSAFE_CDという変数にいれ、これを使うことにしています。

特殊環境変数 %CD% の値を参照して現在いるディレクトリ(これをカレントディレクトリといいますが) のパスを BAK_DIR という変数にセーブしておきます。これは後で元のディレクトリに戻るときに使います。 その上で %DST_DIR%ディレクトリへ移動します。

特殊環境変数 %CD% は、常にカレントディレクトリの値が格納されています。 つまり cd コマンド等でディレクトリを移動すると、この値も自動的に変わります。

今回はBAK_DIRという変数にこの値をセーブする方法をとりましたが、 setlocal endlocalというコマンドを使って元のディレクトリに戻る手法もあります。 ただこの手法は「3分(かそこら)で速習できるバッチファイル講座」のレベルを超えますので、今回は使いません。

一旦 IS_ERRORの値をyesにセットしてから「nmake -f Makefile_vc.make %MAK_TGT%」を実行します。 nmakeが成功した場合、&&で連結された後ろの文、すなわち「set IS_ERROR=」が実行され、IS_ERRORの値は空となります。 nmakeが失敗した場合、&&で連結された後ろの文は実行されません。すなわちIS_ERRORの値はyesであり続けます。 これによりIS_ERROR の値が yes ならば nmake でエラーが発生したと判断できます。

多くの場合、単にコマンドを連結する用途では「&」を使いますが、今回の例は、「&&」を使うべき例です。 「&&」は、直前のコマンドが成功したときのみ「&&」の直後のコマンドが実行されます。 ここではその仕様を使っています。 このあたりはC言語やBourne shellの演算子「&&」と同じですね。

ちなみにバッチファイルでも「||」演算子が用意されており、これを使うと上記は一応次のようにも書けます。

REM Only when the one before || fails, the one after || will be executed.
set IS_ERROR=
nmake -f Makefile_vc.mak %MAK_TGT%|| set IS_ERROR=yes

この場合は逆に「||」の直前のコマンドが失敗したときのみ、「||」の直後のコマンドが実行されます。 つまりnmakeが失敗した場合のみ、IS_ERROR=yesがセットされるという流れですね。 こちらもまたC言語やBourne shellの演算子「||」でよくやる流れと同じで、こちらの方が自然と感じる方もいるかもしれません。
「&&」や「||」を用いることによって直前のコマンドの成否が判定できました。 実はこの他にも、特殊な環境変数errorlevelを使うことにより、これを判定することができます(ただしどうしても必要な場合を除きあまりお勧めはしません)。 一応これについても補足で説明します。

これはifコマンドとともに次のように使います。

nmake -f Makefile_vc.mak
if "%errorlevel%" == "1" goto Error

ifコマンドの「"%errorlevel%" == "1"」という条件は、直前に実行したコマンド(今回の場合は「nmake -f Makefile_vc.make」)が失敗した場合に true となります。 因みに「"%errorlevel%" == "0"」ならば成功した場合に true となります。

これだけでも若干ややこしいですが、少し一般化して説明しますと、環境変数errorlevel の値は、 直前に実行したコマンドの main 関数において「return n;」が実行されたときの n の値であると考えて頂いて結構です。 nmakeなどのコマンドが成功した場合は通常 0 が、失敗した場合は通常 1 が返されます(慣習となっております)ので このような成否の判定が可能なわけです。

「if "%errorlevel%" == "n"」 の他にも「if errorlevel n」といった記法もありますが、後者は「nに等しいとき」ではなく「n以上のとき」という意味になります。 間違いの元ですので後者の記法は避けるべきです。

環境変数errorlevelを使う上で一点注意して頂きたいのは、ここで言う「直前」とは「本当に直前」ということです。 例えば、以下のような記述の場合、errorlevel は「nmake -f Makefile_vc.mak」ではなく「echo ==== End of Makefile_vc.make ====」 の戻り値が格納されます。

echo ==== Begin of %DST_DIR%\Makefile_vc.make ====
nmake -f Makefile_vc.mak
echo ==== End of %DST_DIR%\Makefile_vc.make ====
if "%errorlevel%" == "1" goto Error

仮にnmakeコマンドの実行が失敗しても、このechoコマンド自体は当然成功するため、「goto Error」も実行されないわけです。 echoだとなんとなく心理的に油断してしまうせいかはわかりませんが、意外とやってしまいがちなミスです(主に私が)。

my_projディレクトリ直下にある make_world_clean.bat というバッチファイルについても make_world.batと全く同様で、 「nmake -f Makefile_vc.mak clean」をすべて一括で実行するものです。 中ほどにある記述のみ以下に抜粋します。 make_world.bat との違いは「MAK_TGT=clean」と指定している箇所のみです。

set MAK_TGT=clean
call make_one.bat app1 %MAK_TGT%& if "%IS_ERROR%" == "yes" goto Error_Make
call make_one.bat util %MAK_TGT%& if "%IS_ERROR%" == "yes" goto Error_Make
call make_one.bat app2 %MAK_TGT%& if "%IS_ERROR%" == "yes" goto Error_Make
make_world_clean.batの全ソースコード (35 lines)

@echo off

REM x86 or x64.
REM x86 : Build x86 binary (for 32bit/64bit Windows)
REM x64 : Build x64 binary (for 64bit Windows only)
set MACHINE=x86

REM Define ZNK_VC_DIR, ZNK_VC_WINSDK_DIR, ZNK_VC_UCRT_DIR
if exist "detect_vc_config.bat" call detect_vc_config.bat

REM Define PATH, INCLUDE, LIB and DET_VC_SUBSYS_VER for VC, target binary %MACHINE%.
call detect_vc.bat
if "%DET_VC_SUBSYS_VER%" == "" goto Error_DetectVC


set MAK_TGT=clean
call make_one.bat app1 %MAK_TGT%& if "%IS_ERROR%" == "yes" goto Error_Make
call make_one.bat util %MAK_TGT%& if "%IS_ERROR%" == "yes" goto Error_Make
call make_one.bat app2 %MAK_TGT%& if "%IS_ERROR%" == "yes" goto Error_Make


echo [OK]: The make_world is done successfully.
goto End

:Error_DetectVC
echo [NG]: Failed to execute DetectVC.
goto End

:Error_Make
echo [NG]: Failed to execute nmake.
goto End

:End
pause

Close



さらにその先は?


ここまでの例でも detect_vc.bat と Makefile の使用の基本的なイメージはつかんで頂けたかと思います。 さらに進んだ手法(つまり高レイヤーの話)については、それより低レイヤーのツールである detect_vc.bat とはどんどん関係ない話になっていくため このあたりでもう切り上げますが、以下に少しだけ紹介しておきます。

Makefileは確かに強力なツールなのですが、プロジェクトが大規模になってくるとそもそもMakefile自体のメンテナンスが面倒になってきます。 そのため、多くのオープンソースプロジェクトではこのMakefile自体をさらにツールで自動生成しています。 Moaiもこのタイプですが、mkfgen と auto_trigger という我々が独自に開発したツールでこれを行っています。

あるいはMakefile以外のその他の代替ツールを使っているものもあります。 例えば Android アプリを作るための Android SDK で開発する場合、gradle というJavaベースのツールを使う形になります。 gradle は 単なるビルドに加え、それに必要なライブラリを依存関係に応じて自動でダウンロードする機能を備えています。 では Android アプリの開発では Makefile を全く使わないかといえばそうでもなく、 例えば Android NDK(SDKではなく)での開発ではMakefileを使う形になります。

ツールが高機能になるにつれツールそれ自体のシステムが複雑化します。 万一トラブルが起きた際、その複雑さに比例して原因の分析や対処がしにくくなることもまた事実です。 今作成しているプロジェクトの規模に応じて、この種のツールに関して必要とされる機能のレベルもまた異なります。 そのあたりのバランスをきちんと見極めることが重要です。

おまけ: detect_vc.batの解析


このdetect_vc.batでどのようにして処理を行ってるかを知りたい勉強意欲旺盛な方は detect_vc.batの中身を直接ご覧頂くか、 あるいはもう少しやさしめのサンプルとして以下にあります「3分(かそこら)では速習できないバッチファイル講座: 上級編」の最終課題などをご覧頂くとよろしいかと思います。

3分(かそこら)では速習できないバッチファイル講座: 上級編
上級編の概要

この講座のシリーズその2までの知識があれば、この記事の内容を理解するには十分なのですが、 さらに極めたい(一部の変態)の方のために最後にこのスペシャルハードコースを用意しました。 一般人の方はこの講座はスルーして先へお進みください

この講座では以下のような項目を扱います。
  • callコマンドを使った擬似サブルーチン
  • setlocal、endlocal コマンドと環境変数のエクスポート
  • コマンドの実行結果を環境変数に設定する方法
  • 最終課題:VCの環境変数を自動設定するバッチファイルを自作する

尚、今回に限ってはさすがに3分では無理かと思います。

callコマンドを使った擬似サブルーチン
callコマンドを使った擬似サブルーチン

少し規模の大きい複雑なバッチファイルを作ろうとした場合、やはり関数が欲しくなってきます。

残念ながらバッチファイルでは関数またはサブルーチンというものが明確には用意されておりません。 ここまでの知識でもcallコマンドで他のバッチファイルを呼ぶことで代替できるといえばできますが、 ちょっとした処理を実行するような関数をいくつか作りたい場合、それらの個数分だけバッチファイルを用意するのは面倒です。

バッチファイルではこのような関数の替わりになるものとしてcallコマンド、ラベル、exit /bを使うことができます。 ラベルにサブルーチンとしての名前を付け、その下にサブルーチンの実体を記述します。 サブルーチンの実体の終わりには exit /b を記述します。

一方これを呼ぶ場合、「call :ラベル名」という書式で実行します。このときラベルへジャンプしサブルーチンの実体へ制御が移ります。 callの場合、引数となるラベル名の前にも「:」が必要なことに注意してください

gotoコマンドの引数としてラベルを指定する場合には「:」は必要ありませんでしたが、call文の引数の場合は必要となります。 おそらくcallコマンドでは引数としてバッチファイルのパスを指定することもできるため、それと区別するためにこのような仕様にしたのでしょう。

しかしこれではラベルへジャンプするgotoコマンドと大差ないようにも見えますが、サブルーチン部分の処理にあるexit /bが実行されると呼び出し元に戻ってきてくれることが一番の違いです。 大まかな構造は以下のようになります。

@echo off
REM Call pseudo-subroutine MyFunc
call :MyFunc

REM This goto statement is necessary to go through the part that defines the subroutine.
goto End

:MyFunc
	REM Contents of subroutine MyFunc.
	echo This is MyFunc internal.
	REM Return to caller.
	exit /b

:End
pause

この例ですと、exit /b が実行されると、上の方にある call :MyFunc の次の行に制御が戻ります。 exitを使うとコマンドプロンプト全体が終了してしまうんじゃ?と思われるかもしれません。 確かにただの exit ならばそうなりますが、ここで実行しているのはただの exit ではなく exit /b です。 callコマンドを実行した場合、その中では exit /b は「callの呼び出し元に戻る」という特別な働きをします。 つまりバッチファイルにおける exit /b とは、C言語で言えばexit関数よりむしろreturn文に近いものです。

サブルーチンの実体は、callコマンドの呼び出しより前にあっても後ろにあっても構いませんが、これの開始は所詮ラベルに過ぎないため、 サブルーチンの実体部分にはcallコマンド以外では制御が至らないように、うまくgoto 文でこれをスルーする必要があります (この例ですと goto Endの部分でファイルの最終部分までジャンプしています。あるいはこの部分に「exit」を記述してもよいでしょう)。

Close


setlocal、endlocal コマンドと環境変数のエクスポート
setlocal、endlocal コマンドと環境変数のエクスポート

バッチファイルの規模が大きくなるにつれ、多くの環境変数が定義されることになりますが、そうなると名前衝突の懸念が出てきそうです。 一時的な変数の有効範囲はできるだけ狭めておきたいですね。setlocalとendlocalはこのような場合に使います。

setlocalを実行するとそれまでの環境変数の全リスト(すなわち「set」コマンドで表示される変数全体ですが)を一つのカタマリとして それをスタックへプッシュします。 逆にendlocalを実行するとsetlocalにおいてスタックにプッシュしたそのカタマリをポップし、環境変数の全リストを元通りに復元します。 これによってsetlocalとendlocalで囲まれた部分の処理における環境変数の変更すべてがなかったことになり、 setlocalが呼ばれる直前の状態に戻ります。 このようにsetlocalとendlocalはかならずペアで使うものと考えてよいでしょう。

この様子を確認するためのコードを以下に示します。

@echo off

set  OUT_SIDER=RightValue
echo OUT_SIDER(1)=[%OUT_SIDER%]

setlocal
	echo OUT_SIDER(2)=[%OUT_SIDER%]
	set  OUT_SIDER=TempValue
	echo OUT_SIDER(3)=[%OUT_SIDER%]

	set  IN_SIDER=TempValue
	echo IN_SIDER(1)=[%IN_SIDER%]
endlocal

echo OUT_SIDER(4)=[%OUT_SIDER%]
echo IN_SIDER(2)=[%IN_SIDER%]
pause

またこれの出力結果を以下に示します。

OUT_SIDER(1)=[RightValue]
OUT_SIDER(2)=[RightValue]
OUT_SIDER(3)=[TempValue]
IN_SIDER(1)=[TempValue]
OUT_SIDER(4)=[RightValue]
IN_SIDER(2)=[]

最初、環境変数TESTは RightValue という値であり、本来この値を維持し続けなければならない状況であるとします。 ところが setlocal endlocalで囲まれた部分で、うっかりTESTへの代入が発生し、TempValue という値に書き換えてしまったという状況です。 しかし endlocal を抜けるとこの値はまた RightValueに復元されていることがわかります。

このように setlocal endlocalで囲っておけば、その中で何をどのように書き変えようと とにかくendlocalのタイミングで元に戻るわけですから、 プログラマにとっては名前衝突が起きないように配慮する労力がかなり軽減されるわけです (ここでは例は示しませんが、スタックが使われていますので setlocal setlocal endlocal endlocal の順でネストさせることもできます)。

またこのことは裏を返せば setlocal endlocalで囲まれた内部で設定した環境変数は、endlocalで外に出た瞬間使えなくなるということでもあります。 上の例で、setlocal endlocal内で設定したIN_SIDERの値がendlocalを抜けた後では空値になっていることがわかります。

例えば setlocal endlocal 内で 環境変数INCLUDEやLIBを取得するような処理を書き、 endlocalで外に出た後もこの二つの環境変数のみ、そのままの値で使い続けたいというようなこともあります。 これをこの記事では環境変数のエクスポートと呼ぶことにしますが、以下のように endlocal の後ろに「&」を付けてsetコマンドでその環境変数を再設定するようにすればこれが可能になります。

endlocal& set MY_INCLUDE=%MY_INCLUDE%& set MY_LIB=%MY_LIB%

注意1

%MY_INCLUDE%と&の間にはスペースを入れてはいけないことに注意してください。 ここにスペースを入れると、setコマンド実行時にそのスペースが余計な文字となって最後尾に加わってしまいます。 紛らわしいので私は各コマンドの最後と&の間にはスペースを入れないというルールにしています。 endlocalの直後にもスペースがないのはそういう理由で敢えてそうしています。

注意2

この環境変数のエクスポートではコマンドの連結として必ず「&」を使うべきです。 「&&」を使うべきではありません。 エクスポートする値が空値の場合、「&&」ですとそこで処理が止まってしまうためです。

全体像を見た方がよりわかりやすいと思いますので、以下の例もご覧ください。

@echo off

set MY_INCLUDE=
set MY_LIB=

setlocal
	set  MY_INCLUDE=C:\Compiler\include
	set      MY_LIB=C:\Compiler\lib

REM export MY_INCLUDE, MY_LIB
endlocal& set MY_INCLUDE=%MY_INCLUDE%& set MY_LIB=%MY_LIB%

echo MY_INCLUDE=[%MY_INCLUDE%]
echo MY_LIB    =[%MY_LIB%]
pause

またこれの出力結果を以下に示します。

MY_INCLUDE=[C:\Compiler\include]
MY_LIB    =[C:\Compiler\lib]

setlocal endlocalで囲まれた内部で設定した値が、これを抜けた後でも生きていることがわかります。

参考: なぜこの記述でうまくエクスポートできるのか?
この記述では実際には3つのコマンドが「&」で結ばれて同時に実行されています。 1つ目がendlocalコマンド、2つ目がset MY_INCLUDE=%MY_INCLUDE%コマンド、3つ目がset MY_LIB=%MY_LIB%コマンドです。 かなりトリッキーなことが起きていますので解説します。

まずバッチファイルのインタプリタ(cmd.exe)は、この行に記述された3つのコマンドを実行する前に、先に%MY_INCLUDE%と%MY_LIB%を展開します。 まだ1つ目のendlocalコマンドも実行されていませんから、このとき展開される値はsetlocal endlocalで囲まれた内部で設定された値となります。 つまり展開後の文字列は、cmd.exe の内部バッファに以下のように一時保存されます。

endlocal& set MY_INCLUDE=C:\Compiler\include& set MY_LIB=C:\Compiler\lib

cmd.exe はこの内部バッファを参照しながら、ようやく最初のコマンドであるendlocalから実行を始めます。 endlocalを実行いたしますから環境変数MY_INCLUDE、MY_LIBの値は削除されます(setlocal に入る前の値に戻ってしまいます)。

最後に後ろにあるsetコマンド二つが実行されますが、これらは上述したように内部バッファとして以下のようになって残っているはずです。

set MY_INCLUDE=C:\Compiler\include& set MY_LIB=C:\Compiler\lib

よってC:\Compiler\includeとC:\Compiler\libがここで復活するわけで、晴れて環境変数のエクスポートが完成するわけです。

Close


参考: エクスポートする環境変数が多い場合、1行が長くなりすぎる問題
この手法の欠点はエクスポートする環境変数が多い場合、1行が長くなってしまい、どうにも見づらくなってしまうことです。 例えば以下の例のように環境変数4つだけでも結構な長さになり、いかにも書き間違えがおきそうな感じになってしまいます。

endlocal& set DET_VC_SUBSYS_VER=%DET_VC_SUBSYS_VER%& set PATH=%PATH%& set INCLUDE=%INCLUDE%& set LIB=%LIB%

batファイルでは「^」で改行のエスケープができることを知っていると、これを使いたくなってしまうかもしれません。 しかしこれは使用に際して色々と注意点が多く、さらにはsetコマンドとは非常に相性が悪いのです。 例えば以下のようにしても期待通りの動作となりません。

endlocal& ^
  set DET_VC_SUBSYS_VER=%DET_VC_SUBSYS_VER%& ^
  set PATH=%PATH%& ^
  set INCLUDE=%INCLUDE%& ^
  set LIB=%LIB%


このときなぜか set の前後にあるスペースが無視されなくなり、'  set'という(頭にスペースが2つついた)名前のコマンドとして解釈されてしまいます。 そして「そのような名前のコマンドはありません」といったエラーで終了してしまいます。 setの前にスペースやタブ文字などを置かなければよいのですが、それだとsetを絶対に行頭に置かなければならない制限が生じてしまい、 状況によってはかえって見づらくなってしまいます。 やはりインデントを封じられるのは何かと辛いわけです。

かといって、これを避けようと以下のように「&」を次の行の初めに配置するなどすると、setという文字自体は正常に認識されるようになりますが、 今度はsetに指定する値(右辺値)の最後尾に余計な文字が含まれてしまいます。

endlocal ^
&  set DET_VC_SUBSYS_VER=%DET_VC_SUBSYS_VER% ^
&  set PATH=%PATH% ^
&  set INCLUDE=%INCLUDE% ^
&  set LIB=%LIB%


以下のような set コマンドのラッパーとなるサブルーチンを作り、set コマンドを見かけ上隠してしまえば、 一応上で挙げた問題にすべて対応することはできます。

REM Sub-routine Func_Set
REM Arg    : const "%1" KEY
REM Arg    : const "%2" VAL
:Func_Set
setlocal
	set KEY=%1
	set VAL=%2
	REM Strip sides quotes.
	set KEY=%KEY:~1,-1%
	set VAL=%VAL:~1,-1%

:Func_Set_End
endlocal&& set %KEY%=%VAL%
	exit /b
REM endof Sub-routine

これを例えば以下のようにして使えば、各変数名の列を揃えて記述することもでき、set を使う場合と比べれば多少は見やすくはなります。

endlocal ^
	& call :Func_Set "DET_VC_SUBSYS_VER" "%DET_VC_SUBSYS_VER%" ^
	& call :Func_Set "PATH"              "%PATH%"              ^
	& call :Func_Set "INCLUDE"           "%INCLUDE%"           ^
	& call :Func_Set "LIB"               "%LIB%"

とはいえ、今度はダブルクォートを忘れずにつけなければなりませんし、 このサブルーチンをわざわざ用意する手間も増えますので、このような記述はエクスポートする環境変数が本当に多い場合に限定すべきでしょう。

Close


setlocal endlocal に関しては、無ければ無いで何とかなるものではあります。 しかし少し規模の大きいbatファイルを作成する場合に、このコマンドがあればやはり心強いですね。

Close


コマンドの実行結果を環境変数に設定する方法
使用上のご注意

あらかじめお断りしておきますが、ここで紹介するforコマンドに関するテクニックにはかなり邪悪な部分を含みます。 一般にバッチファイル(そのインタプリタの実体はcmd.exe)ではプログラマが誤った記述をした際のエラーを報告する機能が貧弱であり、 その場合の原因分析が大変ですが、このforコマンドについては特にそれが際立っています。 これを使うのはどうしても必要な場合のみにしましょう。

レベル1: for、dir、findstrコマンドを用いて不明な部分を含むパスを取得する

VCディレクトリ配下には redist\x86\Microsoft.VC*.CRT という形式のサブディレクトリが含まれており、*の部分にはバージョン番号が入ります。 このバージョン番号はVCのバージョンによって異なり、例えば redist\x86\Microsoft.VC110.CRT、redist\x86\Microsoft.VC120.CRT、 redist\x86\Microsoft.VC140.CRT といった名前になっています。 この中にcl.exeやlink.exeを起動させるのに必要なdllなどが入っていることがあるため、このディレクトリも環境変数PATHに追加したいわけですが、 このようにバージョン毎にディレクトリ名も異なると、バージョンに依存しないようにこれを記述するには少し工夫が必要となります。

まずredist\x86 ディレクトリ配下にどのようなディレクトリが含まれるのかを取得しなければなりません。 それを行うためには dir /b redist\x86 を実行します。 これにより、redist\x86 ディレクトリ配下に含まれるディレクトリならびにファイル名がすべて改行区切りで列挙されます。

次にその中から、それらしいものだけを抽出し、一つに絞りたいわけです。 それを行うためには findstr /i コマンドを使います。 この引数には正規表現のパターンを与えます。

ここまでの処理を例で示しましょう。

dir /b redist\x86 | findstr /i "Microsoft\.VC.*\.CRT"

二つのコマンドの中間にある「|」文字はパイプと呼ばれ、これより左に書かれたコマンドで実行した出力を、 これより右に書かれたコマンドの入力となるようにデータを転送します。 すなわち dir /b の結果がさらに findstr /i のフィルタ機能により絞られるわけで、 これにより例えば redist\x86\Microsoft.VC110.CRT といったパスだけが出力されると期待できます。 絞っても尚複数ある場合は依然として改行区切りでそれらが列挙される形になりますし、 あるいは一つも存在しない場合は、最終的に何も出力されません。

今、これらの出力結果を別のコマンドの引数に埋め込むことを考えます。 バッチファイルでこれを実現するにはかなりトリッキーですが、for文を組み合わせて使う必要があります。

これについては書式が複雑なので先にコードから示しましょう。 以下の例をご覧ください。

for /f "delims=" %%a in ( 'dir /b "%TRY_DIR%" ^| findstr /i "Microsoft\.VC.*\.CRT"' ) do echo Target=[%%a]

最初に「for /f "delims=" %%a in」と来て、次に上で述べた dir と findstrを使った組み合わせが(わずかに異なる部分はありますが)in ( '…' ) 内に埋め込まれています。 その後、do が来て、後ろに echo コマンドが記述されています。

このかなり複雑なコマンド群では、まず dir /b redist\x86 | findstr /i "Microsoft\.VC.*\.CRT" の部分が最初に実行されます。 よく見るとfor文の内部では「|」が「^|」に変わっておりますが、これはfor文の中で特別な意味を持つ「|」を一旦エスケープするため、 そうする必要があります。

in( '...' ) では「'」というシングルクォートが使われておりますが、これは単になんとなく使ったのではなく意味があります。 シングルクォートで囲まれた部分にコマンドを記述した場合、そのコマンドを実行した出力結果の文字列がダブルクォート「"」 で囲まれて指定されたのと同じとみなされます(ただし /f usebackq オプションを使うとこの挙動はまた変化します)。

この部分だけを実行した場合の出力結果は上でも述べたとおり、改行区切りで列挙された文字列です。 以下ではこれを単に「列挙された文字列」と呼びましょう。 列挙された文字列は「for /f "delims=" %%a」の部分へ一つずつ送られます(今回の場合は改行区切りですので1行ずつですね)。 もっと正確に言えば、このとき送られた文字列は for 文内だけで有効な特殊変数 %%a に格納されます。

このとき同時に do 後方のコマンドも実行されます。 つまり、列挙された文字列からこれらの文字列を一つ取り出しては、do 後方のコマンドを実行。 その繰り返しを列挙された文字列すべてに対して行います (その他のプログラミング言語でforeach文というのをご存知の方は、まさにあの挙動です)。

ここで重要なことは do 後方のコマンドに特殊変数 %%a の値を埋め込めることです。 今回の例ではおそらく列挙された文字列は一つしかないので、特に繰り返し処理をする意味はないのですが、 この「do 後方のコマンドに%%aを埋め込める」という特性のためだけにfor文に協力してもらったというわけです。

では最後に完全なコードを示しましょう。 おそらくもう読めるはずです。

REM The following scan is intended to find, for example, VC_DIR\redist\x86\Microsoft.VC*.CRT (for VS2013/VS2015)
set TRY_DIR=%VC_DIR%\redist\x86
if exist "%TRY_DIR%" for /f "delims=" %%a in ( 'dir /b "%TRY_DIR%" ^| findstr /i "Microsoft\.VC.*\.CRT"' ) do if exist "%TRY_DIR%\%%a\*.dll" set MY_BIN=%MY_BIN%;%TRY_DIR%\%%a

かなり横に長いですがご了承ください。

この処理ではまず TRY_DIR に %VC_DIR%\redist\x86 を格納し、そのパスが存在するか否かを調べています。 存在する場合、次に %TRY_DIR%( =%VC_DIR%\redist\x86 ) 内を dir /b でリストアップし、"Microsoft\.VC.*\.CRT" にマッチするパターンのみを抽出します。 (これにマッチするのはおそらく一つですが)それをfor文の特殊変数 %%a へ格納します。 このとき同時に do の後方にある「if exist "%TRY_DIR%\%%a\*.dll" set MY_BIN=%MY_BIN%;%TRY_DIR%\%%a」も実行されます。 %TRY_DIR%\%%a 内に確かになんらかのdllが存在するならば、環境変数MY_BINに新たなパス %TRY_DIR%\%%a を追加します。 存在しないならば追加しません。

このようにして完全にはわからないディレクトリ名を含むパスを(可能であれば)取得し、それを環境変数へセットすることに成功したわけです。

このテクニックはVisual Studioの他の環境変数の設定、たとえばINCLUDEやLIBにおいても応用できます。 WindowsSDK 10 配下のinclude、libには、10.0.10240.0 のように厳密なバージョン番号が書かれたディレクトリが含まれます。 確かに筆者の環境ではそのような番号でしたが、だからといってこれをそのままハードコーディングしてしまうと、 その環境でしか使えないバッチファイルになってしまいます。 ここで紹介したような正規表現を含むテクニックを使って、こういった厄介な部分をうまくやり過ごすこともできるでしょう (ただし今回のこの講座ではそこまでは実装しません)。

レベル2: for の in ( '...' ) 内で任意のコマンドを動的に実行する

レベル1の例では in ( '...' ) 内で最初に実行したコマンドは「dir /b」と固定のものでしたが、もっと一般にそれ以外のコマンドを自由に指定できないでしょうか? さらに環境変数でそのコマンドを動的に与えたいとします。 ですがそのためには色々と注意事項があり、起こり得る様々な問題を回避するため以下のようにする必要があります。

@echo off
set IN_BEGIN=if x^^=^^=x

set CMD=C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\link.exe
for /f "delims=" %%a in ( '%IN_BEGIN% "%CMD%" ^| findstr /i "Version"' ) do echo [%%a]

この例では「C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\link.exe」を環境変数CMDにセットし、それを実行コマンドとして最初に与えています。 ですがそれより前の位置に%IN_BEGIN%が埋め込まれており、その具体的な値は「if x^^=^^=x」という謎の固定文字列です。 これを必ず in ( '...' ) 内の先頭に付加するようにしてください。 なぜこんなものを指定するのかその理由に興味がある方は、説明は大変長くなりますが以下の「in ( '...' ) の中身に関する分析」をご覧ください。

in ( '...' ) の中身に関する分析
概要

ここではforのin ( '...' ) 構文の中身を書くときの注意点を説明します。

結論から申しますと、この中身で一番先頭と最後尾の両方に「"」文字を持ってくるのは回避すべきです。 例えば「in ( '"My cat" ... "Your dog"' )」という形は回避して書くべきです。 決して中身にダブルクォートを入れてはいけないと言っているわけではなく、例えば「in ( '... "My cat" ... "Your dog"' )」という形などであれば問題ありません。

以下では、この in ( '...' ) 構文内部においてダブルクォートを含む場合、どのようなタイプのエラーが起き得るのかを、色々な例を挙げて紹介します。 また、せっかくですのでそのようなエラーが起きた際、自力でその原因を解析するためのアプローチの仕方なども紹介します。 エラーの原因分析の基本としては、まずは困難は分割することです。 その上で現象に対する仮説を立て、単純化した例で実験し検証することを繰り返して、考えられる原因の範囲を徐々に狭めていきます。

実験1: パスをダブルクォートで括る

今、我々の実行したいコマンドのパスが「C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\link.exe」であるとします (VS2005をインストールした場合はデフォルトではこの位置にlink.exeが存在します)。

今回の実験では、あらかじめ環境変数PATHなどをセッティングしてこのlink.exeを単独で実行できるような状態にしておきます。 つまりlink.exeがダイナミックリンクするdllのある位置を環境変数PATHに加えておきます。 もっともそれなら場合によっては単に「link」と指定するだけで実行できるかもしれませんが、 今回はパスに関する実験が目的であるためlink.exeをフルパスで指定します。

以下のように記述したバッチファイルを適当な名前で作って(今仮に bill_gates.bat という名前にしときしましょう)、コマンドプロンプト上から実行します (ダブルクリックで実行しますとコマンドプロンプトウィンドウが強制的に閉じて何が起きているのか観察できないと思います)。

@echo off
for /f "delims=" %%a in ( 'C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\link.exe' ) do echo [%%a]

すると以下のようにエラーが表示されます。

C:\Zenkaku>
C:\Zenkaku>bill_gates.bat
\Microsoft の使い方が誤っています。
C:\Zenkaku>

奇妙なエラーですが、「\Microsoft」の周辺を見ますと「(x86)」という部分があり、いかにも問題を起こしそうです。 また今回のパス名にはスペースが含まれています。 パスにこのような文字が含まれている場合、このパス全体をダブルクォートで括る必要があります。

@echo off
for /f "delims=" %%a in ( '"C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\link.exe"' ) do echo [%%a]

外側のシングルクォートは消してはいけません(今、link.exeの出力結果を%%aに格納し、行単位で処理を行いたいからです)。 あくまでin ( '...' ) 内において、さらにダブルクォートで括るようにします ( /f オプションで "usebackq" を指定している場合、このシングルクォートがバッククォート「`」となりますが、 その場合も全く同様にします)。

これを実行しますと、以下のように表示されます。 これは期待通りの結果であり成功です。

C:\Zenkaku>
C:\Zenkaku>bill_gates.bat
[Microsoft (R) Incremental Linker Version 8.00.50727.42]
[Copyright (C) Microsoft Corporation.  All rights reserved.]
...
C:\Zenkaku>


実験2: パイプでsortへ渡す

では次にlink.exeの出力結果をパイプで他のコマンドに渡すことを考えます。 「|」は、in ( '...' ) 内部では特殊文字ですので、「^」でこれをエスケープした「^|」を与えます。 手始めに出力をsortに渡してみましょう。

@echo off
for /f "delims=" %%a in ( '"C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\link.exe" ^| sort' ) do echo [%%a]

実行結果は以下のようになります。 これも期待通りの結果であり成功です。

C:\Zenkaku>
C:\Zenkaku>bill_gates.bat
[                  EFI_ROM|EFI_RUNTIME_DRIVER|NATIVE|POSIX|WINDOWS|]
[                  WINDOWSCE}[,#[.##]]]
[                SH4|THUMB|X64|X86}]
[      /ALIGN:#]
[      /ALLOWBIND[:NO]]
...
C:\Zenkaku>


実験3: パイプでfindstrへ渡す

ではいよいよ本当にやりたかったことです。 link.exeの出力結果をパイプでfindstrコマンドに渡します。 正規表現のパターン(今回は「Version」という極簡単なパターンですが)を与えて、 Versionという文字列を含む行だけを絞り込みたいとします。 以下のようにすればうまくいくのではないでしょうか?

@echo off
for /f "delims=" %%a in ( '"C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\link.exe" ^| findstr /i "Version"' ) do echo [%%a]

実行結果は以下のようになります。 予想に反して、今回は次のようなエラーが表示されます。

C:\Zenkaku>
C:\Zenkaku>bill_gates.bat
'C:\Program' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。
C:\Zenkaku>

ですが、一つ前のsortコマンドに渡したテストは成功していました。 なぜ今回、単にここをfindstrに変えただけでこのようなことが起きるのでしょうか? 原因を考察してみます。

エラーを見る限り、頭の「C:\Program」の部分を単独のコマンドとして実行しようとしています。 つまり cmd.exe がこの部分の字句解析をしくじったためこのエラーが発生しています。 また「C:\Program」の部分は、「findstr」という文字列よりも随分手前に位置します(というか一番先頭です)。 よってこのエラーは findstr が実行して出したものではないと考えられます。

しかしそうだとしてもこれは奇妙です。 なぜなら今我々は、"C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\link.exe" とパス全体をダブルクォートで括っているはずだからです。 これが一塊とみなされなくなったということは、cmd.exe 内部バッファで一番初めの「"」が消滅していると考えられます。

またfindstrに変えたとたんにこのようなエラーが出たとはいうものの、findstrが実行して出したエラーではないのですから、 これはつまり「findstr /i "Version"」という字句を cmd.exe が字句解析しているときに問題が発生していると考えられます。 sort に渡したときとの一番の字句上の違いは "Version" の部分にダブルクォートが使われていることですが、これが悪さをしているのでしょうか? さらに実験してみます。

実はfindstrコマンドは、パターンが"Version"のように単純なものであれば、「findstr /i Version」のようにダブルクォート無しで与えることもできます。 このように変えて実験してみます。

@echo off
for /f "delims=" %%a in ( '"C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\link.exe" ^| findstr /i Version' ) do echo [%%a]

これを実行すると以下のようになります。

C:\Zenkaku>
C:\Zenkaku>bill_gates.bat
[Microsoft (R) Incremental Linker Version 8.00.50727.42]
[      /VERSION:#[.#]]
C:\Zenkaku>

今度はうまくいきました。 つまり原因はやはり「findstr /i "Version"」の部分のダブルクォートなのでしょうか? しかしそっちではなく"C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\link.exe" の方のダブルクォートが何か関係している可能性もまだ捨て切れません。 さらに分析を進めていきます。

今度は後半は元のままにし、前半だけをもっと単純化して、単にechoコマンドで表示するだけの「echo "Hello Version."」というコマンドに変えて実験してみます。

@echo off
for /f "delims=" %%a in ( 'echo "Hello Version." ^| findstr /i "Version"' ) do echo [%%a]

これを実行すると以下のようになります。

C:\Zenkaku>
C:\Zenkaku>bill_gates.bat
["Hello Version." ]

予想に反し、これはうまくいきます。 後半に「"Version"」というダブルクォートで囲まれた字句があるにも関わらず、うまくいっています。 一体、前半部分と後半部分のどっちが原因なのでしょうか?

ここで少し勘を働かせる必要があります。 これの字句解析部分を作ったMicrosoftのプログラマの気分になって考えてみましょう。 ダブルクォートで囲まれた文字の中身の値だけを取り出したいとき、私達が一番最初に直感的に考えるのは、 まず最初の文字が「"」であるかを調べることです。 そして最初の文字が「"」であった場合はそれを削り、今度は最後の文字から調べていって、後ろの方の「"」も削ります。 勿論本当はこれだけでは全く不十分でありますが、例えばこれと同類の処理が行われていると仮定すると、 今回の現象で内部で起きていることがおおよそ推察できます。

つまりは一番先頭と最後尾の両方に「"」が来た場合に、それを削っているのではないかということです。 この仮説を検証するため、以下のコードで確認してみます。 「echo "Hello Version." & echo "Hello World."」という文字列の先頭と最後尾にダブルクォート「"」を付け足した 「"echo "Hello Version." & echo "Hello World.""」という記述を与え、何が起きるか確かめてみます。

@echo off
for /f "delims=" %%a in ( '"echo "Hello Version." & echo "Hello World.""' ) do echo [%%a]

これを実行すると以下のようになります。

C:\Zenkaku>
C:\Zenkaku>bill_gates.bat
["Hello Version." ]
["Hello World."]

Bingo!

「"echo "Hello Version." & echo "Hello World.""」という記述において、 私達人間はダブルクォートで囲まれた塊を、普通は「"echo "」、「" & echo "」、「""」の3つと解釈します。 ですがこの例で cmd.exe は予想どおり「"Hello Version."」、「"Hello World."」の2つの塊と解釈しました。 この結果はそのことを裏づけます。 やはり cmd.exe は in ( '...' ) 内において、まず両サイドにある「"」文字を問答無用でストリップし、その上で普通に我々が行うようなやり方で解釈をし、実行しています。

念のため今までやってきた実験の結果も思い返してみます。 「"C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\link.exe" ^| sort」や「echo "Hello Version." ^| findstr /i "Version"」 という記述ではうまくいっていました。 一方「"C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\link.exe" ^| findstr /i "Version"」という記述では失敗しておりました。 この結果にも合致しますね。

回避策

原因はわかりました。 では具体的にどう書けばこの問題を回避できるでしょうか?

いっそのこと常に in ( '"..."' ) と両サイドのダブルクォートをつける決まりにするとどうでしょうか? しかしこれはダメです。実際に試してみるとわかりますが、以下のような例でうまくいきません。

@echo off
for /f "delims=" %%a in ( '""C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\link.exe" | findstr /i "Version""' ) do echo [%%a]

両サイドのダブルクォートが狙いどおりストリップされるのはよいのですが、この処理が行われてしまうと今度は「(x86)」の部分が特殊文字とみなされてしまいます。 つまり「(86^)」などと「^」をつかってエスケープしなければなりません。 この例のようにまだパスをハードコーディングした場合はこれで回避できますが、環境変数で与える場合などは致命的になります。

この挙動を抑制するため、いかなる場合も一番先頭に「"」を置くべきではありません (あるいは最後尾の「"」を回避してもよろしいですが、今回は先頭の「"」を回避する方法のみを考えます)。 「"C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\link.exe"」のようなコマンドを実行せざるを得ない場合に備え、 これより前になんらかのダミーを置いておくべきです。 ダミーですから、副作用もなにもない何もしない「何か」でなければなりません。 そのようなものとして何を選べばよいでしょうか?

実際に試してみるとわかりますが、先頭にスペースを挿入して「        "C:\Program Files…"」などとしても効果はありません。 この場合もまた、一番先頭に「"」が来ているとみなされ、同様の処理が行われてしまいます。 ではダミーのコマンド(「"」から始まらない、副作用もなにもない何もしないコマンド)ならどうでしょうか? しかしそんなコマンドってあるでしょうか?

「echo:」でも空行が表示されてしまいますし、「set DUMY=」なんてのも(滅多にないとはいえ)定義されているDUMYを消去してしまいます。 一番良いのは常に条件がtrueとなるif文を頭に置くことです。 つまり「if x==x 」を頭に置きます。 これで後ろに続く文は必ず実行されますし、まさしく副作用もなにもない何もしないコマンドとなります

「"C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\link.exe" ^| findstr /i "Version"」と与える例に戻りましょう。 これの頭に以下のように「if x==x 」を付加し、先頭が「"」で開始しないような文字列にします。 ただし in ( '...' ) 文の中に「=」を直で与える場合、これは特殊文字となりますのでエスケープが必要です。 つまり「if x^=^=x 」のようにしたものを先頭に挿入します。

@echo off
for /f "delims=" %%a in ( 'if x^=^=x "C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\link.exe" ^| findstr /i "Version"' ) do echo [%%a]

これを実行すると以下のようになります。 これは期待通りの結果であり成功です。 ようやくbill_gates.batがMicrosoftから怒られなくなりました。

C:\Zenkaku>
C:\Zenkaku>bill_gates.bat
[Microsoft (R) Incremental Linker Version 8.00.50727.42]
[      /VERSION:#[.#]]

少しだけ可読性についても検討しましょう。 毎回「if x^=^=x 」のように書くのも間違いの元ですので、例えば以下のように分かりやすい名前の変数に入れておきます。 また実行するコマンド本体も in ( '...' ) 内に直接書くと見づらいですので変数に格納した上で指定することにします。

@echo off
set IN_BEGIN=if x^^=^^=x

set CMD=C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\link.exe
for /f "delims=" %%a in ( '%IN_BEGIN% "%CMD%" ^| findstr /i "Version"' ) do echo [%%a]

当初と比べ多少見やすくなりました。 「set IN_BEGIN=if x^^=^^=x」それ自体の実行で「^」がエスケープ文字として働きますから、「if x^=^=x 」を格納するには 「if x^^=^^=x」としなければならないことに注意します。

この方式を使って link.exe の替わりに例えば nmake.exeを実行させてみます。 nmake.exe の場合link.exeと違って標準エラー出力にメッセージが出力されますので、パイプを使うには一旦これを標準出力にリダイレクトしなければなりません。 標準エラー出力のディスクリプタが2、標準出力のディスクリプタが1ですので、これを行うためにはnmake.exeの後ろ(パイプの直前)に「2>&1」という記述を置く必要がありますが、 in ( '...' ) 文の中に「>」と「&」を直で与える場合、これは特殊文字となりますのでエスケープが必要です。 つまり「2^>^&1」となります。 全体としては以下のようになるでしょう。

set IN_BEGIN=if x^^=^^=x

set CMD=C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\nmake.exe
for /f "delims=" %%a in ( '%IN_BEGIN% "%CMD%" 2^>^&1 ^| findstr /i "Version"' ) do echo [%%a]

これも毎回「2^>^&1」と書くのも間違いの元ですので、例えば以下のように分かりやすい名前の変数に入れておきます。

@echo off
set IN_BEGIN=if x^^=^^=x
set IN_REDIRECT_2TO1=2^^^>^^^&1

set CMD=C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin\nmake.exe
for /f "delims=" %%a in ( '%IN_BEGIN% "%CMD%" %IN_REDIRECT_2TO1% ^| findstr /i "Version"' ) do echo [%%a]

「set IN_REDIRECT_2TO1=2^^^>^^^&1」それ自体の実行で「^」がエスケープ文字として働き、尚かつここでも「>」と「&」が特殊文字として働きますから、 「2^>^&1」の6文字をそれぞれ「2」「^^」「^>」「^^」「^&」「1」と指定しなければならないことに注意します。 これをまとめて書いたものが「2^^^>^^^&1」ですね。

Close


Close


最終課題:VCの環境変数を自動設定するバッチファイルを自作する
最終課題:VCの環境変数を自動設定するバッチファイルを自作する

ここまでの知識を総動員して、少し実用度を高くした例をやって終わりにしましょう。

今回は、VCにインストールされているヘッダやライブラリの存在性をチェックすることで、 VCの環境変数PAHT、INCLUDE、LIBの設定をある程度自動化するバッチファイルを自作します (VCの環境変数についてあまりご存知でない方は、VCを動作させる3つの環境変数をお読みください)。 実際にはこのサンプルではまだまだ考慮すべき課題が残されていますが、改良していく取り掛かりには良いと思います。

まずイキナリ、今回のゴールとなるサンプルコード simple_detect.dat の全ソースコードを見せます。 ソースコードは約380行とかなり長いですが、使っている文法自体はほとんど今までの知識の組み合わせである上、 似たような記述をずらずらと書いてあるだけなので、見た目ほどは難しくはないと思います (唯一、サブルーチンに引数を指定している箇所はまだ説明しておりませんが、それについては後で解説します)。

simple_detect.batの全ソースコード(約380行) (388 lines)

@echo off
REM Simple VC Include or Lib detection.

REM Target OS binary type (x86 or x64).
if %MACHINE% == "" set MACHINE=x86

if not "%ProgramFiles(x86)%" == "" set PROGRAM_FILE_X86_DIR=%ProgramFiles(x86)%& goto ProgramFilesAutoSet_End
if not "%ProgramFiles%"      == "" set PROGRAM_FILE_X86_DIR=%ProgramFiles%&      goto ProgramFilesAutoSet_End
                                   set PROGRAM_FILE_X86_DIR=C:\Program Files&    goto ProgramFilesAutoSet_End
:ProgramFilesAutoSet_End

REM Default (for VS2017)
set VC_SDK_ROOT_DIR=%PROGRAM_FILE_X86_DIR%
set          VC_DIR=%PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio\2017\BuildTools\VC\Tools\MSVC\14.16.27023
set     VC_UCRT_DIR=%PROGRAM_FILE_X86_DIR%\Windows Kits\10
set VC_UCRT_STRICT_VER=10.0.10240.0

REM Custom directory
REM set VC_SDK_ROOT_DIR=E:\share_e\trunk_id1\mt_redundant_dev\ms_devs\WinSDK_v7
REM set          VC_DIR=E:\share_e\trunk_id1\mt_redundant_dev\mt_compiler_ms\minvs\vs2017\BuildTools\VC\Tools\MSVC\14.16.27023
REM set     VC_UCRT_DIR=E:\share_e\trunk_id1\mt_redundant_dev\ms_devs\WindowsUniversalCRTSDK\Windows Kits\10
REM set VC_UCRT_STRICT_VER=10.0.10240.0

set INCLUDE=
set LIB=
if "%PATH%" == "" set PATH=%windir%\System32;%windir%


echo MACHINE=[%MACHINE%]
echo: 


echo Scan Windows SDK 6.0 earlier:
setlocal
	set WINSDK_DIR=%VC_SDK_ROOT_DIR%\Microsoft SDKs\Windows\v6.0
	set MY_INCLUDE=
	set MY_LIB=
	
	REM Scan include
	call :Func_FindInclude "%WINSDK_DIR%" "" "windows.h"
	if not "%ANS_DIR%" == "" set MY_INCLUDE=%MY_INCLUDE%;%ANS_DIR%
	
	REM Scan lib
	call :Func_FindLib "%WINSDK_DIR%" "" "%MACHINE%" "user32.lib"
	if not "%ANS_DIR%" == "" set MY_LIB=%MY_LIB%;%ANS_DIR%
	
	call :Func_FindLib "%WINSDK_DIR%" "" "%MACHINE%" "msdelta.lib"
	if not "%ANS_DIR%" == "" set MY_LIB=%MY_LIB%;%ANS_DIR%

	call :Func_Update_Include_and_Lib
REM export INCLUDE and LIB
endlocal& set INCLUDE=%INCLUDE%& set LIB=%LIB%


echo Scan Windows SDK 7.1:
setlocal
	set WINSDK_DIR=%VC_SDK_ROOT_DIR%\Microsoft SDKs\Windows\v7.1
	set MY_INCLUDE=
	set MY_LIB=
	
	REM Scan include
	call :Func_FindInclude "%WINSDK_DIR%" "" "windows.h"
	if not "%ANS_DIR%" == "" set MY_INCLUDE=%MY_INCLUDE%;%ANS_DIR%
	
	REM Scan lib
	call :Func_FindLib "%WINSDK_DIR%" "" "%MACHINE%" "user32.lib"
	if not "%ANS_DIR%" == "" set MY_LIB=%MY_LIB%;%ANS_DIR%

	call :Func_Update_Include_and_Lib
REM export INCLUDE and LIB
endlocal& set INCLUDE=%INCLUDE%& set LIB=%LIB%


echo Scan Windows SDK 8.1:
setlocal
	set WINSDK_DIR=%VC_SDK_ROOT_DIR%\Windows Kits\8.1
	set MY_INCLUDE=
	set MY_LIB=
	
	REM Scan include
	call :Func_FindInclude "%WINSDK_DIR%" "um"     "windows.h"
	if not "%ANS_DIR%" == "" set MY_INCLUDE=%MY_INCLUDE%;%ANS_DIR%
	
	call :Func_FindInclude "%WINSDK_DIR%" "shared" "winapifamily.h"
	if not "%ANS_DIR%" == "" set MY_INCLUDE=%MY_INCLUDE%;%ANS_DIR%
	
	REM Scan lib
	call :Func_FindLib "%WINSDK_DIR%" "winv6.3\um" "%MACHINE%" "user32.lib"
	if not "%ANS_DIR%" == "" set MY_LIB=%MY_LIB%;%ANS_DIR%

	call :Func_Update_Include_and_Lib
REM export INCLUDE and LIB
endlocal& set INCLUDE=%INCLUDE%& set LIB=%LIB%


echo Scan Windows SDK 10:
setlocal
	set WINSDK_DIR=%VC_SDK_ROOT_DIR%\Windows Kits\10
	set STRICT_VER=%VC_UCRT_STRICT_VER%
	set MY_INCLUDE=
	set MY_LIB=
	
	REM Scan include
	call :Func_FindInclude "%WINSDK_DIR%" "%STRICT_VER%\um"     "windows.h"
	if not "%ANS_DIR%" == "" set MY_INCLUDE=%MY_INCLUDE%;%ANS_DIR%
	
	call :Func_FindInclude "%WINSDK_DIR%" "%STRICT_VER%\shared" "winapifamily.h"
	if not "%ANS_DIR%" == "" set MY_INCLUDE=%MY_INCLUDE%;%ANS_DIR%
	
	REM Scan lib
	call :Func_FindLib "%WINSDK_DIR%" "%STRICT_VER%\um" "%MACHINE%" "user32.lib"
	if not "%ANS_DIR%" == "" set MY_LIB=%MY_LIB%;%ANS_DIR%

	call :Func_Update_Include_and_Lib
REM export INCLUDE and LIB
endlocal& set INCLUDE=%INCLUDE%& set LIB=%LIB%


echo Scan Windows Universal CRT SDK:
setlocal
	set WINSDK_DIR=%VC_UCRT_DIR%
	set STRICT_VER=%VC_UCRT_STRICT_VER%
	set MY_INCLUDE=
	set MY_LIB=
	
	REM Scan include
	call :Func_FindInclude "%WINSDK_DIR%" "%STRICT_VER%\ucrt" "stdio.h"
	if not "%ANS_DIR%" == "" set MY_INCLUDE=%MY_INCLUDE%;%ANS_DIR%
	
	REM Scan lib
	call :Func_FindLib "%WINSDK_DIR%" "%STRICT_VER%\ucrt" "%MACHINE%" "libucrt.lib"
	if not "%ANS_DIR%" == "" set MY_LIB=%MY_LIB%;%ANS_DIR%

	call :Func_Update_Include_and_Lib
REM export INCLUDE and LIB
endlocal& set INCLUDE=%INCLUDE%& set LIB=%LIB%


echo Scan C-Standard Library in VC-Compiler:
setlocal
	set MY_INCLUDE=
	set MY_LIB=

	REM Scan include
	call :Func_FindInclude "%VC_DIR%" "" "vector"
	if not "%ANS_DIR%" == "" set MY_INCLUDE=%MY_INCLUDE%;%ANS_DIR%
	
	REM Scan lib
	call :Func_FindLib "%VC_DIR%" "" "%MACHINE%" "libcmt.lib"
	if not "%ANS_DIR%" == "" set MY_LIB=%MY_LIB%;%ANS_DIR%

	call :Func_Update_Include_and_Lib
REM export INCLUDE and LIB
endlocal& set INCLUDE=%INCLUDE%& set LIB=%LIB%


echo Scan VC-Compiler BinDir:
setlocal
	set MY_BIN=

	REM Scan bin for exe
	call :Func_FindBin "%VC_DIR%" "%MACHINE%" "cl.exe"
	if not "%ANS_DIR%" == "" set MY_BIN=%MY_BIN%;%ANS_DIR%

	REM Scan bin for dll in "x86" that x86_amd64 depends on.
	call :Func_FindBin "%VC_DIR%" "x86"       "cl.exe"
	if not "%ANS_DIR%" == "" set MY_BIN=%MY_BIN%;%ANS_DIR%

	REM Scan Common7\IDE for dll that old vc depends on.
	if exist "%VC_DIR%\..\Common7\IDE" set MY_BIN=%MY_BIN%;%VC_DIR%\..\Common7\IDE

	REM The following scan is intended to find, for example, VC_DIR\redist\x86\Microsoft.VC*.CRT (for VS2013/VS2015)
	set TRY_DIR=%VC_DIR%\redist\x86
	if exist "%TRY_DIR%" for /f "delims=" %%a in ( 'dir /b "%TRY_DIR%" ^| findstr /i "Microsoft\.VC.*\.CRT"' ) do if exist "%TRY_DIR%\%%a\*.dll" set MY_BIN=%MY_BIN%;%TRY_DIR%\%%a

	call :Func_Update_Path
REM export PATH
endlocal& set PATH=%PATH%


echo:
echo Final Total Result:
echo:
echo PATH=[%PATH%]
echo:
echo INCLUDE=[%INCLUDE%]
echo:
echo LIB=[%LIB%]
echo:


goto End


REM Sub-routine Func_FindInclude
REM Arg    : "%1" TRY_DIR
REM Arg    : "%2" TRY_SUB_DIR
REM Arg    : "%3" LANDMARK_HDR
REM Return : ANS_DIR
REM
:Func_FindInclude
setlocal
	set ANS_DIR=

	REM Quoted parameters.
	set TRY_DIR=%1
	set TRY_SUB_DIR=%2
	set LANDMARK_HDR=%3
	REM Strip sides quotes.
	set TRY_DIR=%TRY_DIR:~1,-1%
	set TRY_SUB_DIR=%TRY_SUB_DIR:~1,-1%
	set LANDMARK_HDR=%LANDMARK_HDR:~1,-1%

	set TRY_DIR_SAVE=%TRY_DIR%

	set TRY_DIR=%TRY_DIR_SAVE%
	if not "%TRY_SUB_DIR%" == "" goto Func_FindInclude_WithSubDir
		REM Check include
		if exist "%TRY_DIR%\include\%LANDMARK_HDR%" set ANS_DIR=%TRY_DIR%\include
		goto Func_FindInclude_WithSubDir_End

	:Func_FindInclude_WithSubDir
		REM Check include\TRY_SUB_DIR
		if exist "%TRY_DIR%\include\%TRY_SUB_DIR%\%LANDMARK_HDR%" set ANS_DIR=%TRY_DIR%\include\%TRY_SUB_DIR%

	:Func_FindInclude_WithSubDir_End

:Func_FindInclude_End
endlocal& set ANS_DIR=%ANS_DIR%
	exit /b
REM endof Sub-routine


REM Sub-routine Func_FindLib
REM Arg    : "%1" TRY_DIR
REM Arg    : "%2" TRY_SUB_DIR
REM Arg    : "%3" TRY_MACHINE (x86 or x64)
REM Arg    : "%4" LANDMARK_LIB
REM Return : ANS_DIR
REM
:Func_FindLib
setlocal
	set ANS_DIR=

	REM Quoted parameters.
	set TRY_DIR=%1
	set TRY_SUB_DIR=%2
	set TRY_MACHINE=%3
	set LANDMARK_LIB=%4
	REM Strip sides quotes.
	set TRY_DIR=%TRY_DIR:~1,-1%
	set TRY_SUB_DIR=%TRY_SUB_DIR:~1,-1%
	set TRY_MACHINE=%TRY_MACHINE:~1,-1%
	set LANDMARK_LIB=%LANDMARK_LIB:~1,-1%

	set TRY_DIR_SAVE=%TRY_DIR%

	REM For Windows SDK 7.1 or 6.0 or 5.2.
	REM In case x86
	REM   Check lib or lib\x86
	REM In case x64
	REM   Check lib\x64
	REM
	set TRY_DIR=%TRY_DIR_SAVE%
	if "%TRY_MACHINE%" == "x64" goto Func_FindLib_x64
		REM In case x86
		if exist "%TRY_DIR%\lib\%LANDMARK_LIB%"     set ANS_DIR=%TRY_DIR%\lib
		if exist "%TRY_DIR%\lib\x86\%LANDMARK_LIB%" set ANS_DIR=%ANS_DIR%;%TRY_DIR%\lib\x86
		goto Func_FindLib_x64_End
	:Func_FindLib_x64
		REM In case x64
		if exist "%TRY_DIR%\lib\x64\%LANDMARK_LIB%" set ANS_DIR=%TRY_DIR%\lib\x64
	:Func_FindLib_x64_End

	REM If the ANS has been obtained, return to the caller.
	if not "%ANS_DIR%" == "" goto Func_FindLib_End
	

	REM For Windows SDK 8.1 or lator
	REM Check lib\TRY_SUB_DIR\TRY_MACHINE
	REM
	set TRY_DIR=%TRY_DIR_SAVE%
	if exist "%TRY_DIR%\lib\%TRY_SUB_DIR%\%TRY_MACHINE%\%LANDMARK_LIB%" set ANS_DIR=%TRY_DIR%\lib\%TRY_SUB_DIR%\%TRY_MACHINE%

	REM If the ANS has been obtained, return to the caller.
	if not "%ANS_DIR%" == "" goto Func_FindLib_End

:Func_FindLib_End
endlocal& set ANS_DIR=%ANS_DIR%
	exit /b
REM endof Sub-routine


REM Sub-routine Func_FindBin
REM Arg    : "%1" TRY_DIR
REM Arg    : "%2" TRY_MACHINE (x86 or x64)
REM Arg    : "%3" LANDMARK_EXE
REM Return : ANS_DIR
REM
:Func_FindBin
setlocal
	set ANS_DIR=

	REM Quoted parameters.
	set TRY_DIR=%1
	set TRY_MACHINE=%2
	set LANDMARK_LIB=%3
	REM Strip sides quotes.
	set TRY_DIR=%TRY_DIR:~1,-1%
	set TRY_MACHINE=%TRY_MACHINE:~1,-1%
	set LANDMARK_LIB=%LANDMARK_LIB:~1,-1%

	set TRY_DIR_SAVE=%TRY_DIR%
	
	if "%TRY_MACHINE%" == "x64" goto Func_FindBin_x64
		REM x86
	
		REM for VC2017
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin\Hostx86\x86\%LANDMARK_EXE%" set ANS_DIR=%TRY_DIR%\bin\Hostx86\x86
		if not "%ANS_DIR%" == "" goto Func_FindBin_End
	
		REM for VC10, VC11
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin\%LANDMARK_EXE%"             set ANS_DIR=%TRY_DIR%\bin
		if not "%ANS_DIR%" == "" goto Func_FindBin_End
	
	goto Func_FindBin_x64_End
	:Func_FindBin_x64
		REM x64
	
		REM for VC2017
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin\Hostx86\x64\%LANDMARK_EXE%" set ANS_DIR=%TRY_DIR%\bin\Hostx86\x64
		if not "%ANS_DIR%" == "" goto Func_FindBin_End
	
		REM for VC10, VC11
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin\x86_amd64\%LANDMARK_EXE%"   set ANS_DIR=%TRY_DIR%\bin\x86_amd64
		if not "%ANS_DIR%" == "" goto Func_FindBin_End
	
		REM Alternatively, we use Native compiler (not Cross-compiler) (in old VC)
		set TRY_DIR=%TRY_DIR_SAVE%
		if exist "%TRY_DIR%\bin\amd64\%LANDMARK_EXE%" set ANS_DIR=%TRY_DIR%\bin\amd64
		if not "%ANS_DIR%" == "" goto Func_FindBin_End
	:Func_FindBin_x64_End

:Func_FindBin_End
endlocal& set ANS_DIR=%ANS_DIR%
	exit /b
REM endof Sub-routine


REM Sub-routine Func_Update_Include_and_Lib
REM
:Func_Update_Include_and_Lib
	echo Result:
	echo MY_INCLUDE=[%MY_INCLUDE%]
	echo MY_LIB    =[%MY_LIB%]
	echo:
	REM update INCLUDE, LIB
	if not "%MY_INCLUDE%" == "" set INCLUDE=%INCLUDE%;%MY_INCLUDE%
	if not "%MY_LIB%"     == "" set LIB=%LIB%;%MY_LIB%

	exit /b
REM endof Sub-routine


REM Sub-routine Func_Update_Path
REM
:Func_Update_Path
	echo Result:
	echo MY_BIN    =[%MY_BIN%]
	echo:
	REM update PATH
	if not "%MY_BIN%"     == "" set PATH=%PATH%;%MY_BIN%

	exit /b
REM endof Sub-routine


:End

REM test.
REM cl.exe test.c
REM link.exe

pause
Close


simple_detect.batのダウンロード

使い方のイメージ

ソースコードの解説に入る前に、使い方のイメージを示しておきます。

simple_detect.bat は、コマンドプロンプトから以下のように実行し、clコマンドやlinkコマンドが使えるように指向して作ったものです。 (説明の都合上極限まで簡単化したため、clやlinkにほとんど何のオプションも指定されていませんが、 実際の運用ではもっと色々とオプション指定を考慮すべきです。指定すべきオプションについては「やさしいテスト」で詳しく述べています)。

C:\Zenkaku>set MACHINE=x86
C:\Zenkaku>call simple_detect.bat
C:\Zenkaku>cl -c test_hello.c
C:\Zenkaku>link /OUT:test.exe test.obj

x64版のバイナリを作りたい場合は環境変数MACHINEの値をx64にしておいてからこれを実行します。 link.exeコマンドのオプションが /MACHINE:AMD64 となることにも注意してください。

C:\Zenkaku>set MACHINE=x64
C:\Zenkaku>call simple_detect.bat
C:\Zenkaku>cl -c test_hello.c
C:\Zenkaku>link /MACHINE:AMD64 /OUT:test.exe test.obj

clがうまく起動できない場合、後述する4つの環境変数に基本的なパスを適切に指定すれば動作するかもしれません。 simple_detect.batをテキストエディタで開き、最初の方にあるCustom directoryと書かれた部分にあるsetコマンドのコメントアウトを解除し、 これに与えている値を直接書き換えてください。 以下にその部分を抜粋します。

REM Custom directory
REM set VC_SDK_ROOT_DIR=E:\share_e\trunk_id1\mt_redundant_dev\ms_devs\WinSDK_v7
REM set          VC_DIR=E:\share_e\trunk_id1\mt_redundant_dev\mt_compiler_ms\minvs\vs2017\BuildTools\VC\Tools\MSVC\14.16.27023
REM set     VC_UCRT_DIR=E:\share_e\trunk_id1\mt_redundant_dev\ms_devs\WindowsUniversalCRTSDK\Windows Kits\10
REM set VC_UCRT_STRICT_VER=10.0.10240.0

ただしこのsimple_detect.batはあくまでサンプルなので、本来処理すべき様々な要素を省いてあるなど完成度はあまり高くないです。 これら4つのパスを設定してもうまくいかない可能性はそこそこあります。 実際の運用にはdetect_vc.batを使うことをお勧めします

4つの環境変数

このsimple_detect.batは最初に設定した4つの環境変数から取得する基本的なパスを元に、それ以降の処理を行います。 この4つの環境変数の意味については以下となります。
  • VC_SDK_ROOT_DIR

  • Windows SDK のルートとなるディレクトリのパスを与えます。 これは例えば、Windows Kits ディレクトリの一つ上のディレクトリ、 または Microsoft SDKs ディレクトリの一つ上のディレクトリなどが該当し、 大抵の場合「C:\Program Files (x86)」です。

    デフォルトの値は、%PROGRAM_FILE_X86_DIR% です (PROGRAM_FILE_X86_DIRの値は、コードの最初で自動的に取得しております)。

  • VC_DIR

  • VCのトップディレクトリのパスを与えます。 これは、bin、include、lib が直下に存在するディレクトリです。 さらにbinの配下にcl.exeなどの基本コマンドが存在していなければなりません。

    デフォルトの値はVS2017向けにハードコーディングしており、以下のようになっています。

    %PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio\2017\BuildTools\VC\Tools\MSVC\14.16.27023

    ただし最後の14.16.27023は筆者がインストールしたVCのバージョンにおいては、たまたまこの名前であっただけです。 あなたがインストールしたVS2017ではまた違った名前になっている可能性があります。 このsimple_detect.batではその辺の違いをうまくスキップ処理できません(detect_vc.batではこのあたりの処理も組み込んでいます)。 よって、お使いのVS2017の該当するディレクトリまで潜り、ここの部分のディレクトリ名がどうなっているのかをあらかじめ調べた上、 この部分を修正する必要はあるかと思います。

    参考までに、VS2015以前のデフォルトのVCディレクトリのパスをVC_DIRに自動取得するコードを以下に示します。 このサンプルをVS2015以前のものに対応させたい方は参考にしてください。

    setlocal
    	if not "%VC_DIR%" == "" goto VCDirAutoSet_End
    
    		REM VS2015(VC14)
    		set TRY_DIR=%PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio 14.0\VC
    		if exist "%TRY_DIR%" set VC_DIR=%TRY_DIR%& goto VCDirAutoSet_End
    
    		REM VS2013(VC12)
    		set TRY_DIR=%PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio 12.0\VC
    		if exist "%TRY_DIR%" set VC_DIR=%TRY_DIR%& goto VCDirAutoSet_End
    
    		REM VS2012(VC11)
    		set TRY_DIR=%PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio 11.0\VC
    		if exist "%TRY_DIR%" set VC_DIR=%TRY_DIR%& goto VCDirAutoSet_End
    
    		REM VS2010(VC10)
    		set TRY_DIR=%PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio 10.0\VC
    		if exist "%TRY_DIR%" set VC_DIR=%TRY_DIR%& goto VCDirAutoSet_End
    
    		REM VS2008(VC9)
    		set TRY_DIR=%PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio 9.0\VC
    		if exist "%TRY_DIR%" set VC_DIR=%TRY_DIR%& goto VCDirAutoSet_End
    
    		REM VS2005(VC8)
    		set TRY_DIR=%PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio 8\VC
    		if exist "%TRY_DIR%" set VC_DIR=%TRY_DIR%& goto VCDirAutoSet_End
    
    		REM VS2003(VC7.1)
    		set TRY_DIR=%PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio .NET 2003\Vc7
    		if exist "%TRY_DIR%" set VC_DIR=%TRY_DIR%& goto VCDirAutoSet_End
    
    		REM VS6.0
    		set TRY_DIR=%PROGRAM_FILE_X86_DIR%\Microsoft Visual Studio\VC98
    		if exist "%TRY_DIR%" set VC_DIR=%TRY_DIR%& goto VCDirAutoSet_End
    
    	:VCDirAutoSet_End
    REM export VC_DIR
    endlocal& set VC_DIR=%VC_DIR%
    
  • VC_UCRT_DIR

  • VS2015以降のVC(VC14)を使用される場合はこの指定が必要となります。

    Windows Universal CRT SDKのトップディレクトリのパスを与えます。 これは、include、lib が直下に存在するディレクトリです。 さらにinclude\%VC_UCRT_STRICT_VER%\ucrtの配下stdio.hなどのC標準ライブラリのヘッダ、 lib\%VC_UCRT_STRICT_VER%\ucrtの配下にlibucrt.libが存在していなければなりません。

  • VC_UCRT_STRICT_VER

  • VS2015以降のVC(VC14)を使用される場合はこの指定が必要となります。

    Windows Universal CRT SDKの厳密なバージョン番号です。 なぜこれを指定する必要があるかといえば、includeパス、libパスにこれを名前とするディレクトリが含まれるからです。 デフォルトではC:\Program Files (x86)\Windows Kits\10\includeなどを開くと現れるディレクトリの名前です。

    筆者の環境ではたまたま 10.0.10240.0 であったためデフォルト値をそのようにしてありますが、 お使いのWindows Universal CRT SDKの該当するディレクトリを調べ、この変数の値を修正する必要があるかもしれません。

    この指定が必要な理由もまた、simple_detect.batではその辺の違いをうまくスキップ処理できないことに起因します (detect_vc.batではこのあたりの処理も組み込んでいます)。

ソースコードの前半部分の解説: サブルーチンを呼び出すパート

4つの環境変数の初期化が終わったら、次は具体的なスキャン処理に入っていきます。 以下は、Windows SDK 7.1における、MY_INCLUDE(INCLUDEに追加すべき値)とMY_LIB(LIBに追加すべき値)を取得する箇所の抜粋です。

echo Scan Windows SDK 7.1:
setlocal
	set WINSDK_DIR=%VC_SDK_ROOT_DIR%\Microsoft SDKs\Windows\v7.1
	set MY_INCLUDE=
	set MY_LIB=

	REM Scan include
	call :Func_FindInclude "%WINSDK_DIR%" "" "windows.h"
	if not "%ANS_DIR%" == "" set MY_INCLUDE=%MY_INCLUDE%;%ANS_DIR%

	REM Scan lib
	call :Func_FindLib "%WINSDK_DIR%" "" "%MACHINE%" "user32.lib"
	if not "%ANS_DIR%" == "" set MY_LIB=%MY_LIB%;%ANS_DIR%

	call :Func_Update_Include_and_Lib
REM export INCLUDE and LIB
endlocal& set INCLUDE=%INCLUDE%& set LIB=%LIB%

call :Func_FindInclude の呼び出しでは、1番目の引数に "%WINSDK_DIR%"、2番目の引数に""、3番目の引数に"windows.h" を与え、 %WINSDK_DIR%\include の下に windows.h というファイルがあるかどうかを調べています。 そのファイルが存在するならば、サブルーチン内でANS_DIRに空でない値がセットされます。 次の行では、ANS_DIRが空でない場合(つまりサブルーチンが成功した場合)、MY_INCLUDEにANS_DIRの値を「;」区切りで追加しています。

call :Func_FindLib の呼び出しも同様に、1番目の引数に "%WINSDK_DIR%"、2番目の引数に""、3番目の引数に"%MACHINE%"、4番目の引数に"user32.lib" を与え、 %WINSDK_DIR%\lib の下に user32.lib というファイルがあるかどうかを調べています。 ただし%MACHINE%の値に応じて、そのバイナリに該当するディレクトリ内を探します。 そのファイルが存在するならば、サブルーチン内でANS_DIRに空でない値がセットされます。 次の行では、ANS_DIRが空でない場合(つまりサブルーチンが成功した場合)、MY_LIBにANS_DIRの値を「;」区切りで追加しています。

call :Func_Update_Include_and_Lib では MY_INCLUDEとMY_LIBの値を表示します。 またそれらの値が空でない場合は、それぞれの値を環境変数INCLUDE、LIBに追加します。

最後に endlocal& set INCLUDE=%INCLUDE%& set LIB=%LIB% の部分により、環境変数LIBとINCLUDEのエクスポートを行っています。 すなわち、INCLUDEとLIBの更新を setlocal endlocal の外部へ反映させます。

このバッチファイルは、これ以降の部分もおおよそこのパターンの繰り返しになっており、 Windows SDK 6.0 、Windows SDK 7.1、Windows SDK 8.0、Windows SDK 10.0 に関しては、すべてこの順に同様にスキャンしています。 Windows SDK 8.1とWindows SDK 10についてはincludeディレクトリやlibディレクトリの直下にさらに中間的なサブディレクトリが存在するため、 Func_FindInclude や Func_FindLib の 2番目の引数にそのサブディレクトリ名を指定した上でスキャンしています。

ソースコードの後半部分の解説: サブルーチンを実装しているパート

具体的にサブルーチンFunc_FindIncludeの実装も見てみましょう。 以下はこれを抜粋したものです(ソースコードの後半にあります)。

REM Sub-routine Func_FindInclude
REM Arg    : "%1" TRY_DIR
REM Arg    : "%2" TRY_SUB_DIR
REM Arg    : "%3" LANDMARK_HDR
REM Return : ANS_DIR
REM
:Func_FindInclude
setlocal
	set ANS_DIR=

	REM Quoted parameters.
	set TRY_DIR=%1
	set TRY_SUB_DIR=%2
	set LANDMARK_HDR=%3
	REM Strip sides quotes.
	set TRY_DIR=%TRY_DIR:~1,-1%
	set TRY_SUB_DIR=%TRY_SUB_DIR:~1,-1%
	set LANDMARK_HDR=%LANDMARK_HDR:~1,-1%

	set TRY_DIR_SAVE=%TRY_DIR%

	set TRY_DIR=%TRY_DIR_SAVE%
	if not "%TRY_SUB_DIR%" == "" goto Func_FindInclude_WithSubDir
		REM Check include
		if exist "%TRY_DIR%\include\%LANDMARK_HDR%" set ANS_DIR=%TRY_DIR%\include
		goto Func_FindInclude_WithSubDir_End

	:Func_FindInclude_WithSubDir
		REM Check include\TRY_SUB_DIR
		if exist "%TRY_DIR%\include\%TRY_SUB_DIR%\%LANDMARK_HDR%" set ANS_DIR=%TRY_DIR%\include\%TRY_SUB_DIR%

	:Func_FindInclude_WithSubDir_End

:Func_FindInclude_End
endlocal& set ANS_DIR=%ANS_DIR%
	exit /b
REM endof Sub-routine

最初に与えられた引数(parameters)の解析を行います。

このサブルーチンでは引数はすべてダブルクォートで囲って与えなければなりません (Func_FindLib、Func_FindBinも同様のルールであるとします)。 このようなルールにしたのは引数が空文字である場合にも対処できるようにするためです。

サブルーチンに与えられた引数(parameters)のうち、第1引数は%1、第2引数は%2、第3引数は%3 といった形で取得できます。 これらをまずTRY_DIR、TRY_SUB_DIR、LANDMARK_HDRという意味の分かりやすい環境変数に格納します。

ただしこのままでは値はまだダブルクォートで囲まれていますので、この両サイドにあるダブルクォートを剥がします。 その処理が例えばTRY_DIRの場合ですと、set TRY_DIR=%TRY_DIR:~1,-1% の部分になります。

%TRY_DIR:~m,-n% という記述は、最初のn文字目から最後の文字のn文字分前までの範囲が抽出された値となります(0文字目から数えます)。 従って %TRY_DIR:~1,-1% という記述は、最初と最後の文字を除いた部分になります。 それを改めてTRY_DIRにセットしていますので、結果的に両サイドのダブルクォートが剥がれたことになります。

それ以降の処理は、ここまで学ばれてきた方なら読めることでしょう。 一番最後の部分で endlocal& set ANS_DIR=%ANS_DIR% として ANS_DIRだけがエクスポートされますので、 setlocal endlocal より外部へこの値が戻り値としてセットされることになります。

その他のサブルーチンについて少し

simple_detect.bat では Func_FindInclude、Func_FindLib、Func_FindBin の3つのサブルーチンを作ってそれをcallしています。 これらのサブルーチンには一応引数らしきものと、戻り値らしきものが用意されています。

Sub-routine: Func_FindInclude
Func_FindInclude には次の引数を与えます。
  • TRY_DIR
  • このパス配下にincludeディレクトリがあるものとして検査を始めます。

  • TRY_SUB_DIR
  • includeディレクトリの配下にこれで指定されたサブディレクトリが存在する場合は、その中も検査します。

  • LANDMARK_HDR
  • この引数で指定されたファイル名のヘッダファイルが検査中に存在すれば、それを含むディレクトリのパスをANS_DIRにセットします。

戻り値はANS_DIRに格納されます。 これにはLANDMARK_HDRで指定されたヘッダが存在するディレクトリへのパスが格納されます。 ただし、LANDMARK_HDRで指定されたヘッダが存在しない場合は、この値は空となります。


Sub-routine: Func_FindLib
Func_FindLib には次の引数を与えます。
  • TRY_DIR
  • このパス配下にlibディレクトリがあるものとして検査を始めます。

  • TRY_SUB_DIR
  • libディレクトリの配下にこれで指定されたサブディレクトリが存在する場合は、その中も検査します。

  • TRY_MACHINE
  • この引数では検査したいライブラリのバイナリタイプを指定します。 作成するアプリのターゲットOSが32bit Windowsである場合はx86を、64bit Windowsである場合はx64を指定します。 この2種類の値以外はこのスクリプトではサポートされません。

  • LANDMARK_LIB
  • この引数で指定されたファイル名のライブラリファイルが検査中に存在すれば、それを含むディレクトリのパスをANS_DIRにセットします。

戻り値はANS_DIRに格納されます。 これにはLANDMARK_LIBで指定されたライブラリが存在するディレクトリへのパスが格納されます。 ただし、LANDMARK_LIBで指定されたライブラリが存在しない場合は、この値は空となります。


Sub-routine: Func_FindBin
Func_FindBin には次の引数を与えます。
  • TRY_DIR
  • このパス配下にbinディレクトリがあるものとして検査を始めます。

  • TRY_MACHINE
  • この引数では検査したいライブラリのバイナリタイプを指定します。 作成するアプリのターゲットOSが32bit Windowsである場合はx86を、64bit Windowsである場合はx64を指定します。 この2種類の値以外はこのスクリプトではサポートされません。

  • LANDMARK_EXE
  • この引数で指定されたファイル名のEXEファイルが検査中に存在すれば、それを含むディレクトリのパスをANS_DIRにセットします。

戻り値はANS_DIRに格納されます。 これにはLANDMARK_EXEで指定されたEXEファイルが存在するディレクトリへのパスが格納されます。 ただし、LANDMARK_EXEで指定されたEXEファイルが存在しない場合は、この値は空となります。


simple_detect.bat では 最後に以下のような部分が実行され、最終的に環境変数PATH、INCLUDE、LIBの値がどうなったか報告します。 このサンプルを弄くる場合、要はこの値が所望の値になるように改修すればよいわけですね。

echo:
echo Final Total Result:
echo:
echo PATH=[%PATH%]
echo:
echo INCLUDE=[%INCLUDE%]
echo:
echo LIB=[%LIB%]
echo:

また Func_Update_Include_and_Lib や Func_Update_Path サブルーチンでは、その時点までで一時的に取得したMY_BIN、MY_INCLUDE、MY_LIBという値を 適宜表示しておりますが、この値も参考になると思います(MY_BINはPATHに追加すべき値です)。

Close




目次に戻る

やさしいテスト

おテストのお時間よー

インストールは一通り終わり、最低限のコンパイルとリンクができるようになりました。 とはいえ、色々弄りまくりました場合は以下の項目については実際にコンパイルしてその結果をテストしておいた方がいいでしょう。

  • detect_vc.batが成功するか?
  • 単純なHello Worldプログラムが、x86版バイナリとx64バイナリのそれぞれでキチンと作成できるか?
  • Windows APIのヘッダがインクルードでき、かつそのインポートライブラリがリンクできるか?
  • C++のSTLをインクルードできるか?
  • デバッグバージョンのコンパイルとリンクができるか?

以上の点を検査することに致しましょう。 このテストで使う全てのファイルをまとめたtest.zipも用意しておきましたので、実際に試したい方はダウンロードしてください。

test.zipのダウンロード

detect_vc.batの検査

detect_vc.bat が成功するか否かの検査です。 これについては「detect_vc.bat の導入と使い方」のセクションで既に述べた通りですが、コマンドプロンプト上から単に「detect_vc.bat」を実行します。 以下のように表示されるはずです。

C:\Zenkaku\cd test
C:\Zenkaku\test>
C:\Zenkaku\test>detect_vc.bat
ExportMode: recommanded

VCVars:
  PATH : {
    [C:\Windows\system32]
    [C:\Windows]
    [C:\Program Files\TortoiseSVN\bin]
    [C:\Program Files (x86)\Git\cmd]
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\bin]
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\..\Common7\IDE]
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\..\Crystal Reports]
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\..\EnterpriseFrameworks]
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\..\ReportViewer]
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\..\sqlserver]
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\redist\x86\Microsoft.VC80.CRT]
  }
  INCLUDE : {
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\PlatformSDK\include]
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\include]
  }
  LIB : {
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\PlatformSDK\lib]
    [C:\Program Files (x86)\Microsoft Visual Studio 8\VC\lib]
  }

DetectSubsystemVersion:
  DET_VC_SUBSYS_VER=[4.00]
C:\Zenkaku\test>

最後の「DET_VC_SUBSYS_VER=[4.00]」の部分になんらかの数字(この例ですと4.00)が表示されているならdetect_vc.batは成功しています。 逆にこれが空、つまり「DET_VC_SUBSYS_VER=[]」のように表示された場合、detect_vc.batは失敗しています。 この値が空になっている場合は、detect_vc_config.bat などを作成して対処してください。 これについての詳細はdetect_vc.bat の導入と使い方のセクションをご覧ください。

test_hello.c

もっとも単純なプログラムに関するテストです (Cコンパイラ本体、ならびにWindows Universal CRT SDK のINCLUDEとLIBが適切に設定されている必要があります。 またkernel32.libをリンクするため、正確に言えばWindows SDKのLIBも適切に設定されている必要があります)。

これをテストするCファイルをtest_hello.cとします。

test_hello.c

#include <stdio.h>
int main( void )
{
	printf( "Hello sizeof(void*)=[%u]\n", (unsigned int)sizeof(void*) );
	printf( "Press Enter Key.\n" );
	getchar();
	return 0;
}



link.exeで明確に指定すべきインポートライブラリは今回はありません。

今回はtest.zip内に含まれる Makefile_vc.mak を使ってnmakeでコンパイルとリンクを行います。

test\Makefile_vc.makの全ソースコード (119 lines)

# Source directory
S = .

!IF "$(MACHINE)" == "x64"
VC_MACHINE=AMD64
PLATFORM=win64
!ELSE
MACHINE=x86
VC_MACHINE=X86
PLATFORM=win32
!ENDIF
# Output directory
ABINAME=vc$(MACHINE)$(DEBUG)
O = .\out_dir\$(ABINAME)

#
# In VS2012(VC11) or later, if you want to generate XP-compatible EXE/DLL, you need to specify the subsystem-version (5.01/5.02)
# explicitly in LINK /SUBSYSTEM: option. For example, LINK /SUBSYSTEM:WINDOWS,5.01 etc.
# This is because the subsystem-version is set to 6.00 (target OS is Vista or later) by default in VC11 or later.
#
# Note that the version specified in the /SUBSYSTEM option of LINK has a different minimum value for each compiler.
# For VC2005 and earlier, the minimum value is 4.00 (target OS is Windows95 or later).
# For VS2008 (VC9) and VS2010 (VC10), the minimum value is 5.00 (target OS is Windows2000 or later).
# For VC2012 or later, the minimum value is 5.01/5.02 (target OS is WindowsXP x86 / Windows XP x64, Windows Server 2003 or later).
#
# If you specify a value that is less than the minimum, LINK will output an error LNK4010,
# so you can automatically get the minimum version value that should be specified by detecting that string.
# The detect_vc.bat (by Zenkaku@znk_project) will automatically retrieve it and set the result to the environment variable DET_VC_SUBSYS_VER.
# 
LINKER_OPT_SUBSYSTEM=/SUBSYSTEM:CONSOLE,$(DET_VC_SUBSYS_VER)

!IF "$(DEBUG)" == "d"
COMPILER=cl -nologo -EHsc /GR /W4 /MTd /Z7 /D_DEBUG /RTC1 
LINKER=link /MACHINE:$(VC_MACHINE) $(LINKER_OPT_SUBSYSTEM) /DEBUG /INCREMENTAL:NO 
!ELSE
COMPILER=cl -nologo -EHsc /GR /W4 /MT /O2 /DNDEBUG 
LINKER=link /MACHINE:$(VC_MACHINE) $(LINKER_OPT_SUBSYSTEM)  
!ENDIF


INCLUDE_FLAG =  \

INSTALL_DIR=out_dir\test_install

EXE_FILE0=$O\test_hello.exe
OBJS0=\
	$O\test_hello.obj \

ILIBS0=\


EXE_FILE1=$O\test_ws.exe
OBJS1=\
	$O\test_ws.obj \

ILIBS1=\
	user32.lib \
	ws2_32.lib \


EXE_FILE2=$O\test_stl.exe
OBJS2=\
	$O\test_stl.obj \

ILIBS2=\


EXE_FILE3=$O\test_dbg.exe
OBJS3=\
	$O\test_dbg.obj \

ILIBS3=\



# Entry rule.
all: all_at_first $O $(EXE_FILE0) $(EXE_FILE1) $(EXE_FILE2) $(EXE_FILE3) 

# Init.
all_at_first:

test_hello: all_at_first $O $(EXE_FILE0)

test_ws:    all_at_first $O $(EXE_FILE1)

test_stl:   all_at_first $O $(EXE_FILE2)

test_dgb:   all_at_first $O $(EXE_FILE3)

# Mkdir rule.
$O:
	if not exist $O mkdir $O


# Product files rule.
$(EXE_FILE0): $(OBJS0) 
	$(LINKER) /OUT:$(EXE_FILE0)  $(OBJS0) $(ILIBS0) 

$(EXE_FILE1): $(OBJS1) 
	$(LINKER) /OUT:$(EXE_FILE1)  $(OBJS1) $(ILIBS1) 

$(EXE_FILE2): $(OBJS2) 
	$(LINKER) /OUT:$(EXE_FILE2)  $(OBJS2) $(ILIBS2) 

$(EXE_FILE3): $(OBJS3) 
	$(LINKER) /OUT:$(EXE_FILE3)  $(OBJS3) $(ILIBS3) 


# Suffix rule.
{$S}.c{$O}.obj:
	$(COMPILER) -I$S $(INCLUDE_FLAG) -Fo$@ -c $<

{$S}.cpp{$O}.obj:
	$(COMPILER) -I$S $(INCLUDE_FLAG) -Fo$@ -c $<


# Clean rule.
clean:
	rmdir /S /Q $O\ 
Close


コマンドプロンプトから以下のように実行します。 今回はx86バイナリとx64バイナリの両方が生成できるかをテストするため、 あらかじめ環境変数MACHINEにそれぞれの値をセットした上でdetect_vc.batを実行する必要があることに注意しましょう。

C:\Zenkaku\test>set MACHINE=x86
C:\Zenkaku\test>detect_vc.bat
C:\Zenkaku\test>nmake -f Makefile_vc.make test_hello
C:\Zenkaku\test>
C:\Zenkaku\test>set MACHINE=x64
C:\Zenkaku\test>detect_vc.bat
C:\Zenkaku\test>nmake -f Makefile_vc.make test_hello
C:\Zenkaku\test>

あるいは上記をそのまま実行するようなバッチファイルbuild_test_hello.batを作っておき、これをダブルクリックしてもよいでしょう。

build_test_hello.batの全ソースコード (29 lines)

@echo off

REM Define ZNK_VC_DIR, ZNK_VC_WINSDK_DIR, ZNK_VC_UCRT_DIR
if exist "detect_vc_config.bat" call detect_vc_config.bat

set MAK_TGT=test_hello

REM Build x86 binary (for 32bit/64bit Windows)

	REM Define PATH, INCLUDE, LIB and DET_VC_SUBSYS_VER for VC, target binary x86.
	set MACHINE=x86
	call detect_vc.bat
	
	REM For release (The binary is optimized)
	set DEBUG=
	nmake -f Makefile_vc.mak %MAK_TGT%
pause


REM Build x64 binary (for 64bit Windows only)

	REM Define PATH, INCLUDE, LIB and DET_VC_SUBSYS_VER for VC, target binary x64.
	set MACHINE=x64
	call detect_vc.bat
	
	REM For release (The binary is optimized)
	set DEBUG=
	nmake -f Makefile_vc.mak %MAK_TGT%
pause
Close


out_dir\vcx86ディレクトリとout_dir\vcx64ディレクトリ内にそれぞれ test_hello.exe が生成できていれば第一関門はクリアです。
out_dir\vcx86\test_hello.exe を実行したとき、sizeof(void*) の値が 4 ならば、きちんと32bit版のバイナリ(x86)が生成されています。
out_dir\vcx64\test_hello.exe を実行したとき、sizeof(void*) の値が 8 ならば、きちんと64bit版のバイナリ(x64)が生成されています。
以降のテストも同様の構成でこのout_dirディレクトリ配下へexeファイルを生成することにします。

test_ws.c

次にWindows APIに関する(Winsockに関する)極単純なテストです(Windows SDKのINCLUDEとLIBがきちんと設定されている必要があります)。 これをテストするCファイルをtest_ws.cとします。

test_ws.c

#include <stdio.h>
#include <stdlib.h>


/***
 * pragma warning(push) stores the current warning state for every warning.
 */
#if defined(_MSC_VER)
#  pragma warning(push)
#  pragma warning(disable: 4005)
#endif

#define WIN32_LEAN_AND_MEAN
/***
 * NOTE: windows.h should be included after winsock2.h
 */
#include <winsock2.h>
#include <windows.h>

/***
 * pragma warning(pop) pops the last warning state pushed onto the stack.
 * Any changes that you made to the warning state between push and pop are undone. 
 */
#if defined(_MSC_VER)
#  pragma warning(pop)
#endif


int main( void )
{
	WSADATA wsaData;
	printf( "Hello sizeof(void*)=[%u]\n", (unsigned int)sizeof(void*) );
	if( WSAStartup(MAKEWORD(2,0), &wsaData) != 0 ){
		printf( "[NG]: WSAStartup : failure.\n" );
		return EXIT_FAILURE;
	}
	MessageBoxA( NULL, "Hello user32.lib and ws2_32.lib !", "MBCS", MB_OK );
	WSACleanup();
	return EXIT_SUCCESS;
}




コマンドプロンプトから以下のように実行します。

C:\Zenkaku\test>set MACHINE=x86
C:\Zenkaku\test>detect_vc.bat
C:\Zenkaku\test>nmake -f Makefile_vc.make test_ws
C:\Zenkaku\test>
C:\Zenkaku\test>set MACHINE=x64
C:\Zenkaku\test>detect_vc.bat
C:\Zenkaku\test>nmake -f Makefile_vc.make test_ws
C:\Zenkaku\test>

あるいは上記をそのまま実行するようなバッチファイルbuild_test_ws.batを作っておき、これをダブルクリックしてもよいでしょう。

build_test_ws.batの全ソースコード (29 lines)

@echo off

REM Define ZNK_VC_DIR, ZNK_VC_WINSDK_DIR, ZNK_VC_UCRT_DIR
if exist "detect_vc_config.bat" call detect_vc_config.bat

set MAK_TGT=test_ws

REM Build x86 binary (for 32bit/64bit Windows)

	REM Define PATH, INCLUDE, LIB and DET_VC_SUBSYS_VER for VC, target binary x86.
	set MACHINE=x86
	call detect_vc.bat
	
	REM For release (The binary is optimized)
	set DEBUG=
	nmake -f Makefile_vc.mak %MAK_TGT%
pause


REM Build x64 binary (for 64bit Windows only)

	REM Define PATH, INCLUDE, LIB and DET_VC_SUBSYS_VER for VC, target binary x64.
	set MACHINE=x64
	call detect_vc.bat
	
	REM For release (The binary is optimized)
	set DEBUG=
	nmake -f Makefile_vc.mak %MAK_TGT%
pause
Close


今回は、linkコマンドの一番後ろにインポートライブラリ user32.lib(MessageBoxA用)とws2_32.lib(WSAStartup、WSAStartupなど用)が追加で指定されています。 Makefile_vc.mak 内を抜粋すると以下の部分です。 ILIBS1の値は、最終的には linkコマンドの一番後ろに指定されます。

EXE_FILE1=$O\test_ws.exe
OBJS1=\
	$O\test_ws.obj \

ILIBS1=\
	user32.lib \
	ws2_32.lib \


これに関してはコンパイルとリンクがうまくいけば合格です。

ちなみに test_ws.exe を実行しても見た目ではダイアログくらいしか表示されません。 一応WinsockというTCP/IP通信のための初期化と終了処理だけを行っています。 winsock2.hはそのためのヘッダです。 これがうまくいくならMoaiのようなアプリも(原理的には)書けることになります。

test_stl.cpp

次にC++ STLに関する極単純なテストです(Cコンパイラ本体側のINCLUDEがきちんと設定されている必要があります)。 これをテストするC++ファイルをtest_stl.cppとします。

test_stl.cpp

#include <stdio.h>
#include <string.h>

/* STL */
#include <string>

int main( void )
{
	std::string str;

	str = "World";
	printf( "Hello sizeof(void*)=[%u]\n", (unsigned int)sizeof(void*) );
	printf( "string.size=[%u]\n", (unsigned int)str.size() );
	printf( "strlen=[%u]\n",      (unsigned int)strlen( str.c_str() ) );
	printf( "string.c_str=[%s]\n", str.c_str() );

	getchar();
	return 0;
}



コマンドプロンプトから以下のように実行します。

C:\Zenkaku\test>set MACHINE=x86
C:\Zenkaku\test>detect_vc.bat
C:\Zenkaku\test>nmake -f Makefile_vc.make test_stl
C:\Zenkaku\test>
C:\Zenkaku\test>set MACHINE=x64
C:\Zenkaku\test>detect_vc.bat
C:\Zenkaku\test>nmake -f Makefile_vc.make test_stl
C:\Zenkaku\test>

あるいは上記をそのまま実行するようなバッチファイルbuild_test_stl.batを作っておき、これをダブルクリックしてもよいでしょう。

build_test_stl.batの全ソースコード (29 lines)

@echo off

REM Define ZNK_VC_DIR, ZNK_VC_WINSDK_DIR, ZNK_VC_UCRT_DIR
if exist "detect_vc_config.bat" call detect_vc_config.bat

set MAK_TGT=test_stl

REM Build x86 binary (for 32bit/64bit Windows)

	REM Define PATH, INCLUDE, LIB and DET_VC_SUBSYS_VER for VC, target binary x86.
	set MACHINE=x86
	call detect_vc.bat
	
	REM For release (The binary is optimized)
	set DEBUG=
	nmake -f Makefile_vc.mak %MAK_TGT%
pause


REM Build x64 binary (for 64bit Windows only)

	REM Define PATH, INCLUDE, LIB and DET_VC_SUBSYS_VER for VC, target binary x64.
	set MACHINE=x64
	call detect_vc.bat
	
	REM For release (The binary is optimized)
	set DEBUG=
	nmake -f Makefile_vc.mak %MAK_TGT%
pause
Close


明確に指定すべきインポートライブラリ等は今回はありません。 stdio.h などのC標準関数のヘッダと string などのSTLのヘッダを混ぜてインクルードしても問題が発生していなければOKです。

参考: コンパイルオプション /EHsc と /GR
今回はC++ですので、cl.exeのオプションとして指定しております /EHsc と /GR の意味について少し解説しておきます。 以下はMicrosoftのサイトの説明をほぼ抜粋し、私の方で少し分かりやすい表現に変えたものです(日本語版の方は自動翻訳のためか説明が腐っておりましたので原文を抜粋します)。

/EHsc

Enables standard C++ stack unwinding. Catches only standard C++ exceptions when you use catch(...) syntax. The compiler assumes that functions declared as extern "C" never throw a C++ exception. Note that if you don't specify c, the compiler assumes that functions declared as extern "C" may throw a C++ exception.

(Excerpt from https://docs.microsoft.com/en-us/cpp/build/reference/eh-exception-handling-model?view=msvc-170)

最初の2文で、C++の標準的な例外を有効にするには /EHs オプションを指定する必要があることが書かれてあります。 最後の文では、/EHsc の最後の c をもし指定しなかった場合は、C言語の関数の場合もC++例外を投げることがあると書かれてあります。 C++では例外が有効、C言語では例外は無効というのが自然であると私は思いますので、オプション /EHsc を指定しております。

次に /GR ですが、以下のようになります。

/GR

If your code uses dynamic_cast Operator or typeid, you usually need the /GR option. By default, /GR is on.

(Excerpt from https://docs.microsoft.com/en-us/cpp/build/reference/gr-enable-run-time-type-information?view=msvc-170)

ダイナミックキャストやtypeidを使いたい場合に必要と書かれています。 C++ではこれらが有効である方が自然であると私は思いますので、オプション /GR を指定しております。 尚、By default, /GR is on. とありますが、昔のコンパイラではデフォルトでは off であったため、 そのようなコンパイラでも有効になるように明示的に指定しております。

どちらのオプションも純粋なC言語をコンパイルする場合において、特別な害はありません。 よってこのオプションはC/C++両方で常に指定しておくべきオプションと考えてよいでしょう。

Close


test_dbg.c

最後にデバッグバージョンでコンパイル可能かどうかの極単純なテストです。 これをテストするCファイルをtest_dbg.cとします。

test_dbg.c

static char func1( void )
{
	const char* null_ptr = (char*)0;
	char c = null_ptr[ 0 ]; /* Force to run debugger break! */
	return c;
}
static void func2( void )
{
	func1();
}
int main( void )
{
	func2();
	return 0;
}



コマンドプロンプトから以下のように実行します。 今回は環境変数DEBUGの値としてdを指定するところに注意です。 これはMakefile_vc.mak内でデバッグバージョンか否かの判定値として使われています。

C:\Zenkaku\test>set DEBUG=d
C:\Zenkaku\test>
C:\Zenkaku\test>set MACHINE=x86
C:\Zenkaku\test>detect_vc.bat
C:\Zenkaku\test>nmake -f Makefile_vc.make test_stl
C:\Zenkaku\test>
C:\Zenkaku\test>set MACHINE=x64
C:\Zenkaku\test>detect_vc.bat
C:\Zenkaku\test>nmake -f Makefile_vc.make test_stl
C:\Zenkaku\test>

あるいは上記をそのまま実行するようなバッチファイルbuild_test_dbg.batを作っておき、これをダブルクリックしてもよいでしょう。

build_test_dbg.batの全ソースコード (29 lines)

@echo off

REM Define ZNK_VC_DIR, ZNK_VC_WINSDK_DIR, ZNK_VC_UCRT_DIR
if exist "detect_vc_config.bat" call detect_vc_config.bat

set MAK_TGT=test_dbg

REM Build x86 binary (for 32bit/64bit Windows)

	REM Define PATH, INCLUDE, LIB and DET_VC_SUBSYS_VER for VC, target binary x86.
	set MACHINE=x86
	call detect_vc.bat
	
	REM For debug (The pdb file is also created)
	set DEBUG=d
	nmake -f Makefile_vc.mak
pause


REM Build x64 binary (for 64bit Windows only)

	REM Define PATH, INCLUDE, LIB and DET_VC_SUBSYS_VER for VC, target binary x64.
	set MACHINE=x64
	call detect_vc.bat
	
	REM For debug (The pdb file is also created)
	set DEBUG=d
	nmake -f Makefile_vc.mak
pause
Close


明確に指定すべきインポートライブラリ等は今回はありません。 ただしデバッグ版の場合、cl.exe と link.exe に与えるオプションが大きく変わります。

まずcl.exeのオプション部分「/MT /O2 /DNDEBUG」が「/MTd /Z7 /D_DEBUG /RTC1」に変わっています。 次にlink.exeのオプション部分に「/DEBUG /INCREMENTAL:NO」という指定が追加されています。 Makefile_vc.mak内で言えば、変数COMPILERとLINKERの初期化がなされている以下の部分です。

LINKER_OPT_SUBSYSTEM=/SUBSYSTEM:CONSOLE,$(DET_VC_SUBSYS_VER)

!IF "$(DEBUG)" == "d"
COMPILER=cl -nologo -EHsc /GR /W4 /MTd /Z7 /D_DEBUG /RTC1 
LINKER=link /MACHINE:$(VC_MACHINE) $(LINKER_OPT_SUBSYSTEM) /DEBUG /INCREMENTAL:NO 
!ELSE
COMPILER=cl -nologo -EHsc /GR /W4 /MT /O2 /DNDEBUG 
LINKER=link /MACHINE:$(VC_MACHINE) $(LINKER_OPT_SUBSYSTEM)  
!ENDIF

「!IF "$(DEBUG)" == "d"」とある部分は、Makefile(ただしnmake独自仕様)のif文です。 ここで環境変数DEBUG(正確にはそのクローンですが)の値が「d」であるかを調べ、true ならば「!ELSE」とある行の手前までが実行されます。 すなわち以下の部分です。

COMPILER=cl -nologo -EHsc /GR /W4 /MTd /Z7 /D_DEBUG /RTC1 
LINKER=link /MACHINE:$(VC_MACHINE) $(LINKER_OPT_SUBSYSTEM) /DEBUG /INCREMENTAL:NO 

ちなみに環境変数DEBGUの値が「d」以外(典型的には空)のとき、「!ELSE」から「!ENDIF」とある行の手前までが実行されますが、 これが通常版のコンパイルとリンクに相当する指定となります。 すなわち以下の部分です。

COMPILER=cl -nologo -EHsc /GR /W4 /MT /O2 /DNDEBUG 
LINKER=link /MACHINE:$(VC_MACHINE) $(LINKER_OPT_SUBSYSTEM)  

インデントを入れていないので、!IF、!ELSE、!ENDIF の構造がわかりにくいと思いますが、 nmake用のMakefileでは変数代入行の先頭にスペースやタブを入れることができない仕様になっているので仕方がありません。

参考: コンパイルオプション /MT、/MTd、/O2、/Z7、/D、/RTC1
/MTを指定するとlink.exeは裏で libcmt.lib をリンクします。 一方、/MTdを指定するとlink.exeは裏で libcmtd.lib をリンクします。 これらは両方ともC標準ライブラリの実体ですが、前者がリリース向けの最適化されたライブラリ、後者がデバッグ用のライブラリとなっています。

/O2は最適化のためのオプションです。 もっと強力なレベルを指定することもできますがとりあえず/O2としています。 一方、デバッグ版ではこの最適化を行っては困るケースがあるため、これを外します。 その替わり/Z7オプションを指定します。これにより生成されるバイナリにデバッグのためのシンボル情報を含ませることができます。

/Dオプションは直後に指定した名前のマクロを、すべてのソースコードの最初に #define せよという意味と思ってもらって結構です。 ここではリリース向けには NDEBUG マクロを、デバッグ向けには _DEBUG マクロ を定義しておきます。 これは場合によってはヘッダやソースファイルの中などで参照されることがあります。

最後の /RTC1(Run-Time error Checks の略)を指定すると、未初期化の変数を参照した場合などにその場でプログラムを終了してその旨を報告します。 デバッグ版では便利な指定です。

Close


参考: リンカオプション /DEBUG、/INCREMENTAL:NO
/DEBUG を指定するとlink.exeは裏で pdb ファイルを作成します。 またデフォルトでは ilk ファイルと呼ばれるものも作成します(Incremental LinK の略)。 ただし ilk の方は必要ないので、これの生成を抑制するため、/INCREMENTAL:NO を指定します。

pdb ファイルは IDE のデバッガでスタックトレースなどを表示する際に必要となります。


デバッグ版にもなるといよいよオプション指定もややこしくなってきました。 しかし最小限インストールを目指す我々にとって、この後真のラスボスが控えています。 それは link.exe を実行したとき、mspdbsrv.exe という名前のプロセスがきちんと起動するか否かです。 このプロセスは一度起動するとしばらくの間生存しますのでタスクマネージャよりこれが存在するか確認してみましょう。

参考: mspdbsrv.exe
link.exeで/DEBUGが指定された場合、例えばVS2012(VC11.0)の場合ですとlink.exeは内部でmspdb110.dllをロードし、同時にmspdbsrv.exeプロセスを起動します。 名前からしてこれはサーバプロセスの一種でしょう。 さらにmspdbsrv.exe は mspdbcore.dll と mspdbobj.dll を起動時ダイナミックリンクしますので、 これらのある位置が環境変数PATHに適切に追加されていない場合、mspdbsrv.exeの起動に失敗することになります。 このとき、例えばVS2012(VC11.0)の場合、以下のようなエラーが表示されるはずです。

@echo off
LINK /MACHINE:X86 /DEBUG /INCREMENTAL:NO /OUT:test_dbg.exe
Microsoft (R) Incremental Linker Version 11.00.50727.1
Copyright (C) Microsoft Corporation.  All rights reserved.

LINK : fatal error LNK1101: MSPDB110.DLL バージョンが正しくありません。この製品のインストールを再確認してください
NMAKE : fatal error U1077: 'E:\minvs\vs2012_vc11\VC\bin\LINK.EXE' : リターン コード '0x44d'
Stop.

一見するとmspdbsrv.exeというより、むしろmspdb110.dllに問題がありそうなエラーに見えますが、実はそうではありません。 これはmspdbsrv.exeが起動できないことによるものです。

mspdbsrv.exeが一度起動すれば、/DEBUGオプション付きでの link.exeコマンドの実行は成功します。 これはたとえば他のバージョンのVCのmspdbsrv.exeが起動していても構いません。 ですから裏を返せば、他のバージョンのVCのセッティングにおいて、環境変数PATHに mspdbcore.dll と mspdbobj.dll の位置が正しく設定されていなかったとしても、 このmspdbsrv.exeが起動している間は、/DEBUGオプション付きでの link.exeコマンドの実行は成功するわけです。 このため、色々なVCのバージョンでテストする場合、ここは環境変数PATHの設定に不備があることに気がつきにくい厄介なポイントとなります。

mspdb*.pdb, mspdbsrv.exe, mspdbcore.dll, mspdbobj.dll のある場所ですが、VS2012以前はCommon7/IDE内にありました。 従ってこの位置をPATHに追加する必要がありました。 VS2013以降はCommon7/IDE内にこれら4つはなくなり、これらはすべてlink.exeが置かれているディレクトリへと移動しました(妥当な配慮だと思います)。

つまり、VS2013からはCommon7/IDEフォルダはもう丸ごと必要ないので削除してオッケーってことっすね♪

Close


このコードではfunc1関数に至った地点でNULLポインタを参照し、つまりJavaで言う所のNullPointerException、 いわゆる「ぬるぽ(もはや死語ですか?)」ですが、これを意図的に発生させています。 従って、これを実行したときいわゆるセグメンテーションフォールトのダイアログ(これも死語ですか?)が表示されれば成功(?)です。

IDE版のVCを持っている方はJITデバッグ機能により半自動的にデバッガが起動し、ビジュアルにスタックトレース等できると思います。 あるいはそれがなくとも、WinDbgというツールで少し手間ですが、同様にスタックトレース等できると思います。 スタックトレースを表示するにはpdbファイルが作成されている必要があります。 test_dbg.obj以外にtest_dbg.pdbが作成されていることも合わせて確認してください。

目次に戻る

はじめてのダイエット


脂肪除去手術


いよいよ最後です。

インストールしたVisual Studioのディレクトリから余計な贅肉を削ぎ落としダイエットします。 一番楽しい所ですね(私が)。

今回の私のインストールでは、Cコンパイラ本体、Windows Universal CRT SDK など全部合わせて1.5GB程度でインストールされておりました。 ほとんど限界までインストールする項目を絞ってさえこのザマでありますので、ここから先は手動で不要なファイルを削除するしかないわけです (実際にはディレクトリ単位で処理していきます)。

基本的なやり方ですが、まず不要と思われるフォルダ(のアイコン)を右クリックし、「プロパティ」からその総サイズをみます。 これがあまり大したサイズでなければ、削除したところで効果は薄いですからとりあえず無視しましょう。 目安として10MB以下なら最初は無視してしまいましょう(なにせ元が1GBですからね)。 それ以上のサイズのものでしたら、さらに中身を詳細に検討した上、不要なら削除してしまいます。

あるいは不要なディレクトリを本当に削除するのではなくzipにしてしまうという手もあります。 削除するよりはサイズの削減効果は薄くなりますが、万一それが後で必要とわかった場合でも即座に復旧できますからね。 しばらく運用してみて全く問題がないならその後にzipも削除するというのでもよいでしょう。

一体どこまで減らせるでしょうか?

Cコンパイラ本体


まずCコンパイラ本体が格納されているトップディレクトリから見ていきましょう。 このディレクトリはデフォルトでは、C:\Program Files (x86)\Visual Studio\2017\BuildTools となっています。 これを直接弄るのはさすがに怖いという方は一旦別のディレクトリへこれをコピーして、それを弄るといいでしょう。 ただし全体でおおよそ1GB程度ありますので、コピー先にそれ以上のディスク容量が必要です。

  • BuildTools\Common7

  • 10MB以上あります。
    Visual Studio 2017においては不要です。削除しましょう(あるいはzip化します)。
    cl.exe等の基本コマンドが依存するdllはこのディレクトリ内には存在しません。

    ただし他のバージョンのVisual Studio(特に古いバージョンのもの)については、 cl.exe等の基本コマンドが依存するdllが、このディレクトリ内に存在することがあります。 また、デバッグバージョンでコンパイルする際に立ち上がるmspdbsrv.exeというサーバプロセスのファイルが含まれていることがあります。 よってこれらのバージョンのVCではそのようなファイルだけは残して他は削除します。 具体的に書きますと以下のファイルになります。

    VS2008の場合ですと、mspdbsrv.exe, msobj80.dll, mspdb80.dll, mspdbcore.dll dbghelp.dll です。
    VS2010の場合ですと、mspdbsrv.exe, msobj100.dll, mspdb100.dll, mspdbcore.dll です。
    VS2012の場合ですと、mspdbsrv.exe, msobj110.dll, mspdb110.dll, mspdbcore.dll です。
    VS2013以降は存在しません(これらのファイルはlink.exeが置いてあるディレクトリへ移動されているためです)。

  • BuildTools\DIA SDK

  • 10MB以上あります。
    不要です。削除しましょう(あるいはzip化します)。
    cl.exe等の基本コマンドが依存するdllはこのディレクトリ内には存在しません。

    DIA SDKとはDebug Interface Access SDKの略であり、pdbファイルの解析をするためのツールを開発する場合などに用いるものです。 このような解析に強い興味がある変態(褒め言葉)以外は、将来においてもまず使うことはないものです。

  • BuildTools\Licenses

  • たかが2MBです。
    不要ですが、ほうっておいても構いません。

  • BuildTools\MSBuild

  • 10MB以上あります。
    不要です。削除しましょう(あるいはzip化します)。
    cl.exe等の基本コマンドが依存するdllはこのディレクトリ内には存在しません。

    この中に含まれているmsbuild.exe はコマンドライン上から使えるといったようなことが書かれてありますが、 これも結局は内部でcl.exeなどを呼び出している皮を被ったツールに過ぎないため、 最小限のインストールを目指す我々にとっては不要なものです。

  • BuildTools\SDK

  • 1MB以下です。
    不要ですが、ほうっておいても構いません。

  • BuildTools\Team Tools

  • 10MB以上あります。
    不要です。削除しましょう(あるいはzip化します)。
    cl.exe等の基本コマンドが依存するdllはこのディレクトリ内には存在しません。

    中を覗くと「Static Analysis Tools」とありますので、おそらくCコンパイラ本体をインストールしたときに強引についてきたアイツです。 「スタティック分析ツール」という項目がありましたがアレでしょう。

    中に含まれる FxCopCmd.exe がその実体でしょうが、私はこれを使ったことはありません。 IDEに搭載されている「VS Code Analysis」も多分これを呼び出しているのでしょう。

  • BuildTools\VC

  • Cコンパイラ本体が格納されており当然必要です。 これは絶対に消してはいけません。

    以降でこの中をさらに詳しく吟味します。

次に BuildTools\VC 内を検討しましょう。

  • BuildTools\VC\Auxiliary

  • 1MB以下です。
    不要ですが、ほうっておいても構いません。

    Auxiliaryは少し難しい英単語ですが「補助」という意味です。 旧来のバージョンのVCを使っていた方にとってはおなじみだった vcvars32.bat などの環境変数設定用バッチファイルは このディレクトリへ移動したようです(detect_vc.batがあればこれらももはや不要です)。

    またwarnings.hという謎のヘッダファイルがありますが、これがインクルードされることはありません (INCLUDEパスにこれの存在する位置を指定する必要はありません)。

  • BuildTools\VC\Redist

  • 10MB以上あります。
    Visual Studio 2017においては不要です。削除しましょう(あるいはzip化します)。
    cl.exe等の基本コマンドが依存するdllはこのディレクトリ内には存在しません。

    ただし他のバージョンのVisual Studio(特に古いバージョンのもの)については、 同名のディレクトリが、bin, include, lib などが存在するディレクトリと同列の位置にあり、 その中にあるredist\x86\Microsoft.VC*.CRT内に含まれるdllがcl.exe等の基本コマンドにの起動に必要なケースがあります。

    よってこれらのバージョンのVCではredist\x86\Microsoft.VC*.CRTディレクトリのみを残して他は削除します。 具体的に書きますと以下のディレクトリになります。

    VS2008(VC9)の場合ですと、 VC\redist\x86\Microsoft.VC90.CRTです。
    VS2010(VC10)の場合ですと、VC\redist\x86\Microsoft.VC100.CRTです。
    VS2012(VC11)の場合ですと、VC\redist\x86\Microsoft.VC110.CRTです。
    VS2013(VC12)の場合ですと、VC\redist\x86\Microsoft.VC120.CRTです。
    VS2015(VC14)の場合ですと、VC\redist\x86\Microsoft.VC140.CRTです。


    これらは「Visual C++ 再頒布可能パッケージ」に含まれる実体でもあります。 よって必要ならばそこからダウンロードすることもできます。

  • BuildTools\VC\Tools

  • まるで脇役のような名前ですがCコンパイラ本体が格納されており必要になります。 これは絶対に消してはいけません。

    以降でこの中をさらに詳しく吟味します。

次に BuildTools\VC\Tools 内を検討しましょう。 MSVCというディレクトリしかありませんので奥へ進みます。

次に BuildTools\VC\Tools\MSVC 内を検討しましょう。 14.15.27023 というディレクトリしかありませんので奥へ進みます。 (ここでのディレクトリ名は私がインストールしたVCの厳密なバージョン番号になっています。 よってお使いのコンパイラのバージョンによっては異なる名前になっているかもしれません。 以下の説明では便宜上この番号を使います)。

次に BuildTools\VC\Tools\MSVC\14.15.27023 内を検討しましょう。 ここでようやくbin, include, libなどのおなじみのキャラクターが登場します。 BuildTools\VC\Tools\MSVC\14.15.27023\bin などと書くと長いので、以下では単に bin などと表記します。

  • bin

  • cl.exe、link.exe、lib.exe、nmake.exeなどの基本コマンドが含まれており、必要なディレクトリです。 ですがVisual Studio 2017では130MBほどもあります。 他のバージョンのVCではここは約60MBほどで収まっておりますのでその約2倍を消費しています。 きっと何か余計なものが含まれていますので中身を検証します。

    中を覗くとHostx64、Hostx86という二つのディレクトリがあります。 その中についてさらにx64、x86というディレクトリがそれぞれ付属しています。 そのさらに奥でようやくcl.exeが登場します。

    つまり以下の4つが存在しますが、それぞれについてみていきます。
    • bin\Hostx86\x86
    • この中にあるcl.exe自体はx86のバイナリで、それで作れるものも x86のバイナリになります。
    • bin\Hostx86\x64
    • この中にあるcl.exe自体はx86のバイナリで、それで作れるものは x64のバイナリになります。
    • bin\Hostx64\x86
    • この中にあるcl.exe自体はx64のバイナリで、それで作れるものは x86のバイナリになります。
    • bin\Hostx64\x64
    • この中にあるcl.exe自体はx64のバイナリで、それで作れるものも x64のバイナリになります。
    これでおわかりいただけますでしょうか? Hostx64内にはcl.exeそれ自体がx64のバイナリになっています。 しかしこれは不要です。 なぜならcl.exe自体はx86のバイナリであったとしても、それを使ってx64のバイナリは作れるからです(クロスコンパイルと呼ばれます)。 さらに開発用のマシン(Host)として今あなたが64bit版Windowsを使用したとしても、x86のバイナリ(Hostx86側に入ってるcl.exe)は動作します。 つまりHostx86側に入っているものだけですべて事足りることになります。

    ではなんでHostx64なんてものが入ってるのかということですが、 コンパイラそのものがx64のバイナリであるため、64bit環境下ではコンパイル速度が速くなるとかそういった利点があるのでしょう。 ただしよほどの速度が要求されるような特殊な環境下でない限り、その程度の差は誤差の範囲であると私は思います。

    まとめますとbin\Hostx64ディレクトリは不要です。削除しましょう(あるいはzip化します)。
    ちなみにHostx86側にあるcl.exe等の基本コマンドが依存するdllは、このHostx64ディレクトリ内には存在しません。

  • crt

  • たかが4MBです。
    不要ですが、ほうっておいても構いません。

    C標準ライブラリのソースコードが入っています。 場合によっては最強のリファレンスマニュアルとなるでしょう。

  • include

  • STLのヘッダ、C99のstdint.h、stdbool.hなどが含まれており、必要なディレクトリです。 (ただしstdio.hは含まれていません)。

  • lib

  • libcmt.libなどのC標準ライブラリ、あるいはそのデバッグシンボルであるpdbファイルなどが含まれており必要なディレクトリです。

    ただ、総サイズが全部で800MB程度あり、他のバージョンのVCに含まれるlibよりも多くなっています。 きっと何か余計なものが含まれていますので中身を検証します。

    中を覗くとx86、x64、onecore という三つのディレクトリがあります。 x86、x64はわかります。 32bit版Windows向けのアプリを作りたいならx86内、64bit版Windows専用のアプリを作りたいならx64内に含まれるlibファイルとリンクしなければなりません。

    ではonecore とは何でしょうか? 380MBとかなり馬鹿にならないサイズを食っています。 これらはWindowsタブレット、Windowsフォンをもターゲットにしたライブラリであると考えていいでしょう。 一応、こちらを使ってもデスクトップ版Windowsのアプリを作れる(つまりすべてを統一したもの)とMSは謳っておりますが、 現時点ではこれを選ぶのはリスクが高いと私は思います。 やはりデスクトップ版Windowsのアプリを作るならば旧来通り、x86、x64とある方のライブラリを使う方が無難でしょう。

    というわけでonecoreは不要となります。zip化しても90MB以上にもなりますので即削除です! (Windowsタブレットなどを持ってる方はゴメンなさい)。 これでようやく他のバージョンのVCのlibファイルと同等程度のサイズに収まりました(420MB程度)。

    その他、x86、x64内にstoreというディレクトリが含まれています。 storeアプリを作るようなことはないと思いますのでこのstoreディレクトリは削除してもよいですが、 こちらはたいしたサイズではないしzip化すれば3MBに収まるので、そうしておいてもいいでしょう。

  • vcperf

  • 1MB以下です。
    不要ですが、ほうっておいても構いません。


以上でようやくCコンパイラ本体のダイエットが終わりました。 元が1GBでしたがどのくらい贅肉が退いたでしょうか?

実際には上のもののうちどれを削除するかは人それぞれでしょうから、総サイズも人それぞれになるでしょうが、 私の場合、今回のダイエットで全部で650MBに収めることができました。 MinGW(150MB)の4倍程度です(べジータ戦の界王拳程度です)。 これなら1枚のCD-Rにでも焼けるサイズですね。 さらに全体をzip化しておけばもっと減らせるでしょう。

何より便利なのはこれをzip化したものを他のPCへ持っていけば、単にこのzipを展開するだけでCコンパイラ本体の必要なコマンド類がすべて揃うことは勿論、 detect_vc.bat を使えば、ただちにコンパイルとリンク可能な状態にもセッティングできることです。 650MB程度ならかなりコンパクトです。

Windows Universal CRT SDK


続いてWindows Universal CRT SDKのダイエットに入ります。

これのオリジナルのサイズはおよそ500MBです。 Cライブラリだけにしてはやや大きいようにも感じますね。 中身を見てまいりましょう。

Windows Kits\10まで奥へ進むとようやくbin, include, libなどのおなじみのキャラクターが登場します。 Windows Kits\10\bin などと書くと長いので、以下では単に bin などと表記します。

  • bin

  • ucrtbased.dllなるファイルがx86、x64、arm、arm64に対して用意されています。 しかしこのファイルを使うことはありません。 従ってbin全体を削除してもよいですが、zip化すれば2MB程度になりますのでそうしておきましょう。

  • include

  • stdio.hなどのおなじみのC標準ライブラリのヘッダが格納されおり、必要なディレクトリです。

  • lib

  • libucrt.lib(デバッグ版はlibucrtd.lib)がx86、x64、arm、arm64に対して用意されています。 x86版、x64版については必要です。 ですがarm、arm64というのは ARM版Windows向けのものであり、ほとんどの方にとって不要です。 しかも一つあたり120MBと結構なサイズを食っています。

    arm、arm64については削除とします(またはzip化します)。

  • Catalogs

  • 1MB以下です。
    不要ですが、ほうっておいても構いません。

  • DesignTime

  • 1MB以下です。
    不要ですが、ほうっておいても構いません。

  • Redist

  • たかが5MBです。
    不要ですが、ほうっておいても構いません。

    DLLが多く入っていますが、スタティックライブラリ libucrt.lib をリンクする限りこれらを使うことはありません。

  • Source

  • たかが3MBです。
    不要ですが、ほうっておいても構いません。

    C標準ライブラリのソースコードが含まれています(中身超C++ですが)。 場合によっては最強のリファレンスマニュアルとなるでしょう。

以上でWindows Universal CRT SDKのダイエットが終わりました。 元が500MBでしたがどのくらい贅肉が退いたでしょうか?

私の場合、今回のダイエットで250MBにまで減らせることができました。 半分になりましたね(arm, arm64版を除去したのが大きかったようです)。

これもまたzip化したものを他のPCへ持っていけば、単にこのzipを展開するだけでWindows Universal CRT SDKの必要なコマンド類がすべて揃うことになります。 250MBですのでかなりコンパクトです。

Windows SDK


続いてWindows SDKのダイエットに入ります。

ここではv7.1Aを取上げます。 これは元が150MB程度です。 Cコンパイラ本体と比べるとかなり小さく感じますね。

libディレクトリよりもincludeディレクトリの方がサイズが大きくなっています。 includeディレクトリには拡張子が h 以外のファイルも含まれ、それがサイズを食っている感じですがこれは削除しない方がいいでしょう。 C言語の「#include」では拡張子が h とは異なるファイルも強引にincludeできてしまいます。 つまり何がどうincludeされているかファイル名だけでは全貌が判断できないため、不用意に削除すべきではありません。

もっとも、bin, include, libディレクトリのいずれも全体的にそこまで無駄なものは含まれていないように思います。 (細かくみれば不要なものもありましょうが、SDKという性質と全体のサイズのバランスを鑑みた場合、無視できるレベルのものです)。

よってこれについては手をつけずこのまま置いておきましょう。

まとめ


今回のミッションでは、最小限インストールとして以下のサイズにまで絞り込めました。

  • Cコンパイラ本体
  • 650MB
  • Windows Universal CRT SDK
  • 250MB
  • Windows SDK v7.1A
  • 150MB

トータルでは1050MB、つまり約1GBですね。 当初の約2/3程度までは減らすことに成功しました。 (ま…まあ、今回はこのくらいにしといたらぁ!)

保管しておくならCコンパイラを1枚目のCD-Rに、Windows Universal CRT SDKとWindows SDK v7.1Aをセットで2枚目のCD-Rにといった所でしょうか? 余ってるCD-Rがある方はお試し頂いてはどうでしょう? 長くなりましたがここまで読んで頂いた方はお疲れさまでした。

目次に戻る

This article was written by:
none image

Zenkaku

@znk project