大项目SDK跨Linux平台编译成功经验总结

在工作中,我们可能会遇到一些项目的SDK(源码)是在很古老的Linux环境下开发的,因为历史的原因,这个项目一直没有进行开发环境的移植。而如果要继续在这个古老的Linux下开发,要么重新购买服务器硬件(费钱),要么在自己的电脑上装这个系统(慢,费时)。解决方案当然是把它移植到装有现代Linux环境的服务器下(资源合理利用)。

本人在三周的时间内,把一个带有Linux嵌入式操作系统的电信终端SDK从原本32bit的CentOS编译环境移植到当代Ubuntu(18.0和20.0)编译成功,可以生成终端正常工作的烧录文件(即程序)。带来的收益是,从本地电脑全编译一个版本需要近2个小时缩短到不到20分钟(服务器硬件资源丰富)。

要完成这个工作当然不容易,这个项目SDK大小就有4个多G(含源文件,配置文件,中间文件),光Makefile. configure等文件也有几百个。完全编译一次(make)输出的消息(不加-d选项的情况下)有7万多条。在整个过程中,我把解决的问题全部记录下来。并整理出如下方法论共享给大家。

一. Host Linux系统准备工作

首先,尽量保证你的当代Linux操作系统拥有最新的各种软件版本,这个虽然非必须,但是是使用Linux系统的常规套路。这个可以通过运行$sudo apt update来查看有哪些软件包可以更新,运行$sudo apt upgrade 加上软件包名称来对现有软件进行升级。

先要保障编译所需工具链已经装好,编译Linux内核所需软件可以通过如下命令获取:

$sudo apt-get install build-essential vim git cscope libncurses-dev libssl-dev bison flex

在大多数情况下,使用最新版本(例外我在后面会具体阐述)。我们编译需要用到GNU make编译工具组件,包括make, automake, autoconf, libtool等等。幸运的是,经我实测,这些软件有很好的向下兼容性,也就是原来的项目可能是用老版本的GNU make组件编译的,切换成最新的组件,不会对编译结果产生影响。

二. 建立特定目录存放编译所需外部程序,库及头文件等

建一个特定的目录,如在/opt/下,用于放置适用于本项目SDK编译的程序,库,头文件等。如编译所使用的特定的gcc版本. 建好并且安装好所需软件后,要修改原有SDK里配置文件(Makefile等)的对应路径。如在Makefile_Main文件里的PATH:

export PATH :=/opt/trendchip/mips-linux-uclibc-4.9.3/usr/bin:$(PATH)

(1) 这里export PATH表示仅在运行make的时候编译程序会把这条加入到PATH中,而不是永久性地加入Linux操作系统的环境变量中。这个可以通过在Linux操作系统运行$echo $PATH查看。

(2) PATH:= xxxx:$(PATH),这个在GNU Make里称为简单扩展变量,支持在赋值的时候在原值基础上附加新的值。因为PATH的查找是从左到右,所以编译程序在查找需要用到的程序时,先从最左边的路径开始查找。在上面的例子里是先查找/opt/trendchip/mips-linux-uclibc-4.9.3/usr/bin,再查找原有PATH(可能就是Linux操作系统缺省的或用户设置的)的路径。所以如果编译过程需要用到和操作系统缺省程序不一样的版本,则要安装在这个特定的目录下。

在本项目的SDK中,有很多文件的很多地方都需要引用到这个目录的内容,可以用Linux的sed命令来进行批量替换,而不需要手动修改。

三. 分情况处理编译需要的外部程序

项目的SDK编译需要用到很多外部程序。这里可以分几种情况:

(1)一些程序要调用到与Host操作系统缺省自带的库文件,如在/lib, /usr/lib中。在移植后,这些库文件相差巨大,有一些很老的库文件网上可能都无法找到,还有的可能和新的操作系统会产生冲突,无法移植过来。这个时候只能尝试用新的版本去替代老版本。比方说GNU的awk(即gawk)就是这种情况。详情请见我的另外一篇博客gawk: error while loading shared libraries: libreadline.so.4解决方案

(2) 有一些程序,对于相同的输入,它的老版本和新版本运行结果有差异,直接导致编译输出偏差,甚至报错。如:

In file included from asp/asp_handler.c:7:0:
asp/mini-asp.h:103:5: error: conflicting types for ‘gb_parse’
grammar.tab.h:83:5: note: previous declaration of ‘gb_parse’ was here

 当看到*.tab.h这种类型的文件时,应马上联想到Bison,即GNU收编的一种广泛使用的parser程序。它通常把*.y文件转换成*.tab.h和*.tab.c文件。在asp/Makefile里,可以看到:

