poky & yocto

最近项目组在准备开发基于 arm的版本,于是涉及到了交叉编译和交叉构建,前者很简单,只需要安装相应的arm-gcc即可。
可惜生活往往不是如此简单,出于某些目的,我们需要构建基于ubuntu 中的源码和包管理机制构建的版本,也就是说,我们需要制作arm版本的ubuntu rootfs,之所以不能直接使用ubuntu core for arm,是因为某些时候可能需要修改某些包的源码,所以需要同时搭建一个交叉编译和构建包的环境。

也就是一个 src -> binary -> package的过程

Yocto

记得在实验室的时候,用bitbake这个工具编译过arm下的用户态程序,于是google一番发现,bitbake原来是一个叫做Yocto的工具下的一个python写的工具集,用于将源码交叉构建为目标硬件的二进制程序(甚至软件包),遂大喜,下了最新的yocto之后,根据官方的 Quick Start迅速构建了一个img

不过转念一想,既然要基于ubuntu的源码,那么yocto中所带的源码则显然无法使用了,因为yocto虽然也能制作deb包,不过和ubuntu中的版本完全无法兼容,并且ubuntu对上游源码所打的一些补丁yocto也没有集成,因此可能需要对yocto进行一番改造。

放弃

参考了yocto的文档后,我们发现,yocto实现deb包的机制和通常的debian/ubuntu构建流程不太一样,后者使用dpkg-buildpackage来构建特定的包,而yocto则采用了自己实现的机制(lib/pm.py利用dpkg apt等工具实现了自己的打包流程)这导致其构建包的流程几乎不透明了,而要通过修改yocto来达到生成特定版本ubuntu兼容的deb包,则可以预见包含比较大的工作量。

故此,yocto的路线暂停,而这里的主要问题是,ubuntu的rootfs是一个基于二进制包的img,我们只需要找到一种方法,能够将上游源码构建为某个版本apt能够识别、安装的包即可,有了这些包,构建rootfs就再轻松不过了

sbuild

