Making Your Libraries Release-Friendly
更友好的发布你的功能库
作者:Sobeit Void
翻译:skywind
原文:http://www.gamedev.net/reference/articles/article2006.asp
内容摘要
今天我写这篇文章是因为我遇到许许多多开发者们在发布他们自己的静态/动态库的时候没有注意到以一种足够“Friendly”的风格来最大可能的减少开发时候碰到的麻烦。功能库的使用者们必须经常做出更多的无益修改,但是这些修改都是没有必要的,然而今天,这一切都将会结束!
接下来的内容将仅针对VC++的类库做讨论,但是其中所涉及到的内容,同样适用于任何C++的编译器。
基本法则
我们从检查整合一个第三方库的所需要基本使用步凑开始,假定这个需要整合的第三方库的名字叫做“Useful”。
1. 拷贝 Useful的头文件和.lib文件到我们的机器中
2. 在我们的工程中 include这些 Useful的头文件
3. 将 Useful的.lib文件加入到连接项中
4. 如果 Useful是一个DLL,拷贝useful.dll到我们工程目录中或一个恰当位置(比如windows\system32)
如果所有的工作都顺利完成,我们的工程就可以愉快的使用 Useful了。有什么地方出错了呢?很多地方,真的。
不好的类库发布
让我们来一起检查这个典型的非常常见的类库发布包,看看我们将会碰到什么问题。
下面是Useful的打包发布状态:
l 头文件都位于 Useful\include。既然头文件很少有 debug/release版本,那么遵从微软的约定是很好的
l 库文件的两个版本位于 Useful\lib\debug和 Useful\lib\release,他们都称为 Useful.lib。将库文件放置在 lib目录是另外一个 Microsoft的约定。
l 两个不同版本的DLL(debug/release)都称作 Useful.dll,他们位于 Useful\bin\debug和 Useful\bin\release两个目录。bin文件夹是另一个标准的目录用于编译器输出二进制结果。
现在我开始将Useful整合到我的工程中。Useful是非常 “useful”的,因为我在五个其他工程里面都使用了它。
注意:VC++中的工程配置就象和 makefile一样,实际上,它正是。
1. 我在编译器的 include search path中增加 Useful\include以便我可以直接在五个不同的工程中使用。万一我将 Useful的目录移动到另一个地方的话(比如从c:\Useful到d:\Useful),我仅需要更改编译器的include搜索路径就可以使我的五个工程毫无麻烦的找到这些头文件了,不错。
2. 对于连接类库来说,这里开始出现问题了。我想让我的debug代码连接debug版本的库文件,release也一样。因为debug/release两个版本都称作 Useful.lib,我不能同事增加他们的目录到编译器的 library搜索目录。编译器不能区分到底该连接哪个。所以唯一的解决方法就是写死 c:\Useful\debug\lib到我的 debug工程配置中,以及 c:\Useful\release\lib到我的 release工程配置中。
3. 不幸的是我必须移动 Useful到一个其他的目录,看看我有多大一堆工作需要做。我不许更改我五个工程的library路径配置。如果你独自工作,这没有问题;如果你和以组开发者一起工作的话,看看将会发生什么。所有安装 Useful在其他目录的(这样做并没有什么过错),当每个开发者从仓库中checkout代码的时候,所有工程的路径配置都必须更改。当他们checkin这些代码的时候,代码同步会检测到这些工程配置的修改情况,并且会对这些改动完成checkin操作,下一个工作者又必须在他checkout以后再次更改这些路径设置。听起来很好笑吧,几乎是很艰辛的。
4. 现在,同样的问题在使用DLL的时候出现了,系统再次无法检测哪个dll是debug哪个又是release,所以我们无法将 Useful.dll放到一个公共的路径中去。(实际上你能做到,如果你希望每次交换 debug/release两个版本的DLL的话)所以我拷贝 Useful.dll分别到我的 debug/release目录中,所以系统可以找到最靠近我执行文件的那个dll,但是这只能适用于一个项目,我需要在五个不同的工程中这么做。
现在,Useful的开发者找到了一个主要的BUG,发布了一个新版本的Useful。我们就必须拷贝新的DLL到每个工程的输出目录。如果Useful发布到另外一个目录(比如c:\UsefulFixed而不是 c:\Useful),我们又必须每次为每个工程配置做一次更改。
通过这些,你可以发现这些问题将在一个项目的Lifetime中一直存在。类库更新了,安装目录更新了,所有的问题将会惯性的发生,我们可怜的开发者们将准备好每次应付这些问题的发生。
微软的解决方案
这里是微软如何完成他们的类库发布的,你可以在你自己的VC++目录中得到验证:
l 头文件被防止到 Useful\include文件夹,include名称只是一个约定,不一定要遵守。
l 库文件被防止到 Useful\lib。Debug版本需要在名称后加一个”_d”,所以我们在c:\Useful\lib中分别得到 Useful_d.lib和 Useful.lib两个文件。
注意,该方法解决了连接问题。在我们的代码中你可以这样连接 Useful库:
#ifdef _DEBUG
#pragma comment( lib, “Useful_d” )
#else
#pragma comment( lib, “Useful” )
#endif
然后我们可以增加 c:\Useful\lib到我们编译器的 library搜索目录。
l Debug版本的DLL同样增加一个”_d”,我们得到 Useful_d.dll和 Useful.dll
这两个都可以同时被放入一个普通的文件夹,尽管微软经常放到windows\system32目录中去。这并不是一个很好的做法,除非是系统DLL。作为开发,你不必这样做,因为开发者通常需要最安装新版本的 DLL。作为一个每天都使用他的用户,这将导致版本冲突最终导致”DLL HELL”。
需要牢记的是,如果你需要发布一个应用程序,那么将你所需要的DLL放到你的本地文件夹里面。你可以使用一个叫做”Dependency Walker”的工具看看你需要哪些DLL。
最终结论
以上的更改对于一个类库的编译过程是很简单的同时不会影响到类库的开发。总之,给你的发布包一点微小的改变,你可以节约成千上万使用你类库的人的时间。
(讨论精选)
Interesting article but I think you missed out a couple of useful facts.
* Try not to put paths into the Directories option in VC++. Instead there is a per build setting option in Project Settings, C/C++, Additional Includes. There is also one for libs under link. This way if Useful comes with a lib\debug\useful.lib and a lib\release\useful.lib you can just link in useful.lib and path to the correct one. This becomes invaluable when doing cross platform. Imagine having debug/release libs of a bunch of xbox, pc, gamecube and ps2. #pragma comment (lib, x) won't work on gamecube or ps2 (as far as I know), even if it did you would have a mess of pragmas for just 1 library, now think about if there were 12 libs.
* Make your includes and libs, where possible, relative paths. If you have your game in a folder called game, have Useful at the same level. This way your path to it would be ..\useful\lib\release\. This works for all so long as they have Game and Useful at the same level.
* If the above isn't an option consider your team subst'ing drives. We subst a drive 'R' to the Renderware install and a drive 'H' to the havok install. This lets people install them to wherever they want without breaking project settings. You can then just path to 'R:\include' for the Renderware includes.
Anyway, that's just my preferences. Hopefully the article will get people thinking think a little more when it comes to distributing their libs.
> But one question I would have for you guys, is how do you
> organise your project solution, source, libs, dlls, etc...
> and what about external libraries ?
As you pointed out, there are no right or wrong answers here and it depends on what product you are building and a lot about team consensus. Where I worked, the main product had around 11 million lines of code. Granted, there was a large amount of comments in there, but even at a 1:1 comment/code ratio that's a pretty huge code base to handle. The product was composed of 92 libraries, 8 of which were developped externally. The product was declined in SGI/Linux/Win32 flavors and in the Debug/Retail/Profiling compilations and in Retail/Demo versions for a total of 18 permutations. We used RCS to align all the libraries under the main product directory and used hard links for libraries that were duplicated across products. We didn't use TMP or such directories because there was one build machine per compilation configuration and it pulled the sources on the local hard disk before starting the build process. Also, we didn't make any difference between external and internal libraries; those that came compiled were inserted in the RCS tree and version-stamped. The libraries were compiled first in .lib/.a files and a final makefile would build the various executables that made up the product. There was a final phase that grabbed executables along with various assets and made the CD images; libraries and header files were 'exported' into a package any developper would pull in for the day's work. Then smoke tests were performed using the product's internal scripting ability, and then QA would perform ad-hoc and regression testing to determine if the build 'passes'.
> To make sure noone screws up, we have this "Build Nazi" dude.
As you can imagine, coordinating a big team around this product was essential; and we wanted to have a full product compiled each night and ready for use the next morning. We used the concept of a "build breaker" where the ones who screwed up were responsible for ensuring that compilations were fixed and restarted, then coordinate QA in testing the product; that usually took the entire day, thus introducing a negative feedback loop in the product development cycle for those loose guns in the team. After 5 build breaks within a period of 3 months, salaries/options/bonuses were revised downward. I guess we didn't have someone 'big enough' in the team... but it worked.