grammar.c grammar.h:grammar.y
         rm -f grammar.c grammar.h
          bison -d -p gb_ $<
          mv grammar.tab.c grammar.c
          mv grammar.tab.h grammar.h

这里有点绕的是,这个时候去查看asp目录下,只有grammar.h文件,没有grammar.tab.h文件,因为这个时候编译程序已经把它mv成grammar.h了,但是报错的时候还是报grammar.tab.h的错误。查找gb_parse在两个头文件中的定义,得到如下:

$ grep gb_parse apps/public/boa-asp/src/asp/grammar.h

int gb_parse (void);

$ grep gb_parse apps/public/boa-asp/src/asp/mini-asp.h

int gb_parse (void*);

也就是在同一个目录下,出现了两个同名函数但是参数类型不一致,这在C语言里是不允许的。gb的原始输入在asp/grammar.y文件里。因为Bison版本不同会导致不同的解析输出。所以,要达到SDK的编译结果,需要使用原SDK中的bison版本,如果它原SDK没有带bison, 则要查找同时代的bison,把它安装到方法二中的特定目录里

(3)有一些外部程序,版本不同语法规则会发生变化。原SDK的源码是依据老的程序版本的语法规则写的,在新的版本程序下运行会出错。这个时候我的原则是尽可能使用新版本程序,对原SDK的源代码进行修改。比方说Perl module解析出错。

Can't use 'defined(@array)' (Maybe you should just omit the defined()?) at /home/minipc/econet/apps/public/samba-4.0.24/pidl/lib/Parse/Pidl/ODL.pm line 73.

Compilation failed in require at ../pidl/pidl line 411

上面的ODL.pm是perl module,73行如下:

