前言

LibreOffice 是一套开源且免费的办公软件,类似于 Microsoft Office 和 Apache OpenOffice。它包括文字处理、电子表格、演示文稿、绘图、数据库等功能模块。
LibreOffice可执行于各种系统平台,包括Microsoft Windows、MacOS及GNU/Linux,LibreOffice 也提供了 Android 平台的版本,支持Office文档的浏览和少量的编辑功能。本文介绍在Ubuntu下编译LibreOffice 的Android 版本,生成apk及so文件。

本文所用编译环境:

  • Ubuntu 22.04.3 LTS, 64Bit
  • gcc/g++ 12.3.0,LibreOffice 要求版本12或以上
  • jdk 17.0.10,根据Gradle版本而定
  • ndk 25.2.9519653,LibreOffice 要求版本23~25

1 下载

1.1 克隆源代码

LibreOffice中文社区源码介绍页面可以获取到仓库地址,除了官方仓库外,也提供了国内镜像
使用git clone命令将源码下载下来,源码非常大,大概有4G,如果下载不来下,参考下一节的常见错误解决

1
2
# 克隆LibreOffice源码,包括子模块;只克隆最新提交以减少下载内容
git clone --recurse-submodules --depth=1 https://git.libreoffice.org/core

1.2 安装jdk

1
2
3
4
5
6
7
8
# 查看jdk 版本
java --version

# 安装jdk
sudo apt install openjdk-17-jdk

# 查看jdk安装路径
update-alternatives --display java

1.3 安装Android sdk及NDK

官方建议通过下载Android studio 进行安装,打开Ubuntu应用商店通过图形界面安装并打开Android studio,然后选择SDKManager下载Android sdk和ndk。

1.4 安装c++环境

c/c++的编译环境可以通过build-essential软件包安装,它包含了g++, gcc, make, dpkg-dev,libc6-dev等 构建编译 C/C++ 项目必要的编译器、构建工具和常用的库文件。
sudo apt install build-essential
不过由于这里build-essential默认安装的编译器版本只有11,而LibreOffice要求最低12,因此这里选择手动安装。

1
2
3
4
5
# 手动安装gcc 12,g++ 12, make 
sudo apt install gcc-12 g++-12 make

# 使用 update-alternatives 命令将gcc12,g++12设置为默认版本
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 100 --slave /usr/bin/g++ g++ /usr/bin/g++-12

1.5 安装其他依赖

输入以下命令,安装编译LibreOffice需要用到的一些工具和库。执行编译环境检查时报错缺什么工具就补上。

1
sudo apt install autoconf pkg-config  libfontconfig1-dev gperf python3-dev bison flex ant gettext nasm

这些工具的主要功能:

  • autoconf:自动配置工具,用于生成软件包的配置脚本。
  • pkg-config:用于检查系统中安装的库的版本和路径等信息。
  • libfontconfig1-dev:Fontconfig库的开发文件,用于构建依赖于Fontconfig的软件。
  • gperf:GNU gperf工具,用于生成完美哈希函数。
  • python3-dev:Python 3的开发文件,包括头文件和静态库,用于编译依赖Python的软件。
  • bison:用于生成解析器的工具。
  • flex:用于生成词法分析器的工具。
  • ant:Java项目构建工具。
  • gettext:用于国际化的工具,包括翻译字符串的工具和库。

2 编译

2.1 配置编译选项

编译过程可以在make命令加上编译参数,也可以通过autogen.input文件来设置编译选项,如不指定则按照默认的编译选项进行。autogen.input文件仅在不存在任何命令行参数时才会生效,完整的编译选项及含义见源代码根目录下的configure文件。

在项目根目录(源码根目录)使用touch命令新建一个配置文件,

1
touch autogen.input

添加以下内容,路径相关选项请根据实际情况修改。

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
# 指定构建的发行版,本例是构建Android平台(armeabi-v7a),因此选择LibreOfficeAndroid
# 更多预设配置参考源码目录/distro-configs文件夹
--with-distro=LibreOfficeAndroid

# 指定 Android SDK 的路径
--with-android-sdk=/home/zhg/Android/Sdk

# 指定 Android NDK 的路径
--with-android-ndk=/home/zhg/Android/Sdk/ndk/25.2.9519653

# 指定 JDK 的安装路径,可通过`update-alternatives --display java`命令查看
--with-jdk-home=/usr/lib/jvm/java-17-openjdk-amd64

