From FedoraProject


此页面包含 Packaging:ScriptletSnippets 的 zh_CN 翻译,由于不具有 ScriptletSnippets 的编辑权限,故在此保存翻译。

RPM 打包脚本综述

Rpm spec 文件有几个部分,允许软件包执行代码来完成安装和卸载操作。这些软件包中的脚本片段大多用于更新系统信息。此页面提供 RPM 脚本片段概述和一些常见软件包的 spec 脚本片段的示例。更完整的脚本片段,请参阅 Maximum RPM book

默认 Shell

在 Fedora ,您可以直接使用默认的 bash shell (/bin/sh)。因此,所有脚本片段可以安全地在 bash 中运行。

语法

基本语法在 %build, %install 以及 rpm spec 文件的其他部分都是一致的。脚本支持一个特殊标记, -p 允许脚本直接调用一个程序,而不必启用 shell 来运行程序。(即 %post -p /sbin/ldconfig)

脚本片段还传递一个参数,用于表示本软件包的个数。执行特定动作时,通过向 $1 传递不同值,来表示不同动作(安装/升级/卸载),除了 %pretrans 和 %posttrans 它们的 $1 为 0 (rpm 4.4+ 支持 %pretrans 和 %posttrans)。对于安装、升级和卸载,所传递的参数值如下表所示:

安装(install) 升级(update/upgrade) 卸载(remove/erase)
 %pretrans $1 == 0 $1 == 0 (N/A)
 %triggerprein 安装本包: $1 == 0, $2 == 1
安装目标包: $1 == 1, $2 == 0
$1 == 1, $2 == 1 (N/A)
 %pre $1 == 1 $1 == 2 (N/A)
 %post $1 == 1 $1 == 2 (N/A)
 %triggerin $1 == 1, $2 == 1 升级本包: $1 == 2, $2 == 1
升级目标包: $1 == 1, $2 == 2
(N/A)
 %triggerun (N/A) $1 == 1, $2 == 1 卸载本包: $1 == 0, $2 == 1
卸载目标包: $1 == 1, $2 == 0
 %preun (N/A) $1 == 1 $1 == 0
 %postun (N/A) $1 == 1 $1 == 0
 %triggerpostun (N/A) 升级目标包: $1 == 1, $2 == 1 卸载目标包: $1 == 1, $2 == 0
 %posttrans $1 == 0 $1 == 0 (N/A)

注意,如果安装相同软件包的多个版本,这些参数值将会不同(这发生于同时安装包,如 kernel 和 multilib 包。然而,它会引发错误,防止软件包升级完成)。所以,使用以下结构的脚本是个好主意:

%pre
if [ $1 -gt 1 ] ; then   # -gt大于
fi

...对于 %pre 和 %post 脚本,检查它的值等于 2。

除了一些特殊情况(如果有的话),我们希望所有的脚本退出时返回 0。因为 rpm 默认配置不使用 -e 参数执行 shell 脚本,不包括明确的 exit 调用(可能出现非 0 返回值),在脚本中,最后一个命令的退出状态决定了脚本片段的退出状态。大多数命令包含 "|| :" 后缀,这会强制以 0 状态退出,无论命令是否可以正常工作。通常,最重要的是在脚本片段的最后一个命令添加此后缀,或在脚本的最后一行添加 ":" 或 "exit 0" 命令。注意,根据情况,使用其他的错误检测/预防措施可能更合适,事先运行一些命令进行检查,检查成功才执行之后的命令。

脚本片段以非 0 状态退出会中断安装/升级/删除操作,避免事务中的包执行进一步的操作(见脚本片段命令部分),这可以防止旧包被删除,同时也在 rpmdb 留下了重复记录,在文件系统上留下了无主文件。某些情况下,事务中的脚本片段执行失败,可能导致部分安装失败(不会中断)。这往往局限于,软件包不影响事务继续执行,而此时中断安装之后的某些包,会导致更严重的系统问题。

脚本片段命令

