AppImage格式

本文介绍了 AppImage格式,并连带介绍了几种别的打包方式。

参考

有多种方法可以在 Ubuntu或任何其他 Linux 发行版中安装软件。下载 .deb 或 .rpm 文件并双击它们以安装软件是最方便的方法之一。

然而近年来出现的AppImage格式则颠覆了以往的软件安装方法。AppImage是一种通用的软件包格式。通过将软件打包在 AppImage 中,开发人员只需提供一个文件即可“统管所有”。最终用户,可以在大多数(不一定是全部)现代 Linux 发行版中使用它。

AppImage 不以传统方式安装软件

一个典型的 Linux 软件会在不同的地方创建文件(例如/usr/bin 目录),需要 root 权限才能对系统进行这些更改。

AppImage 不这样做。事实上,AppImage 并没有真正安装软件。它是一个压缩映像,包含运行所需软件所需的所有依赖项和库。要运行软件,只需要执行 AppImage 文件即可,站在用户视角上没有解压、提取、安装等等系列操作。卸载软件也很简单,只需要删除 AppImage 文件即可。从这点上看,AppImage和Windows系统上的.exe文件很像。

当然,Linux系统有其特殊性,除了程序的二进制文件(称为ELF文件,可以阅读《PE/ELF/Mach-O之比较》 了解更多)以外,还有一些特殊的文本文件也是可以执行的,后者例如shell脚本以及带有shbang字符串的文本文件。例如,著名的python发行版Anaconda在Linux系统平台上提供的安装包就是shell脚本格式的(文件名格式大多为Anaconda3-xxxx-Linux-x86_64.sh),而QQ Linux版也提供shell脚本格式的安装包。这些shell脚本格式的安装包可以直接执行,其原理是调用系统的shell脚本解释器进行解压、提取、安装等操作。

既然调用系统的shell脚本解释器也可以实现这种单文件的程序,且不依赖于系统的包管理器,那么AppImage也是这么做到的吗?并不是如此。如下,我们使用 xxd 指令查看一个典型的AppImage文件的二进制数据编码,可以看见文件的开头4个字节是 .ELF ,这是ELF二进制文件的标志。也就是说,AppImage是个真真切切的二进制文件,而不是调用系统shell指令的脚本。

1
2
3
4
5
6
7
8
9
10
11
~$ xxd Prospect-Mail-0.5.2.AppImage |head
00000000: 7f45 4c46 0201 0100 4149 0200 0000 0000 .ELF....AI......
00000010: 0200 3e00 0100 0000 9046 4000 0000 0000 ..>......F@.....
00000020: 4000 0000 0000 0000 e8d7 0200 0000 0000 @...............
00000030: 0000 0000 4000 3800 0800 4000 2000 1f00 ....@.8...@. ...
00000040: 0600 0000 0500 0000 4000 0000 0000 0000 ........@.......
00000050: 4000 4000 0000 0000 4000 4000 0000 0000 @.@.....@.@.....
00000060: c001 0000 0000 0000 c001 0000 0000 0000 ................
00000070: 0800 0000 0000 0000 0300 0000 0400 0000 ................
00000080: 0002 0000 0000 0000 0002 4000 0000 0000 ..........@.....
00000090: 0002 4000 0000 0000 1c00 0000 0000 0000 ..@.............

AppImage 的一些功能或优点如下:

  • 发行版无关:可以在各种不同的 Linux 发行版上运行
  • 无需安装和编译软件:可一键运行
  • 无需root权限:不触及系统文件
  • 便携性:可以在任何地方运行,包括活动磁盘
  • 应用程序处于只读模式
  • 只需删除 AppImage 文件即可删除软件

在 Linux 中使用 AppImage

使用 AppImage 相当简单。它通过以下 3 个简单的步骤完成:

  • 下载 AppImage 文件
  • 使其可执行
  • 运行

有很多 AppImage 格式的软件可用。在AppImage Hub网站上列出了大量的软件,直接从上面下载.appimage 包即可。GIMP、Firefox等都在其中。

默认情况下,下载的 AppImage 文件没有执行权限。必须更改文件的权限才能使其可执行。在命令行中使用 chmod +x <file name> 即可赋予文件可执行权限。

在这以后,只需要在命令行中输入指令 ./<file name> (其中<file name> 是AppImage 文件名)即可运行。当然,在图形界面中双击运行也是可以的。