# 启用Android上的实验性编辑功能
--enable-android-editing

# 启用构建办公开发工具包(Office Development Kit),用于扩展和定制功能
--enable-odk

# 设置构建平台的 configure 选项,禁用系统 libxml,使用 LibreOffice 提供的 libxml 库
--with-build-platform-configure-options=--without-system-libxml

# 指定LibreOffice构建过程中需要使用的外部依赖项或源代码的路径
--with-external-tar=/home/zhg/Workplace/LibreOffice/external_tar

# 启用简体及繁体中文用户界面
--with-lang=zh-CN zh-TW

2.2 检查编译环境

在项目根目录下运行autogen.sh脚本,这将会执行检查构建环境、读取autogen.input选项并生成配置脚本。

1
./autogen.sh

当输出以下内容表示autogen.sh运行无错误,可以进行下一步。

1
2
3
4
5
To show information on various make targets and make flags, run:
/usr/bin/make help

To just build, run:
/usr/bin/make

2.3 开始构建

输入make命令开始构建,编译耗时取决于下载软件包的速度及电脑配置。

1
make

当输出BUILD SUCCESSFUL表明编译成功。

1
2
3
4
5
6
7
8
BUILD SUCCESSFUL in 39m 3s
46 actionable tasks: 46 executed
[CUS] android/loandroid3
[BIN] android
[MOD] android
[MOD] libreoffice
[BIN] top level modules: libreoffice
[ALL] top level modules: build-non-l10n-only build-l10n-only

构建完成的Android so文件在项目目录/android/jniLibs/armeabi-v7a目录下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
├── libc++_shared.so
├── libfreebl3.so
├── liblo-native-code.so
├── libnspr4.so
├── libnss3.so
├── libnssckbi.so
├── libnssdbm3.so
├── libnssutil3.so
├── libplc4.so
├── libplds4.so
├── libsmime3.so
├── libsoftokn3.so
├── libsqlite3.so
└── libssl3.so

2.4 打包apk

LibreOffice 提供了 Android 的示例项目,其主要源码和gradle配置文件位于/android/source,可以导入到Android studio中进行开发和编译,也可以直接使用gradle命令打包出apk。

定位到android/source文件夹,输入以下命令:

1
2
# 编译并打Debug包
./gradlew assembleDebug

当输出以下字样表示构建完成,apk位于source/build/outputs/apk/<xxFlavor>/debug下。

1
2
BUILD SUCCESSFUL in 1m 53s
101 actionable tasks: 64 executed, 37 up-to-date

2.5 导入Android Studio

由于/android/source下的Android项目还依赖的LibreOffic其他目录的源码,这些都是由build.gradle中的任务来控制的,主要是生成Android端的配置和复制资源文件,在Ubuntu下使用./gradlew build构建完生成相应的文件以后,并修改 gradle sourceSets相关路径,即可去掉这些任务,从而将Android 项目从LibreOffice 源码中独立出来,导入到Android Studio。这里还修改了一下目录位置使其更符合Android项目的默认组织结构,源码已上传Github

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
//以下是这些任务的说明:
//从${liboInstdir}/${liboEtcFolder}/types、${liboInstdir}/share/fonts/truetype目录中
// 复制程序文件和字体文件到assets/unpack目录
task copyUnpackAssets(type: Copy) {
description "copies assets that need to be extracted on the device"
into 'assets/unpack'
into('program') {
from("${liboInstdir}/${liboEtcFolder}/types") {
includes = [
"offapi.rdb",
"oovbaapi.rdb"
]
}
from("${liboInstdir}/${liboUreMiscFolder}") {
includes = ["types.rdb"]
rename 'types.rdb', 'udkapi.rdb'
}
}
into('user/fonts') {
from "${liboInstdir}/share/fonts/truetype"
// Note: restrict list of fonts due to size considerations - no technical reason anymore
// ToDo: fonts would be good candidate for using Expansion Files instead
includes = [
"Liberation*.ttf",
"Caladea-*.ttf",
"Carlito-*.ttf",
"Gen*.ttf",
"opens___.ttf"
]
}
into('etc/fonts') {
from "./"
includes = ['fonts.conf']
filter {
String line ->
line.replaceAll(
'@@APPLICATION_ID@@', new String("${android.defaultConfig.applicationId}")
)
}
}
}
//将${liboInstdir}/share/config、${liboInstdir}/program、${liboInstdir}/share目录中的
// 各种资源文件复制到应用的 assets 目录中,以便在安装后可以访问这些资源
task copyAssets(type: Copy) {
description "copies assets that can be accessed within the installed apk"
into 'assets'

// include icons, Impress styles and required .ui files
into ('share') {
into ('config') {
from ("${liboInstdir}/share/config")
includes = ['images_**.zip',
'**/simpress/**.xml',
'**/annotation.ui',
'**/hfmenubutton.ui',
'**/inforeadonlydialog.ui',
'**/pbmenubutton.ui',
'**/scrollbars.ui',
'**/tabbuttons.ui',
'**/tabviewbar.ui'
]
}
}

into('program') {
from "${liboInstdir}/program"
includes = ['services.rdb', 'services/services.rdb']

into('resource') {
from "${liboInstdir}/${liboSharedResFolder}"
includes = ['*en-US.res']
}
}
into('share') {
from("${liboInstdir}/share") {
// Filter data is needed by e.g. the drawingML preset shape import.
includes = ['registry/**', 'filter/**']
// those two get processed by mobile-config.py
excludes = ['registry/main.xcd', 'registry/res/registry_en-US.xcd']
}
// separate data files for Chinese and Japanese
from("${liboWorkdir}/CustomTarget/i18npool/breakiterator/") {
include '*.data'
}
}
}

