Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions devel/4453.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# 4453 带路径包名的 use-package 找不到 $TEXMACS_HOME_PATH 下的用户包

## 背景

用户包放在 `$TEXMACS_HOME_PATH/packages/TST-core/` 下,自己用
`<use-package|TST-core/unicode/unicode-figures>` 级联加载子包。
GNU TeXmacs 2.1.5 与 2026_2.6 正常,2026_3.2 起子包不再被加载,
宏原样显示成 `⟨big-unicode-table|...⟩`。把三个包一起加进文档的
style/package 列表则正常,只有包体里的级联失效。

## 定位

1200(`17e6bab45`)给 `exec_use_package` 加了带路径包名的直查快路径,
包名含 `/` 时只查 `$TEXMACS_PATH/packages` 和
`$TEXMACS_PATH/plugins/<首段>/packages`,不查 `$TEXMACS_HOME_PATH`。
1200 之前是拿 `$TEXMACS_STYLE_PATH` 去 resolve,那个变量由
`search_sub_dirs (style_root | package_root)` 生成,含
`$TEXMACS_HOME_PATH/packages` 及其全部子目录,所以用户包能找到。

手动加进文档包列表之所以正常:文档自己的列表先过 `preprocess_style`
(`new_style.cpp:118`)解析成绝对路径再交给 `exec`,而
`url::operator*` 遇到 rooted 右操作数直接返回右操作数
(`lolly/System/Classes/url.cpp:645`),于是绝对路径仍能命中。
包体里的相对名没有这一步兜底。

## 改动

`resolve_dotted_package` 改为接收 `base_file_name`,在
`$TEXMACS_PACKAGE_ROOT`、`$TEXMACS_STYLE_ROOT`、文档所在目录
(本地文档沿祖先目录上溯)里依次直查。两个根变量条目数是
「2 + 插件个数」量级,不是递归展开全部子目录的 `$TEXMACS_STYLE_PATH`,
1200 的性能优化基本保留;`plugin_path` 的 base 是
`$TEXMACS_HOME_PATH:$TEXMACS_PATH`,已覆盖原来的插件规则。

`resolve_pack_in` 增加 or-url 分支逐根遍历,让「`.stem` 优先 `.ts`」
只在同一个根内生效,靠前的根(用户包)才能覆盖靠后的根(内置包)。

定位一个内置包的 stat 次数:1200 之前 8 次,1200 为 1 次,现在 3 次。

**涉及文件**:`src/Typeset/Env/env_exec.cpp`

## 回归测试

`tests/Typeset/Env/use_package_resolve_test.cpp`,在临时
`$TEXMACS_HOME_PATH` 下铺出 issue 里的两层包结构,只 use-package
入口包,断言子包的宏确实进了环境。另覆盖文档相对定位、
home 插件目录、style 根、两个根的先后顺序、裸包名不回归、
找不到的包不崩。改动前 4 passed / 5 failed,改动后 9 passed。

**涉及文件**:`tests/Typeset/Env/use_package_resolve_test.cpp`
44 changes: 29 additions & 15 deletions src/Typeset/Env/env_exec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1208,32 +1208,46 @@ filter_style (tree t) {
}

/**
* @brief 在指定根目录下按相对路径定位包文件,优先 .stem 后 .ts
* @param root 包根目录
* @param pi 带路径的包名(如 a/b/c),可显式带 .ts/.stem 后缀
* @return 解析到的文件 url,未找到返回 url_none
* @brief 在根目录下定位包文件,优先 .stem 后 .ts
* @param root 包根目录,可以是 or-url,按先后顺序逐个尝试
* @param pi 带路径的包名,可显式带 .ts/.stem 后缀
*/
static url
resolve_pack_in (url root, string pi) {
// 逐根尝试而不是把 or-url 整个交给 resolve,让「.stem 优先 .ts」只在同一个
// 根内生效,靠前的根里的 .ts 才能覆盖靠后的根里的同名 .stem
if (is_or (root)) {
url name= resolve_pack_in (root[1], pi);
if (!is_none (name)) return name;
return resolve_pack_in (root[2], pi);
}
if (ends (pi, ".ts") || ends (pi, ".stem")) return resolve (root * pi);
url name= resolve (root * (pi * string (".stem")));
if (is_none (name)) name= resolve (root * (pi * string (".ts")));
return name;
}

/**
* @brief 带路径包名(含 /)的直查解析,避免 resolve 对 $TEXMACS_STYLE_PATH
* 全部子目录逐一扫描
* @note 依次尝试 $TEXMACS_PATH/packages 与
* $TEXMACS_PATH/plugins/<首段>/packages
* @brief 带路径包名的搜索根:package 根、style 根、文档所在目录
* @note 两个根变量由 init_env_vars 设置,条目数是「2 + 插件个数」量级,
* 不是 $TEXMACS_STYLE_PATH 那种递归展开出全部子目录的路径
*/
static url
package_search_roots (url base_file_name) {
url roots= url ("$TEXMACS_PACKAGE_ROOT") | url ("$TEXMACS_STYLE_ROOT");
if (is_none (base_file_name)) return roots;
// 本地文档沿祖先目录上溯,与不带路径包名的分支一致
if (is_rooted (base_file_name, "default"))
return roots | ::expand (head (base_file_name) * url_ancestor ());
return roots | head (base_file_name);
}