在yocto的方案受挫后,我们发现,当前主要的问题在于如何将交叉编译生成的二进制文件打包成软件包,经过一番搜索后,cross-build映入了我们眼帘。在看完这篇这篇这篇后,总结出一个结论:妈蛋交叉构建还是个坑啊,大家要构建最好把源码传到launchpad上啊,我们建议大家构建的时候用最新的工具链哟,也欢迎搭建帮忙一起测试sbuild和那些个坑爹的package 维护者挖的坑 (逃

不过第一篇文中,同时也表示业界在crossbuild的泥潭里正缓慢的前行,那么之前的ubuntu for arm版本又是如何构建出来的呢??下面来解答!

crossBuild node

这里就要请出launchpad了,根据这篇文章,可以得知,ubuntu基本是采用launchpad的分布式构建节点来制作软件包的,并且他们还是用的最慢的本地构建方法(也就是利用qemu,在x86_64的虚拟机上模拟arm环境,然后用arm架构的工具构建软件包),事实上,launchpad的这篇文章也是这么说的。

既然如此,那我们何不自己搞一发本地构建?

于是跟随着这篇guide 我们搭建了一个 host,build,target都是armhf的chroot环境(前面的教程搭建的是基于amd64的sbuild chroot,只需要在mk-sbuild和sbuild的时候,将–arch=armhf加入命令行即可),然后就可以轻松的在amd64下构建arm软件包了

buildd

问题

用sbuild构建了一些包之后,发现,我们的日志和launchpad上的build.log并不一样,launchpad似乎使用了一个叫做buildd的工具来进行自动化的构建,google一番之后,发现了这篇文章,原来launchpad利用wanna-build buildd sbuild构建了一套自动化构建环境,buildd周期性的检查upload上来的源码包,而wanna-build则维护了一个包含各个软件包在各个架构上的构建状态的数据库,buildd通过数据库来选择是否重新构建(如果该包当前状态是未成功构建或超过包的保质期)或者忽略本次构建(包已经构建成功并且在保质期内),而最终的构建工具,则是sbuild

还是sbuild

最终真相大白,虽然我们可能没有资源架设openstack集群来进行分布式构建,但是只要采用和launchpad一样策略:使用sbuild构建各个架构的软件包,也是毫无问题的。

最后,送上利用sbuild从零构建arm等架构软件包的官方教程

参考文献:

server如何运作

bb/server/process.py中,定义了当Yocto采用多进程B/S架构时,server进程的启动方式:

  • start_server(),在bin/bitbake中,包含了一个start_server()函数,该函数根据命令行参数,实例化相应的server对象,并且调用serverdetach函数,这个函数则调用了server对象的start()函数
  • run():在bb.server.ProcessServer类中,存在一个run函数,该函数设置了一些UI事件,并且调用了bb.cooker.server_main(),该函数接受两个参数,第一个是一个cooker实例,第二个是一个可执行的函数,Yocto中将self.cookerself.main作为这两个参数,由于ProcessServer类继承于Process类,因此在调用该类的start()方法时,run()会被自动调用,因此在调用server.start()时,实际调用的是server_main()函数
  • server_main():该函数执行一些预处理任务(bb.cooker.pre_serve()),然后调用传进来的函数并且返回其返回值:
1
2
3
4
5
6
7
8
#__file__ = 'bitbake/lib/bb/cooker.py'

def server_main(cooker, func, *args):
cooker.pre_serve()
#something else
ret = func(*args)
cooker.post_serve()
return ret

而这里的func,即是上面传进来的bb.server.ProcessServer.main,因此调用server_main()实际上是调用了ProcessServer类的main()函数

  • ProcessServer.main():该函数会执行一个重要的while循环:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#__file__ = 'bitbake/lib/bb/server/process.py'
def main(self):
# Ignore SIGINT within the server, as all SIGINT handling is done by
# the UI and communicated to us
self.quitin.close()
signal.signal(signal.SIGINT, signal.SIG_IGN)
while not self.quit:
try:
if self.command_channel.poll(): # 检测是否有命令数据
command = self.command_channel.recv()
self.runCommand(command)
if self.quitout.poll():
self.quitout.recv()
self.quit = True
# 若无数据可读,执行注册的idle命令
self.idle_commands(.1, [self.event_queue._reader, self.command_channel, self.quitout])
except Exception:
logger.exception('Running command %s', command)

self.event_queue.close()
bb.event.unregister_UIHhandler(self.event_handle.value)
self.command_channel.close()
self.cooker.shutdown(True)

在其中不断的从两个管道中读取数据,一个管道为命令管道,这个管道两头连接着uiserver,这样server就可以接受来自ui的命令,并把执行结果返回给ui;另一个管道为异常管道,当其他模块在产生不可恢复的异常后,会向这个管道发送'quit'消息,接收到该命令后主循环直接退出;在检查完这两个管道后,主循环调用idle_commands(),并设置0.1秒的延时,用于等待几个管道的数据

  • idle_commands:该函数调用register_idle_function函数注册的idle函数,这个函数在bb.Command.runCommand()中,通过
1
self.cooker.configuration.server_register_idlecallback(self.cooker.runCommands, self.cooker)

这段代码注册,可以看到,注册的函数为bb.cooker.runCommands,然后该函数调用这个注册的函数,如果未找到注册函数,则调用select.select()等待0.1秒后返回。

  • bb.cooker.runCommands:该函数就是被注册的idle函数,他会被server主循环周期的调用,而该函数的实际内容,则是调用bb.command.Command.runAsyncCommand来执行一个已经就绪的异步命令
  • bb.command.Command.runAsyncCommand:该函数会判断当前cooker从状态,而分别调用updateCache()函数或者调用command对象的currentAsyncCommand成员函数,这个函数会在多种情况下被赋值为某个函数对象和其参数组成的元组(command, options),当该函数被调用时,则会执行在currentAsyncCommand注册的函数,而updateCache()则会为启动其他的任务,例如parse
  • currentAsyncCommand的赋值:currentAsyncCommand只会在command.runCommand函数中被赋值,而command.runCommand函数,则会在server对象的runCommand()中被调用,server.runCommand()的调用,则出现在ui端的main()中唯一一次主动调用server的代码,这样,即是在ui端的main函数中,启动了

依赖关系如何解析

代码位于bb.runqueue.RunQueueData.prepare()函数中的注释的PART A部分和内嵌函数generate_recdeps

bb文件如何解析

入口位于bb.cooker.updateCache()函数中,该函数中有如下代码:

1
self.parser = CookerParser(self, filelist, masked)

这段代码初始化了一个CookerParser对象,这个对象的构造函数中,调用了self.start(),因此这段代码直接启动了bb文件的解析,具体的start()函数代码在bb/cooker.py中的CookerParser类中

UI端如何运作

由于在Yocto中,服务进程先于UI启动,因此第一次执行命令需要通过ui传递给server,而ui的入口函数,则是位于lib/ui/ui_module_name.py文件中的main()函数,根据采用的不同的ui模块(默认采用knotty.py),main函数有不同的行为,这里以knotty.py中的main作为例子进行分析

  • bb.ui.knotty.main 这个函数为ui端的入口函数,最核心的代码为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#__file__ = 'bitbake/lib/bb/ui/knotty.py'

if not params.observe_only:
params.updateFromServer(server)
params.updateToServer(server)
cmdline = params.parseActions()
if not cmdline:
print("Nothing to do. Use 'bitbake world' to build everything, or
run 'bitbake --help' for usage information.")
return 1
if 'msg' in cmdline and cmdline['msg']:
logger.error(cmdline['msg'])
return 1

ret, error = server.runCommand(cmdline['action'])
if error:
logger.error("Command '%s' failed: %s" % (cmdline, error))
return 1
elif ret != True:
logger.error("Command '%s' failed: returned %s" % (cmdline, ret))
return 1

这段代码,通过params.parseActions()从用户调用的bitbake <target>命令,解析出一个cmdline字典,其中的action键是一个列表,其中包含了要运行的命令的字符串格式,要构建的目标<target>和构建的cmd(默认为build),例如:cmdline[action]=["buildTarget", "zlib", "build"],就意味着即将要运行的命令为buildTarget,构建目标为zlib,cmd为build;而msg键对应了需要传送给server端显示的消息,当命令行参数解析到不合适的内容时,则会发送给服务器结束命令,关闭uiserver进程。
如果没有出错,通常的第一个action都是buildTarget,这个action随后被作为参数,传给bb.server.ServerCommunicator.runCommand()函数,该函数调用服务端的函数bb.server.ProcessServer.runCommand来执行命令

  • bb.server.ProcessServer.runCommand:该函数将上面action中的命令数据通过bb.cooker.command.runCommand()进行处理,并将返回值通过管道发送给ui端,这也是唯一一次ui端显式的调用server的函数。

  • 各种event的处理:在bb.ui.knotty.main()中,存在着一个while循环,该循环读取服务端的管道,并根据服务端返回的命令执行结果和状态执行相应的代码,或者关闭服务端,或者继续发送命令。

buildTarget

bitbake/bb/cooker.py中,有一个buildTarget函数,该函数为在无任何参数的bitbake命令时的服务端入口,例如:

1
$ bitbake zlib #target 为 zlib

这是服务端会调用buildTarget作为如何,该函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def buildTargets(self, targets, task):
"""
Attempt to build the targets specified
"""

def buildTargetsIdle(server, rq, abort):
msg = None
if abort or self.state == state.forceshutdown:
rq.finish_runqueue(True)
msg = "Forced shutdown"
elif self.state == state.shutdown:
rq.finish_runqueue(False)
msg = "Stopped build"
failures = 0
try:
retval = rq.execute_runqueue()
except runqueue.TaskFailure as exc:
failures += len(exc.args)
retval = False
except SystemExit as exc:
self.command.finishAsyncCommand()
return False

if not retval:
bb.event.fire(bb.event.BuildCompleted(len(rq.rqdata.runq_fnid),
buildname, targets, failures), self.data)
self.command.finishAsyncCommand(msg)
return False
if retval is True:
return True
return retval

self.buildSetVars()

taskdata, runlist, fulltargetlist = self.buildTaskData(targets, task, self.configuration.abort)

buildname = self.data.getVar("BUILDNAME")
bb.event.fire(bb.event.BuildStarted(buildname, fulltargetlist), self.data)

rq = bb.runqueue.RunQueue(self, self.data, self.recipecache, taskdata, runlist)
if 'universe' in targets:
rq.rqdata.warn_multi_bb = True

self.configuration.server_register_idlecallback(buildTargetsIdle, rq)

可以看到,这个函数做了以下几件事:

  1. 定义了一个内嵌函数buildTargetsIdle,看名字可以得知,该内嵌函数会作为idle函数被注册到server中,周期的被调用
  2. self.buildSetVars()用于设置一些和BUILDNAME,BUILDTIME等变量
  3. buildTaskData用于生成任务数据,其中包括taskdatarunlist,和fulltargetlist;其中,taskdata是一个bb.taskdata.TaskData类的实例,这个对象中包含了和该任务相关的信息,例如依赖,任务名等,runlist则是该任务的各个目标的名称和对应的task,并以列表的形式进行存储,例如["base-files","do_build"]就代表了目标base-files,其task为do_build,而fulltargetlist则是所有target的列表
  4. 通过rq = bb.runqueue.RunQueue(self, self.data, self.recipecache, taskdata, runlist)来构造一个RunQueue实例,为随后的build工作做好准备
  5. 将定义的内嵌函数注册为idle回调函数,使其被周期地调用,因此,我们需要分析该函数的实现:

buildTargetsIdle

  1. 根据上面的代码,该函数主要执行了rq.execute_runqueue()函数,该函数位于bb/runqueue.py中,而execute_runqueue()又调用了_execute_runqueue(),而_execute_runqueue()的实际工作,是根据runqueue的实际状态,进行不同的行为:

    • runQueuePrepare态,调用bb.runqueue.RunQueueData.prepare(),这个函数是很相当长的函数,主要行为包括:
      1. STEP A:解析出一个需要执行的任务列表,包括解析依赖
      2. STEP B:标记所有需要执行的任务
      3. STEP C:去掉不需要执行的任务
      4. STEP D:检测并确定最终的需要执行的任务列表
      5. 进入runQueueSceneInit状态
    • runQueueSceneInit状态,调用runqueue.start_worker()启动,启动工作进程,并构建一个RunQueueExecuteScenequeue对象,将状态设置为runQueueSceneRun
    • runQueueSceneRun状态,调用RunQueueExecuteScenequeue.execute(),该函数会将准备好的task依次运行,随后,将状态设置为runQueueRunInit
    • runQueueRunInit状态,会构造一个RunQueueExecuteTasks对象,然后将状态设置为runQueueRunning
    • runQueueRunning状态,会调用RunQueueExecuteTasks对象的execute()函数,该函数会执行在上面的RunQueueData状态中准备的task,并进入runQueueCleanUp状态
    • runQueueCleanUp状态,调用RunQueueExecute.finish()函数,并将状态设置为runQueueComplete
    • runQueueComplete状态,销毁worker,然后该函数返回

载入cache的入口

入口函数是bb/cache.py中的load_cachefile()函数

run.do_xxx 脚本如何生成

bb/build.py中,存在exec_func函数,该函数运行的某个函数,将会在build/tmp/work中创建run.do_xxx.pid名称的脚本,并运行它

如何生成image

yocto在构建完成所有的软件包后,会将所有构建的软件包放在${TMPDIR}/deploy目录下,称之为软件源,在启动构建rootfs的活动(名为do_rootfs的task)后,将会执行三个函数:

  • create_manifest() 构建软件包的manifest用于test image,并且生成一个package列表为create_rootfs()函数提供需要安装的软件包列表
  • create_rootfs() 构建rootfs文件系统,包括执行pre_cmd,安装所需软件包,构建/etc ,/dev等目录,构建内核模块,运行ldconfig等,完成rootfs的构建
  • create_image 根据image的压缩类型和文件系统类型,制作一个或多个image

##介绍
pelican是基于python的静态web站点生成器,由python编写
目前最火热的静态站点利器jekyll则由ruby编写,出于对python的爱,我于是选择用pelican

  • 文档详见pelican,本文基于pelican 3.5.0版本
  • 源码
  • 特性:
    • 支持markdown,html和rst
    • 支持各种主题theme
    • 支持插件
    • 代码高亮

##gitcafe pages
gitcafe pages是类似于github pages的服务,不过国内访问速度更良心

###WHY gitcafe
github大法好,不过国内的访问速度令人蛋碎,当然,如果是海外党,可能恰好相反,不过如果有米,当然最好能够买一个域名,然后通过CNAME将国外和国内IP分别引导到github pages和gitcafe pages,具体做法可以参见该文

##QuickStart

  1. 在centos 7下,首先安装python,pip,virtualenv(可选,virtualenv可以将各种库,插件和主题打包到一起,比较方便)

     yum install -y python python-devel python-libs python-pip
    
  2. 随后安装pelican和markdown库,如果需要建立虚拟环境(virtualenv),则可以参见该文

     pip install pelican markdown 
    
  3. 建立一个存放博客的目录,并进入目录,取名’waaagh’(绿皮万岁)

     mkdir -p waaagh
     cd waaagh
    
  4. QuickStart,运行:
    pelican-quickstart,根据提示,可以快速生成一个静态页面的生产环境,例如:(输入不支持backspace键,不过输入错误可以在随后生成的pelicanconf.py文件中修改,直接按回车则是取默认值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
(blog)[root@localhost waaagh]# pelican-quickstart 
Welcome to pelican-quickstart v3.5.0.

This script will help you create a new Pelican-based website.

Please answer the following questions so this script can generate the files
needed by Pelican.


> Where do you want to create your new web site? [.] .
> What will be the title of this web site? waaagh!!!
> Who will be the author of this web site? lichaoran
> What will be the default language of this web site? [en] zh
> Do you want to specify a URL prefix? e.g., http://example.com (Y/n) yes
> Do you want to specify a URL prefix? e.g., http://example.com (Y/n) yes
> What is your URL prefix? (see above example; no trailing slash) pkking
> Do you want to enable article pagination? (Y/n) y
How many articles per page do you want? [10]
▽ Do you want to generate a Fabfile/Makefile to automate generation and publishing? (Y/n) y
> Do you want an auto-reload & simpleHTTP script to assist with theme and site development? (Y/n) y
> Do you want to upload your website using FTP? (y/N) n
> Do you want to upload your website using SSH? (y/N) y
> What is the hostname of your SSH server? [localhost]
> What is the port of your SSH server? [22]
> What is your username on that server? [root] pkking
> Where do you want to put your web site on that server? [/var/www]
> Do you want to upload your website using Dropbox? (y/N) n
> Do you want to upload your website using S3? (y/N) n
> Do you want to upload your website using Rackspace Cloud Files? (y/N) n
> Do you want to upload your website using GitHub Pages? (y/N) y
> Is this your personal page (username.github.io)? (y/N)
Done. Your new project is available at /root/blog/blog

完成后,目录结构如下:

 yourproject/
├── content
│   └── (pages)
├── output
├── develop_server.sh
├── fabfile.py
├── Makefile
├── pelicanconf.py       # Main settings file
└── publishconf.py       # Settings to use when ready to publish

##push一篇博文
通常,我们将content目录作为存放文章源文件的目录,pelican支持rst,markdown和html文件。
不管3721,先撸一篇markdown文章吧:

1
2
3
4
5
6
7
8
9
Title: 我的第一发博客
Date: 2015-01-01
Category: Python
Tags: pelican, publishing
Slug: 第一篇博客
Authors: lichaoran
Summary: Hello World

hello world!

接下来,解释一下上面的文件内容:

  • 以:隔开的key-value键值对可以成为元素局(metadata),他们构成了一些文章的基础属性,例如日期,标题,摘要等,具体的元数据可以参看pelican文档
  • 正文和metadata以空行隔开

写好文章后,将其命名为hello_world.md(.md为markdown源文件的后缀名),然后在project根目录运行pelican /path/to/your/content/ [-s path/to/your/settings.py],其中,/path/to/your/content即是存放文章源文件的目录,刚才我们使用了content目录,该目录的名称可以在pelicanconf.py中配置,甚至,输出目录output都可以用其他配置文件代替,配置文件pelicanconf.py也可以是其他的配置文件,只需要指定path/to/your/settings.py即可。

TIPS:
写好文章后,利用刚才的命令,就已经生成好页面到output目录了,这时可以利用make serve命令启动一个本地服务器,通过访问localhost.com:8000来访问生成的页面

##主题
pelican支持各种主题,这里有各种主题及其下载链接,主题的安装和配置可以使用pelican-theme工具,具体方法参见pelican-theme --help

##配置pelican
配置文件pelicanconf.py包括了众多选项,可以参见该页进行配置

##git端的配置
在生成好第一篇文章后,可以进入到output目录,这里的内容就是即将托管到gitcafe pages的静态页面,首先,到gitcafe.com建立一个user pages或者project pages,方法参见官方帮助文档,简化下来的步骤就是:

  1. 在gitcafe.com中建立一个和用户名相同的repo
  2. 根据刚建立的空repo首页,将git username和email配置为相应的数据(在github中,非验证邮箱和用户名会导致pages build failure,不知道gitcafe是否有一样的机制)
  3. output目录,依次运行
1
2
3
4
5
6
git init #初始化仓库
git checkout -b gitcafe-pages #建立制定分支,pages只会渲染该分支中的页面
git add -A #添加修改
git commit -m"init the blog" #提交
git remote add gitcafe git@gitcafe.com:pkking/pkking.git #pkking替换为你的gitcafe用户名
git push gitcafe gitcafe-pages #将提交push到gitcafe
  1. OK,一切就绪,访问pkking.gitcafe.io查看渲染好的页面吧
0%