//将 LICENSE 和 NOTICE 文件从 ${liboInstdir} 复制到 res_generated/raw 目录,并将它们重命名为 license.txt 和 notice.txt
task copyAppResources(type: Copy) {
description "copies documents to make them available as app resources"
into 'res_generated/raw'
from("${liboInstdir}") {
includes = ["LICENSE", "NOTICE"]
rename "LICENSE", "license.txt"
rename "NOTICE", "notice.txt"
}
}

//将 soffice.cfg 文件从 ${liboInstdir}/share/config/ 复制到 assets_fullUI/share/config/ 目录下
task createFullConfig(type: Copy) {
// grab dir to clear whole hierarchy on clean target
outputs.dir "assets_fullUI"
into 'assets_fullUI/share/config/soffice.cfg'
from "${liboInstdir}/share/config/soffice.cfg"
}

//创建输出目录和文件,以便将处理后的文件和配置保存到正确的位置
task createStrippedConfig {
def preserveDir = file("assets_strippedUI/share/config/soffice.cfg/empty")
outputs.dir "assets_strippedUI"
outputs.dir "assets_strippedUI/share/registry/res"
outputs.file preserveDir

doLast {
file('assets_strippedUI/share/registry/res').mkdirs()
file("assets_strippedUI/share/config/soffice.cfg").mkdirs()
// just empty file
preserveDir.text = ""
}
}

//执行外部的 Python 脚本 mobile-config.py 来处理main.xcd 配置文件,
// 从而生成一个精简版本的配置文件,输出到assets_strippedUI/share/registry/main.xcd
task createStrippedConfigMain(type: Exec) {
dependsOn 'createStrippedConfig'
inputs.files "${liboInstdir}/share/registry/main.xcd", "${liboSrcRoot}/android/mobile-config.py"
outputs.file "assets_strippedUI/share/registry/main.xcd"
executable "${liboSrcRoot}/android/mobile-config.py"
args = ["${liboInstdir}/share/registry/main.xcd", "assets_strippedUI/share/registry/main.xcd"]
}

//执行外部的 Python 脚本 mobile-config.py 来处理registry_en-US.xcd 配置文件,
// 从而生成一个精简版本的配置文件,输出到assets_strippedUI/share/registry/res/registry_en-US.xcd
task createStrippedConfigRegistry(type: Exec) {
dependsOn 'createStrippedConfig'
inputs.files "${liboInstdir}/share/registry/res/registry_en-US.xcd", "${liboSrcRoot}/android/mobile-config.py"
outputs.file "assets_strippedUI/share/registry/res/registry_en-US.xcd"
executable "${liboSrcRoot}/android/mobile-config.py"
args = ["${liboInstdir}/share/registry/res/registry_en-US.xcd", "assets_strippedUI/share/registry/res/registry_en-US.xcd"]
doFirst {
file('assets_strippedUI/share/registry/res').mkdirs()
}
}