%pre 和 %post 脚本片段分别在软件包安装前和安装后执行。%preun 和 %postun 脚本片段分别在软件包卸载前和卸载后执行。%pretrans 和 %posttrans 脚本片段分别在软件包事务开始和结束时执行。升级软件包时,按如下顺序执行脚本片段:

  1. 检查软件包依赖、下载软件包和 DRPM
  2. (all)%pretrans:事务开始时,执行新软件包的此段代码
  3. ...... (操作其它软件包) ......
  4. (any)%triggerprein:此包的新版本安装之前,触发此包或其他包的脚本(如果有)
  5. (new)%triggerprein:指定的其他软件包安装之前,触发此脚本
  6. (new)%pre:执行新软件包的 %pre 脚本
  7. ...... (安装所有新文件) ......
  8. (new)%post:执行新软件包的 %post 脚本
  9. (any)%triggerin:安装此软件包时,触发此包或其他包的脚本(如果有)
  10. (new)%triggerin:安装指定的其他软件包时,触发此脚本
  11. (old)%triggerun:卸载指定的其他软件包的旧版本时,触发此脚本
  12. (any)%triggerun:卸载此软件包的旧版本时,触发此包或其他包的脚本(如果有)
  13. (old)%preun:执行旧软件包的 %preun 脚本
  14. ...... (删除所有旧文件) ......
  15. (old)%postun:执行旧软件包的 %postun 脚本
  16. (old)%triggerpostun:指定的其他软件包的旧版本已卸载之后,触发此脚本
  17. (any)%triggerpostun:此包的旧版本已卸载之后,触发其他包的脚本(如果有,此包脚本不运行)
  18. ...... (操作其它软件包) ......
  19. (all)%posttrans:事务结束时,执行新软件包的此段代码
  20. 验证软件包,Done

Trigger 示例:

Trigger 用于在操作指定包时,运行您包中的一些代码。通常因为您的包使用其他包的服务,或为其他包提供服务。参考以下的 Trigger 部分。提供以下示例以帮助理解:

%triggerin -p /usr/bin/perl -- ruby > 2.0, perl > 5.20  # -p 指定其他解释器

以下情况触发此段代码:

  • 已安装此包,ruby 或 perl 被安装/升级时
  • 已安装 ruby 或 perl,此包被安装/升级时

编写脚本片段

以下是一些编写高质量打包脚本的建议。

在脚本片段间保存状态

有时脚本需要保存之前脚本的一些状态,以便在之后运行脚本时使用。这常用于对脚本片段进行优化。例如:

  • 如果 %posttrans 需要在软件包升级时注销一些信息,但旧软件包删除时,包含这些信息的文件也一并被删除,%pre%post 脚本片段需要在文件中保存这些信息,以便 %posttrans 脚本可以访问。
  • 如果我们只想让 %posttrans 中的程序在每次事务时工作,我们需要编写一个标志文件,使 %posttrans 执行相应动作。

为了解决这些问题,脚本片段需要输出供 %posttrans 使用的信息。我们建议使用 %{_localstatedir}/lib/rpm-state/ 子目录保存信息。例如, 当安装 eclipse 插件时,脚本需要在 %{_localstatedir}/lib/rpm-state/eclipse/ 创建一个文件。 %posttrans 运行脚本检查文件是否存在。如果存在,执行相应动作,并删除文件。这样,每次事务时脚本只执行一次操作。

Note.png
在 Fedora 中,我们要求 %{_localstatedir}/lib/rpm-state/ 目录属于 rpm 或 filesystem 软件包。使用事务时,软件包需要创建自己的 %{_localstatedir}/lib/rpm-state/ 子目录。

Macros

复杂的脚本片段可以记录在 rpm 宏中。这有两个好处:

  1. 标准包的作者只需要记住宏,不需要记住它复杂的内容。
  2. 宏的实现可能改变,而无需更新包。

编写宏时,FPC 需要审阅宏(指南中记录了打包者需要做什么)。

原则上,宏不能包含脚本片段的起始标记(如 %pre)。这也意味着,一个宏不能同时定义 %pre%post 需要执行的动作。相反,需要编写一个宏在 %pre 执行,编写另一个宏在 %post 执行。此原则使所有 spec 文件可以以同样的方式使用宏,即使它们已定义了 %pre%post

Trigger

Trigger 用于在操作指定包时,运行您包中的一些代码。通常因为您的包使用其他包的服务,或为其他包提供服务。Trigger 语法如下:

%trigger{un|in|postun} [[-n] <subpackage>] [-p <program>] -- <trigger>

