07 Package Definition Guide

zhangly 2021-03-15 17:34:10
Categories: > Tags:

概述

软件包的定义来自文件(package.py),位于每个包安装的根目录下。
比如包的存储位置为/packages/inhouse,则包"foo-1.0.0"的定义文件路径是:
/packages/inhouse/foo/1.0.0/package.py

这里是一个定义文件的例子:

name = 'sequence'

version = '2.1.2'

description = 'Sequence detection library.'

authors = ['ajohns']

tools = [
    'lsq',
    'cpq'
]

requires = [
    'python-2.6+<3',
    'argparse'
]

def commands():
    env.PATH.append("{root}/bin")
    env.PYTHONPATH.append("{root}/python")

uuid = '6c43d533-92bb-4f8b-b812-7020bf54d3f1'

软件包属性

包定义文件中的每一个变量都会成为已构建或已安装的软件包的属性。
所以你可以在包中添加任何自定义属性,不过有些属性并作为包的属性添加,
比如下面这个:

import sys

description = "This package was built on %s" % sys.platform

因为我们并不想让sys成为一个包属性,这会和python标准模块冲突。
所以不会成为包属性的包括:

包属性作为函数

包属性可以包装成函数,函数的返回值为属性值。

有两种类型的属性函数:
预绑定函数(early binding function)和后绑定函数(late binding function)。
它们分别使用装饰器@early@late来装饰。

包定义的commands函数是例外,它们是后期绑定,但与标准的函数属性不同,不能用上述的装饰器来装饰。

预绑定函数

预绑定函数使用@early装饰器,它们在构建时执行,任何包属性都可以作为预绑定函数来实现。
这是一个authors属性的示例,该函数返回包的git项目的贡献者:

@early()
def authors():
    import subprocess
    p = subprocess.Popen("git shortlog -sn | cut -f2",
                         shell=True, stdout=subprocess.PIPE)
    out, _ = p.communicate()
    return out.strip().split('\n')

预绑定函数还可以访问其它软件包的属性:

@early()
def description():
    # a not very useful description
    return "%s version %s" % (this.name, this.version)

注意:不要在预绑定函数中使用其它预绑定函数或后绑定函数的属性,否则会报错。

预绑定函数很方便的一点是,你可以使用任意的函数来替代,像这样:

def _description():
    return "%s version %s" % (this.name, this.version)

description = _description()

可用对象:

**注意:**预绑定函数实际上会在构建过程中执行多次,在构建期间进行一次,然后对每个变体进行一次。这是为了让预绑定函数可以根据比如build_variant_index等variant来更改它们的返回值。

比如你希望requires字段仅在运行时返回某个软件包(软件包构建的时候不需要返回那个软件包)。
可以这样写:

@early()
def requires():
    if building:
        return ["python-2"]
    else:
        return ["runtimeonly-1.2", "python-2"]

后绑定函数

后绑定函数作为函数保留在已安装的软件包定义中,
并且在第一次调用的时候才进行求值(然后会将返回值进行缓存)。
允许的属性有:

下面是一个后绑定函数的示例:

@late()
def tools():
    import os

    # get everything in bin dir
    binpath = os.path.join(this.root, "bin")
    result = os.listdir(binpath)

    # we don't want artists to see the admin tools
    if os.getenv("_USER_ROLE") != "superuser":
        result = set(result) - set(["delete-all", "mod-things"])

    return list(result)

注意:后绑定函数使用到的模块必须在函数内进行导入,而不是package.py文件顶部。
还有一点是,比如函数只是为了返回bin目录的一个路径,最好是将它写作为一个预绑定函数,
这样对性能来说更节省。

但如果有一个环境变量"_USER_ROLE"是在构建时未知的,则适用于写作后绑定函数。
有时候我们会通过预绑定函数来存储一个属性来减少运行时的成本,再到后绑定函数中直接读取这个属性,
比如下面这个示例:

@late()
def tools():
    import os
    result = this._tools

    # we don't want artists to see the admin tools
    if os.getenv("_USER_ROLE") != "superuser":
        result = set(result) - set(["delete-all", "mod-things"])

    return list(result)

@early()
def _tools():
    import os
    return os.listdir("./bin")

注意在上述的代码中,_tools函数使用的一个相对路径,因为预绑定函数在进行构建求值时,该软件包是处于尚未安装的状态,所以诸如this.root之类的属性是不存在的。

