Import解析
本文面向已经读过 rc语法 的读者。本文只回答一个问题:init 读到 import path 后,路径何时展开、文件何时真正解析、目录如何排序、嵌套 import 如何排列,以及失败是否阻断后续配置。它不重复 tokenizer,也不展开 Action 或 Service 的 option 细节。
Android 17 的关键事实是:ImportParser::ParseSection() 只记录已经展开的路径,当前文件结束时 EndFile() 才调用父 Parser::ParseConfig()。因此 import 不是 shell 的“读到即 source”,也没有旧稿所称的全局去重或循环检测集合。
1. 所有权
1.1 两个列表
ImportParser 临时拥有 imports_,每项是路径和源行号;父 Parser 拥有 section parser 和全局 Action/Service parser。路径进入 imports_ 后,当前文件的其余 section 仍会继续解析。
相关源码:
system/core/init/import_parser.cppsystem/core/init/import_parser.hsystem/core/init/parser.cpp
class ImportParser : public SectionParser {
private:
Parser* parser_;
std::string filename_;
std::vector<std::pair<std::string, int>> imports_;
};1.2 生效时机
| 时刻 | 发生的事 | 尚未发生 |
|---|---|---|
ParseSection | 参数校验、属性展开、路径入列表 | 读取目标文件 |
| 当前文件继续 | 其他 section 提交 | import 目标内容解析 |
EndFile | 依次调用 ParseConfig | 目标文件的后续嵌套 import |
2. 路径解析
2.1 参数约束
import 是无 body 的独立 section,只能有一个参数。下一行若仍被 ImportParser::ParseLineSection() 收到,会返回 Unexpected line found after import statement。
源码文件:system/core/init/import_parser.cpp
Result<void> ImportParser::ParseSection(
std::vector<std::string>&& args,
const std::string& filename, int line) {
if (args.size() != 2) {
return Error() << "single argument needed for import\n";
}
// 后续展开并缓存
}
Result<void> ImportParser::ParseLineSection(
std::vector<std::string>&&, int) {
return Error() << "Unexpected line found after import statement";
}2.2 属性展开
路径在解析 import 行时调用 ExpandProps()。展开结果被缓存,而不是把 ${ro.hardware} 留给后续文件系统。属性不存在时,ExpandProps 的具体结果和错误由 property 工具实现决定;Importer 只接受成功的 Result。
源码文件:system/core/init/import_parser.cpp
auto conf_file = ExpandProps(args[1]);
if (!conf_file.ok()) {
return Error() << "Could not expand import: " << conf_file.error();
}
LOG(INFO) << "Added '" << *conf_file << "' to import list";
if (filename_.empty()) filename_ = filename;
imports_.emplace_back(std::move(*conf_file), line);2.3 源行保存
源码同时保存 filename 和 line,供后续错误定位使用;EndFile() 当前实现只按路径解析,但保存源位置说明设计上需要把导入错误关联回原始 import 行。不要把 line number 当作排序键。
3. 文件加载
3.1 ParseConfig
父 Parser 先判断路径类型:目录走 ParseConfigDir(),其他路径走 ParseConfigFile()。这一步发生在 EndFile(),所以 import 路径错误与当前文件语法错误属于两个不同阶段。
源码文件:system/core/init/parser.cpp
bool Parser::ParseConfig(const std::string& path) {
if (is_dir(path.c_str())) {
return ParseConfigDir(path);
}
auto result = ParseConfigFile(path);
if (!result.ok()) {
LOG(ERROR) << "could not parse file '" << path << "': " << result.error();
return false;
}
return true;
}3.2 单文件
ParseConfigFile() 读取完整文件,调用 ParseData(),EOF 时触发当前 Parser 注册的所有 EndFile()。因此一个被导入文件自己的 imports,会在该文件解析结束时再展开。
3.3 目录排序
目录只收集 DT_REG 普通文件,并对完整路径排序。目录中的子目录和非普通文件不因 import 自动递归;若需要子目录,必须显式再 import。
源码文件:system/core/init/parser.cpp
std::vector<std::string> files;
while ((current_file = readdir(config_dir.get()))) {
if (current_file->d_type == DT_REG) {
files.emplace_back(StringPrintf(
"%s/%s", path.c_str(), current_file->d_name));
}
}
std::sort(files.begin(), files.end());
for (const auto& file : files) {
if (auto result = ParseConfigFile(file); !result.ok()) {
LOG(ERROR) << "could not import file '" << file << "': " << result.error();
}
}4. 提交顺序
4.1 EndFile
EndFile() 先 move 出 imports_,清空成员,再按记录顺序调用 ParseConfig()。move 的目的不是去重,而是允许递归解析时使用同一个 ImportParser 继续收集新文件的 import。
源码文件:system/core/init/import_parser.cpp
void ImportParser::EndFile() {
auto current_imports = std::move(imports_);
imports_.clear();
for (const auto& [import, line_num] : current_imports) {
parser_->ParseConfig(import);
}
}4.2 嵌套关系
主文件有 import first、import dir、import last 时,主文件 EOF 先解析 first;随后解析目录中的排序文件;目录文件结束时解析它自己的 import;最后才解析 last。这是“当前文件的 import 列表按顺序展开,递归发生在被调用 ParseConfig 内部”,而不是简单的文本行位置。
4.3 顺序消费者
被导入文件里的 Action 和 Service 进入与主文件相同的 ActionManager/ServiceList;后续同名 Action 的执行顺序和 Service duplicate/override 结果都会看到这一解析顺序。因此目录排序不是纯日志行为,而是会影响配置对象的相对位置。
5. 启动装载
5.1 LoadBootScripts
Android 17 没有 ro.boot.init_rc 时,LoadBootScripts() 明确解析主 init、system、system_ext、vendor、odm、product 路径;属性非空时改为只解析指定 bootscript。
源码文件:system/core/init/init.cpp
相关函数/类型:LoadBootScripts
std::string bootscript = GetProperty("ro.boot.init_rc", "");
if (bootscript.empty()) {
parser.ParseConfig("/system/etc/init/hw/init.rc");
if (!parser.ParseConfig("/system/etc/init")) {
late_import_paths.emplace_back("/system/etc/init");
}
parser.ParseConfig("/system_ext/etc/init");
if (!parser.ParseConfig("/vendor/etc/init")) {
late_import_paths.emplace_back("/vendor/etc/init");
}
parser.ParseConfig("/odm/etc/init");
parser.ParseConfig("/product/etc/init");
} else {
parser.ParseConfig(bootscript);
}5.2 可选目录
部分目录失败会被记录到 late_import_paths,不等于所有已经读取的 rc 回滚。该列表由后续 init 流程使用;文章只说明 LoadBootScripts() 的返回处理,不把缺失目录解释成必然启动失败。
5.3 APEX边界
APEX 配置使用专门的 parser 创建路径和 subcontext 选择,不能用通用 /system/etc/init 目录顺序推导 APEX 的全部生命周期。ImportParser 的通用语义仍是文件结束时展开,但 APEX 何时调用 parser 属于 APEX 初始化入口。
6. 错误路径
6.1 展开失败
ExpandProps() 返回错误时,import 不会进入 imports_;当前文件的其他已提交 Action/Service 不回滚。错误位置由 Parser 的文件和行号日志补充。
6.2 读取失败
单文件读取错误返回失败;目录打不开返回 false。目录中的单个文件解析失败只记录错误并继续下一个文件,因而目录不是事务。
6.3 重复与循环
Android 17 的 ImportParser 没有全局 parsed_files_ 集合,也没有循环检测。重复 import 可能重复解析同一文件;循环 import 可能持续递归。该结论来自源码缺少去重状态,不能改写成系统会自动阻止循环。
7. 导入测试
7.1 顺序测试
EventTriggerOrderMultipleFiles 构造六个临时文件和目录,给每个 on boot 命令编号,断言顺序为 1,2,3,4,5,6。它证明主文件 import、目录排序和嵌套 import 的组合顺序,未证明所有设备分区内容相同。
7.2 最小实验
可构造主文件:import first、import dir、import last,让每个子文件写入一个不同编号的 builtin,再触发 boot。同时加入不存在路径和错误 section,分别观察读取错误、解析错误和后续文件是否继续处理。
7.3 顺序实验
对一个真实 import,写出:
import token
-> ExpandProps
-> imports_ 缓存
-> 当前文件 EndFile
-> ParseConfig(file/dir)
-> Action / Service 提交顺序若服务没有出现在 ServiceList,先查它所在文件是否真的被 ParseConfig() 读取、段结束是否报错,再查 ServiceParser;不要只检查 import 行是否存在。
7.4 边界
本文不证明 APEX 专用加载时机、recovery 脚本集合、vendor subcontext 的完整安全策略,也不覆盖 Action 执行和 Service 启动。结论限定于 Android 17 Parser/ImportParser 的真实实现。
