onos之详解编译运行

一直对onos的编译运行过程很是迷惑,这次就花了些时间仔细研究了一下。

Bazel基本概念

在探究onos的整体编译结构前,先要了解一下Bazel的几个基本概念:

Workspace

工作区是项目的根目录,每个工作区都有一个WORKSPACE(或WORKSPACE.bazel)文件(可以无内容)。每个工作区相互独立,也就是说工作区A的子目录下有个工作区B,则编译A的时候并不会编译B。

Repositories

所有的代码都由仓库管理,它包含了所有待构建的源文件、数据和构建脚本。当前代码仓库(工作区所在目录)由@标识,外部仓库由@external_repository标识。对于外部仓库,一定要在当前WORKSPACE文件中进行声明(如http_archive、local_repository、new_local_repository等)

Packages

仓库中管理代码的基本单位是包。包由相关文件和它们之间的依赖说明组成的。一个包必须包含BUILD或BUILD.bazel文件。需要注意的是,在一个包中递归包含该目录下的所有文件,但是若某个子目录也含有BUILD文件,则该子目录独立为一个新包。

Targets

包中含有一个或多个目标。大多数目标主要是files或是rules(package group不常见)。

  • files又可细分为两种:Source file(源文件)和Generated file(生成文件,编译时根据特殊的规则得到)
  • rules定义了输入和输出间的关系(包括必要的从输入到输出的步骤)。输出永远是生成文件,而输入可能是源文件或生成文件。注意:一个rule的输出可能是另一个rule的输入。另外,规则生成的文件永远与规则属于同一个包。

Labels

每个目标只能属于一个包,目标的名字就是标签,并且每个标签只能表示一个包。比如:

@repo_name//@package_name:target_name
比如@myrepo//my/app/main:app_binary

也就是myrepo工作区下my/app/main包中的app_binary目标,通常情况下可省略开头的@myrepo。关于标签的一些小语法如下:

//my/app  ===  //my/app:app

# 在my/app中的BUILD文件中,对于该文件中的其他目标
//my/app  ===  :app  === app
一般来讲文件会省略冒号,rules则不会。  

Rules

规则规定了输入输出之间的关系以及构建输出的步骤。Rules可以生成编译后的可执行文件、库、测试执行文件或其他支持的输出。比如:

cc_binary(
    name = "my_app",
    src = ["my_app.cc"],
    deps = [
        "//absl/base",
        "//absl/string",
    ],
)

可以看到这个C++库my_app的源文件是my_app.cc,其依赖于//absl/base:base和//absl/string:string。当然有些规则是依赖于具体的语言的。

BUILD files

BUILD文件采用Starlark语言。Build文件中各个变量的声明顺序是随意的,也就是说,变量可以在使用后声明。并且BUILD文件不能包含函数定义、for语句和if语句,函数定义只能出现在.bzl文件中。另外,*args和**kwargs不能出现在BUILD文件中,需要显示声明参数。

bazel其他细节

其他细节可参考https://blog.gmem.cc/bazel-study-note或官网https://docs.bazel.build/versions/master/build-ref.html

onos之编译运行

源码版本为onos2.3.0

WORKSAPCE文件

WORKSPACE文件是用来给当前工作区引入外部依赖的。可以看到onos目录下的WORKSPACE文件,也确实导入外部仓库,如bazel_skylib、build_bazel_rules_nodejs等,当然也调用了一些函数。需要注意的是:

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

load能够加载扩展文件的某些符号到当前的工作环境,注意此时第一个参数:后面不是目标名,而是.bzl文件,后面的参数才是要加载的符号。符号包括规则、函数或常量。

其中的bazel_tools是内置仓库,相应目录为/home/user_name/.cache/bazel/_bazel_user_name/install/xxxx…x/_embedded_binaries/embedded_tools。
另外就是调用一些函数进行环境准备。

BUILD文件

现在打开onos目录下的BUILD文件,探究它是怎么编译的。

load("//tools/build/bazel:variables.bzl", "ONOS_VERSION")

首先加载ONOS_VERSION,为2.3.0。

load(
    "//tools/build/bazel:modules.bzl",
    "CORE",
    "FEATURES",
    "apps",
    "extensions",
    "profiles",
)