in_context 函数

当后绑定函数求值时,将存在一个布尔函数in_context,如果软件包在解析后的环境中,返回True。
比如,仅使用rez API遍历软件包(如rez-search工具),则这些程序不属于解析后环境。

但如果创建一个ResolvedContext对象(如rez-env工具所做的)并遍历其已解析的软件包,则它们属于in_context。下面同样是一个示例:

@late()
def tools():
    result = ["edit"]

    if in_context() and "maya" in request:
        result.append("maya-edit")

    return result

这里检查request对象,查看是否当前环境请求了maya软件。如果是,将maya-edit工具加入列表。

有效对象

如果in_context返回True,下面是一些可用对象:

下面是无论in_context返回值,都可以使用的对象:

示例 - 后绑定函数中的build_requires

name = "maya_thing"

version = "1.0.0"

variants = [
    ["maya-2017"],
    ["maya-2018"]
]

@late()
def build_requires():
    if this.is_package:
        return []
    elif this.index == 0:
        return ["maya_2017_build_utils"]
    else:
        return ["maya_2018_build_utils"]

这里对this.is_package检查,实际上如果运行下面的命令,this的字段是一个包实例,并没有索引:

]$ rez-search maya_thing --type package --format '{build_requires}'

在这种情况下,this.is_package和this.index的返回值都为False,但是仍然可以用else返回一些值。

跨软件包共享

跨包定义函数的属性是可以共享的,但是根据函数是预先绑定还是后绑定,机制上有些不同。

这是为了避免已经安装的软件包依赖于随时可能更改的外部代码。但是依赖于外部代码的构建是没有问题的。

在构建期间共享

函数在package.py文件中构建的功能包括:

你可以使用package_definition_build_python_paths配置通用的共享属性。

在已经安装的程序之间共享

包括:

使用装饰器@include将函数进行共享,该装饰器依赖于package_definition_python_path的设置。

下面是一个共享模块包命令的例子:

# in package.py
@include("utils")
def commands():
    utils.set_common_env_vars(this, env)

requires扩展

通常软件包在构建时,可能比运行时需要更多的依赖项兼容。

例如,一个C++包可以针对任何版本的boost-1进行构建,但是有时需要链接到它针对的特定小版本,比如boost-1.55。你可以使用通配符号在requires属性(或任何相关属性比如build_requires)中这样去描述它:

requires = [
    "boost-1.*"
]

你也可以将requires作为一个预绑定函数来实现刚刚的需求,结合rez包的expand_requires函数:

@early()
def requires():
    from rez.package_py_utils import expand_requires
    return expand_requires(["boost-1.*"])

软件包预处理

你可以在全局或者在package.py文件中定义预处理功能。在构建软件包之前,
可以使用它来验证软件包,甚至修改某些属性。要设置这个功能,参考:
package_preprocess_function配置设置。

看下面这个预处理示例:

def preprocess(package, data):
    from rez.package_py_utils import InvalidPackageError
    import re

    if not re.match("[a-z]+$", package.name):
        raise InvalidPackageError("Invalid name, only lowercase letters allowed")

    if not package.authors:
        from preprocess_utils import get_git_committers
        data["authors"] = get_git_committers()

上面的预处理程序会根据正则来检查软件包名称,并将包的authors属性设置为git_committers的返回值
(当包没有给予这个属性的时候)。如果软件包名称没有通过检查,需要停止构建,
必须在代码中引发一个"InvalidPackageError"的错误。

Tips:要查看package.py的预处理内容,可以在其根目录运行命令:
rez-build --view-pre 将会打印一个标准输出。

在预处理中覆盖配置设置

比如在预处理中,覆盖软件包发布路径设置:

# in package.py
with scope("config") as c:
    c.release_packages_path = "/software/packages/external"

假设一个场景,我们希望将第三方包安装到特定的路径,并且将这些包的属性"external"为True,我们可以在全局预处理函数中这样设置:

def preprocess(package, data):
    if not data.get("external"):
        return

    try:
        _ = data["config"]["release_packages_path"]
        return  # already explicitly specified by package
    except KeyError:
        pass

    data["config"] = data["config"] or {}
    data["config"]["release_packages_path"] = "/software/packages/external"