有一个很好的例子。假设您正在为 Emacs 和 Xemacs 编辑器打包一个很好的插件。它可以和这些编辑器一起工作,但根据不同编辑器,需要做不同配置。安装插件时,可以检查 Emacs 和 Xemacs 是否已安装,并配置插件。但是,如果用户稍后安装 Xemacs,那么会发生什么呢?您的插件在 Xemacs 不可用,除非用户卸载并重装插件。通过触发器,可以告诉此包,“已安装 Xemacs,需要执行插件配置”。怎么样,很赞吧!

%triggerin -- emacs  # 以下情况执行:已安装此包, emasc 被安装/升级时;已安装 emacs,此包被安装/升级时。
# 安装插件相关代码
%triggerin -- xemacs # 以下情况执行:已安装此包,xemasc 被安装/升级时;已安装 xemacs,此包被安装/升级时。
# 安装插件相关代码
%triggerun -- emacs  # 以下情况执行:已安装此包, emacs 被卸载时;已安装 emacs,此包被卸载时。
# 卸载插件相关代码
%triggerun -- xemacs # 以下情况执行:已安装此包,xemacs 被卸载时;已安装 xemacs,此包被卸载时。
# 卸载插件相关代码

rpm 向触发器脚本传递了两个参数。$1 表示脚本完成时本软件包的个数。$2 表示脚本完成时被触发的软件包的个数。

代码片段

共享库

安装共享库需要运行 /sbin/ldconfig 来更新动态链接器的缓存文件。可调用以下语句:

%post
/sbin/ldconfig
%postun
/sbin/ldconfig

仅需要运行 ldconfig 时,可使用 -p 选项避免运行 shell:

%post -p /sbin/ldconfig
%postun -p /sbin/ldconfig

建议使用第二种方式,这样会自动添加 /sbin/ldconfig 依赖到软件包(另外,可以避免启用不必要的 Shell 进程来执行脚本)。

用户和组

这些问题在 separate page 讨论。

服务

Init 脚本约定

完整 SysV 风格的 init 脚本指南: Packaging/SysVInitScript
脚本片段细节: Packaging/SysVInitScript#InitscriptScriptlets

GConf

GConf 是 GNOME 桌面目前使用的配置方案。程序使用的默认值保存在 [NAME].schemas 文件,并安装至 %{_sysconfdir}/gconf/schemas/[NAME].schemas。gconf 守护进程注册并监控这些配置,并在配置变化时通知应用程序。schema 文件中包含了每个值的解释(使用 gconf-editor 查询数据库时显示这些信息)。

在构建包的过程中,必须禁止安装 schema,并在实际安装时,使用 gconf 守护进程注册 [NAME].schemas 值;在删除时,注销这些值。脚本片段包含以下 4 个步骤。

创建包时,禁止安装 Gconf schema:

%install
export GCONF_DISABLE_MAKEFILE_SCHEMA_INSTALL=1
make install DESTDIR=$RPM_BUILD_ROOT
...

GCONF_DISABLE_MAKEFILE_SCHEMA_INSTALL 环境变量在包构建过程中,禁止安装 schema。另一种方法是通过 configure 标识设置:

%build
%configure --disable-schemas
...

不幸的是,此选项需要上游开发者修改 Makefile.am 进行处理 。如果 Makefile.am 未配置,此选项不会生效,需使用环境变量代替。

第 2 步:

BuildRequires: GConf2
Requires(pre): GConf2
Requires(post): GConf2
Requires(preun): GConf2
...
%pre
%gconf_schema_prepare schema1 schema2
%gconf_schema_obsolete schema3

在这一部分,我们使用 2 个宏卸载/更新旧 schema。

%gconf_schema_prepare 用于任何 GConf schema。它需要卸载当前已安装的 schema。它需要不包含路径和后缀的空格分隔的 schema 列表。注意宏幕后的工作,这个宏在 %post 中只处理已更改的 Gconf schema。

%gconf_schema_obsolete 标记废弃包之前提供的 schema。如果系统存在旧 schema,它会将其注销。如果旧 schema 不存在,则不执行动作。此宏接受空格分隔的 schema 列表。以下示例可能用于软件包已改名的情况。如果旧 schema 名为 foo.schemas,新 schema 名为 foobar.schemas,你应该使用:

