My favorite feature in leiningen 2

Recently, the leiningen team has released a preview version for leiningen 2. It brings new features to the clojure build tool. But my favorite one is not listed in any document. So I would like to share with you here.

As you know, leiningen is a project oriented tool. It manages dependencies for a particular project. In Java world, Maven and Gradle are also working in this manner.

In contrast, there are tools which manages dependencies in a system scope. For instance, pip(python), npm(nodejs) and gems(ruby). One advantage of these tools is easy for evaluating a library. Concretely, when you want to test pyclj, just run “pip install pyclj” to install it. Then open a REPL and type “import pyclj”. That’s pretty easy.

But in clojure/leiningen, to take a tutorial of “core.logic”, we have following steps:

  1. Find a right directory and type “lein new logic-abc” to create a project.
  2. cd into it, edit project.clj, add core.logic as a dependency
  3. Run `lein deps`
  4. Start a REPL and follow the tutorial

So I guest you must have a lot of empty projects created for such purpose.

Now we could say goodbye to this situation. Leiningen 2 has move its dependency management core to a new library, called pomegranate. Pomegranate wraps aether, which is a maven library created by sonatype. With pomegranate, we can add a maven artifact from repository to REPL classpath. Still on the core.logic example, it becomes much easier:

$ lein2 repl
Welcome to REPL-y!
Clojure 1.3.0
    Exit: Control+D or (exit) or (quit)