从tools/build/bazel包下的modules.bzl中加载CORE、FEATURES等。需要注意三个函数的作用:

  1. apps:负责在很多XXX_MAP中筛选出符合条件的key。
  2. extensions:从PROTOCOL_MAP和PROVIDER_MAP中筛选出符合条件的key。
  3. profiles:根据给定的参数批量生成本地config_setting。

接下来通过profiles生成三个config_setting。

profiles([
    "minimal",
    "seba",
    "stratum",
])

这三个配置的作用就是控制打包的范围。

filegroup(
    name = "onos",
    srcs = CORE + [
        "//tools/build/conf:onos-build-conf",
        ":onos-package-admin",
        ":onos-package-test",
        ":onos-package",
    ] + select({
        ":minimal_profile": extensions("minimal") + apps("minimal"),
        ":seba_profile": extensions("seba") + apps("seba"),
        ":stratum_profile": extensions("stratum") + apps("stratum"),
        "//conditions:default": extensions() + apps(),
    }),
    visibility = ["//visibility:public"],
)

filegroup是为一组目标指定一个名字,方便bazel监控文件变化。其中select()根据配置选择不同的目标:

bazel build onos-package --define profile=minimal #以最小形式打包

默认情况下完整打包。之后是几个打包规则,如:

# 这里仅举例onos-karaf打包规则
genrule(
    name = "onos-karaf",
    srcs = [
        KARAF,
        BRANDING,
    ] + glob([
        "tools/package/bin/*",
        "tools/package/etc/*",
        "tools/package/init/*",
        "tools/package/runtime/bin/*",
    ]),
    outs = ["karaf.zip"],
    cmd = "$(location tools/package/onos-prep-karaf) $(location karaf.zip) $(location %s) %s $(location %s) '' tools/package" %
      (KARAF, ONOS_VERSION, BRANDING),
    tools = ["tools/package/onos-prep-karaf"],
)

genrule是通过用户自定义的bash命令生成一个或多个文件。

分别打包karaf.zip、onos.tar.gz、onos-admin.tar.gz和onos-test.tar.gz。

alias(
    name = "onos-local",
    actual = select({
        ":run_with_absolute_javabase":     ":onos-local_absolute-javabase",
        "//conditions:default": ":onos-local_current-jdk",
    }),
)

之后则是onos-local目标,这里会根据启动参数选择jdk进行运行,默认采用bazel自带的JDK(openJDK 11)。

genrule(
    name = "onos-local_current-jdk",
    srcs = [
        ":onos-local-base",
        "//tools/build/jdk:current_jdk_tar",
    ],
    outs = ["onos-runner_current-jdk"],
    cmd = "sed \"s#JDK_TAR=#JDK_TAR=$(location //tools/build/jdk:current_jdk_tar)#\" " +
      "$(location :onos-local-base) > $(location onos-runner_current-jdk); ",
    executable = True,
    output_to_bindir = True,
    visibility = ["//visibility:private"],
)

所以使用bazel run onos-local运行onos的话,其实运行的是onos-runner_current-jdk,executable表示输出可否运行,output_to_bindir则是将输出文件放在bin目录下。

onos-runner_current-jdk文件

该文件由很多bash命令组成,主要过程是:

  1. 杀死正在运行的实例
  2. 使用ABSOLUTE_JAVABASE作为JAVA_HOME或是将JDK_TAR解压使用。
  3. 如果之前的onos不存在或者和现在的md5摘要不同,则重新解压,并配置一些参数和准备工作。
  4. 进入ONOS_DIR目录,并执行./bin/onos-service

注意,解压后的onos目录位于/tmp/onos-2.3.0,其目录结构为:

onos-service文件

该文件位于bin目录下,主要是解析参数,并且在需要预激活的app目录下生成active文件,最后启动karaf。

Karaf运行


可执行文件karaf位于bin目录下,但是需要注意的是etc/org.apache.karaf.features.cfg,该文件内容如下:

其中标明了Karaf启动时加载的Feature仓库和Feature,通过这些Feature的加载正式启动了onos的功能。值得一说的是,我一开始以为mvn:org.onosproject/onos-features/2.3.0/xml/features是在~/.m2目录下,但是一直找不到,最后发现其实是在system目录下。