/**
* @brief 带路径包名(含 /)的直查解析,避免逐一扫描 $TEXMACS_STYLE_PATH
*/
static url
resolve_dotted_package (string pi) {
url name= resolve_pack_in (url ("$TEXMACS_PATH/packages"), pi);
if (!is_none (name)) return name;
int pos= search_forwards ("/", 0, pi);
return resolve_pack_in (
url ("$TEXMACS_PATH/plugins") * pi (0, pos) * "packages", pi);
resolve_dotted_package (string pi, url base_file_name) {
return resolve_pack_in (package_search_roots (base_file_name), pi);
}

tree
Expand All @@ -1245,7 +1259,7 @@ edit_env_rep::exec_use_package (tree t) {
string pi = as_string (t[i]);
string task= "use-package " * pi;
bench_start (task);
if (occurs ("/", pi)) name= resolve_dotted_package (pi);
if (occurs ("/", pi)) name= resolve_dotted_package (pi, base_file_name);
else {
if (is_rooted (base_file_name, "default"))
styp= styp | ::expand (head (base_file_name) * url_ancestor ());
Expand Down
221 changes: 221 additions & 0 deletions tests/Typeset/Env/use_package_resolve_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/******************************************************************************
* MODULE : use_package_resolve_test.cpp
* DESCRIPTION: 带路径包名 <use-package|a/b/c> 的解析回归测试
* COPYRIGHT : (C) 2026 Darcy Shen
*******************************************************************************
* This software falls under the GNU general public license version 3 or later.
* It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE
* in the root directory or <http://www.gnu.org/licenses/gpl-3.0.html>.
******************************************************************************/

#include "Metafont/load_tex.hpp"
#include "base.hpp"
#include "data_cache.hpp"
#include "env.hpp"
#include "file.hpp"
#include "sys_utils.hpp"
#include "tm_sys_utils.hpp"
#include <QtTest/QtTest>
#include <moebius/drd/drd_std.hpp>
#include <moebius/tree_label.hpp>

using namespace moebius; // USE_PACKAGE
using moebius::drd::init_std_drd;
using moebius::drd::std_drd;

// edit_env 按引用持有 drd 和各 hashmap,必须与 env 同生命周期
struct test_env {
drd_info drd;
hashmap<string, tree> lref, gref, laux, gaux, latt, gatt;
edit_env env;

test_env (url base_file_name)
: drd ("test", std_drd),
env (drd, base_file_name, lref, gref, laux, gaux, latt, gatt) {}
};

static url the_home= url_none ();
static url the_doc = url_none ();

static void
write_pack (url file, string body) {
make_dir (head (file));
string doc= string ("<TeXmacs|2.1.5>\n\n<style|source>\n\n<\\body>\n ") *
body * string ("\n</body>\n");
QVERIFY (!save_string (file, doc, false));
}

static void
set_roots (url package_root, url style_root) {
set_env ("TEXMACS_PACKAGE_ROOT", as_string (package_root));
set_env ("TEXMACS_STYLE_ROOT", as_string (style_root));
}

class TestUsePackageResolve : public QObject {
Q_OBJECT

private slots:
void initTestCase ();
void init ();
void cleanupTestCase ();

void nested_user_package ();
void document_relative_package ();
void home_plugin_package ();
void style_root_package ();
void package_root_wins_over_style_root ();
void bare_package_name_still_works ();
void missing_package_is_silent ();
};

void
TestUsePackageResolve::initTestCase () {
// edit_env 的构造会走到 update_font,字体栈没起来会抛异常,
// 所以先初始化 TeX/字体环境,之后再改写 TEXMACS_HOME_PATH
init_lolly ();
init_texmacs_home_path ();
cache_initialize ();
init_tex ();
init_std_drd ();

the_home= url_temp ();
the_doc = url_temp ();

// issue #4453 复现用的用户包:入口包在 TST-core/,两个子包在
// TST-core/unicode/
url packages= the_home * "packages";
write_pack (packages * "TST-core" * "unicode" * "unicode-figures.ts",
"<assign|unicode-figures-marker|<macro|figures>>");
write_pack (packages * "TST-core" * "unicode" * "unicode-environments.ts",
"<assign|unicode-environments-marker|<macro|environments>>");
write_pack (packages * "TST-core" * "unicode-core.ts",
"<use-package|TST-core/unicode/unicode-figures>\n"
" <use-package|TST-core/unicode/unicode-environments>\n"
" <assign|unicode-core-marker|<macro|core>>");

// 不带路径的包名,走未改动的 $TEXMACS_STYLE_PATH 分支
write_pack (packages * "solo.ts", "<assign|solo-marker|<macro|solo>>");

// package 根与 style 根同名同路径,用于验证根的先后顺序
write_pack (packages * "over" * "pack.ts",
"<assign|over-from-packages|<macro|packages>>");
write_pack (the_home * "styles" * "over" * "pack.ts",
"<assign|over-from-styles|<macro|styles>>");
write_pack (the_home * "styles" * "TST-style" * "style-pack.ts",
"<assign|style-pack-marker|<macro|style>>");

// $TEXMACS_HOME_PATH 下的插件包
write_pack (the_home * "plugins" * "tstplug" * "packages" * "tstplug" *
"deep" * "plug-pack.ts",
"<assign|plug-pack-marker|<macro|plug>>");

// 文档旁边的包,文档本身放在下一层,同时覆盖祖先目录上溯
write_pack (the_doc * "relative" * "rel-pack.ts",
"<assign|rel-pack-marker|<macro|rel>>");

set_env ("TEXMACS_HOME_PATH", as_string (the_home));
}

void
TestUsePackageResolve::init () {
// 每个用例自己声明搜索根,避免相互影响
set_env ("TEXMACS_PACKAGE_ROOT", "");
set_env ("TEXMACS_STYLE_ROOT", "");
set_env ("TEXMACS_STYLE_PATH", "");
}

void
TestUsePackageResolve::cleanupTestCase () {
if (!is_none (the_home)) rmdir (the_home);
if (!is_none (the_doc)) rmdir (the_doc);
}

// issue #4453:只加 unicode-core,两个子包必须级联加载
void
TestUsePackageResolve::nested_user_package () {
set_roots (url ("$TEXMACS_HOME_PATH/packages"),
url ("$TEXMACS_HOME_PATH/styles"));

test_env te (url ("$PWD/none"));
te.env->exec (tree (USE_PACKAGE, string ("TST-core/unicode-core")));

QVERIFY (te.env->provides ("unicode-core-marker"));
QVERIFY (te.env->provides ("unicode-figures-marker"));
QVERIFY (te.env->provides ("unicode-environments-marker"));
}

// 文档在 <doc>/sub/,包在 <doc>/relative/,靠祖先目录上溯找到
void
TestUsePackageResolve::document_relative_package () {
set_roots (url ("$TEXMACS_HOME_PATH/packages"),
url ("$TEXMACS_HOME_PATH/styles"));

test_env te (the_doc * "sub" * "doc.tm");
te.env->exec (tree (USE_PACKAGE, string ("relative/rel-pack")));

QVERIFY (te.env->provides ("rel-pack-marker"));
}

// $TEXMACS_HOME_PATH/plugins/<插件>/packages 下的包
void
TestUsePackageResolve::home_plugin_package () {
set_roots (url ("$TEXMACS_HOME_PATH/packages") |
url ("$TEXMACS_HOME_PATH/plugins/tstplug/packages"),
url ("$TEXMACS_HOME_PATH/styles"));

test_env te (url ("$PWD/none"));
te.env->exec (tree (USE_PACKAGE, string ("tstplug/deep/plug-pack")));

QVERIFY (te.env->provides ("plug-pack-marker"));
}

// 带路径包名也要能落到 style 根上
void
TestUsePackageResolve::style_root_package () {
set_roots (url ("$TEXMACS_HOME_PATH/packages"),
url ("$TEXMACS_HOME_PATH/styles"));

test_env te (url ("$PWD/none"));
te.env->exec (tree (USE_PACKAGE, string ("TST-style/style-pack")));

QVERIFY (te.env->provides ("style-pack-marker"));
}

// 同名包同时在两个根里时,靠前的根胜出
void
TestUsePackageResolve::package_root_wins_over_style_root () {
set_roots (url ("$TEXMACS_HOME_PATH/packages"),
url ("$TEXMACS_HOME_PATH/styles"));

test_env te (url ("$PWD/none"));
te.env->exec (tree (USE_PACKAGE, string ("over/pack")));

QVERIFY (te.env->provides ("over-from-packages"));
QVERIFY (!te.env->provides ("over-from-styles"));
}

// 不含 / 的包名仍走 $TEXMACS_STYLE_PATH,防回归
void
TestUsePackageResolve::bare_package_name_still_works () {
set_env ("TEXMACS_STYLE_PATH", as_string (the_home * "packages"));

test_env te (url ("$PWD/none"));
te.env->exec (tree (USE_PACKAGE, string ("solo")));

QVERIFY (te.env->provides ("solo-marker"));
}

// 找不到的包不崩、不定义任何东西
void
TestUsePackageResolve::missing_package_is_silent () {
set_roots (url ("$TEXMACS_HOME_PATH/packages"),
url ("$TEXMACS_HOME_PATH/styles"));

test_env te (url ("$PWD/none"));
te.env->exec (tree (USE_PACKAGE, string ("no/such/package")));

QVERIFY (!te.env->provides ("unicode-figures-marker"));
}

QTEST_MAIN (TestUsePackageResolve)
#include "use_package_resolve_test.moc"
Loading