Ubuntu 下编译LibreOffice Android版

前言

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,如果下载不来下,参考下一节的常见错误解决

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

1.2 安装jdk

bash 复制代码
# 查看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,因此这里选择手动安装。

bash 复制代码
# 手动安装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需要用到的一些工具和库。执行编译环境检查时报错缺什么工具就补上。

bash 复制代码
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命令新建一个配置文件,

bash 复制代码
touch autogen.input

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

ini 复制代码
# 指定构建的发行版,本例是构建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选项并生成配置脚本。

bash 复制代码
./autogen.sh

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

bash 复制代码
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命令开始构建,编译耗时取决于下载软件包的速度及电脑配置。

bash 复制代码
make

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

bash 复制代码
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目录下。

css 复制代码
├── 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文件夹,输入以下命令:

bash 复制代码
 # 编译并打Debug包
 ./gradlew assembleDebug

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

bash 复制代码
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

groovy 复制代码
//以下是这些任务的说明:
//从${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错误

bash 复制代码
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发送数据的缓冲区大小

bash 复制代码
# 设置 Git 全局配置中的 http.postBuffer 参数
git config --global http.postBuffer 1024M

3.2 文件权限相关

bash 复制代码
 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++环境错误

bash 复制代码
configure: error: C++ preprocessor "/lib/cpp" fails sanity check

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

3.4 c++版本太低

bash 复制代码
configure: error: GCC 11.4.0 is too old, must be at least GCC 12

解决方法:

bash 复制代码
# 查看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版本不匹配

bash 复制代码
* 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版本太低

bash 复制代码
> 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 缺少某个工具或者库

bash 复制代码
configure: error: gperf not found but needed. Install it.

# 找不到gperf,安装gperf
sudo apt install gperf
bash 复制代码
configure: error: Package requirements (fontconfig >= 2.12.0) were not met:
    No package 'fontconfig' found
    
# 需要fontconfig,安装libfontconfig1-dev
sudo apt install libfontconfig1-dev
bash 复制代码
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
bash 复制代码
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
bash 复制代码
configure: error: msgfmt not found. Install GNU gettext, or re-run without languages.

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

3.8 OpenSSL被禁用

bash 复制代码
* 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也打开。

css 复制代码
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,则先通过以下命令安装它们。

bash 复制代码
sudo apt install libnss3-dev libnspr4-dev

3.9 Gradle Build 报错

bash 复制代码
/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:

gradle 复制代码
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字段:

gradle 复制代码
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中文社区](https://link.juejin.cn?target=https%3A%2F%2Fwww.libreofficechina.org%2Fhow-to-build-libreoffice-zh-cn%2F "https://www.libreofficechina.org/how-to-build-libreoffice-zh-cn/") \[2\] [Development/BuildingForAndroid - The Document Foundation Wiki](https://link.juejin.cn?target=https%3A%2F%2Fwiki.documentfoundation.org%2FDevelopment%2FBuildingForAndroid "https://wiki.documentfoundation.org/Development/BuildingForAndroid")

相关推荐
Yang-Never25 分钟前
Git -> Git使用Patch失败error: patch failed: patch does not apply的解决办法
android·git·android studio
woodWu2 小时前
Android编译时动态插入代码原理与实践
android
百锦再3 小时前
Android Studio 实现自定义全局悬浮按钮
android·java·ide·app·android studio·安卓
百锦再3 小时前
Android Studio 项目文件夹结构详解
android·java·ide·ios·app·android studio·idea
老码识土3 小时前
Kotlin 协程源代码泛读:Continuation
android·kotlin
行墨4 小时前
Replugin 的hook点以及hook流程
android·架构
一一Null4 小时前
Access Token 和 Refresh Token 的双令牌机制,维持登陆状态
android·python·安全·flask
_祝你今天愉快5 小时前
深入理解 Android Handler
android
pengyu6 小时前
【Flutter 状态管理 - 四】 | setState的工作机制探秘
android·flutter·dart
溪饱鱼6 小时前
DHgate爆火背后的技术原因
android·前端·ios