必须说明的一点是,尽管AppImage在设计时就考虑兼容更多的Linux版本,但依然存在一些Linux版本无法运行AppImage的情况,此时AppImage会抛出一段错误,提示缺少的系统组件,然后退出(如下面的终端输出)。尝试根据错误提示补充这些系统库有助于解决问题。

1
2
3
4
5
6
7
8
/home/cyclin/Prospect-Mail-0.5.2$ ./Prospect-Mail-0.5.2.AppImage
dlopen(): error loading libfuse.so.2

AppImages require FUSE to run.
You might still be able to extract the contents of this AppImage
if you run it with the --appimage-extract option.
See https://github.com/AppImage/AppImageKit/wiki/FUSE
for more information

AppImage 文件的内部结构

如前面所言,我在Windows的Linux子系统(WSL1)上运行AppImage得到了一段错误提示,经过查询这个问题无法解决,因为WSL1并不是一个标准的Linux环境。但报错提示给了我们一点提醒,可以用 --appimage-extract 这个参数对AppImage进行解包。

1
2
3
4
5
6
7
8
/home/cyclin/Prospect-Mail-0.5.2$ ./Prospect-Mail-0.5.2.AppImage --appimage-extract
squashfs-root/.DirIcon
squashfs-root/AppRun
squashfs-root/LICENSE.electron.txt
squashfs-root/LICENSES.chromium.html
squashfs-root/chrome-sandbox
... ...
squashfs-root/vk_swiftshader_icd.json

运行上述解包指令,我们得到了一个叫做 squashfs-root 的文件夹。Squashfs 是 Linux 的压缩只读文件系统,这里即是.AppImage文件的运行环境。其中,AppRun文件是软件执行的主要入口,其内部包含了一段shell脚本用于配置环境和调用可执行文件。在这个文件夹下面我们还可以看到许多以 .so 结尾的文件,这是Linux上的动态链接库文件,是软件运行所必须的。此外还有一些.dat.json 之类的配置文件和数据文件。在 /usr 下面则是随软件一起打包的系统库文件,用以保证AppImage在大部分Linux系统上都可以正常执行。

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
/home/cyclin/Prospect-Mail-0.5.2/squashfs-root$ ls -F
AppRun* chrome_100_percent.pak* libEGL.so* libvulkan.so.1* prospect-mail.png@ usr/
LICENSE.electron.txt* chrome_200_percent.pak* libGLESv2.so* locales/ resources/ v8_context_snapshot.bin*
LICENSES.chromium.html* chrome_crashpad_handler* libffmpeg.so* prospect-mail* resources.pak* vk_swiftshader_icd.json*
chrome-sandbox* icudtl.dat* libvk_swiftshader.so* prospect-mail.desktop* snapshot_blob.bin*

/home/cyclin/Prospect-Mail-0.5.2/squashfs-root$ head -n 20 AppRun
#!/bin/bash
set -e

if [ ! -z "$DEBUG" ] ; then
env
set -x
fi

THIS="$0"
# http://stackoverflow.com/questions/3190818/
args=("$@")
NUMBER_OF_ARGS="$#"

