Barrelfish研究——分析hake
上一篇讲了Barrelfish的环境搭建。在搭建过程中,我们注意到一些步骤和通常编译Linux源码包的方法有不同之处。
类比
通常我们编译Linux源码包的步骤基本上都是先configure再make。这种包的源码管理是用的autotools。另外一种常见的源码管理工具是cmake。编译的方法是先cmake再make。以上的两种方法中,不论是configure还是cmake,它们的目的无非是进行一些平台相关的设置,源码结构的配置,然后生成相应的Makefile。有了Makefile之后,源码管理工具的任务到这里就完成了。后面的步骤基本一致。即执行make,批量(一般是用gcc)编译源码。
Barrelfish的编译过程不同在,其既不是执行configure也不是执行cmake,而是执行了hake.sh。执行过程中,也可以看到漫长的扫描过程,然后会在build目录下生成一个庞大的Makefile文件(大约十几MB)。后面同样也是执行make的过程。
在这个过程中,可以体会到hake.sh的角色,大概同autotools与cmake是相同的,即为生成Makefile。分析hake.sh
执行hake.sh,打印所支持的选项如下:-s|--source-dir: path to source tree (required)
-i|--install-dir: path to install directory (defaults to `pwd`)
-a|--architecture: specify archtitecture to build for (can be
given multiple times, default architectures are
"x86_64"
-n|--no-hake: just rebuild hake itself, don't run it (only useful
for debugging hake)
阅读hake.sh,它主要做了以下几项工作:
第一
echo "Setting up hake build directory..."
if [ ! -f hake/Config.hs ]; then
cp $SRCDIR/hake/Config.hs.template hake/Config.hs
cat >> hake/Config.hs <<EOF
-- Automatically added by hake.sh. Do NOT copy these definitions to the defaults
source_dir = "$SRCDIR"
architectures = [ $ARCHS ]
install_dir = "$INSTALLDIR"
EOF
else
echo "You already have Config.hs, leaving it as-is."
fi
通过Config的模板Config.hs.template创建一个新的Config.hs文件,并在文件末尾添加三个变量。这三个变量的值,当然是来于在自hake.sh的参数。
第二
if [ ! -f ./symbolic_targets.mk ]; then
echo "Creating new symbolic_targets.mk file."
cp "$SRCDIR/hake/symbolic_targets.mk" .
else
echo "You already have symbolic_targets.mk, leaving it as-is."
fi
创建一个新的symbolic_targets.mk文件
第三
ghc -O --make -XDeriveDataTypeable \
-package ghc \
-package ghc-paths \
-o hake/hake \
-outputdir hake \
-i$SRCDIR/hake \
-ihake \
$SRCDIR/hake/Main.hs $LDFLAGS || exit 1
终于看到了ghc了。有人会问,为什么编译Barrelfish需要ghc,它在其中起什么作用?答案就在这里:ghc的作用就是编译一些hs为后缀名的Haskell源码,生成一个名为hake的程序。
第四
if [ "$RUN_HAKE" == "No" ] ; then
echo "Not running hake as per your request."
exit
fi
echo "Running hake..."
./hake/hake --output-filename Makefile --source-dir "$SRCDIR" || exit
注意hake.sh文件的第四个参数-n。如果你设置了这个参数,那么表示仅编译生成hake,并不执行hake。那么这个hake的程序做了哪些工作呢?
分析hake
上面说到,hake是用Haskell语言写的,其main函数所在文件名为Main.hs。如果你了解Haskell语言,那是再好不过的了。不过,如果不熟悉也没有关系,我们并不需要读懂它的每一行代码如何工作,只要知道它的大概功能就可以了。(Haskell语言是一个很值得学习的函数式编程语言,如果你未曾接触过函数式编程语言,花一点时间去了解一下Haskell是很有意义的!)
打开Main.hs文件,直奔末尾找到main函数
main :: IO()
main = do
-- parse arguments; architectures default to config file
args <- System.Environment.getArgs
let o1 = parse_arguments args
al = if opt_architectures o1 == []
then Config.architectures
else opt_architectures o1
opts = o1 { opt_architectures = al }
if opt_usage_error opts then do
hPutStrLn stderr usage
exitWith $ ExitFailure 1
else do
-- sanity-check configuration settings
-- this is currently known at compile time, but might not always be!
if isJust configErrors then do
hPutStrLn stderr $ "Error in configuration: " ++ (fromJust configErrors)
exitWith $ ExitFailure 2
else do
hPutStrLn stdout ("Source directory: " ++ opt_sourcedir opts)
hPutStrLn stdout ("BF Source directory: " ++ opt_bfsourcedir opts)
hPutStrLn stdout ("Install directory: " ++ opt_installdir opts)
hPutStrLn stdout "Reading directory tree..."
l <- listFilesR (opt_sourcedir opts)
hPutStrLn stdout "Reading Hakefiles..."
hfl <- readHakeFiles $ hakeFiles l
hPutStrLn stdout "Writing HakeFile module..."
modf <- openFile ("Hakefiles.hs") WriteMode
hPutStrLn modf $ hakeModule l hfl
hClose modf
hPutStrLn stdout "Evaluating Hakefiles..."
inrules <- evalHakeFiles opts l hfl
hPutStrLn stdout "Done!"
-- filter out rules for unsupported architectures and resolve relative paths
let rules =
([(f, resolveRelativePaths opts (fromJust (filterRuleByArch rl)) (strip_hfn opts f))
| (f,rl) <- inrules, isJust (filterRuleByArch rl) ])
hPutStrLn stdout $ "Generating " ++ (opt_makefilename opts) ++ " - this may take some time (and RAM)..."
makef <- openFile(opt_makefilename opts) WriteMode
hPutStrLn makef $ preamble opts args
-- let hfl2 = [ strip_hfn opts (fst h) | h <- hfl ]
hPutStrLn makef $ makeHakeDeps opts $ map fst hfl
hPutStrLn makef $ makeMakefile rules
hPutStrLn makef $ makeDirectories rules
hClose makef
exitWith ExitSuccess
大约50行左右的代码,看懂它,就是hake所做的工作啦。
如果你用的编辑器不错,可以看到以"--"开头的行被高亮显示(vim和gedit均可),这些行是的注释。"hPutStrLn stdout"开头的行表示在终端打印。忽略这些代码。前十行的功能主要是参数的处理,得到源码路径和安装路径。然后调用listFilesR函数,它的输入是一个目录的路径,然后递归扫描其中所有的文件,输出这些文件完整路径的列表。将这个列表传递给hakeFiles函数,过滤出以"/Hakefile"结尾的文件。(回想那个hello world例子程序中的Hakefile文件)然后将这些Hakefile输入到readHakeFiles函数,即可得到每个Hakefile文件的内容。再调用hakeModule函数,对这些内容做整理,输出到一个名为Hakefiles.hs的文件中。(如果觉得有点晕了,不妨停下来,看看build目录下是否有一个Hakefiles.hs文件。打开看看,会发现,这个庞大的Hakefiles.hs文件有点类似与每个子目录中Hakefile内容的一个大汇总。)现在看到evalHakeFiles,这个函数会调用runghc,以脚本方式运行Hakefiles.hs,生成的结果是一个HRule的列表。HRule类型的定义在HakeTypes.hs文件,用来表示hake自己定义的一些语法规则。再后面hPutStrLn makef开头的几行代码,就是往Makefile中写内容了。这些内容包括一下预先定义好的preamble、编译的依赖关系、以及那些HRule解析的结果。总而言之,这时,一个完整的Makefile就生成了。
大功告成,稍微整理一下思路,想一想hake到底是一个什么东西。它是用Haskell语言写的一个程序,这个程序分析Barrelfish源码的结构,从中找出每一个子目录中的Hakefile,这些Hakefile包含一些编译选项,由hake定义的一套语法规则写成。汇总它,解析它,生成一个巨大的Makefile。把后面编译Barrelfish的任务就交给make和gcc来完成,hake就可以功成身退了。