LibreOffice 是由文檔基金會開發的自由及開放源代碼的辦公室套件。LibreOffice 套件包含文字處理器、電子表格、演示文稿程序、矢量圖形編輯器和圖表工具、數據庫管理程序及建立和編輯數學公式的應用程序。藉助 LibreOffice 的命令行接口能夠方便地將 office 文件轉換成 pdf。以下所示:javascript
$ 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 的基礎上解決了以下問題:html
本文側重於記述整個移植過程,記錄關鍵步驟以備忘,也爲相似的轉換工具移植到函數計算平臺提供參考。若是您對於如何快速搭建一個廉價且可擴展的 word 轉換 pdf 雲服務更感興趣,能夠閱讀另外一篇文章《五分鐘上線——函數計算 Word 轉 PDF 雲服務》。java
在開始以前建議找一個臺配置較好的 Debain/Ubuntu 機器,libreoffice 編譯比較消耗計算資源。並在機器上安裝和配置以下工具:node
fun 一款函數計算的編排工具,用於快速部署函數計算應用。python
MacOS 平臺可使用以下方法安裝linux
brew tap vangie/formula brew install fun
其餘平臺能夠經過 npm 安裝git
npm install @alicloud/fun -g
咱們會採用 fc-docker 提供的 aliyunfc/runtime-nodejs8:build
docker 鏡像進行編譯。fc-docker 提供了一系列的 docker 鏡像,這些 docker 鏡像環境很是接近函數計算的真實環境。由於咱們打算把 libreoffice 跑在 nodejs8 環境中,因此咱們選用了 aliyunfc/runtime-nodejs8:build
,build 標籤鏡像相比於其餘鏡像會多一些構建須要的基礎包。github
經過以下命令可啓動一個用於構建 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
表示一旦容器中止了就自動刪除容器。sql
接下來進入容器安裝編譯工具
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 {} . \;"
爲了使用 這個 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`); };
編寫一個 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 掛載共享網盤的方式也是一種新的路徑。
上文完整的源碼能夠在 fc-libreoffice 項目中找到。