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 的基础上解决了如下问题:

  1. 重新编译和裁剪 libreoffice ,使其适配 FC nodejs8 runtime 内置的 gcc 和内核版本;
  2. 安装运行时缺失的 libssl3 依赖;
  3. 借助 OSS 运行时下载解压,以绕过 zip 程序包 50M 的限制;
  4. 制作了一个 example 项目,支持一键部署,快速体验。

本文侧重于记述整个移植过程,记录关键步骤以备忘,也为类似的转换工具移植到函数计算平台提供参考。如果您对于如何快速搭建一个廉价且可扩展的 word 转换 pdf 云服务更感兴趣,可以阅读另一篇文章《五分钟上线——函数计算 Word 转 PDF 云服务》。

准备工作

在开始之前建议找一个台配置较好的 Debain/Ubuntu 机器,libreoffice 编译比较消耗计算资源。并在机器上安装和配置如下工具:

  • docker-ce 安装方法参考官方安装文档
  • fun 一款函数计算的编排工具,用于快速部署函数计算应用。

    MacOS 平台可以使用如下方法安装

    brew tap vangie/formula
    brew install fun

    其他平台可以通过 npm 安装

    npm install @alicloud/fun -g
  • ossutil oss 的命令行工具。将其下载并放置到 $PATH 所在目录。

编译 libreoffice

我们会采用 fc-docker 提供的 aliyunfc/runtime-nodejs8:build docker 镜像进行编译。fc-docker 提供了一系列的 docker 镜像,这些 docker 镜像环境非常接近函数计算的真实环境。因为我们打算把 libreoffice 跑在 nodejs8 环境中,所以我们选用了 aliyunfc/runtime-nodejs8:build,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 的容器并把当前目录挂载到容器内文件系统的 /code 目录。附加参数 --cap-add=SYS_PTRACE --security-opt seccomp=unconfined 是 cpp 程序编译需要的,否则会报出一些警告。-d 表示以后台 daemon 的方式启动。-t 表示启动 tty,配合后面的 bash 命令是为了卡主容器不退出。而 --rm 表示一旦容器停止了就自动删除容器。

安装编译工具

接下来进入容器安装编译工具

apt-get install -y ccache
apt-get build-dep -y libreoffice

ccache 是一个编译工具,可以加速 gcc 对同一个程序的多次编译。尽管第一次编译会花费长一点的时间,有了ccache,后续的编译将变得非常非常快。

apt-get 的 build-dep 子命令会建立某个要编译软件的环境。具体行为就是把所有依赖的工具和软件包都安装上。

克隆源码

git clone --depth=1 git://anongit.freedesktop.org/libreoffice/core libreoffice
cd 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

最终的编译结果位于 ./instdir/ 目录下。

精简尺寸

使用 strip 命令去除二进制文件中的符号信息和编译信息

# this will remove ~100 MB of symbols from shared objects
strip ./instdir/**/*

删除不必要的文件

# remove unneeded stuff for headless mode
rm -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

打包

# archive
tar -zcvf lo.tar.gz instdir

然后使用如下命令将 lo.tar.gz 文件从容器文件系统拷贝到宿主机文件系统。

docker cp libre-builder:/code/libreoffice/lo.tar.gz ./lo.tar.gz

Gzip vs Zopfli vs Brotli 
Gzip 、Zopfli 和 Brotli 是三种开源的压缩算法,对于一个 130M 的 chromium 文件,分别采用这三种压缩算法最大 level 的压缩效果是

文件 算法 MiB 压缩比 解压耗时
chromium - 130.62 - -
chromium.gz Gzip 44.13 66.22% 0.968s
chromium.gz Zopfli 43.00 67.08% 0.935s
chromium.br Brotli 33.21 74.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 bash
docker exec -t brotli-util apt-get update
docker exec -t brotli-util apt-get install -y brotli
docker exec -t brotli-util gzip -d lo.tar.gz
docker 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 存在 native binding,所以在开发机 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 libnss3
docker 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 环境变量里的动态链接库才能被找到,而函数计算将代码目录/code 下的 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 文件,需要先上传到 OSS

ossutil 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

在函数的 initializer 方法中下载。

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-unpacker npm 包解压 lo.tar.br