软件包范例

下面是一个软件包定义文件,演示了几个特性。
这是个python软件包,该软件包不会实际安装python,而是检测现有系统的python安装,
并将其绑定到rez软件包中。(结合上述的知识来阅读它吧)

name = "python"

@early()
def version():
    return this.__version + "-detected"

authors = [
    "Guido van Rossum"
]

description = \
    """
    The Python programming language.
    """

@early()
def variants():
    from rez.package_py_utils import expand_requires
    requires = ["platform-**", "arch-**", "os-**"]
    return [expand_requires(*requires)]

@early()
def tools():
    version_parts = this.__version.split('.')

    return [
        "2to3",
        "pydoc",
        "python",
        "python%s" % (version_parts[0]),
        "python%s.%s" % (version_parts[0], version_parts[1])
    ]

uuid = "recipes.python"

def commands():
    env.PATH.append("{this._bin_path}")

    if building:
        env.CMAKE_MODULE_PATH.append("{root}/cmake")

# --- internals

def _exec_python(attr, src):
    import subprocess

    p = subprocess.Popen(
        ["python", "-c", src],
        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    out, err = p.communicate()

    if p.returncode:
        from rez.exceptions import InvalidPackageError
        raise InvalidPackageError(
            "Error determining package attribute '%s':\n%s" % (attr, err))

    return out.strip()

@early()
def _bin_path():
    return this._exec_python(
        "_bin_path",
        "import sys, os.path; print(os.path.dirname(sys.executable))")

def _version():
    return _exec_python(
        "version",
        "import sys; print(sys.version.split()[0])")

__version = _version()

软件包标准属性

authors(List of string)

authors = ["jchrist", "sclaus"]

软件包的作者,顺序从主要贡献者开始。

build_requires(List of string)

build_requires = [
    "cmake-2.8",
    "doxygen"
]

与require相同,只是这些依赖项只包含在构建期间(通常配合rez-build调用)

cachable(Boolean)

cachable = True

在启用包缓存时,是否可以缓存包。如果没有提供此参数,则由全局配置的"default_cachable"和相关的"default_cachable_*"设置中获取。

commands(Function)

def commands():
    env.PYTHONPATH.append("{root}/python")
    env.PATH.append("{root}/bin")

一个python代码块,告诉rez如何使用这个包更新环境。比如:

config

with scope("config"):
    release_packages_path = "/software/packages/apps"

软件包可以使用它来覆盖rez配置设置。只在某些情况下有效,比如我们希望将包发布到一个与默认目录不同的路径。

description(String)

description = "Library for communicating with the dead."
对软件包的总体描述。不要用它描述关于包的特定版本和细节,只应该提到包的一般特性。

has_plugins(Boolean)

has_plugins = True
标识这个包是有插件的。

hashed_variants(Boolean)

hashed_variants = True
标识软件包根据variants内容的哈希值将variants安装到子目录中。

help(String or List of string)

help = "https://github.com/nerdvegas/rez/wiki"
软件包的帮助url页面,如果是包含空格的字符,则是要运行的命令。
如果是一个列表,代表帮助条目,可以使用SECTION参数指定要查看的条目。

name(String,mandatory)

name = "maya_utils"
包的名称。允许使用字母数字和下划线,名称区分大小写。

plugin_for(String)

plugin_for = "maya"
表明这个包是另一个包的插件。如上例子,这是一个maya插件。

post_commands(Function)

def post_commands():
    env.FOO_PLUGIN_PATH.append("@")

与pre_commands类似,但在最后运行,而不是在开始阶段。

pre_commands(Function)

def pre_commands():
    import os.path
    env.FOO_PLUGIN_PATH = os.path.join(this.root, "plugins")

和commands一样,执行顺序是pre_commands优先执行,然后是commands,最后是post_commands。

pre_test_commands(Function)

def pre_test_commands():
    if test.name == "unit":
        env.IS_UNIT_TEST = 1

和commands类似,但它是在测试中的定义的每个测试之前运行。

relocatable(Boolean)

relocatable = True
确定这个包是否可以复制到另一个包存储库。(例如使用rez-cp命令)
如果没有提供,则由全局配置"default_relocatable"和相关设置"default_relocatable_*"设置确定。

requires(List of string)

requires = [
    "python-2",
    "maya-2016",
    "maya_utils-3.4+<4"
]

这是包所依赖的其它包列表。

tests(Dict)

tests = {
    "unit": "python -m unittest discover -s {root}/python/tests",
    "lint": {
        "command": "pylint mymodule",
        "requires": ["pylint"],
        "run_on": ["default", "pre_release"]
    },
    "maya_CI": {
        "command": "python {root}/ci_tests/maya.py",
        "on_variants": {
            "type": "requires",
            "value": ["maya"]
        },
        "run_on": "explicit"
    }
}

定义的这个字典,代表可以使用rez-test工具在软件包运行测试。

例如在上述带有test属性的包maya_utils上运行linter:

]$ rez-test maya_utils lint