if [ -z "$APPDIR" ] ; then
# Find the AppDir. It is the directory that contains AppRun.
# This assumes that this script resides inside the AppDir or a subdirectory.
# If this script is run inside an AppImage, then the AppImage runtime likely has already set $APPDIR
path="$(dirname "$(readlink -f "${THIS}")")"
while [[ "$path" != "" && ! -e "$path/$1" ]]; do
path=${path%/*}

其他注意事项

1. 打包不好的AppImage即使有执行权限也不会运行

AppImage 的概念是将所有依赖项都包含在包本身中。但是,如果开发人员认为他已经打包了所有依赖项但实际上并没有发生呢?

在这种情况下,即使授予 AppImage 执行权限也无法运行,就像前面所说的那种情况一样。对于这种情况,用户首先可以尝试根据报错信息安装缺少的系统库;如果依然无济于事,则应该联系开发人员并告知其这个问题。

2.桌面集成

当运行 AppImage 文件时,某些软件可能会提示“安装桌面文件”。如果选择是, AppImage 将像常规安装的应用程序一样与 Linux 系统进行集成。

这意味着这个AppImage软件可以通过 Unity 或 GNOME 桌面进行搜索。可以在菜单中找到它的程序图标(如下图)。

image.png

拓展:

1、python和java的打包

AppImage的这种打包方式看起来很眼熟,它相当于把一个软件要用到的所有东西全部放在了一个压缩包里,而不管其中有没有用到系统库,这一点倒是和python的Pyinstaller很像。

众所周知,python是解释型语言,一段python代码必须在python解释器的处理下才能执行,无法像C++、Go这类编译型语言一样编译到原生二进制可执行文件格式。然而,如果要用python开发一个软件给很多人用,而用户电脑上不一定有python环境,该怎么办呢?总不可能一个一个用户都手把手教他们安装python吧。所以Pyinstaller应运而生,它将.py程序源码、程序用到的各个python模块,以及一个python解释器全部打包到一个.exe文件当中,从而让最终得到的.exe文件就像普通的软件一样开箱即用。

要了解更多,可以参考文章 《pyinstaller安装与使用——那些我踩过的坑》 以及 Pyinstaller官方文档

Java也是如此。一般来说,一个java程序需要编译为字节码文件(.class,通常一个类生成一个文件,因此一个java程序可能会编译出多个字节码文件),这虽然也是一种二进制格式,但无法在系统上原生运行,需要一个java虚拟机(Java Runtime, jre)提供运行环境。更常见的打包方式是使用 jar 工具将多个 .class 打包为一个.jar文件,后者包含了java程序的所有字节码文件以及一些必要的资源文件。然而即使是 .jar 文件,也需要jre的运行时环境。

那么,如果用户电脑上没有jre运行环境,该怎么办呢?思路和Pyinstaller一致,就是在打包的时候把jre一起打包进去。有一些公司会简单粗暴的把jre打包,然后发布版本的时候,再安装程序里写一个脚本,偷偷临时添加jre的环境变量(如何将Java打包成exe文件在没有JRE环境的电脑上执行? - 知乎用户的回答 )。当然,现在也有一些高级工具可以自动进行jre的打包操作,例如exe4j 。 要了解更多,可以参考文章 《Java打包成exe 在没有JRE环境的电脑上运行》

2、二进制程序在Windows和Linux上的跨平台执行

参考:《大新闻:C语言二进制程序同时“跨平台”在Windows和Linux上跑了》 - 空童的文章 - 知乎

大新闻!C语言二进制程序同时“跨平台”在Windows和Linux上跑了!!!

这不是标题党,而是去年在科技界真实发生的新闻。湾区大佬Justine Tunney实现了这个伟大的目标。她做了个跨平台的C标准库Cosmopolitan Libc,能够把C语言程序变成为“一次编译到处运行的语言”(build-once run-anywhere language)。没错,就是Java当年叫嚣的口号。不过,这里的C可执行文件可不需要解释器或者虚拟机,而是真正的本地运行的二进制文件,POSIX可运行的多语言格式,能够本地运行于Linux + Mac + Windows + FreeBSD + OpenBSD + NetBSD + BIOS,7大平台。

为了描述这种C语言的二进制跨平台文件格式,她甚至给它取了个名字:APE,即αcτµαlly pδrταblε εxεcµταblε。官网的字体就是那样写的,但读起来感觉就是:Actually portable executable。这个名字完美的契合了“build-once run-anywhere”的口号。

从官网的介绍来看,Cosmopolitan Libc打包了C语言库的很多东西,因此编译出的文件有更强的兼容性。但对于二进制文件来说,Linux平台上的ELF格式和Windows平台上的PE格式并不是一个东西,APE是怎么做到让Linux和Windows同时识别出来的呢?

答案藏在文件头里。在Windows系统下,exe文件(PE格式,“portable Execute”)为了保持和DOS系统的兼容性,其文件以MZ两个字母开头,如下:

1
2
3
4
5
6
7
8
9
10
(base) C:\Users\ab124\bin>xxd bunzip2.exe|head 
00000000: 4d5a 9000 0300 0000 0400 0000 ffff 0000 MZ..............
00000010: b800 0000 0000 0000 4000 0000 0000 0000 ........@.......
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000030: 0000 0000 0000 0000 0000 0000 8000 0000 ................
00000040: 0e1f ba0e 00b4 09cd 21b8 014c cd21 5468 ........!..L.!Th
00000050: 6973 2070 726f 6772 616d 2063 616e 6e6f is program canno
00000060: 7420 6265 2072 756e 2069 6e20 444f 5320 t be run in DOS
00000070: 6d6f 6465 2e0d 0d0a 2400 0000 0000 0000 mode....$.......
00000080: 5045 0000 6486 0b00 e626 7364 0000 0000 PE..d....&sd....

在Linux系统下,ELF格式(“Executable and Linkable Format”)的文件以 .ELF 三个字母开头。

1
2
3
4
5
6
7
8
9
10
11
~/.local/bin$ xxd gzip |head 
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
00000010: 0300 3e00 0100 0000 c042 0000 0000 0000 ..>......B......
00000020: 4000 0000 0000 0000 c0f9 0400 0000 0000 @...............
00000030: 0000 0000 4000 3800 0d00 4000 2800 2700 ....@.8...@.(.'.
00000040: 0600 0000 0400 0000 4000 0000 0000 0000 ........@.......
00000050: 4000 0000 0000 0000 4000 0000 0000 0000 @.......@.......
00000060: d802 0000 0000 0000 d802 0000 0000 0000 ................
00000070: 0800 0000 0000 0000 0300 0000 0400 0000 ................
00000080: 1803 0000 0000 0000 1803 0000 0000 0000 ................
00000090: 1803 0000 0000 0000 1c00 0000 0000 0000 ................

然而APE二进制跨平台文件格式则另辟蹊径,通过hack下文件头,把pe文件头和符合 UNIX Sixth Edition shell 规范的脚本混合在了一起,Windows程序加载器识别其为PE文件,Linux程序加载器识别其为shell脚本,从而实现跨平台运行。

1
2
3
4
5
6
7
8
9
10
11
/home/cyclin/APE/cosmocc/bin$ xxd make |head 
00000000: 4d5a 7146 7044 3d27 0a0a 0010 00f8 0000 MZqFpD='........
00000010: 0000 0000 0001 0008 4000 0000 0000 0000 ........@.......
00000020: 0000 0000 0000 0000 2720 3c3c 276a 7573 ........' <<'jus
00000030: 7469 6e65 306c 6b73 6a6b 270a 580e 0100 tine0lksjk'.X...
00000040: b240 eb00 eb14 9090 eb06 4883 ec08 31d2 .@........H...1.
00000050: bd00 00eb 05e9 2545 0000 fc0f 1f87 3ee0 ......%E......>.
00000060: bf00 7031 c98e c1fa 8ed7 89cc fb0e 1fe8 ..p1............
00000070: 0000 5e81 ee72 00b8 0002 5050 0731 ffb9 ..^..r....PP.1..
00000080: 0002 f3a4 0f1f 87d2 ffea 8e20 0000 8ed9 ........... ....
00000090: b900 1bb8 5000 8ec0 31c0 31ff f3aa 80fa ....P...1.1.....

如上面的代码块所示,使用xxd指令查看APE格式的make程序前100个字节,可以看到这个程序以MZqFpD 开头,在 Windows PE 格式下它就是普通的 PE 格式头,但它又可以被解释为汇编指令 pop %r10 ; jno 0x4a ; jo 0x4a ,再加上后续的指令,就可以实现判断当前是从用户空间启动还是被 boot,从而跳转到对应的代码。这真是个精妙的设计。

当然,对于这个项目,也有一些不同的声音,例如有人就将其评价为“毫无意义,在不同的系统用的是不同的系统api,合成一个畸形的执行程序根本改变不了任何事”。更有人认为APE这个项目仅仅停留在helloworld阶段,涉及更深层次系统调用时就无法解决系统兼容性的问题。不管怎么说,这个项目为我们提供了一个新的视角,可以通过一些巧妙的手法绕过系统限制实现平台之间的兼容。

事实上,计算机技术发展到今天,各个操作系统都在蓬勃发展,一方面为大家带来了更多选择,另一方面则由于系统之间的不兼容造成割裂。许多跨平台技术的出现正是为了弥合系统之间的不兼容性,而这些技术,从HTML到Java,从QT到electron,从WINE再到APE,不论最终是否真正解决了软件跨平台的问题,都对今天的计算机行业造成了很深远的影响。