Commands: (help)
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
          (sourcery function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
Examples from clojuredocs.org:
          (clojuredocs name-here)
          (clojuredocs "ns-here" "name-here")
nil
user=> (use '[cemerick.pomegranate :only (add-dependencies)])
nil
user=> (add-dependencies :coordinates '[[org.clojure/core.logic "0.6.8"]])
{[org.clojure/clojure "1.3.0"] nil, [org.clojure/core.logic "0.6.8"] #{[org.clojure/clojure "1.3.0"]}}
user=> (use  '[clojure.core.logic])
nilWARNING: == already refers to: #'clojure.core/== in namespace: user, being replaced by: #'clojure.core.logic/==
user=> (run* [q] (== q 1))
(1)

Now core.logic is right on you classpath and you are ready to use any functions under the namespace. There’s no need to create project, no need to care about where the jars stored. Just start a REPL at anywhere you want. When you finished, send EOF to the REPL. Nothing to clean up.

My jython dependency manager jip has similar feature as I described above. It does great help to me. So I have been waiting for this feature in leiningen for a long time. Thanks to leiningen guys, it finally comes.

Edit 20120323 21:39

If you want to load libraries from clojars, you should explicitly add clojars in add-dependencies:

(add-dependencies :coordinates '[[incanter "1.2.3"]]
                  :repositories (merge cemerick.pomegranate.aether/maven-central
                                       {"clojars" "http://clojars.org/repo"}))

(The example is copied from Pomegranate document.)

Grails的核心依赖必须保证项目中版本一致!

好久没写语录式大标题了,实在是今天玩Grails受害很深很深。

Grails因为本身是通过ivy来管理依赖的,虽然后续的版本和maven的集成不断加深,但是本身还是通过ivy来解析pom.xml。也就是说最后的活还是ivy干的。如果你的项目比较大,选择了通过maven来管理项目,而你的依赖中又牵扯了很多其他的依赖,那么你危险了。

:: UNRESOLVED DEPENDENCIES ::

:: log4j#log4j;1.2.16: configuration not found in log4j#log4j;1.2.16: ‘master’. It was required from …;1.7.0 runtime

对maven用户这是一条多么发指的报错!(比如这个

它的原因其实是你的dependency中有!=1.2.16版本你的log4j,比如1.2.12,即使你在pom里将它添加到了exclusion中也没有用,必须要保持版本的一致!至于你用什么办法让他们保持一致,还是干脆放弃用maven管理你的grails项目:It’s up to you.

同样的问题还出现在其他核心依赖上,例如commons-pool。

jip 0.2 released

As you may know, jip is a dependency management tool for Jython/Java development. It resolves and downloads Java packages from maven-compatible repositories. jip also follows some best practices, encouraging you to use a portable and standalone environment (virtualenv) for your Jython development.

It has been four months since the initial release. In version 0.2, I made following changes for you:

  • Improved console output format
  • Correct scope dependency management inheritance
  • Snapshot management, alpha
  • Environment independent configuration
  • Bug fixes

You can find the typical usage doc on github:
https://github.com/sunng87/jip

jip-0.2 has been published to pypi so you can install or upgrade it with:
easy_install -U jip

Please do remember to use it inside virtualenv!

包管理与路径管理

现在几乎每一种语言都有一些依赖管理工具,或者是中央的包仓库。比如这些:

  • Java: maven, ivy, gradle
  • Ruby: gems
  • Python: easy_install, pip
  • Clojure: leinigen
  • Groovy: maven, grape, gradle
  • Hashkell: cabal
  • PHP: pear
  • Nodejs: npm

这些工具在管理包和路径时都会采用各不相同的策略,有的是通过自身实现,有的是借助语言平台本身的特点。

Java

其中最注明的Maven,它的方式是在POM文件中定义你的依赖,Maven会在本地仓库中维护这些依赖。Maven的本地仓库默认是在$HOME/.m2/repository目录下,是用户独立的,当然要下载一个依赖也不需要root权限。而在通过Maven运行Java项目时,Maven插件会自动管理classpath,你并不需要把这些依赖从本地仓库里拷贝出来而单独维护一个所谓lib目录(这样也不好管理)。这是Maven的方式,目前看来,这也是最经典的一种方式。除了maven本身的特性以外,这也和Java的classpath机制有关。

最近势头很猛的Java构建工具gradle,方式也与Maven类似,它会把下载的依赖存放在$HOME/.gradle/cache目录里,并自动管理classpath。

同样是Java,与maven相对应的是ant+ivy。ivy可以为你管理依赖,但是ivy不会帮你管理classpath。ivy的包管理,是以project为scope的,你需要维护一个lib目录来存放这些下载的包,再通过传统的ant的方式去管理classpath,从而使项目可以进行编译和运行。

在Java世界里,还有一个特别的工具叫做grape,它是专门用于groovy的轻量级的依赖解决方案。grape是以脚本为scope,在需要依赖的脚本中通过@Grab声明依赖,grape工具可以从maven仓库中下载依赖到$HOME/.groovy/grapes中,并把相关的依赖加入groovy的classpath。除此以外,grape还有一个命令行工具帮助你手动下载依赖到本地仓库。grape的内部是基于ivy的,不过它的方式比ant要自动化很多。

在Java世界里还有一个特例,是Clojure的依赖管理工具leiningen。leiningen本身也比较简单,它的方式与ivy相同,会解析project.clj文件中定义的依赖关系,并下载到当前的工程目录下的lib中。lein是鼓励通过uberjar的方式把依赖统统打包的,所以它并没有classpath的管理功能。

总体来说,Java世界的工具和Java是相似的,其最大特点就是System independent,安装包不需要root权限,每次的运行都需要管理classpath。作为开发人员,classpath中有哪些可以访问的类库是可以控制的,这也使Java程序的移植性得到良好的控制和管理。

Python

与Java不同,Python通常作为系统分发的一部分,他的包管理和PATH管理要相对混乱一些。通常我们有两种方式来安装一个Python的软件包:

  • sudo apt-get install python-redis
  • sudo easy_install redis

一种是通过系统的包管理工具(如apt-get)从系统的软件仓库里安装,一种是通过python自己的包管理工具(如easy_install / pip)从Python Cheese Shop中下载安装。这两种安装方式有什么不同呢。以Ubuntu为例,通过apt-get安装的python包通常会被放在 /usr/share/pyshared 或 /usr/lib/python2.6/dist-packages 中,相对应的,由easy_install安装的Python包,则存放在 /usr/local/lib/python2.6/dist-packages 中。Python启动后可以通过查看sys.path来了解当前的path情况。

除了安装到系统目录,easy_install可以通过 –user 选项来把软件包安装到用户目录 $HOME/.local/lib/python2.6/site-packages。不过无论是系统级别还是用户级别,python都很难在启动时管理Path,即任何时候python都可以访问安装在系统中的所有软件包。这导致了混乱的情况,导致编写的python软件难以进行依赖管理和移植(即使没有定义在setup.py中,很多依赖还是可以访问的)。

由此virtualenv营运而生,virtualenv帮助你创建一个独立的python运行环境。激活这个小环境之后,easy_install/pip仅仅安装软件到小环境,python仅能访问环境内部的site-packages,这样整个环境中的依赖关系就非常清楚,也保障了程序的移植性。这样,就将原本系统scope的python包管理级别改进为项目级别。我之前写的jip也是将依赖下载或拷贝到virtualenv的小环境中,并且修改jython的启动脚本修改PYTHONPATH的设置,保证Java依赖对Jython的透明可访问。

Nodejs

nodejs是一个新兴的生态系统,一个包管理工具对其也是必不可少。npm是目前整个社区都比较认可的工具。

不过目前npm并不好用。npm默认会把自己安装到 node安装前缀的目录,比如node安装时你选择了默认前缀/usr/local,那么npm会把自己安装到/usr/local/lib/node里。这个目录是系统级别的,所以需要root权限,而npm本身又不鼓励用户用root权限来安装软件包(安全问题)。所以作者说希望用户把/usr/local/lib/node权限授予用户,或者把node安装到用户目录里。这两种方式其实都不太优雅。

Ruby的gems在这方面最符合unix哲学,即用户知道自己在做什么。如果用户以root权限运行gem install,gem会把软件包安装到系统目录中对所有用户可用,而如果以普通用户权限运行,则安装到用户目录 $HOME/.gem 中仅当前用户可见。

nodejs在加载软件包时,会在require.paths中的几个目录里查找,前两个都是用户目录,所以npm也并非一定要把包安装到系统目录里去。虽然现在可以用过修改.npmrc文件在修改npm的默认行为,不过在这个CoC的时代,显然太繁琐了。

Best Pratice

总结一下,包管理和路径管理的最佳实践应该是:语言平台有CoC的路径机制,包管理器有基于环境变量、用户权限的判断执行合适行为。

LM Tooling, 2010

阿哈,大家都开始总结2010年了,我也想找个角度来总结一下今年。三句不理老本行,还是聊软件开发周边的事情。Lifecycle Management Tooling,软件生命周期管理的工具和我一年的感受。

一月份的时候我还有大把的业余时间,在做一个叫做Yan的验证码服务。这个项目从我刚一到盛大工作就开始了,不过因为没有特别的需求,最后变成了自己的开源项目。这个项目用Maven做dependency management,用hg做版本控制,代码放在bitbucket上。它对Maven的使用还很基本,是一个Single Module的小项目,因为服务本身已经实现了插件机制,本来我准备把它拆成Multimodule的Maven工程来着,不过后来因为比较忙,这件事就放掉了。那时我已经开始尝试用hg进行分支管理、每个release打tag,甚至还有一段时间会有一些两个分支的并行开发。这些基本上是实验性质的尝试,不过和后来相比也算是做的比较规范的了。

三月份我注册了github,顺带就开始用git了。到目前为止,我对hg和git的区别还没有特别的感受,选择的主要依据其实bitbucket和github。相对social一些的项目,github是当仁不让的选择。bitbucket上人少比较清净,一些incubator项目放在上面,烂尾了也不至于太丢脸。扯远了,哈哈。

之后我们开始做一个规模比较大的项目,我们使用Maven,使用Nexus,使用SVN。项目规模比较大,我们有数个Multimodule的Maven项目,遇到的第一个问题就是集成的成本非常大。最初几周,我都是在自己的机器上做集成,Windows,你懂的,又不太方便做自动化,结果整个过程非常痛苦。到了四月份项目还没有全部完成,已经不能忍了,于是我引入了Hudson。我们为每一个项目创建单独的build task,unstable的构建会有邮件通知开发人员。一方面把我从每天反复的手动集成打包中解脱出来,另一方面也促进了大家写高质量的UnitTest,因为一旦有测试不通过就会有一个虚构的200000000@snda.com的邮箱给大家发邮件,以至于最后几乎所有人都把这个地址拉黑了。

写真正能够Automation的单元测试其实很不简单,尤其是DAL层。理想的solution是用内存数据库来做单元测试的数据源,不过在实际开发里,因为一些SQL会依赖特定的数据库,所以这个方案并不太理想。如果有事务的话,Spring的TestCase可以设置自动回滚,可是互联网项目,你也懂的,连一致性都搞成最终一致了,事务这么重的东西我们是不用的。

我们遇到的另一个问题是,多个独立的Maven项目,在做集成时,会存在依赖的冲突。这类问题一旦出现,找原因的代价还是比较大的。这方面,老大有一个Best Practice,定义一个统一的、专门管理版本的pom,通过dependency management的import来使用,你可以看这里来了解。

我们suffer的另一个问题是SVN的分支管理,到了项目的后期,功能越来越多,也出现了两个分支的并行开发,回滚的情况也出现了。我们基本上可以保证每次发布都有tag,但是分支的管理实在是非常痛苦。我现在的认识是,两方面原因。第一,SVN的分支成本还是比较高的,远程整个目录拷贝一下,本地需要指向新的地址。第二,项目的计划不明确,根本没有办法去估计新的分支、新的版本。两个原因结合起来说,其实分支管理不完全是一个版本控制系统层面的问题,和整个项目规划都有一定的联系。

到了后期,我们对Hudson的使用也有了一些最佳实践,结合ssh/scp的插件,task的trigger机制,我们有了自动化的部署,你可以在这里了解。不过我们的自动化最后倒在公司强制的动态密码验证机制上,每次登录需要使用动态密码,这样一切最后又都回归了手动。

除了在公司的项目,在9月份我又做了一个实验性的项目,叫做Bason,他可以在编译时生成JavaBean到Mongodb BSON代码。也是用Maven管理,不过值得一提的是,我申请了Sonatype提供的OSS Repository,这样可以把项目都deploy到公共的服务器上,使用会非常方便。还值得一提的是,OSS Repository的管理者是校友@Juvenxu

不过今年Maven并不是Java生命周期管理的唯一工具,还有一些工具也开始进入视线,当然我说的不是ant+ivy。前段时间发布0.9的Gradle是比较受关注的,它又回归了ant的task oriented ,结合Groovy的DSL和插件机制提供更高的灵活性。另外,断断续续学了一年的clojure,clojure的生态系统里已经有了一个叫做leinigen的生命周期管理工具,在clojure的世界里,它非常好用。

另外Python的世界,我逐渐开始强制自己用virtualenv来创建一个更可控,更具有移植性的python环境。随着年底开始的一个小项目,我应该对python项目的生命周期管理有更深的认识。

年底到了SAP,开始在NetWeaver平台上做Abap开发。NetWeaver是一个复杂的平台,它的Scope也涵盖了生命周期管理。在NetWeaver中,开发一个非local的程序,需要首先创建一个Change Request,之后所有的变更,最后都会保存到这个ChangeRequest上,有些类似传统的分支。当开发完成后,开发人员把change request release掉,这部分代码会自动被transport到Test系统或者其他下游系统上。实际开发里,一个Change Request可能对应一个Ticket,一个特性。这样的平台上,开发人员需要额外操心的事情,非常少。