//根据 liboSettings.gradle 文件,生成sofficerc, fundamentalrc, unorc, bootstraprc, versionrc等配置文件
task createRCfiles {
inputs.file "liboSettings.gradle"
dependsOn copyUnpackAssets, copyAssets
def sofficerc = file('assets/unpack/program/sofficerc')
def fundamentalrc = file('assets/program/fundamentalrc')
def bootstraprc = file('assets/program/bootstraprc')
def unorc = file('assets/program/unorc')
def versionrc = file('assets/program/versionrc')

outputs.files sofficerc, fundamentalrc, unorc, bootstraprc, versionrc

doLast {
sofficerc.text = '''\
[Bootstrap]
Logo=1
NativeProgress=1
URE_BOOTSTRAP=file:///assets/program/fundamentalrc
HOME=$APP_DATA_DIR/cache
OSL_SOCKET_PATH=$APP_DATA_DIR/cache
'''.stripIndent()

fundamentalrc.text = '''\
[Bootstrap]
LO_LIB_DIR=file://$APP_DATA_DIR/lib/
BRAND_BASE_DIR=file:///assets
BRAND_SHARE_SUBDIR=share
CONFIGURATION_LAYERS=xcsxcu:${BRAND_BASE_DIR}/share/registry res:${BRAND_BASE_DIR}/share/registry
URE_BIN_DIR=file:///assets/ure/bin/dir/nothing-here/we-can/exec-anyway
'''.stripIndent()

bootstraprc.text = '''\
[Bootstrap]
InstallMode=<installmode>
ProductKey=LibreOffice '''+ "${liboVersionMajor}.${liboVersionMinor}" + '''
UserInstallation=file://$APP_DATA_DIR
'''.stripIndent()

unorc.text = '''\
[Bootstrap]
URE_INTERNAL_LIB_DIR=file://$APP_DATA_DIR/lib/
UNO_TYPES=file://$APP_DATA_DIR/program/udkapi.rdb file://$APP_DATA_DIR/program/offapi.rdb file://$APP_DATA_DIR/program/oovbaapi.rdb
UNO_SERVICES=file:///assets/program/services.rdb file:///assets/program/services/services.rdb
'''.stripIndent()

versionrc.text = '''\
[Version]
AllLanguages=en-US
buildid=''' + "${liboGitFullCommit}" + '''
ReferenceOOoMajorMinor=4.1
'''.stripIndent()
}
}

3 常见错误解决

3.1 Git clone错误

1
2
3
4
5
error: RPC 失败。curl 56 GnuTLS recv error (-9): Error decoding the received TLS packet.
error: 预期仍然需要 7923 个字节的正文
fetch-pack: unexpected disconnect while reading sideband packet
fatal: 过早的文件结束符(EOF)
fatal: fetch-pack:无效的 index-pack 输出

解决方法:切换到 SSH 仓库地址或者增加git发送数据的缓冲区大小

1
2
# 设置 Git 全局配置中的 http.postBuffer 参数
git config --global http.postBuffer 1024M

3.2 文件权限相关

1
2
3
4
 mkdir: cannot create directory '/LibreOffice': Permission denied
configure: error: Failed to resolve absolute path.
or
`/bin/sh: 1: cannot create /LibreOffice/lo-externalsrc-core/fetch.log: Permission denied`

解决方法:使用chmod -R 777 <root_path>命令赋予读写权限。注意如果该路径如果是定义在autogen.input,检查路径是否在系统根目录下(/),如果是则改到其他目录。普通用户对系统根目录是没有读写权限的。

也不建议使用root用户进行编译(Building LibreOffice as root is a very bad idea, use a regular user.)。日常操作也应该遵循最小权限原则,少用root账号直接操作。

3.3 c++环境错误

1
configure: error: C++ preprocessor "/lib/cpp" fails sanity check

解决方法:缺少c++编译器,安装gccg++

3.4 c++版本太低

1
configure: error: GCC 11.4.0 is too old, must be at least GCC 12

解决方法:

1
2
3
4
5
6
7
8
9
# 查看gcc g++版本
gcc --version
g++ --version

# 手动安装gcc12 g++12
sudo apt-get install gcc-12 g++-12

# 使用 update-alternatives 命令将gcc12,g++12设置为默认版本
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 100 --slave /usr/bin/g++ g++ /usr/bin/g++-12

3.5 ndk版本不匹配

1
* WARNING : Untested Android NDK version 26.2.11394342, only versions 23.* to 25.* have been used successfully. Proceed at your own risk.

解决:重新安装版本为23~25的ndk。

3.6 jdk版本太低

1
2
> Failed to apply plugin 'com.android.internal.application'.
> Android Gradle plugin requires Java 17 to run. You are currently using Java 11.