%gconf_schema_prepare foobar
%gconf_schema_obsolete foo

下一步,对新安装的 schema 进行处理:

%post
%gconf_schema_upgrade schema1 schema2

%gconf_schema_upgrade 接受以空格分隔的 schema 列表。在宏幕后,它实际注册新版本的 schema 并注销旧版本。

最后一步,在包删除时注销 schema:

%preun
%gconf_schema_remove schema1 schema2

当包升级时,rpm 调用 %pre 脚本来注册和注销 schema。当包卸载时,使用 %preun 脚本。%gconf_schema_remove 接受空格分隔的 schema 列表,并提供删除方法。

为已修改的宏重建包

当宏修改时,使用这些宏的软件包必须重建,以适应这些变化。repoquery 命令用于查找包含 schema 的软件包:

repoquery --whatprovides "/etc/gconf/schemas/*" |sort |uniq |wc -l

EPEL 说明

EPEL 不包含 macros.gconf2,所以请按此说明操作: Packaging:EPEL#GConf

GSettings Schema

GSettings是 GNOME 3 桌面的配置系统。它代替了 GNOME 2 中使用的 GConf 系统。GSettings 支持可插拔后端,本地的 GNOME 使用 DConf 存储设置。GSettings API 和工具属于 glib2 软件包。

程序使用的 GSettings schema 文件保存在 %{_datadir}/glib-2.0/schemas 目录。schema 文件是扩展名为 .gschema.xml 的 XML 文件。运行时,GSettings 使用已编译的二进制缓存(平台无关),缓存通过 glib-compile-schemas 创建。只要修改 schema,就需要运行 /usr/bin/glib-compile-schemas 更新缓存。

%postun
if [ $1 -eq 0 ] ; then
    /usr/bin/glib-compile-schemas %{_datadir}/glib-2.0/schemas &> /dev/null || :
fi

%posttrans
    /usr/bin/glib-compile-schemas %{_datadir}/glib-2.0/schemas &> /dev/null || :

gdk-pixbuf 加载器

gdk-pixbuf 库属于 gdk-pixbuf2 软件包。它用于在 GNOME 中加载各种图像格式。gdk-pixbuf 可通过载入模块来扩展对图像格式的支持。这些模块必须安装在 %{_libdir}/gdk-pixbuf-2.0/2.10.0/loaders。为了避免载入所有模块,gdk-pixbuf 在 %{_libdir}/gdk-pixbuf-2.0/2.10.0/loaders.cache 文件中保存模块信息缓存。当模块更改时,需要调用 /usr/bin/gdk-pixbuf-query-loaders 程序更新缓存信息。gdk-pixbuf-query-loaders 包含 -32 和 -64 版本,用于生成对应架构的缓存。

维护缓存文件的脚本是:

%postun
    /usr/bin/gdk-pixbuf-query-loaders-%{__isa_bits} --update-cache &> /dev/null || :

%post
if [ $1 -eq 1 ] ; then
    # For upgrades, the cache will be regenerated by the new package's %postun
    /usr/bin/gdk-pixbuf-query-loaders-%{__isa_bits} --update-cache &> /dev/null || :
fi

注意, %{__isa_bits} 宏根据软件包架构返回 32 或 64。

GTK+ 模块

GTK+ 工具包(gtk3)支持通过加载模块来提供主题引擎,输入方法,打印后端或其他功能。这些模块必须安装至 %{_libdir}/gtk-3.0 或 %{_libdir}/gtk-3.0/3.0.0 目录。对于输入法,GTK+ 使用 %{_libdir}/gtk-3.0/3.0.0/immodules.cache 缓存记录输入法模块信息。当修改输入法时,需要调用 gtk-query-immodules-3.0 更新缓存信息。gtk-query-immodules-3.0 包含 -32 和 -64 版本,用于生成对应架构的缓存。

维护缓存文件的脚本是:

%postun
/usr/bin/gtk-query-immodules-3.0-%{__isa_bits} --update-cache &> /dev/null || :

%post
if [ $1 -eq 1 ] ; then
    # For upgrades, the cache will be regenerated by the new package's %postun
    /usr/bin/gtk-query-immodules-3.0-%{__isa_bits} --update-cache &> /dev/null || :
fi

