LibreOffice是由文档基金会开发的自由及开放源代码的办公室套件.LibreOffice套件包含文字处理器,电子表格,演示文稿程序,数量库管理程序及创建和编辑数学公式的应用程序。借助LibreOffice的命令行接口可以方便地将office文件转换成pdf。如下所示:$ soffice –convert-to pdf –outdir /tmp /tmp/test.doc一个完整版本的LibreOffice大小为2 GB,而函数计算运行时缓存目录/ tmp空间限制为512M,zip程序包大小限制为50M。好在社区已经有项目aws-lambda-libreoffice成功的将libreoffice移植到AWS Lambda平台,基于前人的方法和经验,本人创建了fc-libreoffice项目,使libreoffice成功的运行在阿里云函数计算平台.fc -libreoffice 在aws-lambda-libreoffice的基础上解决了如下问题:重新编译和裁剪libreoffice,使其适配FC nodejs8 runtime内置的gcc和内核版本;安装运行时缺失的libssl3依赖;借助OSS运行时下载解压,以绕过zip程序包50M的限制;制作了一个例子项目,支持一键部署,快速体验。本文侧重于记述整个移植过程,记录关键步骤以备忘,也为类似的转换工具移植到函数计算平台提供参考。如果您对于如何快速搭建一个廉价且可扩展的词转换pdf云服务更感兴趣,可以阅读另一篇文章“五分钟上线 - 函数计算Word转PDF云服务”。准备工作在开始之前建议找一个台配置较好的Debain / Ubuntu机器,libreoffice编译比较消耗计算资源。并在机器上安装和配置如下工具:docker-ce安装方法参考官方安装文档好玩的一款函数计算的编排工具,用于快速部署函数计算应用。MacOS平台可以使用如下方法安装brew tap vangie/formulabrew install fun其他平台可以通过npm安装npm install @alicloud/fun -gossutil oss的命令行工具。将其下载并放置到$ PATH所在目录。编译libreoffice我们会采用fc-aliyunfc/runtime-nodejs8:build docker 提供的docker镜像进行编译.fc-docker提供了一系列的docker镜像,这些docker镜像环境非常接近函数计算的真实环境。因为我们打算把libreoffice跑在nodejs8环境中,所以我们选用了aliyunfc/runtime-nodejs8:build,建造标签镜像相比于其他镜像会多一些构建需要的基础包。启动一个编译环境通过如下命令可启动一个用于构建libreoffice的容器。docker run –name libre-builder –rm -v $(pwd):/code -d -t –cap-add=SYS_PTRACE –security-opt seccomp=unconfined aliyunfc/runtime-nodejs8:build bash上面的命令,我们启动了一个名为libre-builder的容器并把当前目录挂载到容器内文件系统的/代码目录。附加参数–cap-add=SYS_PTRACE –security-opt seccomp=unconfined是cpp程序编译需要的,否则会报出一些警告。-d表示以后台daemon的方式启动。-t表示启动tty,配合后面的bash命令是为了卡主容器不退出。而–rm表示一旦容器停止了就自动删除容器。安装编译工具接下来进入容器安装编译工具apt-get install -y ccacheapt-get build-dep -y libreofficeccache是一个编译工具,可以加速gcc对同一个程序的多次编译。尽管第一次编译会花费长一点的时间,有了ccache,后续的编译将变得非常非常快。apt-get的build-dep子命令会建立某个要编译软件的环境。具体行为就是把所有依赖的工具和软件包都安装上。克隆源码git clone –depth=1 git://anongit.freedesktop.org/libreoffice/core libreofficecd libreoffice记得加上–depth=1参数,因为libreoffice项目比较大,进行全量克隆会比较费时间,对于编译来说git提交历史没有意义。配置并编译# 如果多次编译,该设置可以加速后续编译ccache –max-size 16 G && ccache -s通过–disable参数去掉不需要的模块,以减少最终编译产物的体积。# the most important part. Run ./autogen.sh –help to see wha each option means./autogen.sh –disable-report-builder –disable-lpsolve –disable-coinmp \ –enable-mergelibs –disable-odk –disable-gtk –disable-cairo-canvas \ –disable-dbus –disable-sdremote –disable-sdremote-bluetooth –disable-gio –disable-randr \ –disable-gstreamer-1-0 –disable-cve-tests –disable-cups –disable-extension-update \ –disable-postgresql-sdbc –disable-lotuswordpro –disable-firebird-sdbc –disable-scripting-beanshell \ –disable-scripting-javascript –disable-largefile –without-helppack-integration \ –without-system-dicts –without-java –disable-gtk3 –disable-dconf –disable-gstreamer-0-10 \ –disable-firebird-sdbc –without-fonts –without-junit –with-theme=“no” –disable-evolution2 \ –disable-avahi –without-myspell-dicts –with-galleries=“no” \ –disable-kde4 –with-system-expat –with-system-libxml –with-system-nss \ –disable-introspection –without-krb5 –disable-python –disable-pch \ –with-system-openssl –with-system-curl –disable-ooenv –disable-dependency-tracking开始编译make的名单最终compile-查询查询结果位于./instdir/目录下。精简尺寸使用strip命令去除二进制文件中的符号信息和编译信息# this will remove ~100 MB of symbols from shared objectsstrip ./instdir/**/删除不必要的文件# remove unneeded stuff for headless moderm -rf ./instdir/share/gallery \ ./instdir/share/config/images_.zip \ ./instdir/readmes \ ./instdir/CREDITS.fodt \ ./instdir/LICENSE* \ ./instdir/NOTICE验证使用如下命令,测试一下编译出来的soffice是否能正常将txt文件转换成pdf文件。echo “hello world” > a.txt./instdir/program/soffice –headless –invisible –nodefault –nofirststartwizard \ –nolockcheck –nologo –norestore –convert-to pdf –outdir $(pwd) a.txt打包# archivetar -zcvf lo.tar.gz instdir然后使用如下命令将lo.tar.gz文件从容器文件系统拷贝到宿主机文件系统。docker cp libre-builder:/code/libreoffice/lo.tar.gz ./lo.tar.gzGzip vs Zopfli vs Brotli Gzip,Zopfli和Brotli是三种开源的压缩算法,对于一个130M的铬文件,分别采用这三种压缩算法最大级别的压缩效果是文件算法MIB压缩比解压耗时铬-130.62–chromium.gzGzip已44.1366.22%0.968schromium.gzZopfli43.0067.08%0.935schromium.brBrotli33.2174.58%0.712s从上面的结果看Brotli算法的效果最优。由于aliyunfc/runtime-nodejs8:build是基于debain jessie发行版的。在debain jessie上安装brotli较为麻烦,所以我们借助ubuntu容器安装brotli工具,将tar.gz格式转为tar.br格式。docker run –name brotli-util –rm -v $(pwd):/root -w /root -d -t ubuntu:18.04 bashdocker exec -t brotli-util apt-get updatedocker exec -t brotli-util apt-get install -y brotlidocker exec -t brotli-util gzip -d lo.tar.gzdocker exec -t brotli-util brotli -q 11 -j -f lo.tar然后当前目录会多一个lo.tar.br文件。安装依赖在函数计算nodejs8环境中运行soffice,需要安装通过npm安装tar.br的解压依赖包@shelf/aws-lambda-brotli-unpacker 和通过apt-get安装libnss3依赖。先启动一个nodejs8的容器,以保证依赖的安装环境和运行时环境是一致的。docker run –rm –name libreoffice-builder -t -d -v $(pwd):/code –entrypoint /bin/sh aliyunfc/runtime-nodejs8注意:存在@shelf/aws-lambda-brotli-unpacker本机绑定,所以在开发机MacOS上npm install打包上传是无法工作。docker exec -t libreoffice-builder npm install由于函数计算运行时无法安装全局的deb包,所以需要将deb和依赖的deb包下载下来,再安装到当前工作目录而不是系统目录。当前工作目录下可以随代码一起打包上传。docker exec -t libreoffice-builder apt-get install -y -d -o=dir::cache=/code libnss3docker exec -t libreoffice-builder bash -c ‘for f in $(ls /code/archives/.deb); do dpkg -x $f $(pwd) ; done;’libnss3包含了许多.so动态链接库文件,linux系统下LD_LIBRARY_PATH环境变量里的动态链接库才能被找到,而函数计算将代码目录/代码下的lib目录默认添加到了LD_LIBRARY_PATH中。所以我们写个脚本,把所有安装的.so文件软连接到/ code / lib目录下docker exec -t libreoffice-builder bash -c “rm -rf /code/archives/; mkdir -p /code/lib;cd /code/lib; find ../usr/lib -type f ( -name ‘.so’ -o -name ‘*.chk’ ) -exec ln -sf {} . ;“下载并解压tar.br为了使用这个lo.tar.br文件,需要先上传到OSSossutil cp $SCRIPT_DIR/../node_modules/fc-libreoffice/bin/lo.tar.br oss://${OSS_BUCKET}/lo.tar.br \ -i ${ALIBABA_CLOUD_ACCESS_KEY_ID} -k ${ALIBABA_CLOUD_ACCESS_KEY_SECRET} -e oss-${ALIBABA_CLOUD_DEFAULT_REGION}.aliyuncs.com -f在函数的初始化方法中下载。module.exports.initializer = (context, callback) => { store = new OSS({ region: oss-${process.env.ALIBABA_CLOUD_DEFAULT_REGION}
, bucket: process.env.OSS_BUCKET, accessKeyId: context.credentials.accessKeyId, accessKeySecret: context.credentials.accessKeySecret, stsToken: context.credentials.securityToken, internal: process.env.OSS_INTERNAL === ’true’ }); if (fs.existsSync(binPath) === true) { callback(null, “already downloaded.”); return; } co(store.get(’lo.tar.br’, binPath)).then(function (val) { callback(null, val) }).catch(function (err) { callback(err) });};然后借助于@shelf/aws-lambda-brotli-unpackernpm包解压lo.tar.brconst {unpack} = require(’@shelf/aws-lambda-brotli-unpacker’);const {execSync} = require(‘child_process’);const inputPath = path.join(__dirname, ‘..’, ‘bin’, ’lo.tar.br’);const outputPath = ‘/tmp/instdir/program/soffice’;module.exports.handler = async event => { await unpack({inputPath, outputPath}); execSync(${outputPath} --convert-to pdf --outdir /tmp /tmp/example.docx
);};有趣部署函数编写一个template.yml文件,将函数计算的配置都写在该文件中,然后使用fun deploy命令部署函数。ROSTemplateFormatVersion: ‘2015-09-01’Transform: ‘Aliyun::Serverless-2018-04-03’Resources: libre-svc: # service name Type: ‘Aliyun::Serverless::Service’ Properties: Description: ‘fc test’ Policies: - AliyunOSSFullAccess libre-fun: # function name Type: ‘Aliyun::Serverless::Function’ Properties: Handler: index.handler Initializer: index.initializer Runtime: nodejs8 CodeUri: ‘./’ Timeout: 60 MemorySize: 640 EnvironmentVariables: ALIBABA_CLOUD_DEFAULT_REGION: ${ALIBABA_CLOUD_DEFAULT_REGION} OSS_BUCKET: ${OSS_BUCKET} OSS_INTERNAL: ’true’真实场景下,把秘钥和一起变量写在template.yml里并不合适。为了做到代码和配置相分离,上面使用了变量占位符${ALIBABA_CLOUD_DEFAULT_REGION}和${OSS_BUCKET}。然后使用envsubst进行替换SCRIPT_DIR=dirname -- "$0"
source $SCRIPT_DIR/../.envexport ALIBABA_CLOUD_DEFAULT_REGION OSS_BUCKETenvsubst < $SCRIPT_DIR/../template.yml.tpl > $SCRIPT_DIR/../template.ymlcd $SCRIPT_DIR/../上面所有的配置都写在了.env文件中,dotenv是社区常见的方案,也有广泛的工具支持。小结本文重点介绍了编译libreoffice的过程,这也是移植中较为困难的部分。由于libreoffice又涉及到npm的本地绑定和apt-get安装到本地目录的问题,所以在函数计算依赖方面本例也是非常经典的场景。无论是编译还是依赖安装,本文中的步骤都强烈地依赖fc-docker镜像,正因为有了该镜像,解决了环境差异问题,大大降低了移植的难度。大文件运行时加载也是函数计算的常见问题,对于转换工具场景中常见的大文件是二进制程序,对于机器学习场景中大文件常是训练模型的数据问题,但是无论是哪一种,采用OSS下载解压的方法都是通用的,随着函数计算支持了NAS,使用NAS挂载共享网盘的方式也是一种新的路径。上文完整的源码可以在FC-LibreOffice的项目中找到。参考阅读https://zh.wikipedia.org/wiki/LibreOffice如何在AWS Lambda中运行LibreOffice以获得大规模的廉价PDFhttps://github.com/alixaxel/chrome-aws-lambdahttps://github.com/shelfio/aws-lambda-brotli-unpacker本文作者:倚贤阅读原文本文为云栖社区原创内容,未经允许不得转载。