软件开发过程中常常需要搭建各种环境:开发环境、测试环境,集成构建环境等等。一个不可复制的环境是低效的根源,它引起的常见问题比如:
- 产品只能在你的机器上编译通过
- 产品在你机器上运行正常, 可在测试环境中总是出错
- 新加入一个项目成员,需要一天时间来为其建立开发环境
- 把测试环境和集成环境迁移到另外一台服务器上花了几天时间
这些问题的原因以及解决方案,在最新出版《 The Productive Programmer 》(卓有成效的程序员)中,Neal Ford 给出了详细的介绍。我们列举几种细化的方案,作为书中提到的“间接”、“规范性”等原则的实践。
试图解决的问题:环境的各个部分散落在不同角落,不是少了这个就是少了那个,或不同机器上版本不一样;环境配置依赖全局环境变量或属性,硬编码的绝对路径等。
目标:机器环境虽然各有各的不同,但依然有可能创建一个“环境无关的环境”。
1. 使用相对路径代替绝对路径
关键是如何获得当前路径,如何确定根路径,如何确保目录结构。
获得当前路径
- Windows 和 Unix 都有内置的环境变量来表示当前路径,分别是
%cd%
和$PWD
- Windows 批处理脚本中,还可以使用
%~dp0%
获得脚本所在路径 - 而在 Unix Bash 脚本中,则可以使用“
pwd
”,即获得pwd
命令的输出 - Makefile 中, 也可以获得 shell 命令的输出, 比如
this_dir = $(shell pwd)
- Ant 脚本中, 内置的 basedir 属性缺省代表的是脚本文件所在的路径
%cd%
和%~dp0%
的区别:
%cd%
是脚本运行时的当前工作路径,与脚本所在位置无关%~dp0%
则相反,是脚本所在路径,与运行时的工作路径无关
举例来说,有如下内容的D:\project\run.bat
:
echo %cd% echo %~dp0%
如果在D:\
盘根目录下运行project\run.bat
,则输出如下
D:\>project\run.bat D:\ D:\project\ {1}
一般来说%~dp0%
比%cd%
更常用
下面是一个环境无关的 makefile 的例子(只列出了变量定义部分):
PROJ_ROOT=$(shell pwd) INCLUDE += $(PROJ_ROOT)/include LIB += $(PROJ_ROOT)/lib
当然也可以根本不定义变量表示当前路径,而直接以相对路径的形式引用子目录,和通过“…”来引用父目录及兄弟目录,然而显式的变量定义提供了一层间接,你可以通过多种方式覆盖它的缺省值,从而适应不同的环境。参见后面的“缺省值 + 用户自定义属性”。
确定根目录
如果总是需要引用某个根路径的话,则可以使用环境变量来定义根路径(参见后面的环境变量)。其实相对路径配合固定的目录结构,大大削减了对显式定义根路径的需求。
前面 makefile 的例子可以改写如下:
ifeq "$(origin PROJ_ROOT)" "undefined" PROJ_ROOT = You_should_firstly_specify_$${PROJ_ROOT}_in_your_environment_or_by_command_line endif INCLUDE += $(PROJ_ROOT)/include LIB += $(PROJ_ROOT)/lib
保证目录结构的固定,自然是使用配置管理系统。
2. 使用配置管理系统 (版本控制系统)
这是一个“规范性”或者“标准化”的问题。配置管理系统不只是放源代码的,只要有配置管理需求或配置管理能带来好处,或需要有唯一的官方来源,都可以使用配置管理系统来管理;环境的配置文件,环境本身,都可以置入配置管理之下。有些公司的版本控制系统只放源代码,连测试代码都分开另放,耗费很多精力来维护源代码之外的文档。
- 强制使用配置管理可解决固定的目录结构的问题
- 使用配置管理还可以解决丢三落四的问题
- 自然也可以解决所用工具版本不一致的问题
这里有几个常见的问题:
- 大型的系统软件如何处理,比如 JDK、VC++ 编译器等,是否也需要置入版本控制:这个基本不用,如果对其特定的版本有需求,可在脚本中加入检查其版本的逻辑,不满足则提示并退出即可
- 依赖的大量二进制库如何处理:如果有必要可使用依赖管理工具如 Maven,Ivy 等,而用于存放依赖的本地仓库依然应该置入配置管理,哪怕不用其版本控制功能,而只是利用其官方来源 / 备份存档等好处
- 必须放在特定位置的文件如何处理,比如某个文件必须放在
/etc
目录下:这个文件还是可以放在配置管理下的目录中,而在/etc
下创建符号链接来指向它;并提供脚本来干这件事。
3. 环境变量
必要时使用环境变量来引用环境。全局的环境变量可用作缺省值,在脚本中覆盖它(基本上,这是“用户自定义属性”的一个实例)。
4. 缺省值 + 用户自定义属性
这是创建“环境无关的环境”的核心机制。无论如何,环境要在不同机器上部署,总会需要修改某些配置,以适应宿主机器。然而前面我们提到,所有配置都已置入版本控制。如果我们直接修改,则每个环境中都会存在未提交的本地修改。这是我们不希望看到的,因为当我们升级配置并更新到所有部署时,可能会产生冲突。这里其实是两个层面的问题:
- 提供一种机制,当环境与缺省配置不一致时,允许用户修改
- 用户修改的文件应避免与官方文件更新的冲突
先说第一个。
缺省值当然可以直接定义在脚本或配置文件中。而多数常用的脚本和配置系统都提供了用户定义属性覆盖缺省值的机制。比如:
- Windows 批处理:
set path=my_extra_path;%path%
- Bash:
export PATH=my_extra_path:$PATH
- Ant:
<property file="user.properties"/> \<!-- user.properties 中可定义任何后面引用到的属性,以覆盖其缺省值 -->``<property name="src.dir" path="${basedir}" /> <!-- 定义"src.dir"的缺省值 -->
- CruiseControl:
<property name="src.dir" value="." /> \<!-- 定义"src.dir"的缺省值 -->``<property file="user.properties"/> <!-- user.properties 中可定义任何后面引用到的属性,以覆盖其缺省值 -->
- 注意 Ant 的属性是只读的,先入为主。CruiseControl 的属性则是后发制人。
- Makefile 则可以直接在命令行覆盖文件里面定义的缺省属性。如覆盖前面例子中的
PROJ_ROOT
:
make PROJ_ROOT=/home/mike/project
再说第二个。
有一个很简单的解决办法,就是把用户自定义属性置入单独的文件,并且不要把它提交到版本控制系统中(一个理由是这一部分相对整个组织来说,不存在也不需要唯一的官方来源)
前面例子中的user.properties
就是一个用户自定义属性文件,只存在每个用户自己的机器上,不在配置库中。在 Windows 和 Bash 脚本中也可以类似处理:
Windows: call user_env.bat
Bash: source ./user_env.sh
或. ./user_env.sh
随之而来的一个问题是,这个用户相关的文件应该放在何处。这里其实约定好就可以了,比如当前路径,根路径,甚至 user 的 home 路径都可以。
参考借鉴
- 至此,这套东西在不同的机器上部署时,只要从版本控制系统中 check out 出来即可使用了。更进一步,还可以提供脚本,即生成器,自动探测用户的环境,来生成全套配置,类似 Rails 生成应用框架那样。
- 其实,这是一个规范性或标准性的问题。Neal Ford 在《卓有成效的程序员》中,用一章的篇幅详述了各种解决方案,包括配置管理,符号链接等。除此之外,他还建议可以 / 应该使用虚拟机来统一项目组的开发环境等。参见《卓有成效的程序员》第五章。
- 另请参阅《 CruiseControl Enterprise 最佳实践 (3) : Configuring CruiseControl the CruiseControl way 》,是创建环境无关的持续集成环境的实例。
作者简介
李光磊,软件工程师,同时还是一位敏捷教练,就职于 ThoughtWorks。他还是活跃的 blog 作者,了解他最新的想法,请访问 http://blog.csdn.net/chelsea 。
相关阅读
[ ThoughtWorks 实践集锦(1)] 我和敏捷团队的五个约定。
[ ThoughtWorks 实践集锦(2)] 如何在敏捷开发中做好数据迁移。
[ ThoughtWorks 实践集锦(3)] RichClient/RIA 原则与实践(上)、(下)。
[ ThoughtWorks 实践集锦(4)] 为什么我们要放弃Subversion 。
[ ThoughtWorks 实践集锦(5)] “持续集成”也需要重构。
[ ThoughtWorks 实践集锦(6)] Mock 不是测试的银弹。
给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。
评论