3.0版本的可执行文件,是由于 gtk2 包含相同功能的工具(gtk-query-immodules-2.0)。注意, %{__isa_bits} 宏根据软件包架构返回 32 或 64。

GIO 模块

GIO 共享库属于 glib2 软件包。它是 GNOME 中的底层堆栈。GIO 可以通过模块中的 extension points 实现扩展。这些扩展模块必须安装至 %{_libdir}/gio/modules。为了避免载入所有模块,GIO 在相同目录的 giomodule.cache 文件中记录可用模块的缓存信息。当模块改变时,通过调用 gio-querymodules 程序更新缓存信息。 gio-querymodules 包含 -32 和 -64 版本,用于生成对应架构的缓存。

维护缓存文件的脚本是:

%postun
/usr/bin/gio-querymodules-%{__isa_bits} %{_libdir}/gio/modules &> /dev/null || :

%post
# We run this after every install or upgrade because of a cornercase
# when installing the second architecture of a multilib package 
/usr/bin/gio-querymodules-%{__isa_bits} %{_libdir}/gio/modules || :

注意, %{__isa_bits} 宏根据软件包架构返回 32 或 64。

Texinfo

GNU 项目和许多其他程序使用 texinfo 文件格式保存文档。这些 info 文件保存在 /usr/share/info/。安装或删除软件包时,install-info 添加新文件至 info 索引中,并在卸载时删除它们。

Requires(post): info
Requires(preun): info
...
%post
/sbin/install-info %{_infodir}/%{name}.info %{_infodir}/dir || :

%preun
if [ $1 = 0 ] ; then
  /sbin/install-info --delete %{_infodir}/%{name}.info %{_infodir}/dir || :
fi

这两个脚本使用 install-info 在安装时将 info 页的信息添加至 info 索引文件,并在卸载时删除索引信息。 "|| :" 可防止在系统已配置为不安装任何 %doc 文件,或使用只读挂载 %_netsharedpath /usr/share时,命令执行失败。

Scrollkeeper

在当前 Fedora 中, scrollkeeper 已被 rarian 代替。rarian 不需要使用脚本片段进行处理。关于 EPEL 源的进一步说明,请访问 Packaging:EPEL#Scrollkeeper

desktop-database

当 desktop entry 包含 MimeType 关键字时,应添加以下脚本。

%post
/usr/bin/update-desktop-database &> /dev/null || :

%postun
/usr/bin/update-desktop-database &> /dev/null || :

注意:对于 Fedora Core 5 之后的版本,使用相同的 mimeinfo 文件和 gtk-icon-cache。即 spec 文件不需要为此添加 Require desktop-file-utils 。对于旧发行版,应添加以下语句:

Requires(post): desktop-file-utils
Requires(postun): desktop-file-utils