const {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`);
};

fun 部署函数

编写一个 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/../.env export ALIBABA_CLOUD_DEFAULT_REGION OSS_BUCKET
envsubst < $SCRIPT_DIR/../template.yml.tpl > $SCRIPT_DIR/../template.yml cd $SCRIPT_DIR/../

上面所有的配置都写在了 .env 文件中,dotenv 是社区常见的方案,也有广泛的工具支持。

小结

本文重点介绍了编译 libreoffice 的过程,这也是移植中较为困难的部分。由于 libreoffice 又涉及到 npm 的 native binding 和 apt-get 安装到本地目录的问题,所以在函数计算依赖方面本例也是非常经典的场景。无论是编译还是依赖安装,本文中的步骤都强烈地依赖 fc-docker 镜像,正因为有了该镜像,解决了环境差异问题,大大降低了移植的难度。大文件运行时加载也是函数计算的常见问题,对于转换工具场景中常见的大文件是二进制程序,对于机器学习场景中大文件常是训练模型的数据问题,但是无论是哪一种,采用 OSS 下载解压的方法都是通用的,随着函数计算支持了 NAS,使用 NAS 挂载共享网盘的方式也是一种新的路径。

原文链接
本文为云栖社区原创内容,未经允许不得转载。

手把手教您将 libreoffice 移植到函数计算平台的更多相关文章

  1. 快速部署 Spring PetClinic 到函数计算平台

    简介 首先介绍下在本文出现的几个比较重要的概念: 函数计算(Function Compute):函数计算是一个事件驱动的服务,通过函数计算,用户无需管理服务器等运行情况,只需编写代码并上传.函数计算准 ...

  2. Vue手把手教你撸一个 beforeEnter 钩子函数

    地址 :https://www.jb51.net/article/138821.htm 地址 :https://www.jb51.net/article/108964.htm

  3. TensorFlow-谷歌深度学习库 手把手教你如何使用谷歌深度学习云平台

    自己的电脑跑cnn, rnn太慢? 还在为自己电脑没有好的gpu而苦恼? 程序一跑一俩天连睡觉也要开着电脑训练? 如果你有这些烦恼何不考虑考虑使用谷歌的云平台呢?注册之后即送300美元噢-下面我就来介 ...

  4. 开发函数计算的正确姿势——OCR 服务

    作者 | 杜万(倚贤) 阿里云技术专家 简介 首先介绍下在本文出现的几个比较重要的概念: OCR(光学字符识别):光学字符识别(Optical Character Recognition, OCR)是 ...

  5. 从函数计算架构看 Serverless 的演进与思考

    作者 | 杨皓然  阿里巴巴高级技术专家 导读:云计算之所以能够成为 DT 时代颠覆性力量,是因为其本质是打破传统架构模式.降低成本并简化体系结构,用全新的思维更好的满足了用户需求.而无服务器计算(S ...

  6. 一元建站-基于函数计算 + wordpress 构建 serverless 网站

    前言 本文旨在通过 快速部署一个 wordpress 网站到阿里云函数计算平台 这个示例来展示 serverless web 新的开发模式, 包括 FUN 工具一键初始化 NAS, 同步网站到 NAS ...

  7. 使用函数计算三步实现深度学习 AI 推理在线服务

    目前深度学习应用广发, 其中 AI 推理的在线服务是其中一个重要的可落地的应用场景.本文将为大家介绍使用函数计算部署深度学习 AI 推理的最佳实践,  其中包括使用 FUN 工具一键部署安装第三方依赖 ...

  8. 基于函数计算 + TensorFlow 的 Serverless AI 推理

    前言概述 本文介绍了使用函数计算部署深度学习 AI 推理的最佳实践, 其中包括使用 FUN 工具一键部署安装第三方依赖.一键部署.本地调试以及压测评估, 全方位展现函数计算的开发敏捷特性.自动弹性伸缩 ...

  9. 从零入门 Serverless | 函数计算的开发与配置

    导读:在本篇文章中,"基本概念"部分主要对函数计算最核心的概念进行详细介绍,包括服务.函数.触发器.版本.别名以及相关的配置:"开发流程"部分介绍了基于函数计算 ...

随机推荐

  1. 虚拟环境更新HA

    停止HA服务 sudo systemctl stop homeassistant@homeassistant 开始更新HA sudo -u homeassistant -H -s cd /srv/ho ...

  2. Mem系列函数介绍及案例实现

      昨天导师甩给我们一个项目案例,让我们自己去看一看熟悉一下项目内容,我看到了这个项目里面大量使用memset(sBuf,0,sizeof(sBuf));这一块内存填充的代码,于是回想起以前查过Mem ...

  3. JavaScript字符串与数组方法整理

    字符串(String)的方法: 代码后面的都是返回值 var str = "atusdgafsvg"; var str1 = "123456789"; var ...

  4. Redis安装及使用

    1.我们可以通过在官网下载tar.gz的安装包,或者通过wget的方式下载 进入要下载到的文件夹: wget http://download.redis.io/releases/redis-4.0.1 ...

  5. 02 . 处理axios的三个问题 :设置基路径/axios挂载到vue原型/请求时自动携带token

    //使用API时必须在请求头中使用 Authorization 字段提供 token 令牌 import axios from 'axios' // 处理axios的三个问题 // 处理一:基路径 a ...

  6. 配置NFS固定端口

    NFS启动时会随机启动多个端口并向RPC注册,为了设置安全组以及iptables规则,需要设置NFS固定端口.NFS服务需要开启 mountd,nfs,nlockmgr,portmapper,rquo ...

  7. Python练手例子(13)

    73.反向输出一个链表. #python3.7 if __name__ == '__main__': ptr = [] for i in range(5): num = int(input('Plea ...

  8. Javascript——浅谈 Event Flow

    1.Javascript Events : Event Bubbling(事件冒泡) 如果事件从最特定的元素开始,则事件流中的一个阶段称为事件冒泡(DOM中可能最深的节点)然后向上流向最不特定的节点( ...

  9. Web前端-JavaScript基础教程上

    Web前端-JavaScript基础教程 将放入菜单栏中,便于阅读! JavaScript是web前端开发的编程语言,大多数网站都使用到了JavaScript,所以我们要进行学习,JavaScript ...

  10. [Swift]LeetCode202. 快乐数 | Happy Number

    Write an algorithm to determine if a number is "happy". A happy number is a number defined ...