解决:Gradle与jdk版本不匹配,按照提示安装对应的jdk版本。

3.7 缺少某个工具或者库

1
2
3
4
configure: error: gperf not found but needed. Install it.

# 找不到gperf,安装gperf
sudo apt install gperf
1
2
3
4
5
configure: error: Package requirements (fontconfig >= 2.12.0) were not met:
No package 'fontconfig' found

# 需要fontconfig,安装libfontconfig1-dev
sudo apt install libfontconfig1-dev
1
2
3
4
configure: error: Python headers not found. You probably want to set both the PYTHON_CFLAGS and PYTHON_LIBS environment variables.

# 缺少 Python 头文件,安装python3-dev
sudo apt install python3-dev
1
2
3
4
5
configure: error: Could not find CUPS. Install libcups2-dev or cups-devel.
Error running configure at ./autogen.sh line 321.

# 缺少 CUPS 相关的开发包,安装 libcups2-dev
sudo apt install libcups2-dev
1
2
3
4
configure: error: msgfmt not found. Install GNU gettext, or re-run without languages.

# 缺少 msgfmt 工具,安装gettext
sudo apt install gettext

3.8 OpenSSL被禁用

1
* WARNING : OpenSSL has been disabled. No code compiled here will make use of it but system libraries may create indirect dependencies

项目根目录打开configure.ac文件,将openssl、nss设置为enable。OpenSSL 是默认的 TLS/SSL 实现,但仍然可能存在一些依赖于 NSS 的代码,因此把nss也打开。

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
sub_conf_defaults=" \
--build="$build_alias" \
--disable-cairo-canvas \
--disable-cups \
--disable-customtarget-components \
--disable-firebird-sdbc \
--disable-gpgmepp \
--disable-gstreamer-1-0 \
--disable-gtk3 \
--disable-gtk4 \
--disable-libcmis \
--disable-mariadb-sdbc \
--enable-nss \
--disable-online-update \
--disable-opencl \
--enable-openssl \
--disable-pdfimport \
--disable-postgresql-sdbc \
--disable-skia \
--disable-xmlhelp \
--enable-dynamic-loading \
--enable-icecream="$enable_icecream" \
--without-doxygen \
--without-tls \
--without-webdav \
--without-x \

如果提示No package 'nss' 'nss'found,则先通过以下命令安装它们。

1
sudo apt install libnss3-dev libnspr4-dev

3.9 Gradle Build 报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/home/zhg/zhg/core/android/source/src/java/org/libreoffice/SettingsActivity.java:33: 错误: 找不到符号
if(!BuildConfig.ALLOW_EDITING) {
^
符号: 变量 ALLOW_EDITING
位置: 类 BuildConfig

/home/zhg/zhg/core/android/source/src/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java:38: 警告: [deprecation] Handler中的Handler()已过时
mUiHandler = new Handler();
^
...
3 个错误
78 个警告

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':compileFullUIDebugJavaWithJavac'.
> Compilation failed; see the compiler error output for details.

解决:根据具体报错修复,最好导入Android studio 方便查定和定位,这里报错BuildConfig.ALLOW_EDITING找不到,
打开build.gradle文件,可以看到项目配置了三个flavor:

1
2
3
4
5
6
7
8
9
10
11
productFlavors {
strippedUI {
dimension "default"
buildConfigField 'boolean', 'ALLOW_EDITING', 'false'
}
strippedUIEditing {
dimension "default"
buildConfigField 'boolean', 'ALLOW_EDITING', 'true'
}
fullUI.dimension "default"
}

可以选择其中一个变体,例如strippedUI。重新使用./gradlew assembleStrippedUIDebug命令打包apk,当看到BUILD SUCCESSFUL说明打包成功,apk输出文件在/build/outputs/apk/strippedUI/debug目录下。

或者将fullUI变体增加ALLOW_EDITING字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
productFlavors {
strippedUI {
dimension "default"
buildConfigField 'boolean', 'ALLOW_EDITING', 'false'
}
strippedUIEditing {
dimension "default"
buildConfigField 'boolean', 'ALLOW_EDITING', 'true'
}
fullUI{
dimension "default"
buildConfigField 'boolean', 'ALLOW_EDITING', 'true'
}
}

重新使用./gradlew assembleDebug编译和打包所有变体。

参考链接

[1] 如何编译 LibreOffice - LibreOffice中文社区
[2] Development/BuildingForAndroid - The Document Foundation Wiki