(访问 http://bugzilla.redhat.com/180898http://bugzilla.redhat.com/180899)

mimeinfo

当软件包在 %{_datadir}/mime/packages 安装 XML 文件时,需使用以下脚本。

%post
/bin/touch --no-create %{_datadir}/mime/packages &>/dev/null || :

%postun
if [ $1 -eq 0 ] ; then
  /usr/bin/update-mime-database %{_datadir}/mime &> /dev/null || :
fi

%posttrans
/usr/bin/update-mime-database %{?fedora:-n} %{_datadir}/mime &> /dev/null || :

注意,与 gtk-update-icon-cache 代码类似,这些脚本只在安装 shared-mime-info 软件包时运行,但不需要指定 Requires: shared-mime-info 。如果未安装 shared-mime-info,update-mime-database 不会运行。然而这并不重要,因为大部分系统默认安装 shared-mime-info。

Icon 缓存

如果应用程序在 %{_datadir}/icons/ 的子目录(如 hicolor)安装图标,则必须更新图标缓存,以便菜单正常显示图标。这包括:更新图标上层目录的时间戳,并运行 gtk-update-icon-cache。'touch' 顶层目录以便兼容 Icon theme specification 的环境可以刷新缓存,并且 gtk-update-icon-cache 的运行也需要基于目录的时间戳。

注意,不需要为此添加依赖关系。如果 gtk-update-icon-cache 不可用,则不会更新图标缓存,同上,如果 "touch" 不可用,则不会更新图标缓存。不添加 gtk-update-icon-cache(即 gtk2 >= 2.6.0)或 "touch" 依赖,使软件包(spec)对系统的兼容性更好。例如,旧发行版或最小化安装方式,通常在 spec 文件、rpmdb 和源的元数据中不包含这些包的条目。

%post
/bin/touch --no-create %{_datadir}/icons/hicolor &>/dev/null || :

%postun
if [ $1 -eq 0 ] ; then
    /bin/touch --no-create %{_datadir}/icons/hicolor &>/dev/null
    /usr/bin/gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || :
fi

%posttrans
/usr/bin/gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || :

Systemd

包含 systemd unit 文件的软件包,需要使用脚本以确保妥善处理这些服务。默认服务可以启用或禁用。确定哪些情况下可以启动服务,请参考 FESCo 策略: Starting_services_by_default。升级包时,如果服务正在运行,则会重启服务;如果服务未运行,则不启动它。同时,如果服务当前被禁用,则服务不会启用。

新软件包

Note.png
什么是新软件包?
在这段文字中,一个新软件包指不包含任何 SysV init 脚本文件的包。

Scriptlets

Fedora 18+ 之后的 systemd 软件包,提供了一系列宏来帮助处理 systemd 服务。这些宏的功能相当于旧版本 Fedora 的启动脚本,但它还加入了 systemd "presets" 支持,参考文档: https://fedoraproject.org/wiki/Features/PackagePresets
注意不要使用 %systemd_requires 宏。

Requires(post): systemd
Requires(preun): systemd
Requires(postun): systemd
BuildRequires: systemd

[...]
%post
%systemd_post apache-httpd.service

%preun
%systemd_preun apache-httpd.service

%postun
%systemd_postun_with_restart apache-httpd.service 

有些服务不支持重启(如 D-Bus 和某些存储守护进程)。如果你的服务不需要在升级时重启,使用以下 %post 脚本代替以上脚本:

%postun
%systemd_postun

如果你的软件包包含的一个或多个 systemd unit,需要在安装时默认启用,它们 必须 符合 Fedora preset policy

宏的细节信息,请参考以下链接:
http://cgit.freedesktop.org/systemd/systemd/tree/src/core/macros.systemd.in
http://www.freedesktop.org/software/systemd/man/daemon.html

将软件包从 SysV init 脚本迁移至 Systemd Unit

当从包含 SysV init 脚本的软件包升级至包含 systemd unit 文件的软件包时,将使用新的管理策略,不迁移用户之前的配置。因此,可以简单的使用以上脚本,不必担心将 SysV 的相关信息迁移到 systemd。

Warning.png
在发行版之间迁移
软件包严禁从 systemd 更新到一个不包含 systemd 的版本。由于迁移将重置系统管理服务。只允许在 Fedora 版本间进行迁移。


Shells

/etc/shells 配置文件用于控制应用程序是否可以作为系统的用户登录 shell。它包含用于系统的有效 shells。如果你制作了一个新的 shell 软件包,你需要将 shell 添加至此文件。详细参考: man 5 SHELLS 获得更多信息。

由于此配置可以编辑,所以首先需要确定配置已包含相关路径。如果路径不存在,就需要查看 shell 的二进制文件路径。自从 Fedora 17 应用 UsrMove Feature 后,/bin 作为 /usr/bin 目录的软链接,我们需要导出所有路径至 /etc/shells 文件。以下脚本,以打包名为 "foo" 的 shell 为例:

%post
if [ "$1" = 1 ]; then
  if [ ! -f %{_sysconfdir}/shells ] ; then
    echo "%{_bindir}/foo" > %{_sysconfdir}/shells
    echo "/bin/foo" >> %{_sysconfdir}/shells
  else
    grep -q "^%{_bindir}/foo$" %{_sysconfdir}/shells || echo "%{_bindir}/foo" >> %{_sysconfdir}/shells
    grep -q "^/bin/foo$" %{_sysconfdir}/shells || echo "/bin/foo" >> %{_sysconfdir}/shells
fi

%postun
if [ "$1" = 0 ] && [ -f %{_sysconfdir}/shells ] ; then
  sed -i '\!^%{_bindir}/foo$!d' %{_sysconfdir}/shells
  sed -i '\!^/bin/foo$!d' %{_sysconfdir}/shells
fi

参考

http://wiki.wenyinos.org/post-39.html