如果测试条目是一个字符串或一个字符串列表,这将会解释为要运行的命令。
字符串扩展任何包属性引用,比如{root}。
如果提供了一个嵌套字典,你可以为每个测试指定额外的字段:

tools(List of string)

tools = [
    "houdini",
    "hescape",
    "hython"
]

软件包提供的工具列表。

uuid(String)

uuid = "489ad32867494baab7e5be3e462473c6"

一个唯一值,可以用于标识相似名称的两个包。(一旦设置就不要更改它)
可以这样去获得一个uuid:

]$ python -c 'import uuid; print(uuid.uuid4().hex)'

variants(List of list of string)

variants = [
    ["maya-2015.3"],
    ["maya-2016.1"],
    ["maya-2016.7"]
]

一个包可以包含的各种变体,可以看作是同一个包的不同版本变体,具有不同的依赖性。

version(String)

version = "1.0.0"
标识这个包的版本。

构建包时的属性

这些属性只在包构建的时候生效,一旦安装完成,这些属性就走了。

build_command(String or False)

build_command = "bash {root}/build.sh {install}"

包的构建命令。如果设置,它将在运行"rez-build"时执行这个构建命令。
如果为False,则不需要任何构建步骤(仍然会安装包)。
{root}字符同样是可用的字符扩展,代表构建路径根目录。
{install}字符为“install”如果安装正在进行,否则为空。
可以在build命令中引用的变量有:

build_system(String)

build_system = "cmake"
指定构建这个包所使用的构建系统,如果未设置则在构建时自动检测。
或者也可以使用 —build-system option 来指定。

pre_build_commands(Function)

def pre_build_commands():
    env.FOO_BUILT_BY_REZ = 1

与commands类似,不同的是它在构建包之前运行。

preprocess(Function)

见本文之前讲到的 “软件包预处理”

private_build_requires(List of string)

private_build_requires = [
    "cmake-2.8",
    "doxygen"
]

与build_requires相同,区别是这些依赖项只有在构建时才包括。

requires_rez_version(String)

requires_rez_version = "2.10"
定义了软件包所需的rez最小版本。

发布包时的属性

当你的包通过rez-release工具发布时,Rez会创建以下包属性:

changelog(String)

changelog = \
    """
    commit 22abe31541ceebced8d4e209e3f6c44d8d0bea1c
    Author: allan johns <nerdvegas at gee mail dot com>
    Date:   Sun May 15 15:39:10 2016 -0700

        first commit
    """

包含上次发布到现在的所有提交更改日志。这个更新日志的语法取决于版本管理工具,这里是基于git的一个例子。

previous_revision(Type varies)

先前发布的软件包的修订信息。(如果有)

previous_version(String)

previous_version = "1.0.1"
先前发布的包的版本(如果有的话)。

release_message(String)

release_message = "Fixed the flickering thingo"
程序包发布信息。可以通过发布工具rez-release —message option设置,
或者在发布时的文本编辑器中输入(后者需要rez配置设置TODO_ADD_THIS)。

revision(Type varies)

revision = \
    {'branch': 'master',
     'commit': '22abe31541ceebced8d4e209e3f6c44d8d0bea1c',
     'fetch_url': 'git@github.com:nerdvegas/dummy.git',
     'push_url': 'git@github.com:nerdvegas/dummy.git',
     'tracking_branch': 'origin/master'}

有关已发布包源代码修订信息。数据类型由所用的版本控制工具确定,这里是git-based的修订信息。

timestamp(Integer)

timestamp = 1463350552
包发布的时间。

vcs(String)

vcs = "git"
发布包的版本控制工具的名字。