if (defined(@$podl)) { 

这里podl是一个数组,在老的perl语法规则下,上面的语句返回podl这个数组是否为空,但是在新版本perl的语法规则下,defined(@array)总是返回true,因为数组在声明的时候其元素的个数就已经定义了。

解决方法是把源代码中的defined去掉,

if (@$podl) { 

即可以达到源程序的目标。 

四. 修改Makefile等配置文件中带有的本SDK顶级目录的名称

Makefile等配置文件中通常都有包含文件路径,这些路径如果以绝对路径的形式来表述,则会包括当前编译环境下的路径名称,如下:

apps/private/tr64/libupnp-1.6.22/ixml/Makefile:abs_top_srcdir = /home/minipc/cucc/apps/private/tr64/libupnp-1.6.22

apps/private/WSC_UPNP/libupnp-1.3.1/upnp/doc/Makefile:AUTOMAKE = ${SHELL} /home/minipc/cucc/apps/private/WSC_UPNP/libupnp-1.3.1/config.aux/missing automake-1.16

 其中,/home/minipc/cucc是该SDK在本编译环境下的顶级目录,apps是其中的一个一级目录。当进行平台移植编译时,新的路径下该顶级目录也要相应变化。用$find | xargs sed 在指定类型的配置文件里进行批量自动替换。

五. 删除在原平台编译过程中产生的某些中间文件

大家都知道C程序在编译过程中会产生*.o文件,这不属于中间文件,因为它是最终程序运行所需要的目标文件。这类文件在运行$make clean时会被删除,从而让运行$make时进行全量编译。但是还有一类文件,它在编译过程中产生,但并不被最终程序所需要。它存储了编译过程中的环境变量等信息,这样可以节省下一次编译所需要的时间。这些文件在SDK进行编译平台移植时可能需要主动删除,因为环境变量发生变化了。

如,SDK使用了GNU autoconf,它调用了autotool,它又调用了autom4te,中间会产生autom4te.cache目录,存储包含有本地环境变量的一些文件,可以加快编译时生成configure, Makefile文件的过程。在移植到新平台后,直接把这些目录删除。详情请见我的另一篇博客autom4te.cache简介及可能引发的问题。

还有,SDK如果使用了CMake, 在首次运行cmake的时候将在build tree的顶部目录产生一个CMakeCache.txt文件,存储了相关的本地环境变量。在进行编译平台移植后,需要将这些文件全部删除。详情请见我的另一篇博客CMakeCache.txt引发的CMake Error解决方案

六. 运行autoreconf重新生成configure和makefile文件

大家都知道稍微大一点规模的C项目都需要configure文件进行配置,Makefile进行复杂编译过程变量设定与管控。为了方便这些文件的生成,GNU有自动化工具automake, autoconf。而在这个过程中还用到了autoheader, aclocal, autopoint等程序。这些程序在运行过程中都会在SDK里留下印记,如在某sh文件里会记下使用了某个版本的aclocal,而当编译环境发生改变时,这个版本的aclocal找不到了,会报错。如下:

/home/lijun/cucc/apps/public/libjson-1.5/json-1.5/missing: line 81: aclocal-1.11: command not found

WARNING: 'aclocal-1.11' is missing on your system.

在这种情况下,最简单,也是最彻底的解决方法是调用autoreconf,它会自动调用automake等其他几乎所有需要的编译配置程序,来更新本目录和子目录下的GNU Build system。 而这些程序,如在方法一所述,使用host安装的最新版本即可,无需安装配套的特定版本。

于是,在出现问题的目录,或者其有configure.ac或configure.in的最上层目录,在上面这个案例中就是/home/lijun/cucc/apps/public/libjson-1.5/json-1.5, 运行:

$autoreconf -ivf

这里-i选项表示 安装包里丢失的附属文件,缺省会拷贝文件。如果程序认为合适,这个选项会触发 ‘automake --add-missing’, ‘libtoolize’, ‘autopoint’, 等程序。-f等同于force, 即强制重新生成编译所需要用到的最终文件,即使它们已经比输入文件还要新(也就是输入文件没有变化的情况下)。-v即verbose。

运行成功,它会生成带有本host环境的Makefile.in, aclocal.m4,configure等文件。有时候即使显示如下错误:

configure.ac:151: error: possibly undefined macro: AC_EILSEQ
      If this token and others are legitimate, please use m4_pattern_allow.
      See the Autoconf documentation.
autoreconf: /usr/bin/autoconf failed with exit status: 1

也可以不用理会,只要make的过程没有报错(或者是可以ignore的error)即可。 

这个方法也适用于编译过程中出现和这些配置文件相关的其他一些报错,如:

  configure.in:12: error: required file './compile' not found
50712 configure.in:12:   'automake --add-missing' can install 'compile'

运行 $autoreconf -ivf输出如下:

configure.in:12: installing './compile'

表示解决了该问题。

七. 修改config.status中的部分参数

在运行configure时,后面带的参数会被记录在config.status文件中,再由config.status生成最后的Makefile和config.h等文件。如果configure文件比config.status还新,则会触发config.status --recheck。这些参数可能包括当前编译环境,目标运行环境,编译工具链等。当编译环境等发生变化时,这些参数需要修改。可以用find -name config. status | xargs sed的方式进行批量自动替换。。 详情见我的另一篇博客./config.status --recheck 而发现的error的根因与解决方案

    八. 修改include头文件

随着版本的发展,GNU C Library的一些函数,变量的定义也发生变化。2010年以前编写的C代码,在Ubuntu 20.04 上报如下错:

Ubuntu 20.04,  mksquashfs.c:987:24: error: called object ‘major’ is not a function or function pointer.                                           

而在Ubuntu 18.0 上有如下告警:

Ubuntu 18.0 warning: In the GNU C Library, "minor" is defined by . For historical compatibility, it is currently defined by as well, but we plan to

 remove this soon. To use "minor", include directly. If you did not intend to use a system-defined macro "minor", you should undefine it after including . 

上面说明minor这个函数定义在里,但是在Ubuntu 18.0的环境下,也在 中定义了,但是很快就会移出。果然在Ubuntu 20.04的环境下就报错了,因为头文件发生了变化 

解决方案是把include头文件从换到

九. .sh文件第一行从/bin/sh改为/bin/bash

如果SDK原来是在CentOS下编译的,在转到Ubuntu时需要注意.sh文件。因为在CentOS下,/bin/sh缺省是bash。而在Ubuntu下, /bin/sh缺省是dash。dash缺少bash的一些功能,因此在解析一些脚本的时候,可能会报错。解决方案是在报错的.sh文件里,如果第一行缺少#!/bin/sh,则加上#!/bin/bash,如果是#!/bin/sh,则改为#!/bin/bash。详情见我的另一篇博客.sh脚本文件shebang行引发的syntax error:“(“ unexpected

以上是大型项目SDK在进行跨平台移植编译时,你需要重点关注的几方面内容。还有一个小技巧是,大型项目往往分好多个模块,编译的时候你可以选择模块进行编译,这样就不会每次耗费你大量的时间。另外,有时候发现莫名其妙的编译问题,不妨尝试先调用make clean,再make,说不定就通过了。

当然,实际工作可能还会遇到各种奇怪,特殊的问题,不在上述之列。我给你的建议是:(1)不要轻易放弃,只要坚持学习,不断尝试改进,总能搞定;(2)要有很好的学习能力,我在做这个移植之前Makefile不熟,GNU automake, autoconf都没听过,都是一点点学过来的;(3)要学会在互联网上向他人请教,国外有stack overflow,中国也有类似的一些提问回答的站点;(4)最后,要及时记录,适时总结,为自己积累知识,也为他人和后人少走弯路。

你可能感兴趣的:(Linux,linux,ubuntu,gnu)